OverviewFilesCommitsIssuesDocsReleasesPeers

P2P-Hiverelay / test / unit / fleet-channel-promotion.test.js

680 lines · javascript ref:
import test from 'brittle'
import { spawnSync } from 'node:child_process'
import { createHash } from 'node:crypto'
import {
  chmodSync,
  mkdirSync,
  mkdtempSync,
  readFileSync,
  readdirSync,
  rmSync,
  statSync,
  writeFileSync
} from 'node:fs'
import { tmpdir } from 'node:os'
import path from 'node:path'
import { encodeHiveAppKey } from '../../packages/core/gateway/hive-host.js'
import { createPublicT1OpsFixture } from '../fixtures/public-hive-gateway-ops.js'

const tool = path.resolve('scripts/promote-fleet-channel.mjs')
const source = readFileSync(tool, 'utf8')
const canRunFunctional = have('git') && have('ssh-keygen')

test('fleet promotion tool exposes no push or deploy mutation path', (t) => {
  t.ok(source.includes("channel !== 'canary' && channel !== 'stable'"))
  t.ok(source.includes('writeChannelsAtomic(channelsPath'))
  t.absent(source.includes("'push'"))
  t.absent(source.includes("'commit'"))
  t.absent(source.includes('systemctl'))
})

test('fleet promotion dry-run validates a signed tag without filesystem side effects', { skip: !canRunFunctional }, (t) => {
  const fixture = createFixture(t)
  const before = readFileSync(fixture.channelsPath)
  const result = promote(fixture, 'canary', fixture.target, ['--dry-run'])

  t.is(result.status, 0, result.stderr)
  const report = JSON.parse(result.stdout)
  t.is(report.status, 'dry-run')
  t.is(report.channel, 'canary')
  t.is(report.target, fixture.target)
  t.is(report.targetSha, fixture.targetSha)
  t.is(report.wouldChange, true)
  t.ok(readFileSync(fixture.channelsPath).equals(before), 'dry-run does not rewrite channels.json')
  t.absent(readdirSync(path.dirname(fixture.channelsPath)).some(name => /\.(?:lock|tmp-)/.test(name)),
    'dry-run creates no lock or temporary file')
})

test('canary promotion atomically updates only canary and preserves unrelated data', { skip: !canRunFunctional }, (t) => {
  const fixture = createFixture(t)
  const beforeHead = git(fixture.repo, ['rev-parse', 'HEAD']).stdout.trim()
  chmodSync(fixture.channelsPath, 0o640)

  const result = promote(fixture, 'canary', fixture.target)
  t.is(result.status, 0, result.stderr)
  const report = JSON.parse(result.stdout)
  const channels = readJson(fixture.channelsPath)

  t.is(report.status, 'updated')
  t.is(channels.canary, fixture.target)
  t.is(channels.stable, 'v1.0.0', 'stable is never changed implicitly')
  t.alike(channels.routing, { wave: ['one', 'two'], hold: true })
  t.is(channels._doc, 'fixture control plane')
  t.is(statSync(fixture.channelsPath).mode & 0o777, 0o640, 'atomic replacement preserves file mode')
  t.is(git(fixture.repo, ['rev-parse', 'HEAD']).stdout.trim(), beforeHead, 'tool does not create a commit')
  t.absent(readdirSync(path.dirname(fixture.channelsPath)).some(name => /\.(?:lock|tmp-)/.test(name)),
    'successful promotion cleans atomic-write artifacts')
})

test('enabled tagged gateway manifest automatically gates canary promotion and current inventory mapping', { skip: !canRunFunctional }, (t) => {
  const fixture = createFixture(t, {}, { gatewayEnabled: true, publicT1: true })
  const result = promote(fixture, 'canary', fixture.target)
  t.is(result.status, 0, result.stderr)
  t.is(JSON.parse(result.stdout).publicGatewayRequired, true,
    'the signed manifest forces the gate without an operator flag')

  const drifted = createFixture(t, {}, { gatewayEnabled: true, publicT1: true })
  const before = readFileSync(drifted.channelsPath)
  const inventory = readJson(drifted.relaysPath)
  inventory.relays[0].channel = 'stable'
  writeFileSync(drifted.relaysPath, JSON.stringify(inventory, null, 2) + '\n')
  const rejected = promote(drifted, 'canary', drifted.target)
  t.not(rejected.status, 0)
  t.ok(rejected.stderr.includes('current full fleet inventory does not contain signed gateway cohort relay'))
  t.ok(readFileSync(drifted.channelsPath).equals(before), 'inventory drift cannot move canary')
})

