aboutsummaryrefslogtreecommitdiff
path: root/modules/test
diff options
context:
space:
mode:
authorAgando <roduccomm@gmail.com>2026-11-02 21:21:24 +0100
committerAgando <roduccomm@gmail.com>2026-11-02 21:21:24 +0100
commit4587c757b17d8a92df7c98ec37ace4c5500a4f5a (patch)
tree4b89cdf25fb7f2354cc8299260b1ddafeb568f25 /modules/test
parent08a77451f503f531eee44c69c14bed3a2961cc5c (diff)
fix(newos4): read_file pages were silently truncated; delegation no longer invents "completed"
Two defects found by re-deriving a report that claimed both were still open. read_file paging (module-aiassist.js) - A page larger than the result clip was clipped AFTER read_file had computed next_offset. On the last page next_offset is null, so the model was told truncated:false while its tail had been removed: measured 11 890 of 19 999 chars delivered, returned:19999, no pointer to the rest - 8 109 chars lost with no way to ask for them. * the clip now sets truncated=true and next_offset=offset+keep when it removes the tail of a page that had no continuation (and never splits a surrogate pair) * read_file caps a page at the result budget (min(pageCap, toolResultChars-200)), so a page is always delivered whole and next_offset is exactly contiguous Live v4.6.143: 19 999-char file -> page 11 800 (next_offset 11 800), page 2 8 199 (next_offset null): 11 800 + 8 199 = 19 999, nothing withheld. A model asking for limit:32000 is clamped to 11 800 and told truncated:true. The ~500-char symptom was the 500-char floor: a caller passing limit:200 gets 500, which is why targeted sed -n reads looked better than the tool. Delegation reporting (module-orchestrator.js, module-aiassist.js) - chat() returning {content} was already fixed in 8a9333b, but three other places still substituted the word "completed" for an absent answer, so the symptom survived: delegateTask's outbox wrote status:'completed' even when the worker returned nothing; the delegate_task tool returned {success:true, result:"completed"}; orchestrate-parallel summarised an unmatched agent as {status:'ok', result:'completed (agent recycled)'} and a missing outbox as "completed (no outbox)". - Now: an empty/undefined answer is status 'failed' with error 'agent produced no answer' / 'no result in outbox' / 'agent not found — result unverifiable', and the tool returns success:false with result:null. Tests: test-delegation-reporting.js 6 (RED 3/6 against HEAD: D3/D4 success:true, D5 the literal "completed"), test-tool-context-bound 26 with E6-E8 (RED 24/2: truncated:false, next_offset null). maintain 27/27.
Diffstat (limited to 'modules/test')
-rw-r--r--modules/test/test-delegation-reporting.js57
-rw-r--r--modules/test/test-tool-context-bound.js17
2 files changed, 74 insertions, 0 deletions
diff --git a/modules/test/test-delegation-reporting.js b/modules/test/test-delegation-reporting.js
new file mode 100644
index 0000000..7d4854e
--- /dev/null
+++ b/modules/test/test-delegation-reporting.js
@@ -0,0 +1,57 @@
+// node modules/test/test-delegation-reporting.js
+// A delegated task must report the worker's answer, and must report FAILURE when there is
+// no answer. Three places used to substitute the literal "completed": delegateTask's
+// outbox, the delegate_task tool, and orchestrate-parallel's per-sub-task summary.
+const fs = require('fs'), path = require('path');
+const ROOT = __dirname + '/../core';
+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.indexedDB = { open() { throw new Error('no idb'); }, deleteDatabase() { throw new Error('no idb'); } };
+global.window = global;
+global.addEventListener = () => {};
+
+const AI = new Function(fs.readFileSync(path.join(ROOT, 'module-aiassist.js'), 'utf8') + '\nreturn WebX11AIAssistantModule;')();
+
+let fails = 0, passes = 0;
+const ok = (n, c, d) => { if (c) { passes++; return; } fails++; console.log('FAIL ' + n + (d ? ' — ' + d : '')); };
+
+function mk(orch) {
+ const stub = {
+ print: () => {}, log: () => {}, setAiBusy: () => {}, markDirty: () => {},
+ setPrompt: () => {}, clearInput: () => {}, scrollToBottom: () => {}, refreshPrompt: () => {},
+ ollamaManager: null,
+ fs: { mkdirRecursive: () => {}, writeFile: () => true, readFile: () => null, exists: () => false, list: () => [] },
+ addEventListener: () => {}, removeEventListener: () => {},
+ };
+ stub.moduleManager = { modules: { OrchestratorModule: orch } };
+ const ai = new AI();
+ ai.terminal = stub;
+ return ai;
+}
+
+(async () => {
+ // D1/D2: an answer comes back as the answer, and success is true
+ const withAnswer = mk({ delegateTask: async () => 'the worker answer' });
+ const r1 = JSON.parse(await withAnswer.runTool('delegate_task', JSON.stringify({ task: 'do a thing' })));
+ ok('D1 the answer is the worker answer', r1.result === 'the worker answer', JSON.stringify(r1));
+ ok('D2 success is true when there is an answer', r1.success === true, JSON.stringify(r1.success));
+
+ // D3/D4: no answer must NOT be reported as completed
+ const silent = mk({ delegateTask: async () => '' });
+ const r2 = JSON.parse(await silent.runTool('delegate_task', JSON.stringify({ task: 'do a thing' })));
+ ok('D3 an empty answer reports success=false', r2.success === false, JSON.stringify(r2));
+ ok('D4 an empty answer is not called "completed"', r2.result === null && /no answer/i.test(r2.error || ''), JSON.stringify(r2));
+
+ // D5: undefined (an older orchestrator, or a rejected internal path) is also not success
+ const undef = mk({ delegateTask: async () => undefined });
+ const r3 = JSON.parse(await undef.runTool('delegate_task', JSON.stringify({ task: 'do a thing' })));
+ ok('D5 undefined is a failure, not a completion', r3.success === false, JSON.stringify(r3));
+
+ // D6: an object answer (the shape chat() now returns) is unwrapped, not stringified
+ const objAnswer = mk({ delegateTask: async () => 'plain string from orchestrator' });
+ const r4 = JSON.parse(await objAnswer.runTool('delegate_task', JSON.stringify({ task: 'x' })));
+ ok('D6 the tool keeps the answer verbatim', r4.result === 'plain string from orchestrator', JSON.stringify(r4));
+
+ console.log('test-delegation-reporting: ' + passes + ' passed, ' + fails + ' failed');
+ process.exit(fails ? 1 : 0);
+})();
diff --git a/modules/test/test-tool-context-bound.js b/modules/test/test-tool-context-bound.js
index 0b42eeb..848b307 100644
--- a/modules/test/test-tool-context-bound.js
+++ b/modules/test/test-tool-context-bound.js
@@ -103,6 +103,23 @@ function ok(name, cond, detail) {
ok('E4 clip shortens content', parsed && parsed.content.length < 16000 && parsed.content.length >= 3000,
parsed && String(parsed.content.length));
ok('E5 clipped envelope fits the limit', clipped.length <= 6000 + 400, String(clipped.length));
+
+ // E6-E8: the last page of a file has next_offset=null. If the clip removes its tail,
+ // telling the caller "truncated: false" hides the remainder with no way to ask for it
+ // (measured 2026-11-02: 8 109 of 19 999 chars withheld, returned claimed 19 999).
+ const lastPage = JSON.stringify({
+ path: '/big.txt', offset: 0, returned: 19999, total_chars: 19999,
+ truncated: false, next_offset: null,
+ content: 'y'.repeat(19999),
+ });
+ const lc = ai._clipToolResult(lastPage);
+ let lp = null;
+ try { lp = JSON.parse(lc); } catch (e) {}
+ ok('E6 a clipped last page reports truncated=true', lp && lp.truncated === true, lp && String(lp.truncated));
+ ok('E7 it gives a resumable next_offset', lp && lp.next_offset === lp.returned_chars,
+ lp && String(lp.next_offset) + ' vs ' + String(lp.returned_chars));
+ ok('E8 returned no longer claims the whole file', lp && lp.returned === lp.returned_chars,
+ lp && String(lp.returned));
}
// F. budgets are resolvable per model class, with precedence call arg > profile > ai-config > default