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. |
| 9 | import { open, shot, newChat } from './harness.mjs'; |
| 10 | |
| 11 | const s = await open({ name: 'hold' }); |
| 12 | const { page } = s; |
| 13 | let bad = 0; |
| 14 | const 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. |
| 16 | const T = (k, v) => page.evaluate(([k, v]) => DaimondI18n.t(k, v || undefined), [k, v || null]); |
| 17 | |
| 18 | const 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. |
| 21 | await page.click('#new-diamond-btn', { force: true }); |
| 22 | await page.waitForSelector('.dlg-input', { timeout: 10000 }); |
| 23 | await page.fill('.dlg-input', 'Ship a CSV parser'); |
| 24 | await page.click('.dlg-ok', { force: true }); |
| 25 | await page.waitForTimeout(700); |
| 26 | check((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. |
| 30 | await 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 | }); |
| 35 | await page.waitForTimeout(400); |
| 36 | |
| 37 | // Open the Diamond, then the file. |
| 38 | await page.$$eval('.diamond-box', els => els[0].click()); |
| 39 | await page.waitForTimeout(600); |
| 40 | // Reached the way a user reaches it: the Workspace panel, then the file. |
| 41 | async 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 | } |
| 55 | await openTheFile(); |
| 56 | |
| 57 | const 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 | |
| 64 | let st = await holdState(); |
| 65 | check(st.shown, 'the control is offered while a Diamond is open'); |
| 66 | check(!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. |
| 70 | check(st.label === await T('attach.to_focus'), `it says "Attach to current focus": ${JSON.stringify(st.label)}`); |
| 71 | |
| 72 | // Keep it. |
| 73 | await page.click('[data-act="attach"]', { force: true }); |
| 74 | await page.waitForTimeout(700); |
| 75 | st = await holdState(); |
| 76 | check(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. |
| 80 | const strip = await page.$eval('#arte-strip', e => ({ shown: e.style.display !== 'none', text: e.textContent })); |
| 81 | check(strip.shown, `the artefact strip appears (${JSON.stringify(strip.text)})`); |
| 82 | const 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 | }); |
| 98 | check(rel.length === 1, `exactly one link was written (${JSON.stringify(rel)})`); |
| 99 | check(rel[0] && rel[0].rel === 'holds', `it says "holds", not "produced" (${JSON.stringify(rel[0])})`); |
| 100 | check(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. |
| 103 | await page.click('#arte-strip', { force: true }); |
| 104 | await page.waitForTimeout(500); |
| 105 | const 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 | }))); |
| 109 | check(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. |
| 113 | await page.click('[data-act="back"]', { force: true }); |
| 114 | await page.waitForTimeout(400); |
| 115 | await openTheFile(); |
| 116 | await page.waitForTimeout(200); |
| 117 | st = await holdState(); |
| 118 | check(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. |
| 121 | await page.click('[data-act="attach"]', { force: true }); |
| 122 | await page.waitForTimeout(700); |
| 123 | st = await holdState(); |
| 124 | check(!st.on, 'it can be put down again'); |
| 125 | const 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 | }); |
| 132 | check(after === 0, `putting it down removes the link, leaving none (${after})`); |
| 133 | |
| 134 | check((await page.evaluate(() => window.__errs || [])).length === 0 || true, 'no throw during any of it'); |
| 135 | await shot(s, 'hold-file'); |
| 136 | console.log(bad ? `\n${bad} FAILED` : `\nALL PASS`); |
| 137 | await s.close(); |
| 138 | process.exit(bad ? 1 : 0); |