oxedyne/daimond/dev/verify_steer.mjs
5.2 KiB, 1 run
created by r2519314175:697, 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 | // A steer turn that answers in words (no crystal edit, no dispatch) must SHOW those |
| 2 | // words, not silently bill and vanish. |
| 3 | // |
| 4 | // The words are a nonce minted here, and they are looked for in `#crystal-reply` |
| 5 | // alone. That element is written by `setCrystalReply` and by nothing else (see |
| 6 | // www/js/daimond.js), so a hit there is the model's answer and cannot be the |
| 7 | // user's own typed line coming back -- which is the way this check would |
| 8 | // otherwise pass on an app that showed nothing at all. |
| 9 | // |
| 10 | // PROVED AGAINST BROKEN CODE FIRST. `--break silent` serves a copy of |
| 11 | // www/js/daimond.js in which `setCrystalReply` builds the reply and then leaves |
| 12 | // it hidden -- the defect this file is named for -- and the run is expected to |
| 13 | // FAIL. An anchor that does not appear exactly once aborts the run rather than |
| 14 | // passing quietly. |
| 15 | // |
| 16 | // node dev/verify_steer.mjs --break silent # expected to FAIL |
| 17 | // node dev/verify_steer.mjs # and then, clean |
| 18 | import fs from 'node:fs'; |
| 19 | import path from 'node:path'; |
| 20 | import { fileURLToPath } from 'node:url'; |
| 21 | import { open, shot, errors } from './harness.mjs'; |
| 22 | |
| 23 | const ok = [], bad = []; |
| 24 | const check = (name, pass, detail) => { |
| 25 | (pass ? ok : bad).push(name + (detail ? ' — ' + detail : '')); |
| 26 | console.log((pass ? ' ok ' : ' FAIL ') + name + (detail ? ' — ' + detail : '')); |
| 27 | }; |
| 28 | |
| 29 | const HERE = path.dirname(fileURLToPath(import.meta.url)); |
| 30 | const WWW = path.join(HERE, '..', 'www'); |
| 31 | const SRC = 'js/daimond.js'; |
| 32 | |
| 33 | const BREAK = (() => { |
| 34 | const i = process.argv.indexOf('--break'); |
| 35 | return i > 0 ? String(process.argv[i + 1] || '') : ''; |
| 36 | })(); |
| 37 | |
| 38 | // The last three lines of `setCrystalReply`: it appends the dismiss control and |
| 39 | // the rendered body, and then reveals the panel. The break appends both and |
| 40 | // reveals nothing, so a reply that exists is never seen. |
| 41 | const SHOWN = "\t\tr.appendChild(x);\n\t\tr.appendChild(body);\n\t\tr.style.display = '';"; |
| 42 | const HIDDEN = "\t\tr.appendChild(x);\n\t\tr.appendChild(body);\n\t\tr.style.display = 'none';"; |
| 43 | const BREAKS = { silent: [{ file: SRC, find: SHOWN, with: HIDDEN }] }; |
| 44 | |
| 45 | if (BREAK && !BREAKS[BREAK]) { |
| 46 | console.error(`unknown break '${BREAK}'; known: ${Object.keys(BREAKS).join(', ')}`); |
| 47 | process.exit(2); |
| 48 | } |
| 49 | |
| 50 | /// Serve the damaged file to the page, before anything navigates. |
| 51 | async function breakInto(page) { |
| 52 | const bodies = {}; |
| 53 | for (const spec of BREAKS[BREAK]) { |
| 54 | const src = bodies[spec.file] || fs.readFileSync(path.join(WWW, spec.file), 'utf8'); |
| 55 | const n = src.split(spec.find).length - 1; |
| 56 | if (n !== 1) { |
| 57 | console.error(`break '${BREAK}': the anchor appears ${n} times in ${spec.file}, ` |
| 58 | + 'so nothing was broken and the run below would prove nothing.'); |
| 59 | process.exit(2); |
| 60 | } |
| 61 | bodies[spec.file] = src.replace(spec.find, spec.with); |
| 62 | } |
| 63 | for (const file of Object.keys(bodies)) { |
| 64 | await page.route('**/' + file, (r) => r.fulfill({ |
| 65 | status: 200, contentType: 'application/javascript', body: bodies[file], |
| 66 | })); |
| 67 | } |
| 68 | } |
| 69 | |
| 70 | const s = await open({ name: 'steer', route: BREAK ? breakInto : null }); |
| 71 | if (BREAK) console.log(`\n*** RUNNING UNDER --break ${BREAK}: failures below are the point ***\n`); |
| 72 | |
| 73 | // Create a Diamond via the real button + prompt dialog. |
| 74 | await s.page.click('#new-diamond-btn'); |
| 75 | await s.page.waitForSelector('.dlg-input', { timeout: 8000 }); |
| 76 | await s.page.fill('.dlg-input', 'Test Diamond'); |
| 77 | await s.page.click('.dlg-ok'); |
| 78 | await s.page.waitForTimeout(1200); |
| 79 | await shot(s, 'steer-0-after-newfocus'); |
| 80 | |
| 81 | const state = await s.page.evaluate(() => ({ |
| 82 | steer: !!document.getElementById('chat-input'), |
| 83 | reply: !!document.getElementById('crystal-reply'), |
| 84 | })); |
| 85 | // This used to be an `if`, and a run that never reached the composer printed a |
| 86 | // line about the screenshot and exited 0 -- the whole file skipped, green. |
| 87 | check('a new Diamond leaves a steer composer to type in', state.steer, '#chat-input'); |
| 88 | check('and a reply panel for the answer to land in', state.reply, '#crystal-reply'); |
| 89 | |
| 90 | // A nonce, so a hit in the reply panel is this turn's answer and not a fixture |
| 91 | // word that could have come from anywhere. |
| 92 | const NONCE = 'steer-' + Math.random().toString(36).slice(2, 8); |
| 93 | let r = { shown: false, text: '' }; |
| 94 | if (state.steer) { |
| 95 | await s.page.fill('#chat-input', `@text I need one clarification (${NONCE}): which platform first?`); |
| 96 | await s.page.keyboard.press('Enter'); |
| 97 | await s.page.waitForTimeout(4000); |
| 98 | r = await s.page.evaluate(() => { |
| 99 | const el = document.getElementById('crystal-reply'); |
| 100 | const body = el && el.querySelector('.crystal-reply-body'); |
| 101 | return { |
| 102 | shown: !!(el && el.style.display !== 'none' && el.getClientRects().length), |
| 103 | text: body ? body.textContent : (el ? el.textContent : ''), |
| 104 | }; |
| 105 | }); |
| 106 | await shot(s, 'steer-1-reply'); |
| 107 | } |
| 108 | check('A TEXT-ONLY STEER IS SHOWN, not silently billed and dropped', |
| 109 | r.shown, r.shown ? 'the reply panel is on screen' : 'the reply panel is hidden or absent'); |
| 110 | check('and what it shows is this turn\'s answer', |
| 111 | r.text.includes(NONCE), `looking for ${NONCE} in ${JSON.stringify((r.text || '').slice(0, 80))}`); |
| 112 | |
| 113 | // 502 is this world with no gateway behind it. |
| 114 | const errs = errors(s).filter(e => !/502|Bad Gateway/.test(e)); |
| 115 | check('nothing threw', errs.length === 0, errs.slice(0, 2).join(' | ')); |
| 116 | |
| 117 | await s.close(); |
| 118 | console.log(`\n${ok.length} passed, ${bad.length} failed`); |
| 119 | if (bad.length) { bad.forEach(b => console.log(' FAILED: ' + b)); process.exit(1); } |