Oregami
Repositories/oxedyne/daimond

oxedyne/daimond/dev/verify_viewer.mjs

6.2 KiB, 1 run

created by r2519314175:787, 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// A file open in the Workspace viewer must not go stale when an agent edits it.
2// Open a file, have the agent rewrite it on the next turn, and confirm the
3// viewer reloads to the agent's new content rather than showing the old.
4//
5// THE FILE LIVES IN A FOLDER THE USER MARKED INTO THE CHAT, and that is not
6// decoration. Until 2026-08-12 the agent was asked to write `live.txt` at the
7// workspace ROOT; since the chat fence landed a chat is confined to
8// `chats/<id>/work` (`scopeChatTo`, www/js/daimond.js) and `Tool::guard`
9// (src/tools.rs:5490) refuses a root path outright -- the refusal arriving as an
10// ordinary tool result, so nothing was written and nothing threw.
11//
12// THE SCRATCH IS NOT A DROP-IN HERE, which is why this file is marked rather than
13// re-pathed: `chats/` is not a row in the Workspace panel, and this test is about
14// what the PANEL shows. So the fixture does what a user does -- a folder is marked
15// into the chat with the paperclip (`chatToggle` + `chatWs`, the mark being the
16// permission) -- and the folder is then both writable by the agent and visible in
17// the tree. Moving the path back to the root makes every check below vacuous.
18import { open, chat, newChat, shot, errors } from './harness.mjs';
19
20const ok = [], bad = [];
21const check = (name, pass, detail) => {
22 (pass ? ok : bad).push(name + (detail ? ' — ' + detail : ''));
23 console.log((pass ? ' ok ' : ' FAIL ') + name + (detail ? ' — ' + detail : ''));
24};
25
26const s = await open({ name: 'viewer' });
27await newChat(s);
28
29// ── The fixture: a folder in the browser workspace, marked into this chat ──
30const FOLDER = 'live';
31const FILE = FOLDER + '/live.txt';
32const marked = await s.page.evaluate(async (folder) => {
33 const root = await navigator.storage.getDirectory();
34 await root.getDirectoryHandle(folder, { create: true });
35 const f = window.DaimondAttach.focus();
36 if (!f || !f.id) return { id: '', scope: [] };
37 // `[browser]` is the sandbox root this test runs in -- see `rootedRef`. The
38 // paperclip attaches, and the MARK is the separate press that widens the fence.
39 const ref = 'dir:[browser]' + folder;
40 window.DaimondAttach.chatToggle(f.id, ref, true, folder);
41 window.DaimondAttach.chatWs(f.id, ref, true);
42 return { id: f.id, scope: await window.DaimondAttach.chatScope(f.id) };
43}, FOLDER);
44check('the folder is marked into the chat\'s workspace, which is what lets the agent write in it',
45 (marked.scope || []).includes(FOLDER), JSON.stringify(marked));
46
47// 1. Agent creates a file.
48const wrote = await chat(s, `@tool file_write {"path":"${FILE}","content":"ORIGINAL CONTENT"}`);
49check('the agent really wrote it — a refused write leaves nothing to open',
50 !/Refused/.test(wrote) && /Wrote \d+ bytes/.test(wrote),
51 wrote.slice(-140).replace(/\n/g, ' | '));
52
53// 2. Open the Workspace panel and the file in its viewer.
54await s.page.evaluate(() => {
55 // Open the Workspace dock panel the real way, so DaimondPanels registers it
56 // as open (which the panel's own isOpen() gate checks).
57 if (window.DaimondPanels && DaimondPanels.show) DaimondPanels.show('work');
58});
59await s.page.waitForTimeout(500);
60/// Click the row for a path, wherever the tree currently is.
61///
62/// THE VIEWER IS `#doc-view`, NOT the Workspace panel. `Files.open` shows a text
63/// file in the DOC panel (`DaimondPanels.show('doc')`, www/js/daimond.js), and this
64/// file went on asking `#panel-work` for a `.files-view-body` that has not been
65/// there for as long as the doc panel has existed — a second way the checks below
66/// could only ever have printed a red nobody read.
67///
68/// Rows are found by `data-path` and inside `.files-tree`, never by their text:
69/// the panel draws a SECOND tree of its own (`.sys-tree`, the system folders),
70/// whose rows are also `.files-row`, and `live` is a substring of `live.txt`. A
71/// matcher that cannot tell those apart clicks the wrong row and reports the file
72/// as missing — a green-looking red, which is the thing this whole pass is about.
73const clickRow = (p) => s.page.evaluate(async (p) => {
74 const rows = [...document.querySelectorAll('#panel-work .files-tree .files-row')];
75 const row = rows.find(r => r.dataset.path === p);
76 if (!row) return { ok: false, rows: rows.map(r => r.dataset.path || r.textContent.trim()) };
77 row.click();
78 await new Promise(r => setTimeout(r, 700));
79 const body = document.querySelector('#doc-view .files-view-body');
80 return { ok: true, body: body ? body.textContent : '(no body)' };
81}, p);
82// Into the folder first: the file is one level down, because the top level is
83// where the fence does not let a chat write.
84const intoFolder = await clickRow(FOLDER);
85check('the marked folder is a row in the Workspace tree', intoFolder.ok, JSON.stringify(intoFolder).slice(0, 200));
86const opened = await clickRow(FILE);
87console.log('opened viewer:', JSON.stringify(opened).slice(0, 200));
88check('the file opens in the viewer, showing what the agent first wrote',
89 opened.ok && /ORIGINAL CONTENT/.test(opened.body || ''), JSON.stringify(opened).slice(0, 200));
90
91// 3. Agent rewrites the file on a fresh turn (triggers Files.refresh()).
92await chat(s, `@tool file_write {"path":"${FILE}","content":"AGENT REWROTE THIS"}`);
93await s.page.waitForTimeout(600);
94
95// 4. The viewer should now show the agent's new content.
96const after = await s.page.evaluate(() => {
97 const body = document.querySelector('#doc-view .files-view-body');
98 const msg = document.querySelector('#doc-view .files-view-msg');
99 return { body: body ? body.textContent : '(none)', msg: msg ? msg.textContent : '' };
100});
101await shot(s, 'viewer-after-agent-edit');
102console.log('viewer after agent edit:', JSON.stringify(after));
103check('VIEWER RELOADED to the agent\'s new content', /AGENT REWROTE THIS/.test(after.body),
104 JSON.stringify(after).slice(0, 200));
105// And it is not merely showing both: the stale copy has to be gone.
106check('and the old content is no longer on screen', !/ORIGINAL CONTENT/.test(after.body),
107 JSON.stringify(after).slice(0, 200));
108
109const errs = errors(s).filter(e => !/502|Bad Gateway/.test(e));
110check('nothing threw', errs.length === 0, errs.slice(0, 2).join(' | '));
111
112await s.close();
113console.log(`\n${ok.length} passed, ${bad.length} failed`);
114if (bad.length) { bad.forEach(b => console.log(' FAILED: ' + b)); process.exit(1); }