test('public-t1 gateway promotion derives the canonical tagged operator-contract binding', { skip: !canRunFunctional }, (t) => {
  const fixture = createFixture(t, {}, { gatewayEnabled: true, publicT1: true })
  const result = promote(fixture, 'canary', fixture.target, ['--dry-run'])
  t.is(result.status, 0, result.stderr)
  t.alike(JSON.parse(result.stdout).operatorContracts, [fixture.operatorBinding])

  const missing = createFixture(t, {}, {
    gatewayEnabled: true,
    publicT1: true,
    omitOperatorContract: true
  })
  const missingResult = promote(missing, 'canary', missing.target, ['--dry-run'])
  t.not(missingResult.status, 0)
  t.ok(missingResult.stderr.includes('operator contract for canary-1') &&
    missingResult.stderr.includes('missing from the verified release commit'))

  const drifted = createFixture(t, {}, {
    gatewayEnabled: true,
    publicT1: true,
    driftOperatorContract: true
  })
  const driftedResult = promote(drifted, 'canary', drifted.target, ['--dry-run'])
  t.not(driftedResult.status, 0)
  t.ok(driftedResult.stderr.includes('digest does not match'))

  const legacy = createFixture(t, {}, { gatewayEnabled: true })
  const legacyResult = promote(legacy, 'canary', legacy.target, ['--dry-run'])
  t.not(legacyResult.status, 0)
  t.ok(legacyResult.stderr.includes('must use public-t1-gateway with a canonical operator contract digest'))
})

test('missing or explicitly disabled tagged gateway manifests preserve legacy canary promotion', { skip: !canRunFunctional }, (t) => {
  for (const options of [{}, { omitGatewayManifest: true }]) {
    const fixture = createFixture(t, {}, options)
    const result = promote(fixture, 'canary', fixture.target, ['--dry-run'])
    t.is(result.status, 0, result.stderr)
    t.is(JSON.parse(result.stdout).publicGatewayRequired, false)
  }
})

test('invalid enabled tagged gateway manifest cannot opt out of canary validation', { skip: !canRunFunctional }, (t) => {
  const fixture = createFixture(t, {}, { gatewayEnabled: true, publicT1: true, gatewayCohortChannel: 'stable' })
  const before = readFileSync(fixture.channelsPath)
  const result = promote(fixture, 'canary', fixture.target)
  t.not(result.status, 0)
  t.ok(result.stderr.includes('must include at least one canary relay'))
  t.ok(readFileSync(fixture.channelsPath).equals(before))
})

test('stable promotion fails closed without exact verified canary evidence', { skip: !canRunFunctional }, (t) => {
  const fixture = createFixture(t, { canary: 'v1.2.3' })
  const before = readFileSync(fixture.channelsPath)

  const missing = promote(fixture, 'stable', fixture.target)
  t.not(missing.status, 0)
  t.ok(missing.stderr.includes('--canary-evidence is required'))
  t.ok(readFileSync(fixture.channelsPath).equals(before))

  const evidencePath = path.join(fixture.work, 'canary-evidence.json')
  const failed = rolloutEvidence(fixture, { status: 'failed' })
  writeFileSync(evidencePath, JSON.stringify(failed))
  const failedResult = promote(fixture, 'stable', fixture.target, ['--canary-evidence', evidencePath])
  t.not(failedResult.status, 0)
  t.ok(failedResult.stderr.includes('verified pass status'))
  t.ok(readFileSync(fixture.channelsPath).equals(before))

  const wrongTarget = rolloutEvidence(fixture, {
    target: { tag: 'v1.2.4', version: '1.2.4', sha: fixture.targetSha, channel: 'canary' }
  })
  writeFileSync(evidencePath, JSON.stringify(wrongTarget))
  const wrongResult = promote(fixture, 'stable', fixture.target, ['--canary-evidence', evidencePath])
  t.not(wrongResult.status, 0)
  t.ok(wrongResult.stderr.includes(`must target ${fixture.target}`))
  t.ok(readFileSync(fixture.channelsPath).equals(before), 'wrong-version evidence cannot mutate stable')
})

