Files
orca/config/scripts/diagnostics/runtime-lifecycle/archive-hook-removal-repro.mjs
Neil 6bb2e13c35 refactor(tooling): organize remaining scripts by domain owners (#38407)
* refactor(tooling): group remaining scripts by concrete domain owners

* fix(tooling): preserve relocated launcher resource roots
2026-11-05 00:29:16 -08:00

502 lines
18 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Run node config/scripts/archive-hook-removal-repro.mjs after build:cli/build:electron-vite.
// Drive built CLI/headless runtime against real worktrees: failed/unverifiable archive hooks must preserve registration and file contents.
import { spawn, spawnSync } from 'node:child_process'
import { mkdtempSync, writeFileSync, readFileSync, existsSync, rmSync, mkdirSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join, dirname, resolve } from 'node:path'
import { randomBytes } from 'node:crypto'
const projectDir = resolve(import.meta.dirname, '../../../..')
const serveEntry = join(projectDir, 'out', 'main', 'index.js')
const cliEntry = join(projectDir, 'out', 'cli', 'index.js')
const PORT = 6900 + Math.floor(Math.random() * 400)
const READY_TIMEOUT_MS = 180_000
const control = mkdtempSync(join(tmpdir(), 'agh-control-'))
const modeFile = join(control, 'mode')
const ranFile = join(control, 'ran')
const setMode = (m) => writeFileSync(modeFile, m)
const hookRuns = () => (existsSync(ranFile) ? readFileSync(ranFile, 'utf8').trim().split('\n') : [])
let failures = 0
const out = (s) => process.stdout.write(`${s}\n`)
const banner = (s) => out(`\n${'='.repeat(78)}\n${s}\n${'='.repeat(78)}`)
function check(label, ok, detail = '') {
out(` ${ok ? 'PASS' : 'FAIL'} ${label}${detail ? ` -- ${detail}` : ''}`)
if (!ok) {
failures++
}
}
let pairingCode = null
function cli(args, { json = true } = {}) {
return spawnSync(
process.execPath,
[cliEntry, ...args, '--pairing-code', pairingCode, ...(json ? ['--json'] : [])],
{ encoding: 'utf8', shell: false }
)
}
function ok(args) {
const r = cli(args)
const parsed = parseJsonLine(r)
if (!parsed) {
throw new Error(`orca ${args.join(' ')} produced no JSON:\n${r.stdout}\n${r.stderr}`)
}
if (parsed.ok === false) {
throw new Error(`orca ${args.join(' ')} failed: ${parsed.error?.code} ${parsed.error?.message}`)
}
return parsed.result
}
/** The CLI pretty-prints one JSON document to stdout. */
function parseJsonLine(r) {
const text = (r.stdout ?? '').trim()
const start = text.indexOf('{')
if (start === -1) {
return null
}
try {
return JSON.parse(text.slice(start))
} catch {
return null
}
}
function git(cwd, ...args) {
const r = spawnSync('git', args, { cwd, encoding: 'utf8' })
if (r.status !== 0) {
throw new Error(`git ${args.join(' ')}: ${r.stderr || r.stdout}`)
}
return r.stdout
}
const ARCHIVE_HOOK = `echo "[archive-hook] running in $PWD"
echo "$PWD" >> ${JSON.stringify(ranFile).slice(1, -1)}
mode=$(cat ${JSON.stringify(modeFile).slice(1, -1)})
case "$mode" in
ok) echo "[archive-hook] archived OK"; exit 0 ;;
fail) echo "[archive-hook] backup target unreachable" >&2; exit 23 ;;
signal) echo "[archive-hook] losing the execution host now"; kill -KILL $$ ;;
esac
echo "unknown mode $mode" >&2; exit 99
`
/** A throwaway git repo with one commit; optionally an orca.yaml archive hook. */
function seedGitRepo(label, withHook, githubSlug) {
const dir = mkdtempSync(join(tmpdir(), `agh-repo-${label}-`))
writeFileSync(join(dir, 'README.md'), `# ${label}\n`)
if (withHook) {
writeFileSync(
join(dir, 'orca.yaml'),
`scripts:\n archive: |\n${ARCHIVE_HOOK.split('\n')
.map((l) => ` ${l}`)
.join('\n')}\n`
)
}
git(dir, 'init', '-b', 'main')
git(dir, 'config', 'user.email', 'verify@orca.test')
git(dir, 'config', 'user.name', 'Archive Gate Verify')
if (githubSlug) {
git(dir, 'remote', 'add', 'origin', `https://github.com/agh-owner/${githubSlug}.git`)
}
git(dir, 'add', '-A')
git(dir, 'commit', '-m', 'seed')
return dir
}
function waitForReady(child) {
return new Promise((res, rej) => {
let buffered = ''
let serverErr = ''
const timer = setTimeout(
() => rej(new Error(`no ready payload in ${READY_TIMEOUT_MS}ms\n${serverErr}`)),
READY_TIMEOUT_MS
)
child.stderr.setEncoding('utf8')
child.stderr.on('data', (c) => {
serverErr += c
})
child.stdout.setEncoding('utf8')
child.stdout.on('data', (chunk) => {
buffered += chunk
for (const line of buffered.split('\n')) {
if (!line.startsWith('{')) {
continue
}
try {
const p = JSON.parse(line)
if (p.type === 'orca_server_ready') {
clearTimeout(timer)
res(p)
return
}
} catch {
/* partial */
}
}
})
child.on('exit', (code) => {
clearTimeout(timer)
rej(new Error(`server exited ${code} before ready:\n${serverErr}`))
})
})
}
/** Filesystem + git truth about a worktree, read directly rather than through Orca. */
function evidence(repoPath, wtPath) {
const ls = spawnSync('ls', ['-la', wtPath], { encoding: 'utf8' })
const list = spawnSync('git', ['worktree', 'list'], { cwd: repoPath, encoding: 'utf8' })
return {
dirExists: existsSync(wtPath),
fileExists: existsSync(join(wtPath, 'PRECIOUS.txt')),
fileBody: existsSync(join(wtPath, 'PRECIOUS.txt'))
? readFileSync(join(wtPath, 'PRECIOUS.txt'), 'utf8').trim()
: null,
registered: (list.stdout ?? '').includes(wtPath),
ls: (ls.stdout ?? '').trim(),
worktreeList: (list.stdout ?? '').trim()
}
}
function showEvidence(e) {
out(' --- ls -la <worktree> ---')
out(
e.ls
.split('\n')
.map((l) => ` ${l}`)
.join('\n')
)
out(' --- git worktree list (in the repo) ---')
out(
e.worktreeList
.split('\n')
.map((l) => ` ${l}`)
.join('\n')
)
}
async function main() {
const userDataDir = mkdtempSync(join(tmpdir(), 'agh-userdata-'))
out(`booting headless runtime on port ${PORT}, userData ${userDataDir}`)
const child = spawn(
'npx',
[
'electron',
serveEntry,
'--serve',
'--serve-port',
String(PORT),
'--serve-json',
`--user-data-dir=${userDataDir}`
],
{
cwd: projectDir,
stdio: ['ignore', 'pipe', 'pipe'],
env: { ...process.env, ORCA_BACKGROUND_LAUNCH: '1' }
}
)
const created = []
try {
const ready = await waitForReady(child)
pairingCode = new URL(ready.pairing.url).searchParams.get('code')
out(`ready: ${ready.advertisedEndpoint}`)
const hookRepoPath = seedGitRepo('hooked', true)
const folderProjectSlug = `agh-folder-proof-${randomBytes(3).toString('hex')}`
const bareRepoPath = seedGitRepo('nohook', false, folderProjectSlug)
const hookRepo = ok(['repo', 'add', '--path', hookRepoPath]).repo
const bareRepo = ok(['repo', 'add', '--path', bareRepoPath]).repo
out(`repo with archive hook: ${hookRepoPath} (${hookRepo.id})`)
out(`repo without archive hook: ${bareRepoPath} (${bareRepo.id})`)
const makeWorktree = (repo, repoPath, name) => {
const wt = ok([
'worktree',
'create',
'--repo',
`id:${repo.id}`,
'--name',
name,
'--setup',
'skip'
]).worktree
created.push(wt)
const unarchivedBody = `unarchived work for ${name}`
writeFileSync(join(wt.path, 'PRECIOUS.txt'), `${unarchivedBody}\n`)
return { ...wt, repoPath, unarchivedBody }
}
banner('SCENARIO 1 — archive hook exits 23: removal MUST be refused, nothing deleted')
setMode('fail')
const wt1 = makeWorktree(hookRepo, hookRepoPath, `gate-fail-${randomBytes(3).toString('hex')}`)
out(`worktree: ${wt1.path}`)
const before = evidence(wt1.repoPath, wt1.path)
out('\n$ orca worktree rm --worktree <id> --run-hooks (human output)')
const human = cli(['worktree', 'rm', '--worktree', wt1.id, '--run-hooks'], { json: false })
out(` exit code: ${human.status}`)
out(' --- stdout ---')
out(
(human.stdout ?? '')
.trimEnd()
.split('\n')
.map((l) => ` ${l}`)
.join('\n')
)
out(' --- stderr ---')
out(
(human.stderr ?? '')
.trimEnd()
.split('\n')
.map((l) => ` ${l}`)
.join('\n')
)
out(
'\n$ orca worktree rm --worktree <id> --force --run-hooks --json (--force must NOT waive)'
)
const forced = cli(['worktree', 'rm', '--worktree', wt1.id, '--force', '--run-hooks'])
const forcedJson = parseJsonLine(forced)
out(` exit code: ${forced.status}`)
out(` ${JSON.stringify(forcedJson)}`)
const after1 = evidence(wt1.repoPath, wt1.path)
showEvidence(after1)
check('CLI exits non-zero', human.status !== 0, `got ${human.status}`)
check(
'human stderr names the archive hook',
/Archive hook failed for worktree/.test(human.stderr ?? '')
)
check('--force also refused (non-zero)', forced.status !== 0, `got ${forced.status}`)
check(
'typed error code',
forcedJson?.error?.code === 'worktree_archive_hook_failed',
JSON.stringify(forcedJson?.error?.code)
)
check(
"error data outcome is 'exited'",
forcedJson?.error?.data?.outcome === 'exited',
JSON.stringify(forcedJson?.error?.data)
)
check('error data carries exitCode 23', forcedJson?.error?.data?.exitCode === 23)
check('checkout directory still exists', after1.dirExists)
// Check contents too: an empty surviving stub would not prove archive protection.
check(
'unarchived file PRECIOUS.txt survives with its contents',
after1.fileExists && after1.fileBody === wt1.unarchivedBody,
`exists=${after1.fileExists} body=${JSON.stringify(after1.fileBody)}`
)
check('git worktree registration survives', after1.registered)
check(
'nothing changed vs. before the attempt',
before.dirExists === after1.dirExists && before.registered === after1.registered
)
const shown = ok(['worktree', 'show', '--worktree', wt1.id]).worktree
check('Orca still resolves the worktree', shown?.id === wt1.id)
check(
'the hook really ran (twice: plain + --force)',
hookRuns().length >= 2,
`runs=${hookRuns().length}`
)
// Dirty checkout reports archive failure first, proving gate precedes dirty preflight.
check(
'archive gate precedes the dirty preflight (dirty checkout, archive error reported)',
/Archive hook failed/.test(human.stderr ?? '') &&
!/\?\? PRECIOUS\.txt/.test(human.stderr ?? '')
)
banner('SCENARIO 2 — --allow-failed-archive-hook: removal proceeds, waiver recorded')
// --force waives only dirty preflight; scenario 1 proves archive gate still requires its own waiver.
const waived = cli([
'worktree',
'rm',
'--worktree',
wt1.id,
'--force',
'--run-hooks',
'--allow-failed-archive-hook'
])
const waivedJson = parseJsonLine(waived)
out(` exit code: ${waived.status}`)
out(` ${JSON.stringify(waivedJson)}`)
const after2 = evidence(wt1.repoPath, wt1.path)
out(` checkout still on disk: ${after2.dirExists}`)
out(
` --- git worktree list ---\n${after2.worktreeList
.split('\n')
.map((l) => ` ${l}`)
.join('\n')}`
)
check('override exits zero', waived.status === 0, `got ${waived.status}`)
check('removal reported', waivedJson?.result?.removed === true)
check('checkout is GONE', !after2.dirExists)
check('git registration is gone', !after2.registered)
check(
'archiveHookOverride recorded',
waivedJson?.result?.archiveHookOverride?.overridden === true,
JSON.stringify(waivedJson?.result?.archiveHookOverride)
)
check(
'override records exit 23 / exited',
waivedJson?.result?.archiveHookOverride?.exitCode === 23 &&
waivedJson?.result?.archiveHookOverride?.outcome === 'exited'
)
banner('SCENARIO 3 — archive hook exits 0: removal proceeds')
setMode('ok')
const wt3 = makeWorktree(hookRepo, hookRepoPath, `gate-ok-${randomBytes(3).toString('hex')}`)
out(`worktree: ${wt3.path}`)
const okRm = cli(['worktree', 'rm', '--worktree', wt3.id, '--force', '--run-hooks'])
const okJson = parseJsonLine(okRm)
out(` exit code: ${okRm.status}`)
out(` ${JSON.stringify(okJson)}`)
const after3 = evidence(wt3.repoPath, wt3.path)
check('exits zero', okRm.status === 0)
check('checkout deleted', !after3.dirExists)
check('git registration gone', !after3.registered)
check(
'no archiveHookOverride on a clean run',
okJson?.result?.archiveHookOverride === undefined
)
banner('SCENARIO 4 — no archive hook configured: removal proceeds unchanged')
const wt4 = makeWorktree(
bareRepo,
bareRepoPath,
`gate-nohook-${randomBytes(3).toString('hex')}`
)
out(`worktree: ${wt4.path}`)
const runsBefore = hookRuns().length
const noHook = cli(['worktree', 'rm', '--worktree', wt4.id, '--force', '--run-hooks'])
const noHookJson = parseJsonLine(noHook)
out(` exit code: ${noHook.status}`)
out(` ${JSON.stringify(noHookJson)}`)
const after4 = evidence(wt4.repoPath, wt4.path)
check('exits zero', noHook.status === 0)
check('checkout deleted', !after4.dirExists)
check('no hook was run', hookRuns().length === runsBefore)
banner('SCENARIO 5 — hook never reports an exit (killed): must BLOCK as `unverifiable`')
setMode('signal')
const wt5 = makeWorktree(hookRepo, hookRepoPath, `gate-unver-${randomBytes(3).toString('hex')}`)
out(`worktree: ${wt5.path}`)
const unver = cli(['worktree', 'rm', '--worktree', wt5.id, '--run-hooks'])
const unverJson = parseJsonLine(unver)
out(` exit code: ${unver.status}`)
out(` ${JSON.stringify(unverJson)}`)
const after5 = evidence(wt5.repoPath, wt5.path)
showEvidence(after5)
check('blocked (non-zero)', unver.status !== 0, `got ${unver.status}`)
check('typed error code', unverJson?.error?.code === 'worktree_archive_hook_failed')
check(
"outcome is 'unverifiable', NOT 'exited'",
unverJson?.error?.data?.outcome === 'unverifiable',
JSON.stringify(unverJson?.error?.data)
)
check(
'exit code is WITHHELD (never read as a pass)',
unverJson?.error?.data?.exitCode === undefined
)
check('checkout survives', after5.dirExists && after5.fileExists)
check('git registration survives', after5.registered)
setMode('ok')
cli(['worktree', 'rm', '--worktree', wt5.id, '--force'])
banner('SCENARIO 6 — folder workspace removal (the boundary that runs no hook) is unchanged')
const folderDir = mkdtempSync(join(tmpdir(), 'agh-folder-'))
mkdirSync(join(folderDir, 'src'))
writeFileSync(join(folderDir, 'src', 'app.txt'), 'folder workspace content\n')
// Folder imports use existing project identity; anchor to the hookless repo’s GitHub project.
const folderProjectId = `github:agh-owner/${folderProjectSlug}`
ok([
'project',
'setup-existing-folder',
'--project',
folderProjectId,
'--host',
'local',
'--path',
folderDir,
'--kind',
'folder'
])
const folderRepo = (ok(['repo', 'list']).repos ?? []).find((r) => r.path === folderDir) ?? null
out(`folder repo: ${folderDir} (${folderRepo?.id}) kind=${folderRepo?.kind}`)
check('registered repo kind is folder', folderRepo?.kind === 'folder', String(folderRepo?.kind))
// Folder project root remains undeletable; test removal on its child workspace.
const folderRoot = ok(['worktree', 'show', '--worktree', `path:${folderDir}`]).worktree
const rootRm = cli(['worktree', 'rm', '--worktree', folderRoot.id, '--force', '--run-hooks'])
out(
` root refusal (unchanged): exit ${rootRm.status} ${parseJsonLine(rootRm)?.error?.code} -- ${parseJsonLine(rootRm)?.error?.message}`
)
check(
'folder project root still refuses for its own reason, not the archive gate',
rootRm.status !== 0 && parseJsonLine(rootRm)?.error?.code !== 'worktree_archive_hook_failed'
)
const folderChild = ok([
'worktree',
'create',
'--repo',
`id:${folderRepo.id}`,
'--name',
'agh-folder-child',
'--setup',
'skip'
]).worktree
out(`folder workspace: ${folderChild.id}`)
const runsBeforeFolder = hookRuns().length
out(`\n$ orca worktree rm --worktree <folder workspace> --force --run-hooks`)
const folderRm = cli(['worktree', 'rm', '--worktree', folderChild.id, '--force', '--run-hooks'])
const folderJson = parseJsonLine(folderRm)
out(` exit code: ${folderRm.status}`)
out(` ${JSON.stringify(folderJson)}`)
const stillThere = cli(['worktree', 'show', '--worktree', folderChild.id])
check(
'folder workspace removal exits zero',
folderRm.status === 0,
`${folderRm.status} ${folderRm.stderr}`
)
check('folder removal ran no archive hook', hookRuns().length === runsBeforeFolder)
check(
'folder contents left on disk (forget, not delete)',
existsSync(join(folderDir, 'src', 'app.txt'))
)
check(
'folder workspace is deregistered',
stillThere.status !== 0 && parseJsonLine(stillThere)?.error?.code === 'selector_not_found'
)
rmSync(folderDir, { recursive: true, force: true })
banner(failures === 0 ? 'ALL CHECKS PASSED' : `${failures} CHECK(S) FAILED`)
} catch (error) {
out(`\nHARNESS ERROR: ${error instanceof Error ? error.stack : String(error)}`)
failures++
} finally {
for (const wt of created) {
if (existsSync(wt.path)) {
cli(['worktree', 'rm', '--worktree', wt.id, '--force'])
rmSync(dirname(wt.path), { recursive: true, force: true })
}
}
if (child.exitCode === null && child.signalCode === null) {
child.kill('SIGTERM')
await Promise.race([
new Promise((r) => child.on('exit', r)),
new Promise((r) => setTimeout(r, 15_000))
])
child.kill('SIGKILL')
}
rmSync(userDataDir, { recursive: true, force: true })
}
process.exitCode = failures === 0 ? 0 : 1
}
setMode('ok')
main()