Oregami
Repositories/oxedyne/daimond

oxedyne/daimond/dev/verify_links.mjs

20.7 KiB, 1 run

created by r2519314175:507, 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_links.mjs — the link substrate: one stored record, found from both ends.
2//
3// Links are the layer under whatever gets built on them, so what matters here is not a
4// feature but the guarantees the rest will rest on: that a link is stored ONCE and still
5// found from either end, that its ends can name things that are not Diamonds, that direction
6// survives even though both ends find it, that provenance says who drew it, and that an
7// agent can read and write the sidecar with the file tools it already has — because if it
8// cannot, "agents read and write the graph" needs a whole tool surface that does not exist.
9//
10// The second half drives the association UI itself -- the real form, the real picker, the
11// real suggestion chips -- because a substrate that is right underneath a control nobody
12// can complete is still a feature nobody has.
13//
14// Run with dev/serve.mjs up. No gateway needed.
15import { open, signInAs, shot } from './harness.mjs';
16
17const ok = [], bad = [];
18const check = (name, pass, detail) => {
19 (pass ? ok : bad).push(name + (detail ? ' — ' + detail : ''));
20 console.log((pass ? ' ok ' : ' FAIL ') + name + (detail ? ' — ' + detail : ''));
21};
22
23const s = await open({ name: 'links', connect: false });
24const p = s.page;
25await p.waitForTimeout(2500); // open() has already signed in; wait for the app to settle
26
27// ── Two Diamonds to hang links between ────────────────────────────────────
28const ids = await p.evaluate(async () => {
29 const mod = await import('../pkg/oxedyne_daimond.js');
30 const app = new mod.DaimondApp('http://127.0.0.1/v1/chat/completions', '', 'none', 256, '', true);
31 const a = await app.create_diamond('Ship the launch');
32 const b = await app.create_diamond('Brand voice');
33 return { a, b };
34});
35check('two Diamonds were created', !!ids.a && !!ids.b, `${ids.a} / ${ids.b}`);
36
37const call = (fn, args = []) => p.evaluate(async ({ fn, args }) => {
38 const mod = await import('../pkg/oxedyne_daimond.js');
39 const app = new mod.DaimondApp('http://127.0.0.1/v1/chat/completions', '', 'none', 256, '', true);
40 return await app[fn](...args);
41}, { fn, args });
42
43// ── A link, asserted once ───────────────────────────────────────────────
44const linkId = await call('add_link', [
45 ids.a, `diamond:${ids.a}`, `diamond:${ids.b}`, 'Informs',
46 'The launch copy has to sound like this.', 'user',
47]);
48check('a link was asserted', typeof linkId === 'string' && linkId.length > 0, linkId);
49
50// ── Found from the end it was asserted from ─────────────────────────────
51const fromA = JSON.parse(await call('links_touching', [`diamond:${ids.a}`]));
52check('found from the end it was asserted from', fromA.length === 1, `${fromA.length} link(s)`);
53check('the relation was normalised, as a Diamond tag would be',
54 fromA[0]?.rel === 'informs', fromA[0]?.rel);
55check('the note was kept as written',
56 fromA[0]?.note === 'The launch copy has to sound like this.', fromA[0]?.note);
57check('provenance says who drew it', fromA[0]?.by === 'user', fromA[0]?.by);
58check('the owner is carried, so it can be deleted without a search',
59 fromA[0]?.owner === ids.a, fromA[0]?.owner);
60
61// ── And from the OTHER end, with no second copy stored ──────────────────
62const fromB = JSON.parse(await call('links_touching', [`diamond:${ids.b}`]));
63check('the same link is found from the far end', fromB.length === 1, `${fromB.length} link(s)`);
64check('it is the SAME record, not a mirrored copy', fromB[0]?.id === linkId, fromB[0]?.id);
65check('the far end is told which end is the other one',
66 fromB[0]?.other === `diamond:${ids.a}`, fromB[0]?.other);
67check('direction survived being found from the wrong end',
68 fromB[0]?.from === `diamond:${ids.a}` && fromB[0]?.to === `diamond:${ids.b}`,
69 `${fromB[0]?.from} -> ${fromB[0]?.to}`);
70
71// Only one record exists on disk, in one sidecar.
72const onDisk = await p.evaluate(async ({ a, b }) => {
73 const mod = await import('../pkg/oxedyne_daimond.js');
74 const read = async (path) => {
75 try { return await mod.read_file(path); } catch (e) { return null; }
76 };
77 return {
78 a: await read(`diamonds/${a}/.daimond/links.jsonl`),
79 b: await read(`diamonds/${b}/.daimond/links.jsonl`),
80 };
81}, ids);
82check('the record is stored once, in the asserting Diamond’s sidecar',
83 (onDisk.a || '').trim().split('\n').filter(Boolean).length === 1 && onDisk.b === null,
84 `a=${(onDisk.a || '').length}b b=${onDisk.b === null ? 'absent' : 'present'}`);
85
86// ── The node space is not Diamonds only ───────────────────────────────────
87await call('add_link', [ids.a, `diamond:${ids.a}`, 'file:notes/pricing.md', 'produced', '', 'agent:worker-1']);
88await call('add_link', [ids.a, `diamond:${ids.a}`, 'url:https://stripe.com/docs?a=1', 'consulted', '', 'user']);
89await call('add_link', [ids.a, `diamond:${ids.a}`, 'email:msg-99@example.com', 'from', '', 'user']);
90
91const all = JSON.parse(await call('links_touching', [`diamond:${ids.a}`]));
92check('a Diamond can link to a file', all.some(l => l.to === 'file:notes/pricing.md'));
93check('a Diamond can link to a page, colons in the URL and all',
94 all.some(l => l.to === 'url:https://stripe.com/docs?a=1'),
95 all.find(l => l.to.startsWith('url:'))?.to);
96check('a kind this build does not model is stored rather than refused',
97 all.some(l => l.to === 'email:msg-99@example.com'));
98check('an agent-asserted link says so',
99 all.find(l => l.to === 'file:notes/pricing.md')?.by === 'agent:worker-1');
100
101// A file is a node too, so the link is findable from the file's side.
102const fromFile = JSON.parse(await call('links_touching', ['file:notes/pricing.md']));
103check('a link is findable from the file end as well',
104 fromFile.length === 1 && fromFile[0].other === `diamond:${ids.a}`, `${fromFile.length} link(s)`);
105
106// ── What is refused ─────────────────────────────────────────────────────
107const selfLink = await call('add_link', [ids.a, `diamond:${ids.a}`, `diamond:${ids.a}`, '', '', 'user'])
108 .then(() => 'accepted').catch(() => 'refused');
109check('a link from a thing to itself is refused', selfLink === 'refused', selfLink);
110
111const badRef = await call('add_link', [ids.a, `diamond:${ids.a}`, 'not a reference', '', '', 'user'])
112 .then(() => 'accepted').catch(() => 'refused');
113check('an end that is not a kind:rest reference is refused', badRef === 'refused', badRef);
114
115// ── An agent can reach the sidecar with the tools it already has ────────
116//
117// This is the load-bearing one. The sidecar sits inside `.daimond/`, and if the tool
118// fence covered it, agents could not touch the graph at all without a new tool surface.
119const agent = await p.evaluate(async ({ a }) => {
120 const mod = await import('../pkg/oxedyne_daimond.js');
121 const app = new mod.DaimondApp('http://127.0.0.1/v1/chat/completions', '', 'none', 256, '', true);
122 const path = `diamonds/${a}/.daimond/links.jsonl`;
123 const read = await app.run_tool('file_read', JSON.stringify({ path }));
124 // Append a link the way an agent would: by writing the file.
125 const line = JSON.stringify({
126 id: 'agent-made-1', ts: 1700000000000,
127 from: `diamond:${a}`, to: 'file:notes/agent-wrote-this.md',
128 rel: 'produced', note: 'written straight into the sidecar', by: 'agent:tester',
129 });
130 const wrote = await app.run_tool('file_write',
131 JSON.stringify({ path, content: read.replace(/\n*$/, '\n') + line + '\n' }));
132 return { read, wrote };
133}, ids);
134check('an agent can READ the sidecar with file_read',
135 /diamond:/.test(agent.read || ''), (agent.read || '').slice(0, 40));
136check('an agent can WRITE the sidecar with file_write',
137 !/error|refus|denied/i.test(agent.wrote || ''), (agent.wrote || '').slice(0, 60));
138
139const afterAgent = JSON.parse(await call('links_touching', [`diamond:${ids.a}`]));
140check('the link the agent wrote is read back by the store',
141 afterAgent.some(l => l.id === 'agent-made-1' && l.by === 'agent:tester'),
142 `${afterAgent.length} link(s)`);
143
144// ── A hand-edited sidecar is forgiving ──────────────────────────────────
145const handEdit = await p.evaluate(async ({ a }) => {
146 const mod = await import('../pkg/oxedyne_daimond.js');
147 const path = `diamonds/${a}/.daimond/links.jsonl`;
148 const text = await mod.read_file(path);
149 // A person edits this file. One line comes out mangled.
150 await mod.write_file(path, text + 'oops, half a line{\n'
151 + JSON.stringify({ from: `diamond:${a}`, to: 'file:by-hand.md', rel: 'noted' }) + '\n');
152 return true;
153}, ids);
154check('the sidecar was hand-edited', handEdit === true);
155
156const afterHand = JSON.parse(await call('links_touching', [`diamond:${ids.a}`]));
157check('a mangled line does not take the other links with it',
158 afterHand.length === afterAgent.length + 1, `${afterHand.length} link(s)`);
159check('a link written by hand with only its two ends still counts',
160 afterHand.some(l => l.to === 'file:by-hand.md'));
161
162// ── Removal ─────────────────────────────────────────────────────────────
163const removed = await call('remove_link', [ids.a, linkId]);
164check('a link can be removed', removed === true, String(removed));
165const afterRemove = JSON.parse(await call('links_touching', [`diamond:${ids.b}`]));
166check('and is then gone from the far end too', afterRemove.length === 0,
167 `${afterRemove.length} link(s)`);
168const removeAgain = await call('remove_link', [ids.a, linkId]);
169check('removing what is not there is false, not an error', removeAgain === false);
170
171// ── Deleting a Diamond takes its links with it ────────────────────────────
172await call('delete_diamond', [ids.a]);
173const afterDelete = JSON.parse(await call('links_touching', ['file:notes/pricing.md']));
174check('deleting a Diamond takes the links in its sidecar with it',
175 afterDelete.length === 0, `${afterDelete.length} link(s)`);
176
177// ── The UI: the same record, made and read from both ends ────────────────
178//
179// Everything above went straight at the store. This drives what a person drives: the
180// Links section on the crystal, the picker that finds the other Diamond by name, the three
181// suggested relations, the note, and the × that takes the link away again. The direction
182// is the load-bearing part -- the row on the Diamond the link was asserted FROM must read
183// `out`, and the very same record must show as `in` on the other one.
184
185// Three Diamonds to link between, made through the store rather than through the rail's
186// "+": that dialog will not complete without a model, and this session deliberately has
187// none. Then a clean page, so the rail holds what the store holds.
188for (const n of ['Ship the console', 'Pricing research', 'Support playbook']) {
189 await call('create_diamond', [n]);
190}
191await p.reload({ waitUntil: 'domcontentloaded' });
192await signInAs(s, 'links');
193await p.waitForTimeout(1800);
194
195/// Put a Diamond in the Centre by clicking its box in the rail.
196async function selectDiamond(name) {
197 await p.$$eval('.diamond-box', (els, n) => {
198 for (const e of els) {
199 const t = (e.querySelector('.session-box-name') || {}).textContent;
200 if (t === n) { e.click(); return; }
201 }
202 }, name);
203 await p.waitForTimeout(900);
204}
205/// Open the Links section, which renders no rows while it is shut.
206async function expandLinks() {
207 await p.waitForSelector('#link-strip', { timeout: 10000 });
208 const shut = await p.$eval('#link-strip', e => e.getAttribute('aria-expanded') !== 'true');
209 if (shut) { await p.click('#link-strip', { force: true }); await p.waitForTimeout(700); }
210}
211const rows = () => p.$$eval('.link-row', els => els.map(e => ({
212 lid: e.dataset.linkId,
213 dir: e.dataset.dir,
214 other: e.dataset.other,
215 cls: e.getAttribute('class'),
216 rel: (e.querySelector('.link-rel') || {}).textContent ?? null,
217 name: (e.querySelector('.link-other') || {}).textContent ?? null,
218 note: (e.querySelector('.link-note') || {}).textContent ?? null,
219 hasNoteBtn: !!e.querySelector('.link-note-btn'),
220})));
221
222const railIds = await p.$$eval('.diamond-box', els => els.map(e => ({
223 id: e.dataset.id, name: (e.querySelector('.session-box-name') || {}).textContent,
224})));
225const idOf = (n) => (railIds.find(r => r.name === n) || {}).id;
226check('three Diamonds are in the rail to link between',
227 !!idOf('Ship the console') && !!idOf('Pricing research') && !!idOf('Support playbook'),
228 railIds.map(r => r.name).join(', '));
229
230await selectDiamond('Ship the console');
231check('the Links section is on the crystal, whether or not there are links',
232 await p.isVisible('#link-sec'));
233check('a shut section renders no rows', (await rows()).length === 0
234 && await p.$eval('#link-body', e => e.style.display === 'none'), 'body hidden');
235await expandLinks();
236check('the strip opens the section', await p.$eval('#link-strip', e => e.getAttribute('aria-expanded') === 'true'),
237 await p.textContent('#link-strip'));
238check('an empty section still offers the way in to making one', await p.isVisible('#link-add'));
239
240// ── Making one through the form ─────────────────────────────────────────
241await p.click('#link-add', { force: true });
242await p.waitForSelector('#link-form', { timeout: 5000 });
243check('the add control becomes the form', await p.isVisible('#link-form')
244 && await p.isVisible('#link-pick') && await p.isVisible('#link-rel')
245 && await p.isVisible('#link-note') && await p.isVisible('#link-save'));
246check('the form says which way the link will run', /\S/.test(await p.textContent('#link-says')),
247 (await p.textContent('#link-says')).slice(0, 60));
248
249const sugs = await p.$$eval('#link-rel-sug .link-sug', els => els.map(e => e.dataset.rel));
250check('exactly three relations are suggested, and none enforced',
251 JSON.stringify(sugs) === JSON.stringify(['part-of', 'relates-to', 'derives-from']), JSON.stringify(sugs));
252
253await p.fill('#link-pick', 'pricing');
254await p.waitForTimeout(500);
255const hits = await p.$$eval('.link-pick-hit', els => els.map(e => ({ id: e.dataset.id, name: e.textContent })));
256check('typing part of a name finds the Diamond, and offers only the others',
257 hits.length === 1 && hits[0].id === idOf('Pricing research'), JSON.stringify(hits));
258await p.click('.link-pick-hit', { force: true });
259await p.waitForTimeout(400);
260check('picking one replaces the search with the choice, and a way to change it',
261 await p.isVisible('#link-chosen') && await p.isVisible('#link-change')
262 && (await p.textContent('#link-chosen')).includes('Pricing research'));
263
264await p.click('.link-sug[data-rel="part-of"]', { force: true });
265await p.waitForTimeout(300);
266// A link carries a SET of relations now, edited as the Graph edits the same
267// field: a suggestion becomes a chip ON the link and leaves the box clear for
268// the next word, rather than filling the box in and being the only one.
269const chosenRels = await p.$$eval('#link-rel-chips .tag-chip',
270 els => els.map(e => e.textContent.replace(/×$/, '').trim()));
271check('a suggestion chip puts that relation on the link, and clears the box for another',
272 chosenRels.join('|') === 'part-of' && await p.inputValue('#link-rel') === '',
273 JSON.stringify(chosenRels));
274await p.fill('#link-note', 'the pricing work feeds the console rollout');
275await p.click('#link-save', { force: true });
276await p.waitForTimeout(1200);
277
278const outRows = await rows();
279check('the link appears on the Diamond it was asserted from, pointing out',
280 outRows.length === 1 && outRows[0].dir === 'out' && /link-row-out/.test(outRows[0].cls),
281 JSON.stringify(outRows[0]));
282check('the row names the far end and the relation',
283 outRows[0]?.rel === 'part-of' && outRows[0]?.name === 'Pricing research'
284 && outRows[0]?.other === `diamond:${idOf('Pricing research')}`,
285 `${outRows[0]?.rel} → ${outRows[0]?.name}`);
286check('a link with a note offers the control that shows it', outRows[0]?.hasNoteBtn === true);
287check('the strip carries the count', /1/.test(await p.textContent('#link-strip')),
288 await p.textContent('#link-strip'));
289const madeId = outRows[0]?.lid;
290await shot(s, 'links-out-row');
291
292// ── The same record, read from the far end ──────────────────────────────
293await selectDiamond('Pricing research');
294await expandLinks();
295const inRows = await rows();
296check('the far end shows the SAME record, pointing in',
297 inRows.length === 1 && inRows[0].lid === madeId && inRows[0].dir === 'in'
298 && /link-row-in/.test(inRows[0].cls), JSON.stringify(inRows[0]));
299check('and names the Diamond at the other end of it',
300 inRows[0]?.other === `diamond:${idOf('Ship the console')}` && inRows[0]?.name === 'Ship the console',
301 inRows[0]?.name);
302
303// ── A relation of the user's own words ──────────────────────────────────
304await p.click('#link-add', { force: true });
305await p.waitForSelector('#link-form', { timeout: 5000 });
306await p.fill('#link-pick', 'playbook');
307await p.waitForTimeout(500);
308await p.click('.link-pick-hit', { force: true });
309await p.waitForTimeout(300);
310await p.fill('#link-rel', ' Feeds INTO ');
311await p.click('#link-save', { force: true });
312await p.waitForTimeout(1200);
313const freeRows = await rows();
314const free = freeRows.find(r => r.lid !== madeId);
315check('a relation nobody suggested is accepted', !!free && freeRows.length === 2,
316 `${freeRows.length} row(s)`);
317check('and is normalised to lowercase, the way a tag is',
318 free?.rel === 'feeds into', JSON.stringify(free?.rel));
319
320// ── Shut again, with links to shut over ─────────────────────────────────
321// The rows are built only while the section is open, so closing it has to take them out of
322// the document rather than merely hide them -- with two links stored, this can tell.
323await p.click('#link-strip', { force: true });
324await p.waitForTimeout(700);
325check('closing the section takes the rows out of the document, not just out of sight',
326 (await rows()).length === 0 && await p.$eval('#link-body', e => e.style.display === 'none'),
327 `${(await rows()).length} row(s) in the DOM with 2 links stored`);
328await expandLinks();
329check('and opening it again brings them back', (await rows()).length === 2,
330 `${(await rows()).length} row(s)`);
331
332// ── Escape leaves the form without asking anything ──────────────────────
333await p.click('#link-add', { force: true });
334await p.waitForSelector('#link-form', { timeout: 5000 });
335await p.fill('#link-pick', 'ship');
336await p.waitForTimeout(300);
337await p.focus('#link-pick');
338await p.keyboard.press('Escape');
339await p.waitForTimeout(600);
340check('Escape closes the form', await p.$('#link-form') === null);
341check('and asks nothing on the way out', await p.$('.dlg-card') === null);
342check('the rows it was sitting under are still there', (await rows()).length === 2);
343
344// ── Taking one away, from one end, removes it from both ─────────────────
345await p.$$eval('.link-row', (els, lid) => {
346 for (const e of els) if (e.dataset.linkId === lid) { e.querySelector('.link-drop').click(); return; }
347}, madeId);
348await p.waitForSelector('.dlg-card', { timeout: 5000 });
349check('dropping a link asks first, in the app’s own dialog', await p.isVisible('.dlg-card'));
350await p.click('.dlg-ok', { force: true });
351await p.waitForTimeout(1200);
352const leftHere = await rows();
353check('the dropped link is gone from the end it was dropped at',
354 leftHere.length === 1 && !leftHere.some(r => r.lid === madeId), `${leftHere.length} row(s)`);
355await selectDiamond('Ship the console');
356await expandLinks();
357const leftThere = await rows();
358check('and from the other end as well, with no second copy to clean up',
359 leftThere.length === 0, `${leftThere.length} row(s)`);
360
361// ── The way through to the picture ──────────────────────────────────────
362check('the graph button is on the rail head', await p.isVisible('#link-graph-btn'));
363await p.click('#link-graph-btn', { force: true });
364await p.waitForTimeout(1200);
365check('it opens the Graph pane',
366 await p.$eval('#panel-graph', e => !e.classList.contains('closed') && e.offsetParent !== null),
367 await p.$eval('#panel-graph', e => e.className));
368check('and the pane has drawn the link that is left',
369 await p.$$eval('#graph-body g.graph-edge', els => els.length) === 1,
370 `${await p.$$eval('#graph-body g.graph-edge', els => els.length)} edge(s)`);
371await shot(s, 'links-graph-open');
372
373await s.close();
374
375console.log(`\n${ok.length} passed, ${bad.length} failed`);
376if (bad.length) console.log('FAILED:\n ' + bad.join('\n '));
377process.exit(bad.length ? 1 : 0);