test('stable promotion accepts current all-green canary evidence for the same signed commit', { skip: !canRunFunctional }, (t) => {
  const fixture = createFixture(t, { canary: 'v1.2.3' })
  const evidencePath = path.join(fixture.work, 'canary-evidence.json')
  writeFileSync(evidencePath, JSON.stringify(rolloutEvidence(fixture), null, 2) + '\n')

  const result = promote(fixture, 'stable', fixture.target, ['--canary-evidence', evidencePath])
  t.is(result.status, 0, result.stderr)
  const report = JSON.parse(result.stdout)
  const channels = readJson(fixture.channelsPath)

  t.is(report.status, 'updated')
  t.is(report.canaryEvidence.status, 'verified')
  t.is(report.canaryEvidence.relayCount, 1)
  t.is(channels.stable, fixture.target)
  t.is(channels.canary, fixture.target)
  t.alike(channels.routing, { wave: ['one', 'two'], hold: true })
})

test('enabled tagged gateway manifest automatically requires all-green stable evidence', { skip: !canRunFunctional }, (t) => {
  const fixture = createFixture(t, { canary: 'v1.2.3' }, { gatewayEnabled: true, publicT1: true })
  const evidencePath = path.join(fixture.work, 'canary-evidence.json')
  const statePath = path.join(fixture.work, 'gateway-window-state.json')
  const requiredArgs = [
    '--canary-evidence', evidencePath,
    '--gateway-window-state', statePath,
    '--relays', fixture.relaysPath
  ]

  writeFileSync(evidencePath, JSON.stringify(rolloutEvidence(fixture)))
  const missingState = promote(fixture, 'stable', fixture.target, ['--canary-evidence', evidencePath])
  t.not(missingState.status, 0)
  t.ok(missingState.stderr.includes('--gateway-window-state is required'))

  gatewayRolloutEvidence(fixture, statePath)
  writeFileSync(evidencePath, JSON.stringify(rolloutEvidence(fixture)))
  const legacy = promote(fixture, 'stable', fixture.target, requiredArgs)
  t.not(legacy.status, 0)
  t.ok(legacy.stderr.includes('gateway evidence gate was enabled'))

  const red = gatewayRolloutEvidence(fixture, statePath)
  red.summary.gatewayHealthy = 0
  red.relays[0].gatewayHealthy = false
  writeFileSync(evidencePath, JSON.stringify(red))
  const redResult = promote(fixture, 'stable', fixture.target, requiredArgs)
  t.not(redResult.status, 0)
  t.ok(redResult.stderr.includes('gatewayHealthy is not fully green'))

  writeFileSync(evidencePath, JSON.stringify(gatewayRolloutEvidence(fixture, statePath)))
  const green = promote(fixture, 'stable', fixture.target, requiredArgs)
  t.is(green.status, 0, green.stderr)
  const report = JSON.parse(green.stdout)
  t.is(report.publicGatewayRequired, true)
  t.is(report.canaryEvidence.publicGateway.status, 'verified')
  t.is(report.canaryEvidence.publicGateway.relayCount, 1)
  t.is(report.canaryEvidence.publicGateway.manifestSha256, fixture.gatewayManifestSha256)
  t.is(report.canaryEvidence.publicGateway.window.complete, true)
  t.is(readJson(fixture.channelsPath).stable, fixture.target)
})

test('stable promotion rejects stale channel snapshots and red relay evidence', { skip: !canRunFunctional }, (t) => {
  const fixture = createFixture(t, { canary: 'v1.2.3' })
  const evidencePath = path.join(fixture.work, 'canary-evidence.json')
  const before = readFileSync(fixture.channelsPath)

  writeFileSync(evidencePath, JSON.stringify(rolloutEvidence(fixture, {
    channelConfig: { sha256: '0'.repeat(64), targets: { canary: fixture.target } }
  })))
  const stale = promote(fixture, 'stable', fixture.target, ['--canary-evidence', evidencePath])
  t.not(stale.status, 0)
  t.ok(stale.stderr.includes('current channel configuration'))

  const red = rolloutEvidence(fixture)
  red.summary.healthy = 0
  red.relays[0].healthy = false
  writeFileSync(evidencePath, JSON.stringify(red))
  const redResult = promote(fixture, 'stable', fixture.target, ['--canary-evidence', evidencePath])
  t.not(redResult.status, 0)
  t.ok(redResult.stderr.includes('summary healthy is not fully green'))
  t.ok(readFileSync(fixture.channelsPath).equals(before))
})

