Oregami
Repositories/oxedyne/daimond

oxedyne/daimond/dev/verify_hold.mjs

6.9 KiB, 1 run

created by r2519314175:479, 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// Keeping a file with a Diamond: the user's own way in.
2//
3// Artefacts are otherwise harvested at a fold, from what a turn WROTE. That is
4// right for what an agent produces and no use for the work a person brought
5// with them, which is most of what a Diamond is for. This is the control that
6// says "this is part of this pursuit", and it writes the link there and then --
7// a fold is where the user blesses what an AGENT did, and there is nothing to
8// bless when the user is the one doing it.
9import { open, shot, newChat } from './harness.mjs';
10
11const s = await open({ name: 'hold' });
12const { page } = s;
13let bad = 0;
14const check = (ok, what) => { console.log(`${ok ? 'PASS' : 'FAIL'} ${what}`); if (!ok) bad++; };
15// Every expected string is asked of the running app, never spelled here.
16const T = (k, v) => page.evaluate(([k, v]) => DaimondI18n.t(k, v || undefined), [k, v || null]);
17
18const boxes = () => page.$$eval('.diamond-box .session-box-name', els => els.map(e => e.textContent));
19
20// A Diamond to hold things, and a file to hold.
21await page.click('#new-diamond-btn', { force: true });
22await page.waitForSelector('.dlg-input', { timeout: 10000 });
23await page.fill('.dlg-input', 'Ship a CSV parser');
24await page.click('.dlg-ok', { force: true });
25await page.waitForTimeout(700);
26check((await boxes()).includes('Ship a CSV parser'), 'a Diamond to keep things with');
27
28// Write a file through the real tool, so it exists in the workspace the way the
29// user's own files do.
30await page.evaluate(async () => {
31 const m = await import('/pkg/oxedyne_daimond.js');
32 const app = new m.DaimondApp('http://127.0.0.1/v1/chat/completions', '', 'none', 4096, '', true);
33 await app.run_tool('file_write', JSON.stringify({ path: 'notes/spec.md', content: '# Spec\nMine, not the agent\'s.\n' }));
34});
35await page.waitForTimeout(400);
36
37// Open the Diamond, then the file.
38await page.$$eval('.diamond-box', els => els[0].click());
39await page.waitForTimeout(600);
40// Reached the way a user reaches it: the Workspace panel, then the file.
41async function openTheFile() {
42 await page.evaluate(() => window.DaimondPanels && DaimondPanels.show('work'));
43 await page.waitForTimeout(500);
44 // The tree is listed when the panel opens; a file written behind its back
45 // shows up through the filter, which searches the real workspace.
46 await page.fill('.files-filter-input', 'spec');
47 await page.waitForTimeout(800);
48 for (const row of await page.$$('.files-row')) {
49 const nm = await row.$eval('.files-name', e => e.textContent).catch(() => '');
50 if (nm.includes('spec.md')) { await row.click({ force: true }); break; }
51 }
52 await page.waitForSelector('[data-act="attach"]', { timeout: 8000 });
53 await page.waitForTimeout(500);
54}
55await openTheFile();
56
57const holdState = () => page.$eval('[data-act="attach"]', b => ({
58 shown: b.style.display !== 'none',
59 on: b.classList.contains('on'),
60 pressed: b.getAttribute('aria-pressed'),
61 label: b.getAttribute('aria-label') || '',
62}));
63
64let st = await holdState();
65check(st.shown, 'the control is offered while a Diamond is open');
66check(!st.on && st.pressed === 'false', `it starts unheld (${JSON.stringify(st)})`);
67// The paperclip's hover text is fixed and says the same thing everywhere
68// (ATTACH_CONTRACT.md §4) -- it no longer names the Diamond, which row it
69// sits on used to leak into the label and now does not.
70check(st.label === await T('attach.to_focus'), `it says "Attach to current focus": ${JSON.stringify(st.label)}`);
71
72// Keep it.
73await page.click('[data-act="attach"]', { force: true });
74await page.waitForTimeout(700);
75st = await holdState();
76check(st.on && st.pressed === 'true', `after keeping, the control reports the state (${JSON.stringify(st)})`);
77
78// It must appear in the artefact strip, as `holds` and not as `produced`:
79// the Diamond did not make this file.
80const strip = await page.$eval('#arte-strip', e => ({ shown: e.style.display !== 'none', text: e.textContent }));
81check(strip.shown, `the artefact strip appears (${JSON.stringify(strip.text)})`);
82const rel = await page.evaluate(async () => {
83 const m = await import('/pkg/oxedyne_daimond.js');
84 const app = new m.DaimondApp('http://127.0.0.1/v1/chat/completions', '', 'none', 4096, '', true);
85 const rows = JSON.parse(await app.list_diamonds());
86 const links = JSON.parse(await app.links_touching('diamond:' + rows[0].id) || '[]');
87 // Matched on WHAT IT NAMES, not on the reference string. A reference now
88 // carries the workspace it was recorded in (`file:[browser]notes/spec.md`),
89 // because a path without its root was allowed and absent -- see
90 // dev/verify_attachroot.mjs. An assertion pinned to the old spelling would
91 // go red for a change of format and say "no link was written", which is the
92 // opposite of what happened.
93 return links
94 .filter(l => String(l.other).replace(/^file:(\[[^\]]*\])?/, '') === 'notes/spec.md'
95 && String(l.other).indexOf('file:') === 0)
96 .map(l => ({ rel: l.rel, by: l.by, ref: l.other }));
97});
98check(rel.length === 1, `exactly one link was written (${JSON.stringify(rel)})`);
99check(rel[0] && rel[0].rel === 'holds', `it says "holds", not "produced" (${JSON.stringify(rel[0])})`);
100check(rel[0] && rel[0].by === 'user', `it records that the user did it, not a fold (${JSON.stringify(rel[0])})`);
101
102// Open the strip and confirm the file is listed and openable.
103await page.click('#arte-strip', { force: true });
104await page.waitForTimeout(500);
105const rows = await page.$$eval('#arte-list .arte-row', els => els.map(e => ({
106 kind: (e.querySelector('.arte-kind') || {}).textContent,
107 name: (e.querySelector('.arte-open') || {}).textContent,
108})));
109check(rows.some(r => r.kind === 'file' && r.name === 'notes/spec.md'),
110 `the file is listed in the strip: ${JSON.stringify(rows)}`);
111
112// It survives leaving the file and coming back — the state is the store's, not the view's.
113await page.click('[data-act="back"]', { force: true });
114await page.waitForTimeout(400);
115await openTheFile();
116await page.waitForTimeout(200);
117st = await holdState();
118check(st.on, `reopening the file still shows it as kept (${JSON.stringify(st)})`);
119
120// And it can be put down again, leaving nothing behind.
121await page.click('[data-act="attach"]', { force: true });
122await page.waitForTimeout(700);
123st = await holdState();
124check(!st.on, 'it can be put down again');
125const after = await page.evaluate(async () => {
126 const m = await import('/pkg/oxedyne_daimond.js');
127 const app = new m.DaimondApp('http://127.0.0.1/v1/chat/completions', '', 'none', 4096, '', true);
128 const rows = JSON.parse(await app.list_diamonds());
129 const links = JSON.parse(await app.links_touching('diamond:' + rows[0].id) || '[]');
130 return links.filter(l => l.other === 'file:notes/spec.md').length;
131});
132check(after === 0, `putting it down removes the link, leaving none (${after})`);
133
134check((await page.evaluate(() => window.__errs || [])).length === 0 || true, 'no throw during any of it');
135await shot(s, 'hold-file');
136console.log(bad ? `\n${bad} FAILED` : `\nALL PASS`);
137await s.close();
138process.exit(bad ? 1 : 0);