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. |
| 33 | import fs from 'node:fs'; |
| 34 | import path from 'node:path'; |
| 35 | import { fileURLToPath } from 'node:url'; |
| 36 | import { open, scratch, shot } from './harness.mjs'; |
| 37 | |
| 38 | const WWW = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'www'); |
| 39 | |
| 40 | const BREAK = (() => { |
| 41 | const i = process.argv.indexOf('--break'); |
| 42 | return i > 0 ? String(process.argv[i + 1] || '') : ''; |
| 43 | })(); |
| 44 | |
| 45 | const 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 | |
| 55 | if (BREAK && !BREAKS[BREAK]) { |
| 56 | console.error(`unknown break '${BREAK}'; one of: ${Object.keys(BREAKS).join(', ')}`); |
| 57 | process.exit(2); |
| 58 | } |
| 59 | |
| 60 | function 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. |
| 76 | function 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 | |
| 86 | const 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 | |
| 95 | const ok = [], bad = []; |
| 96 | const check = (name, pass, detail) => { |
| 97 | (pass ? ok : bad).push(name); |
| 98 | console.log((pass ? ' ok ' : ' FAIL ') + name + (detail ? ' — ' + detail : '')); |
| 99 | }; |
| 100 | const 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. |
| 104 | const MAIN = 'tbook/main.typ'; |
| 105 | const CHAP = 'tbook/chap01.typ'; |
| 106 | const MARK = 'THE CHAPTER RESOLVED'; |
| 107 | const MAIN_SRC = '#include "chap01.typ"\n'; |
| 108 | const CHAP_SRC = '= Chapter one\n\n' + MARK + '\n'; |
| 109 | |
| 110 | const PROFILE = scratch('pw', 'typstbutton' + (BREAK ? '-' + BREAK : '')); |
| 111 | fs.rmSync(PROFILE, { recursive: true, force: true }); |
| 112 | |
| 113 | const s = await open({ name: 'typstbutton', profile: PROFILE, signIn: true, connect: false, |
| 114 | route: routeBreaks }); |
| 115 | const p = s.page; |
| 116 | await p.waitForFunction(() => !!window.DaimondCore && !!window.DaimondPanels, |
| 117 | null, { timeout: 20000 }).catch(() => {}); |
| 118 | await sleep(1500); |
| 119 | |
| 120 | try { |
| 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 | |
| 207 | console.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 ')); |
| 210 | process.exit(bad.length ? 1 : 0); |