22 * Live E2E: Global Research Swarm — Real LLM + Dockerized Services
33 *
44 * These tests run the complete distributed pipeline end-to-end:
5- * - 4 services in Docker: Redis, Gateway, Agent nodes (searcher×4, writer, reviewer, editor)
5+ * - Services in Docker: Redis, Gateway, Agent nodes (searcher×4, writer, reviewer, editor)
66 * - Real LLM calls via OpenRouter/OpenAI (no mocks)
77 * - Orchestrator run as compiled Node.js subprocess (dist/examples/...)
8- * - AUTO_PUBLISH=1 skips the readline HITL prompt
8+ * - AUTO_PUBLISH=1 skips the readline HITL prompt (decision forced to PUBLISH)
99 *
1010 * Run with:
1111 * npm run test:e2e:live
1212 *
1313 * Requires:
1414 * OPENROUTER_API_KEY or OPENAI_API_KEY in .env
1515 * Docker running
16+ * (On networks whose DNS blocks nom.telemetrydeck.com, also set
17+ * KAIBAN_TELEMETRY_OPT_OUT=1 in .env — see .env.example.)
18+ *
19+ * Assertions target the v2.0 orchestrator's output contract
20+ * (examples/global-research/orchestrator.ts + phases.ts):
21+ * - stdout phase banners: STEP 1 Fan-Out → STEP 2 Fan-In → STEP 3 governance
22+ * → STEP 4 editorial → HUMAN DECISION REQUIRED (HITL)
23+ * - phase summaries: "SEARCH PHASE COMPLETE — n/N results",
24+ * "SYNTHESIS COMPLETE (n chars)", "Compliance Score: … Recommendation: …"
25+ * - economics/metadata: "Tokens used:", "Estimated cost: $", "Active nodes:"
26+ * - the machine-readable run log (RunLogger): "Run log saved to <path>" — the
27+ * JSON's `outcome` field is the authoritative terminal verdict
28+ * (PUBLISHED | REVISED | REJECTED | FAILED | STOPPED).
29+ *
30+ * A governance REJECTED verdict is a legitimate terminal state (the reviewer
31+ * gate doing its job on live LLM output), so scenarios accept it as a clean
32+ * stop — but when the pipeline proceeds, the full editorial + HITL + PUBLISHED
33+ * chain is asserted strictly.
1634 *
1735 * Scenarios:
1836 * 1. Golden Path — full pipeline (search → write → review → edit → publish)
1937 * 2. Governance output — structured compliance review present in output
2038 * 3. ResearchContext — metadata fields (nodes, tokens, cost) reported
21- * 4. Fault tolerance — CHAOS_MODE: some searchers crash , pipeline still completes
39+ * 4. Fault tolerance — partial searcher failure tolerated , pipeline completes
2240 */
2341import { describe , it , expect } from "vitest" ;
2442import { spawnSync } from "child_process" ;
43+ import { readFileSync } from "fs" ;
2544import { resolve } from "path" ;
2645import * as dotenv from "dotenv" ;
2746
@@ -63,58 +82,90 @@ function runOrchestrator(
6382 } ;
6483}
6584
85+ /** Shape of the RunLogger JSON flushed to examples/global-research/runs/ */
86+ interface RunLogFile {
87+ query : string ;
88+ contextId : string ;
89+ numSearchers : number ;
90+ tasks : Array < { phase : string ; agentId : string ; outputTokens : number } > ;
91+ errors : unknown [ ] ;
92+ totalTokens : number ;
93+ totalCost : number ;
94+ outcome : string ;
95+ }
96+
97+ /** Parse the "Run log saved to <path>" line and load the JSON run log. */
98+ function readRunLog ( stdout : string ) : RunLogFile {
99+ const m = stdout . match ( / R u n l o g s a v e d t o ( .+ \. j s o n ) / ) ;
100+ expect ( m , "orchestrator must flush a run log" ) . not . toBeNull ( ) ;
101+ return JSON . parse ( readFileSync ( ( m as RegExpMatchArray ) [ 1 ] , "utf8" ) ) as RunLogFile ;
102+ }
103+
104+ /** True when the governance gate rejected the report (a legitimate clean stop). */
105+ function governanceRejected ( stdout : string ) : boolean {
106+ return / G o v e r n a n c e r e v i e w R E J E C T E D t h e r e p o r t \. W o r k f l o w s t o p p e d \. / . test (
107+ stdout ,
108+ ) ;
109+ }
110+
111+ function echo ( label : string , stdout : string , stderr = "" ) : void {
112+ console . log ( `\n── Orchestrator stdout (${ label } ) ──────────────────────────` ) ;
113+ console . log ( stdout . slice ( 0 , 6000 ) ) ;
114+ if ( stderr . trim ( ) ) {
115+ console . log ( "\n── stderr ────────────────────────────────────────────────" ) ;
116+ console . log ( stderr . slice ( 0 , 1000 ) ) ;
117+ }
118+ }
119+
66120// ── Test suite ────────────────────────────────────────────────────────────
67121
68122describe (
69123 "Live E2E: Global Research Swarm (Real LLM + Docker)" ,
70124 { timeout : 600_000 } ,
71125 ( ) => {
72126 // ─────────────────────────────────────────────────────────────────────
73- // Scenario 1 — Golden Path: all 4 stages complete and research publishes
127+ // Scenario 1 — Golden Path: all stages complete and the report publishes
74128 // ─────────────────────────────────────────────────────────────────────
75129 it ( "Scenario 1 — Golden Path: search → write → governance → editorial → PUBLISHED" , ( ) => {
76130 const { stdout, stderr, status } = runOrchestrator (
77131 baseEnv ( {
78132 QUERY : "Distributed AI Agent Systems in 2025" ,
79133 } ) ,
80134 ) ;
81-
82- console . log (
83- "\n── Orchestrator stdout (Scenario 1) ──────────────────────────" ,
84- ) ;
85- console . log ( stdout . slice ( 0 , 6000 ) ) ;
86- if ( stderr . trim ( ) ) {
87- console . log (
88- "\n── stderr ────────────────────────────────────────────────────" ,
89- ) ;
90- console . log ( stderr . slice ( 0 , 1000 ) ) ;
91- }
135+ echo ( "Scenario 1" , stdout , stderr ) ;
92136
93137 // Gateway reachable
94138 expect ( stdout ) . toMatch ( / G a t e w a y : O K | G a t e w a y : U P / i) ;
95139
96- // Fan-out search phase
97- expect ( stdout ) . toMatch ( / F a n - O u t .* S e a r c h e r | S T E P 1 / i) ;
98- expect ( stdout ) . toMatch ( / S e a r c h t a s k \d + \/ \d + q u e u e d / ) ;
99- expect ( stdout ) . toMatch ( / S E A R C H P H A S E C O M P L E T E / ) ;
100- expect ( stdout ) . toMatch ( / S u c c e e d e d : [ 1 - 9 ] / ) ;
101-
102- // Fan-in write phase
103- expect ( stdout ) . toMatch ( / F a n - I n .* W r i t e r | S T E P 2 / i) ;
104- expect ( stdout ) . toMatch ( / S Y N T H E S I S C O M P L E T E / ) ;
105-
106- // Governance review
107- expect ( stdout ) . toMatch ( / S T E P 3 / i) ;
108- expect ( stdout ) . toMatch ( / G O V E R N A N C E R E V I E W B Y S A G E / ) ;
109- expect ( stdout ) . toMatch ( / C o m p l i a n c e S c o r e : / ) ;
110- expect ( stdout ) . toMatch ( / R e c o m m e n d a t i o n : / ) ;
111-
112- // Editorial + HITL (auto-published)
113- expect ( stdout ) . toMatch ( / S T E P 4 / i) ;
114- expect ( stdout ) . toMatch ( / E D I T O R I A L R E V I E W B Y M O R G A N / ) ;
115- expect ( stdout ) . toMatch (
116- / A U T O _ P U B L I S H .* a u t o - a p p r o v i n g | R E S E A R C H P U B L I S H E D / i,
117- ) ;
140+ // Fan-out search phase: banner, generated sub-topics, ≥1 of 2 results
141+ expect ( stdout ) . toMatch ( / S T E P 1 — F a n - O u t : 2 S e a r c h e r n o d e s / ) ;
142+ expect ( stdout ) . toMatch ( / S u b - t o p i c s : [ \s \S ] * 1 \. / ) ;
143+ expect ( stdout ) . toMatch ( / S E A R C H P H A S E C O M P L E T E — [ 1 - 2 ] \/ 2 r e s u l t s / ) ;
144+
145+ // Fan-in write phase: non-empty synthesis
146+ expect ( stdout ) . toMatch ( / S T E P 2 — F a n - I n / ) ;
147+ expect ( stdout ) . toMatch ( / S Y N T H E S I S C O M P L E T E \( [ 1 - 9 ] \d * c h a r s \) / ) ;
148+
149+ // Governance review: score + structured recommendation
150+ expect ( stdout ) . toMatch ( / S T E P 3 / ) ;
151+ expect ( stdout ) . toMatch ( / C o m p l i a n c e S c o r e : \S + + R e c o m m e n d a t i o n : ( A P P R O V E D | C O N D I T I O N A L | R E J E C T E D ) / ) ;
152+
153+ const log = readRunLog ( stdout ) ;
154+ if ( governanceRejected ( stdout ) ) {
155+ // The governance gate stopping a weak report IS correct system behavior.
156+ expect ( log . outcome ) . toBe ( "REJECTED" ) ;
157+ } else {
158+ // Editorial + HITL (AUTO_PUBLISH forces the PUBLISH decision)
159+ expect ( stdout ) . toMatch ( / S T E P 4 / ) ;
160+ expect ( stdout ) . toMatch ( / E d i t o r i a l : + \S + + — R e c o m m e n d a t i o n : / ) ;
161+ expect ( stdout ) . toMatch ( / H U M A N D E C I S I O N R E Q U I R E D \( H I T L \) / ) ;
162+ expect ( stdout ) . toMatch ( / B o a r d : F I N I S H E D / ) ;
163+ expect ( log . outcome ) . toBe ( "PUBLISHED" ) ;
164+ }
165+
166+ // Machine-readable verdict: real tokens were spent across the fleet
167+ expect ( log . totalTokens ) . toBeGreaterThan ( 0 ) ;
168+ expect ( log . tasks . length ) . toBeGreaterThanOrEqual ( 1 ) ;
118169
119170 // Clean exit
120171 expect ( status ) . toBe ( 0 ) ;
@@ -129,26 +180,23 @@ describe(
129180 QUERY : "AI Safety and Alignment Research" ,
130181 } ) ,
131182 ) ;
183+ echo ( "Scenario 2" , stdout ) ;
132184
133- console . log (
134- "\n── Orchestrator stdout (Scenario 2) ──────────────────────────" ,
135- ) ;
136- console . log ( stdout . slice ( 0 , 4000 ) ) ;
137-
138- // Must reach governance stage
139- expect ( stdout ) . toMatch ( / G O V E R N A N C E R E V I E W B Y S A G E / ) ;
185+ // Must reach the governance stage
186+ expect ( stdout ) . toMatch ( / S T E P 3 — S a g e \( R e v i e w e r \) r u n n i n g g o v e r n a n c e c o m p l i a n c e c h e c k / ) ;
140187
141- // Compliance score present
142- expect ( stdout ) . toMatch ( / C o m p l i a n c e S c o r e : / i) ;
143-
144- // Recommendation is one of the valid values
145- expect ( stdout ) . toMatch ( / A P P R O V E D | C O N D I T I O N A L | R E J E C T E D / ) ;
188+ // Compliance score + structured recommendation present
189+ expect ( stdout ) . toMatch ( / C o m p l i a n c e S c o r e : \S + + R e c o m m e n d a t i o n : ( A P P R O V E D | C O N D I T I O N A L | R E J E C T E D ) / ) ;
146190
147191 // Either continues to editorial (approved/conditional) or stops cleanly (rejected)
148- const reachedEditorial = / E D I T O R I A L R E V I E W B Y M O R G A N / . test ( stdout ) ;
149- const rejectedByGovernance =
150- / G o v e r n a n c e r e v i e w R E J E C T E D | W o r k f l o w s t o p p e d / i. test ( stdout ) ;
151- expect ( reachedEditorial || rejectedByGovernance ) . toBe ( true ) ;
192+ const reachedEditorial = / S T E P 4 — M o r g a n \( E d i t o r \) / . test ( stdout ) ;
193+ expect ( reachedEditorial || governanceRejected ( stdout ) ) . toBe ( true ) ;
194+
195+ // The run log records the governance task with real token usage
196+ const log = readRunLog ( stdout ) ;
197+ const govTask = log . tasks . find ( ( t ) => t . phase === "governance" ) ;
198+ expect ( govTask , "governance phase must be recorded in the run log" ) . toBeDefined ( ) ;
199+ expect ( govTask ?. outputTokens ) . toBeGreaterThan ( 0 ) ;
152200
153201 expect ( status ) . toBe ( 0 ) ;
154202 } , 600_000 ) ;
@@ -162,55 +210,52 @@ describe(
162210 QUERY : "Large Language Models and Autonomous Agents" ,
163211 } ) ,
164212 ) ;
213+ echo ( "Scenario 3" , stdout ) ;
165214
166- console . log (
167- "\n── Orchestrator stdout (Scenario 3) ──────────────────────────" ,
168- ) ;
169- console . log ( stdout . slice ( 0 , 4000 ) ) ;
170-
171- // ResearchContext populated message
172- expect ( stdout ) . toMatch (
173- / R e s e a r c h C o n t e x t p o p u l a t e d w i t h \d + s e a r c h r e s u l t s / ,
174- ) ;
215+ // Search results flowed into the shared ResearchContext
216+ expect ( stdout ) . toMatch ( / S E A R C H P H A S E C O M P L E T E — [ 1 - 2 ] \/ 2 r e s u l t s / ) ;
175217
176- // Active nodes tracked
177- expect ( stdout ) . toMatch ( / A c t i v e n o d e s : / ) ;
218+ // Economics/metadata report: tokens, cost, active nodes, context id
219+ expect ( stdout ) . toMatch ( / T o k e n s u s e d : + [ 1 - 9 ] \d * / ) ;
220+ expect ( stdout ) . toMatch ( / E s t i m a t e d c o s t : + \$ \d + \. \d + / ) ;
221+ expect ( stdout ) . toMatch ( / A c t i v e n o d e s : + .* w r i t e r / ) ;
222+ expect ( stdout ) . toMatch ( / C o n t e x t I D : [ 0 - 9 a - f - ] { 36 } / ) ;
178223
179- // If published: economics report section
180- if ( / R E S E A R C H P U B L I S H E D / . test ( stdout ) ) {
181- expect ( stdout ) . toMatch ( / N o d e s A c t i v e : / ) ;
182- expect ( stdout ) . toMatch ( / S t a r t e d : / ) ;
183- expect ( stdout ) . toMatch ( / C o m p l e t e d : / ) ;
184- }
224+ // Machine-readable metadata mirrors the CLI report
225+ const log = readRunLog ( stdout ) ;
226+ expect ( log . totalTokens ) . toBeGreaterThan ( 0 ) ;
227+ expect ( log . totalCost ) . toBeGreaterThan ( 0 ) ;
228+ expect ( log . contextId ) . toMatch ( / ^ [ 0 - 9 a - f - ] { 36 } $ / ) ;
185229
186230 expect ( status ) . toBe ( 0 ) ;
187231 } , 600_000 ) ;
188232
189233 // ─────────────────────────────────────────────────────────────────────
190- // Scenario 4 — Chaos Mode: ~20% crash rate, pipeline still completes
234+ // Scenario 4 — Fault tolerance: partial searcher failure tolerated
191235 // ─────────────────────────────────────────────────────────────────────
192236 it ( "Scenario 4 — Chaos Mode: searcher crashes tolerated, pipeline completes" , ( ) => {
193- // CHAOS_MODE is passed via env to the searcher containers via docker-compose
194- // but for the orchestrator we just verify it handles partial failures gracefully.
195- // We use NUM_SEARCHERS=3 so there is headroom even if 1 searcher fails permanently.
237+ // CHAOS_MODE crash-injection is a property of the searcher *containers*
238+ // (set via docker-compose env at stack-up time). From the orchestrator's
239+ // side we verify the fan-out phase degrades gracefully: with 3 sub-topics
240+ // across the searcher pool, the pipeline must complete with at least one
241+ // successful search result even if individual searchers fail or restart.
196242 const { stdout, status } = runOrchestrator (
197243 baseEnv ( {
198244 QUERY : "Fault Tolerance in Distributed AI Systems" ,
199245 NUM_SEARCHERS : "3" ,
200246 } ) ,
201247 ) ;
248+ echo ( "Scenario 4" , stdout ) ;
202249
203- console . log (
204- "\n── Orchestrator stdout (Scenario 4) ──────────────────────────" ,
205- ) ;
206- console . log ( stdout . slice ( 0 , 4000 ) ) ;
207-
208- // Search phase must complete (with at least 1 result)
209- expect ( stdout ) . toMatch ( / S E A R C H P H A S E C O M P L E T E / ) ;
210- expect ( stdout ) . toMatch ( / S u c c e e d e d : [ 1 - 9 ] / ) ;
250+ // Search phase must complete with at least 1 of 3 results
251+ expect ( stdout ) . toMatch ( / S E A R C H P H A S E C O M P L E T E — [ 1 - 3 ] \/ 3 r e s u l t s / ) ;
211252
212253 // Writer must have received enough data to proceed
213- expect ( stdout ) . toMatch ( / S Y N T H E S I S C O M P L E T E | R e s e a r c h C o n t e x t p o p u l a t e d / ) ;
254+ expect ( stdout ) . toMatch ( / S Y N T H E S I S C O M P L E T E \( [ 1 - 9 ] \d * c h a r s \) / ) ;
255+
256+ // Terminal verdict is a legitimate outcome, with no unhandled errors
257+ const log = readRunLog ( stdout ) ;
258+ expect ( [ "PUBLISHED" , "REVISED" , "REJECTED" ] ) . toContain ( log . outcome ) ;
214259
215260 expect ( status ) . toBe ( 0 ) ;
216261 } , 600_000 ) ;
0 commit comments