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) <noreply@anthropic.com>
This commit is contained in:
elpresidank 2026-04-05 23:41:39 -05:00
parent 0042f9259c
commit 28747e1a92
15 changed files with 826 additions and 107 deletions

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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"
},

View file

@ -98,38 +98,28 @@ class NatsConsumer<T> implements BackendConsumer<T> {
private readonly subject: string,
private readonly subscription: string,
private readonly initialPosition: "latest" | "earliest",
private readonly streamName: string,
) {}
async init(): Promise<void> {
// 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<T> implements BackendConsumer<T> {
async close(): Promise<void> {
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<string>();
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<string> {
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<T>(options: CreateProducerOptions): Promise<BackendProducer<T>> {
await this.ensureConnected();
await this.ensureStream(options.topic);
return new NatsProducer<T>(this.js!, options.topic);
}
async createConsumer<T>(options: CreateConsumerOptions): Promise<BackendConsumer<T>> {
await this.ensureConnected();
const streamName = await this.ensureStream(options.topic);
const consumer = new NatsConsumer<T>(
this.js!,
this.jsm!,
options.topic,
options.subscription,
options.initialPosition ?? "latest",
streamName,
);
await consumer.init();
return consumer;

View file

@ -111,11 +111,14 @@ export class Subscriber<T> {
}
private async dispatchLoop(): Promise<void> {
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<T> {
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));
}
}
}

View file

@ -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<void>> = [];
constructor(protected readonly config: ProcessorConfig) {

View file

@ -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<string, unknown>;
}
export abstract class FlowProcessor extends AsyncProcessor {
private specifications: Spec[] = [];
private flows = new Map<string, Flow>();
private configConsumer: BackendConsumer<ConfigPush> | 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<void> {
// 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<void>((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<ConfigPush>({
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<void> {
const flowDefs = config.flows as Record<string, FlowDefinition> | 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<void> {
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<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}

357
ts/pnpm-lock.yaml generated
View file

@ -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

19
ts/scripts/reset-nats.ts Normal file
View file

@ -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();

15
ts/scripts/run-config.ts Normal file
View file

@ -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);
});

16
ts/scripts/run-gateway.ts Normal file
View file

@ -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);
});

View file

@ -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);
});

View file

@ -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);
});

292
ts/scripts/test-pipeline.ts Normal file
View file

@ -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<unknown> {
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<boolean> {
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<boolean> {
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<boolean> {
try {
const res = await post("/api/v1/config", {
operation: "get",
keys: ["test"],
});
log("config/get", res);
const r = res as Record<string, unknown>;
const values = r.values as Record<string, unknown> | 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<boolean> {
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<string, unknown>;
const values = check.values as Record<string, unknown> | 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<boolean> {
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<boolean> {
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<string, unknown>;
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<boolean> {
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<boolean>((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<void> {
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<boolean>) => {
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();