From 28747e1a921dc5333c07c4d6179ffc49a0e7b48e Mon Sep 17 00:00:00 2001 From: elpresidank Date: Sun, 5 Apr 2026 23:41:39 -0500 Subject: [PATCH] fix: NATS pipeline bugs, add integration tests and service runners Fix three critical bugs preventing the NATS message pipeline from working: - FlowProcessor now subscribes to config-push topic (was missing entirely), using DeliverPolicy.All to replay config on service restart - NATS streams use wildcard subjects (tg.flow.>) instead of per-topic narrow filters that caused 503 errors on publish - Subscriber dispatch loop has exponential backoff on errors to prevent tight error loops Add service runner scripts (gateway, config, LLM) and a 7-test integration suite that verifies config CRUD, WebSocket round-trip, and full LLM text-completion through the NATS pipeline. Fix Docker Compose infra: pin Tempo to v2.6.1, remove deprecated Loki config fields, add user:0 for volume permissions, remap conflicting ports (FalkorDB 6380, OTLP 4327/4328). Co-Authored-By: Claude Opus 4.6 (1M context) --- ts/deploy/docker-compose.yml | 12 +- ts/deploy/loki/loki-config.yml | 11 - ts/deploy/tempo/tempo-config.yml | 27 +- ts/package.json | 8 +- ts/packages/base/src/backend/nats.ts | 54 +-- ts/packages/base/src/messaging/subscriber.ts | 13 +- .../base/src/processor/async-processor.ts | 2 +- .../base/src/processor/flow-processor.ts | 76 +++- ts/pnpm-lock.yaml | 357 ++++++++++++++++-- ts/scripts/reset-nats.ts | 19 + ts/scripts/run-config.ts | 15 + ts/scripts/run-gateway.ts | 16 + ts/scripts/run-llm-claude.ts | 15 + ts/scripts/run-llm-openai.ts | 16 + ts/scripts/test-pipeline.ts | 292 ++++++++++++++ 15 files changed, 826 insertions(+), 107 deletions(-) create mode 100644 ts/scripts/reset-nats.ts create mode 100644 ts/scripts/run-config.ts create mode 100644 ts/scripts/run-gateway.ts create mode 100644 ts/scripts/run-llm-claude.ts create mode 100644 ts/scripts/run-llm-openai.ts create mode 100644 ts/scripts/test-pipeline.ts diff --git a/ts/deploy/docker-compose.yml b/ts/deploy/docker-compose.yml index 460be51e..d01cca7b 100644 --- a/ts/deploy/docker-compose.yml +++ b/ts/deploy/docker-compose.yml @@ -42,7 +42,7 @@ services: falkordb: image: falkordb/falkordb:latest ports: - - "6379:6379" + - "6380:6379" volumes: - falkordb-data:/data networks: @@ -111,6 +111,7 @@ services: loki: image: grafana/loki:3.0.0 + user: "0" ports: - "3100:3100" volumes: @@ -128,12 +129,13 @@ services: restart: unless-stopped tempo: - image: grafana/tempo:latest + image: grafana/tempo:2.6.1 + user: "0" ports: - "3200:3200" # Tempo API volumes: - ./tempo/tempo-config.yml:/etc/tempo/config.yml:ro - - tempo-data:/tmp/tempo + - tempo-data:/var/tempo command: ["-config.file=/etc/tempo/config.yml"] networks: - trustgraph @@ -148,8 +150,8 @@ services: otel-collector: image: otel/opentelemetry-collector-contrib:latest ports: - - "4317:4317" # OTLP gRPC (apps send traces/metrics here) - - "4318:4318" # OTLP HTTP + - "4327:4317" # OTLP gRPC (apps send traces/metrics here) + - "4328:4318" # OTLP HTTP - "8889:8889" # Prometheus exporter (scraped by Prometheus) volumes: - ./otel-collector/config.yml:/etc/otelcol-contrib/config.yaml:ro diff --git a/ts/deploy/loki/loki-config.yml b/ts/deploy/loki/loki-config.yml index a61f1f0f..4e92a717 100644 --- a/ts/deploy/loki/loki-config.yml +++ b/ts/deploy/loki/loki-config.yml @@ -24,9 +24,6 @@ query_range: enabled: true max_size_mb: 100 -limits_config: - metric_aggregation_enabled: true - schema_config: configs: - from: 2024-01-01 @@ -37,16 +34,8 @@ schema_config: prefix: index_ period: 24h -pattern_ingester: - enabled: true - metric_aggregation: - loki_address: localhost:3100 - ruler: alertmanager_url: http://localhost:9093 frontend: encoding: protobuf - -analytics: - reporting_enabled: false diff --git a/ts/deploy/tempo/tempo-config.yml b/ts/deploy/tempo/tempo-config.yml index b43e4bc1..619667fd 100644 --- a/ts/deploy/tempo/tempo-config.yml +++ b/ts/deploy/tempo/tempo-config.yml @@ -17,33 +17,10 @@ compactor: compaction: block_retention: 48h -metrics_generator: - registry: - external_labels: - source: tempo - cluster: trustgraph-dev - storage: - path: /tmp/tempo/generator/wal - remote_write: - - url: http://prometheus:9090/api/v1/write - send_exemplars: true - storage: trace: backend: local wal: - path: /tmp/tempo/wal + path: /var/tempo/wal local: - path: /tmp/tempo/blocks - -overrides: - defaults: - metrics_generator: - processors: - - service-graphs - - span-metrics - -search_enabled: true - -analytics: - reporting_enabled: false + path: /var/tempo/blocks diff --git a/ts/package.json b/ts/package.json index 57dbb915..99c02db2 100644 --- a/ts/package.json +++ b/ts/package.json @@ -6,9 +6,15 @@ "dev": "turbo dev", "lint": "turbo lint", "test": "turbo test", - "clean": "turbo clean" + "clean": "turbo clean", + "gateway": "tsx scripts/run-gateway.ts", + "config-svc": "tsx scripts/run-config.ts", + "llm:claude": "tsx scripts/run-llm-claude.ts", + "llm:openai": "tsx scripts/run-llm-openai.ts", + "test:pipeline": "tsx scripts/test-pipeline.ts" }, "devDependencies": { + "tsx": "^4.21.0", "turbo": "^2.5.0", "typescript": "^5.8.0" }, diff --git a/ts/packages/base/src/backend/nats.ts b/ts/packages/base/src/backend/nats.ts index c268f1a5..ff621aaf 100644 --- a/ts/packages/base/src/backend/nats.ts +++ b/ts/packages/base/src/backend/nats.ts @@ -98,38 +98,28 @@ class NatsConsumer implements BackendConsumer { private readonly subject: string, private readonly subscription: string, private readonly initialPosition: "latest" | "earliest", + private readonly streamName: string, ) {} async init(): Promise { - // Ensure stream exists - const streamName = this.streamNameFromSubject(this.subject); - try { - await this.jsm.streams.info(streamName); - } catch { - await this.jsm.streams.add({ - name: streamName, - subjects: [this.subject], - }); - } - + // Stream is already ensured by NatsBackend.ensureStream(). // Create or bind to durable consumer. - // Try to get an existing durable consumer first; if it doesn't exist, create it. try { - this.consumer = await this.js.consumers.get(streamName, this.subscription); + this.consumer = await this.js.consumers.get(this.streamName, this.subscription); } catch { const deliverPolicy = this.initialPosition === "earliest" ? DeliverPolicy.All : DeliverPolicy.New; - await this.jsm.consumers.add(streamName, { + await this.jsm.consumers.add(this.streamName, { durable_name: this.subscription, ack_policy: AckPolicy.Explicit, deliver_policy: deliverPolicy, filter_subject: this.subject, }); - this.consumer = await this.js.consumers.get(streamName, this.subscription); + this.consumer = await this.js.consumers.get(this.streamName, this.subscription); } } @@ -164,18 +154,13 @@ class NatsConsumer implements BackendConsumer { async close(): Promise { this.consumer = null; } - - private streamNameFromSubject(subject: string): string { - // Convert topic like "tg.flow.text-completion" to stream name "tg_flow" - const parts = subject.split("."); - return parts.slice(0, 2).join("_"); - } } export class NatsBackend implements PubSubBackend { private connection: NatsConnection | null = null; private js: JetStreamClient | null = null; private jsm: JetStreamManager | null = null; + private initializedStreams = new Set(); constructor(private readonly url: string = "nats://localhost:4222") {} @@ -187,19 +172,46 @@ export class NatsBackend implements PubSubBackend { } } + /** + * Ensure the stream for a given subject exists with a wildcard filter. + * E.g. subject "tg.flow.config-request" → stream "tg_flow" with subjects ["tg.flow.>"] + */ + private async ensureStream(subject: string): Promise { + const parts = subject.split("."); + const streamName = parts.slice(0, 2).join("_"); + + if (this.initializedStreams.has(streamName)) return streamName; + + const wildcardSubject = `${parts.slice(0, 2).join(".")}.>`; + + try { + await this.jsm!.streams.info(streamName); + } catch { + await this.jsm!.streams.add({ + name: streamName, + subjects: [wildcardSubject], + }); + } + this.initializedStreams.add(streamName); + return streamName; + } + async createProducer(options: CreateProducerOptions): Promise> { await this.ensureConnected(); + await this.ensureStream(options.topic); return new NatsProducer(this.js!, options.topic); } async createConsumer(options: CreateConsumerOptions): Promise> { await this.ensureConnected(); + const streamName = await this.ensureStream(options.topic); const consumer = new NatsConsumer( this.js!, this.jsm!, options.topic, options.subscription, options.initialPosition ?? "latest", + streamName, ); await consumer.init(); return consumer; diff --git a/ts/packages/base/src/messaging/subscriber.ts b/ts/packages/base/src/messaging/subscriber.ts index 1d685a91..4ec7a8b3 100644 --- a/ts/packages/base/src/messaging/subscriber.ts +++ b/ts/packages/base/src/messaging/subscriber.ts @@ -111,11 +111,14 @@ export class Subscriber { } private async dispatchLoop(): Promise { + let consecutiveErrors = 0; while (this.running) { try { const msg = await this.backend!.receive(2000); if (!msg) continue; + consecutiveErrors = 0; + const props = msg.properties(); const id = props.id; const value = msg.value(); @@ -136,7 +139,15 @@ export class Subscriber { await this.backend!.acknowledge(msg); } catch (err) { if (!this.running) break; - console.error("[Subscriber] Error:", err); + consecutiveErrors++; + if (consecutiveErrors <= 3) { + console.error("[Subscriber] Error:", err); + } else if (consecutiveErrors === 4) { + console.error("[Subscriber] Suppressing further errors (will retry with backoff)"); + } + // Exponential backoff: 1s, 2s, 4s, max 10s + const delay = Math.min(1000 * Math.pow(2, consecutiveErrors - 1), 10_000); + await new Promise((r) => setTimeout(r, delay)); } } } diff --git a/ts/packages/base/src/processor/async-processor.ts b/ts/packages/base/src/processor/async-processor.ts index dd2e8367..50ba9de4 100644 --- a/ts/packages/base/src/processor/async-processor.ts +++ b/ts/packages/base/src/processor/async-processor.ts @@ -24,7 +24,7 @@ export type ConfigHandler = ( export abstract class AsyncProcessor { protected pubsub: PubSubBackend; protected running = false; - private configHandlers: ConfigHandler[] = []; + protected configHandlers: ConfigHandler[] = []; private shutdownCallbacks: Array<() => Promise> = []; constructor(protected readonly config: ProcessorConfig) { diff --git a/ts/packages/base/src/processor/flow-processor.ts b/ts/packages/base/src/processor/flow-processor.ts index c490508a..44e2c229 100644 --- a/ts/packages/base/src/processor/flow-processor.ts +++ b/ts/packages/base/src/processor/flow-processor.ts @@ -1,20 +1,30 @@ /** * Flow-aware processor that manages dynamic flow instances. * + * Subscribes to config-push topic and dynamically creates/destroys + * flow instances based on the configuration received. + * * Python reference: trustgraph-base/trustgraph/base/flow_processor.py */ import { AsyncProcessor, type ProcessorConfig } from "./async-processor.js"; import type { Spec } from "../spec/types.js"; +import type { BackendConsumer } from "../backend/types.js"; import { Flow, type FlowDefinition } from "./flow.js"; +import { topics } from "../schema/topics.js"; + +interface ConfigPush { + version: number; + config: Record; +} export abstract class FlowProcessor extends AsyncProcessor { private specifications: Spec[] = []; private flows = new Map(); + private configConsumer: BackendConsumer | null = null; constructor(config: ProcessorConfig) { super(config); - this.registerConfigHandler(this.onConfigureFlows.bind(this)); } registerSpecification(spec: Spec): void { @@ -22,16 +32,38 @@ export abstract class FlowProcessor extends AsyncProcessor { } protected async run(): Promise { - // The processor sits idle waiting for flow configurations - // to arrive via the config push topic. In the meantime, - // the consumer loop runs in the background. - await new Promise((resolve) => { - const check = () => { - if (!this.running) resolve(); - else setTimeout(check, 1000); - }; - check(); + // Subscribe to config-push topic to receive flow definitions. + // Use "earliest" to replay any config pushes that arrived before this service started. + this.configConsumer = await this.pubsub.createConsumer({ + topic: topics.configPush, + subscription: `${this.config.id}-config-push`, + initialPosition: "earliest", }); + + console.log(`[${this.config.id}] Listening for config pushes on ${topics.configPush}`); + + while (this.running) { + try { + const msg = await this.configConsumer.receive(2000); + if (!msg) continue; + + const push = msg.value(); + console.log(`[${this.config.id}] Received config push version=${push.version}`); + + await this.onConfigureFlows(push.config, push.version); + + // Also call any registered config handlers + for (const handler of this.configHandlers) { + await handler(push.config, push.version); + } + + await this.configConsumer.acknowledge(msg); + } catch (err) { + if (!this.running) break; + console.error(`[${this.config.id}] Config consumer error:`, err); + await sleep(1000); + } + } } private async onConfigureFlows( @@ -39,27 +71,43 @@ export abstract class FlowProcessor extends AsyncProcessor { version: number, ): Promise { const flowDefs = config.flows as Record | undefined; - if (!flowDefs) return; + if (!flowDefs) { + console.log(`[${this.config.id}] No flows in config push, skipping`); + return; + } // Stop removed flows for (const [name, flow] of this.flows) { if (!(name in flowDefs)) { + console.log(`[${this.config.id}] Stopping removed flow: ${name}`); await flow.stop(); this.flows.delete(name); } } - // Start new flows + // Start or update flows for (const [name, defn] of Object.entries(flowDefs)) { + // Skip invalid definitions (e.g., stringified JSON) + if (typeof defn !== "object" || defn === null) { + console.warn(`[${this.config.id}] Skipping flow "${name}": definition is not an object`); + continue; + } + if (!this.flows.has(name)) { + console.log(`[${this.config.id}] Starting flow "${name}" with topics:`, defn.topics); const flow = new Flow(name, this.config.id, this.pubsub, defn, this.specifications); await flow.start(); this.flows.set(name, flow); + console.log(`[${this.config.id}] Flow "${name}" started`); } } } override async stop(): Promise { + if (this.configConsumer) { + await this.configConsumer.close(); + this.configConsumer = null; + } for (const flow of this.flows.values()) { await flow.stop(); } @@ -67,3 +115,7 @@ export abstract class FlowProcessor extends AsyncProcessor { await super.stop(); } } + +function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} diff --git a/ts/pnpm-lock.yaml b/ts/pnpm-lock.yaml index e1862b4c..e0d86942 100644 --- a/ts/pnpm-lock.yaml +++ b/ts/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: devDependencies: + tsx: + specifier: ^4.21.0 + version: 4.21.0 turbo: specifier: ^2.5.0 version: 2.9.4 @@ -32,7 +35,7 @@ importers: version: 5.9.3 vitest: specifier: ^3.1.0 - version: 3.2.4(@types/debug@4.1.13)(@types/node@22.19.17)(happy-dom@20.8.9)(jiti@2.6.1)(lightningcss@1.32.0) + version: 3.2.4(@types/debug@4.1.13)(@types/node@22.19.17)(happy-dom@20.8.9)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0) packages/cli: dependencies: @@ -57,7 +60,7 @@ importers: version: 5.9.3 vitest: specifier: ^3.1.0 - version: 3.2.4(@types/debug@4.1.13)(@types/node@25.5.2)(happy-dom@20.8.9)(jiti@2.6.1)(lightningcss@1.32.0) + version: 3.2.4(@types/debug@4.1.13)(@types/node@25.5.2)(happy-dom@20.8.9)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0) packages/client: dependencies: @@ -79,7 +82,7 @@ importers: version: 5.9.3 vitest: specifier: ^3.1.0 - version: 3.2.4(@types/debug@4.1.13)(@types/node@22.19.17)(happy-dom@20.8.9)(jiti@2.6.1)(lightningcss@1.32.0) + version: 3.2.4(@types/debug@4.1.13)(@types/node@22.19.17)(happy-dom@20.8.9)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0) packages/flow: dependencies: @@ -110,7 +113,7 @@ importers: version: 5.9.3 vitest: specifier: ^3.1.0 - version: 3.2.4(@types/debug@4.1.13)(@types/node@25.5.2)(happy-dom@20.8.9)(jiti@2.6.1)(lightningcss@1.32.0) + version: 3.2.4(@types/debug@4.1.13)(@types/node@25.5.2)(happy-dom@20.8.9)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0) packages/mcp: dependencies: @@ -135,7 +138,7 @@ importers: version: 5.9.3 vitest: specifier: ^3.1.0 - version: 3.2.4(@types/debug@4.1.13)(@types/node@22.19.17)(happy-dom@20.8.9)(jiti@2.6.1)(lightningcss@1.32.0) + version: 3.2.4(@types/debug@4.1.13)(@types/node@22.19.17)(happy-dom@20.8.9)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0) packages/workbench: dependencies: @@ -178,7 +181,7 @@ importers: devDependencies: '@tailwindcss/vite': specifier: ^4.1.0 - version: 4.2.2(vite@6.4.1(@types/node@25.5.2)(jiti@2.6.1)(lightningcss@1.32.0)) + version: 4.2.2(vite@6.4.1(@types/node@25.5.2)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)) '@types/react': specifier: ^19.1.0 version: 19.2.14 @@ -187,7 +190,7 @@ importers: version: 19.2.3(@types/react@19.2.14) '@vitejs/plugin-react': specifier: ^4.5.0 - version: 4.7.0(vite@6.4.1(@types/node@25.5.2)(jiti@2.6.1)(lightningcss@1.32.0)) + version: 4.7.0(vite@6.4.1(@types/node@25.5.2)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)) tailwindcss: specifier: ^4.1.0 version: 4.2.2 @@ -196,7 +199,7 @@ importers: version: 5.9.3 vite: specifier: ^6.3.0 - version: 6.4.1(@types/node@25.5.2)(jiti@2.6.1)(lightningcss@1.32.0) + version: 6.4.1(@types/node@25.5.2)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0) packages: @@ -292,156 +295,312 @@ packages: cpu: [ppc64] os: [aix] + '@esbuild/aix-ppc64@0.27.7': + resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + '@esbuild/android-arm64@0.25.12': resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} engines: {node: '>=18'} cpu: [arm64] os: [android] + '@esbuild/android-arm64@0.27.7': + resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm@0.25.12': resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} engines: {node: '>=18'} cpu: [arm] os: [android] + '@esbuild/android-arm@0.27.7': + resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + '@esbuild/android-x64@0.25.12': resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} engines: {node: '>=18'} cpu: [x64] os: [android] + '@esbuild/android-x64@0.27.7': + resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + '@esbuild/darwin-arm64@0.25.12': resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] + '@esbuild/darwin-arm64@0.27.7': + resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-x64@0.25.12': resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} engines: {node: '>=18'} cpu: [x64] os: [darwin] + '@esbuild/darwin-x64@0.27.7': + resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + '@esbuild/freebsd-arm64@0.25.12': resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-arm64@0.27.7': + resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-x64@0.25.12': resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] + '@esbuild/freebsd-x64@0.27.7': + resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + '@esbuild/linux-arm64@0.25.12': resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} engines: {node: '>=18'} cpu: [arm64] os: [linux] + '@esbuild/linux-arm64@0.27.7': + resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm@0.25.12': resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} engines: {node: '>=18'} cpu: [arm] os: [linux] + '@esbuild/linux-arm@0.27.7': + resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + '@esbuild/linux-ia32@0.25.12': resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} engines: {node: '>=18'} cpu: [ia32] os: [linux] + '@esbuild/linux-ia32@0.27.7': + resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-loong64@0.25.12': resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} engines: {node: '>=18'} cpu: [loong64] os: [linux] + '@esbuild/linux-loong64@0.27.7': + resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-mips64el@0.25.12': resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] + '@esbuild/linux-mips64el@0.27.7': + resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-ppc64@0.25.12': resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] + '@esbuild/linux-ppc64@0.27.7': + resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-riscv64@0.25.12': resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] + '@esbuild/linux-riscv64@0.27.7': + resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-s390x@0.25.12': resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} engines: {node: '>=18'} cpu: [s390x] os: [linux] + '@esbuild/linux-s390x@0.27.7': + resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-x64@0.25.12': resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} engines: {node: '>=18'} cpu: [x64] os: [linux] + '@esbuild/linux-x64@0.27.7': + resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + '@esbuild/netbsd-arm64@0.25.12': resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] + '@esbuild/netbsd-arm64@0.27.7': + resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + '@esbuild/netbsd-x64@0.25.12': resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] + '@esbuild/netbsd-x64@0.27.7': + resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + '@esbuild/openbsd-arm64@0.25.12': resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] + '@esbuild/openbsd-arm64@0.27.7': + resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + '@esbuild/openbsd-x64@0.25.12': resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] + '@esbuild/openbsd-x64@0.27.7': + resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + '@esbuild/openharmony-arm64@0.25.12': resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] + '@esbuild/openharmony-arm64@0.27.7': + resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + '@esbuild/sunos-x64@0.25.12': resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} engines: {node: '>=18'} cpu: [x64] os: [sunos] + '@esbuild/sunos-x64@0.27.7': + resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + '@esbuild/win32-arm64@0.25.12': resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} engines: {node: '>=18'} cpu: [arm64] os: [win32] + '@esbuild/win32-arm64@0.27.7': + resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-ia32@0.25.12': resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} engines: {node: '>=18'} cpu: [ia32] os: [win32] + '@esbuild/win32-ia32@0.27.7': + resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-x64@0.25.12': resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} engines: {node: '>=18'} cpu: [x64] os: [win32] + '@esbuild/win32-x64@0.27.7': + resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@falkordb/client@1.6.0': resolution: {integrity: sha512-uZfP3/Ialejan9pLwIKXxqJoQtZaUqWwPVvdUVSFvQU4nQCAHLNxgmKUMHHqI7EckXqRmkY53SzTH2EvSZ50wQ==} engines: {node: '>=14'} @@ -1220,6 +1379,11 @@ packages: engines: {node: '>=18'} hasBin: true + esbuild@0.27.7: + resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==} + engines: {node: '>=18'} + hasBin: true + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -1361,6 +1525,9 @@ packages: resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} engines: {node: '>= 0.4'} + get-tsconfig@4.13.7: + resolution: {integrity: sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==} + gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} @@ -1922,6 +2089,9 @@ packages: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + ret@0.5.0: resolution: {integrity: sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==} engines: {node: '>=10'} @@ -2113,6 +2283,11 @@ packages: trough@2.2.0: resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} + tsx@4.21.0: + resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} + engines: {node: '>=18.0.0'} + hasBin: true + turbo@2.9.4: resolution: {integrity: sha512-wZ/kMcZCuK5oEp7sXSSo/5fzKjP9I2EhoiarZjyCm2Ixk0WxFrC/h0gF3686eHHINoFQOOSWgB/pGfvkR8rkgQ==} hasBin: true @@ -2459,81 +2634,159 @@ snapshots: '@esbuild/aix-ppc64@0.25.12': optional: true + '@esbuild/aix-ppc64@0.27.7': + optional: true + '@esbuild/android-arm64@0.25.12': optional: true + '@esbuild/android-arm64@0.27.7': + optional: true + '@esbuild/android-arm@0.25.12': optional: true + '@esbuild/android-arm@0.27.7': + optional: true + '@esbuild/android-x64@0.25.12': optional: true + '@esbuild/android-x64@0.27.7': + optional: true + '@esbuild/darwin-arm64@0.25.12': optional: true + '@esbuild/darwin-arm64@0.27.7': + optional: true + '@esbuild/darwin-x64@0.25.12': optional: true + '@esbuild/darwin-x64@0.27.7': + optional: true + '@esbuild/freebsd-arm64@0.25.12': optional: true + '@esbuild/freebsd-arm64@0.27.7': + optional: true + '@esbuild/freebsd-x64@0.25.12': optional: true + '@esbuild/freebsd-x64@0.27.7': + optional: true + '@esbuild/linux-arm64@0.25.12': optional: true + '@esbuild/linux-arm64@0.27.7': + optional: true + '@esbuild/linux-arm@0.25.12': optional: true + '@esbuild/linux-arm@0.27.7': + optional: true + '@esbuild/linux-ia32@0.25.12': optional: true + '@esbuild/linux-ia32@0.27.7': + optional: true + '@esbuild/linux-loong64@0.25.12': optional: true + '@esbuild/linux-loong64@0.27.7': + optional: true + '@esbuild/linux-mips64el@0.25.12': optional: true + '@esbuild/linux-mips64el@0.27.7': + optional: true + '@esbuild/linux-ppc64@0.25.12': optional: true + '@esbuild/linux-ppc64@0.27.7': + optional: true + '@esbuild/linux-riscv64@0.25.12': optional: true + '@esbuild/linux-riscv64@0.27.7': + optional: true + '@esbuild/linux-s390x@0.25.12': optional: true + '@esbuild/linux-s390x@0.27.7': + optional: true + '@esbuild/linux-x64@0.25.12': optional: true + '@esbuild/linux-x64@0.27.7': + optional: true + '@esbuild/netbsd-arm64@0.25.12': optional: true + '@esbuild/netbsd-arm64@0.27.7': + optional: true + '@esbuild/netbsd-x64@0.25.12': optional: true + '@esbuild/netbsd-x64@0.27.7': + optional: true + '@esbuild/openbsd-arm64@0.25.12': optional: true + '@esbuild/openbsd-arm64@0.27.7': + optional: true + '@esbuild/openbsd-x64@0.25.12': optional: true + '@esbuild/openbsd-x64@0.27.7': + optional: true + '@esbuild/openharmony-arm64@0.25.12': optional: true + '@esbuild/openharmony-arm64@0.27.7': + optional: true + '@esbuild/sunos-x64@0.25.12': optional: true + '@esbuild/sunos-x64@0.27.7': + optional: true + '@esbuild/win32-arm64@0.25.12': optional: true + '@esbuild/win32-arm64@0.27.7': + optional: true + '@esbuild/win32-ia32@0.25.12': optional: true + '@esbuild/win32-ia32@0.27.7': + optional: true + '@esbuild/win32-x64@0.25.12': optional: true + '@esbuild/win32-x64@0.27.7': + optional: true + '@falkordb/client@1.6.0': dependencies: cluster-key-slot: 1.1.2 @@ -2771,12 +3024,12 @@ snapshots: '@tailwindcss/oxide-win32-arm64-msvc': 4.2.2 '@tailwindcss/oxide-win32-x64-msvc': 4.2.2 - '@tailwindcss/vite@4.2.2(vite@6.4.1(@types/node@25.5.2)(jiti@2.6.1)(lightningcss@1.32.0))': + '@tailwindcss/vite@4.2.2(vite@6.4.1(@types/node@25.5.2)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0))': dependencies: '@tailwindcss/node': 4.2.2 '@tailwindcss/oxide': 4.2.2 tailwindcss: 4.2.2 - vite: 6.4.1(@types/node@25.5.2)(jiti@2.6.1)(lightningcss@1.32.0) + vite: 6.4.1(@types/node@25.5.2)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0) '@tanstack/query-core@5.96.2': {} @@ -2891,7 +3144,7 @@ snapshots: '@ungap/structured-clone@1.3.0': {} - '@vitejs/plugin-react@4.7.0(vite@6.4.1(@types/node@25.5.2)(jiti@2.6.1)(lightningcss@1.32.0))': + '@vitejs/plugin-react@4.7.0(vite@6.4.1(@types/node@25.5.2)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0))': dependencies: '@babel/core': 7.29.0 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0) @@ -2899,7 +3152,7 @@ snapshots: '@rolldown/pluginutils': 1.0.0-beta.27 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: 6.4.1(@types/node@25.5.2)(jiti@2.6.1)(lightningcss@1.32.0) + vite: 6.4.1(@types/node@25.5.2)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0) transitivePeerDependencies: - supports-color @@ -2911,21 +3164,21 @@ snapshots: chai: 5.3.3 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.4(vite@6.4.1(@types/node@22.19.17)(jiti@2.6.1)(lightningcss@1.32.0))': + '@vitest/mocker@3.2.4(vite@6.4.1(@types/node@22.19.17)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 6.4.1(@types/node@22.19.17)(jiti@2.6.1)(lightningcss@1.32.0) + vite: 6.4.1(@types/node@22.19.17)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0) - '@vitest/mocker@3.2.4(vite@6.4.1(@types/node@25.5.2)(jiti@2.6.1)(lightningcss@1.32.0))': + '@vitest/mocker@3.2.4(vite@6.4.1(@types/node@25.5.2)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 6.4.1(@types/node@25.5.2)(jiti@2.6.1)(lightningcss@1.32.0) + vite: 6.4.1(@types/node@25.5.2)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0) '@vitest/pretty-format@3.2.4': dependencies: @@ -3278,6 +3531,35 @@ snapshots: '@esbuild/win32-ia32': 0.25.12 '@esbuild/win32-x64': 0.25.12 + esbuild@0.27.7: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.7 + '@esbuild/android-arm': 0.27.7 + '@esbuild/android-arm64': 0.27.7 + '@esbuild/android-x64': 0.27.7 + '@esbuild/darwin-arm64': 0.27.7 + '@esbuild/darwin-x64': 0.27.7 + '@esbuild/freebsd-arm64': 0.27.7 + '@esbuild/freebsd-x64': 0.27.7 + '@esbuild/linux-arm': 0.27.7 + '@esbuild/linux-arm64': 0.27.7 + '@esbuild/linux-ia32': 0.27.7 + '@esbuild/linux-loong64': 0.27.7 + '@esbuild/linux-mips64el': 0.27.7 + '@esbuild/linux-ppc64': 0.27.7 + '@esbuild/linux-riscv64': 0.27.7 + '@esbuild/linux-s390x': 0.27.7 + '@esbuild/linux-x64': 0.27.7 + '@esbuild/netbsd-arm64': 0.27.7 + '@esbuild/netbsd-x64': 0.27.7 + '@esbuild/openbsd-arm64': 0.27.7 + '@esbuild/openbsd-x64': 0.27.7 + '@esbuild/openharmony-arm64': 0.27.7 + '@esbuild/sunos-x64': 0.27.7 + '@esbuild/win32-arm64': 0.27.7 + '@esbuild/win32-ia32': 0.27.7 + '@esbuild/win32-x64': 0.27.7 + escalade@3.2.0: {} escape-html@1.0.3: {} @@ -3479,6 +3761,10 @@ snapshots: dunder-proto: 1.0.1 es-object-atoms: 1.1.1 + get-tsconfig@4.13.7: + dependencies: + resolve-pkg-maps: 1.0.0 + gopd@1.2.0: {} graceful-fs@4.2.11: {} @@ -4141,6 +4427,8 @@ snapshots: require-from-string@2.0.2: {} + resolve-pkg-maps@1.0.0: {} + ret@0.5.0: {} reusify@1.1.0: {} @@ -4351,6 +4639,13 @@ snapshots: trough@2.2.0: {} + tsx@4.21.0: + dependencies: + esbuild: 0.27.7 + get-tsconfig: 4.13.7 + optionalDependencies: + fsevents: 2.3.3 + turbo@2.9.4: optionalDependencies: '@turbo/darwin-64': 2.9.4 @@ -4434,13 +4729,13 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 - vite-node@3.2.4(@types/node@22.19.17)(jiti@2.6.1)(lightningcss@1.32.0): + vite-node@3.2.4(@types/node@22.19.17)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0): dependencies: cac: 6.7.14 debug: 4.4.3 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 6.4.1(@types/node@22.19.17)(jiti@2.6.1)(lightningcss@1.32.0) + vite: 6.4.1(@types/node@22.19.17)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0) transitivePeerDependencies: - '@types/node' - jiti @@ -4455,13 +4750,13 @@ snapshots: - tsx - yaml - vite-node@3.2.4(@types/node@25.5.2)(jiti@2.6.1)(lightningcss@1.32.0): + vite-node@3.2.4(@types/node@25.5.2)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0): dependencies: cac: 6.7.14 debug: 4.4.3 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 6.4.1(@types/node@25.5.2)(jiti@2.6.1)(lightningcss@1.32.0) + vite: 6.4.1(@types/node@25.5.2)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0) transitivePeerDependencies: - '@types/node' - jiti @@ -4476,7 +4771,7 @@ snapshots: - tsx - yaml - vite@6.4.1(@types/node@22.19.17)(jiti@2.6.1)(lightningcss@1.32.0): + vite@6.4.1(@types/node@22.19.17)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0): dependencies: esbuild: 0.25.12 fdir: 6.5.0(picomatch@4.0.4) @@ -4489,8 +4784,9 @@ snapshots: fsevents: 2.3.3 jiti: 2.6.1 lightningcss: 1.32.0 + tsx: 4.21.0 - vite@6.4.1(@types/node@25.5.2)(jiti@2.6.1)(lightningcss@1.32.0): + vite@6.4.1(@types/node@25.5.2)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0): dependencies: esbuild: 0.25.12 fdir: 6.5.0(picomatch@4.0.4) @@ -4503,12 +4799,13 @@ snapshots: fsevents: 2.3.3 jiti: 2.6.1 lightningcss: 1.32.0 + tsx: 4.21.0 - vitest@3.2.4(@types/debug@4.1.13)(@types/node@22.19.17)(happy-dom@20.8.9)(jiti@2.6.1)(lightningcss@1.32.0): + vitest@3.2.4(@types/debug@4.1.13)(@types/node@22.19.17)(happy-dom@20.8.9)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0): dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@6.4.1(@types/node@22.19.17)(jiti@2.6.1)(lightningcss@1.32.0)) + '@vitest/mocker': 3.2.4(vite@6.4.1(@types/node@22.19.17)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -4526,8 +4823,8 @@ snapshots: tinyglobby: 0.2.15 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 6.4.1(@types/node@22.19.17)(jiti@2.6.1)(lightningcss@1.32.0) - vite-node: 3.2.4(@types/node@22.19.17)(jiti@2.6.1)(lightningcss@1.32.0) + vite: 6.4.1(@types/node@22.19.17)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0) + vite-node: 3.2.4(@types/node@22.19.17)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/debug': 4.1.13 @@ -4547,11 +4844,11 @@ snapshots: - tsx - yaml - vitest@3.2.4(@types/debug@4.1.13)(@types/node@25.5.2)(happy-dom@20.8.9)(jiti@2.6.1)(lightningcss@1.32.0): + vitest@3.2.4(@types/debug@4.1.13)(@types/node@25.5.2)(happy-dom@20.8.9)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0): dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@6.4.1(@types/node@25.5.2)(jiti@2.6.1)(lightningcss@1.32.0)) + '@vitest/mocker': 3.2.4(vite@6.4.1(@types/node@25.5.2)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -4569,8 +4866,8 @@ snapshots: tinyglobby: 0.2.15 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 6.4.1(@types/node@25.5.2)(jiti@2.6.1)(lightningcss@1.32.0) - vite-node: 3.2.4(@types/node@25.5.2)(jiti@2.6.1)(lightningcss@1.32.0) + vite: 6.4.1(@types/node@25.5.2)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0) + vite-node: 3.2.4(@types/node@25.5.2)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/debug': 4.1.13 diff --git a/ts/scripts/reset-nats.ts b/ts/scripts/reset-nats.ts new file mode 100644 index 00000000..07704274 --- /dev/null +++ b/ts/scripts/reset-nats.ts @@ -0,0 +1,19 @@ +import { connect } from "nats"; + +async function main() { + const nc = await connect({ servers: "nats://localhost:4222" }); + const jsm = await nc.jetstreamManager(); + + try { + const info = await jsm.streams.info("tg_flow"); + console.log("Current stream subjects:", info.config.subjects); + await jsm.streams.delete("tg_flow"); + console.log("Deleted tg_flow stream"); + } catch (e) { + console.log("No stream to delete:", (e as Error).message); + } + + await nc.close(); +} + +main(); diff --git a/ts/scripts/run-config.ts b/ts/scripts/run-config.ts new file mode 100644 index 00000000..73b1ca09 --- /dev/null +++ b/ts/scripts/run-config.ts @@ -0,0 +1,15 @@ +/** + * Start the config service. + * + * Usage: pnpm tsx scripts/run-config.ts + * + * Env: + * NATS_URL (default: nats://localhost:4222) + * CONFIG_PERSIST_PATH (optional, e.g., ./data/config.json) + */ +import { run } from "../packages/flow/src/config/service.js"; + +run().catch((err) => { + console.error("Config service failed:", err); + process.exit(1); +}); diff --git a/ts/scripts/run-gateway.ts b/ts/scripts/run-gateway.ts new file mode 100644 index 00000000..2bdf31e9 --- /dev/null +++ b/ts/scripts/run-gateway.ts @@ -0,0 +1,16 @@ +/** + * Start the API gateway. + * + * Usage: pnpm tsx scripts/run-gateway.ts + * + * Env: + * NATS_URL (default: nats://localhost:4222) + * GATEWAY_PORT (default: 8088) + * GATEWAY_SECRET (optional) + */ +import { run } from "../packages/flow/src/gateway/server.js"; + +run().catch((err) => { + console.error("Gateway failed to start:", err); + process.exit(1); +}); diff --git a/ts/scripts/run-llm-claude.ts b/ts/scripts/run-llm-claude.ts new file mode 100644 index 00000000..66498928 --- /dev/null +++ b/ts/scripts/run-llm-claude.ts @@ -0,0 +1,15 @@ +/** + * Start the Claude text-completion service. + * + * Usage: CLAUDE_KEY=sk-... pnpm tsx scripts/run-llm-claude.ts + * + * Env: + * NATS_URL (default: nats://localhost:4222) + * CLAUDE_KEY (required) + */ +import { run } from "../packages/flow/src/model/text-completion/claude.js"; + +run().catch((err) => { + console.error("Claude LLM service failed:", err); + process.exit(1); +}); diff --git a/ts/scripts/run-llm-openai.ts b/ts/scripts/run-llm-openai.ts new file mode 100644 index 00000000..ba11a163 --- /dev/null +++ b/ts/scripts/run-llm-openai.ts @@ -0,0 +1,16 @@ +/** + * Start the OpenAI text-completion service. + * + * Usage: OPENAI_TOKEN=sk-... pnpm tsx scripts/run-llm-openai.ts + * + * Env: + * NATS_URL (default: nats://localhost:4222) + * OPENAI_TOKEN (required) + * OPENAI_BASE_URL (optional) + */ +import { run } from "../packages/flow/src/model/text-completion/openai.js"; + +run().catch((err) => { + console.error("OpenAI LLM service failed:", err); + process.exit(1); +}); diff --git a/ts/scripts/test-pipeline.ts b/ts/scripts/test-pipeline.ts new file mode 100644 index 00000000..185f9640 --- /dev/null +++ b/ts/scripts/test-pipeline.ts @@ -0,0 +1,292 @@ +/** + * Integration test — exercises the full pipeline: + * + * 1. Start config service + gateway + * 2. Test config CRUD via REST + * 3. Push a flow definition (for LLM service) + * 4. Optionally test LLM text-completion (if CLAUDE_KEY or OPENAI_TOKEN set) + * + * Usage: pnpm tsx scripts/test-pipeline.ts + */ + +const GATEWAY_URL = process.env.GATEWAY_URL ?? "http://localhost:8088"; + +// ─── Helpers ────────────────────────────────────────────────────────── + +async function post(path: string, body: unknown): Promise { + const res = await fetch(`${GATEWAY_URL}${path}`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + }); + const text = await res.text(); + try { + return JSON.parse(text); + } catch { + return { status: res.status, body: text }; + } +} + +function log(label: string, data: unknown): void { + console.log(`\n[${label}]`, JSON.stringify(data, null, 2)); +} + +function pass(test: string): void { + console.log(` ✓ ${test}`); +} + +function fail(test: string, err: unknown): void { + console.error(` ✗ ${test}:`, err); +} + +// ─── Tests ──────────────────────────────────────────────────────────── + +async function testConfigList(): Promise { + try { + const res = await post("/api/v1/config", { operation: "list", keys: [] }); + log("config/list", res); + if (typeof res === "object" && res !== null && "version" in res) { + pass("Config list returns version"); + return true; + } + fail("Config list", "unexpected response"); + return false; + } catch (err) { + fail("Config list", err); + return false; + } +} + +async function testConfigPut(): Promise { + try { + const res = await post("/api/v1/config", { + operation: "put", + keys: ["test"], + values: { greeting: "hello from trustgraph-ts!" }, + }); + log("config/put", res); + if (typeof res === "object" && res !== null && "version" in res) { + pass("Config put accepted"); + return true; + } + fail("Config put", "unexpected response"); + return false; + } catch (err) { + fail("Config put", err); + return false; + } +} + +async function testConfigGet(): Promise { + try { + const res = await post("/api/v1/config", { + operation: "get", + keys: ["test"], + }); + log("config/get", res); + const r = res as Record; + const values = r.values as Record | undefined; + if (values?.greeting === "hello from trustgraph-ts!") { + pass("Config get returns stored value"); + return true; + } + fail("Config get", "value mismatch"); + return false; + } catch (err) { + fail("Config get", err); + return false; + } +} + +async function testConfigDelete(): Promise { + try { + const res = await post("/api/v1/config", { + operation: "delete", + keys: ["test"], + }); + log("config/delete", res); + + // Verify it's gone + const check = await post("/api/v1/config", { + operation: "get", + keys: ["test"], + }) as Record; + + const values = check.values as Record | undefined; + if (!values || Object.keys(values).length === 0) { + pass("Config delete removes value"); + return true; + } + fail("Config delete", "value still present"); + return false; + } catch (err) { + fail("Config delete", err); + return false; + } +} + +async function testPushFlowConfig(): Promise { + try { + // Push a flow definition that LLM services will pick up + const res = await post("/api/v1/config", { + operation: "put", + keys: ["flows"], + values: { + default: { + topics: { + request: "tg.flow.text-completion-request", + response: "tg.flow.text-completion-response", + }, + }, + }, + }); + log("config/push-flow", res); + if (typeof res === "object" && res !== null && "version" in res) { + pass("Flow config pushed"); + return true; + } + fail("Flow config push", "unexpected response"); + return false; + } catch (err) { + fail("Flow config push", err); + return false; + } +} + +async function testTextCompletion(): Promise { + try { + console.log("\n Sending text-completion request (may take a few seconds)..."); + // Use model from env or default to qwen2.5:0.5b (Ollama-compatible) + const model = process.env.LLM_MODEL ?? "qwen2.5:0.5b"; + const res = await post("/api/v1/flow/default/service/text-completion", { + system: "You are a helpful assistant. Reply in one sentence.", + prompt: "What is 2+2?", + model, + }); + log("text-completion", res); + const r = res as Record; + if (r.response && typeof r.response === "string") { + pass(`Text completion returned: "${(r.response as string).slice(0, 80)}..."`); + return true; + } + if (r.error) { + fail("Text completion", r.error); + return false; + } + fail("Text completion", "unexpected response"); + return false; + } catch (err) { + fail("Text completion", err); + return false; + } +} + +async function testWebSocket(): Promise { + try { + // Use the vendored client's WebSocket adapter + const { getWebSocketConstructor } = await import( + "../packages/client/src/socket/websocket-adapter.js" + ); + const WS = getWebSocketConstructor(); + + return new Promise((resolve) => { + const ws = new WS(`${GATEWAY_URL.replace("http", "ws")}/api/v1/socket`); + const timeout = setTimeout(() => { + ws.close(); + fail("WebSocket", "connection timeout"); + resolve(false); + }, 5000); + + ws.onopen = () => { + clearTimeout(timeout); + // Send a config list request + const msg = JSON.stringify({ + id: "test-ws-1", + service: "config", + request: { operation: "list", keys: [] }, + }); + ws.send(msg); + }; + + ws.onmessage = (event: { data: unknown }) => { + clearTimeout(timeout); + const data = JSON.parse(String(event.data)); + log("websocket/response", data); + ws.close(); + if (data.id === "test-ws-1") { + pass("WebSocket round-trip works"); + resolve(true); + } else { + fail("WebSocket", "unexpected response id"); + resolve(false); + } + }; + + ws.onerror = (err: unknown) => { + clearTimeout(timeout); + fail("WebSocket", err); + resolve(false); + }; + }); + } catch (err) { + fail("WebSocket", err); + return false; + } +} + +// ─── Main ───────────────────────────────────────────────────────────── + +async function main(): Promise { + console.log("╔══════════════════════════════════════════════════╗"); + console.log("║ TrustGraph TypeScript — Integration Test ║"); + console.log("╚══════════════════════════════════════════════════╝"); + console.log(`\nGateway: ${GATEWAY_URL}`); + + // Check gateway is reachable + try { + const res = await fetch(`${GATEWAY_URL}/api/v1/metrics`); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + pass("Gateway reachable"); + } catch (err) { + fail("Gateway reachable", err); + console.error("\n⚠ Gateway not running. Start it first:"); + console.error(" pnpm tsx scripts/run-gateway.ts"); + process.exit(1); + } + + let passed = 0; + let failed = 0; + const run = async (name: string, fn: () => Promise) => { + console.log(`\n── ${name} ──`); + if (await fn()) passed++; + else failed++; + }; + + // Config CRUD tests + await run("Config List", testConfigList); + await run("Config Put", testConfigPut); + await run("Config Get", testConfigGet); + await run("Config Delete", testConfigDelete); + + // WebSocket test + await run("WebSocket Round-Trip", testWebSocket); + + // Flow config push + await run("Push Flow Config", testPushFlowConfig); + + // LLM test (only if a running LLM service is available) + if (process.env.SKIP_LLM !== "1") { + console.log("\n (Testing text-completion — set SKIP_LLM=1 to skip)"); + await run("Text Completion", testTextCompletion); + } else { + console.log("\n (SKIP_LLM=1 — skipping LLM test)"); + } + + console.log("\n══════════════════════════════════════════════════"); + console.log(` Results: ${passed} passed, ${failed} failed`); + console.log("══════════════════════════════════════════════════\n"); + + process.exit(failed > 0 ? 1 : 0); +} + +main();