test('stable promotion requires fresh generatedAt and relay observedAt timestamps', { skip: !canRunFunctional }, (t) => {
  const fixture = createFixture(t, { canary: 'v1.2.3' })
  const evidencePath = path.join(fixture.work, 'canary-evidence.json')
  const args = ['--canary-evidence', evidencePath]
  const before = readFileSync(fixture.channelsPath)

  const staleGenerated = rolloutEvidence(fixture)
  staleGenerated.generatedAt = new Date(Date.now() - 31 * 60 * 1000).toISOString()
  writeFileSync(evidencePath, JSON.stringify(staleGenerated))
  const staleGeneratedResult = promote(fixture, 'stable', fixture.target, args)
  t.not(staleGeneratedResult.status, 0)
  t.ok(staleGeneratedResult.stderr.includes('generatedAt is older than 30 minutes'))

  const staleRelay = rolloutEvidence(fixture)
  staleRelay.relays[0].observedAt = new Date(Date.now() - 31 * 60 * 1000).toISOString()
  writeFileSync(evidencePath, JSON.stringify(staleRelay))
  const staleRelayResult = promote(fixture, 'stable', fixture.target, args)
  t.not(staleRelayResult.status, 0)
  t.ok(staleRelayResult.stderr.includes('observedAt is older than 30 minutes'))

  const future = rolloutEvidence(fixture)
  future.generatedAt = new Date(Date.now() + 6 * 60 * 1000).toISOString()
  writeFileSync(evidencePath, JSON.stringify(future))
  const futureResult = promote(fixture, 'stable', fixture.target, args)
  t.not(futureResult.status, 0)
  t.ok(futureResult.stderr.includes('more than five minutes in the future'))
  t.ok(readFileSync(fixture.channelsPath).equals(before), 'freshness failures never mutate stable')
})

test('public gateway stable promotion rejects manifest, identity, and continuity drift', { skip: !canRunFunctional }, (t) => {
  const fixture = createFixture(t, { canary: 'v1.2.3' }, { gatewayEnabled: true, publicT1: true })
  const evidencePath = path.join(fixture.work, 'canary-evidence.json')
  const statePath = path.join(fixture.work, 'gateway-window-state.json')
  const args = [
    '--canary-evidence', evidencePath,
    '--gateway-window-state', statePath,
    '--relays', fixture.relaysPath
  ]

  const manifestDrift = gatewayRolloutEvidence(fixture, statePath)
  manifestDrift.publicGateway.manifest.sha256 = '0'.repeat(64)
  writeFileSync(evidencePath, JSON.stringify(manifestDrift))
  const manifestResult = promote(fixture, 'stable', fixture.target, args)
  t.not(manifestResult.status, 0)
  t.ok(manifestResult.stderr.includes('manifest binding is stale or drifted'))

  const identityDrift = gatewayRolloutEvidence(fixture, statePath)
  identityDrift.relays[0].gateway.origin = 'https://drift.example'
  writeFileSync(evidencePath, JSON.stringify(identityDrift))
  const identityResult = promote(fixture, 'stable', fixture.target, args)
  t.not(identityResult.status, 0)
  t.ok(identityResult.stderr.includes('origin drifted from the signed manifest'))

  const incomplete = gatewayRolloutEvidence(fixture, statePath)
  const state = readJson(statePath)
  state.relays[0].samples = [state.relays[0].samples.at(-1)]
  writeFileSync(statePath, JSON.stringify(state, null, 2) + '\n')
  incomplete.publicGateway.windowStateSha256 = sha256(readFileSync(statePath))
  writeFileSync(evidencePath, JSON.stringify(incomplete))
  const incompleteResult = promote(fixture, 'stable', fixture.target, args)
  t.not(incompleteResult.status, 0)
  t.ok(incompleteResult.stderr.includes('requires multiple bounded samples'))
})

