Oregami
Repositories/oxedyne/daimond

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.
34import fs from 'node:fs';
35import { open, scratch } from './harness.mjs';
36
37const PROFILE = scratch('pw', 'rawreads');
38fs.rmSync(PROFILE, { recursive: true, force: true });
39
40let bad = 0;
41const 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.
48const NUMBERED = /(^|\n)\d+\t/;
49
50const s = await open({ name: 'rawreads', profile: PROFILE, connect: false });
51const { page } = s;
52
53try {
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
160console.log(bad ? `\n${bad} failed` : '\nall checks passed');
161process.exit(bad ? 1 : 0);