OverviewFilesCommitsIssuesDocsReleasesPeers

pgh / test / chaos / scenarios / concurrent-same-app.chaos.js

77 lines · javascript ref:
/**
 * concurrent-same-app — two simultaneous `pgh app install <name>`.
 *
 * Asserts:
 *   - Both exit with deterministic codes (both 0, or one 0 + one EBUSY-like).
 *   - The install dir ends up consistent (matches the latest write).
 *   - No corrupted refs.json (every hash referenced still exists).
 *
 * This surfaces B3 from the audit (CLI ↔ autoupdate / CLI ↔ CLI
 * coordination). Today's sidecar has the _installInFlight mutex but
 * the CLIs talk to the sidecar separately; this test checks the
 * end-to-end behavior.
 */

import fs from 'fs'
import path from 'path'
import { publishFixture, pghUrl } from '../../bench/scenarios/_lib.js'
import { runPgh } from '../../bench/runner.js'
import { CLI_PATH } from '../../bench/scenarios/_lib.js'

const fixture = process.env.FIXTURE || 'bench-medium'

export const name = 'concurrent-same-app/' + fixture
export const description = 'two pgh installs of the same app fired simultaneously'

export async function setup (ctx) {
  const { lockfile } = await publishFixture(ctx, fixture)
  ctx._lockfile = lockfile
  ctx.emit('fixture', fixture)
}

export async function run (ctx) {
  // Fire both in parallel — no awaits between them.
  const url = pghUrl(ctx._lockfile)
  const [a, b] = await Promise.all([
    runPgh(ctx, ['app', 'install', url, '--name', fixture], { cliPath: CLI_PATH }),
    runPgh(ctx, ['app', 'install', url, '--name', fixture], { cliPath: CLI_PATH })
  ])

  ctx.emit('a_exit_code', a.code)
  ctx.emit('b_exit_code', b.code)
  ctx.emit('a_duration_ms', a.durationMs)
  ctx.emit('b_duration_ms', b.durationMs)

  // At least one must succeed.
  if (a.code !== 0 && b.code !== 0) {
    throw new Error('both concurrent installs failed:\n[A]\n' + a.stderr + '\n[B]\n' + b.stderr)
  }

  // The install dir must exist + be consistent.
  const appPath = path.join(ctx.appsDir, fixture)
  if (!fs.existsSync(appPath)) {
    throw new Error('no app dir after concurrent installs')
  }
  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('app dir is inconsistent (missing core files)')
  }

  // refs.json points at real trees.
  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 corrupted: ' + orphans.length + ' orphans')
    }
  }
}