oxedyne/daimond/dev/verify_creditage.mjs
23.0 KiB, 1 run
created by r2519314175:321, 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_creditage.mjs — the provider credit figure, kept honest instead of stale. |
| 2 | // |
| 3 | // THE COMPLAINT. The author added $100 to his OpenRouter account and Daimond went on showing |
| 4 | // the old number for hours: "if my Daimond is just sitting there for hours after I added OR |
| 5 | // credit, I feel like it should be showing an honest credit level." The figure was probed at |
| 6 | // exactly three moments — unlock, a pasted key, and the "Ask again" button — and nothing else |
| 7 | // ever asked again. |
| 8 | // |
| 9 | // A displayed balance goes wrong two ways and they want opposite treatments: |
| 10 | // |
| 11 | // THE USER SPENT. Daimond watched them do it; the turn and its cost are in the ledger |
| 12 | // already. So the figure is walked down locally, with NO REQUEST AT ALL, and the next probe |
| 13 | // replaces it outright. That is the frequent case and it must cost nothing. |
| 14 | // |
| 15 | // THE USER TOPPED UP. Nothing in the browser can know that; only a probe finds it. So the |
| 16 | // probe happens when the tab comes back to the front, on a beat while it is in front, and |
| 17 | // when the Models panel is opened — every one of them behind ONE floor, because the user's |
| 18 | // own rate limit is not free and the way to find out what OpenRouter allows is not to |
| 19 | // hammer it. |
| 20 | // |
| 21 | // AND THE FIGURE CARRIES ITS AGE, because a probe that fails writes nothing and keeps the |
| 22 | // old number — which is right, no balance beats a wrong one — and silence and freshness |
| 23 | // look identical on screen without it. |
| 24 | // |
| 25 | // HOW THIS IS DRIVEN, and why you can believe it. |
| 26 | // |
| 27 | // * EVERY PROBE ASSERTION IS AT THE NETWORK. `openrouter.ai/api/v1/key` and `/credits` are |
| 28 | // intercepted and counted. A check that counted internal calls could not see a request |
| 29 | // that never went, nor one that went twice. |
| 30 | // * EVERY CHECK IS PROVED RED. The same scenario is run five more times against a patched |
| 31 | // `models.js` served from the dev server — the floor removed, the beat left running while |
| 32 | // hidden, the age line silenced, the decrement disabled, the "who can answer" test made to |
| 33 | // say everyone — and the check each patch breaks is asserted to FAIL in that run. A check |
| 34 | // that passes on the broken build is reported as a broken check. |
| 35 | // * TIME is Playwright's fake clock, installed before the page loads, so the five-minute |
| 36 | // floor and a three-hour age are exercised in seconds rather than approximated. |
| 37 | // * VISIBILITY is the one thing stood in for. Headless Chrome reports every page visible, |
| 38 | // whatever is in front of it (measured: a second tab brought to front leaves |
| 39 | // `visibilityState` at "visible", and neither `Emulation.setPageVisibilityOverride` nor |
| 40 | // `Page.setWebLifecycleState` moves it), so the SIGNAL is faked in an init script and the |
| 41 | // real `visibilitychange` event is dispatched. Everything downstream of it — the handler, |
| 42 | // the gate, the request — is the shipped code, and what is measured is the request. |
| 43 | // |
| 44 | // node dev/verify_creditage.mjs |
| 45 | // |
| 46 | // Needs a world: `bash dev/world.sh N --up` then `eval "$(bash dev/world.sh N --env)"`. |
| 47 | import fs from 'node:fs'; |
| 48 | import path from 'node:path'; |
| 49 | import { fileURLToPath } from 'node:url'; |
| 50 | import { open, signInAs, shot } from './harness.mjs'; |
| 51 | |
| 52 | const ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), '..'); |
| 53 | const SRC = fs.readFileSync(path.join(ROOT, 'www/js/models.js'), 'utf8'); |
| 54 | |
| 55 | const ok = [], bad = []; |
| 56 | const check = (name, pass, detail) => { |
| 57 | (pass ? ok : bad).push(name + (detail ? ' — ' + detail : '')); |
| 58 | console.log((pass ? ' ok ' : ' FAIL ') + name + (detail ? ' — ' + detail : '')); |
| 59 | }; |
| 60 | |
| 61 | const OR = 'https://openrouter.ai/api/v1'; |
| 62 | const CORS = { 'access-control-allow-origin': '*', 'access-control-allow-headers': '*' }; |
| 63 | const json = (body, status = 200) => ({ |
| 64 | status, contentType: 'application/json', headers: CORS, body: JSON.stringify(body), |
| 65 | }); |
| 66 | const MIN = 60 * 1000; |
| 67 | /// The floor in models.js. Named here so the waits below say what they are relative to; the |
| 68 | /// checks assert the PROPERTY (a probe, or none) and never a count of milliseconds. |
| 69 | const FLOOR = 5 * MIN; |
| 70 | |
| 71 | // ── The patches that prove each check can fail ───────────────────────── |
| 72 | // Each is [anchor, replacement] against the shipped `www/js/models.js`, applied to the file the |
| 73 | // browser fetches. An anchor that no longer appears is a broken proof, not a passing one, so |
| 74 | // every anchor is checked against the file on disk before a browser is opened. |
| 75 | const BREAK = { |
| 76 | floor: { |
| 77 | what: 'the shared floor removed, so every trigger probes', |
| 78 | // TWO PATCHES, AND THE SECOND ONE IS NOT PART OF THE CLAIM. Removing the floor also |
| 79 | // removes the only thing standing between `render()` and itself: models.js:1987 calls |
| 80 | // `refreshCredits()` from a redraw, a probe that answers calls `render()`, and with no |
| 81 | // floor that loop feeds itself. MEASURED with the one patch (2026-08-25): 8,238 probes |
| 82 | // after five round trips and 20,790 by the end of the scenario, every one of them a |
| 83 | // route round trip across the Playwright channel -- 73 seconds against the 16 every |
| 84 | // other scenario takes, and enough to expire a click's own 30-second actionability |
| 85 | // wait on a busy box. The second patch cuts the success -> redraw edge, so what is |
| 86 | // left is exactly the claim: one probe per trigger, five round trips making five |
| 87 | // requests where the shipped build makes none. A sharper proof and a cheaper one. |
| 88 | patches: [ |
| 89 | ['if (!probeDue(id)) continue;', '/* floor removed */'], |
| 90 | ['if (got && document.getElementById(\'models-list\')) render();\n\t\t\t\t\telse ageLines();', |
| 91 | 'if (got) ageLines();'], |
| 92 | ], |
| 93 | }, |
| 94 | hidden: { |
| 95 | what: 'the beat left running while the tab is hidden', |
| 96 | patches: [['if (document.visibilityState === \'hidden\') { beatOff(); return; }', |
| 97 | 'if (document.visibilityState === \'hidden\') { return; }']], |
| 98 | }, |
| 99 | age: { |
| 100 | what: 'the age line silenced', |
| 101 | patches: [['function ageSentence(id, c) {', 'function ageSentence(id, c) { if (1) return \'\';']], |
| 102 | }, |
| 103 | decrement: { |
| 104 | what: 'the local decrement disabled', |
| 105 | patches: [['var gone = spentSince(id, c.asOf);', 'var gone = 0;']], |
| 106 | }, |
| 107 | idle: { |
| 108 | what: 'the finished-turn repaint removed', |
| 109 | patches: [['window.addEventListener(\'daimond:idle\', function () { ageLines(); });', |
| 110 | '/* no repaint when a turn finishes */']], |
| 111 | }, |
| 112 | headsync: { |
| 113 | // The defect this check was written from: the block below the head counted the turns |
| 114 | // off and the head went on quoting the provider, so the panel held two balances. |
| 115 | what: 'the closed row left behind by the open one', |
| 116 | patches: [['if (bal) paintBal(bal, id, creditFor(id));', '/* head left behind */']], |
| 117 | }, |
| 118 | anyone: { |
| 119 | what: 'every provider treated as one that will answer', |
| 120 | patches: [['return String(url || \'\').indexOf(\'openrouter.ai\') !== -1;', 'return !!url;']], |
| 121 | }, |
| 122 | }; |
| 123 | |
| 124 | for (const [name, b] of Object.entries(BREAK)) { |
| 125 | for (const [anchor] of b.patches) { |
| 126 | const n = SRC.split(anchor).length - 1; |
| 127 | if (n !== 1) { |
| 128 | console.log(` FAIL the "${name}" proof cannot be applied — its anchor appears ${n} times in models.js`); |
| 129 | console.log(` anchor: ${anchor}`); |
| 130 | process.exit(1); |
| 131 | } |
| 132 | } |
| 133 | } |
| 134 | |
| 135 | /// Run the whole scenario once, against the shipped file or a patched one. |
| 136 | /// |
| 137 | /// Returns the observations; the checks are made afterwards, once for the green run and once |
| 138 | /// per patched run, so the same sentence is asserted true here and false there. |
| 139 | async function scenario(label, patches) { |
| 140 | // What the provider says, and the levers the scenario pulls on it. |
| 141 | const or = { key: 0, credits: 0, total: 40, usage: 0, fail: false }; |
| 142 | const fw = { hits: 0 }; |
| 143 | let patched = 0; |
| 144 | |
| 145 | const s = await open({ |
| 146 | name: `creditage-${label}`, signIn: false, connect: false, defaults: false, |
| 147 | route: async (page) => { |
| 148 | // Fake time, and a faked visibility SIGNAL. Both before the first byte of the app. |
| 149 | await page.clock.install(); |
| 150 | await page.addInitScript(() => { |
| 151 | var vis = 'visible'; |
| 152 | Object.defineProperty(document, 'visibilityState', { configurable: true, get: function () { return vis; } }); |
| 153 | Object.defineProperty(document, 'hidden', { configurable: true, get: function () { return vis === 'hidden'; } }); |
| 154 | window.__vis = function (v) { vis = v; document.dispatchEvent(new Event('visibilitychange')); }; |
| 155 | }); |
| 156 | if (patches) { |
| 157 | await page.route('**/js/models.js', async (r) => { |
| 158 | const res = await r.fetch(); |
| 159 | let body = await res.text(); |
| 160 | for (const [from, to] of patches) { |
| 161 | if (body.includes(from)) { patched++; body = body.split(from).join(to); } |
| 162 | } |
| 163 | await r.fulfill({ status: 200, contentType: 'application/javascript', body }); |
| 164 | }); |
| 165 | } |
| 166 | // The two probe endpoints, counted. `limit: null` is an UNCAPPED key, which is what |
| 167 | // a user's own pasted key normally is, so both requests are exercised. |
| 168 | await page.route(`${OR}/key`, (r) => { |
| 169 | or.key++; |
| 170 | if (or.fail) return r.fulfill(json({ error: 'upstream' }, 500)); |
| 171 | return r.fulfill(json({ data: { label: 'test', limit: null, limit_remaining: null, usage: or.usage } })); |
| 172 | }); |
| 173 | await page.route(`${OR}/credits`, (r) => { |
| 174 | or.credits++; |
| 175 | if (or.fail) return r.fulfill(json({ error: 'upstream' }, 500)); |
| 176 | return r.fulfill(json({ data: { total_credits: or.total, total_usage: or.usage } })); |
| 177 | }); |
| 178 | await page.route(`${OR}/models`, (r) => r.fulfill(json({ data: [{ id: 'z-ai/glm-5p2' }] }))); |
| 179 | // A provider that has no CREDIT endpoint. A request for one is a request to |
| 180 | // nowhere, so everything but the catalogue is counted -- the whole host used to |
| 181 | // be, and `/models` stopped being nowhere on 2026-08-20, when a stale catalogue |
| 182 | // began asking for itself. The route still covers the host so that nothing can |
| 183 | // leave this machine; only the counting narrowed. |
| 184 | await page.route('https://api.fireworks.ai/**', (r) => { |
| 185 | if (!/\/models(\?|$)/.test(new URL(r.request().url()).pathname + '')) fw.hits++; |
| 186 | return r.fulfill(json({}, 404)); |
| 187 | }); |
| 188 | }, |
| 189 | }); |
| 190 | const p = s.page; |
| 191 | // A BROKEN BUILD IS ALLOWED TO BE SLOW, and the plumbing waits for it rather than the |
| 192 | // assertions being loosened -- nothing in this file asserts a duration, so a longer wait |
| 193 | // costs a slow run time and cannot buy a wrong answer. Playwright's default is 30 seconds |
| 194 | // per action, and on a busy box a patched build that is deliberately making requests can |
| 195 | // expire it inside a click that has already happened; the run then dies with an uncaught |
| 196 | // TimeoutError, which reads as the app being broken and is not. |
| 197 | p.setDefaultTimeout(120000); |
| 198 | await signInAs(s, `creditage-${label}`); |
| 199 | await p.waitForTimeout(1500); |
| 200 | |
| 201 | const ff = async (ms) => { await p.clock.fastForward(ms); await p.waitForTimeout(250); }; |
| 202 | const vis = async (v) => { await p.evaluate((x) => window.__vis(x), v); await p.waitForTimeout(250); }; |
| 203 | /// What the expanded OpenRouter row says: the figure sentence, its age line, and the |
| 204 | /// balance on the head. This is what the user reads, so it is what is asserted. |
| 205 | const said = () => p.evaluate(() => { |
| 206 | const w = document.querySelector('.models-credit[data-prov="openrouter"]'); |
| 207 | const heads = Array.prototype.slice.call(document.querySelectorAll('.models-prov-head')); |
| 208 | const head = heads.find((h) => /OpenRouter/.test(h.textContent || '')); |
| 209 | const bal = head && head.querySelector('.models-bal'); |
| 210 | const ageEl = w && w.querySelector('.models-credit-age'); |
| 211 | return { |
| 212 | line: w ? (w.querySelector('.models-credit-line') || {}).textContent || '' : '', |
| 213 | age: ageEl ? ageEl.textContent || '' : '', |
| 214 | // The loud colour on the age line, and the mark on the closed row's balance. Both |
| 215 | // have to keep up, or the staleness warning is itself stale. |
| 216 | ageStale: !!(ageEl && ageEl.classList.contains('stale')), |
| 217 | bal: bal ? bal.textContent || '' : '', |
| 218 | tip: bal ? bal.getAttribute('title') || '' : '', |
| 219 | stale: !!(bal && bal.hasAttribute('data-stale')), |
| 220 | }; |
| 221 | }); |
| 222 | |
| 223 | // ── A provider with a key, the way the settings form adds one ── |
| 224 | await p.evaluate(async () => { |
| 225 | DaimondModels.addProvider('openrouter', { url: 'https://openrouter.ai/api/v1/chat/completions' }); |
| 226 | DaimondModels.addProvider('fireworks', { url: 'https://api.fireworks.ai/inference/v1/chat/completions' }); |
| 227 | await DaimondModels.setKey('fireworks', 'fw-test-key'); |
| 228 | await DaimondModels.setKey('openrouter', 'sk-or-v1-testkey0000'); |
| 229 | }); |
| 230 | await p.waitForTimeout(1200); |
| 231 | const paste = { key: or.key, credits: or.credits }; |
| 232 | |
| 233 | // The panel, opened the way a user opens it, and the row expanded. |
| 234 | await p.click('#astat-model'); |
| 235 | await p.waitForTimeout(600); |
| 236 | await p.evaluate(() => { |
| 237 | const heads = Array.prototype.slice.call(document.querySelectorAll('.models-prov-head')); |
| 238 | const h = heads.find((x) => /OpenRouter/.test(x.textContent || '')); |
| 239 | if (h) h.click(); |
| 240 | }); |
| 241 | await p.waitForTimeout(400); |
| 242 | const opened = { key: or.key, said: await said() }; |
| 243 | await shot(s, `creditage-${label}-opened`); |
| 244 | |
| 245 | // ── The floor, against the trigger that fires most often ── |
| 246 | // Away and back five times in a row, well inside the floor. |
| 247 | for (let i = 0; i < 5; i++) { await vis('hidden'); await vis('visible'); } |
| 248 | const toggled = { key: or.key }; |
| 249 | |
| 250 | // ── And against the panel being opened again ── |
| 251 | // Shut by whichever closer this window is wearing: the rail's drawer has its own cross, |
| 252 | // and a window with no rail hosts the same view in a modal card. |
| 253 | const shut = async () => { |
| 254 | for (const sel of ['#admin-close', '#settings-close']) { |
| 255 | if (await p.isVisible(sel)) { await p.click(sel); return; } |
| 256 | } |
| 257 | throw new Error('nothing visible closes the Admin panel'); |
| 258 | }; |
| 259 | for (let i = 0; i < 3; i++) { |
| 260 | await shut(); |
| 261 | await p.waitForTimeout(200); |
| 262 | await p.click('#astat-model'); |
| 263 | await p.waitForTimeout(300); |
| 264 | } |
| 265 | const reopened = { key: or.key }; |
| 266 | |
| 267 | // ── The user spent: the figure moves with no request at all ── |
| 268 | // The two things daimond.js does at the end of every metered turn, in that order: the cost |
| 269 | // goes into the ledger, and the app announces that it is idle again. No clock is moved and |
| 270 | // no floor is spent — if the figure changes here, it changed out of books this device |
| 271 | // already keeps. |
| 272 | await p.evaluate(() => { |
| 273 | DaimondLedger.record({ |
| 274 | ts: Date.now(), model: 'z-ai/glm-5p2', provider: 'openrouter', |
| 275 | promptTokens: 1000, completionTokens: 500, cachedTokens: 0, costUsd: 2.5, |
| 276 | }); |
| 277 | window.dispatchEvent(new Event('daimond:idle')); |
| 278 | }); |
| 279 | await p.waitForTimeout(500); |
| 280 | const spent = { key: or.key, credits: or.credits, said: await said() }; |
| 281 | |
| 282 | // ── The user topped up: found when the tab comes back ── |
| 283 | or.total = 140; |
| 284 | await vis('hidden'); |
| 285 | await ff(FLOOR + MIN); |
| 286 | await vis('visible'); |
| 287 | await p.waitForTimeout(600); |
| 288 | const toppedUp = { key: or.key, said: await said() }; |
| 289 | |
| 290 | // ── A hidden tab does not poll ── |
| 291 | await vis('hidden'); |
| 292 | await ff(4 * FLOOR); |
| 293 | const whileHidden = { key: or.key }; |
| 294 | await vis('visible'); |
| 295 | await p.waitForTimeout(600); |
| 296 | const backAgain = { key: or.key, said: await said() }; |
| 297 | |
| 298 | // ── A probe that fails writes nothing, and says so ── |
| 299 | or.fail = true; |
| 300 | await ff(FLOOR + MIN); |
| 301 | await p.waitForTimeout(600); |
| 302 | const failed = { key: or.key, said: await said() }; |
| 303 | |
| 304 | // ── ...and the age goes on climbing, which is how you can tell ── |
| 305 | await ff(3 * 60 * MIN); |
| 306 | await p.waitForTimeout(400); |
| 307 | const aged = { said: await said() }; |
| 308 | |
| 309 | // ── A failing key backs off rather than retrying on the next beat ── |
| 310 | const beforeBackoff = or.key; |
| 311 | await ff(FLOOR + MIN); |
| 312 | const backoffHeld = { key: or.key, from: beforeBackoff }; |
| 313 | await ff(60 * MIN); |
| 314 | const backoffFreed = { key: or.key }; |
| 315 | |
| 316 | // ── The user asks by hand: never held back by the floor ── |
| 317 | or.fail = false; |
| 318 | or.total = 200; |
| 319 | await p.evaluate(() => { |
| 320 | const w = document.querySelector('.models-credit[data-prov="openrouter"]'); |
| 321 | const ask = w && w.querySelector(':scope > .models-refetch'); |
| 322 | if (ask) ask.click(); |
| 323 | }); |
| 324 | await p.waitForTimeout(900); |
| 325 | const asked = { key: or.key, said: await said() }; |
| 326 | |
| 327 | const errs = s.errs.filter((e) => !/favicon|502|Failed to load resource/i.test(e)); |
| 328 | await s.close(); |
| 329 | return { |
| 330 | label, patched, or, fw, errs, |
| 331 | paste, opened, toggled, reopened, spent, toppedUp, |
| 332 | whileHidden, backAgain, failed, aged, backoffHeld, backoffFreed, asked, |
| 333 | }; |
| 334 | } |
| 335 | |
| 336 | /// The first money figure in a sentence, as a number. |
| 337 | const amount = (s) => { |
| 338 | const m = String(s || '').match(/\$([0-9][0-9,]*\.?[0-9]*)/); |
| 339 | return m ? parseFloat(m[1].replace(/,/g, '')) : null; |
| 340 | }; |
| 341 | const near = (a, b) => a !== null && Math.abs(a - b) < 0.005; |
| 342 | |
| 343 | // ── The claims, each one a function of a run's observations ──────────── |
| 344 | // Written once and asserted twice: TRUE of the shipped code, FALSE of the build whose patch |
| 345 | // breaks it. A claim that holds on the broken build is a claim that was measuring something |
| 346 | // else, and is reported as such. |
| 347 | const CLAIMS = { |
| 348 | pasted: { |
| 349 | says: 'pasting a key still asks what is on it', |
| 350 | holds: (r) => r.paste.key === 1 && r.paste.credits === 1, |
| 351 | shows: (r) => `${r.paste.key} /key, ${r.paste.credits} /credits`, |
| 352 | }, |
| 353 | shown: { |
| 354 | says: 'and the panel shows what it answered', |
| 355 | holds: (r) => near(amount(r.opened.said.line), 40) && /40/.test(r.opened.said.bal), |
| 356 | shows: (r) => `${r.opened.said.bal} | ${r.opened.said.line}`, |
| 357 | }, |
| 358 | floorVisibility: { |
| 359 | says: 'FLOOR: away and back five times is not five probes', |
| 360 | breaks: 'floor', |
| 361 | holds: (r) => r.toggled.key === r.opened.key, |
| 362 | shows: (r) => `${r.opened.key} probe(s) before, ${r.toggled.key} after five round trips`, |
| 363 | }, |
| 364 | floorPanel: { |
| 365 | says: 'FLOOR: opening the panel three more times is not three probes', |
| 366 | breaks: 'floor', |
| 367 | holds: (r) => r.reopened.key === r.opened.key, |
| 368 | shows: (r) => `${r.opened.key} before, ${r.reopened.key} after three re-opens`, |
| 369 | }, |
| 370 | decrementCosts: { |
| 371 | says: 'SPENT: a turn moves the figure with NO request', |
| 372 | breaks: 'decrement', |
| 373 | holds: (r) => r.spent.key === r.reopened.key && r.spent.credits === r.paste.credits |
| 374 | && near(amount(r.spent.said.line), 37.5), |
| 375 | shows: (r) => `${r.spent.key} /key, ${r.spent.credits} /credits, "${r.spent.said.line}"`, |
| 376 | }, |
| 377 | decrementAtOnce: { |
| 378 | says: 'SPENT: a finished turn moves it at once, without waiting for the beat', |
| 379 | breaks: 'idle', |
| 380 | holds: (r) => near(amount(r.spent.said.line), 37.5), |
| 381 | shows: (r) => r.spent.said.line, |
| 382 | }, |
| 383 | decrementHead: { |
| 384 | says: 'SPENT: and the closed row never shows a different figure from the open one', |
| 385 | breaks: 'headsync', |
| 386 | holds: (r) => near(amount(r.spent.said.bal), amount(r.spent.said.line)), |
| 387 | shows: (r) => `head "${r.spent.said.bal}" vs block "${r.spent.said.line}"`, |
| 388 | }, |
| 389 | decrementSays: { |
| 390 | says: 'SPENT: and the sentence stops claiming the provider said the new figure', |
| 391 | breaks: 'decrement', |
| 392 | holds: (r) => /40/.test(r.spent.said.line) && /2\.5/.test(r.spent.said.line), |
| 393 | shows: (r) => r.spent.said.line, |
| 394 | }, |
| 395 | // No `breaks`: removing the floor makes the app probe MORE, so it finds the money too. What |
| 396 | // this claim is for is the other half — that coming back finds it with ONE request. |
| 397 | topUp: { |
| 398 | says: 'TOPPED UP: the tab coming back after the floor finds the money, in one request', |
| 399 | holds: (r) => r.toppedUp.key === r.opened.key + 1 && near(amount(r.toppedUp.said.line), 140), |
| 400 | shows: (r) => `${r.toppedUp.key} probe(s), "${r.toppedUp.said.line}"`, |
| 401 | }, |
| 402 | freshNotDoubled: { |
| 403 | says: 'TOPPED UP: a fresh reading is not decremented by spending it already knew about', |
| 404 | holds: (r) => near(amount(r.toppedUp.said.line), 140), |
| 405 | shows: (r) => r.toppedUp.said.line, |
| 406 | }, |
| 407 | hiddenQuiet: { |
| 408 | says: 'HIDDEN: four floors face down produces no request at all', |
| 409 | breaks: 'hidden', |
| 410 | holds: (r) => r.whileHidden.key === r.toppedUp.key, |
| 411 | shows: (r) => `${r.toppedUp.key} before hiding, ${r.whileHidden.key} after four floors hidden`, |
| 412 | }, |
| 413 | hiddenThenBack: { |
| 414 | says: 'HIDDEN: and coming back asks once', |
| 415 | holds: (r) => r.backAgain.key === r.whileHidden.key + 1, |
| 416 | shows: (r) => `${r.whileHidden.key} -> ${r.backAgain.key}`, |
| 417 | }, |
| 418 | failKeeps: { |
| 419 | says: 'FAILED: a probe that errors leaves the figure exactly where it was', |
| 420 | holds: (r) => near(amount(r.failed.said.line), amount(r.backAgain.said.line)) |
| 421 | && r.failed.key > r.backAgain.key, |
| 422 | shows: (r) => `asked ${r.backAgain.key}->${r.failed.key}, "${r.failed.said.line}"`, |
| 423 | }, |
| 424 | failSays: { |
| 425 | says: 'FAILED: and the age line says the last check did not answer', |
| 426 | breaks: 'age', |
| 427 | holds: (r) => /did not answer/i.test(r.failed.said.age) && r.failed.said.ageStale, |
| 428 | shows: (r) => `"${r.failed.said.age}" loud=${r.failed.said.ageStale}`, |
| 429 | }, |
| 430 | failMarksClosedRow: { |
| 431 | says: 'FAILED: and the mark on the closed row keeps up without a redraw', |
| 432 | breaks: 'age', |
| 433 | holds: (r) => r.failed.said.stale && /did not answer/i.test(r.failed.said.tip), |
| 434 | shows: (r) => `mark=${r.failed.said.stale} tip="${r.failed.said.tip}"`, |
| 435 | }, |
| 436 | failAges: { |
| 437 | says: 'FAILED: and the age goes on climbing, so the user can see it has stopped', |
| 438 | breaks: 'age', |
| 439 | holds: (r) => /minute/i.test(r.failed.said.age) && /hour/i.test(r.aged.said.age), |
| 440 | shows: (r) => `"${r.failed.said.age}" then "${r.aged.said.age}"`, |
| 441 | }, |
| 442 | backoff: { |
| 443 | says: 'BACKOFF: a failing key waits longer than the floor before it is asked again', |
| 444 | breaks: 'floor', |
| 445 | holds: (r) => r.backoffHeld.key === r.backoffHeld.from && r.backoffFreed.key > r.backoffHeld.key, |
| 446 | shows: (r) => `${r.backoffHeld.from} -> ${r.backoffHeld.key} after a floor, -> ${r.backoffFreed.key} after twelve`, |
| 447 | }, |
| 448 | byHand: { |
| 449 | says: 'BY HAND: "Ask again" is never held back, and picks up the new figure', |
| 450 | holds: (r) => r.asked.key === r.backoffFreed.key + 1 && near(amount(r.asked.said.line), 200), |
| 451 | shows: (r) => `${r.backoffFreed.key} -> ${r.asked.key}, "${r.asked.said.line}"`, |
| 452 | }, |
| 453 | nowhere: { |
| 454 | says: 'NOWHERE: a provider with no such endpoint is never asked, ever', |
| 455 | breaks: 'anyone', |
| 456 | holds: (r) => r.fw.hits === 0, |
| 457 | shows: (r) => `${r.fw.hits} request(s) to api.fireworks.ai`, |
| 458 | }, |
| 459 | quiet: { |
| 460 | says: 'and none of it throws', |
| 461 | holds: (r) => r.errs.length === 0, |
| 462 | shows: (r) => r.errs.slice(0, 2).join(' | '), |
| 463 | }, |
| 464 | }; |
| 465 | |
| 466 | // ── The shipped build ────────────────────────────────────────────────── |
| 467 | console.log('\n── the shipped build ──'); |
| 468 | const green = await scenario('green', null); |
| 469 | for (const [k, c] of Object.entries(CLAIMS)) check(c.says, c.holds(green), c.shows(green)); |
| 470 | |
| 471 | // ── The broken builds ────────────────────────────────────────────────── |
| 472 | // One per patch, and in each the claims that name it must FAIL. Anything else it breaks is |
| 473 | // reported but not counted: a patch is allowed collateral damage, it is not allowed to leave |
| 474 | // its own claim standing. |
| 475 | for (const [name, b] of Object.entries(BREAK)) { |
| 476 | const want = Object.entries(CLAIMS).filter(([, c]) => c.breaks === name); |
| 477 | if (!want.length) { console.log(` FAIL the "${name}" patch proves nothing: no claim names it`); bad.push(name); continue; } |
| 478 | console.log(`\n── broken on purpose: ${b.what} ──`); |
| 479 | const run = await scenario(name, b.patches); |
| 480 | check(`the "${name}" patch reached the browser`, run.patched >= b.patches.length, |
| 481 | `${run.patched} of ${b.patches.length} applied`); |
| 482 | for (const [, c] of want) { |
| 483 | check(`RED: "${c.says}" fails when ${b.what}`, !c.holds(run), c.shows(run)); |
| 484 | } |
| 485 | } |
| 486 | |
| 487 | console.log(`\n${bad.length ? `verify_creditage: ${bad.length} FAILED` : 'verify_creditage: all checks pass.'}`); |
| 488 | if (bad.length) { bad.forEach((b) => console.log(' - ' + b)); process.exit(1); } |