oxedyne/daimond/dev/verify_verifyverb.mjs
28.5 KiB, 1 run
created by r2519314175:781, 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_verifyverb.mjs — the `verify` verb, against the real hand binary. |
| 2 | // |
| 3 | // The verb exists because a daimon cannot produce browser evidence: `listen()` is |
| 4 | // refused by the hand's seccomp filter, a browser needs the display server's unix |
| 5 | // socket and the filter takes that away, so every `dev/verify_*.mjs` that drives a |
| 6 | // real page dies under the fence a command gets. What replaces the fence here is |
| 7 | // PROVENANCE: the verb takes a NAME, looks it up in the granted tree, and builds |
| 8 | // the command itself, so nothing the model wrote reaches a process. |
| 9 | // |
| 10 | // ── What this drives, and what it does not ────────────────────────── |
| 11 | // |
| 12 | // The real `daimond-hand` binary, spawned as a process, spoken to over Chrome's |
| 13 | // native messaging framing — a 4-byte native-endian length prefix and UTF-8 JSON. |
| 14 | // No browser and no extension: `ext/hand.js` is a second line that refuses a |
| 15 | // message type it does not know, and it is not what decides any of the properties |
| 16 | // below. The hand is. |
| 17 | // |
| 18 | // The verifiers it runs are FIXTURES this file writes into a scratch git |
| 19 | // repository, never the real ones — the real suite takes hours and half of it |
| 20 | // wants a browser. What is being measured is the verb, not the suite. |
| 21 | // |
| 22 | // ── THE ONE PROPERTY THIS FILE EXISTS FOR ─────────────────────────── |
| 23 | // |
| 24 | // A verb that runs a verifier clean and reports "27 checks passed" is exactly the |
| 25 | // evidence that has been lying. So the fixture repository contains a DELIBERATELY |
| 26 | // DEAD BREAK: `verify_fixdead.mjs` declares `--break dead` and its `dead` mode |
| 27 | // does nothing whatsoever. A run of that verifier must come back saying, in its |
| 28 | // own trailer, that one break proved nothing — and must name it. That is check 7, |
| 29 | // and it is the whole feature. |
| 30 | // |
| 31 | // node dev/verify_verifyverb.mjs the checks |
| 32 | // node dev/verify_verifyverb.mjs --break realname 3 fails: a real name is sent where a bad one is expected |
| 33 | // node dev/verify_verifyverb.mjs --break declaredbreak 5 fails: a declared break is sent where an invented one is expected |
| 34 | // node dev/verify_verifyverb.mjs --break proveclean 6 fails: every break is run where clean_only is expected |
| 35 | // node dev/verify_verifyverb.mjs --break liveinstrument 7 fails: the dead break is made to bite |
| 36 | // node dev/verify_verifyverb.mjs --break silentdead 8 fails: the dead break prints an extra line |
| 37 | // node dev/verify_verifyverb.mjs --break noshot 9 fails: the fixture writes no picture |
| 38 | // node dev/verify_verifyverb.mjs --break committed 10 fails: the untracked fixture is committed |
| 39 | // node dev/verify_verifyverb.mjs --break slander 10b fails: the committed fixture is edited after its commit |
| 40 | // node dev/verify_verifyverb.mjs --break noedit 10c fails: the edited fixture is left as the commit wrote it |
| 41 | // node dev/verify_verifyverb.mjs --break nodev 1 fails: the dev directory is out of the way at the handshake |
| 42 | // node dev/verify_verifyverb.mjs --break deadlive 7d fails: the control verifier's second break is made dead |
| 43 | // node dev/verify_verifyverb.mjs --break ghost 12 fails: the fixture leaves no trace on disk |
| 44 | // node dev/verify_verifyverb.mjs --break wrongjournal 14 fails: the journal is read from an empty directory |
| 45 | // |
| 46 | // Each break damages ONE thing and reddens the check named beside it. THREE checks |
| 47 | // have no break, and it is worth saying which rather than leaving a reader to |
| 48 | // count. Check 11 -- that the report says it ran outside the command fence -- is a |
| 49 | // constant in `hand/src/verify.rs` and nothing out here can reach it. Check 1b -- |
| 50 | // that `ext/hand.js` and the hand agree on the protocol version -- reads both |
| 51 | // shipped numbers, and damaging either of them is damaging the tree rather than |
| 52 | // this file. Check 1a -- |
| 53 | // that the binary being driven is this source and not an older build -- has been |
| 54 | // demonstrated by hand (`DAIMOND_HAND_BIN=hand/target/release/daimond-hand` turns |
| 55 | // eighteen checks red against a binary from three days before the verb existed) |
| 56 | // and is deliberately NOT a break mode: a break that aborts the run leaves every |
| 57 | // later check missing rather than failing, which is the shape of instrument this |
| 58 | // whole file exists to warn about. |
| 59 | // |
| 60 | // The hand binary: `DAIMOND_HAND_BIN` if set, else the first of |
| 61 | // `hand/target/{release,debug}` or this slot's cache that is NEWER than `hand/src`, |
| 62 | // else built. A stale binary is refused rather than driven -- see `newestSource`. |
| 63 | import fs from 'node:fs'; |
| 64 | import os from 'node:os'; |
| 65 | import path from 'node:path'; |
| 66 | import { spawn, spawnSync } from 'node:child_process'; |
| 67 | import { fileURLToPath } from 'node:url'; |
| 68 | |
| 69 | // Derived, never written down: gate.sh runs the suite inside a git worktree at a |
| 70 | // different path, and an absolute path here would measure the main tree. |
| 71 | const HERE = path.dirname(fileURLToPath(import.meta.url)); |
| 72 | const ROOT = path.dirname(HERE); |
| 73 | |
| 74 | const BI = process.argv.indexOf('--break'); |
| 75 | const BEQ = process.argv.find(a => a.startsWith('--break=')); |
| 76 | const BREAK = BEQ ? BEQ.split('=')[1] : (BI >= 0 ? (process.argv[BI + 1] || '') : ''); |
| 77 | const KNOWN = ['nodev', 'realname', 'declaredbreak', 'proveclean', 'liveinstrument', 'deadlive', |
| 78 | 'silentdead', 'noshot', 'ghost', 'committed', 'slander', 'noedit', 'wrongjournal']; |
| 79 | if (BREAK && !KNOWN.includes(BREAK)) { |
| 80 | console.error(`unknown break '${BREAK}'; known: ${KNOWN.join(', ')}`); |
| 81 | process.exit(2); |
| 82 | } |
| 83 | if (BREAK) console.log(`\n*** RUNNING UNDER --break ${BREAK}: failures below are the point ***\n`); |
| 84 | |
| 85 | const ok = [], bad = []; |
| 86 | const check = (name, pass, detail) => { |
| 87 | (pass ? ok : bad).push(name); |
| 88 | console.log((pass ? ' ok ' : ' FAIL ') + name + (detail ? ' — ' + detail : '')); |
| 89 | }; |
| 90 | const note = (line) => console.log(' .. ' + line); |
| 91 | |
| 92 | // ── The scratch tree ──────────────────────────────────────────────── |
| 93 | // |
| 94 | // Under the home cache and never /tmp: that is a tmpfs here, its pages are charged |
| 95 | // to whoever wrote them, and filling it has taken this machine down before. |
| 96 | const BASE = path.join(os.homedir(), '.cache/daimond/lane-verifyverb/run'); |
| 97 | const GRANT = path.join(BASE, 'grant'); // what the hand is granted |
| 98 | const JOURNAL = path.join(BASE, 'journal'); // OUTSIDE the grant, as the hand requires |
| 99 | const DEV = path.join(GRANT, 'dev'); |
| 100 | |
| 101 | fs.rmSync(BASE, { recursive: true, force: true }); |
| 102 | fs.mkdirSync(DEV, { recursive: true }); |
| 103 | |
| 104 | // A nonce this run generated a moment ago. A fixture that prints it cannot have |
| 105 | // been stood in for: nothing but a process that read this file could know it. |
| 106 | const NONCE = 'n' + Math.random().toString(36).slice(2) + Date.now().toString(36); |
| 107 | fs.writeFileSync(path.join(DEV, 'nonce.txt'), NONCE + '\n'); |
| 108 | |
| 109 | /// The line every fixture prints its checks with, copied from the real ones. |
| 110 | const HELPER = `const say = (n, pass, detail) => console.log((pass ? ' ok ' : ' FAIL ') + n + (detail ? ' \\u2014 ' + detail : '')); |
| 111 | const bi = process.argv.indexOf('--break'); |
| 112 | const BRK = bi >= 0 ? (process.argv[bi + 1] || '') : ''; |
| 113 | `; |
| 114 | |
| 115 | // A fixture whose breaks BOTH BITE. The control: it is what proves that the verb |
| 116 | // can tell a live instrument from a dead one, and that check 7 is not simply |
| 117 | // reporting "dead" about everything. |
| 118 | fs.writeFileSync(path.join(DEV, 'verify_fixlive.mjs'), `// A fixture. Not a check of anything. |
| 119 | // node dev/verify_fixlive.mjs --break bites # 'the nonce is read' goes red |
| 120 | // node dev/verify_fixlive.mjs --break second # 'the second check' goes red |
| 121 | import fs from 'node:fs'; |
| 122 | ${HELPER}const nonce = fs.readFileSync('dev/nonce.txt', 'utf8').trim(); |
| 123 | say('the fixture ran', true); |
| 124 | say('the nonce is read', BRK !== 'bites', BRK === 'bites' ? 'not read' : nonce); |
| 125 | ${BREAK === 'deadlive' ? "say('the second check', true);" : "say('the second check', BRK !== 'second');"} |
| 126 | `); |
| 127 | |
| 128 | // **THE POINT OF THE FILE.** `dead` is declared and does nothing at all, so its |
| 129 | // run is byte-for-byte the clean run's. An instrument that cannot fail. |
| 130 | const deadBody = BREAK === 'liveinstrument' |
| 131 | ? `say('the second check', BRK !== 'dead');` // now it bites, so nothing is dead |
| 132 | : (BREAK === 'silentdead' |
| 133 | ? `if (BRK === 'dead') console.log('a line the clean run does not print');\nsay('the second check', true);` |
| 134 | : `say('the second check', true);`); |
| 135 | const shotBody = BREAK === 'noshot' |
| 136 | ? '' |
| 137 | : `fs.mkdirSync('dev/shots', { recursive: true });\nfs.writeFileSync('dev/shots/fixture.png', nonce);`; |
| 138 | fs.writeFileSync(path.join(DEV, 'verify_fixdead.mjs'), `// A fixture with a DELIBERATELY DEAD BREAK. Not a check of anything. |
| 139 | // node dev/verify_fixdead.mjs --break bites # 'the nonce is read' goes red |
| 140 | // node dev/verify_fixdead.mjs --break dead # does nothing at all, on purpose |
| 141 | import fs from 'node:fs'; |
| 142 | ${HELPER}const nonce = fs.readFileSync('dev/nonce.txt', 'utf8').trim(); |
| 143 | say('the fixture ran', true); |
| 144 | say('the nonce is read', BRK !== 'bites', BRK === 'bites' ? 'not read' : nonce); |
| 145 | ${deadBody} |
| 146 | ${BREAK === 'ghost' ? '' : "fs.writeFileSync('dev/ran-' + (BRK || 'clean') + '.txt', nonce);"} |
| 147 | ${shotBody} |
| 148 | `); |
| 149 | |
| 150 | // ── The three provenance fixtures ─────────────────────────────────── |
| 151 | // |
| 152 | // REVIEW.md 1.21. A verifier runs OUTSIDE the command fence and the whole of the |
| 153 | // reason is that its bytes came with the checkout; until 2026-08-25 nothing |
| 154 | // enforced that, so a daimon could write `dev/verify_x.mjs` with `file_write` -- |
| 155 | // every turn may, the granted root is writable -- and have it run unfenced. |
| 156 | // |
| 157 | // Each of these three is used by ONE check and by nothing else, so the break |
| 158 | // aimed at that check damages one thing. Reusing `fixlive` would refuse every run |
| 159 | // of the control verifier and redden half this file, which is the shape of |
| 160 | // instrument this whole file exists to warn against. |
| 161 | |
| 162 | // NOT in git. Once that only meant a gate building its tree with `git worktree |
| 163 | // add` would never run it; now it means the verb refuses it. |
| 164 | fs.writeFileSync(path.join(DEV, 'verify_fixloose.mjs'), `// A fixture that is not tracked. |
| 165 | import fs from 'node:fs'; |
| 166 | ${HELPER}say('the loose fixture ran', true); |
| 167 | `); |
| 168 | |
| 169 | // Committed and left alone: the file that IS the commit, which must still run. |
| 170 | fs.writeFileSync(path.join(DEV, 'verify_fixclean.mjs'), `// A fixture that is committed. |
| 171 | import fs from 'node:fs'; |
| 172 | ${HELPER}say('the committed fixture ran', true); |
| 173 | `); |
| 174 | |
| 175 | // Committed and then edited, below, after the commit. `git ls-files |
| 176 | // --error-unmatch` goes on exiting 0 for this file while `git diff --quiet HEAD` |
| 177 | // exits 1, which is the whole of why the question is content and not membership. |
| 178 | fs.writeFileSync(path.join(DEV, 'verify_fixedit.mjs'), `// A fixture that is edited after its commit. |
| 179 | import fs from 'node:fs'; |
| 180 | ${HELPER}say('the edited fixture ran', true); |
| 181 | `); |
| 182 | |
| 183 | const git = (...args) => spawnSync('git', ['-C', GRANT, ...args], |
| 184 | { stdio: 'ignore', env: { ...process.env, GIT_CONFIG_GLOBAL: '/dev/null', GIT_CONFIG_SYSTEM: '/dev/null' } }); |
| 185 | git('init', '-q'); |
| 186 | git('config', 'user.email', 'fixture@example.invalid'); |
| 187 | git('config', 'user.name', 'Fixture'); |
| 188 | git('add', 'dev/verify_fixlive.mjs', 'dev/verify_fixdead.mjs', 'dev/verify_fixclean.mjs', |
| 189 | 'dev/verify_fixedit.mjs', 'dev/nonce.txt'); |
| 190 | // COMMITTED and not merely added, because the check this damages is about the |
| 191 | // bytes and not about the index -- `git add` alone leaves the file refused, so a |
| 192 | // break that only staged it would damage nothing and read as green. |
| 193 | if (BREAK === 'committed') { |
| 194 | git('add', 'dev/verify_fixloose.mjs'); |
| 195 | } |
| 196 | git('commit', '-q', '-m', 'fixtures'); |
| 197 | const editLine = '// and now it is mine\n'; |
| 198 | // Unconditional: `fixedit` is the edited case, and 'noedit' takes the edit away. |
| 199 | if (BREAK !== 'noedit') fs.appendFileSync(path.join(DEV, 'verify_fixedit.mjs'), editLine); |
| 200 | // And the one that damages 10b, by making the unslandered file slanderable. |
| 201 | if (BREAK === 'slander') fs.appendFileSync(path.join(DEV, 'verify_fixclean.mjs'), editLine); |
| 202 | |
| 203 | // ── The hand ──────────────────────────────────────────────────────── |
| 204 | |
| 205 | /// When the newest of the hand's own sources was last changed. |
| 206 | /// |
| 207 | /// **The staleness guard, and it is not decoration.** `hand/target/release/daimond-hand` |
| 208 | /// on this machine was three days older than the verb, and a stale binary answers |
| 209 | /// `verify` with "there is no request called verify" — which every check below would |
| 210 | /// have read as a refusal and half of them would have PASSED on. A test that measures |
| 211 | /// the wrong binary proves nothing in either direction, and it fails silently towards |
| 212 | /// green, which is the worst way for it to fail. |
| 213 | function newestSource() { |
| 214 | let newest = 0; |
| 215 | const look = (dir) => { |
| 216 | let names = []; |
| 217 | try { names = fs.readdirSync(dir); } catch (e) { return; } |
| 218 | for (const n of names) { |
| 219 | const f = path.join(dir, n); |
| 220 | let st; |
| 221 | try { st = fs.statSync(f); } catch (e) { continue; } |
| 222 | if (st.isDirectory()) look(f); |
| 223 | else if (/\.(rs|toml)$/.test(n)) newest = Math.max(newest, st.mtimeMs); |
| 224 | } |
| 225 | }; |
| 226 | look(path.join(ROOT, 'hand/src')); |
| 227 | try { newest = Math.max(newest, fs.statSync(path.join(ROOT, 'hand/Cargo.toml')).mtimeMs); } |
| 228 | catch (e) { /* no manifest, and the build below will say so */ } |
| 229 | return newest; |
| 230 | } |
| 231 | |
| 232 | /// The hand binary to drive, built if the ones lying about are older than the source. |
| 233 | /// |
| 234 | /// **The ambient CARGO_TARGET_DIR is deliberately NOT used.** The hand is its own |
| 235 | /// workspace root; building it into a target directory the app is also built into |
| 236 | /// puts two differently resolved copies of one crate in the same place, and the |
| 237 | /// binary then links a mix. The tell is rustc's "multiple different versions of |
| 238 | /// crate X" while `cargo tree -d` shows one. So the hand gets a directory of its |
| 239 | /// own, named for it, and `DAIMOND_HAND_BIN` skips the question entirely. |
| 240 | function handBinary() { |
| 241 | if (process.env.DAIMOND_HAND_BIN) return { bin: process.env.DAIMOND_HAND_BIN, fresh: true, why: 'named by DAIMOND_HAND_BIN' }; |
| 242 | const src = newestSource(); |
| 243 | const target = process.env.DAIMOND_HAND_TARGET_DIR |
| 244 | || path.join(os.homedir(), '.cache/cargo-targets', process.env.RC_SLOT || 'solo', 'daimond-hand'); |
| 245 | // What `hand/install/README.md` names and what verify_handreal.mjs builds, reused |
| 246 | // where it is already there AND newer than the source. |
| 247 | const seen = []; |
| 248 | for (const c of [ |
| 249 | path.join(ROOT, 'hand/target/release/daimond-hand'), |
| 250 | path.join(ROOT, 'hand/target/debug/daimond-hand'), |
| 251 | path.join(target, 'debug', 'daimond-hand'), |
| 252 | ]) { |
| 253 | let st; |
| 254 | try { st = fs.statSync(c); } catch (e) { continue; } |
| 255 | if (st.mtimeMs >= src) return { bin: c, fresh: true, why: 'newer than hand/src' }; |
| 256 | seen.push(`${c} is ${Math.round((src - st.mtimeMs) / 1000)}s older than hand/src`); |
| 257 | } |
| 258 | note('the hand binaries here are older than its source; building into ' + target); |
| 259 | const r = spawnSync('cargo', ['build', '--manifest-path', path.join(ROOT, 'hand/Cargo.toml')], |
| 260 | { cwd: ROOT, stdio: 'inherit', env: { ...process.env, CARGO_TARGET_DIR: target } }); |
| 261 | const built = path.join(target, 'debug', 'daimond-hand'); |
| 262 | if (r.status !== 0 || !fs.existsSync(built)) { |
| 263 | return { bin: null, fresh: false, why: seen.join('; ') || 'the hand did not build' }; |
| 264 | } |
| 265 | return { bin: built, fresh: true, why: 'built here' }; |
| 266 | } |
| 267 | |
| 268 | const { bin: HAND, fresh: FRESH, why: WHY } = handBinary(); |
| 269 | check('1a the hand binary is there, and is this source rather than an older one', |
| 270 | !!HAND && fs.existsSync(HAND) && FRESH, `${HAND || 'none'} — ${WHY}`); |
| 271 | if (!HAND || !fs.existsSync(HAND) || !FRESH) { |
| 272 | console.log(`\n${ok.length} ok, ${bad.length} failed`); |
| 273 | process.exit(1); |
| 274 | } |
| 275 | |
| 276 | const LE = os.endianness() === 'LE'; |
| 277 | |
| 278 | /// One conversation with a fresh hand process. |
| 279 | /// |
| 280 | /// A process per exchange, which is what Chrome does: it starts a host for each |
| 281 | /// port and kills it when the port closes. It also means one request cannot leave |
| 282 | /// state behind for the next, so a check that passed because of an earlier one is |
| 283 | /// not a failure mode this file has. |
| 284 | function talk(messages, ms = 60000) { |
| 285 | return new Promise((resolve) => { |
| 286 | const child = spawn(HAND, [], { |
| 287 | cwd: BASE, |
| 288 | env: { |
| 289 | ...process.env, |
| 290 | DAIMOND_HAND_ROOT: GRANT, |
| 291 | DAIMOND_HAND_JOURNAL_DIR: JOURNAL, |
| 292 | }, |
| 293 | stdio: ['pipe', 'pipe', 'pipe'], |
| 294 | }); |
| 295 | const got = []; |
| 296 | let buf = Buffer.alloc(0); |
| 297 | let err = ''; |
| 298 | let done = false; |
| 299 | const finish = () => { |
| 300 | if (done) return; |
| 301 | done = true; |
| 302 | clearTimeout(timer); |
| 303 | try { child.kill('SIGKILL'); } catch (e) { /* already gone */ } |
| 304 | resolve({ msgs: got, stderr: err }); |
| 305 | }; |
| 306 | const timer = setTimeout(finish, ms); |
| 307 | child.stderr.on('data', (d) => { err += d.toString(); }); |
| 308 | child.stdout.on('data', (d) => { |
| 309 | buf = Buffer.concat([buf, d]); |
| 310 | for (;;) { |
| 311 | if (buf.length < 4) return; |
| 312 | const n = LE ? buf.readUInt32LE(0) : buf.readUInt32BE(0); |
| 313 | if (buf.length < 4 + n) return; |
| 314 | const body = buf.subarray(4, 4 + n).toString('utf8'); |
| 315 | buf = buf.subarray(4 + n); |
| 316 | try { got.push(JSON.parse(body)); } catch (e) { got.push({ t: '__unparseable', body }); } |
| 317 | // `ended` and `refused` are the two ways an answer finishes. |
| 318 | const last = got[got.length - 1]; |
| 319 | // `error` is terminal here as well as `ended` and `refused`: a hand that does |
| 320 | // not know the message answers with one and never with an ending, and waiting |
| 321 | // for an ending that is not coming is a test that hangs rather than fails. |
| 322 | if (last && (last.t === 'ended' || last.t === 'refused' || last.t === 'error')) { |
| 323 | // Give the writer a moment to flush anything queued behind it. |
| 324 | setTimeout(finish, 150); |
| 325 | } |
| 326 | } |
| 327 | }); |
| 328 | child.on('error', finish); |
| 329 | child.on('exit', () => setTimeout(finish, 50)); |
| 330 | for (const m of messages) { |
| 331 | const body = Buffer.from(JSON.stringify(m), 'utf8'); |
| 332 | const head = Buffer.alloc(4); |
| 333 | if (LE) head.writeUInt32LE(body.length, 0); else head.writeUInt32BE(body.length, 0); |
| 334 | child.stdin.write(Buffer.concat([head, body])); |
| 335 | } |
| 336 | }); |
| 337 | } |
| 338 | |
| 339 | /// The protocol version the PAGE announces, read out of the page's own relay. |
| 340 | /// |
| 341 | /// Written down here as `1` until 2026-08-25, when `hand/src/lib.rs` went to 2 and |
| 342 | /// `ext/hand.js` went with it. Every exchange below then met `proto_refusal` and |
| 343 | /// twenty-three checks went red at once, none of them about anything this file |
| 344 | /// measures. A constant restated in a third place is a third place to forget. |
| 345 | /// |
| 346 | /// It is read from `ext/hand.js` rather than from the hand, deliberately: this file |
| 347 | /// STANDS IN FOR THE PAGE, and a stand-in that asked the hand what to say could not |
| 348 | /// be wrong about it. Reading the page's own number means the two shipped halves |
| 349 | /// have to agree, and check 1b below says so in one line when they do not. |
| 350 | function pageProto() { |
| 351 | const src = fs.readFileSync(path.join(ROOT, 'ext/hand.js'), 'utf8'); |
| 352 | const m = /^\s*const PROTO\s*=\s*(\d+)\s*;/m.exec(src); |
| 353 | return m ? Number(m[1]) : null; |
| 354 | } |
| 355 | |
| 356 | /// What the hand itself says it speaks, from the mode a person runs. |
| 357 | function handProto(bin) { |
| 358 | const r = spawnSync(bin, ['--report'], { encoding: 'utf8', timeout: 30000 }); |
| 359 | const m = /^protocol (\d+)$/m.exec((r.stdout || '') + (r.stderr || '')); |
| 360 | return m ? Number(m[1]) : null; |
| 361 | } |
| 362 | |
| 363 | const PAGE_PROTO = pageProto(); |
| 364 | const HAND_PROTO = handProto(HAND); |
| 365 | check('1b the hand and the page agree on what protocol they speak', |
| 366 | PAGE_PROTO !== null && PAGE_PROTO === HAND_PROTO, |
| 367 | `ext/hand.js says ${PAGE_PROTO}, ${path.basename(HAND)} --report says ${HAND_PROTO}`); |
| 368 | |
| 369 | const HELLO = { t: 'hello', proto: PAGE_PROTO, client: 'verify_verifyverb' }; |
| 370 | |
| 371 | /// One verify request, and everything the hand said about it. |
| 372 | async function verify(req, ms) { |
| 373 | const r = await talk([HELLO, { t: 'verify', id: 'v1', timeout_ms: 60000, break: null, ...req }], ms); |
| 374 | const hello = r.msgs.find(m => m.t === 'hello'); |
| 375 | const refused = r.msgs.find(m => m.t === 'refused'); |
| 376 | const ended = r.msgs.find(m => m.t === 'ended'); |
| 377 | const out = r.msgs.filter(m => m.t === 'chunk' && m.stream === 'out').map(m => m.data).join(''); |
| 378 | const progress = r.msgs.filter(m => m.t === 'chunk' && m.stream === 'err').map(m => m.data).join(''); |
| 379 | return { hello, refused, ended, out, progress, stderr: r.stderr, msgs: r.msgs }; |
| 380 | } |
| 381 | |
| 382 | /// The trailer line, which is where all three numbers live. |
| 383 | const trailerOf = (out) => |
| 384 | (out.split('\n').reverse().find(l => l.trim().startsWith('[verify:')) || '').trim(); |
| 385 | |
| 386 | // ── 1. The handshake says whether this folder has verifiers ───────── |
| 387 | // |
| 388 | // No caller break: the capability is computed in the hand from the directory, and |
| 389 | // there is nothing out here that can make it lie. |
| 390 | |
| 391 | { |
| 392 | // Surgical: the directory goes back before anything else asks about it, so this |
| 393 | // break reddens check 1 and nothing else. |
| 394 | const aside = path.join(GRANT, 'dev-aside'); |
| 395 | if (BREAK === 'nodev') fs.renameSync(DEV, aside); |
| 396 | const r = await talk([HELLO, { t: 'bye' }]); |
| 397 | if (BREAK === 'nodev') fs.renameSync(aside, DEV); |
| 398 | const caps = (r.msgs.find(m => m.t === 'hello') || {}).caps || []; |
| 399 | check('1 the handshake says this folder has verifiers', caps.includes('verify:dev'), |
| 400 | JSON.stringify(caps)); |
| 401 | } |
| 402 | |
| 403 | // ── 2, 3, 4. A name is a selector, and nothing else ───────────────── |
| 404 | |
| 405 | { |
| 406 | const r = await verify({ name: BREAK === 'realname' ? 'fixlive' : 'nosuchthing', breaks: 'none' }); |
| 407 | check('2 a name that is not a verifier is refused', |
| 408 | !!r.refused && /Refused:/.test(r.refused.reason || ''), |
| 409 | r.refused ? r.refused.reason.slice(0, 110) : `no refusal; ended=${JSON.stringify(r.ended)}`); |
| 410 | check('3 the refusal says nothing was run', |
| 411 | !!r.refused && /Nothing was run/.test(r.refused.reason || ''), |
| 412 | r.refused ? r.refused.reason.slice(0, 110) : 'no refusal'); |
| 413 | } |
| 414 | |
| 415 | { |
| 416 | // A path, which is the shape a model reaches for first. Refused on its |
| 417 | // alphabet, before the directory is read at all. |
| 418 | const r = await verify({ name: BREAK === 'realname' ? 'fixlive' : 'dev/verify_fixlive.mjs', breaks: 'none' }); |
| 419 | check('4 a path is refused as a name', |
| 420 | !!r.refused && /not a verifier name|does not know the message/.test(r.refused.reason || ''), |
| 421 | r.refused ? r.refused.reason.slice(0, 110) : 'no refusal'); |
| 422 | } |
| 423 | |
| 424 | // ── 5. A break must be one the verifier itself declares ───────────── |
| 425 | |
| 426 | { |
| 427 | const r = await verify({ |
| 428 | name: 'fixdead', |
| 429 | breaks: 'one', |
| 430 | break: BREAK === 'declaredbreak' ? 'bites' : 'inventedbreak', |
| 431 | }); |
| 432 | const why = r.refused ? r.refused.reason : ''; |
| 433 | check('5 a break the verifier does not declare is refused', |
| 434 | !!r.refused && /does not declare a break/.test(why), |
| 435 | r.refused ? why.slice(0, 120) : `no refusal; trailer=${trailerOf(r.out)}`); |
| 436 | check('5b the refusal lists the breaks it does declare', |
| 437 | /\bbites\b/.test(why) && /\bdead\b/.test(why), |
| 438 | why.slice(0, 160) || 'no refusal'); |
| 439 | } |
| 440 | |
| 441 | // ── 6. A clean-only run is labelled, in the words a model repeats ─── |
| 442 | |
| 443 | { |
| 444 | const r = await verify({ name: 'fixlive', breaks: BREAK === 'proveclean' ? 'all' : 'none' }); |
| 445 | const t = trailerOf(r.out); |
| 446 | check('6 a clean-only run is labelled NOT PROVEN', /NOT PROVEN/.test(t), t || r.out.slice(0, 200)); |
| 447 | check('6b and says it is not evidence', /not evidence/i.test(r.out), |
| 448 | t || r.out.slice(0, 200)); |
| 449 | } |
| 450 | |
| 451 | // ── 7. THE FEATURE: a dead break is counted and named ─────────────── |
| 452 | |
| 453 | { |
| 454 | const r = await verify({ name: 'fixdead', breaks: 'all' }, 120000); |
| 455 | const t = trailerOf(r.out); |
| 456 | check('7 a break that reddens nothing is counted in the third number', |
| 457 | /1 breaks proved nothing/.test(t), t || r.out.slice(0, 300)); |
| 458 | check('7b the live break beside it is counted red', |
| 459 | /1 breaks confirmed red/.test(t), t || r.out.slice(0, 300)); |
| 460 | check('7c the dead break is named so the check it aims at can be disowned', |
| 461 | /PROVED NOTHING/.test(r.out) && /^BREAK dead\b/m.test(r.out), |
| 462 | (r.out.match(/^BREAK .*$/gm) || []).join(' | ').slice(0, 200)); |
| 463 | check('8 a dead break whose output is the clean run\'s is said to be exactly that', |
| 464 | /its output was the clean run's/.test(r.out), |
| 465 | (r.out.match(/^BREAK dead.*$/m) || ['(no dead line)'])[0].slice(0, 200)); |
| 466 | check('9 pictures the run wrote are named by path', |
| 467 | /dev\/shots\/fixture\.png/.test(r.out), |
| 468 | (r.out.match(/dev\/shots\/[^\s]*/g) || []).join(' ') || 'none named'); |
| 469 | check('11 the report says it ran outside the command fence', |
| 470 | /OUTSIDE the command fence/.test(r.out), r.out.slice(0, 120)); |
| 471 | // A report is text and text can be fabricated. What cannot is a file on this |
| 472 | // disk holding a nonce generated seconds ago: only a process that read |
| 473 | // dev/nonce.txt could have written it, and there is one per run, so the |
| 474 | // sequence really made three of them. |
| 475 | const ran = ['clean', 'bites', 'dead'] |
| 476 | .map(w => path.join(DEV, `ran-${w}.txt`)) |
| 477 | .map(f => { try { return fs.readFileSync(f, 'utf8').trim(); } catch (e) { return ''; } }); |
| 478 | check('12 all three runs really happened, each leaving this run\'s nonce on disk', |
| 479 | ran.length === 3 && ran.every(v => v === NONCE), |
| 480 | `clean=${ran[0] === NONCE} bites=${ran[1] === NONCE} dead=${ran[2] === NONCE}`); |
| 481 | // The exit status is the tri-state: 0 proved, 1 the code failed, 2 unproven. |
| 482 | check('13 a sequence holding a dead break does not exit as proved', |
| 483 | !!r.ended && r.ended.exit === 2, |
| 484 | r.ended ? `exit ${r.ended.exit}` : 'no ending'); |
| 485 | } |
| 486 | |
| 487 | // ── 7d. The control: every break biting IS reported as proved ─────── |
| 488 | // |
| 489 | // Without this, check 7 would pass just as well on a verb that called everything |
| 490 | // dead. Both halves are needed and only both together mean anything. |
| 491 | |
| 492 | { |
| 493 | const r = await verify({ name: 'fixlive', breaks: 'all' }, 120000); |
| 494 | const t = trailerOf(r.out); |
| 495 | check('7d a verifier whose breaks all bite is reported as proved', |
| 496 | /2 breaks confirmed red/.test(t) && /0 breaks proved nothing/.test(t), |
| 497 | t || r.out.slice(0, 300)); |
| 498 | check('13b and it exits as proved', |
| 499 | !!r.ended && r.ended.exit === 0, r.ended ? `exit ${r.ended.exit}` : 'no ending'); |
| 500 | } |
| 501 | |
| 502 | // ── 10. Whose code is this -- ENFORCED, not merely reported ───────── |
| 503 | // |
| 504 | // REVIEW.md 1.21, and three checks because one would not do it. 10 is the |
| 505 | // untracked case. 10c is the edited case, which membership cannot see at all: |
| 506 | // `git ls-files --error-unmatch` exits 0 for a file with a comment appended while |
| 507 | // `git diff --quiet HEAD` exits 1. And 10b is what keeps both honest -- a file |
| 508 | // that IS the commit must still run, so a verb that refused everything would go |
| 509 | // red here rather than reading as a pass. |
| 510 | |
| 511 | { |
| 512 | const r = await verify({ name: 'fixloose', breaks: 'none' }); |
| 513 | const why = r.refused ? r.refused.reason : ''; |
| 514 | check('10 a verifier git does not know is REFUSED, not run', |
| 515 | !!r.refused && /not this repository's committed code/.test(why), |
| 516 | r.refused ? why.slice(0, 140) |
| 517 | : `no refusal; report=${(r.out.split('\n')[0] || '').slice(0, 120)}`); |
| 518 | // A refusal a model cannot converge on costs the run anyway, so the sentence |
| 519 | // has to carry the fenced route, the tool that takes it, and what fenced |
| 520 | // running cannot do -- or a daimon reads the fence as its script being broken. |
| 521 | check('10a and the refusal hands over the fenced way to run it anyway', |
| 522 | /\["node","dev\/verify_fixloose\.mjs"\]/.test(why) && /'run'/.test(why) |
| 523 | && /browser/.test(why) && /commit the file/.test(why), |
| 524 | why.slice(0, 220) || 'no refusal'); |
| 525 | } |
| 526 | { |
| 527 | const r = await verify({ name: 'fixclean', breaks: 'none' }); |
| 528 | check('10b and one that IS the commit is not slandered', |
| 529 | !r.refused && /byte for byte the commit's/.test(r.out), |
| 530 | r.refused ? r.refused.reason.slice(0, 140) : (r.out.split('\n')[0] || '').slice(0, 160)); |
| 531 | } |
| 532 | { |
| 533 | const r = await verify({ name: 'fixedit', breaks: 'none' }); |
| 534 | check('10c an edit made after the commit is seen, which the index cannot show', |
| 535 | !!r.refused && /not what the commit holds/.test(r.refused.reason || ''), |
| 536 | r.refused ? r.refused.reason.slice(0, 140) |
| 537 | : `RAN -- the check followed the index; report=${(r.out.split('\n')[0] || '').slice(0, 100)}`); |
| 538 | } |
| 539 | |
| 540 | // ── 14. The journal holds the node command, not a verb ────────────── |
| 541 | |
| 542 | { |
| 543 | const where = BREAK === 'wrongjournal' ? path.join(BASE, 'empty') : JOURNAL; |
| 544 | if (BREAK === 'wrongjournal') fs.mkdirSync(where, { recursive: true }); |
| 545 | const files = fs.existsSync(where) |
| 546 | ? fs.readdirSync(where).filter(f => f.endsWith('.jsonl') || f.includes('journal')) |
| 547 | : []; |
| 548 | const text = files.map(f => { |
| 549 | try { return fs.readFileSync(path.join(where, f), 'utf8'); } catch (e) { return ''; } |
| 550 | }).join('\n'); |
| 551 | check('14 the journal records the real node command line', |
| 552 | /verify_fixdead\.mjs/.test(text) && /"?fence:none"?/.test(text), |
| 553 | files.join(', ') || 'no journal files found'); |
| 554 | } |
| 555 | |
| 556 | // ── The summary ───────────────────────────────────────────────────── |
| 557 | |
| 558 | console.log(`\n${ok.length} ok, ${bad.length} failed`); |
| 559 | if (BREAK) { |
| 560 | console.log(bad.length |
| 561 | ? `\nbreak '${BREAK}' produced failures, as it must.` |
| 562 | : `\nBREAK '${BREAK}' CHANGED NOTHING — the check it targets is not proving anything.`); |
| 563 | process.exit(bad.length ? 0 : 1); |
| 564 | } |
| 565 | process.exit(bad.length ? 1 : 0); |