/** * 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 full flow definition with all service topic mappings const res = await post("/api/v1/config", { operation: "put", keys: ["flows"], values: { default: { topics: { // 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 "text-completion-request": "tg.flow.text-completion-request", "text-completion-response": "tg.flow.text-completion-response", // Prompt service "prompt-request": "tg.flow.prompt-request", "prompt-response": "tg.flow.prompt-response", // Graph RAG "graph-rag-request": "tg.flow.graph-rag-request", "graph-rag-response": "tg.flow.graph-rag-response", // Document RAG "document-rag-request": "tg.flow.document-rag-request", "document-rag-response": "tg.flow.document-rag-response", // Triples query "triples-request": "tg.flow.triples-request", "triples-response": "tg.flow.triples-response", // Agent "agent-request": "tg.flow.agent-request", "agent-response": "tg.flow.agent-response", // 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", }, }, }, }); 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; } } // ─── Librarian Tests ────────────────────────────────────────────────── let testDocId = ""; async function testLibrarianAdd(): Promise { 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; const meta = r.documentMetadata as Record | 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 { try { const res = await post("/api/v1/librarian", { operation: "list-documents", user: "test-user", }); log("librarian/list", res); const r = res as Record; 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 { 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; 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 { 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; 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; } } // ─── Document Load Test ────────────────────────────────────────────── async function testDocumentLoad(): Promise { 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; const meta = addRes.documentMetadata as Record | 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; 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; } } // ─── Agent Test ─────────────────────────────────────────────────────── async function testAgentQuery(): Promise { 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; // 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; 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; } } // ─── 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); // 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)"); } // 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)"); } // 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)"); } console.log("\n══════════════════════════════════════════════════"); console.log(` Results: ${passed} passed, ${failed} failed`); console.log("══════════════════════════════════════════════════\n"); process.exit(failed > 0 ? 1 : 0); } main();