// node modules/test/test-orchestrator-parallel-harvest.js // Defect 2b (2026-11-02/03): orchestrate-parallel reported success it could not prove. // // Three separate lies, all in one block: // 1. The harvest re-found each sub-task's agent by task STRING (`currentTask === sub.goal || // lastTask === sub.goal`). An agent that had already moved on to another sub-task no longer // carried the old goal, so a task that had demonstrably finished was dropped as // "agent not found — result unverifiable". // 2. The "N/N succeeded" header counted Promise.allSettled resolutions, not outcomes — a spawn // refusal returns terminal.print() (undefined), which FULFILS, so a batch where nothing ran // printed green. // 3. At pool cap, delegateTask stole a BUSY agent, so two sub-tasks overwrote // currentTask/leaseId and erased each other's results. // // This file pins the contract: results are harvested by sub-task id, the tally comes from the // summaries, and a sub-task that could not get an agent is a failure, never a green row. const fs = require('fs'), path = require('path'); const ROOT = path.join(__dirname, '..', 'core'); // Point ORCH_SRC at modules/core/core-bundle.js to run these assertions against the DEPLOYED // artifact instead of the source file — a green source test says nothing about the bundle. const SRC_FILE = process.env.ORCH_SRC ? path.resolve(process.env.ORCH_SRC) : path.join(ROOT, 'module-orchestrator.js'); const store = {}; global.localStorage = { getItem: k => (k in store ? store[k] : null), setItem: (k, v) => { store[k] = String(v); }, removeItem: k => { delete store[k]; } }; global.window = global; global.addEventListener = () => {}; const Orch = new Function(fs.readFileSync(SRC_FILE, 'utf8') + '\nreturn OrchestratorModule;')(); let fails = 0, passes = 0; const ok = (n, c, d) => { if (c) { passes++; return; } fails++; console.log('FAIL ' + n + (d ? ' — ' + d : '')); }; // A VFS stub that behaves like the real one where it matters: paths normalise, and // fsReadJSON returns null (never throws) for a missing/corrupt file. function mkEnv(opts) { const prints = []; const files = new Map(); const norm = p => String(p).replace(/^\/+/, ''); const term = { prints, print(msg, type) { prints.push({ msg: String(msg), type: type || '' }); }, fs: { readFile: p => (files.has(norm(p)) ? files.get(norm(p)) : null), writeFile: (p, c) => { files.set(norm(p), c); return true; }, readDir: () => true, mkdir: () => true, rm: () => true, fsReadJSON: p => { try { const raw = files.get(norm(p)); return raw === undefined ? null : JSON.parse(raw); } catch (e) { return null; } }, }, moduleManager: { modules: {}, _setInterval: () => 0, _setTimeout: () => 0 }, ollamaManager: null, }; const ai = { _sessions: new Map(), chat: opts.chat, terminal: term }; term.moduleManager.modules['WebX11AIAssistantModule'] = ai; const orch = Object.create(Orch.prototype); orch.terminal = term; orch.commands = {}; orch._agents = {}; orch._a2aMessages = {}; orch._eventBus = {}; orch._restartBackoffs = {}; orch._delegations = 0; orch._SPAWN_LIMIT = opts.spawnLimit || 5; orch._RESTART_POLICY = 'none'; return { orch, term, files, prints }; } const GOAL = 'read alpha log and write beta report and check gamma status'; const tick = (ms) => new Promise(r => setTimeout(r, ms || 5)); (async () => { // ── T1: three independent sub-tasks are harvested by identity, not by task string ── { const { orch, files, prints } = mkEnv({ chat: async (msgs) => { await tick(); return { content: 'ANSWER[' + msgs[0] + ']', toolTurns: 0 }; }, }); const merged = await orch.delegateTask(GOAL, { parallel: true }); const decomposed = prints.some(p => /Decomposed into 3 sub-tasks/.test(p.msg)); ok('T1a the goal decomposes into 3 sub-tasks (harness precondition)', decomposed, prints.map(p => p.msg).join(' | ')); ok('T1b all three sub-tasks are harvested', merged.summaries.length === 3, 'summaries=' + merged.summaries.length); ok('T1c every sub-task carries its own answer', merged.summaries.every(s => s.status === 'ok' && /^ANSWER\[\[DELEGATED\]/.test(s.result || '')), JSON.stringify(merged.summaries.map(s => [s.status, s.result]))); ok('T1d no sub-task is discarded as unverifiable', !merged.summaries.some(s => /agent not found/.test(s.error || '')), JSON.stringify(merged.summaries.map(s => s.error))); ok('T1e successes come from the summaries', merged.successes === 3 && merged.failures === 0, 'successes=' + merged.successes + ' failures=' + merged.failures); const onDisk = JSON.parse(files.get('orchestrator/parallel-results.json')); ok('T1f the persisted payload agrees with the returned tally', onDisk.successes === merged.successes && onDisk.summaries.length === 3, JSON.stringify({ onDisk: onDisk.successes, merged: merged.successes })); ok('T1g each summary is keyed by its own sub-task id', new Set(merged.summaries.map(s => s.taskId)).size === 3, JSON.stringify(merged.summaries.map(s => s.taskId))); } // ── T2: at pool cap a sub-task is REFUSED, never run on a busy agent ── // Old behaviour: all three ran on the one agent, clobbering each other, and the header // still read "3/3 succeeded" while two summaries said failed. { const { orch, prints } = mkEnv({ spawnLimit: 1, chat: async () => { await tick(10); return { content: 'single-agent answer', toolTurns: 0 }; }, }); const merged = await orch.delegateTask(GOAL, { parallel: true }); const oks = merged.summaries.filter(s => s.status === 'ok').length; ok('T2a only the one agent that was free produced a result', oks === 1 && merged.successes === 1, 'ok=' + oks + ' successes=' + merged.successes); ok('T2b the refused sub-tasks are reported as failures with a reason', merged.failures === 2 && merged.summaries.filter(s => s.status === 'failed').every(s => /idle|busy|limit/i.test(s.error || '')), JSON.stringify(merged.summaries.map(s => [s.status, s.error]))); ok('T2c the tally never exceeds the green rows', !merged.summaries.some(s => s.status !== 'ok') || merged.successes < merged.total, 'successes=' + merged.successes + '/' + merged.total); const header = prints.map(p => p.msg).find(m => /Parallel batch:/.test(m)) || ''; ok('T2d the header reports the real outcome', /1\/3 succeeded, 2 failed/.test(header), header); ok('T2e a batch with failures is not printed as a green header', !/^✅/.test(header), header); } // ── T3: a worker that answers nothing is a failure, not a success ── { const { orch } = mkEnv({ chat: async (msgs) => { await tick(); return { content: /beta/.test(msgs[0]) ? '' : 'ok answer', toolTurns: 0 }; }, }); const merged = await orch.delegateTask(GOAL, { parallel: true }); const silent = merged.summaries.filter(s => /beta/.test(s.task)); ok('T3a the silent worker is a failed row', silent.length === 1 && silent[0].status === 'failed', JSON.stringify(silent)); ok('T3b successes count the real answers only', merged.successes === 2 && merged.failures === 1, 'successes=' + merged.successes + ' failures=' + merged.failures); } // ── T4: a thrown sub-task is not swallowed, and does not inflate the tally ── { const { orch } = mkEnv({ chat: async (msgs) => { await tick(); if (/gamma/.test(msgs[0])) throw new Error('worker exploded'); return { content: 'fine', toolTurns: 0 }; }, }); const merged = await orch.delegateTask(GOAL, { parallel: true }); const bad = merged.summaries.filter(s => /gamma/.test(s.task)); ok('T4a the throwing sub-task is a failed row with its error', bad.length === 1 && bad[0].status === 'failed' && /worker exploded/.test(bad[0].error || ''), JSON.stringify(bad)); ok('T4b the tally reflects 2 real answers out of 3', merged.successes === 2 && merged.failures === 1, 'successes=' + merged.successes + ' failures=' + merged.failures); } console.log('test-orchestrator-parallel-harvest: ' + passes + ' passed, ' + fails + ' failed'); process.exit(fails ? 1 : 0); })();