Oregami
Repositories/oxedyne/daimond

oxedyne/daimond/dev/verify_typstbutton.mjs

9.5 KiB, 1 run

created by r2519314175:769, 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_typstbutton.mjs — the ⚙ Compile button compiles the PROJECT.
2//
3// `typst_compile` shipped a whole multi-file gatherer in seq 116 and NOTHING IN
4// PRODUCTION CALLED IT. The model's tool took the single-source arm, and the Doc
5// panel's ⚙ Compile button — the other door, the one a person presses — handed
6// `DaimondTypst.compile` one file's text. A 63-file book compiled through it came
7// back "only the one source was given to the compiler", and the daimon reading
8// that concluded the compiler could not resolve imports at all.
9//
10// The 24-check verifier that covered the gatherer could not see this, and the
11// reason is the whole point of this file: IT CALLED THE GATHERER ITSELF. A check
12// that reaches for `Wasm.typst_compile_project` passes whether or not anything a
13// user can press is wired to it.
14//
15// So this one presses the button. It seeds a two-file project in the workspace,
16// opens the file the way a person opens it, clicks ⚙ Compile, and asks what
17// appeared. One property, and it cannot be satisfied by the single-file door:
18//
19// THE BUTTON COMPILES A DOCUMENT THAT IMPORTS ANOTHER FILE. The chapter is a
20// separate file; a compiler handed only `main.typ` cannot find it and says so.
21// Asserted on the panel's own message and on the PDF landing in the workspace,
22// not on a return value.
23//
24// node dev/verify_typstbutton.mjs --break singlefile # the button as it was:
25// # one source, and the
26// # import cannot resolve
27// node dev/verify_typstbutton.mjs # and then, clean
28//
29// eval "$(bash dev/world.sh 5 --up)"
30// node dev/verify_typstbutton.mjs
31//
32// Needs dev/serve.mjs only. The compiler is the real 30 MB wasm, so this is slow.
33import fs from 'node:fs';
34import path from 'node:path';
35import { fileURLToPath } from 'node:url';
36import { open, scratch, shot } from './harness.mjs';
37
38const WWW = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'www');
39
40const BREAK = (() => {
41 const i = process.argv.indexOf('--break');
42 return i > 0 ? String(process.argv[i + 1] || '') : '';
43})();
44
45const BREAKS = {
46 // The button exactly as it stood until this seam: read the one file, hand the
47 // driver the string. Everything else about the panel is unchanged.
48 singlefile: [{
49 file: 'js/daimond.js',
50 find: '\t\t\t\tvar out = await Wasm.typst_compile_project(path);',
51 with: '\t\t\t\tvar out = await window.DaimondTypst.compile(await readBytes(path));',
52 }],
53};
54
55if (BREAK && !BREAKS[BREAK]) {
56 console.error(`unknown break '${BREAK}'; one of: ${Object.keys(BREAKS).join(', ')}`);
57 process.exit(2);
58}
59
60function damaged(src, spec) {
61 const n = src.split(spec.find).length - 1;
62 if (n !== 1) {
63 console.error(`break '${BREAK}': the anchor appears ${n} times in ${spec.file}.`);
64 process.exit(2);
65 }
66 return src.replace(spec.find, spec.with);
67}
68
69/// The damaged files, ONE BODY PER FILE.
70///
71/// Every edit a break names for a file goes into the SAME body, in order, and
72/// that one body is what the route serves. A `page.route` per edit spec does not
73/// work and does not say so: Playwright hands a request to the LAST route
74/// registered for its URL, so a two-edit break shipped only its second edit --
75/// and still went red, for half the reason it claims, with nothing to notice it.
76function damagedFiles() {
77 const byFile = new Map();
78 for (const spec of (BREAKS[BREAK] || [])) {
79 const src = byFile.has(spec.file) ? byFile.get(spec.file)
80 : fs.readFileSync(path.join(WWW, spec.file), 'utf8');
81 byFile.set(spec.file, damaged(src, spec));
82 }
83 return byFile;
84}
85
86const routeBreaks = async (pg) => {
87 if (!BREAK) return;
88 for (const [file, body] of damagedFiles()) {
89 await pg.route('**/' + file, r => r.fulfill({
90 status: 200, contentType: 'application/javascript', body,
91 }));
92 }
93};
94
95const ok = [], bad = [];
96const check = (name, pass, detail) => {
97 (pass ? ok : bad).push(name);
98 console.log((pass ? ' ok ' : ' FAIL ') + name + (detail ? ' — ' + detail : ''));
99};
100const sleep = (ms) => new Promise(r => setTimeout(r, ms));
101
102// A project, not a file: the chapter is somewhere else and the main source has
103// to reach it. This is the whole difference between the two doors.
104const MAIN = 'tbook/main.typ';
105const CHAP = 'tbook/chap01.typ';
106const MARK = 'THE CHAPTER RESOLVED';
107const MAIN_SRC = '#include "chap01.typ"\n';
108const CHAP_SRC = '= Chapter one\n\n' + MARK + '\n';
109
110const PROFILE = scratch('pw', 'typstbutton' + (BREAK ? '-' + BREAK : ''));
111fs.rmSync(PROFILE, { recursive: true, force: true });
112
113const s = await open({ name: 'typstbutton', profile: PROFILE, signIn: true, connect: false,
114 route: routeBreaks });
115const p = s.page;
116await p.waitForFunction(() => !!window.DaimondCore && !!window.DaimondPanels,
117 null, { timeout: 20000 }).catch(() => {});
118await sleep(1500);
119
120try {
121 // The account these claims are about holds the typesetting pack, said out
122 // loud: a locked pack refuses the compile and the check would be red for a
123 // reason that has nothing to do with which door the button uses.
124 const held = await p.evaluate(async () => {
125 const mod = await import('../pkg/oxedyne_daimond.js');
126 mod.set_locked_packs('');
127 return { locked: mod.tool_locked('typst_compile'),
128 hasProject: typeof mod.typst_compile_project === 'function' };
129 });
130 check('the account holds the typesetting pack', held.locked === false, 'locked: ' + held.locked);
131 check('and this build carries the project door at all', held.hasProject,
132 held.hasProject ? '' : 'no typst_compile_project in the wasm: nothing below can pass');
133
134 // Seeded through the file tools, which write into the same workspace root the
135 // Files panel reads.
136 const seeded = await p.evaluate(async (f) => {
137 const mod = await import('../pkg/oxedyne_daimond.js');
138 const app = new mod.DaimondApp('http://127.0.0.1/v1/chat/completions', '', 'none', 256, '', true);
139 await app.run_tool('file_write', JSON.stringify({ path: f.main, content: f.mainSrc }));
140 await app.run_tool('file_write', JSON.stringify({ path: f.chap, content: f.chapSrc }));
141 return String(await app.run_tool('file_list', JSON.stringify({ path: 'tbook' })));
142 }, { main: MAIN, chap: CHAP, mainSrc: MAIN_SRC, chapSrc: CHAP_SRC });
143 check('a two-file project is in the workspace',
144 /main\.typ/.test(seeded) && /chap01\.typ/.test(seeded), seeded.replace(/\n/g, ' ').slice(0, 90));
145
146 // Opened the way a person opens it: the Work panel, then the file's own row.
147 // Closed and reopened rather than merely shown: `DaimondPanels.show` returns
148 // early for a panel that is already open, so a tree drawn before the seeding
149 // would never list what was just written.
150 await p.evaluate(() => { window.DaimondPanels.hide('work'); window.DaimondPanels.show('work'); });
151 await sleep(2000);
152 const opened = await p.evaluate(async (main) => {
153 // The tree draws lazily; the folder has to be entered before the file has
154 // a row to click.
155 const rows = () => [...document.querySelectorAll('#files-tree [data-path], #work-tree [data-path], .files-row')];
156 const byPath = (want) => rows().find((r) => (r.dataset && r.dataset.path) === want);
157 const folder = byPath('tbook');
158 if (folder) { folder.click(); await new Promise((r) => setTimeout(r, 700)); }
159 const row = byPath(main);
160 if (!row) return { clicked: false, saw: rows().map((r) => r.dataset && r.dataset.path).filter(Boolean).slice(0, 12) };
161 row.click();
162 await new Promise((r) => setTimeout(r, 1200));
163 return { clicked: true };
164 }, MAIN);
165 // A compile button on screen is the precondition for the whole file. If it is
166 // not there, nothing below means anything, so it is a check of its own.
167 const hasBtn = await p.evaluate(() => !!document.querySelector('[data-act="compile"]'));
168 check('the document opens with a ⚙ Compile button on it', hasBtn,
169 hasBtn ? '' : 'no compile button; rows seen: ' + JSON.stringify(opened.saw || []));
170
171 if (hasBtn) {
172 await p.evaluate(() => document.querySelector('[data-act="compile"]').click());
173 // The 30 MB compiler is built on first use, so this waits generously — and
174 // on the OUTCOME, not on a timer: either a message that is not "compiling"
175 // or a PDF in the workspace.
176 let said = '';
177 for (let i = 0; i < 240; i++) {
178 await sleep(1000);
179 said = await p.evaluate(() => {
180 const m = document.querySelector('.files-view-msg');
181 return m ? (m.textContent || '') : '';
182 });
183 if (said && !/compiling/i.test(said)) break;
184 }
185 const pdf = await p.evaluate(async (main) => {
186 const mod = await import('../pkg/oxedyne_daimond.js');
187 const app = new mod.DaimondApp('http://127.0.0.1/v1/chat/completions', '', 'none', 256, '', true);
188 const out = String(await app.run_tool('file_list', JSON.stringify({ path: 'tbook' })));
189 return { listing: out, has: /main\.pdf/.test(out) };
190 }, MAIN);
191 const errored = await p.evaluate(() => {
192 const m = document.querySelector('.files-view-msg');
193 return !!(m && m.classList.contains('err'));
194 });
195 check('pressing ⚙ Compile on a document that imports another file produces a PDF',
196 pdf.has && !errored, errored ? ('the panel says: ' + said.slice(0, 160)) : said.slice(0, 120));
197 check('and the panel says so where the reader is looking',
198 /\.pdf/i.test(said) && !errored, said.slice(0, 160));
199 }
200 await shot(s, 'typst-button');
201} catch (e) {
202 check('the run finished', false, String(e && e.message || e));
203} finally {
204 await s.close();
205}
206
207console.log(bad.length === 0
208 ? `\nverify_typstbutton: all ${ok.length} checks pass.`
209 : `\nverify_typstbutton: ${bad.length} of ${ok.length + bad.length} failed:\n ` + bad.join('\n '));
210process.exit(bad.length ? 1 : 0);