Oregami
Repositories/oxedyne/daimond

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
18import fs from 'node:fs';
19import path from 'node:path';
20import { fileURLToPath } from 'node:url';
21import { open, shot, errors } from './harness.mjs';
22
23const ok = [], bad = [];
24const check = (name, pass, detail) => {
25 (pass ? ok : bad).push(name + (detail ? ' — ' + detail : ''));
26 console.log((pass ? ' ok ' : ' FAIL ') + name + (detail ? ' — ' + detail : ''));
27};
28
29const HERE = path.dirname(fileURLToPath(import.meta.url));
30const WWW = path.join(HERE, '..', 'www');
31const SRC = 'js/daimond.js';
32
33const 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.
41const SHOWN = "\t\tr.appendChild(x);\n\t\tr.appendChild(body);\n\t\tr.style.display = '';";
42const HIDDEN = "\t\tr.appendChild(x);\n\t\tr.appendChild(body);\n\t\tr.style.display = 'none';";
43const BREAKS = { silent: [{ file: SRC, find: SHOWN, with: HIDDEN }] };
44
45if (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.
51async 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
70const s = await open({ name: 'steer', route: BREAK ? breakInto : null });
71if (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.
74await s.page.click('#new-diamond-btn');
75await s.page.waitForSelector('.dlg-input', { timeout: 8000 });
76await s.page.fill('.dlg-input', 'Test Diamond');
77await s.page.click('.dlg-ok');
78await s.page.waitForTimeout(1200);
79await shot(s, 'steer-0-after-newfocus');
80
81const 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.
87check('a new Diamond leaves a steer composer to type in', state.steer, '#chat-input');
88check('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.
92const NONCE = 'steer-' + Math.random().toString(36).slice(2, 8);
93let r = { shown: false, text: '' };
94if (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}
108check('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');
110check('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.
114const errs = errors(s).filter(e => !/502|Bad Gateway/.test(e));
115check('nothing threw', errs.length === 0, errs.slice(0, 2).join(' | '));
116
117await s.close();
118console.log(`\n${ok.length} passed, ${bad.length} failed`);
119if (bad.length) { bad.forEach(b => console.log(' FAILED: ' + b)); process.exit(1); }