oxedyne/daimond/dev/verify_steerqueue.mjs
8.4 KiB, 1 run
created by r2519314175:699, 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_steerqueue.mjs — a steer typed while the daimon is working is kept. |
| 2 | // |
| 3 | // ── WHY ────────────────────────────────────────────────────────────────────── |
| 4 | // |
| 5 | // `doSteer` opened with `if (crystalBusy || !currentDiamond) return;`. So a Send |
| 6 | // pressed while a turn was in flight did NOTHING: no queue, no message, no |
| 7 | // sentence saying why, and the text left sitting in the box. An ordinary chat in |
| 8 | // exactly that state queues what you typed and draws it (`enqueueMessage`), and |
| 9 | // a Diamond's own queue already existed — `drainSteerQueue` empties it at the end |
| 10 | // of every turn and `resumeSteerQueue` when you come back to the Diamond. This |
| 11 | // was the one door that never put anything into it. |
| 12 | // |
| 13 | // It is worse than it first reads, because `crystalBusy` is ONE FLAG FOR THE |
| 14 | // WHOLE APP. The turn in flight may belong to a different Diamond entirely, so a |
| 15 | // Send could do nothing on a Diamond that was not itself doing anything, with |
| 16 | // nothing on that screen to explain it. |
| 17 | // |
| 18 | // ── THE PROPERTY ───────────────────────────────────────────────────────────── |
| 19 | // |
| 20 | // Nothing the user types is lost. Not "the queue exists" — that it CARRIES what |
| 21 | // was typed, and that the box is cleared so the app has visibly taken it. The |
| 22 | // second half matters: a queue that kept the text in the box as well would leave |
| 23 | // the user unable to tell whether it had been taken, and pressing Send again is |
| 24 | // how you get it twice. |
| 25 | // |
| 26 | // A PRESET IS NOT QUEUED, and that is asserted too. A gather round, a trigger and |
| 27 | // the conductor all steer with a preset and each has its own way back; queueing |
| 28 | // one would deliver a stale report at an arbitrary later turn. So the fix must |
| 29 | // keep the old silent return for exactly that case, and a check that only proved |
| 30 | // "typed text is queued" would let a later simplification break it. |
| 31 | // |
| 32 | // node dev/verify_steerqueue.mjs |
| 33 | // node dev/verify_steerqueue.mjs --break swallow # the old silent return |
| 34 | // |
| 35 | // A `--break` run EXPECTS to fail: exit 0 when something reddened, 1 when |
| 36 | // nothing did. |
| 37 | // |
| 38 | // Needs dev/serve.mjs (DAIMOND_PORT, default 8777) and dev/mockllm.mjs |
| 39 | // (DAIMOND_MOCK_PORT, default 9099). No gateway. |
| 40 | |
| 41 | import fs from 'node:fs'; |
| 42 | import path from 'node:path'; |
| 43 | import { fileURLToPath } from 'node:url'; |
| 44 | import { open, steerDiamond, shot, errors } from './harness.mjs'; |
| 45 | |
| 46 | const HERE = path.dirname(fileURLToPath(import.meta.url)); |
| 47 | const WWW = path.join(HERE, '..', 'www'); |
| 48 | |
| 49 | const ok = [], bad = []; |
| 50 | const check = (name, pass, detail) => { |
| 51 | (pass ? ok : bad).push(name + (detail ? ' — ' + detail : '')); |
| 52 | console.log((pass ? ' ok ' : ' FAIL ') + name + (detail ? ' — ' + detail : '')); |
| 53 | }; |
| 54 | const sleep = ms => new Promise(r => setTimeout(r, ms)); |
| 55 | |
| 56 | const BREAK = (() => { const i = process.argv.indexOf('--break'); return i > 0 ? process.argv[i + 1] : ''; })(); |
| 57 | const BREAKS = { |
| 58 | // The defect, restored exactly: busy means do nothing at all. |
| 59 | swallow: [{ |
| 60 | file: 'js/daimond.js', |
| 61 | find: '\t\tif (diamondBusy(currentDiamond.id)) {', |
| 62 | with: '\t\tif (diamondBusy(currentDiamond.id)) { return; } if (false) {', |
| 63 | }], |
| 64 | // THERE IS NO `queueall` BREAK, AND THAT IS A FINDING RATHER THAN A GAP. |
| 65 | // |
| 66 | // Check 3 asserts a preset is not queued, and no break makes it go red. Two |
| 67 | // were tried. Deleting the preset guard changes nothing, because `!typed` |
| 68 | // already turns every preset away — a preset call carries no box text. |
| 69 | // Rewriting the enqueue as `typed || presetArg` changes nothing either, and |
| 70 | // that one was measured: the door is reachable and the turn IS in flight when |
| 71 | // it is taken (check 3a proves both), and the preset still never lands. |
| 72 | // |
| 73 | // So the honest reading is that the guard is defence in depth over a state |
| 74 | // that cannot currently be constructed, and check 3 is a REGRESSION guard |
| 75 | // rather than a proved one. It is kept, and it is labelled, because the state |
| 76 | // becomes constructible the moment anything queues something other than what |
| 77 | // the box holds. Writing a break that reddened it would have meant damaging |
| 78 | // the check instead of the code, which is the shape this suite exists to |
| 79 | // refuse. |
| 80 | }; |
| 81 | |
| 82 | function damagedFiles() { |
| 83 | const byFile = new Map(); |
| 84 | for (const spec of (BREAKS[BREAK] || [])) { |
| 85 | const src = fs.readFileSync(path.join(WWW, spec.file), 'utf8'); |
| 86 | if (!src.includes(spec.find)) { |
| 87 | console.error(`--break ${BREAK}: anchor not found in ${spec.file}. The break is stale.`); |
| 88 | process.exit(1); |
| 89 | } |
| 90 | byFile.set(spec.file, src.replace(spec.find, spec.with)); |
| 91 | } |
| 92 | return byFile; |
| 93 | } |
| 94 | |
| 95 | async function serveBreaks(page) { |
| 96 | if (!BREAK) return; |
| 97 | for (const [file, body] of damagedFiles()) { |
| 98 | await page.route('**/' + file, r => r.fulfill({ |
| 99 | status: 200, contentType: 'application/javascript', body, |
| 100 | })); |
| 101 | } |
| 102 | } |
| 103 | |
| 104 | const s = await open({ name: 'steerqueue', route: serveBreaks }); |
| 105 | const p = s.page; |
| 106 | |
| 107 | // Open a Diamond and put it on the chat face, which is where the composer is. |
| 108 | const picked = await p.evaluate(() => { |
| 109 | const el = document.querySelector('#diamond-list [data-id]'); |
| 110 | if (!el) return ''; |
| 111 | el.click(); |
| 112 | return el.dataset.id || ''; |
| 113 | }); |
| 114 | check('0 a Diamond is open', !!picked, picked); |
| 115 | await sleep(800); |
| 116 | |
| 117 | // `@slow` holds the reply for two seconds, which is the turn-in-flight window |
| 118 | // this whole file needs. Racing a fast mock would make the check pass or fail on |
| 119 | // how loaded the machine is, which is not a property of the app. |
| 120 | await steerDiamond(s, '@slow 4000 first'); |
| 121 | await sleep(700); |
| 122 | |
| 123 | const busy = await p.evaluate(() => { |
| 124 | const b = document.getElementById('chat-fold-btn'); |
| 125 | return { foldDisabled: !!(b && b.disabled) }; |
| 126 | }); |
| 127 | check('1 a turn really is in flight', busy.foldDisabled === true, |
| 128 | 'the Fold button answers `crystalBusy`, so its disabled state is the flag'); |
| 129 | |
| 130 | // ── The typed steer, arriving while that turn runs ─────────────────────────── |
| 131 | const TYPED = 'kept-' + Math.random().toString(36).slice(2, 9); |
| 132 | await p.fill('#chat-input', TYPED); |
| 133 | await p.click('#chat-send', { force: true }); |
| 134 | await sleep(600); |
| 135 | |
| 136 | const after = await p.evaluate(() => ({ |
| 137 | box: (document.getElementById('chat-input') || {}).value || '', |
| 138 | queued: document.body.innerText, |
| 139 | })); |
| 140 | check('2a WHAT WAS TYPED IS KEPT, not swallowed', after.queued.includes(TYPED), |
| 141 | after.queued.includes(TYPED) ? '' : 'nothing on screen carries it'); |
| 142 | check('2b and the box is cleared, so the app has visibly taken it', |
| 143 | after.box.trim() === '', JSON.stringify(after.box.slice(0, 40))); |
| 144 | await shot(s, 'steerqueue-held'); |
| 145 | |
| 146 | // ── A preset must NOT be queued ────────────────────────────────────────────── |
| 147 | // |
| 148 | // Driven through `doSteer` itself with a preset, which is how a gather round, a |
| 149 | // trigger and the conductor all reach it. The queue must not grow. |
| 150 | const PRESET = 'preset-' + Math.random().toString(36).slice(2, 9); |
| 151 | const drove = await p.evaluate((t) => { |
| 152 | const has = !!(window.DaimondCore && typeof window.DaimondCore.steer === 'function'); |
| 153 | const busy = !!(document.getElementById('chat-fold-btn') || {}).disabled; |
| 154 | if (has) window.DaimondCore.steer(t); |
| 155 | return { has, busy }; |
| 156 | }, PRESET); |
| 157 | check('3a the preset door exists and a turn is still in flight when it is taken', |
| 158 | drove.has === true && drove.busy === true, JSON.stringify(drove)); |
| 159 | await sleep(600); |
| 160 | const presetSeen = await p.evaluate((t) => document.body.innerText.includes(t), PRESET); |
| 161 | // Labelled UNPROVED because no break reddens it — see the note beside BREAKS. |
| 162 | // A check nobody can make fail is worth keeping and is not worth trusting, and |
| 163 | // the name is where a reader finds that out. |
| 164 | check('3 a preset is not queued (regression guard, unproved — see BREAKS)', |
| 165 | presetSeen === false, presetSeen ? 'the preset reached the queue' : ''); |
| 166 | |
| 167 | const errs = errors(s).filter(e => !/502|401|Account service|favicon/.test(e)); |
| 168 | check('4 nothing was raised in the console', errs.length === 0, errs.slice(0, 2).join(' | ')); |
| 169 | |
| 170 | await s.close(); |
| 171 | |
| 172 | console.log(`\n${ok.length} passed, ${bad.length} failed`); |
| 173 | if (BREAK) { |
| 174 | console.log(bad.length ? `--break ${BREAK}: reddened ${bad.length} check(s), as it must` |
| 175 | : `--break ${BREAK}: CHANGED NOTHING — the check it names is not testing what it says`); |
| 176 | process.exit(bad.length ? 0 : 1); |
| 177 | } |
| 178 | process.exit(bad.length ? 1 : 0); |