// Every repair/diagnostic module must be REACHABLE as a command. Run: node test/cli-reachable.test.mjs
//
// WHY THIS TEST EXISTS. `lib/repair-strongrefs.js` (0.3.3) and `lib/conformance.js` (0.3.4) both shipped
// with full test coverage and NO CALLER — the only thing that imported either was its own test. They were
// correct, proven, and impossible to run. `repair-strongrefs.js`'s own header states the rule ("A defect
// whose repair is un-runnable is not fixed") and then shipped breaking it; #23 lesson 7 says the same
// thing after the fact ("A repair that cannot be run is not a fix. Two fixes existed only as knowledge
// until turned into commands").
//
// So this does NOT test the repairs — they have their own suites. It tests the property that was
// actually missing: that a module whose whole purpose is to be RUN is wired to something a human can
// type. A new lib/repair-*.js will fail this the moment it lands without a command, which is the point.
//
// Deliberately source-level (read the files, no import): lib/cli.js pulls bare-process/bare-path and the
// record store, none of which load cleanly under plain node, and the property under test is a wiring
// fact that source text answers exactly.
import { readFileSync, readdirSync } from 'node:fs'
import { fileURLToPath } from 'node:url'
import { dirname, join } from 'node:path'
const root = join(dirname(fileURLToPath(import.meta.url)), '..')
const cli = readFileSync(join(root, 'lib', 'cli.js'), 'utf8')
let pass = 0
const ok = (c, m) => { if (!c) { console.error('FAIL:', m); process.exit(1) } console.log(' ok -', m); pass++ }
// The modules that exist to be executed by an operator. A repair module is identified by name, so the
// convention (lib/repair-*.js) is load-bearing — that is a fair trade for catching the whole class.
const RUNNABLE = readdirSync(join(root, 'lib'))
.filter((f) => f.startsWith('repair-') || f === 'conformance.js')
.map((f) => f.replace(/\.js$/, ''))
ok(RUNNABLE.length >= 2, `found the runnable modules to check (${RUNNABLE.join(', ')})`)
// 1. Every runnable module is REACHABLE from a typed command. Two shapes qualify, and the distinction is
// architectural rather than stylistic: a module that only needs the record store is imported by the
// CLI directly, while one that needs the DAEMON'S state cannot be — the daemon holds the index store's
// lock, so `repair-bytes` runs inside it and the CLI drives it over the UDS control surface (the same
// reason `fronts`/`lens`/`status` read the running process instead of building a second router).
// What matters to an operator is identical either way: a verb they can type. So test THAT.
const dispatched = (mod) => new RegExp(`cmd === '${mod}'`).test(cli)
for (const mod of RUNNABLE) {
ok(cli.includes(`./${mod}.js`) || dispatched(mod),
`lib/${mod}.js is reachable — imported by the CLI, or driven by a '${mod}' command`)
}
// 2. Each is listed in CLI_COMMANDS, which is what bin/favela-web.js dispatches on. Wiring the handler but
// forgetting the list is a silent no-op: the launcher falls through and boots the SERVER instead.
const listed = (cli.match(/export const CLI_COMMANDS = \[([^\]]*)\]/) || [])[1] || ''
for (const cmd of RUNNABLE) {
ok(listed.includes(`'${cmd}'`), `'${cmd}' is in CLI_COMMANDS — the launcher routes it instead of booting the server`)
ok(dispatched(cmd), `'${cmd}' is dispatched in runCli`)
}
// 2b. ⛔ AND IT IS DISCOVERABLE. The 2026-08-06 audit found the byte gap by hand because nothing in the
// CLI advertised a repair for it. A healing command an operator cannot FIND is only marginally better
// than one that does not exist, so every runnable module must appear in the HEALERS registry that
// `favela-web heal` prints.
const healers = (cli.match(/export const HEALERS = \[([\s\S]*?)\n\]/) || [])[1] || ''
ok(healers.length > 0, 'the CLI exports a HEALERS registry')
for (const mod of RUNNABLE) {
ok(new RegExp(`name: '${mod}'`).test(healers), `'${mod}' is listed in HEALERS — 'favela-web heal' shows it`)
}
ok(/cmd === 'heal'/.test(cli), "'heal' is dispatched — the one place an operator can look")
// 2c. The convergence verbs. lib/repo-sync.js is one module behind THREE commands, so the module-name
// convention above cannot reach it — assert the verbs directly. These are the ones that answer the
// INBOUND question ("does my copy match the authority"), which nothing asked before 2026-08-06.
for (const cmd of ['head', 'diff', 'backfill']) {
ok(listed.includes(`'${cmd}'`), `'${cmd}' is in CLI_COMMANDS`)
ok(dispatched(cmd), `'${cmd}' is dispatched in runCli`)
ok(new RegExp(`name: '${cmd}'`).test(healers), `'${cmd}' is listed in HEALERS`)
}
ok(/const write = hasFlag\(args, '--write'\)/.test(cli.slice(cli.indexOf('function backfillCmd'))), 'backfill is DRY RUN unless --write')
// 3. A repair must default to DRY RUN. Every destructive step in the 2026-07-31 incident was previewed
// before it was applied, and that is the only reason a wrong reconstruction was caught rather than
// written. A repair command that writes by default removes that safety. Asserted for EVERY repair, not
// just the first one to land.
const repairCmds = RUNNABLE.filter((m) => m.startsWith('repair-'))
ok(repairCmds.length >= 2, `checking dry-run defaults for every repair (${repairCmds.join(', ')})`)
for (const cmd of repairCmds) {
// Locate the handler by NAME-INSENSITIVE match: `repair-strongrefs` is handled by
// `repairStrongRefsCmd`, whose internal capitalisation a mechanical camelCase of the command name does
// not reproduce. The property under test is the dry-run default, not a naming convention.
const want = cmd.replace(/-/g, '') + 'cmd'
const found = [...cli.matchAll(/function\s+([A-Za-z0-9_]+Cmd)\s*\(/g)]
.find((m) => m[1].toLowerCase() === want)
ok(!!found, `${cmd} has a handler function`)
const at = cli.indexOf(found[0])
const body = cli.slice(at, at + 900)
ok(/const write = hasFlag\(args, '--write'\)/.test(body), `${cmd} is DRY RUN unless --write is passed`)
}
// 4. ⛔ The repair must write through hpp, never straight into the record store. hpp is the authority for
// those bytes: it hashes what it writes and commits that hash to the shared autobase. A direct
// store.put() from here is a SECOND WRITER into hpp's canonical store — verbatim the clobber that
// permanently lost two records on 2026-07-31 (#23 root cause 2).
ok(/applyWrite: \(op\) => w\.putRecord\(/.test(cli), 'repair writes go through hpp (putRecord), not store.put()')
ok(!/applyWrite:[^\n]*store\.put/.test(cli), 'the CLI never writes directly into the shared record store')
// 5. A ZERO READ MUST BE LOUD. Shipped 0.3.6 defaulted the record root to a favela-local path that does
// not exist, so both commands opened an empty store and printed "checked 0 · 0 findings" — a clean
// bill of health for a directory that was never there. The daemon gets ATPROTO_OVERLAY_ROOT from its
// install record when the SUPERVISOR spawns it; a CLI call inherits none of that. Silent zeros are the
// dominant failure mode in this codebase (#23 lesson 4: "Both real interop defects reported success
// locally while the network saw nothing") — a diagnostic that does it is worse than no diagnostic.
ok(/function assertRead/.test(cli), 'the CLI has an explicit guard against reading zero records')
ok(/assertRead\(s\.checked \|\| 0, did\)/.test(cli), 'conformance refuses to report health on a zero read')
ok(/assertRead\(r\.checked \|\| 0, did\)/.test(cli), 'repair-strongrefs refuses to report clean on a zero read')
ok(/process\.exit\(1\)/.test(cli.slice(cli.indexOf('function assertRead'))), 'and it EXITS NON-ZERO rather than printing a reassuring summary')
// The default must point at hpp's real record dir, not a favela-local path that never exists.
ok(/hyper-proto-peer\/data\/atproto-repo/.test(cli), 'the default record root is hpp\'s actual directory')
// 6. SAME BUG, SECOND PATH. A CLI call inherits none of the supervisor's env, so any default built from
// an unset env var must still resolve to the daemon's REAL location. `repair-bytes` first shipped with
// `(FAVELA_STORE_ROOT || 'data') + '/svc.sock'`, which resolved against the operator's cwd and could
// not find a running daemon. A relative default is the tell.
ok(/\.config\/favela-web\/data\/svc\.sock/.test(cli), 'the default control socket is an ABSOLUTE path under $HOME')
ok(!/\|\| 'data'\) \+ '\/svc\.sock'/.test(cli), 'no relative cwd-dependent socket default')
// 7. And it must speak the control socket's ACTUAL verb. hyper-svc's Client exposes call(op, params);
// `client.request(...)` is not a function and fails only at runtime, against a live daemon — which is
// exactly where it was found, one publish too late.
ok(/client\.call\('repair-bytes'/.test(cli), 'the CLI uses hyper-svc Client.call(), the method that exists')
ok(!/client\.request\(/.test(cli), 'and never client.request(), which does not')
console.log(`\nCLI-REACHABLE OK: ${pass} assertions`)