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 |
| 35 | import fs from 'node:fs'; |
| 36 | import path from 'node:path'; |
| 37 | import { fileURLToPath } from 'node:url'; |
| 38 | import { open, shot } from './harness.mjs'; |
| 39 | |
| 40 | const HERE = path.dirname(fileURLToPath(import.meta.url)); |
| 41 | const WWW = path.join(HERE, '..', 'www'); |
| 42 | |
| 43 | const BREAK = (() => { |
| 44 | const i = process.argv.indexOf('--break'); |
| 45 | return i > 0 ? String(process.argv[i + 1] || '') : ''; |
| 46 | })(); |
| 47 | |
| 48 | const 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 | |
| 83 | if (BREAK && !BREAKS[BREAK]) { |
| 84 | console.error(`unknown break '${BREAK}'; one of: ${Object.keys(BREAKS).join(', ')}`); |
| 85 | process.exit(2); |
| 86 | } |
| 87 | |
| 88 | let ok = 0, bad = 0; |
| 89 | const 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 | |
| 94 | const s = await open({ name: 'attachroot', signIn: false, connect: false }); |
| 95 | const { page } = s; |
| 96 | |
| 97 | if (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. |
| 113 | await page.goto(process.env.DAIMOND_APP || 'http://localhost:8777', { waitUntil: 'domcontentloaded' }); |
| 114 | const { signInAs, connectMock } = await import('./harness.mjs'); |
| 115 | await signInAs(s, 'attachroot'); |
| 116 | await connectMock(s); |
| 117 | await page.waitForTimeout(1500); |
| 118 | |
| 119 | // A Diamond, made the way a person makes one so the rail knows about it. |
| 120 | await page.click('#new-diamond-btn', { force: true }); |
| 121 | await page.waitForSelector('.dlg-input', { timeout: 10000 }); |
| 122 | await page.fill('.dlg-input', 'Roots'); |
| 123 | await page.click('.dlg-ok', { force: true }); |
| 124 | await page.waitForTimeout(2000); |
| 125 | await page.$$eval('.diamond-box', els => els[0] && els[0].click()); |
| 126 | await page.waitForTimeout(1200); |
| 127 | |
| 128 | const 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 | }); |
| 134 | check('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. |
| 139 | await 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 | }); |
| 144 | await page.evaluate(() => DaimondPanels.show('work')); |
| 145 | await 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. |
| 150 | await page.click('#panel-work [data-act="refresh"]', { force: true }).catch(() => {}); |
| 151 | await page.waitForTimeout(1200); |
| 152 | let attachedIt = false; |
| 153 | for (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 | } |
| 165 | check('the folder could be attached through its own control', attachedIt, |
| 166 | attachedIt ? '' : 'no papers row with an attach control'); |
| 167 | await page.waitForTimeout(1200); |
| 168 | |
| 169 | const 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); |
| 174 | const made = refs.find(r => /papers/.test(r)) || ''; |
| 175 | check('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. |
| 181 | await 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); |
| 189 | await page.waitForTimeout(900); |
| 190 | |
| 191 | const scope = await page.evaluate(async (did) => await window.DaimondDiamond.bounds(did), id); |
| 192 | check('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)); |
| 195 | check('and what IS here still is', |
| 196 | scope.attached.some(p => /^papers/.test(p)), |
| 197 | JSON.stringify(scope.attached)); |
| 198 | check('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. |
| 203 | await page.evaluate(() => { |
| 204 | const strip = document.getElementById('arte-strip'); |
| 205 | if (strip) { strip.dataset.open = '1'; } |
| 206 | }); |
| 207 | await page.evaluate(() => window.DaimondArtefacts && DaimondArtefacts.render()); |
| 208 | await page.waitForTimeout(900); |
| 209 | const 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 | }))); |
| 213 | const stray = tiles.find(x => /CheapThinking/.test(x.text)) || {}; |
| 214 | check('THE UNREACHABLE ONE IS SHOWN, NOT SILENTLY DROPPED', |
| 215 | !!stray.text, JSON.stringify(tiles.map(t => t.text))); |
| 216 | check('and it says WHERE it lives rather than reading as empty', |
| 217 | /machine|folder/i.test(stray.title || ''), JSON.stringify(stray.title)); |
| 218 | check('and it is marked as being elsewhere', stray.away === true, JSON.stringify(stray)); |
| 219 | const mine = tiles.find(x => /papers/.test(x.text)) || {}; |
| 220 | check('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 ────────────────────────── |
| 224 | const 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); |
| 229 | await 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 | }); |
| 236 | await page.waitForTimeout(1000); |
| 237 | const 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); |
| 243 | check('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 | |
| 247 | await shot(s, 'attachroot'); |
| 248 | console.log(`\n${ok} ok, ${bad} failed`); |
| 249 | await s.browser.close(); |
| 250 | process.exit(bad ? 1 : 0); |