oxedyne/daimond/dev/verify_rawreads.mjs
7.6 KiB, 1 run
created by r2519314175:627, 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_rawreads.mjs — nothing user-facing reads a file through the model's eyes. |
| 2 | // |
| 3 | // `run_tool('file_read')` renders a file FOR A MODEL: every line is prefixed with |
| 4 | // its number and a TAB, truncation is announced in prose, and anything under an |
| 5 | // untrusted path arrives wrapped in an envelope. Handed to something that treats |
| 6 | // the result as the file, the numbers become part of the content. |
| 7 | // |
| 8 | // This has shipped as a live bug four times, each found only after the last was |
| 9 | // written down: |
| 10 | // |
| 11 | // 1. The Doc panel showed the numbered rendering AND seeded its editor with it, |
| 12 | // so opening a file and pressing Save wrote the numbers in — compounding on |
| 13 | // every repeat. (Fixed 2026-08-06.) |
| 14 | // 2. The Email panel read every message's headers that way, so `parseHeaders` |
| 15 | // saw `1\tFrom: …`, matched nothing, and every message in the panel read |
| 16 | // "(unknown)" and "(no subject)". (Fixed 2026-08-07.) |
| 17 | // 3. The `conductor` → `daimon` prompt migration read the old file and WROTE it |
| 18 | // into the new one, baking the numbers into the user's own edited prompt. |
| 19 | // 4. The Web panel read an agent-written HTML page and rendered it, numbers and |
| 20 | // all, as the page. |
| 21 | // |
| 22 | // Four instances of one rule is not four mistakes, it is a missing check. So this |
| 23 | // hunts the PROPERTY rather than the four: for each surface that turns a file into |
| 24 | // something a person sees or a machine parses, the bytes that come out are the |
| 25 | // bytes that went in. |
| 26 | // |
| 27 | // The tell to look for is a line beginning `<digits><TAB>` where the file had no |
| 28 | // such thing, so the fixtures are written with content that could never produce |
| 29 | // one by accident. |
| 30 | // |
| 31 | // node dev/verify_rawreads.mjs |
| 32 | // |
| 33 | // Needs dev/serve.mjs (DAIMOND_PORT, default 8777). No gateway, no model. |
| 34 | import fs from 'node:fs'; |
| 35 | import { open, scratch } from './harness.mjs'; |
| 36 | |
| 37 | const PROFILE = scratch('pw', 'rawreads'); |
| 38 | fs.rmSync(PROFILE, { recursive: true, force: true }); |
| 39 | |
| 40 | let bad = 0; |
| 41 | const check = (pass, name, detail) => { |
| 42 | if (!pass) bad++; |
| 43 | console.log((pass ? ' ok ' : ' FAIL ') + name + (detail ? ' — ' + detail : '')); |
| 44 | }; |
| 45 | |
| 46 | /// The signature of the model's rendering: a line that opens with a number and a |
| 47 | /// tab. `file_read` produces one for EVERY line, so one is enough to convict. |
| 48 | const NUMBERED = /(^|\n)\d+\t/; |
| 49 | |
| 50 | const s = await open({ name: 'rawreads', profile: PROFILE, connect: false }); |
| 51 | const { page } = s; |
| 52 | |
| 53 | try { |
| 54 | await page.waitForFunction(() => !!(window.DaimondCore && window.DaimondPanels), |
| 55 | null, { timeout: 20000 }); |
| 56 | await page.waitForTimeout(800); |
| 57 | |
| 58 | const write = (path, content) => page.evaluate(async (a) => { |
| 59 | const mod = await import('../pkg/oxedyne_daimond.js'); |
| 60 | const app = new mod.DaimondApp('http://127.0.0.1/v1/chat/completions', '', 'none', 256, '', true); |
| 61 | await app.run_tool('file_write', JSON.stringify({ path: a.path, content: a.content })); |
| 62 | }, { path, content }); |
| 63 | |
| 64 | // Three lines, none of which begins with a digit, so a `1\t` in the output can |
| 65 | // only have come from the renderer. |
| 66 | const BODY = 'alpha one\nbeta two\ngamma three\n'; |
| 67 | |
| 68 | console.log('the two readers disagree, which is the whole point'); |
| 69 | await write('rawreads.txt', BODY); |
| 70 | const both = await page.evaluate(async () => { |
| 71 | const mod = await import('../pkg/oxedyne_daimond.js'); |
| 72 | const app = new mod.DaimondApp('http://127.0.0.1/v1/chat/completions', '', 'none', 256, '', true); |
| 73 | const rendered = await app.run_tool('file_read', JSON.stringify({ path: 'rawreads.txt' })); |
| 74 | const raw = await mod.read_file('rawreads.txt'); |
| 75 | return { rendered, raw }; |
| 76 | }); |
| 77 | check(NUMBERED.test(both.rendered), |
| 78 | '`file_read` numbers every line — so the tell this file hunts for is real', |
| 79 | JSON.stringify(both.rendered.slice(0, 24))); |
| 80 | check(both.raw === BODY, |
| 81 | '`read_file` gives back exactly what was written', |
| 82 | JSON.stringify(both.raw.slice(0, 24))); |
| 83 | |
| 84 | console.log('the Web panel renders the page, not a listing of it'); |
| 85 | // Through the panel's own door — `DaimondWeb.open` then `read()` — and not by |
| 86 | // reading the file a second way and comparing it with itself. A check that |
| 87 | // proves `read_file` works proves nothing about what the panel calls. |
| 88 | await write('page.html', '<!doctype html>\n<title>Hi</title>\n<p>Hello there.</p>\n'); |
| 89 | const shown = await page.evaluate(async () => { |
| 90 | try { |
| 91 | const res = await window.DaimondWeb.open('page.html'); |
| 92 | await new Promise(r => setTimeout(r, 400)); |
| 93 | const text = await window.DaimondWeb.read(); |
| 94 | return { driver: res && res.driver, text: (text && text.text) || String(text || '') }; |
| 95 | } catch (e) { return { err: String(e).slice(0, 120) }; } |
| 96 | }); |
| 97 | if (shown.err) { |
| 98 | check(false, 'the Web panel opened the page', shown.err); |
| 99 | } else { |
| 100 | check(shown.driver === 'local', 'the page opened in the local driver', String(shown.driver)); |
| 101 | // NOT `NUMBERED` here, and the reason is the whole lesson of this file: a |
| 102 | // browser collapses the tab, so the RENDERED text of a numbered page reads |
| 103 | // "1 2 3 Hello there." with no tab anywhere in it. The first version of |
| 104 | // this check used the same regex as the others and passed against the |
| 105 | // broken code — proof, again, that a check has to be run against the fault |
| 106 | // it exists for. What survives rendering is that the numbers are there at |
| 107 | // all, so the tell is visible text opening with a bare run of integers. |
| 108 | const opensWithNumbers = /^\s*\d+(\s+\d+)+/.test(shown.text); |
| 109 | check(!opensWithNumbers && !NUMBERED.test(shown.text), |
| 110 | 'and what the page SHOWS is the page, not a numbered listing of its source', |
| 111 | JSON.stringify(shown.text.slice(0, 40))); |
| 112 | } |
| 113 | |
| 114 | console.log('a prompt carried to its new name keeps its own words'); |
| 115 | await write('prompts/conductor.md', BODY); |
| 116 | const carried = await page.evaluate(async () => { |
| 117 | try { |
| 118 | await DaimondPrompts.migrateRenamed |
| 119 | ? DaimondPrompts.migrateRenamed() |
| 120 | : (DaimondPrompts.migrate && await DaimondPrompts.migrate()); |
| 121 | } catch (e) { /* the migration may have nothing to do */ } |
| 122 | const mod = await import('../pkg/oxedyne_daimond.js'); |
| 123 | try { return await mod.read_file('prompts/daimon.md'); } catch (e) { return ''; } |
| 124 | }); |
| 125 | if (!carried) { |
| 126 | console.log(' skip the rename migration did not run in this state'); |
| 127 | } else { |
| 128 | check(!NUMBERED.test(carried), 'the migrated prompt is the words, not a listing of them', |
| 129 | JSON.stringify(carried.slice(0, 28))); |
| 130 | } |
| 131 | |
| 132 | console.log('the source handed to the compiler is source'); |
| 133 | await write('doc.typ', '= Title\n\nA paragraph.\n'); |
| 134 | const typ = await page.evaluate(async () => { |
| 135 | const mod = await import('../pkg/oxedyne_daimond.js'); |
| 136 | return await mod.read_file('doc.typ'); |
| 137 | }); |
| 138 | check(!NUMBERED.test(typ), 'a Typst file reads back compilable', |
| 139 | JSON.stringify(typ.slice(0, 24))); |
| 140 | |
| 141 | console.log('and the rule is kept where it is easy to break'); |
| 142 | const src = await (await fetch(`${new URL(page.url()).origin}/js/daimond.js`)).text() |
| 143 | .catch(() => null) || await page.evaluate(async () => (await fetch('/js/daimond.js')).text()); |
| 144 | // Every remaining `file_read` in daimond.js must be an existence test. The |
| 145 | // content-using ones now go through `readBytes`, so a new one is a new bug. |
| 146 | const reads = (src.match(/run_tool\('file_read'/g) || []).length; |
| 147 | const raws = (src.match(/readBytes\(/g) || []).length; |
| 148 | check(raws >= 4, 'the shared byte reader is used by every surface that needs content', |
| 149 | `${raws} call site(s)`); |
| 150 | console.log(` note ${reads} \`file_read\` call(s) remain in daimond.js; each should be an existence test only`); |
| 151 | |
| 152 | const errs = (await import('./harness.mjs')).errors(s) |
| 153 | .filter((e) => !/50[23]|Bad Gateway|Failed to load resource/i.test(e)); |
| 154 | check(errs.length === 0, 'no console errors beyond the offline gateway', |
| 155 | JSON.stringify(errs.slice(0, 2))); |
| 156 | } finally { |
| 157 | await s.close(); |
| 158 | } |
| 159 | |
| 160 | console.log(bad ? `\n${bad} failed` : '\nall checks passed'); |
| 161 | process.exit(bad ? 1 : 0); |