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>
2026-04-05 23:41:39 -05:00
|
|
|
/**
|
|
|
|
|
* 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 {
|
fix: resolve FlowProcessor topic collisions, librarian timeout, tests
Fix critical bug where all FlowProcessor services shared the same spec
names ("request"/"response"), causing them to steal each other's NATS
topics. Now each service uses unique spec names matching the flow config
topic keys (e.g., "text-completion-request", "prompt-request",
"agent-request").
Fix librarian NATS consumer timeout (500ms → 2000ms, below NATS minimum).
Update seed-config and test-pipeline with correct flow topic mappings.
Add prompt template runner script.
Smoke test results: 11/11 passing (config CRUD, WebSocket, LLM,
librarian CRUD). Agent routing verified via manual curl test.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 01:02:10 -05:00
|
|
|
// Push a full flow definition with all service topic mappings
|
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>
2026-04-05 23:41:39 -05:00
|
|
|
const res = await post("/api/v1/config", {
|
|
|
|
|
operation: "put",
|
|
|
|
|
keys: ["flows"],
|
|
|
|
|
values: {
|
|
|
|
|
default: {
|
|
|
|
|
topics: {
|
2026-04-06 23:47:43 -05:00
|
|
|
// Document processing pipeline
|
|
|
|
|
"decode-input": "tg.flow.document",
|
|
|
|
|
"decode-output": "tg.flow.text-document",
|
|
|
|
|
"decode-triples": "tg.flow.triples",
|
|
|
|
|
"chunk-input": "tg.flow.text-document",
|
|
|
|
|
"chunk-output": "tg.flow.chunk",
|
|
|
|
|
"chunk-triples": "tg.flow.triples",
|
|
|
|
|
"extract-input": "tg.flow.chunk",
|
|
|
|
|
"extract-triples": "tg.flow.triples",
|
|
|
|
|
"extract-entity-contexts": "tg.flow.entity-contexts",
|
|
|
|
|
// Storage consumers
|
|
|
|
|
"store-triples-input": "tg.flow.triples",
|
|
|
|
|
"store-graph-embeddings-input": "tg.flow.entity-contexts",
|
|
|
|
|
// LLM text completion
|
fix: resolve FlowProcessor topic collisions, librarian timeout, tests
Fix critical bug where all FlowProcessor services shared the same spec
names ("request"/"response"), causing them to steal each other's NATS
topics. Now each service uses unique spec names matching the flow config
topic keys (e.g., "text-completion-request", "prompt-request",
"agent-request").
Fix librarian NATS consumer timeout (500ms → 2000ms, below NATS minimum).
Update seed-config and test-pipeline with correct flow topic mappings.
Add prompt template runner script.
Smoke test results: 11/11 passing (config CRUD, WebSocket, LLM,
librarian CRUD). Agent routing verified via manual curl test.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 01:02:10 -05:00
|
|
|
"text-completion-request": "tg.flow.text-completion-request",
|
|
|
|
|
"text-completion-response": "tg.flow.text-completion-response",
|
2026-04-06 23:47:43 -05:00
|
|
|
// Prompt service
|
fix: resolve FlowProcessor topic collisions, librarian timeout, tests
Fix critical bug where all FlowProcessor services shared the same spec
names ("request"/"response"), causing them to steal each other's NATS
topics. Now each service uses unique spec names matching the flow config
topic keys (e.g., "text-completion-request", "prompt-request",
"agent-request").
Fix librarian NATS consumer timeout (500ms → 2000ms, below NATS minimum).
Update seed-config and test-pipeline with correct flow topic mappings.
Add prompt template runner script.
Smoke test results: 11/11 passing (config CRUD, WebSocket, LLM,
librarian CRUD). Agent routing verified via manual curl test.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 01:02:10 -05:00
|
|
|
"prompt-request": "tg.flow.prompt-request",
|
|
|
|
|
"prompt-response": "tg.flow.prompt-response",
|
2026-04-06 23:47:43 -05:00
|
|
|
// Graph RAG
|
fix: resolve FlowProcessor topic collisions, librarian timeout, tests
Fix critical bug where all FlowProcessor services shared the same spec
names ("request"/"response"), causing them to steal each other's NATS
topics. Now each service uses unique spec names matching the flow config
topic keys (e.g., "text-completion-request", "prompt-request",
"agent-request").
Fix librarian NATS consumer timeout (500ms → 2000ms, below NATS minimum).
Update seed-config and test-pipeline with correct flow topic mappings.
Add prompt template runner script.
Smoke test results: 11/11 passing (config CRUD, WebSocket, LLM,
librarian CRUD). Agent routing verified via manual curl test.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 01:02:10 -05:00
|
|
|
"graph-rag-request": "tg.flow.graph-rag-request",
|
|
|
|
|
"graph-rag-response": "tg.flow.graph-rag-response",
|
2026-04-06 23:47:43 -05:00
|
|
|
// Document RAG
|
fix: resolve FlowProcessor topic collisions, librarian timeout, tests
Fix critical bug where all FlowProcessor services shared the same spec
names ("request"/"response"), causing them to steal each other's NATS
topics. Now each service uses unique spec names matching the flow config
topic keys (e.g., "text-completion-request", "prompt-request",
"agent-request").
Fix librarian NATS consumer timeout (500ms → 2000ms, below NATS minimum).
Update seed-config and test-pipeline with correct flow topic mappings.
Add prompt template runner script.
Smoke test results: 11/11 passing (config CRUD, WebSocket, LLM,
librarian CRUD). Agent routing verified via manual curl test.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 01:02:10 -05:00
|
|
|
"document-rag-request": "tg.flow.document-rag-request",
|
|
|
|
|
"document-rag-response": "tg.flow.document-rag-response",
|
2026-04-06 23:47:43 -05:00
|
|
|
// Triples query
|
fix: resolve FlowProcessor topic collisions, librarian timeout, tests
Fix critical bug where all FlowProcessor services shared the same spec
names ("request"/"response"), causing them to steal each other's NATS
topics. Now each service uses unique spec names matching the flow config
topic keys (e.g., "text-completion-request", "prompt-request",
"agent-request").
Fix librarian NATS consumer timeout (500ms → 2000ms, below NATS minimum).
Update seed-config and test-pipeline with correct flow topic mappings.
Add prompt template runner script.
Smoke test results: 11/11 passing (config CRUD, WebSocket, LLM,
librarian CRUD). Agent routing verified via manual curl test.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 01:02:10 -05:00
|
|
|
"triples-request": "tg.flow.triples-request",
|
|
|
|
|
"triples-response": "tg.flow.triples-response",
|
2026-04-06 23:47:43 -05:00
|
|
|
// Agent
|
fix: resolve FlowProcessor topic collisions, librarian timeout, tests
Fix critical bug where all FlowProcessor services shared the same spec
names ("request"/"response"), causing them to steal each other's NATS
topics. Now each service uses unique spec names matching the flow config
topic keys (e.g., "text-completion-request", "prompt-request",
"agent-request").
Fix librarian NATS consumer timeout (500ms → 2000ms, below NATS minimum).
Update seed-config and test-pipeline with correct flow topic mappings.
Add prompt template runner script.
Smoke test results: 11/11 passing (config CRUD, WebSocket, LLM,
librarian CRUD). Agent routing verified via manual curl test.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 01:02:10 -05:00
|
|
|
"agent-request": "tg.flow.agent-request",
|
|
|
|
|
"agent-response": "tg.flow.agent-response",
|
2026-04-06 23:47:43 -05:00
|
|
|
// Embeddings
|
|
|
|
|
"embeddings-request": "tg.flow.embeddings-request",
|
|
|
|
|
"embeddings-response": "tg.flow.embeddings-response",
|
|
|
|
|
// Librarian RPC (for PDF decoder)
|
|
|
|
|
"librarian-request": "tg.flow.librarian-request",
|
|
|
|
|
"librarian-response": "tg.flow.librarian-response",
|
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>
2026-04-05 23:41:39 -05:00
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-06 00:37:03 -05:00
|
|
|
// ─── Librarian Tests ──────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
let testDocId = "";
|
|
|
|
|
|
|
|
|
|
async function testLibrarianAdd(): Promise<boolean> {
|
|
|
|
|
try {
|
|
|
|
|
const content = Buffer.from("Hello from TrustGraph TypeScript!").toString("base64");
|
|
|
|
|
const res = await post("/api/v1/librarian", {
|
|
|
|
|
operation: "add-document",
|
|
|
|
|
user: "test-user",
|
|
|
|
|
collection: "test-collection",
|
|
|
|
|
content,
|
|
|
|
|
documentMetadata: {
|
|
|
|
|
id: "",
|
|
|
|
|
time: Date.now(),
|
|
|
|
|
kind: "text/plain",
|
|
|
|
|
title: "Test Document",
|
|
|
|
|
comments: "",
|
|
|
|
|
user: "test-user",
|
|
|
|
|
tags: ["test"],
|
|
|
|
|
documentType: "source",
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
log("librarian/add", res);
|
|
|
|
|
const r = res as Record<string, unknown>;
|
|
|
|
|
const meta = r.documentMetadata as Record<string, unknown> | undefined;
|
|
|
|
|
if (meta?.id && typeof meta.id === "string") {
|
|
|
|
|
testDocId = meta.id;
|
|
|
|
|
pass(`Librarian add-document returned id: ${testDocId.slice(0, 8)}...`);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
if (r.error) {
|
|
|
|
|
fail("Librarian add-document", r.error);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
fail("Librarian add-document", "no documentMetadata.id in response");
|
|
|
|
|
return false;
|
|
|
|
|
} catch (err) {
|
|
|
|
|
fail("Librarian add-document", err);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function testLibrarianList(): Promise<boolean> {
|
|
|
|
|
try {
|
|
|
|
|
const res = await post("/api/v1/librarian", {
|
|
|
|
|
operation: "list-documents",
|
|
|
|
|
user: "test-user",
|
|
|
|
|
});
|
|
|
|
|
log("librarian/list", res);
|
|
|
|
|
const r = res as Record<string, unknown>;
|
|
|
|
|
const docs = r.documents as unknown[] | undefined;
|
|
|
|
|
if (docs && docs.length > 0) {
|
|
|
|
|
pass(`Librarian list-documents returned ${docs.length} document(s)`);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
fail("Librarian list-documents", "empty or missing documents array");
|
|
|
|
|
return false;
|
|
|
|
|
} catch (err) {
|
|
|
|
|
fail("Librarian list-documents", err);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function testLibrarianGetContent(): Promise<boolean> {
|
|
|
|
|
if (!testDocId) {
|
|
|
|
|
fail("Librarian get-content", "no document ID from add test");
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
const res = await post("/api/v1/librarian", {
|
|
|
|
|
operation: "get-document-content",
|
|
|
|
|
documentId: testDocId,
|
|
|
|
|
user: "test-user",
|
|
|
|
|
});
|
|
|
|
|
log("librarian/get-content", res);
|
|
|
|
|
const r = res as Record<string, unknown>;
|
|
|
|
|
if (r.content && typeof r.content === "string") {
|
|
|
|
|
const decoded = Buffer.from(r.content, "base64").toString("utf-8");
|
|
|
|
|
if (decoded === "Hello from TrustGraph TypeScript!") {
|
|
|
|
|
pass("Librarian get-content round-trips correctly");
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
fail("Librarian get-content", `decoded: "${decoded}"`);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
fail("Librarian get-content", "no content in response");
|
|
|
|
|
return false;
|
|
|
|
|
} catch (err) {
|
|
|
|
|
fail("Librarian get-content", err);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function testLibrarianDelete(): Promise<boolean> {
|
|
|
|
|
if (!testDocId) {
|
|
|
|
|
fail("Librarian delete", "no document ID from add test");
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
const res = await post("/api/v1/librarian", {
|
|
|
|
|
operation: "remove-document",
|
|
|
|
|
documentId: testDocId,
|
|
|
|
|
user: "test-user",
|
|
|
|
|
});
|
|
|
|
|
log("librarian/delete", res);
|
|
|
|
|
|
|
|
|
|
// Verify it's gone
|
|
|
|
|
const listRes = await post("/api/v1/librarian", {
|
|
|
|
|
operation: "list-documents",
|
|
|
|
|
user: "test-user",
|
|
|
|
|
}) as Record<string, unknown>;
|
|
|
|
|
const docs = listRes.documents as unknown[] | undefined;
|
|
|
|
|
if (!docs || docs.length === 0) {
|
|
|
|
|
pass("Librarian remove-document deleted successfully");
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
fail("Librarian remove-document", "document still present after delete");
|
|
|
|
|
return false;
|
|
|
|
|
} catch (err) {
|
|
|
|
|
fail("Librarian delete", err);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-06 23:47:43 -05:00
|
|
|
// ─── Document Load Test ──────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
async function testDocumentLoad(): Promise<boolean> {
|
|
|
|
|
try {
|
|
|
|
|
// First upload a test document via librarian
|
|
|
|
|
const content = Buffer.from("Test document for pipeline processing.").toString("base64");
|
|
|
|
|
const addRes = await post("/api/v1/librarian", {
|
|
|
|
|
operation: "add-document",
|
|
|
|
|
user: "test-user",
|
|
|
|
|
collection: "test-collection",
|
|
|
|
|
content,
|
|
|
|
|
documentMetadata: {
|
|
|
|
|
id: "",
|
|
|
|
|
time: Date.now(),
|
|
|
|
|
kind: "application/pdf",
|
|
|
|
|
title: "Test Pipeline Document",
|
|
|
|
|
comments: "",
|
|
|
|
|
user: "test-user",
|
|
|
|
|
tags: ["test"],
|
|
|
|
|
documentType: "source",
|
|
|
|
|
},
|
|
|
|
|
}) as Record<string, unknown>;
|
|
|
|
|
|
|
|
|
|
const meta = addRes.documentMetadata as Record<string, unknown> | undefined;
|
|
|
|
|
if (!meta?.id) {
|
|
|
|
|
fail("Document load", "failed to upload test document");
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
const docId = meta.id as string;
|
|
|
|
|
|
|
|
|
|
// Trigger document processing via the load endpoint
|
|
|
|
|
const res = await fetch(`${GATEWAY_URL}/api/v1/flow/default/load`, {
|
|
|
|
|
method: "POST",
|
|
|
|
|
headers: { "Content-Type": "application/json" },
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
documentId: docId,
|
|
|
|
|
user: "test-user",
|
|
|
|
|
collection: "test-collection",
|
|
|
|
|
}),
|
|
|
|
|
});
|
|
|
|
|
const data = await res.json() as Record<string, unknown>;
|
|
|
|
|
log("document-load", data);
|
|
|
|
|
|
|
|
|
|
if (data.status === "processing") {
|
|
|
|
|
pass(`Document load triggered for ${docId.slice(0, 8)}...`);
|
|
|
|
|
|
|
|
|
|
// Clean up the test document
|
|
|
|
|
await post("/api/v1/librarian", {
|
|
|
|
|
operation: "remove-document",
|
|
|
|
|
documentId: docId,
|
|
|
|
|
user: "test-user",
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
fail("Document load", "unexpected response");
|
|
|
|
|
return false;
|
|
|
|
|
} catch (err) {
|
|
|
|
|
fail("Document load", err);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-06 00:37:03 -05:00
|
|
|
// ─── Agent Test ───────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
async function testAgentQuery(): Promise<boolean> {
|
|
|
|
|
try {
|
|
|
|
|
console.log("\n Sending agent request (may take a few seconds)...");
|
|
|
|
|
const model = process.env.LLM_MODEL ?? "qwen2.5:0.5b";
|
|
|
|
|
const res = await post("/api/v1/flow/default/service/agent", {
|
|
|
|
|
question: "What is the capital of France?",
|
|
|
|
|
model,
|
|
|
|
|
});
|
|
|
|
|
log("agent", res);
|
|
|
|
|
const r = res as Record<string, unknown>;
|
|
|
|
|
// Agent sends streaming chunks — gateway returns the first/final response
|
|
|
|
|
if (r.chunk_type || r.answer || r.content) {
|
|
|
|
|
pass("Agent returned a response");
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
if (r.error) {
|
|
|
|
|
// Agent may error if no graph data — that's OK, proves routing works
|
|
|
|
|
const err = r.error as Record<string, unknown>;
|
|
|
|
|
pass(`Agent responded with error (routing works): ${err.message ?? err.type}`);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
fail("Agent", "unexpected response format");
|
|
|
|
|
return false;
|
|
|
|
|
} catch (err) {
|
|
|
|
|
fail("Agent", err);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
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>
2026-04-05 23:41:39 -05:00
|
|
|
// ─── 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);
|
|
|
|
|
|
2026-04-06 23:47:43 -05:00
|
|
|
// Document pipeline load test (requires librarian + gateway)
|
|
|
|
|
if (process.env.SKIP_PIPELINE !== "1" && process.env.SKIP_LIBRARIAN !== "1") {
|
|
|
|
|
console.log("\n (Testing document load — set SKIP_PIPELINE=1 to skip)");
|
|
|
|
|
await run("Document Load", testDocumentLoad);
|
|
|
|
|
} else {
|
|
|
|
|
console.log("\n (Skipping document pipeline load test)");
|
|
|
|
|
}
|
|
|
|
|
|
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>
2026-04-05 23:41:39 -05:00
|
|
|
// 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)");
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-06 00:37:03 -05:00
|
|
|
// Librarian tests (only if librarian service is running)
|
|
|
|
|
if (process.env.SKIP_LIBRARIAN !== "1") {
|
|
|
|
|
console.log("\n (Testing librarian — set SKIP_LIBRARIAN=1 to skip)");
|
|
|
|
|
await run("Librarian Add", testLibrarianAdd);
|
|
|
|
|
await run("Librarian List", testLibrarianList);
|
|
|
|
|
await run("Librarian Get Content", testLibrarianGetContent);
|
|
|
|
|
await run("Librarian Delete", testLibrarianDelete);
|
|
|
|
|
} else {
|
|
|
|
|
console.log("\n (SKIP_LIBRARIAN=1 — skipping librarian tests)");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Agent test (only if agent + LLM services are running)
|
|
|
|
|
if (process.env.SKIP_AGENT !== "1" && process.env.SKIP_LLM !== "1") {
|
|
|
|
|
console.log("\n (Testing agent — set SKIP_AGENT=1 to skip)");
|
|
|
|
|
await run("Agent Query", testAgentQuery);
|
|
|
|
|
} else {
|
|
|
|
|
console.log("\n (Skipping agent test)");
|
|
|
|
|
}
|
|
|
|
|
|
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>
2026-04-05 23:41:39 -05:00
|
|
|
console.log("\n══════════════════════════════════════════════════");
|
|
|
|
|
console.log(` Results: ${passed} passed, ${failed} failed`);
|
|
|
|
|
console.log("══════════════════════════════════════════════════\n");
|
|
|
|
|
|
|
|
|
|
process.exit(failed > 0 ? 1 : 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
main();
|