Oregami
Repositories/oxedyne/daimond

oxedyne/daimond/dev/verify_attachroot.mjs

11.6 KiB, 1 run

created by r2519314175:243, 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_attachroot.mjs — an attachment remembers which workspace it was made in.
2//
3// THE DEFECT THIS PINS. An attachment was recorded as a bare path. A folder
4// attached while the MACHINE workspace was open therefore came back as
5// `books/elearnity/CheapThinking` with nothing saying where that was, and when the
6// Diamond was next opened against the BROWSER sandbox the path went into the
7// scope anyway: `diamond_bounds` allowed it, the tool door let the daimon reach
8// for it, and the store answered `NotFoundError`. Allowed and absent. The daimon
9// reported an empty book-shaped container and offered to write that into the
10// crystal, which is what a person reads as "my Diamond is broken" — when in fact
11// the Diamond was fine and the link had come loose from its root.
12//
13// FOUR PROPERTIES, and the fourth is the one that protects the user's existing work:
14//
15// 1. An attachment RECORDS the workspace it was made in.
16// 2. One recorded against the other workspace is NOT in the scope handed to the
17// engine — no phantom permission to somewhere that is not there.
18// 3. It is SHOWN as being elsewhere, not silently dropped. It is the user's own
19// attachment; they may simply have the other workspace closed.
20// 4. An attachment written BEFORE roots were recorded still works — in scope,
21// and still detachable by the button that now writes a root. Refusing those
22// would empty the workspace of every Diamond that predates this.
23//
24// EACH CHECK IS PROVED AGAINST BROKEN CODE FIRST. `--break <name>` serves a
25// damaged copy of daimond.js to the real page through `page.route`; the run is
26// then expected to FAIL. A break whose anchor does not match aborts rather than
27// passing quietly — a check proved against code that was never broken is not
28// proved at all.
29//
30// node dev/verify_attachroot.mjs --break rootless # 1 fails: no root recorded
31// node dev/verify_attachroot.mjs --break noscope # 2 fails: elsewhere is in scope
32// node dev/verify_attachroot.mjs --break silent # 3 fails: says nothing about where
33// node dev/verify_attachroot.mjs --break strict # 4 fails: legacy links dropped
34// node dev/verify_attachroot.mjs # and then, clean
35import fs from 'node:fs';
36import path from 'node:path';
37import { fileURLToPath } from 'node:url';
38import { open, shot } from './harness.mjs';
39
40const HERE = path.dirname(fileURLToPath(import.meta.url));
41const WWW = path.join(HERE, '..', 'www');
42
43const BREAK = (() => {
44 const i = process.argv.indexOf('--break');
45 return i > 0 ? String(process.argv[i + 1] || '') : '';
46})();
47
48const BREAKS = {
49 // The old behaviour: a path with no workspace on it.
50 rootless: {
51 file: 'js/daimond.js',
52 find: ` function rootedRef(kind, path) {
53 var r = currentRoot();
54 return kind + ':[' + (r.kind === 'machine' ? 'machine:' + r.name : 'browser')
55 + ']' + path;
56 }`,
57 with: ` function rootedRef(kind, path) {
58 return kind + ':' + path;
59 }`,
60 },
61 // The scope stops filtering, so a path from the other workspace is granted.
62 noscope: {
63 file: 'js/daimond.js',
64 find: ` var here = list.filter(function (a) { return a.here !== false; });`,
65 with: ` var here = list;`,
66 },
67 // The tile stops saying where an unreachable attachment lives.
68 silent: {
69 file: 'js/daimond.js',
70 find: ` openBtn.title = away
71 ? t('dws.not_here', { where: refWhere(l.other) })`,
72 with: ` openBtn.title = away
73 ? (l.rel || '')`,
74 },
75 // Legacy rootless links treated as unreachable, which empties old Diamonds.
76 strict: {
77 file: 'js/daimond.js',
78 find: ` if (!p.root) return true; // written before roots were recorded`,
79 with: ` if (!p.root) return false;`,
80 },
81};
82
83if (BREAK && !BREAKS[BREAK]) {
84 console.error(`unknown break '${BREAK}'; one of: ${Object.keys(BREAKS).join(', ')}`);
85 process.exit(2);
86}
87
88let ok = 0, bad = 0;
89const check = (name, pass, detail) => {
90 if (pass) { ok++; console.log(` ok ${name}${detail ? ' — ' + detail : ''}`); }
91 else { bad++; console.log(` FAIL ${name}${detail ? ' — ' + detail : ''}`); }
92};
93
94const s = await open({ name: 'attachroot', signIn: false, connect: false });
95const { page } = s;
96
97if (BREAK) {
98 const spec = BREAKS[BREAK];
99 const src = fs.readFileSync(path.join(WWW, spec.file), 'utf8');
100 const n = src.split(spec.find).length - 1;
101 if (n !== 1) {
102 console.error(`break '${BREAK}': the anchor appears ${n} times in ${spec.file}, `
103 + 'so nothing was broken and the run below would prove nothing.');
104 process.exit(2);
105 }
106 const body = src.replace(spec.find, spec.with);
107 await page.route('**/' + spec.file, r => r.fulfill({
108 status: 200, contentType: 'application/javascript', body,
109 }));
110}
111
112// The stub has to be in place before the page loads it.
113await page.goto(process.env.DAIMOND_APP || 'http://localhost:8777', { waitUntil: 'domcontentloaded' });
114const { signInAs, connectMock } = await import('./harness.mjs');
115await signInAs(s, 'attachroot');
116await connectMock(s);
117await page.waitForTimeout(1500);
118
119// A Diamond, made the way a person makes one so the rail knows about it.
120await page.click('#new-diamond-btn', { force: true });
121await page.waitForSelector('.dlg-input', { timeout: 10000 });
122await page.fill('.dlg-input', 'Roots');
123await page.click('.dlg-ok', { force: true });
124await page.waitForTimeout(2000);
125await page.$$eval('.diamond-box', els => els[0] && els[0].click());
126await page.waitForTimeout(1200);
127
128const id = await page.evaluate(async () => {
129 const m = await import('/pkg/oxedyne_daimond.js');
130 const app = new m.DaimondApp('http://127.0.0.1/v1/chat/completions', '', 'none', 4096, '', true);
131 const d = JSON.parse(await app.list_diamonds()).find(x => x.name === 'Roots');
132 return d ? d.id : '';
133});
134check('a Diamond to attach things to', !!id, id);
135
136// ── 1. Attaching records the workspace ───────────────────────────────
137// Through the app's own control, not by writing a link: what is under test is
138// what the BUTTON records.
139await page.evaluate(async () => {
140 const m = await import('/pkg/oxedyne_daimond.js');
141 const app = new m.DaimondApp('http://127.0.0.1/v1/chat/completions', '', 'none', 4096, '', true);
142 await app.run_tool('file_write', JSON.stringify({ path: 'papers/one.md', content: '# one\n' }));
143});
144await page.evaluate(() => DaimondPanels.show('work'));
145await page.waitForTimeout(700);
146// The tree is listed when the panel opens, so a file written behind its back is
147// not in it. Press the panel's own refresh rather than filtering: the filter
148// surfaces the FILE, and a file row carries no attach control — which is the gap
149// notes4 asks to close and is not what this verifier is about.
150await page.click('#panel-work [data-act="refresh"]', { force: true }).catch(() => {});
151await page.waitForTimeout(1200);
152let attachedIt = false;
153for (const row of await page.$$('#panel-work .files-row')) {
154 const nm = await row.$eval('.files-name', e => e.textContent).catch(() => '');
155 // The row's name carries a folder glyph before the name — matched on the
156 // name, not on the decoration.
157 if (nm.replace(/^[^A-Za-z0-9._-]+/, '').trim() === 'papers') {
158 // The paperclip superseded this ◈ on 2026-08-11 (ATTACH_CONTRACT.md §8) --
159 // same control, new class.
160 const hold = await row.$('.attach-btn');
161 if (hold) { await hold.click({ force: true }); attachedIt = true; }
162 break;
163 }
164}
165check('the folder could be attached through its own control', attachedIt,
166 attachedIt ? '' : 'no papers row with an attach control');
167await page.waitForTimeout(1200);
168
169const refs = await page.evaluate(async (did) => {
170 const m = await import('/pkg/oxedyne_daimond.js');
171 const app = new m.DaimondApp('http://127.0.0.1/v1/chat/completions', '', 'none', 4096, '', true);
172 return JSON.parse(await app.links_touching('diamond:' + did) || '[]').map(l => l.other);
173}, id);
174const made = refs.find(r => /papers/.test(r)) || '';
175check('THE ATTACHMENT RECORDS THE WORKSPACE IT WAS MADE IN',
176 /^dir:\[browser\]papers/.test(made), made || 'nothing was attached');
177
178// ── 2 and 3. An attachment from the OTHER workspace ──────────────────
179// Written directly, because the only other way to make one is to open a real
180// folder, and a native directory picker cannot be driven from a test.
181await page.evaluate(async (did) => {
182 const m = await import('/pkg/oxedyne_daimond.js');
183 const app = new m.DaimondApp('http://127.0.0.1/v1/chat/completions', '', 'none', 4096, '', true);
184 await app.add_link(did, 'diamond:' + did, 'dir:[machine:usr]books/elearnity/CheapThinking',
185 'holds', '', 'user');
186 // And one written before roots existed at all, which must keep working.
187 await app.add_link(did, 'diamond:' + did, 'dir:legacy/notes', 'holds', '', 'user');
188}, id);
189await page.waitForTimeout(900);
190
191const scope = await page.evaluate(async (did) => await window.DaimondDiamond.bounds(did), id);
192check('THE OTHER WORKSPACE IS NOT IN THE SCOPE HANDED TO THE ENGINE',
193 !scope.attached.some(p => /CheapThinking/.test(p)),
194 JSON.stringify(scope.attached));
195check('and what IS here still is',
196 scope.attached.some(p => /^papers/.test(p)),
197 JSON.stringify(scope.attached));
198check('A LINK WRITTEN BEFORE ROOTS WERE RECORDED IS STILL IN SCOPE',
199 scope.attached.some(p => p === 'legacy/notes'),
200 JSON.stringify(scope.attached));
201
202// It is shown, and it says where it lives.
203await page.evaluate(() => {
204 const strip = document.getElementById('arte-strip');
205 if (strip) { strip.dataset.open = '1'; }
206});
207await page.evaluate(() => window.DaimondArtefacts && DaimondArtefacts.render());
208await page.waitForTimeout(900);
209const tiles = await page.$$eval('.arte-open', els => els.map(e => ({
210 text: e.textContent.trim(), title: e.getAttribute('title') || '',
211 away: e.classList.contains('away'),
212})));
213const stray = tiles.find(x => /CheapThinking/.test(x.text)) || {};
214check('THE UNREACHABLE ONE IS SHOWN, NOT SILENTLY DROPPED',
215 !!stray.text, JSON.stringify(tiles.map(t => t.text)));
216check('and it says WHERE it lives rather than reading as empty',
217 /machine|folder/i.test(stray.title || ''), JSON.stringify(stray.title));
218check('and it is marked as being elsewhere', stray.away === true, JSON.stringify(stray));
219const mine = tiles.find(x => /papers/.test(x.text)) || {};
220check('while an attachment in THIS workspace is not marked away',
221 mine.away === false, JSON.stringify(mine));
222
223// ── 4. The toggle still finds a legacy link ──────────────────────────
224const before = await page.evaluate(async (did) => {
225 const m = await import('/pkg/oxedyne_daimond.js');
226 const app = new m.DaimondApp('http://127.0.0.1/v1/chat/completions', '', 'none', 4096, '', true);
227 return JSON.parse(await app.links_touching('diamond:' + did) || '[]').length;
228}, id);
229await page.evaluate(async () => {
230 // The same path the old link names, offered to the control that now writes a root.
231 const rows = [...document.querySelectorAll('.arte-row')];
232 const row = rows.find(r => /legacy\/notes/.test(r.textContent));
233 const drop = row && row.querySelector('.arte-drop');
234 if (drop) drop.click();
235});
236await page.waitForTimeout(1000);
237const after = await page.evaluate(async (did) => {
238 const m = await import('/pkg/oxedyne_daimond.js');
239 const app = new m.DaimondApp('http://127.0.0.1/v1/chat/completions', '', 'none', 4096, '', true);
240 const ls = JSON.parse(await app.links_touching('diamond:' + did) || '[]');
241 return { n: ls.length, refs: ls.map(l => l.other) };
242}, id);
243check('A LEGACY ATTACHMENT CAN STILL BE TAKEN OFF',
244 after.n === before - 1 && !after.refs.some(r => /legacy\/notes/.test(r)),
245 `${before} → ${after.n}: ${JSON.stringify(after.refs)}`);
246
247await shot(s, 'attachroot');
248console.log(`\n${ok} ok, ${bad} failed`);
249await s.browser.close();
250process.exit(bad ? 1 : 0);