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). |
| 22 | import { open, chat, signInAs, clearMockLog, mockLog, errors } from './harness.mjs'; |
| 23 | |
| 24 | const ok = [], bad = []; |
| 25 | const check = (name, pass, detail) => { |
| 26 | (pass ? ok : bad).push(name + (detail ? ' — ' + detail : '')); |
| 27 | console.log((pass ? ' ok ' : ' FAIL ') + name + (detail ? ' — ' + detail : '')); |
| 28 | }; |
| 29 | |
| 30 | const NAME = 'foldreload'; |
| 31 | const MARK = 'ZEBRAFISH-THE-FIRST-THING-SAID'; |
| 32 | const s = await open({ name: NAME }); |
| 33 | const 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. |
| 38 | await p.evaluate(() => { |
| 39 | window.DaimondPricing.contextWindow = function () { return 3000; }; |
| 40 | }); |
| 41 | |
| 42 | clearMockLog(); |
| 43 | await 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. |
| 48 | for (let i = 0; i < 14; i++) { |
| 49 | await chat(s, '@text turn ' + i + ' ' + 'padding '.repeat(30)); |
| 50 | } |
| 51 | |
| 52 | const folded = await p.evaluate(() => ({ |
| 53 | onScreen: !!document.querySelector('.chat-msg-compacted'), |
| 54 | text: (document.querySelector('.chat-msg-compacted') || {}).innerText || '', |
| 55 | })); |
| 56 | check('the conversation was folded, and the user was told where it happened', |
| 57 | folded.onScreen, folded.text.replace(/\n/g, ' / ').slice(0, 110)); |
| 58 | check('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 | |
| 62 | const before = mockLog(); |
| 63 | const 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. |
| 68 | const isNotice = (m) => String(m.content || '').startsWith('[Daimond folded the earlier part'); |
| 69 | const stillThere = (req) => (req.messages || []).some(m => !isNotice(m) && String(m.content || '').includes(MARK)); |
| 70 | check('and the first thing said is no longer in the request', |
| 71 | !stillThere(lastBefore), `${(lastBefore.messages || []).length} messages`); |
| 72 | check('while the notice that replaced it is', |
| 73 | JSON.stringify(lastBefore.messages || []).includes('Daimond folded the earlier part')); |
| 74 | |
| 75 | // ── The reload ──────────────────────────────────────────────────────────── |
| 76 | |
| 77 | await p.reload({ waitUntil: 'domcontentloaded' }); |
| 78 | await p.waitForTimeout(1200); |
| 79 | await signInAs(s, NAME); |
| 80 | await 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. |
| 83 | await p.evaluate(() => { |
| 84 | const b = [...document.querySelectorAll('#session-list .chat-box')][0]; |
| 85 | if (b) b.click(); |
| 86 | }); |
| 87 | await p.waitForSelector('#chat-input', { state: 'visible', timeout: 10000 }); |
| 88 | await p.waitForTimeout(600); |
| 89 | |
| 90 | const drawn = await p.evaluate(() => !!document.querySelector('.chat-msg-compacted')); |
| 91 | check('the fold is still drawn in the reloaded thread', drawn); |
| 92 | |
| 93 | clearMockLog(); |
| 94 | await chat(s, '@text after the reload'); |
| 95 | const after = mockLog(); |
| 96 | const lastAfter = after[after.length - 1] || { messages: [] }; |
| 97 | check('the reloaded chat sent something', after.length > 0, `${after.length} requests`); |
| 98 | check('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`); |
| 101 | check('and the notice survived the reload with it', |
| 102 | JSON.stringify(lastAfter.messages || []).includes('Daimond folded the earlier part')); |
| 103 | |
| 104 | const errs = errors(s).filter(e => !/502|Bad Gateway/.test(e)); |
| 105 | check('nothing threw', errs.length === 0, errs.slice(0, 2).join(' | ')); |
| 106 | |
| 107 | await s.close(); |
| 108 | console.log(`\n${ok.length} passed, ${bad.length} failed`); |
| 109 | if (bad.length) { bad.forEach(b => console.log(' FAILED: ' + b)); process.exit(1); } |