mirror of
https://github.com/stablyai/orca.git
synced 2026-11-04 02:57:36 +00:00
* [comment audit] Align legacy failure-read counts with cached display * [comment audit] Condense shared provider lifecycle and browser recovery comments [skip ci] * [comment audit] Condense shared generation, path and preview comments [skip ci] * [comment audit] Condense host, file and Git contract rationale [skip ci] * [comment audit] Tighten Git lifecycle and provider contract comments [skip ci] * [comment audit] Clarify shared host authority and launch isolation [skip ci] * [comment audit] simplify local access mobile and native chat contracts [skip ci] * [comment audit] simplify native chat presentation and state contracts [skip ci] * [comment audit] simplify native runtime and provider contracts [skip ci] * [comment audit] simplify migration and orchestration contracts [skip ci] * [comment audit] simplify plugin trust and process capture rationale [skip ci] * [comment audit] simplify terminal delivery and remote compatibility rationale [skip ci] * [comment audit] clarify remote recovery and launch wire rationale [skip ci] * [comment audit] shorten shared RPC compatibility and runtime evidence comments [skip ci] * [comment audit] simplify secure storage shell startup and sidebar rationale [skip ci] * [comment audit] clarify sidebar, skill and SSH invariants [skip ci] * [comment audit] condense structured chat delivery and timing rationale [skip ci] * [comment audit] condense terminal replay and telemetry invariants [skip ci] * [comment audit] clarify launch permission and terminal delivery rationale [skip ci] * [comment audit] clarify workspace ownership and layout contracts [skip ci] * [comment audit] wait for fitted and painted SSH terminal links * [comment audit] clarify layout persistence and session ownership [skip ci] * [comment audit] clarify host routing and workspace lifecycle contracts [skip ci] * [comment audit] clarify startup persistence and app lifetime comments [skip ci] * [comment audit] clarify recovery and activity boundaries [skip ci] * [comment audit] condense activity and artifact invariants [skip ci] * [comment audit] condense automation authority contracts [skip ci] * [comment audit] simplify automation scheduling and history contracts [skip ci] * [comment audit] clarify browser chrome and annotation lifecycles [skip ci] * [comment audit] clarify browser routing and retained guest ownership [skip ci] * [comment audit] clarify browser recovery and document ownership comments [skip ci] * [comment audit] restore hosted runners required by native IME fixtures * [comment audit] shorten preview, palette and dashboard invariants [skip ci] * [comment audit] clarify terminal preview, dashboard and retention comments [skip ci] * [comment audit] clarify diff-note recovery and editor lifecycle comments [skip ci] * [comment audit] shorten rich-text and combined-diff ownership comments [skip ci] * [comment audit] shorten autosave, draft recovery and model ownership comments [skip ci] * [comment audit] shorten save-queue, notebook and Markdown constraints [skip ci] * [comment audit] shorten Monaco, PDF and rich Markdown safeguards [skip ci] * [comment audit] clarify rich Markdown source and selection safeguards [skip ci] * [comment audit] clarify editor loading and emulator stream lifetimes [skip ci] * [comment audit] trim emulator and feature tour narration [skip ci] * [comment audit] clarify floating workspace and GitHub cache ownership [skip ci] * [comment audit] trim provider controls and Linear filter explanations [skip ci] * [comment audit] clarify pairing and chat resume lifetimes [skip ci] * [comment audit] clarify chat delivery and transcript lifetimes [skip ci] * [comment audit] clarify session options and host-owned status [skip ci] * [comment audit] clarify draft durability and live-status precedence [skip ci] * [comment audit] Trim chat send and model ownership explanations [skip ci] * [comment audit] Shorten transcript ownership and send recovery comments [skip ci] * [comment audit] Simplify native chat composer and transcript hook comments [skip ci] * [comment audit] Shorten native chat scroll and structured session explanations [skip ci] * [comment audit] Trim workspace picker and onboarding commentary [skip ci] * [comment audit] Simplify pet rendering, review cache and quick open comments [skip ci] * [comment audit] Trim sidebar and session history explanations [skip ci] * [comment audit] Condense session resume and review checks comments [skip ci] * [comment audit] Shorten checks state and file explorer refresh comments [skip ci] * [comment audit] Trim plugin bridge and source-control action rationale [skip ci] * [comment audit] Trim source-control listing and review rationale [skip ci] * [comment audit] Shorten sidebar lifecycle and explorer safeguards [skip ci] * [comment audit] Condense Settings account and appearance rationale [skip ci] * [comment audit] Trim pairing and repository Settings explanations [skip ci] * [comment audit] Condense Settings search SSH and terminal safeguards [skip ci] * [comment audit] Shorten Settings voice and provider ownership comments [skip ci] * [comment audit] Condense Settings draft persistence and host-state rationale [skip ci] * [comment audit] Trim Settings hooks and sidebar setup rationale [skip ci] * [comment audit] Condense sidebar deletion and board-filter safeguards [skip ci] * [comment audit] Condense worktree cards, host ownership and Markdown contracts [skip ci] * [comment audit] Condense sidebar drag, folder and filtering contracts [skip ci] * [comment audit] Condense sidebar parity, sleep and host ownership contracts [skip ci] * [comment audit] Prune focus-read baseline after comment cleanup * [comment audit] Clarify sidebar visibility, board drops and agent evidence [skip ci] * [comment audit] Tighten sidebar listing, drag and reveal contracts [skip ci] * [comment audit] Clarify viewport geometry and skill update safeguards [skip ci] * [comment audit] Tighten skill inventory, usage and host status comments [skip ci] * [comment audit] Clarify usage ownership and resource kill safeguards [skip ci] * [comment audit] Tighten tab selection, rename and search contracts [skip ci] * [comment audit] Tighten tab-strip geometry and drag ownership comments [skip ci] * [comment audit] Tighten Tasks recovery, pagination and mutation comments [skip ci] * [comment audit] Tighten provider loading and terminal recovery comments [skip ci] * [comment audit] Tighten completion, cadence and PTY ownership comments [skip ci] * [comment audit] Clarify terminal restore and parked-pane boundaries [skip ci] * [comment audit] Tighten PTY startup and replay explanations [skip ci] * [comment audit] Clarify PTY identity and delivery ownership [skip ci] * [comment audit] Shorten remote reconnect and input replay rationale [skip ci] * [comment audit] Condense terminal parking clipboard and file link comments [skip ci] * [comment audit] Clarify IME ownership and trim trace history [skip ci] * [comment audit] Shorten pane recovery and parked watcher explanations [skip ci] * [comment audit] Condense snapshot replay and visibility recovery rationale [skip ci] * [comment audit] Shorten terminal parking and keyboard ownership rationale [skip ci] * [comment audit] Trim renderer lifecycle and menu rationale [skip ci] * [comment audit] Condense workspace cleanup ownership and settlement notes [skip ci] * [comment audit] Clarify renderer hook evidence and source ownership [skip ci] * [comment audit] Tighten IPC ownership and reconnect explanations [skip ci] * [comment audit] Trim renderer focus, paste and attention comments [skip ci] * [comment audit] Shorten scroll, locale and agent lifecycle comments [skip ci] * [comment audit] Trim agent launch, resume and ownership commentary [skip ci] * [comment audit] Shorten account, browser and editor comments [skip ci] * [comment audit] Clarify host-mirror waits, previews and input ownership [skip ci] * [comment audit] Condense launch recovery and editor tokenization comments [skip ci] * [comment audit] Condense workspace selection and terminal pane rationale [skip ci] * [comment audit] Condense terminal fitting and rendering recovery comments [skip ci] * [comment audit] Condense terminal input and host routing explanations [skip ci] * [comment audit] Condense ownership and sleeping-session recovery rationale [skip ci] * [comment audit] Condense structured launch and terminal link explanations [skip ci] * [comment audit] Condense terminal ownership and workspace file routing rationale [skip ci] * [comment audit] Condense session persistence and workspace activation rationale [skip ci] * [comment audit] Condense workspace routing and browser ownership rationale [skip ci] * [comment audit] Condense mirror recovery and layout authority rationale [skip ci] * [comment audit] Condense runtime transport and file safety rationale [skip ci] * [comment audit] Condense runtime graph and structured session rationale [skip ci] * [comment audit] Condense browser creation and remote tab recovery rationale [skip ci] * [comment audit] Condense snapshot authority and publisher recovery rationale [skip ci] * [comment audit] Condense frame, startup and provider cache rationale [skip ci] * [comment audit] Condense host twin and agent lifecycle rationale [skip ci] * [comment audit] Condense browser authority and editor cleanup rationale [skip ci] * [comment audit] Condense editor buffer identity and review cache rationale [skip ci] * [comment audit] Condense provider lifetime and repository ownership rationale [skip ci] * [comment audit] Condense reconnect, hydration and terminal retirement rationale [skip ci] * [comment audit] Condense recovery, reconciliation and persistence rationale [skip ci] * [comment audit] Condense cleanup ownership and listing admission rationale [skip ci] * [comment audit] Condense teardown ordering and terminal ownership rationale [skip ci] * [comment audit] Condense recovery ledger and web host-boundary rationale [skip ci] * [comment audit] Condense web reconnect and credential lifecycle rationale [skip ci] * [comment audit] Condense cloud rollout and assignment safety rationale [skip ci] * [comment audit] Condense relay session and database recovery rationale [skip ci] * [comment audit] Condense relay placement and protocol compatibility rationale [skip ci] * [comment audit] Condense build and packaging constraints [skip ci] * [comment audit] Condense CI gates and validation isolation rationale [skip ci] * [comment audit] Condense catalog identity and browser validation rationale [skip ci] * [comment audit] Condense mobile browser and frame-budget rationale [skip ci] * [comment audit] Shorten mobile route and browser test explanations [skip ci] * [comment audit] Condense editor and session seam test rationale [skip ci] * [comment audit] Trim session and terminal closure history [skip ci] * [comment audit] Shorten terminal browser and census explanations [skip ci] * [comment audit] Preserve root-only E2E diagnostic baseline * [comment audit] Condense mobile seam and HTML preview test rationale [skip ci] * [comment audit] Condense preview instrumentation and render harness rationale [skip ci] * [comment audit] Condense bundle contracts and native-build constraints [skip ci] * [comment audit] Shorten native packaging and CI routing rationale [skip ci] * [comment audit] Condense release tooling and benchmark constraints [skip ci] * [comment audit] Condense packaging and platform gate rationale [skip ci] * [comment audit] Shorten mobile route and history rationale [skip ci] * [comment audit] Condense mobile browser and component rationale [skip ci] * [comment audit] Shorten drawer and workspace-form rationale [skip ci] * [comment audit] Trim mobile review and editor-document explanations [skip ci] * [comment audit] Condense editor lifecycle and Markdown explanations [skip ci] * [comment audit] Trim mobile file, diagnostics and dictation commentary [skip ci] * [comment audit] Simplify mobile host-list and workspace-view explanations [skip ci] * [comment audit] Clarify mobile shell lifetime and bridge delivery constraints [skip ci] * [comment audit] Condense bridge stream, native request and audio rationale [skip ci] * [comment audit] Reduce bridge schema and page-client commentary [skip ci] * [comment audit] Tighten mobile page bridge contracts [skip ci] * [comment audit] Clarify mobile cache and session constraints [skip ci] * [comment audit] Condense mobile route and lifecycle rationale [skip ci] * [comment audit] Tighten mobile shell hook timing rationale [skip ci] * [comment audit] Clarify route handoff and notification compatibility [skip ci] * [comment audit] Clarify push registration and dictation cleanup constraints [skip ci] * [comment audit] Condense keyboard geometry and native media ownership rationale [skip ci] * [comment audit] Tighten mobile chat scroll and composer concurrency rationale [skip ci] * [comment audit] Condense mobile reply compatibility and upload ownership rationale [skip ci] * [comment audit] Condense optimistic echo and scroll ownership rationale [skip ci] * [comment audit] Tighten stream replay and queued session ownership rationale [skip ci] * [comment audit] Condense mobile composer and reply-schema constraints [skip ci] * [comment audit] Clarify mobile transcript recovery and session ownership [skip ci] * [comment audit] Clarify mobile queue and source-control compatibility [skip ci] * [comment audit] Condense storage and task launch constraints [skip ci] * [comment audit] Clarify task schemas and terminal document ownership [skip ci] * [comment audit] Tighten terminal input and viewport rationale [skip ci] * [comment audit] Shorten terminal and RPC recorder contracts [skip ci] * [comment audit] Condense recorder engine and connection contracts [skip ci] * [comment audit] Shorten host credential and relay recovery comments [skip ci] * [comment audit] Tighten pairing recovery and bundle transport rationale [skip ci] * [comment audit] Clarify RPC delivery and workspace catalog boundaries [skip ci] * [comment audit] Condense catalog and relay entry contracts [skip ci] * [comment audit] Shorten shutdown and SSH relay delivery contracts [skip ci] * [comment audit] Condense WSL relay and E2E regression rationale [skip ci] * [comment audit] Shorten cross-version harness and renderer fixture explanations [skip ci] * [comment audit] Condense paired-host and E2E lifecycle rationale [skip ci] * [comment audit] Shorten layout oracle and regression explanations [skip ci] * [comment audit] Condense paired recovery and ownership regression contracts [skip ci] * [comment audit] Shorten restart, sidebar and SSH lifecycle contracts [skip ci] * [comment audit] Condense terminal rendering and IME regression rationale [skip ci] * [comment audit] Shorten terminal recovery and benchmark contracts [skip ci] * [comment audit] Condense tooling and Windows lifecycle proof rationale [skip ci] * [comment audit] Condense workflow routing and release rationale [skip ci] * [comment audit] Condense mobile security and installer contracts [skip ci] * [comment audit] Shorten native rationale and stylesheet workarounds [skip ci] * [comment audit] Shorten Windows visibility probe rationale [skip ci] * [comment audit] Condense embedded release and signing rationale [skip ci] * [comment audit] Retain current main test behavior in final reconciliation [skip ci] * [comment audit] Complete final comment review across current source inventory * [comment audit] Handle consolidated audit reports before final CI * [comment audit] Preserve credential storage census detail [skip ci] * [comment audit] Scale broad changed E2E selections with verified timing shards * [comment audit] Align E2E assertions with browser close and settled session metadata * [comment audit] Match existing E2Es to current resource and windowing contracts * [comment audit] Observe the shared server outage notice in reconnect coverage * [comment audit] Make existing renderer probes measure settled tab and PTY work * [comment audit] Align palette E2E selector with task search * [comment audit] Refresh remaining palette search selectors * [comment audit] Keep selector correction within selected audit specs * [comment audit] Await emoji fixture settings inside renderer evaluation * [comment audit] Cancel fixture fit retries between terminal tests --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
502 lines
18 KiB
JavaScript
502 lines
18 KiB
JavaScript
// 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()
|