test('public gateway stable promotion requires controller-collected continuity', { skip: !canRunFunctional }, (t) => {
  const fixture = createFixture(t, { canary: 'v1.2.3' }, { gatewayEnabled: true, publicT1: true })
  const evidencePath = path.join(fixture.work, 'canary-evidence.json')
  const statePath = path.join(fixture.work, 'gateway-window-state.json')
  const args = [
    '--canary-evidence', evidencePath,
    '--gateway-window-state', statePath,
    '--relays', fixture.relaysPath
  ]

  const missingCollected = gatewayRolloutEvidence(fixture, statePath)
  const missingState = readJson(statePath)
  delete missingState.relays[0].samples[0].collectedAt
  writeFileSync(statePath, JSON.stringify(missingState, null, 2) + '\n')
  missingCollected.publicGateway.windowStateSha256 = sha256(readFileSync(statePath))
  writeFileSync(evidencePath, JSON.stringify(missingCollected))
  const missing = promote(fixture, 'stable', fixture.target, args)
  t.not(missing.status, 0)
  t.ok(missing.stderr.includes('sample collectedAt must be a canonical ISO timestamp'))

  const compressed = gatewayRolloutEvidence(fixture, statePath)
  const compressedState = readJson(statePath)
  const compressedStart = Date.now() - 10 * 60 * 1000
  const compressedStep = Math.floor((10 * 60 * 1000) / (compressedState.relays[0].samples.length - 1))
  compressedState.relays[0].samples.forEach((sample, index) => {
    const timestamp = new Date(compressedStart + index * compressedStep).toISOString()
    sample.observedAt = timestamp
    sample.collectedAt = timestamp
  })
  writeFileSync(statePath, JSON.stringify(compressedState, null, 2) + '\n')
  compressed.publicGateway.windowStateSha256 = sha256(readFileSync(statePath))
  writeFileSync(evidencePath, JSON.stringify(compressed))
  const shortControllerWindow = promote(fixture, 'stable', fixture.target, args)
  t.not(shortControllerWindow.status, 0)
  t.ok(shortControllerWindow.stderr.includes('has not completed the signed 24-hour continuity window'))

  const stale = gatewayRolloutEvidence(fixture, statePath)
  const staleState = readJson(statePath)
  for (const sample of staleState.relays[0].samples) {
    sample.observedAt = new Date(Date.parse(sample.observedAt) - 31 * 60 * 1000).toISOString()
    sample.collectedAt = new Date(Date.parse(sample.collectedAt) - 31 * 60 * 1000).toISOString()
  }
  writeFileSync(statePath, JSON.stringify(staleState, null, 2) + '\n')
  stale.publicGateway.windowStateSha256 = sha256(readFileSync(statePath))
  writeFileSync(evidencePath, JSON.stringify(stale))
  const staleController = promote(fixture, 'stable', fixture.target, args)
  t.not(staleController.status, 0)
  t.ok(staleController.stderr.includes('latest relay/controller sample is not fresh'))
})

test('fleet promotion refuses unsigned tags and signed version mismatches', { skip: !canRunFunctional }, (t) => {
  const fixture = createFixture(t)
  const before = readFileSync(fixture.channelsPath)

  git(fixture.repo, ['tag', 'v2.0.0'])
  const unsigned = promote(fixture, 'canary', 'v2.0.0')
  t.not(unsigned.status, 0)
  t.ok(unsigned.stderr.includes('must be annotated and signed'))

  const attackerKey = path.join(fixture.work, 'attacker')
  command('ssh-keygen', ['-t', 'ed25519', '-f', attackerKey, '-N', '', '-q', '-C', 'attacker@example'])
  git(fixture.repo, ['tag', '-d', fixture.target])
  git(fixture.repo, ['-c', `user.signingkey=${attackerKey}.pub`, 'tag', '-s', '-m', 'forged', fixture.target])
  const untrusted = promote(fixture, 'canary', fixture.target)
  t.not(untrusted.status, 0)
  t.ok(untrusted.stderr.includes('not signed by a trusted fleet signer'))

  writeFileSync(path.join(fixture.repo, 'package.json'), JSON.stringify({ version: '9.9.8' }))
  git(fixture.repo, ['add', 'package.json'])
  git(fixture.repo, ['commit', '-qm', 'mismatched package version'])
  git(fixture.repo, ['-c', `user.signingkey=${fixture.signingKey}.pub`, 'tag', '-s', '-m', 'mismatch', 'v9.9.9'])
  const mismatch = promote(fixture, 'canary', 'v9.9.9')
  t.not(mismatch.status, 0)
  t.ok(mismatch.stderr.includes('does not match package.json version'))
  t.ok(readFileSync(fixture.channelsPath).equals(before))
})

