diff options
| author | Agando <roduccomm@gmail.com> | 2026-11-06 11:45:21 +0100 |
|---|---|---|
| committer | Agando <roduccomm@gmail.com> | 2026-11-06 11:45:21 +0100 |
| commit | 03143a6e88832d06c7b08cdbf8de96ba79bda7d6 (patch) | |
| tree | a82db0d323e36aec94587332b263e2be9fd062c3 /modules/test | |
| parent | 15b42d6379484bb3f439eb5d7f77f12877f74497 (diff) | |
release(v4.6.150): commit the module work that has been live but uncommitted
Operator commit-approve, 2026-11-06. These changes have been served since
2026-11-02/03 while git HEAD still stamped v4.6.146, so the mirror published a
tree four builds behind the running instance and the release tag named the
wrong build.
module-orchestrator.js — orchestrate-parallel reported success it could not
prove. Results are now recorded by sub-task id through a resultSink handed to
delegateTask (harvest matched by currentTask/lastTask strings, which a later
delegation on the same agent overwrote, losing finished work as "agent not
found"); the tally counts summaries with status ok instead of
Promise.allSettled resolutions (a spawn refusal fulfils with undefined and used
to print green); and a busy agent is no longer reused at pool cap, where two
siblings erased each other's lease. Guard:
modules/test/test-orchestrator-parallel-harvest.js (16 cases, reproduces the
one-agent three-sub-task cap case that printed 3/3 succeeded).
module-billing.js — the balance said "not signed in — newOS4 sends no session
token", which was already false: the browser sends the HttpOnly cookie and
parse_bearer_token() falls back to it. `cost` is on by default (the flag was
opt-in and nobody knew the command existed; `cost off` opts out) and the
command now prints today's platform spend from GET /api/billing/usage?days=1
alongside the session estimate.
modules/ext/webdav.js — the inline WebView detector missed "Version/4.0"
Android WebViews, including the operator's own phone, so the login offered a
Telegram widget that cannot complete there. It now delegates to
modules/wx11-tg-webview.js when the page loaded it and keeps a matching
fallback (wv token, Version/4.0-Android, Telegram, FBAN/Instagram/Line).
modules/ext/media.js — text analysis model renamed deepseek-v4-flash ->
deepseek-v4.1-flash in both the field and the printed label.
main.html + module-loader.js — v4.6.146 -> v4.6.150 in LATEST_VER and the three
asset queries; core-bundle.js rebuilt (rebuild-bundles.py reports it already
in sync, 1,250,569 bytes).
Verified: node --check on all six scripts and the new test; the 16-case
orchestrator guard passes against the deployed bundle (ORCH_SRC=...); bundle
rebuild is a no-op; core-bundle.js?v=v4.6.150 answers 200.
Diffstat (limited to 'modules/test')
| -rw-r--r-- | modules/test/test-orchestrator-parallel-harvest.js | 132 |
1 files changed, 132 insertions, 0 deletions
diff --git a/modules/test/test-orchestrator-parallel-harvest.js b/modules/test/test-orchestrator-parallel-harvest.js new file mode 100644 index 0000000..4381699 --- /dev/null +++ b/modules/test/test-orchestrator-parallel-harvest.js @@ -0,0 +1,132 @@ +// 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); +})(); |