Oregami
Repositories/oxedyne/daimond

oxedyne/daimond/dev/verify_foldreload.mjs

5.4 KiB, 1 run

created by r2519314175:421, 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_foldreload.mjs — does a folded conversation stay folded across a reload?
2//
3// Compaction replaces the earlier part of a conversation with one notice when it
4// outgrows the model's context window. It reported that the fold did not survive a
5// reload: `restore` rebuilt the session from the FULL, untouched screen transcript,
6// so a reloaded chat sprang back to its unfolded size and paid for another summary
7// on the next turn — for ever, since nothing it did could make the transcript
8// smaller.
9//
10// The two halves interlock. The fold notice is deliberately a `user`-role message
11// because `restore` drops system ones; and the session store this asserts against
12// is what makes the fold durable, because what it stores is the agent's own message
13// list AFTER folding, not the screen's before it.
14//
15// So: three things, read from the mock provider's own log.
16// - the fold happened, and the user was told
17// - what it replaced is gone from the request
18// - after a reload, still gone, and the notice is still there
19//
20// Needs dev/serve.mjs (DAIMOND_PORT, default 8777) and dev/mockllm.mjs
21// (DAIMOND_MOCK_PORT, default 9099).
22import { open, chat, signInAs, clearMockLog, mockLog, errors } from './harness.mjs';
23
24const ok = [], bad = [];
25const check = (name, pass, detail) => {
26 (pass ? ok : bad).push(name + (detail ? ' — ' + detail : ''));
27 console.log((pass ? ' ok ' : ' FAIL ') + name + (detail ? ' — ' + detail : ''));
28};
29
30const NAME = 'foldreload';
31const MARK = 'ZEBRAFISH-THE-FIRST-THING-SAID';
32const s = await open({ name: NAME });
33const p = s.page;
34
35// A window small enough that a handful of turns will not fit. Stubbed on the
36// pricing table, which is where `ensureApp` asks — the app's own path is exercised,
37// including the setter that carries the figure into the wasm.
38await p.evaluate(() => {
39 window.DaimondPricing.contextWindow = function () { return 3000; };
40});
41
42clearMockLog();
43await chat(s, '@text ' + MARK);
44// Many small turns rather than a few large ones. `tail_start` keeps at least
45// MIN_KEEP_MESSAGES (6), so a conversation of a few enormous messages has nothing
46// it is allowed to cut and compaction falls back to shortening instead — which is
47// a real behaviour, but not the one this is about.
48for (let i = 0; i < 14; i++) {
49 await chat(s, '@text turn ' + i + ' ' + 'padding '.repeat(30));
50}
51
52const folded = await p.evaluate(() => ({
53 onScreen: !!document.querySelector('.chat-msg-compacted'),
54 text: (document.querySelector('.chat-msg-compacted') || {}).innerText || '',
55}));
56check('the conversation was folded, and the user was told where it happened',
57 folded.onScreen, folded.text.replace(/\n/g, ' / ').slice(0, 110));
58check('the notice says what it is and how much went, not merely that something happened',
59 /folded/i.test(folded.text) && /\d+\s+earlier messages/i.test(folded.text),
60 folded.text.replace(/\n/g, ' / ').slice(0, 140));
61
62const before = mockLog();
63const lastBefore = before[before.length - 1] || { messages: [] };
64// Outside the fold notice. The mock's summariser echoes its own prompt back, so
65// the notice quotes the conversation it replaced verbatim — a fixture artefact, and
66// a real summary would not. What is asked is whether the ORIGINAL messages are
67// still being sent, so the notice is excluded from the search.
68const isNotice = (m) => String(m.content || '').startsWith('[Daimond folded the earlier part');
69const stillThere = (req) => (req.messages || []).some(m => !isNotice(m) && String(m.content || '').includes(MARK));
70check('and the first thing said is no longer in the request',
71 !stillThere(lastBefore), `${(lastBefore.messages || []).length} messages`);
72check('while the notice that replaced it is',
73 JSON.stringify(lastBefore.messages || []).includes('Daimond folded the earlier part'));
74
75// ── The reload ────────────────────────────────────────────────────────────
76
77await p.reload({ waitUntil: 'domcontentloaded' });
78await p.waitForTimeout(1200);
79await signInAs(s, NAME);
80await p.waitForTimeout(1000);
81// The stub does not survive a reload, and must not: the point is that the FOLD
82// survives, not that the window is still small.
83await p.evaluate(() => {
84 const b = [...document.querySelectorAll('#session-list .chat-box')][0];
85 if (b) b.click();
86});
87await p.waitForSelector('#chat-input', { state: 'visible', timeout: 10000 });
88await p.waitForTimeout(600);
89
90const drawn = await p.evaluate(() => !!document.querySelector('.chat-msg-compacted'));
91check('the fold is still drawn in the reloaded thread', drawn);
92
93clearMockLog();
94await chat(s, '@text after the reload');
95const after = mockLog();
96const lastAfter = after[after.length - 1] || { messages: [] };
97check('the reloaded chat sent something', after.length > 0, `${after.length} requests`);
98check('what the fold replaced does NOT come back',
99 !stillThere(lastAfter),
100 stillThere(lastAfter) ? 'the conversation sprang back to full size' : `${(lastAfter.messages || []).length} messages`);
101check('and the notice survived the reload with it',
102 JSON.stringify(lastAfter.messages || []).includes('Daimond folded the earlier part'));
103
104const errs = errors(s).filter(e => !/502|Bad Gateway/.test(e));
105check('nothing threw', errs.length === 0, errs.slice(0, 2).join(' | '));
106
107await s.close();
108console.log(`\n${ok.length} passed, ${bad.length} failed`);
109if (bad.length) { bad.forEach(b => console.log(' FAILED: ' + b)); process.exit(1); }