Oregami
Repositories/oxedyne/daimond

oxedyne/daimond/dev/verify_continue_resume.mjs

11.3 KiB, 1 run

created by r2519314175:315, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1// verify_continue_resume.mjs — Continue RESUMES a turn, it does not re-run the prompt.
2//
3// THE DEFECT (dev/PERSISTENCE_STUDY.md §1.3 and §4.4 item 2). `continueTurn` used to tombstone
4// every message of the interrupted turn — the partial reply included — drop `chat.app`, and call
5// `runTurn(chat, text)` with the ORIGINAL prompt. Output tokens the user had already paid for were
6// thrown away and bought again, and the answer they had been READING was replaced by a different
7// one.
8//
9// THE FIX. When something arrived, the partial STAYS as an ordinary assistant message, its badge
10// comes off, `chat.app` is nulled so `ensureApp` rebuilds the session from the messages that remain
11// — which now end with the partial — and the model is asked to carry on with `CONTINUE_NUDGE`. Only
12// the pre-token case, where nothing arrived and nothing was billed, still re-runs the prompt.
13//
14// WHAT THIS FILE PROVES, without a browser or a model: that `continueTurn` builds a RESUME payload
15// carrying the partial (the partial kept, not tombstoned, and the continuation dispatched rather
16// than the original prompt), that the empty case still re-runs, and that the idempotency guards
17// hold. The real function is LIFTED from www/js/daimond.js and run against stubbed dependencies, so
18// a change to the shipped logic is what this measures.
19//
20// The visible append itself — the continuation text landing after the retained partial — is
21// runTurn's job and is covered in a real page by dev/verify_dropped.mjs and dev/verify_predrop.mjs.
22// Here the claim is the DISPATCH: partial retained + continue message, not a bare re-run.
23//
24// PROVED AGAINST THE PRE-FIX DISPATCH. `--break rerun` flips the one deciding line — the resume
25// branch's `runTurn(chat, CONTINUE_NUDGE)` back to `runTurn(chat, text)`, which is what the old
26// code did — and runs the SAME checks. The two dispatch assertions in section A then fail, which
27// is the proof they bite on the resume and not on something incidental:
28//
29// node dev/verify_continue_resume.mjs --break rerun # the resume-dispatch checks fail
30// node dev/verify_continue_resume.mjs # and then, clean
31//
32// node dev/verify_continue_resume.mjs
33//
34// Needs nothing running.
35import fs from 'node:fs';
36import path from 'node:path';
37import { fileURLToPath } from 'node:url';
38
39const HERE = path.dirname(fileURLToPath(import.meta.url));
40const SRC = fs.readFileSync(path.join(HERE, '..', 'www', 'js', 'daimond.js'), 'utf8');
41
42const BREAK = (() => {
43 const i = process.argv.indexOf('--break');
44 return i > 0 ? String(process.argv[i + 1] || '') : '';
45})();
46if (BREAK && BREAK !== 'rerun') {
47 console.error(`unknown break '${BREAK}'; the only one is 'rerun'`);
48 process.exit(2);
49}
50
51let bad = 0, ran = 0;
52const check = (pass, name, detail) => {
53 ran++;
54 if (!pass) bad++;
55 console.log((pass ? ' ok ' : ' FAIL ') + name + (detail ? ' — ' + detail : ''));
56};
57
58// ── Lift the real function and its nudge out of the source ───
59
60/// A `var NAME = …;` statement, however many lines its right-hand side spans, up to the first `;`
61/// that ends the statement. `CONTINUE_NUDGE` is a multi-line string concatenation with no `;`
62/// inside its literals.
63function grabVarStmt(name) {
64 const start = SRC.indexOf('var ' + name + ' =');
65 if (start < 0) { console.error(`could not find 'var ${name}' in js/daimond.js`); process.exit(2); }
66 const end = SRC.indexOf(';', start);
67 return SRC.slice(start, end + 1);
68}
69
70/// A function declaration by signature, brace-matched from its opening `{`.
71function grabFn(sig) {
72 const start = SRC.indexOf(sig);
73 if (start < 0) { console.error(`could not find '${sig}' in js/daimond.js`); process.exit(2); }
74 const open = SRC.indexOf('{', start);
75 let depth = 0, i = open;
76 for (; i < SRC.length; i++) {
77 const c = SRC[i];
78 if (c === '{') depth++;
79 else if (c === '}') { depth--; if (depth === 0) { i++; break; } }
80 }
81 return SRC.slice(start, i);
82}
83
84const NUDGE_STMT = grabVarStmt('CONTINUE_NUDGE');
85const CT_ORIG = grabFn('function continueTurn('); // the pristine lift, for the sentinels
86
87// The source actually built. `--break rerun` flips the one deciding line to the pre-fix dispatch;
88// the sentinels above still read CT_ORIG, so under break only the resume-dispatch checks fail.
89let CT_SRC = CT_ORIG;
90if (BREAK === 'rerun') {
91 const from = 'runTurn(chat, CONTINUE_NUDGE);';
92 if (CT_SRC.split(from).length !== 2) {
93 console.error("the line '--break rerun' patches is not in continueTurn exactly once");
94 process.exit(2);
95 }
96 CT_SRC = CT_SRC.replace(from, 'runTurn(chat, text);');
97 console.log('\n*** RUNNING UNDER --break rerun: the resume-dispatch failures below are the point ***\n');
98}
99
100// Sentinels: a mis-lift or a rename must fail here, not silently test nothing. Read the pristine
101// lift, so `--break rerun` leaves these green and trips only the dispatch checks in section A.
102check(/CONTINUE_NUDGE/.test(CT_ORIG),
103 'the lifted continueTurn references CONTINUE_NUDGE');
104check(/runTurn\(chat, CONTINUE_NUDGE\)/.test(CT_ORIG),
105 'and dispatches the continuation with runTurn(chat, CONTINUE_NUDGE)');
106check(/runTurn\(chat, text\)/.test(CT_ORIG),
107 'and keeps runTurn(chat, text) for the empty-partial case');
108
109/// Build the real `continueTurn` with its free identifiers supplied as stubs. Everything else it
110/// uses is a parameter or a local.
111function makeContinueTurn(stubs) {
112 const names = ['loadMsgTombs', 'msgTombstone', 'touchChat', 'persistChats', 'renderHistory', 'runTurn'];
113 const f = new Function(
114 ...names,
115 NUDGE_STMT + '\n' + CT_SRC + '\nreturn continueTurn;');
116 return f(...names.map((n) => stubs[n]));
117}
118
119/// A spy set with sensible defaults; a test overrides `loadMsgTombs` where it needs to.
120function spies(over) {
121 const calls = { runTurn: [], msgTombstone: [] };
122 const s = {
123 loadMsgTombs: () => ({}),
124 msgTombstone: (mids) => { calls.msgTombstone.push(mids); },
125 touchChat: () => {},
126 persistChats: () => {},
127 renderHistory: () => {},
128 runTurn: (chat, text) => { calls.runTurn.push({ chat, text }); },
129 };
130 Object.assign(s, over || {});
131 return { stubs: s, calls };
132}
133
134/// A fresh interrupted turn: the user's prompt and a half-written assistant answer, both tagged
135/// with the same `iturn`.
136function freshChat(partialText) {
137 return {
138 _generating: false,
139 app: { marker: 1 },
140 messages: [
141 { role: 'user', iturn: 'T1', mid: 'u1', content: 'What is the capital of France?' },
142 { role: 'assistant', iturn: 'T1', mid: 'a1', interrupted: true, why: 'offline', content: partialText },
143 ],
144 };
145}
146
147// The nudge value the real statement defines, to compare dispatch against it.
148const NUDGE = new Function(NUDGE_STMT + '\nreturn CONTINUE_NUDGE;')();
149
150console.log('the nudge is a real continuation instruction, distinct from any prompt');
151check(typeof NUDGE === 'string' && NUDGE.length > 0, 'CONTINUE_NUDGE is a non-empty string');
152check(NUDGE !== 'What is the capital of France?',
153 'and it is not the original prompt', JSON.stringify(NUDGE.slice(0, 40)));
154
155// ── A: something arrived → RESUME ────────────────────────────
156console.log('\nA turn with a partial resumes, carrying the partial');
157{
158 const { stubs, calls } = spies();
159 const ct = makeContinueTurn(stubs);
160 const chat = freshChat('The capital of France is');
161 ct(chat, 'T1', 'What is the capital of France?');
162
163 check(calls.runTurn.length === 1 && calls.runTurn[0].text === NUDGE,
164 'runTurn is called with the CONTINUE nudge',
165 calls.runTurn.length ? JSON.stringify(String(calls.runTurn[0].text).slice(0, 40)) : 'not called');
166 check(calls.runTurn.length === 1 && calls.runTurn[0].text !== 'What is the capital of France?',
167 'and NOT with the original prompt — this is the whole fix');
168 check(calls.msgTombstone.length === 0,
169 'the partial is NOT tombstoned — it is being kept, not deleted from other devices');
170 const asst = chat.messages.find((m) => m.mid === 'a1');
171 check(!!asst && asst.content === 'The capital of France is',
172 'the partial text is retained intact for the model to continue from',
173 asst ? JSON.stringify(asst.content) : 'gone');
174 check(!!asst && !('interrupted' in asst) && !('why' in asst),
175 'and its badge is cleared — it is now an ordinary answer');
176 check(chat.app === null,
177 'chat.app is nulled so ensureApp rebuilds the session ENDING with the partial');
178 check(chat.messages.filter((m) => m.role === 'user').length === 1,
179 'the original prompt stays in the thread exactly once');
180}
181
182// ── B: nothing arrived → RE-RUN (the one right re-run) ───────
183console.log('\na turn that died before the first token re-runs the prompt');
184{
185 const { stubs, calls } = spies();
186 const ct = makeContinueTurn(stubs);
187 const chat = freshChat(''); // no token ever came back
188 ct(chat, 'T1', 'What is the capital of France?');
189
190 check(calls.runTurn.length === 1 && calls.runTurn[0].text === 'What is the capital of France?',
191 'runTurn is called with the ORIGINAL prompt — nothing to continue from',
192 calls.runTurn.length ? JSON.stringify(String(calls.runTurn[0].text).slice(0, 40)) : 'not called');
193 check(calls.runTurn.length === 1 && calls.runTurn[0].text !== NUDGE,
194 'and NOT with the continue nudge — a model cannot continue an empty answer');
195 check(calls.msgTombstone.length === 1,
196 'the empty turn IS tombstoned, so the append-only merge cannot resurrect it beside the retry');
197 check(chat.messages.filter((m) => m.iturn === 'T1').length === 0,
198 'and its messages are dropped from this tab');
199}
200
201// ── C-F: the idempotency and safety guards ───────────────────
202console.log('\nthe guards that stop a double-run or a wipe');
203{
204 // C. Already tombstoned by another tab: do nothing.
205 const { stubs, calls } = spies({ loadMsgTombs: () => ({ u1: true, a1: true }) });
206 const ct = makeContinueTurn(stubs);
207 ct(freshChat('The capital of France is'), 'T1', 'What is the capital of France?');
208 check(calls.runTurn.length === 0 && calls.msgTombstone.length === 0,
209 'C: an already-continued turn does nothing — cross-tab idempotence');
210}
211{
212 // D. No turn id: the filter would match every message, so the guard must fire FIRST.
213 const { stubs, calls } = spies();
214 const ct = makeContinueTurn(stubs);
215 ct(freshChat('The capital of France is'), undefined, 'What is the capital of France?');
216 check(calls.runTurn.length === 0,
217 'D: a missing iturn is refused — never a whole-transcript wipe');
218}
219{
220 // E. A turn already running: withTurnLock's partner guard.
221 const { stubs, calls } = spies();
222 const ct = makeContinueTurn(stubs);
223 const chat = freshChat('The capital of France is');
224 chat._generating = true;
225 ct(chat, 'T1', 'What is the capital of France?');
226 check(calls.runTurn.length === 0,
227 'E: a chat mid-generation is not continued on top of itself');
228}
229{
230 // F. No prompt at all: the `!text` guard.
231 const { stubs, calls } = spies();
232 const ct = makeContinueTurn(stubs);
233 ct(freshChat('The capital of France is'), 'T1', '');
234 check(calls.runTurn.length === 0, 'F: an empty prompt argument is refused');
235}
236
237// ── The count is pinned ──────────────────────────────────────
238const EXPECTED = 20;
239const ranBefore = ran;
240check(ranBefore === EXPECTED,
241 `exactly ${EXPECTED} checks ran — a displaced case trips this`,
242 `ran ${ranBefore}`);
243
244console.log(bad ? `\n${bad} check(s) FAILED` : '\nall checks passed');
245process.exit(bad ? 1 : 0);