oxedyne/daimond/dev/verify_workspace_nav.mjs
9.6 KiB, 1 run
created by r2519314175:827, 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_workspace_nav.mjs — getting back out of a folder in the Workspace |
| 2 | // dock panel. |
| 3 | // |
| 4 | // The report: "When I click a folder, there is not up/return icon or button, |
| 5 | // I'm trapped in lower directories." The plain workspace tree already carried |
| 6 | // a "parent folder" icon in its header (one level per click, easy to miss |
| 7 | // among five others); the System section had nothing of the kind at all — |
| 8 | // only a "📁 .." row mixed into the file listing itself. |
| 9 | // |
| 10 | // The fix put the path line to work: `.files-path` and `.sys-path` now draw a |
| 11 | // clickable breadcrumb trail (Workspace/System › folder › folder …), one click |
| 12 | // to any ancestor rather than one click per level, and the same pattern in |
| 13 | // both trees rather than two different ones. |
| 14 | // |
| 15 | // Two properties, each proved against the ORIGINAL bug shape first: |
| 16 | // |
| 17 | // 1. THE SYSTEM SECTION HAS A WAY BACK OUT AT ALL. It never had a header |
| 18 | // icon; the inline "…" row was removed as part of this fix on the |
| 19 | // understanding that the breadcrumb replaces it. `--break crumbs` guts |
| 20 | // the breadcrumb back to a plain, non-interactive string — precisely |
| 21 | // what shipped before — and with it gone the System section is provably |
| 22 | // unreachable from three folders down: nothing on screen can shorten the |
| 23 | // path. |
| 24 | // |
| 25 | // 2. A BREADCRUMB SEGMENT JUMPS DIRECTLY, NOT ONE LEVEL AT A TIME. Three |
| 26 | // folders down in the plain tree, clicking the FIRST segment must land |
| 27 | // at the root in one click. `--break crumbs` removes every clickable |
| 28 | // segment, so the same click hits nothing and the path does not move. |
| 29 | // |
| 30 | // node dev/verify_workspace_nav.mjs --break crumbs |
| 31 | // node dev/verify_workspace_nav.mjs |
| 32 | // |
| 33 | // Needs dev/serve.mjs (dev/world.sh N --up). No gateway needed. |
| 34 | import fs from 'node:fs'; |
| 35 | import path from 'node:path'; |
| 36 | import { fileURLToPath } from 'node:url'; |
| 37 | import { open, scratch, shot } from './harness.mjs'; |
| 38 | |
| 39 | const HERE = path.dirname(fileURLToPath(import.meta.url)); |
| 40 | const WWW = path.join(HERE, '..', 'www'); |
| 41 | |
| 42 | const BREAK = (() => { |
| 43 | const i = process.argv.indexOf('--break'); |
| 44 | return i > 0 ? String(process.argv[i + 1] || '') : ''; |
| 45 | })(); |
| 46 | |
| 47 | const ok = [], bad = []; |
| 48 | const check = (name, pass, detail) => { |
| 49 | (pass ? ok : bad).push(name); |
| 50 | console.log((pass ? ' ok ' : ' FAIL ') + name + (detail ? ' — ' + detail : '')); |
| 51 | }; |
| 52 | |
| 53 | // The single break: gut `renderCrumbs` to the plain string it replaced, with |
| 54 | // no element ever made clickable. This is not a hypothetical regression — |
| 55 | // it is what `.files-path` and `.sys-path` literally rendered before this fix. |
| 56 | const BREAKS = { |
| 57 | crumbs: [{ |
| 58 | file: 'js/daimond.js', |
| 59 | find: `\t\tfunction renderCrumbs(el, root, dir, go) {\n\t\t\tif (!el) return;\n\t\t\tel.innerHTML = '';`, |
| 60 | with: `\t\tfunction renderCrumbs(el, root, dir, go) {\n\t\t\tif (!el) return;\n\t\t\tel.textContent = dir ? ('/' + dir) : root; return;\n\t\t\tel.innerHTML = '';`, |
| 61 | }], |
| 62 | }; |
| 63 | |
| 64 | if (BREAK && !BREAKS[BREAK]) { |
| 65 | console.error(`unknown break '${BREAK}'; one of: ${Object.keys(BREAKS).join(', ')}`); |
| 66 | process.exit(2); |
| 67 | } |
| 68 | |
| 69 | /// The damaged source, or a hard stop. Nothing is served that was not verified |
| 70 | /// to differ from the file on disk — an anchor that no longer matches would |
| 71 | /// silently serve the WORKING file and the run below would prove nothing. |
| 72 | function damaged(src, spec) { |
| 73 | const n = src.split(spec.find).length - 1; |
| 74 | if (n !== 1) { |
| 75 | console.error(`break '${BREAK}': the anchor appears ${n} times in ${spec.file}, ` |
| 76 | + 'so nothing was broken and the run below would prove nothing.'); |
| 77 | process.exit(2); |
| 78 | } |
| 79 | return src.replace(spec.find, spec.with); |
| 80 | } |
| 81 | |
| 82 | /// The damaged files, ONE BODY PER FILE. |
| 83 | /// |
| 84 | /// Every edit a break names for a file goes into the SAME body, in order, and |
| 85 | /// that one body is what the route serves. A `page.route` per edit spec does not |
| 86 | /// work and does not say so: Playwright hands a request to the LAST route |
| 87 | /// registered for its URL, so a two-edit break shipped only its second edit -- |
| 88 | /// and still went red, for half the reason it claims, with nothing to notice it. |
| 89 | function damagedFiles() { |
| 90 | const byFile = new Map(); |
| 91 | for (const spec of (BREAKS[BREAK] || [])) { |
| 92 | const src = byFile.has(spec.file) ? byFile.get(spec.file) |
| 93 | : fs.readFileSync(path.join(WWW, spec.file), 'utf8'); |
| 94 | byFile.set(spec.file, damaged(src, spec)); |
| 95 | } |
| 96 | return byFile; |
| 97 | } |
| 98 | |
| 99 | const PROFILE = scratch('pw', 'wsnav' + (BREAK ? '-' + BREAK : '')); |
| 100 | fs.rmSync(PROFILE, { recursive: true, force: true }); |
| 101 | |
| 102 | const s = await open({ name: 'wsnav', profile: PROFILE, signIn: false, connect: false }); |
| 103 | const { page } = s; |
| 104 | |
| 105 | if (BREAK) { |
| 106 | for (const [file, body] of damagedFiles()) { |
| 107 | await page.route('**/' + file, r => r.fulfill({ |
| 108 | status: 200, contentType: 'application/javascript', body, |
| 109 | })); |
| 110 | } |
| 111 | } |
| 112 | |
| 113 | // The stub (or lack of one) only takes effect on a load that comes after it — |
| 114 | // `open()`'s own navigation ran before routes existed. |
| 115 | await page.goto(process.env.DAIMOND_APP || 'http://localhost:8777', { waitUntil: 'domcontentloaded' }); |
| 116 | const { signInAs } = await import('./harness.mjs'); |
| 117 | await signInAs(s, 'wsnav'); |
| 118 | await page.waitForTimeout(2000); |
| 119 | |
| 120 | /// Click the row named `name` in whichever tree is on screen. |
| 121 | async function clickRowNamed(sel, name) { |
| 122 | const rows = await page.$$(sel); |
| 123 | for (const r of rows) { |
| 124 | const t = await r.$eval('.files-name', e => e.textContent).catch(() => ''); |
| 125 | if (t.includes(name)) { await r.click({ force: true }); return true; } |
| 126 | } |
| 127 | return false; |
| 128 | } |
| 129 | |
| 130 | const crumbTexts = (sel) => page.$$eval(sel + ' .path-crumb', els => els.map(e => e.textContent)); |
| 131 | |
| 132 | try { |
| 133 | // ── Seed three folders deep in the plain workspace tree ──────────── |
| 134 | await page.evaluate(async () => { |
| 135 | const m = await import('/pkg/oxedyne_daimond.js'); |
| 136 | const app = new m.DaimondApp('http://127.0.0.1/v1/chat/completions', '', 'none', 4096, '', true); |
| 137 | await app.run_tool('file_write', JSON.stringify({ path: 'alpha/beta/gamma/deep.md', content: '# deep\n' })); |
| 138 | }); |
| 139 | await page.waitForTimeout(300); |
| 140 | |
| 141 | await page.evaluate(() => window.DaimondPanels && DaimondPanels.show('work')); |
| 142 | await page.waitForTimeout(600); |
| 143 | await page.click('#panel-work [data-act="refresh"]', { force: true }).catch(() => {}); |
| 144 | await page.waitForTimeout(600); |
| 145 | |
| 146 | await clickRowNamed('.files-tree .files-row', 'alpha'); |
| 147 | await page.waitForTimeout(500); |
| 148 | await clickRowNamed('.files-tree .files-row', 'beta'); |
| 149 | await page.waitForTimeout(500); |
| 150 | await clickRowNamed('.files-tree .files-row', 'gamma'); |
| 151 | await page.waitForTimeout(500); |
| 152 | |
| 153 | let pathText = await page.$eval('.files-path', e => e.textContent); |
| 154 | check('three levels down in the plain tree', /gamma/.test(pathText), pathText); |
| 155 | |
| 156 | // ── 2. The first breadcrumb segment jumps straight to the root ───── |
| 157 | const crumbs = await crumbTexts('.files-path'); |
| 158 | if (BREAK === 'crumbs') { |
| 159 | // The whole claim of this half of the break: with the breadcrumb gutted, |
| 160 | // nothing in the path line can be clicked at all, so the "jump straight |
| 161 | // to the root" property has nothing to stand on. |
| 162 | check('(broken) no clickable segment exists in the plain tree’s path line', |
| 163 | crumbs.length === 0, JSON.stringify(crumbs)); |
| 164 | } else { |
| 165 | check('the path line offers at least one crumb to click', crumbs.length >= 1, JSON.stringify(crumbs)); |
| 166 | await page.click('.files-path .path-crumb >> nth=0', { force: true }); |
| 167 | await page.waitForTimeout(600); |
| 168 | const after = await page.$eval('.files-path', e => e.textContent); |
| 169 | check('one click on the first segment surfaces all the way to the root, not one level', |
| 170 | !/gamma/.test(after) && !/beta/.test(after), `was "${pathText}", now "${after}"`); |
| 171 | } |
| 172 | |
| 173 | // ── 1. The System section has a way back out ─────────────────────── |
| 174 | // A Diamond gives the store real depth to descend into: diamonds/<id>/versions. |
| 175 | const id = await page.evaluate(async () => { |
| 176 | const m = await import('/pkg/oxedyne_daimond.js'); |
| 177 | const app = new m.DaimondApp('http://127.0.0.1/v1/chat/completions', '', 'none', 4096, '', true); |
| 178 | return await app.create_diamond('Nav probe'); |
| 179 | }); |
| 180 | await page.waitForTimeout(600); |
| 181 | |
| 182 | const head = await page.$('#sys-head'); |
| 183 | const expanded = head ? await head.getAttribute('aria-expanded') : null; |
| 184 | if (expanded !== 'true') { await page.click('#sys-head', { force: true }); await page.waitForTimeout(500); } |
| 185 | |
| 186 | await clickRowNamed('#sys-tree .sys-row', 'diamonds'); |
| 187 | await page.waitForTimeout(500); |
| 188 | await clickRowNamed('#sys-tree .sys-row', id); |
| 189 | await page.waitForTimeout(500); |
| 190 | |
| 191 | const sysPathDeep = await page.$eval('#sys-path', e => e.textContent); |
| 192 | check('two levels down inside the System section', sysPathDeep.includes(id), sysPathDeep); |
| 193 | |
| 194 | const sysCrumbs = await crumbTexts('#sys-path'); |
| 195 | if (BREAK === 'crumbs') { |
| 196 | // The whole claim of this break: with the breadcrumb gutted and the old |
| 197 | // inline ".." row gone, NOTHING on screen can shorten the path. |
| 198 | check('(broken) no clickable segment exists in the System path line', |
| 199 | sysCrumbs.length === 0, JSON.stringify(sysCrumbs)); |
| 200 | const upRow = await page.$('#sys-tree .sys-row:has-text("..")'); |
| 201 | check('(broken) and no fallback ".." row either — the section is provably stuck', |
| 202 | !upRow, upRow ? 'a ".." row still exists' : 'none'); |
| 203 | } else { |
| 204 | check('the System path line offers clickable ancestors', sysCrumbs.length >= 2, JSON.stringify(sysCrumbs)); |
| 205 | await page.click('#sys-path .path-crumb >> nth=0', { force: true }); |
| 206 | await page.waitForTimeout(500); |
| 207 | const sysBack = await page.$eval('#sys-path', e => e.textContent); |
| 208 | check('clicking the root segment gets the System section back out', |
| 209 | !sysBack.includes(id), `was "${sysPathDeep}", now "${sysBack}"`); |
| 210 | } |
| 211 | } finally { |
| 212 | await shot(s, 'wsnav' + (BREAK ? '-' + BREAK : '')); |
| 213 | await s.close(); |
| 214 | } |
| 215 | |
| 216 | console.log(bad.length ? `\n${bad.length} FAILED` : `\nALL PASS (${ok.length})`); |
| 217 | process.exit(bad.length ? 1 : 0); |