Oregami
Repositories/oxedyne/daimond

oxedyne/daimond/dev/verify_system.mjs

9.9 KiB, 1 run

created by r2519314175:721, 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_system.mjs — phase F: Daimond's own store is visible, and the rules
2// come in two layers.
3//
4// Notes2 asks two questions with one answer: *"where are all the system files
5// like DAIMOND.md??"* and *"All these settings should exist as text files in an
6// intuitive system directory hierarchy."*
7//
8// Six properties:
9//
10// 1. The Workspace panel has a System section, and it lists the store —
11// `diamonds/`, `prompts/`, `DAIMOND.md` — which `renderTree` deliberately
12// filters out of the tree above it (a × beside `diamonds/` would delete
13// every Diamond you have).
14// 2. It is a section, NOT the tree: nothing from the store leaks into the
15// workspace listing, and nothing in the section carries a delete control.
16// 3. Its rows open in the Doc panel and read from the STORE, which is a
17// different root from the workspace whenever a folder is open.
18// 4. A role prompt lives in the store, so it does not change under the user
19// when they open a folder. This was the deferral's whole reason: doing the
20// instructions half alone would leave the user's rules surviving a root
21// switch while the agent's own prompt silently changed.
22// 5. `DAIMOND.md` is TWO layers: the user's own from the store, always in
23// force, and the project's from the folder, appended below it under a
24// heading that says whose it is.
25// 6. With no folder open there is one layer, not the same file counted twice.
26//
27// Properties 5 and 6 are measured through `DaimondPrompts`/`Instructions`'
28// composed text rather than from a screenshot, because what matters is what
29// reaches the MODEL.
30//
31// node dev/verify_system.mjs
32// node dev/verify_system.mjs --break hidden # the store stays invisible
33// node dev/verify_system.mjs --break onelayer # the project's copy wins outright
34//
35// Needs dev/serve.mjs (dev/world.sh N --up). No gateway and no model needed,
36// but a connected provider makes the Diamond path work.
37import fs from 'node:fs';
38import path from 'node:path';
39import os from 'node:os';
40import { open, connectMock, scratch, shot } from './harness.mjs';
41
42const OUT = path.join(os.homedir(), '.cache/daimond/system-shots');
43fs.mkdirSync(OUT, { recursive: true });
44
45const BI = process.argv.indexOf('--break');
46const BEQ = process.argv.find(a => a.startsWith('--break='));
47const BREAK = BEQ ? BEQ.split('=')[1] : (BI >= 0 ? (process.argv[BI + 1] || '') : '');
48
49let failures = 0;
50const check = (cond, msg, detail) => {
51 console.log((cond ? ' ok ' : ' FAIL ') + msg + (detail != null ? ' — ' + detail : ''));
52 if (!cond) failures++;
53};
54
55const MODEL = 'accounts/fireworks/models/glm-5p2';
56
57const s = await open({ name: 'system', profile: scratch('pw', 'system-' + process.pid) });
58const { page: p } = s;
59try {
60 await connectMock(s, { model: MODEL });
61 if (BREAK) console.log(` .. running with --break ${BREAK}`);
62 if (BREAK === 'hidden') {
63 // The section removed: what the panel looked like before this phase, when
64 // the only answer to "where is DAIMOND.md" was "in a place you cannot see".
65 await p.addStyleTag({ content: '#sys-sec { display: none !important; }' });
66 }
67
68 // Something in the store to look at: a Diamond, and a role prompt seeded from
69 // its shipped default.
70 await p.evaluate(() => document.getElementById('new-diamond-btn').click());
71 await p.waitForSelector('.dlg-card', { timeout: 8000 });
72 await p.evaluate(() => {
73 const c = [...document.querySelectorAll('.dlg-card')]
74 .filter(x => x.getClientRects().length).pop();
75 const i = c.querySelector('input.dlg-input');
76 i.value = 'Dee';
77 i.dispatchEvent(new Event('input', { bubbles: true }));
78 c.querySelector('.dlg-ok').click();
79 });
80 await p.waitForTimeout(1200);
81 await p.evaluate(() => DaimondPrompts.edit('compactor'));
82 await p.waitForTimeout(1800);
83
84 // ══ 1. The section lists the store ════════════════════════════════
85 // Opened the way a person does: the section is collapsed until asked for.
86 const collapsed = await p.evaluate(() => {
87 const h = document.getElementById('sys-head');
88 return h ? h.getAttribute('aria-expanded') : null;
89 });
90 await p.evaluate(() => { const h = document.getElementById('sys-head'); if (h) h.click(); });
91 await p.waitForTimeout(700);
92 const sec = await p.evaluate(() => {
93 const el = document.getElementById('sys-sec');
94 if (!el) return null;
95 return {
96 shown: el.getClientRects().length > 0,
97 rows: [...document.querySelectorAll('#sys-tree .sys-row')]
98 .map(r => (r.querySelector('.files-name') || r).textContent.trim()),
99 };
100 });
101 check(sec !== null, 'the Workspace panel has a System section');
102 check(!!(sec && sec.shown), 'and it is on screen');
103 check(collapsed === 'false',
104 'collapsed by default — it answers a question rather than being where the work is',
105 String(collapsed));
106 check(!!(sec && sec.rows.some(r => /diamonds/.test(r))),
107 'diamonds/ is listed', sec && sec.rows.join(', '));
108 check(!!(sec && sec.rows.some(r => /prompts/.test(r))),
109 'prompts/ is listed', sec && sec.rows.join(', '));
110
111 // ══ 2. It is a section, not the tree ══════════════════════════════
112 const tree = await p.evaluate(() =>
113 [...document.querySelectorAll('.files-tree .files-row')]
114 .map(r => (r.querySelector('.files-name') || r).textContent.trim()));
115 check(!tree.some(r => /diamonds/.test(r)),
116 'and the workspace tree above it still does NOT list the store',
117 tree.join(', ') || '(empty)');
118 const dels = await p.evaluate(() =>
119 [...document.querySelectorAll('#sys-tree .sys-row')]
120 .filter(r => r.querySelector('.files-del')
121 && getComputedStyle(r.querySelector('.files-del')).display !== 'none').length);
122 check(dels === 0, 'no row in the section carries a delete control', String(dels));
123
124 // ══ 3. A row opens, and reads from the STORE ══════════════════════
125 const opened = await p.evaluate(async () => {
126 const row = [...document.querySelectorAll('#sys-tree .sys-row')]
127 .find(r => /prompts/.test(r.textContent || ''));
128 if (!row) return 'no prompts row';
129 row.click();
130 return 'ok';
131 });
132 check(opened === 'ok', 'the prompts/ row can be opened', opened);
133 await p.waitForTimeout(900);
134 const inside = await p.evaluate(() =>
135 [...document.querySelectorAll('#sys-tree .sys-row')]
136 .map(r => (r.querySelector('.files-name') || r).textContent.trim()));
137 check(inside.some(r => /compactor\.md/.test(r)),
138 'and the role prompt is inside it', inside.join(', '));
139
140 // ══ 4. A role prompt lives in the store, not the folder ═══════════
141 //
142 // Proved where it can be: the file is READ through the store door. A real
143 // folder cannot be opened here (`showDirectoryPicker` needs a user gesture
144 // the harness cannot supply), so what is checked is that the read goes to
145 // the store at all — which is what makes it survive a root switch.
146 const promptSrc = await p.evaluate(async () => {
147 const W = await import('/pkg/oxedyne_daimond.js');
148 let store = null, ws = null;
149 try { store = await W.store_read('prompts/compactor.md'); } catch (e) { store = null; }
150 try { ws = await W.read_file('prompts/compactor.md'); } catch (e) { ws = null; }
151 return { store: (store || '').slice(0, 60), ws: (ws || '').slice(0, 60),
152 held: (DaimondPrompts.md.compactor || '').slice(0, 60) };
153 });
154 check(!!promptSrc.store, 'the compactor prompt is written in the store',
155 promptSrc.store.slice(0, 40));
156 check(promptSrc.held === promptSrc.store,
157 'and what the app holds is what the STORE says', promptSrc.held.slice(0, 40));
158
159 // ══ 5–6. DAIMOND.md in two layers ═════════════════════════════════
160 const layers = await p.evaluate(async (brk) => {
161 const W = await import('/pkg/oxedyne_daimond.js');
162 await W.store_write('DAIMOND.md', 'Always answer in British English.');
163 const I = window.DaimondInstructions;
164 if (!I) return null;
165 // One layer: no folder is open, so the project's copy is the same file and
166 // must not be counted twice.
167 await I.refresh();
168 const one = { md: I.md, mine: I.mine, theirs: I.theirs };
169 // Two layers, simulated at the seam the folder would arrive through. A real
170 // folder needs a user gesture the harness cannot make; what is under test is
171 // the COMPOSITION, and that is this function.
172 I.theirs = brk === 'onelayer' ? 'Use tabs, not spaces.' : 'Use tabs, not spaces.';
173 if (brk === 'onelayer') I.mine = ''; // the project's wins outright
174 const two = { md: I.layered(), mine: I.mine, theirs: I.theirs };
175 return { one, two };
176 }, BREAK);
177 if (!layers) {
178 check(false, 'the instructions module is reachable');
179 } else {
180 check(layers.one.mine === 'Always answer in British English.',
181 'your own DAIMOND.md is read from the store', layers.one.mine);
182 check(layers.one.theirs === '',
183 'with no folder open there is no second layer', `"${layers.one.theirs}"`);
184 check(layers.one.md === layers.one.mine,
185 'so the composed text is exactly yours, not yours twice over',
186 layers.one.md);
187 check(/British English/.test(layers.two.md),
188 'with a folder open, YOUR rules are still in force', layers.two.md.slice(0, 80));
189 check(/Use tabs/.test(layers.two.md),
190 'and the project’s are too', layers.two.md.slice(0, 120));
191 check(layers.two.md.indexOf('British English') < layers.two.md.indexOf('Use tabs'),
192 'yours first: a project cannot quietly overrule how you work',
193 layers.two.md.replace(/\n+/g, ' / '));
194 check(/## For this project/.test(layers.two.md),
195 'and the project’s are headed, so a model can tell whose is whose',
196 layers.two.md.replace(/\n+/g, ' / '));
197 }
198 await shot(s, 'system');
199
200} catch (e) {
201 check(false, 'the run finished', String(e && e.message || e));
202 try { await shot(s, 'threw'); } catch {}
203} finally {
204 await s.close();
205}
206
207console.log(failures === 0
208 ? `\nverify_system: all checks pass.`
209 : `\nverify_system: ${failures} failed.`);
210process.exit(failures === 0 ? 0 : 1);