OverviewFilesCommitsIssuesDocsReleasesPeers

pgh / test / chaos / scenarios / sigint-mid-install.chaos.js

96 lines · javascript ref:
/**
 * 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)
}