Oregami
Repositories/oxedyne/daimond

oxedyne/daimond/dev/verify_wsident.mjs

29.9 KiB, 1 run

created by r2519314175:835, 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_wsident.mjs — the hand's folder and the page's folder are the SAME
2// folder, or nothing runs.
3//
4// `hand/REVIEW.md` §1.14. The hand is told once, in `<root>/.daimond/workspace.id`,
5// which folder it may work in. The app separately has a folder open, chosen by
6// the user in the Workspace panel. Until this check was armed, nothing compared
7// them: a daimon could read and edit `~/projects/alpha` through the browser and
8// then run `cargo test` in `~/projects/beta`, because that is what the hand's
9// `root.txt` said — reporting results about a codebase it had never touched.
10// Silent, and confidently wrong.
11//
12// ── Why this file exists at all ─────────────────────────────────────
13//
14// The refusal cannot be satisfied by any headless run, ever. A page holds a real
15// folder only through `showDirectoryPicker()`, a native dialog no harness can
16// answer, so an automated browser always has an OPFS workspace — which is
17// §1.14's third outcome and a refusal. The two verifiers that drive the real
18// hand therefore stand in for `status()` and get on with what they are actually
19// testing; the REFUSAL itself is tested here, and nowhere else.
20//
21// ── What stands in for what ─────────────────────────────────────────
22//
23// The two folders are real `FileSystemDirectoryHandle`s, taken from OPFS, each
24// with a real `.daimond/workspace.id` written into it. So every line of the
25// comparison runs for real: `getDirectoryHandle`, `getFileHandle`, the read, the
26// comment-skipping and the string compare. What is stood in for is only the one
27// thing a headless browser cannot have — a handle the user picked — and the
28// hand, whose `hello` is fed to `DaimondHand.adopt` directly, exactly as the
29// extension feeds it.
30//
31// Nothing here breaks the harness to make a check go red. Every property is
32// proved against a BROKEN `www/js/hand.js`, served through a patch: the break is
33// in the file under test, and a check that still passes with the code broken is
34// reported as blind rather than counted.
35//
36// ── Running it ──────────────────────────────────────────────────────
37//
38// node dev/verify_wsident.mjs
39//
40// Headless, and it needs nothing running: no extension, no hand binary, no dev
41// server. `www/pkg` must be built, because the engine's own refusal is checked
42// through the real `pty_request`.
43import fs from 'node:fs';
44import http from 'node:http';
45import os from 'node:os';
46import path from 'node:path';
47import { fileURLToPath, pathToFileURL } from 'node:url';
48import { whyStaleWasm, refuse } from './staleguard.mjs';
49
50const PW = process.env.DAIMOND_PW
51 || path.join(os.homedir(), '.red-pw/node_modules/playwright-core/index.mjs');
52const { chromium } = await import(pathToFileURL(PW).href);
53const CHROME = process.env.DAIMOND_CHROME
54 || `${process.env.HOME}/.cache/ms-playwright/chromium-1229/chrome-linux64/chrome`;
55
56const HERE = path.dirname(fileURLToPath(import.meta.url));
57const ROOT = path.join(HERE, '..');
58const WWW = path.join(ROOT, 'www');
59
60// The granted folder, as the PATH the hand would report. Nothing on this machine
61// is ever opened there: the hand is stood in for, and all a path has to be here
62// is absolute.
63const PATH_A = '/home/u/projects/alpha';
64// A grant whose last component is the same as a folder the page can hold, which
65// is the case the check must not fall back to comparing names on.
66const PATH_SITE = '/home/u/work/site';
67
68const TOKEN_A = 'a1b2c3d4e5f60718293a4b5c6d7e8f90';
69const TOKEN_B = '0f9e8d7c6b5a49382716f5e4d3c2b1a0';
70
71/// The file the hand writes, verbatim: four comment lines and then the token.
72/// The comments are not decoration — they are why the token is not simply the
73/// first line, and a reader who deletes them is testing something else.
74const HEADER = '# Daimond wrote this so that the browser and the machine hand can tell whether\n'
75 + '# they are talking about the same folder. It is not a secret and not a key.\n'
76 + '# Deleting it costs nothing: the next hand to start writes a new one, and the\n'
77 + '# page will ask you to confirm the folder again.\n';
78
79const ok = [], bad = [];
80const check = (name, pass, detail) => {
81 (pass ? ok : bad).push(name);
82 console.log((pass ? ' ok ' : ' FAIL ') + name + (detail ? ' — ' + detail : ''));
83};
84
85/// A property proved twice: broken, and required to fail; whole, and required to
86/// pass. Only a pair where BOTH halves answered as they should is counted — a
87/// check that passes with the code broken is blind, and is reported as such.
88const provedNames = [];
89async function proved(name, brk, testIt) {
90 breaking = brk;
91 await reload();
92 let red = true;
93 try { red = await testIt(); } catch (e) { red = false; }
94 breaking = '';
95 await reload();
96 let green = false;
97 try { green = await testIt(); } catch (e) { green = false; }
98 provedNames.push(name);
99 check(`PROVED ${name}`, !red && green,
100 `broken=${red ? 'PASSED — the check is blind' : 'failed, correctly'}, `
101 + `whole=${green ? 'passed' : 'FAILED'}`);
102}
103
104// ┌───────────────────────────────────────────────────────────────┐
105// │ Breaking the code under test │
106// └───────────────────────────────────────────────────────────────┘
107//
108// Each break is what the file looked like before the property was there, or what
109// a reasonable person would write who had not thought about it. None of them is
110// nonsense: `not-armed` is literally the state this file was written to end.
111
112const PATCHES = {
113 // The arming itself: report the verdict and pair anyway, which is where
114 // §1.14 stood until today.
115 'not-armed': ['\t\t\t\tout.paired = false;\n', ''],
116 // The fallback §1.14 forbids by name: compare the folder's NAME with the
117 // last component of the hand's root.
118 'name-compare': [
119 '\t\tif (firstLine(mine) !== token) return { ok: false, why: mismatch(dir) };',
120 '\t\tif (dir.name !== String(state.root).split(\'/\').pop()) '
121 + 'return { ok: false, why: mismatch(dir) };'],
122 // A page that creates the file it is about to read proves nothing.
123 'creates-file': [
124 '\t\t\tvar sub = await dir.getDirectoryHandle(WS_DIR);\n'
125 + '\t\t\tvar file = await sub.getFileHandle(WS_FILE);',
126 '\t\t\tvar sub = await dir.getDirectoryHandle(WS_DIR, { create: true });\n'
127 + '\t\t\tvar file = await sub.getFileHandle(WS_FILE, { create: true });'],
128 // The token taken as the first line of the file rather than the first line
129 // that is neither blank nor a comment.
130 'first-line': ['\t\t\tif (!line || line.charAt(0) === \'#\') continue;', ''],
131 // Refusing a hand that publishes no `ws:` at all, which breaks every older
132 // hand and every mock host permanently.
133 'silence-refuses': ['\t\t\treturn { ok: true, why: \'\' };\n\t\t}\n\t\tif (token === WS_UNPROVEN)',
134 '\t\t\treturn { ok: false, why: mismatch(dir) };\n\t\t}\n\t\tif (token === WS_UNPROVEN)'],
135 // The cache keyed by the grant alone, so a folder swapped underneath the page
136 // is answered for out of memory.
137 'stale-cache': ['if (wsProof && wsProof.key === key && wsProof.dir === dir) return wsProof;',
138 'if (wsProof && wsProof.key === key) return wsProof;'],
139 // Skipping the check for want of a handle, which is the outcome that must NOT
140 // be skipped: there is nothing to compare, so it cannot pass.
141 'opfs-passes': ['\t\t\treturn { ok: false, why: \'This workspace lives in the browser',
142 '\t\t\treturn { ok: true, why: \'This workspace lives in the browser'],
143 // A hand that could not write its identity file, believed anyway.
144 'unproven-passes': ['\t\t\treturn { ok: false, why: \'The machine hand could not write',
145 '\t\t\treturn { ok: true, why: \'The machine hand could not write'],
146 // A comparison that could not be made at all, reported and then paired anyway.
147 'unchecked-passes': ['\t\t\tout.paired = false;\n\t\t\tout.workspace = \'unchecked\';',
148 '\t\t\tout.workspace = \'unchecked\';'],
149};
150
151/// The break in force, or ''. Read by the server as it serves `hand.js`.
152let breaking = '';
153
154/// One served file's source, patched where a break names it.
155function served(rel) {
156 let src = fs.readFileSync(path.join(WWW, rel), 'utf8');
157 const p = PATCHES[breaking];
158 if (p && rel === 'js/hand.js') {
159 if (src.indexOf(p[0]) < 0) {
160 console.error(` !! the break "${breaking}" no longer matches www/js/hand.js; `
161 + 'the patch is stale and would prove nothing');
162 process.exitCode = 3;
163 }
164 src = src.replace(p[0], p[1]);
165 }
166 return src;
167}
168
169// ┌───────────────────────────────────────────────────────────────┐
170// │ The page │
171// └───────────────────────────────────────────────────────────────┘
172//
173// The relay and the wasm module, and nothing else. The relay is reached exactly
174// as the app reaches it — `DaimondHand.init`, `DaimondHand.adopt`, and then
175// `status()` — so what is under test is the shipped object and not a copy.
176
177const PAGE = `<!doctype html><meta charset="utf-8"><title>wsident</title>
178<body>
179<script src="/js/hand.js"><\/script>
180<script type="module">
181import init, * as W from '/pkg/oxedyne_daimond.js';
182await init();
183window.Wasm = W;
184window.__ready = true;
185<\/script>
186<script>
187/// The folder the page has open, as the Workspace panel would hand it over: a
188/// directory handle and never a path.
189window.__folder = null;
190DaimondHand.init({ folder: function () { return window.__folder; } });
191
192/// Write one folder's identity file, through the same API a page reads it with.
193///
194/// Real OPFS handles, so the read under test is a real read. 'create: true' here
195/// is the TEST writing the fixture, which is a different act from the relay
196/// creating what it is about to compare against.
197window.__folderWith = async function (parent, name, body) {
198 const top = await navigator.storage.getDirectory();
199 const up = await top.getDirectoryHandle(parent, { create: true });
200 const dir = await up.getDirectoryHandle(name, { create: true });
201 if (body === null) return dir;
202 const sub = await dir.getDirectoryHandle('.daimond', { create: true });
203 const f = await sub.getFileHandle('workspace.id', { create: true });
204 const w = await f.createWritable();
205 await w.write(body);
206 await w.close();
207 return dir;
208};
209
210/// A folder with a '.daimond' directory and no identity file in it.
211window.__folderBare = async function (parent, name) {
212 const top = await navigator.storage.getDirectory();
213 const up = await top.getDirectoryHandle(parent, { create: true });
214 const dir = await up.getDirectoryHandle(name, { create: true });
215 await dir.getDirectoryHandle('.daimond', { create: true });
216 return dir;
217};
218
219/// Whether a folder has a '.daimond', and whether that has a 'workspace.id'.
220/// Read WITHOUT creating anything, or the reading would be the writing.
221window.__whatIsThere = async function (dir) {
222 let sub = null;
223 try { sub = await dir.getDirectoryHandle('.daimond'); } catch (e) { return { dir: false, file: false }; }
224 try { await sub.getFileHandle('workspace.id'); } catch (e) { return { dir: true, file: false }; }
225 return { dir: true, file: true };
226};
227
228/// Tell the relay what a hand said, exactly as the extension's greeting does.
229///
230/// # Arguments
231/// * root - The folder the hand was granted.
232/// * ws - The identity it published, or null to publish none at all.
233window.__handSays = function (root, ws) {
234 DaimondHand.forget();
235 const caps = ['fence:linux', 'landlock:abi-8', 'root:' + root];
236 if (ws !== null) caps.push('ws:' + ws);
237 DaimondHand.adopt({ transport: 'machine', host: 'test', version: '0.0.0', os: 'linux', caps: caps });
238};
239<\/script>
240</body>`;
241
242const server = http.createServer((req, res) => {
243 const url = (req.url || '').split('?')[0];
244 const send = (type, body) => { res.writeHead(200, { 'content-type': type }); res.end(body); };
245 if (url === '/favicon.ico') { res.writeHead(404); return res.end('no'); }
246 if (url.startsWith('/js/')) return send('text/javascript; charset=utf-8', served(url.slice(1)));
247 if (url.startsWith('/pkg/')) {
248 const f = path.join(WWW, 'pkg', url.slice('/pkg/'.length));
249 if (!fs.existsSync(f)) { res.writeHead(404); return res.end('no'); }
250 return send(f.endsWith('.wasm') ? 'application/wasm' : 'text/javascript; charset=utf-8',
251 fs.readFileSync(f));
252 }
253 return send('text/html; charset=utf-8', PAGE);
254});
255
256/// The first free port from `from`, so a dev server already holding one is left
257/// alone rather than fought over.
258async function listen(from) {
259 for (let port = from; port < from + 40; port++) {
260 try {
261 await new Promise((resolve, reject) => {
262 server.once('error', reject);
263 server.listen(port, '127.0.0.1', () => { server.removeListener('error', reject); resolve(); });
264 });
265 return port;
266 } catch (e) { if (e.code !== 'EADDRINUSE') throw e; }
267 }
268 throw new Error(`No free port from ${from}.`);
269}
270
271// ── The engine under test has to be this tree's ─────────────────────
272//
273// This file asked only whether `www/pkg` EXISTED. That is the fail-open that
274// `hand/src/exec.rs`'s `shipping_hand` and `dev/verify_ptyedge.mjs` were both
275// written to close: the whole subject here is the ENGINE's own refusal, reached
276// through the real `pty_request` in the bundle, so a bundle built before that
277// refusal existed would have this file report, in detail and in green, on a
278// refusal that is no longer in the code. Existence proves the file is there and
279// nothing at all about what is in it.
280//
281// So: every `.rs` under `src/` must be older than the bundle. Not rebuilt here —
282// a wasm build is minutes and a surprise one inside a verifier is worse than a
283// sentence saying what to run.
284refuse(whyStaleWasm(path.join(WWW, 'pkg/oxedyne_daimond_bg.wasm'), path.join(ROOT, 'src'), {
285 subject: 'The workspace-identity refusal',
286 holds: '`pty_request`, whose refusal is the subject of this file,',
287}));
288
289const PORT = await listen(Number(process.env.WSIDENT_PORT || 8811));
290const APP = `http://127.0.0.1:${PORT}`;
291
292const b = await chromium.launch({ executablePath: CHROME, headless: true,
293 args: ['--no-sandbox', '--disable-dev-shm-usage'] });
294const page = await b.newPage();
295const errs = [];
296page.on('pageerror', (e) => errs.push(`pageerror: ${e.message}`));
297
298/// Loads the page again, with whatever break is in force. The reload is what
299/// puts a patched relay into the running page.
300async function reload() {
301 await page.goto(APP, { waitUntil: 'domcontentloaded' });
302 await page.waitForFunction(() => window.__ready === true, null, { timeout: 30000 });
303}
304
305// ┌───────────────────────────────────────────────────────────────┐
306// │ The one question, asked both ways round │
307// └───────────────────────────────────────────────────────────────┘
308
309/// A fresh OPFS parent for every fixture. Reloading the page resets its
310/// JavaScript and not its storage, so a folder made under one break would
311/// otherwise still be there — with whatever the break did to it — when the whole
312/// code is asked the same question afterwards.
313let seq = 0;
314
315/// Set the two ends up and ask the relay.
316///
317/// # Arguments
318/// * `root` - The folder the hand was granted.
319/// * `ws` - The identity the hand published, or null for a hand that publishes none.
320/// * `folder` - `{ name, body }` for the folder the page holds, `{ name, body: null }`
321/// for one with no `.daimond` in it, `'bare'` for one with an empty `.daimond`,
322/// or null for an OPFS-only workspace with no folder at all.
323async function ask(root, ws, folder) {
324 const parent = `w${++seq}`;
325 return await page.evaluate(async ([root, ws, folder, parent]) => {
326 window.__folder = null;
327 if (folder === 'bare') window.__folder = await window.__folderBare(parent, 'bare');
328 else if (folder) window.__folder = await window.__folderWith(parent, folder.name, folder.body);
329 window.__handSays(root, ws);
330 const proof = await DaimondHand.workspaceProof();
331 const st = JSON.parse(await DaimondHand.status());
332 const there = window.__folder ? await window.__whatIsThere(window.__folder) : null;
333 return { proof, st, there, folderName: window.__folder ? window.__folder.name : null };
334 }, [root, ws, folder || null, parent]);
335}
336
337/// What the ENGINE does with that answer, through the real `pty_request` — the
338/// same composition a Terminal panel goes through, on the real wasm.
339/// A Diamond with a folder ATTACHED, which is the only kind that has anywhere to
340/// run. Its own directory `diamonds/d1` is in the browser's storage whatever
341/// folder is open, so a Diamond holding nothing else is refused a terminal on
342/// its own account — a true answer, and not the one this file is asking about.
343/// Passing it here would leave every check below reading a refusal for the wrong
344/// reason, which is how this one went red on 2026-08-13: the fence stopped
345/// mapping store paths onto the disk, and the fixture had never attached
346/// anything.
347///
348/// `notes` is never opened, made or reached. The folder verdict is settled
349/// before a path is looked at, so what it needs is a NAME the fence can express.
350async function engine() {
351 return await page.evaluate(async () => JSON.parse(await window.Wasm.pty_request(JSON.stringify({
352 own_dir: 'diamonds/d1', attached: ['notes'], read_only: [],
353 cwd: 'diamonds/d1', cols: 80, rows: 24,
354 }))));
355}
356
357await reload();
358
359console.log('\n── 1. The same folder, and silence ───────────────────');
360
361const same = await ask(PATH_A, TOKEN_A, { name: 'alpha', body: HEADER + TOKEN_A + '\n' });
362check('a page holding the granted folder is not refused',
363 same.proof.ok === true && same.proof.why === '', JSON.stringify(same.proof).slice(0, 200));
364check('and the ordinary case says NOTHING: no reason, no complaint, just paired',
365 same.st.paired === true && same.st.workspace === 'ok'
366 && same.st.workspace_reason === undefined && same.st.reason === undefined,
367 JSON.stringify(same.st).slice(0, 240));
368check('and the token is read past the four comment lines the hand writes',
369 same.proof.ok === true, 'the fixture is the hand\'s own file, header and all');
370
371const opened = await engine();
372check('so the engine composes a terminal for it', opened.t === 'open' && !opened.refused,
373 JSON.stringify(opened).slice(0, 160));
374
375console.log('\n── 2. Folder A granted, folder B open ────────────────');
376
377// THE case this check exists for. The hand was granted alpha and says so; the
378// page has beta open, and beta carries its own, different identity. Both folders
379// exist, both are perfectly good workspaces, and the two ends mean different
380// files.
381const AvsB = await ask(PATH_A, TOKEN_A, { name: 'beta', body: HEADER + TOKEN_B + '\n' });
382check('a hand granted A with the page holding B is REFUSED',
383 AvsB.proof.ok === false, JSON.stringify(AvsB.proof).slice(0, 200));
384check('and the refusal names BOTH ends, so the user can tell which one is wrong',
385 new RegExp('Daimond has “beta”; the hand has “'
386 + PATH_A.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '”').test(AvsB.proof.why),
387 AvsB.proof.why);
388check('and it says what to do about each end',
389 /root\.txt/.test(AvsB.proof.why) && /open the other folder here/.test(AvsB.proof.why),
390 AvsB.proof.why.slice(-120));
391check('and the relay refuses the pairing rather than merely reporting it',
392 AvsB.st.paired === false && AvsB.st.reason === AvsB.proof.why
393 && AvsB.st.workspace === 'mismatch',
394 JSON.stringify(AvsB.st).slice(0, 200));
395check('and what the hand said about itself survives the refusal, for the user to read',
396 AvsB.st.root === PATH_A && (AvsB.st.caps || []).includes('fence:linux'),
397 JSON.stringify(AvsB.st).slice(0, 200));
398
399const refusedByEngine = await engine();
400check('so the ENGINE will not open a terminal, and passes the sentence on whole',
401 !!refusedByEngine.refused && refusedByEngine.t === undefined
402 && refusedByEngine.refused.includes('is not the folder the machine hand was told to work in'),
403 JSON.stringify(refusedByEngine).slice(0, 220));
404
405console.log('\n── 3. Two projects called "site" ─────────────────────');
406
407// §1.14 forbids falling back to the folder's name by name, and this is why: one
408// machine with two `site` directories is the ordinary case. Both directions are
409// asserted, because a name check is wrong in both — it passes the wrong folder
410// AND refuses the right one.
411const wrongSite = await ask(PATH_SITE, TOKEN_A, { name: 'site', body: HEADER + TOKEN_B + '\n' });
412check('a folder whose NAME matches the grant, and whose identity does not, is refused',
413 wrongSite.proof.ok === false && wrongSite.folderName === 'site',
414 JSON.stringify(wrongSite.proof).slice(0, 160));
415const renamed = await ask(PATH_SITE, TOKEN_A, { name: 'moved', body: HEADER + TOKEN_A + '\n' });
416check('and a folder whose name matches NOTHING, but whose identity matches, is allowed',
417 renamed.proof.ok === true && renamed.folderName === 'moved',
418 JSON.stringify(renamed.proof).slice(0, 160));
419
420console.log('\n── 4. The other three outcomes ───────────────────────');
421
422const opfs = await ask(PATH_A, TOKEN_A, null);
423check('an OPFS-only workspace is refused, and told it has no folder at all',
424 opfs.proof.ok === false && /lives in the browser and not in a folder/.test(opfs.proof.why)
425 && opfs.st.paired === false, opfs.proof.why.slice(0, 140));
426
427const unproven = await ask(PATH_A, 'unproven', { name: 'alpha', body: HEADER + TOKEN_A + '\n' });
428check('a hand that could not write its identity file is refused, not believed',
429 unproven.proof.ok === false && /could not write its identity file/.test(unproven.proof.why)
430 && unproven.st.paired === false, unproven.proof.why.slice(0, 140));
431
432const silent = await ask(PATH_A, null, null);
433check('a hand that publishes no identity at all is an OLDER hand, and still pairs',
434 silent.proof.ok === true && silent.st.paired === true && silent.st.workspace === 'ok',
435 JSON.stringify(silent.st).slice(0, 200));
436
437console.log('\n── 5. What the page must never do ────────────────────');
438
439const missing = await ask(PATH_A, TOKEN_A, { name: 'empty', body: null });
440check('a folder with no .daimond in it is refused',
441 missing.proof.ok === false, JSON.stringify(missing.proof).slice(0, 160));
442check('and the page did not CREATE the directory it failed to find',
443 missing.there.dir === false && missing.there.file === false, JSON.stringify(missing.there));
444
445const bare = await ask(PATH_A, TOKEN_A, 'bare');
446check('a .daimond with no identity file in it is refused',
447 bare.proof.ok === false, JSON.stringify(bare.proof).slice(0, 160));
448check('and the page did not CREATE the file it failed to find',
449 bare.there.dir === true && bare.there.file === false, JSON.stringify(bare.there));
450
451// The token is the first line that is neither blank nor a comment. A file whose
452// COMMENT quotes the hand's token, and whose payload line is something else, is
453// a different folder — and reading it the lazy way would call it a match.
454const commented = await ask(PATH_A, TOKEN_A,
455 { name: 'commented', body: `# ${TOKEN_A}\n\n${TOKEN_B}\n` });
456check('a token quoted in a COMMENT is not the folder\'s identity',
457 commented.proof.ok === false, JSON.stringify(commented.proof).slice(0, 160));
458
459// A comparison that cannot be made is not a comparison that passed. The page is
460// asked for its folder and throws instead of answering — the shape of a handle
461// the browser has let go of.
462const threw = await page.evaluate(async ([root, tok]) => {
463 DaimondHand.init({ folder: function () { throw new Error('the folder handle is gone'); } });
464 window.__handSays(root, tok);
465 const st = JSON.parse(await DaimondHand.status());
466 DaimondHand.init({ folder: function () { return window.__folder; } });
467 return st;
468}, [PATH_A, TOKEN_A]);
469check('a check that cannot answer at all refuses, rather than passing by default',
470 threw.paired === false && threw.workspace === 'unchecked'
471 && threw.reason === threw.workspace_reason && /will not run a command/.test(threw.reason || ''),
472 JSON.stringify(threw).slice(0, 220));
473
474console.log('\n── 6. The folder changing underneath the page ────────');
475
476// The user opens a different folder in the Workspace panel. The hand has said
477// nothing — the grant has not changed — so a verdict remembered by grant alone
478// would answer for a folder it never read.
479const swapped = await page.evaluate(async ([root, tok, header, other, parent]) => {
480 window.__folder = await window.__folderWith(parent, 'alpha', header + tok + '\n');
481 window.__handSays(root, tok);
482 const first = await DaimondHand.workspaceProof();
483 // Nothing else changes: same hand, same grant, same token. Only the folder.
484 window.__folder = await window.__folderWith(parent, 'beta', header + other + '\n');
485 const second = await DaimondHand.workspaceProof();
486 const st = JSON.parse(await DaimondHand.status());
487 return { first: first.ok, second: second.ok, paired: st.paired, why: second.why };
488}, [PATH_A, TOKEN_A, HEADER, TOKEN_B, `w${++seq}`]);
489check('a folder swapped under a PASSING verdict is checked again, and refused',
490 swapped.first === true && swapped.second === false && swapped.paired === false,
491 JSON.stringify(swapped).slice(0, 200));
492
493check('nothing on the page threw along the way', errs.length === 0, errs.slice(0, 3).join(' | '));
494
495// ┌───────────────────────────────────────────────────────────────┐
496// │ 7. Every property, proved against broken code │
497// └───────────────────────────────────────────────────────────────┘
498//
499// The break is in `www/js/hand.js`, served through a patch and the page
500// reloaded. Nothing in this file breaks itself.
501
502console.log('\n── 7. Proved against broken code ─────────────────────');
503
504await proved('the refusal is armed at all', 'not-armed', async () => {
505 const r = await ask(PATH_A, TOKEN_A, { name: 'beta', body: HEADER + TOKEN_B + '\n' });
506 return r.st.paired === false && r.st.reason === r.proof.why;
507});
508
509await proved('the engine will not open a terminal on the wrong folder', 'not-armed', async () => {
510 await ask(PATH_A, TOKEN_A, { name: 'beta', body: HEADER + TOKEN_B + '\n' });
511 const r = await engine();
512 return !!r.refused && r.t === undefined
513 && r.refused.includes('is not the folder the machine hand was told to work in');
514});
515
516await proved('the folder\'s name is never what settles it', 'name-compare', async () => {
517 const wrong = await ask(PATH_SITE, TOKEN_A, { name: 'site', body: HEADER + TOKEN_B + '\n' });
518 const right = await ask(PATH_SITE, TOKEN_A, { name: 'moved', body: HEADER + TOKEN_A + '\n' });
519 return wrong.proof.ok === false && right.proof.ok === true;
520});
521
522await proved('the page reads the identity file and never writes one', 'creates-file', async () => {
523 const r = await ask(PATH_A, TOKEN_A, { name: 'empty', body: null });
524 return r.proof.ok === false && r.there.dir === false && r.there.file === false;
525});
526
527await proved('the token is the first line that is not a comment', 'first-line', async () => {
528 const good = await ask(PATH_A, TOKEN_A, { name: 'alpha', body: HEADER + TOKEN_A + '\n' });
529 const bad2 = await ask(PATH_A, TOKEN_A, { name: 'commented', body: `# ${TOKEN_A}\n\n${TOKEN_B}\n` });
530 return good.proof.ok === true && bad2.proof.ok === false;
531});
532
533await proved('silence is an older hand and not a mismatch', 'silence-refuses', async () => {
534 const r = await ask(PATH_A, null, null);
535 return r.proof.ok === true && r.st.paired === true;
536});
537
538await proved('a folder swapped under the page is checked again', 'stale-cache', async () => {
539 const r = await page.evaluate(async ([root, tok, header, other, parent]) => {
540 window.__folder = await window.__folderWith(parent, 'alpha', header + tok + '\n');
541 window.__handSays(root, tok);
542 const first = await DaimondHand.workspaceProof();
543 window.__folder = await window.__folderWith(parent, 'beta', header + other + '\n');
544 const second = await DaimondHand.workspaceProof();
545 return { first: first.ok, second: second.ok };
546 }, [PATH_A, TOKEN_A, HEADER, TOKEN_B, `w${++seq}`]);
547 return r.first === true && r.second === false;
548});
549
550await proved('a workspace with no folder is refused rather than skipped', 'opfs-passes', async () => {
551 const r = await ask(PATH_A, TOKEN_A, null);
552 return r.proof.ok === false && r.st.paired === false;
553});
554
555await proved('a comparison that cannot be made is refused', 'unchecked-passes', async () => {
556 const r = await page.evaluate(async ([root, tok]) => {
557 DaimondHand.init({ folder: function () { throw new Error('the folder handle is gone'); } });
558 window.__handSays(root, tok);
559 const st = JSON.parse(await DaimondHand.status());
560 DaimondHand.init({ folder: function () { return window.__folder; } });
561 return st;
562 }, [PATH_A, TOKEN_A]);
563 return r.paired === false && r.workspace === 'unchecked';
564});
565
566await proved('an unprovable identity is refused rather than believed', 'unproven-passes', async () => {
567 const r = await ask(PATH_A, 'unproven', { name: 'alpha', body: HEADER + TOKEN_A + '\n' });
568 return r.proof.ok === false && r.st.paired === false;
569});
570
571console.log(`\n${ok.length} ok, ${bad.length} failed, `
572 + `${provedNames.length} properties proved against broken code.`);
573if (bad.length) console.log(`failed:\n ${bad.join('\n ')}`);
574await b.close();
575server.close();
576process.exit(bad.length ? 1 : (process.exitCode || 0));