Oregami
Repositories/oxedyne/daimond

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.
34import fs from 'node:fs';
35import path from 'node:path';
36import { fileURLToPath } from 'node:url';
37import { open, scratch, shot } from './harness.mjs';
38
39const HERE = path.dirname(fileURLToPath(import.meta.url));
40const WWW = path.join(HERE, '..', 'www');
41
42const BREAK = (() => {
43 const i = process.argv.indexOf('--break');
44 return i > 0 ? String(process.argv[i + 1] || '') : '';
45})();
46
47const ok = [], bad = [];
48const 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.
56const 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
64if (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.
72function 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.
89function 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
99const PROFILE = scratch('pw', 'wsnav' + (BREAK ? '-' + BREAK : ''));
100fs.rmSync(PROFILE, { recursive: true, force: true });
101
102const s = await open({ name: 'wsnav', profile: PROFILE, signIn: false, connect: false });
103const { page } = s;
104
105if (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.
115await page.goto(process.env.DAIMOND_APP || 'http://localhost:8777', { waitUntil: 'domcontentloaded' });
116const { signInAs } = await import('./harness.mjs');
117await signInAs(s, 'wsnav');
118await page.waitForTimeout(2000);
119
120/// Click the row named `name` in whichever tree is on screen.
121async 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
130const crumbTexts = (sel) => page.$$eval(sel + ' .path-crumb', els => els.map(e => e.textContent));
131
132try {
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
216console.log(bad.length ? `\n${bad.length} FAILED` : `\nALL PASS (${ok.length})`);
217process.exit(bad.length ? 1 : 0);