test('fleet promotion respects an existing atomic promotion lock', { skip: !canRunFunctional }, (t) => {
  const fixture = createFixture(t)
  const before = readFileSync(fixture.channelsPath)
  const lockPath = path.join(path.dirname(fixture.channelsPath), `.${path.basename(fixture.channelsPath)}.promote.lock`)
  writeFileSync(lockPath, 'other promoter\n')

  const result = promote(fixture, 'canary', fixture.target)
  t.not(result.status, 0)
  t.ok(result.stderr.includes('another channel promotion holds'))
  t.ok(readFileSync(fixture.channelsPath).equals(before))
  t.is(readFileSync(lockPath, 'utf8'), 'other promoter\n', 'failed contender does not remove another process lock')
})

function createFixture (t, channelOverrides = {}, options = {}) {
  const work = mkdtempSync(path.join(tmpdir(), 'hiverelay-promote-'))
  t.teardown(() => rmSync(work, { recursive: true, force: true }))
  const repo = path.join(work, 'repo')
  const fleetDir = path.join(repo, 'fleet')
  const channelsPath = path.join(fleetDir, 'channels.json')
  const allowedSigners = path.join(fleetDir, 'allowed-signers')
  const relaysPath = path.join(fleetDir, 'relays.json')
  const gatewayManifestPath = 'fleet/public-hive-gateway-release.json'
  const signingKey = path.join(work, 'trusted')
  const target = 'v1.2.3'
  const appKey = 'aa'.repeat(32)
  const suffix = 'hive-canary.operator.example'
  const origin = `https://${encodeHiveAppKey(Buffer.from(appKey, 'hex'))}.${suffix}`
  const baseGatewayEntry = {
    relay: 'canary-1',
    channel: options.gatewayCohortChannel || 'canary',
    suffix,
    origin,
    connectAddress: '127.0.0.1',
    appKey,
    path: '/index.html',
    contentSha256: 'b'.repeat(64),
    driveVersion: '7',
    peerFingerprint256: Array(32).fill('AA').join(':'),
    nginxConfigSha256: 'c'.repeat(64)
  }
  const publicT1 = options.publicT1
    ? createPublicT1OpsFixture({
      relay: baseGatewayEntry.relay,
      channel: baseGatewayEntry.channel,
      appKey,
      suffix,
      contentSha256: baseGatewayEntry.contentSha256,
      nginxSha256: baseGatewayEntry.nginxConfigSha256,
      fingerprint256: baseGatewayEntry.peerFingerprint256,
      releaseTarget: target
    })
    : null
  const gatewayEntry = publicT1 ? publicT1.manifest.cohort[0] : baseGatewayEntry
  const gatewayManifest = options.gatewayEnabled === true
    ? {
        schema: 'hiverelay-public-gateway-release-v1',
        enabled: true,
        releaseTarget: target,
        admissionProfile: 'blind-substrate-public-v1',
        observationWindowMs: 24 * 60 * 60 * 1000,
        maxProbeGapMs: 20 * 60 * 1000,
        cohort: [gatewayEntry]
      }
    : {
        schema: 'hiverelay-public-gateway-release-v1',
        enabled: false
      }

  mkdirSync(fleetDir, { recursive: true })
  command('ssh-keygen', ['-t', 'ed25519', '-f', signingKey, '-N', '', '-q', '-C', 'release@hiverelay'])
  writeFileSync(allowedSigners, `release@hiverelay ${readFileSync(signingKey + '.pub', 'utf8').trim()}\n`)
  writeFileSync(path.join(repo, 'package.json'), JSON.stringify({ name: 'fixture', version: target.slice(1) }, null, 2) + '\n')
  const relaysBytes = Buffer.from(JSON.stringify({
    relays: [{ name: 'canary-1', channel: 'canary', publicIp: '127.0.0.1' }]
  }, null, 2) + '\n')
  const gatewayManifestBytes = Buffer.from(JSON.stringify(gatewayManifest, null, 2) + '\n')
  writeFileSync(relaysPath, relaysBytes)
  if (!options.omitGatewayManifest) writeFileSync(path.join(repo, gatewayManifestPath), gatewayManifestBytes)
  if (publicT1 && !options.omitOperatorContract) {
    const operatorPath = path.join(repo, publicT1.contractPath)
    mkdirSync(path.dirname(operatorPath), { recursive: true })
    const operatorContract = options.driftOperatorContract
      ? { ...publicT1.contract, certificateSpkiSha256: 'f'.repeat(64) }
      : publicT1.contract
    writeFileSync(operatorPath, JSON.stringify(operatorContract, null, 2) + '\n')
  }
  writeFileSync(channelsPath, JSON.stringify({
    _doc: 'fixture control plane',
    stable: 'v1.0.0',
    canary: 'v1.0.0',
    routing: { wave: ['one', 'two'], hold: true },
    ...channelOverrides
  }, null, 2) + '\n')

  command('git', ['init', '-q', repo])
  git(repo, ['config', 'user.name', 'tester'])
  git(repo, ['config', 'user.email', 'release@hiverelay'])
  git(repo, ['config', 'gpg.format', 'ssh'])
  const releaseFiles = ['package.json', 'fleet/channels.json', 'fleet/allowed-signers', 'fleet/relays.json']
  if (!options.omitGatewayManifest) releaseFiles.push(gatewayManifestPath)
  if (publicT1 && !options.omitOperatorContract) releaseFiles.push(publicT1.contractPath)
  git(repo, ['add', ...releaseFiles])
  git(repo, ['commit', '-qm', 'fixture release'])
  git(repo, ['-c', `user.signingkey=${signingKey}.pub`, 'tag', '-s', '-m', 'release', target])

  return {
    work,
    repo,
    channelsPath,
    allowedSigners,
    signingKey,
    target,
    targetSha: git(repo, ['rev-parse', `${target}^{commit}`]).stdout.trim(),
    relaysPath,
    relaysSha256: sha256(relaysBytes),
    gatewayManifestPath,
    gatewayManifest,
    gatewayManifestSha256: sha256(gatewayManifestBytes),
    gatewayEntry,
    operatorBinding: publicT1?.binding || null
  }
}

