/**
* sigint-mid-install — kill `pgh app install` partway through.
*
* Asserts:
* - Process exits non-zero with a clear error message (not 0 silent).
* - No half-materialized app dir left behind (full presence or full absence).
* - Store may have stale partial trees, but refs.json is consistent
* (no app names point at hashes that aren't real dirs).
* - A subsequent install of the same app succeeds.
*/
import fs from 'fs'
import path from 'path'
import { publishFixture, pghUrl } from '../../bench/scenarios/_lib.js'
import { runPgh, dirSize } from '../../bench/runner.js'
import { CLI_PATH } from '../../bench/scenarios/_lib.js'
const fixture = process.env.FIXTURE || 'bench-medium'
export const name = 'sigint-mid-install/' + fixture
export const description = 'SIGINT pgh ~halfway through install; verify clean recovery'
export async function setup (ctx) {
const { lockfile } = await publishFixture(ctx, fixture)
ctx._lockfile = lockfile
ctx.emit('fixture', fixture)
}
export async function run (ctx) {
// Pick a kill delay that's likely to land mid-dep-loop. For
// bench-medium (50 deps) ~250 ms is roughly half way on a warm
// machine. Cold/slow machines may hit teardown — that's still a
// valid signal (we want clean recovery in both cases).
const killAfter = parseInt(process.env.KILL_AFTER_MS, 10) || 250
const url = pghUrl(ctx._lockfile)
const first = await runPgh(ctx, [
'app', 'install', url, '--name', fixture
], { cliPath: CLI_PATH, killAfterMs: killAfter, killSignal: 'SIGINT' })
ctx.emit('first_exit_code', first.code)
ctx.emit('first_duration_ms', first.durationMs)
if (first.code === 0) {
// The install completed before we could kill it — bump the fixture
// size or shorten killAfter. Not a chaos pass, mark as inconclusive.
ctx.emit('killed_in_time', false)
throw new Error('install completed before SIGINT (try larger fixture or smaller killAfter)')
}
ctx.emit('killed_in_time', true)
// Assert: no half-materialized install dir. Either it doesn't exist,
// or it exists fully (with a working launcher).
const appPath = path.join(ctx.appsDir, fixture)
const exists = fs.existsSync(appPath)
ctx.emit('app_dir_present_after_kill', exists)
if (exists) {
// If present, package.json + index.js MUST also be present.
const ok = fs.existsSync(path.join(appPath, 'package.json')) &&
fs.existsSync(path.join(appPath, 'index.js'))
ctx.emit('app_dir_consistent', ok)
if (!ok) {
throw new Error('partial install dir left behind after SIGINT — inconsistent state')
}
}
// Assert: refs.json doesn't reference hashes that don't exist.
const refsPath = path.join(ctx.storeRoot, 'refs.json')
const depsDir = path.join(ctx.storeRoot, 'dependencies')
if (fs.existsSync(refsPath)) {
const refs = JSON.parse(fs.readFileSync(refsPath, 'utf8'))
const orphans = []
for (const hash of Object.keys(refs)) {
if (!fs.existsSync(path.join(depsDir, hash))) orphans.push(hash)
}
ctx.emit('orphan_refs', orphans.length)
if (orphans.length > 0) {
throw new Error(`refs.json has ${orphans.length} entries pointing at non-existent tree dirs`)
}
}
// Recovery: install again, expect success.
const second = await runPgh(ctx, [
'app', 'install', url, '--name', fixture
], { cliPath: CLI_PATH })
ctx.emit('recovery_exit_code', second.code)
ctx.emit('recovery_duration_ms', second.durationMs)
if (second.code !== 0) {
throw new Error('recovery install failed:\n' + second.stderr.slice(-1000))
}
const sz = dirSize(path.join(ctx.appsDir, fixture))
ctx.emit('recovered_app_files', sz.files)
}