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`. |
| 43 | import fs from 'node:fs'; |
| 44 | import http from 'node:http'; |
| 45 | import os from 'node:os'; |
| 46 | import path from 'node:path'; |
| 47 | import { fileURLToPath, pathToFileURL } from 'node:url'; |
| 48 | import { whyStaleWasm, refuse } from './staleguard.mjs'; |
| 49 | |
| 50 | const PW = process.env.DAIMOND_PW |
| 51 | || path.join(os.homedir(), '.red-pw/node_modules/playwright-core/index.mjs'); |
| 52 | const { chromium } = await import(pathToFileURL(PW).href); |
| 53 | const CHROME = process.env.DAIMOND_CHROME |
| 54 | || `${process.env.HOME}/.cache/ms-playwright/chromium-1229/chrome-linux64/chrome`; |
| 55 | |
| 56 | const HERE = path.dirname(fileURLToPath(import.meta.url)); |
| 57 | const ROOT = path.join(HERE, '..'); |
| 58 | const 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. |
| 63 | const 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. |
| 66 | const PATH_SITE = '/home/u/work/site'; |
| 67 | |
| 68 | const TOKEN_A = 'a1b2c3d4e5f60718293a4b5c6d7e8f90'; |
| 69 | const 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. |
| 74 | const 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 | |
| 79 | const ok = [], bad = []; |
| 80 | const 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. |
| 88 | const provedNames = []; |
| 89 | async 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 | |
| 112 | const 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`. |
| 152 | let breaking = ''; |
| 153 | |
| 154 | /// One served file's source, patched where a break names it. |
| 155 | function 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 | |
| 177 | const PAGE = `<!doctype html><meta charset="utf-8"><title>wsident</title> |
| 178 | <body> |
| 179 | <script src="/js/hand.js"><\/script> |
| 180 | <script type="module"> |
| 181 | import init, * as W from '/pkg/oxedyne_daimond.js'; |
| 182 | await init(); |
| 183 | window.Wasm = W; |
| 184 | window.__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. |
| 189 | window.__folder = null; |
| 190 | DaimondHand.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. |
| 197 | window.__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. |
| 211 | window.__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. |
| 221 | window.__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. |
| 233 | window.__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 | |
| 242 | const 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. |
| 258 | async 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. |
| 284 | refuse(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 | |
| 289 | const PORT = await listen(Number(process.env.WSIDENT_PORT || 8811)); |
| 290 | const APP = `http://127.0.0.1:${PORT}`; |
| 291 | |
| 292 | const b = await chromium.launch({ executablePath: CHROME, headless: true, |
| 293 | args: ['--no-sandbox', '--disable-dev-shm-usage'] }); |
| 294 | const page = await b.newPage(); |
| 295 | const errs = []; |
| 296 | page.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. |
| 300 | async 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. |
| 313 | let 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. |
| 323 | async 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. |
| 350 | async 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 | |
| 357 | await reload(); |
| 358 | |
| 359 | console.log('\n── 1. The same folder, and silence ───────────────────'); |
| 360 | |
| 361 | const same = await ask(PATH_A, TOKEN_A, { name: 'alpha', body: HEADER + TOKEN_A + '\n' }); |
| 362 | check('a page holding the granted folder is not refused', |
| 363 | same.proof.ok === true && same.proof.why === '', JSON.stringify(same.proof).slice(0, 200)); |
| 364 | check('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)); |
| 368 | check('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 | |
| 371 | const opened = await engine(); |
| 372 | check('so the engine composes a terminal for it', opened.t === 'open' && !opened.refused, |
| 373 | JSON.stringify(opened).slice(0, 160)); |
| 374 | |
| 375 | console.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. |
| 381 | const AvsB = await ask(PATH_A, TOKEN_A, { name: 'beta', body: HEADER + TOKEN_B + '\n' }); |
| 382 | check('a hand granted A with the page holding B is REFUSED', |
| 383 | AvsB.proof.ok === false, JSON.stringify(AvsB.proof).slice(0, 200)); |
| 384 | check('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); |
| 388 | check('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)); |
| 391 | check('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)); |
| 395 | check('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 | |
| 399 | const refusedByEngine = await engine(); |
| 400 | check('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 | |
| 405 | console.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. |
| 411 | const wrongSite = await ask(PATH_SITE, TOKEN_A, { name: 'site', body: HEADER + TOKEN_B + '\n' }); |
| 412 | check('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)); |
| 415 | const renamed = await ask(PATH_SITE, TOKEN_A, { name: 'moved', body: HEADER + TOKEN_A + '\n' }); |
| 416 | check('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 | |
| 420 | console.log('\n── 4. The other three outcomes ───────────────────────'); |
| 421 | |
| 422 | const opfs = await ask(PATH_A, TOKEN_A, null); |
| 423 | check('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 | |
| 427 | const unproven = await ask(PATH_A, 'unproven', { name: 'alpha', body: HEADER + TOKEN_A + '\n' }); |
| 428 | check('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 | |
| 432 | const silent = await ask(PATH_A, null, null); |
| 433 | check('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 | |
| 437 | console.log('\n── 5. What the page must never do ────────────────────'); |
| 438 | |
| 439 | const missing = await ask(PATH_A, TOKEN_A, { name: 'empty', body: null }); |
| 440 | check('a folder with no .daimond in it is refused', |
| 441 | missing.proof.ok === false, JSON.stringify(missing.proof).slice(0, 160)); |
| 442 | check('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 | |
| 445 | const bare = await ask(PATH_A, TOKEN_A, 'bare'); |
| 446 | check('a .daimond with no identity file in it is refused', |
| 447 | bare.proof.ok === false, JSON.stringify(bare.proof).slice(0, 160)); |
| 448 | check('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. |
| 454 | const commented = await ask(PATH_A, TOKEN_A, |
| 455 | { name: 'commented', body: `# ${TOKEN_A}\n\n${TOKEN_B}\n` }); |
| 456 | check('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. |
| 462 | const 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]); |
| 469 | check('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 | |
| 474 | console.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. |
| 479 | const 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}`]); |
| 489 | check('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 | |
| 493 | check('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 | |
| 502 | console.log('\n── 7. Proved against broken code ─────────────────────'); |
| 503 | |
| 504 | await 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 | |
| 509 | await 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 | |
| 516 | await 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 | |
| 522 | await 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 | |
| 527 | await 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 | |
| 533 | await 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 | |
| 538 | await 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 | |
| 550 | await 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 | |
| 555 | await 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 | |
| 566 | await 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 | |
| 571 | console.log(`\n${ok.length} ok, ${bad.length} failed, ` |
| 572 | + `${provedNames.length} properties proved against broken code.`); |
| 573 | if (bad.length) console.log(`failed:\n ${bad.join('\n ')}`); |
| 574 | await b.close(); |
| 575 | server.close(); |
| 576 | process.exit(bad.length ? 1 : (process.exitCode || 0)); |