function promote (fixture, channel, target, extra = []) {
  return spawnSync(process.execPath, [
    tool,
    '--repo', fixture.repo,
    '--channels', fixture.channelsPath,
    '--allowed-signers', fixture.allowedSigners,
    '--channel', channel,
    '--target', target,
    ...extra
  ], {
    encoding: 'utf8',
    timeout: 20000
  })
}

function rolloutEvidence (fixture, overrides = {}) {
  const now = new Date().toISOString()
  const evidence = {
    schemaVersion: 1,
    generatedAt: now,
    status: 'verified',
    target: {
      tag: fixture.target,
      version: fixture.target.slice(1),
      sha: fixture.targetSha,
      channel: 'canary'
    },
    channelConfig: {
      sha256: sha256(readFileSync(fixture.channelsPath)),
      targets: { canary: fixture.target }
    },
    summary: {
      total: 1,
      updated: 1,
      packageVersionMatches: 1,
      healthy: 1,
      runtimeVersionMatches: 1
    },
    relays: [{
      name: 'canary-1',
      channel: 'canary',
      packageVersion: fixture.target,
      healthVersion: fixture.target.slice(1),
      observedAt: now,
      headSha: fixture.targetSha,
      updated: true,
      packageVersionMatches: true,
      healthy: true,
      runtimeVersionMatches: true
    }]
  }
  return { ...evidence, ...overrides }
}

function gatewayRolloutEvidence (fixture, statePath) {
  const evidence = rolloutEvidence(fixture)
  const checkedAt = evidence.generatedAt
  const endMs = Date.parse(checkedAt)
  const samples = []
  for (let offset = 24 * 60; offset >= 0; offset -= 20) {
    const timestamp = new Date(endMs - offset * 60 * 1000).toISOString()
    samples.push({
      observedAt: timestamp,
      collectedAt: timestamp,
      evidenceSha256: offset === 0
        ? 'd'.repeat(64)
        : createHash('sha256').update(`promotion-window-${offset}`).digest('hex')
    })
  }
  const state = {
    schema: 'hiverelay-public-gateway-window-state-v1',
    releaseTarget: fixture.target,
    releaseSha: fixture.targetSha,
    channel: 'canary',
    manifestSha256: fixture.gatewayManifestSha256,
    observationWindowMs: fixture.gatewayManifest.observationWindowMs,
    maxProbeGapMs: fixture.gatewayManifest.maxProbeGapMs,
    cohortNames: ['canary-1'],
    relays: [{ name: 'canary-1', samples }]
  }
  const stateBytes = Buffer.from(JSON.stringify(state, null, 2) + '\n')
  writeFileSync(statePath, stateBytes)
  const window = {
    windowStartedAt: samples[0].observedAt,
    windowEndedAt: samples.at(-1).observedAt,
    durationMs: 24 * 60 * 60 * 1000,
    sampleCount: samples.length,
    maxGapMs: 20 * 60 * 1000,
    relayCount: 1,
    complete: true
  }
  evidence.schemaVersion = 2
  evidence.probes = { publicGatewayEvidence: true }
  evidence.summary.gatewayHealthy = evidence.summary.total
  evidence.inventory = {
    sha256: fixture.relaysSha256,
    relayNames: ['canary-1']
  }
  evidence.publicGateway = {
    manifest: {
      path: fixture.gatewayManifestPath,
      sha256: fixture.gatewayManifestSha256,
      releaseTarget: fixture.target,
      admissionProfile: fixture.gatewayManifest.admissionProfile,
      observationWindowMs: fixture.gatewayManifest.observationWindowMs,
      maxProbeGapMs: fixture.gatewayManifest.maxProbeGapMs,
      cohortNames: ['canary-1']
    },
    windowStateSha256: sha256(stateBytes),
    window
  }
  for (const relay of evidence.relays) {
    relay.gatewayHealthy = true
    relay.gateway = {
      schema: 'hiverelay-public-gateway-evidence-verification-v2',
      status: 'verified',
      mode: 'fleet',
      admissionProfile: fixture.gatewayManifest.admissionProfile,
      publicSuffixReady: false,
      physicalEnforcementRequired: true,
      releaseTarget: fixture.target,
      releaseSha: fixture.targetSha,
      checkedAt,
      probeObservedAt: samples.at(-1).observedAt,
      origin: fixture.gatewayEntry.origin,
      connectAddress: fixture.gatewayEntry.connectAddress,
      appKey: fixture.gatewayEntry.appKey,
      path: fixture.gatewayEntry.path,
      contentSha256: fixture.gatewayEntry.contentSha256,
      driveVersion: fixture.gatewayEntry.driveVersion,
      tlsProtocol: 'TLSv1.3',
      peerFingerprint256: fixture.gatewayEntry.peerFingerprint256,
      nginxSha256: fixture.gatewayEntry.nginxConfigSha256,
      checks: {
        metadata: true,
        exactBytes: true,
        range: true,
        head: true,
        canonicalIdentity: true,
        managementIsolation: true,
        forwardedHostIsolation: true,
        unavailableAppIsolation: true,
        defaultSniRejection: true,
        sniHostBinding: true
      },
      evidenceSha256: samples.at(-1).evidenceSha256
    }
  }
  return evidence
}

function readJson (file) {
  return JSON.parse(readFileSync(file, 'utf8'))
}

function sha256 (value) {
  return createHash('sha256').update(value).digest('hex')
}

function git (repo, args) {
  return command('git', ['-C', repo, ...args])
}

function command (name, args) {
  const result = spawnSync(name, args, { encoding: 'utf8', timeout: 20000 })
  if (result.status !== 0) throw new Error(`${name} ${args.join(' ')} failed: ${result.stderr || result.stdout}`)
  return result
}

function have (name) {
  return spawnSync('sh', ['-c', `command -v ${name}`], { stdio: 'ignore' }).status === 0
}