OverviewFilesCommitsIssuesDocsReleasesPeers

P2P-Hiverelay / test / unit / public-hive-gateway-deploy.test.js

511 lines · javascript ref:
import test from 'brittle'
import { chmod, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import path from 'node:path'
import { runPublicHiveGatewayDeploy } from '../../scripts/public-hive-gateway-deploy.mjs'

const TARGET = 'v1.2.3'
const PREVIOUS = 'v1.2.2'
const RECOVERY = 'v1.2.4'
const TARGET_SHA = 'a'.repeat(40)
const PREVIOUS_SHA = 'b'.repeat(40)
const RECOVERY_SHA = 'd'.repeat(40)
const PUBLISHED_SHA = 'c'.repeat(40)
const WINDOW_MS = 24 * 60 * 60 * 1000

test('public gateway deploy - canary is validation-only unless exact tag/SHA authorization is explicit', async (t) => {
  const f = await fixture(t)
  const input = canaryInput(f)

  const validation = await runPublicHiveGatewayDeploy(input, f.injected)
  t.is(validation.status, 'validated')
  t.is(validation.authorizationRequired, `publish-canary:${TARGET}:${TARGET_SHA}`)
  t.is(f.calls.length, 1)
  t.absent(f.calls[0].publish)
  t.is((await f.channels()).canary, PREVIOUS)

  await t.exception(async () => runPublicHiveGatewayDeploy({
    ...input,
    publish: true,
    authorize: `publish-canary:${TARGET}:${PREVIOUS_SHA}`,
    receipt: f.canaryReceipt
  }, f.injected), /--authorize must exactly equal/)
  t.is(f.calls.length, 2, 'bad authorization never reaches the mutating publisher')
  t.is((await f.channels()).canary, PREVIOUS)

  const published = await runPublicHiveGatewayDeploy({
    ...input,
    publish: true,
    authorize: validation.authorizationRequired,
    receipt: f.canaryReceipt
  }, f.injected)
  t.is(published.status, 'published')
  t.is((await f.channels()).canary, TARGET)
  const receipt = await privateJson(f.canaryReceipt)
  t.is(receipt.status, 'published')
  t.is(receipt.targetSha, TARGET_SHA)
  t.is(receipt.previousTarget, PREVIOUS)
  t.is(receipt.previousTargetSha, PREVIOUS_SHA)
})

test('public gateway deploy - legacy target is rejected before any publication mutation', async (t) => {
  const f = await fixture(t, { gatewayRequired: false })
  await t.exception(async () => runPublicHiveGatewayDeploy({
    ...canaryInput(f),
    publish: true,
    authorize: `publish-canary:${TARGET}:${TARGET_SHA}`,
    receipt: f.canaryReceipt
  }, f.injected), /enabled canonical public gateway release/)
  t.is(f.calls.length, 1)
  t.absent(f.calls[0].publish)
  t.is((await f.channels()).canary, PREVIOUS)
  await t.exception(() => readFile(f.canaryReceipt), /ENOENT/)
})

test('public gateway deploy - enabled publication requires public-t1 operator bindings', async (t) => {
  const f = await fixture(t, { operatorContracts: [] })
  await t.exception(async () => runPublicHiveGatewayDeploy(canaryInput(f), f.injected),
    /requires public-t1 operator contracts for every enabled gateway cohort relay/)
  t.is((await f.channels()).canary, PREVIOUS)
})

test('public gateway deploy - fresh observer session binds canary and stable stays separately authorized', async (t) => {
  const f = await fixture(t)
  await publishCanary(f)
  const start = Date.parse('2026-07-01T00:00:00.000Z')
  const times = [start, start + WINDOW_MS]
  let observerArgs
  const observe = await runPublicHiveGatewayDeploy(observeInput(f), {
    ...f.injected,
    now: () => times.shift(),
    runObserver: async (args) => {
      observerArgs = args
      await writeFile(f.windowState, JSON.stringify(windowState()) + '\n')
      await writeFile(f.rolloutEvidence, JSON.stringify(rolloutEvidence()) + '\n')
      return { code: 0 }
    }
  })

  t.is(observe.status, 'complete')
  t.is(observe.stableRemainsExplicit, true)
  t.is(observe.continuityAuthority, 'controller-retained-manifest-bound')
  t.is(observe.independentWitnessProof, false)
  t.ok(observerArgs.includes('--gateway-window-state'))
  t.ok(observerArgs.includes('--known-hosts'))
  const session = await privateJson(f.session)
  t.is(session.status, 'complete')
  t.is(session.elapsedMs, WINDOW_MS)

  const stableValidation = await runPublicHiveGatewayDeploy(stableInput(f), f.injected)
  t.is(stableValidation.status, 'validated')
  t.is(stableValidation.authorizationRequired, `publish-stable:${TARGET}:${TARGET_SHA}`)
  t.is((await f.channels()).stable, PREVIOUS, 'observation and validation cannot move stable')

  const stable = await runPublicHiveGatewayDeploy({
    ...stableInput(f),
    publish: true,
    authorize: stableValidation.authorizationRequired,
    receipt: f.stableReceipt
  }, f.injected)
  t.is(stable.status, 'published')
  t.is((await f.channels()).stable, TARGET)
  t.is((await privateJson(f.stableReceipt)).status, 'published')
})

test('public gateway deploy - refreshed ops evidence preserves signed operator invariants across canary and stable', async (t) => {
  const initial = operatorContractPublication()
  const f = await fixture(t, { operatorContracts: [initial] })
  await publishCanary(f)
  const canaryReceipt = await privateJson(f.canaryReceipt)
  t.is(canaryReceipt.gatewayOpsEvidenceDir, f.gatewayOpsEvidenceDir)
  t.is(canaryReceipt.operatorContracts[0].operatorContractSha256, initial.operatorContractSha256)
  t.absent('checkedAt' in canaryReceipt.operatorContracts[0], 'rotating artifact time is not receipt-bound')
  t.absent('evidenceSha256' in canaryReceipt.operatorContracts[0], 'rotating artifact bytes are not receipt-bound')

  await completeObservation(f)
  const refreshed = {
    ...initial,
    checkedAt: '2026-07-13T00:00:00.000Z',
    evidenceSha256: '9'.repeat(64)
  }
  f.setOperatorContracts([refreshed])
  const validation = await runPublicHiveGatewayDeploy(stableInput(f), f.injected)
  t.is(validation.status, 'validated')
  t.alike(validation.operatorContracts, canaryReceipt.operatorContracts)

  f.setOperatorContracts([{ ...refreshed, operatorId: 'operator-b' }])
  await t.exception(async () => runPublicHiveGatewayDeploy(stableInput(f), f.injected),
    /changed the signed operator-contract bindings/)

  const swappedDirectory = path.join(f.root, 'swapped-ops')
  await mkdir(swappedDirectory)
  await t.exception(async () => runPublicHiveGatewayDeploy({
    ...stableInput(f),
    gatewayOpsEvidenceDir: swappedDirectory
  }, f.injected), /evidence directory changed after canary/)
})

test('public gateway deploy - preseeded state and early observer success fail closed', async (t) => {
  const seeded = await fixture(t)
  await publishCanary(seeded)
  await writeFile(seeded.windowState, '{}\n')
  await t.exception(async () => runPublicHiveGatewayDeploy(observeInput(seeded), {
    ...seeded.injected,
    runObserver: async () => ({ code: 0 })
  }), /gateway-window-state must not already exist/)
  await t.exception(() => readFile(seeded.session), /ENOENT/)

  const early = await fixture(t)
  await publishCanary(early)
  const start = Date.parse('2026-07-01T00:00:00.000Z')
  const times = [start, start + 60_000, start + 60_000]
  await t.exception(async () => runPublicHiveGatewayDeploy(observeInput(early), {
    ...early.injected,
    now: () => times.shift(),
    runObserver: async () => {
      await writeFile(early.windowState, JSON.stringify(windowState()) + '\n')
      await writeFile(early.rolloutEvidence, JSON.stringify(rolloutEvidence()) + '\n')
      return { code: 0 }
    }
  }), /returned success before this controller session spanned/)
  t.is((await privateJson(early.session)).status, 'failed')
  await t.exception(async () => runPublicHiveGatewayDeploy({
    ...observeInput(early),
    resume: true
  }, early.injected), /only an observing session can be resumed/)
})

test('public gateway deploy - evidence drift blocks stable and reverse canary abort is retired', async (t) => {
  const drift = await fixture(t)
  await publishCanary(drift)
  await completeObservation(drift)
  await writeFile(drift.rolloutEvidence, JSON.stringify({ ...rolloutEvidence(), note: 'drift' }) + '\n')
  await t.exception(async () => runPublicHiveGatewayDeploy(stableInput(drift), drift.injected),
    /observation evidence changed/)
  t.is((await drift.channels()).stable, PREVIOUS)

  const trustDrift = await fixture(t)
  await publishCanary(trustDrift)
  await writeFile(trustDrift.allowedSigners, 'changed trust root\n')
  await t.exception(async () => runPublicHiveGatewayDeploy(observeInput(trustDrift), trustDrift.injected),
    /allowed signers bytes changed/)

  const hostDrift = await fixture(t)
  await publishCanary(hostDrift)
  await completeObservation(hostDrift)
  await writeFile(hostDrift.knownHosts, 'changed host key authority\n')
  await t.exception(async () => runPublicHiveGatewayDeploy(stableInput(hostDrift), hostDrift.injected),
    /knownHostsSha256 changed/)

  const retired = await fixture(t)
  await publishCanary(retired)
  const callsBefore = retired.calls.length
  await t.exception(async () => runPublicHiveGatewayDeploy(abortInput(retired), retired.injected),
    /abort-canary is retired.*recover-canary/)
  t.is(retired.calls.length, callsBefore, 'retired reverse transition never reaches the publisher')
  t.is((await retired.channels()).canary, TARGET)
})

test('public gateway deploy - recovery moves canary only to a signed descendant with receipt-bound CAS', async (t) => {
  const f = await fixture(t)
  await publishCanary(f)

  const validation = await runPublicHiveGatewayDeploy(recoveryInput(f), f.injected)
  t.is(validation.status, 'validated')
  t.is(validation.failedTarget, TARGET)
  t.is(validation.failedTargetSha, TARGET_SHA)
  t.is(validation.target, RECOVERY)
  t.is(validation.targetSha, RECOVERY_SHA)
  t.ok(validation.authorizationRequired.startsWith(
    `recover-canary:${TARGET}:${TARGET_SHA}->${RECOVERY}:${RECOVERY_SHA}:`))
  t.is(validation.recoveryBindingSha256.length, 64)
  t.is(f.calls.at(-1).expectedCurrentTarget, TARGET)
  t.is(f.calls.at(-1).expectedCurrentTargetSha, TARGET_SHA)
  t.is((await f.channels()).canary, TARGET)

  const result = await runPublicHiveGatewayDeploy({
    ...recoveryInput(f),
    publish: true,
    authorize: validation.authorizationRequired,
    receipt: f.recoveryReceipt
  }, f.injected)
  t.is(result.status, 'published')
  t.is((await f.channels()).canary, RECOVERY)
  const receipt = await privateJson(f.recoveryReceipt)
  t.is(receipt.action, 'recover-canary')
  t.is(receipt.failedTarget, TARGET)
  t.is(receipt.failedTargetSha, TARGET_SHA)
  t.is(receipt.recoveryBindingSha256, validation.recoveryBindingSha256)
  t.alike(receipt.operatorContracts, validation.operatorContracts)
})

test('public gateway deploy - recovery rejects replay, non-descendants, drift, and unchanged mutation', async (t) => {
  const same = await fixture(t)
  await publishCanary(same)
  await t.exception(async () => runPublicHiveGatewayDeploy({
    ...recoveryInput(same),
    target: TARGET,
    targetSha: TARGET_SHA
  }, same.injected), /new forward tag and commit/)

  const nonDescendant = await fixture(t)
  await publishCanary(nonDescendant)
  await t.exception(async () => runPublicHiveGatewayDeploy(recoveryInput(nonDescendant), {
    ...nonDescendant.injected,
    isAncestor: async () => false
  }), /must descend from the failed canary target/)

  const channelDrift = await fixture(t)
  await publishCanary(channelDrift)
  await channelDrift.setChannels({ canary: PREVIOUS, stable: PREVIOUS })
  await t.exception(async () => runPublicHiveGatewayDeploy(recoveryInput(channelDrift), channelDrift.injected),
    /canary channel no longer points/)

  const stableDrift = await fixture(t)
  await publishCanary(stableDrift)
  await stableDrift.setChannels({ canary: TARGET, stable: TARGET })
  await t.exception(async () => runPublicHiveGatewayDeploy(recoveryInput(stableDrift), stableDrift.injected),
    /already published to stable/)

  const contractDrift = await fixture(t)
  await publishCanary(contractDrift)
  const validation = await runPublicHiveGatewayDeploy(recoveryInput(contractDrift), contractDrift.injected)
  contractDrift.mutateContractsOnPublish()
  await t.exception(async () => runPublicHiveGatewayDeploy({
    ...recoveryInput(contractDrift),
    publish: true,
    authorize: validation.authorizationRequired,
    receipt: contractDrift.recoveryReceipt
  }, contractDrift.injected), /changed the signed operator-contract bindings/)
  t.is((await privateJson(contractDrift.recoveryReceipt)).status, 'failed')

  const unchanged = await fixture(t)
  await publishCanary(unchanged)
  const unchangedValidation = await runPublicHiveGatewayDeploy(recoveryInput(unchanged), unchanged.injected)
  unchanged.forceUnchangedOnPublish()
  await t.exception(async () => runPublicHiveGatewayDeploy({
    ...recoveryInput(unchanged),
    publish: true,
    authorize: unchangedValidation.authorizationRequired,
    receipt: unchanged.recoveryReceipt
  }, unchanged.injected), /cannot succeed as unchanged/)
  t.is((await privateJson(unchanged.recoveryReceipt)).status, 'failed')
})

async function fixture (t, options = {}) {
  const root = await mkdtemp(path.join(tmpdir(), 'hiverelay-deploy-'))
  t.teardown(() => rm(root, { recursive: true, force: true }))
  const repo = path.join(root, 'repo')
  const fleet = path.join(repo, 'fleet')
  const channelsPath = path.join(fleet, 'channels.json')
  const allowedSigners = path.join(root, 'allowed-signers')
  const relays = path.join(fleet, 'relays.json')
  const canaryReceipt = path.join(root, 'canary-receipt.json')
  const stableReceipt = path.join(root, 'stable-receipt.json')
  const recoveryReceipt = path.join(root, 'recovery-receipt.json')
  const session = path.join(root, 'observation-session.json')
  const windowState = path.join(root, 'window-state.json')
  const rolloutEvidencePath = path.join(root, 'rollout-evidence.json')
  const knownHosts = path.join(root, 'known-hosts')
  const initialOperatorContracts = options.operatorContracts ?? [operatorContractPublication()]
  const gatewayOpsEvidenceDir = initialOperatorContracts.length > 0 ? path.join(root, 'gateway-ops') : null
  await mkdir(fleet, { recursive: true })
  if (gatewayOpsEvidenceDir) await mkdir(gatewayOpsEvidenceDir)
  await writeFile(channelsPath, JSON.stringify({ canary: PREVIOUS, stable: PREVIOUS }, null, 2) + '\n')
  await writeFile(relays, '{"relays":[]}\n')
  await writeFile(allowedSigners, 'fixture\n')
  await writeFile(knownHosts, 'fixture\n')
  const calls = []
  let operatorContracts = structuredClone(initialOperatorContracts)
  let driftContractsOnPublish = false
  let unchangedOnPublish = false
  const publish = async (input) => {
    calls.push({ ...input })
    if (input.target === PREVIOUS && options.previousPublicT1 && !input.gatewayOpsEvidenceDir) {
      throw new Error('--gateway-ops-evidence-dir is required for a public-t1-gateway channel publication')
    }
    const channels = JSON.parse(await readFile(channelsPath, 'utf8'))
    if (input.expectedCurrentTarget) {
      if (channels[input.channel] !== input.expectedCurrentTarget || input.expectedCurrentTargetSha !== TARGET_SHA) {
        throw new Error('fixture recovery compare-and-swap rejected')
      }
    }
    const wouldChange = channels[input.channel] !== input.target
    if (input.publish && wouldChange && !unchangedOnPublish) {
      channels[input.channel] = input.target
      await writeFile(channelsPath, JSON.stringify(channels, null, 2) + '\n')
    }
    const publishedContracts = driftContractsOnPublish && input.publish
      ? [{ ...operatorContracts[0], operatorId: 'operator-drift' }]
      : operatorContracts
    const publicTarget = input.target === TARGET || input.target === RECOVERY
    return {
      schema: 'hiverelay-fleet-channel-publication-v1',
      status: input.publish ? (wouldChange && !unchangedOnPublish ? 'published' : 'unchanged') : 'dry-run',
      channel: input.channel,
      target: input.target,
      publicGatewayRequired: publicTarget ? options.gatewayRequired !== false : false,
      operatorContracts: publicTarget ? structuredClone(publishedContracts) : [],
      wouldChange,
      publishedHead: PUBLISHED_SHA,
      remoteHead: PUBLISHED_SHA
    }
  }
  const resolveTargetSha = async (_repo, target) => {
    if (target === TARGET) return TARGET_SHA
    if (target === PREVIOUS) return PREVIOUS_SHA
    if (target === RECOVERY) return RECOVERY_SHA
    throw new Error('unknown fixture tag')
  }
  return {
    root,
    repo,
    channelsPath,
    allowedSigners,
    relays,
    canaryReceipt,
    stableReceipt,
    recoveryReceipt,
    session,
    windowState,
    rolloutEvidence: rolloutEvidencePath,
    knownHosts,
    gatewayOpsEvidenceDir,
    calls,
    setOperatorContracts: value => { operatorContracts = structuredClone(value) },
    mutateContractsOnPublish: () => { driftContractsOnPublish = true },
    forceUnchangedOnPublish: () => { unchangedOnPublish = true },
    injected: {
      publish,
      resolveTargetSha,
      isAncestor: async (_repo, ancestor, descendant) => ancestor === TARGET_SHA && descendant === RECOVERY_SHA
    },
    channels: async () => JSON.parse(await readFile(channelsPath, 'utf8')),
    setChannels: async value => writeFile(channelsPath, JSON.stringify(value, null, 2) + '\n')
  }
}

function commonInput (f) {
  return {
    target: TARGET,
    targetSha: TARGET_SHA,
    repo: f.repo,
    remote: 'origin',
    branch: 'main',
    allowedSigners: f.allowedSigners,
    relays: f.relays,
    channels: f.channelsPath
  }
}

function canaryInput (f) {
  return {
    action: 'canary',
    ...commonInput(f),
    ...(f.gatewayOpsEvidenceDir ? { gatewayOpsEvidenceDir: f.gatewayOpsEvidenceDir } : {})
  }
}

function observeInput (f) {
  return {
    action: 'observe',
    ...commonInput(f),
    canaryReceipt: f.canaryReceipt,
    session: f.session,
    knownHosts: f.knownHosts,
    gatewayEvidence: '/root/gateway-evidence.json',
    gatewayWindowState: f.windowState,
    evidence: f.rolloutEvidence
  }
}

function stableInput (f) {
  return {
    action: 'stable',
    ...commonInput(f),
    canaryReceipt: f.canaryReceipt,
    session: f.session,
    ...(f.gatewayOpsEvidenceDir ? { gatewayOpsEvidenceDir: f.gatewayOpsEvidenceDir } : {})
  }
}

function abortInput (f) {
  return {
    action: 'abort-canary',
    ...commonInput(f),
    canaryReceipt: f.canaryReceipt
  }
}

function recoveryInput (f) {
  return {
    action: 'recover-canary',
    ...commonInput(f),
    target: RECOVERY,
    targetSha: RECOVERY_SHA,
    canaryReceipt: f.canaryReceipt,
    ...(f.gatewayOpsEvidenceDir ? { gatewayOpsEvidenceDir: f.gatewayOpsEvidenceDir } : {})
  }
}

async function publishCanary (f) {
  return runPublicHiveGatewayDeploy({
    ...canaryInput(f),
    publish: true,
    authorize: `publish-canary:${TARGET}:${TARGET_SHA}`,
    receipt: f.canaryReceipt
  }, f.injected)
}

async function completeObservation (f) {
  const start = Date.parse('2026-07-01T00:00:00.000Z')
  const times = [start, start + WINDOW_MS]
  return runPublicHiveGatewayDeploy(observeInput(f), {
    ...f.injected,
    now: () => times.shift(),
    runObserver: async () => {
      await writeFile(f.windowState, JSON.stringify(windowState()) + '\n')
      await writeFile(f.rolloutEvidence, JSON.stringify(rolloutEvidence()) + '\n')
      return 0
    }
  })
}

function windowState () {
  return {
    schema: 'hiverelay-public-gateway-window-state-v1',
    releaseTarget: TARGET,
    releaseSha: TARGET_SHA,
    channel: 'canary',
    observationWindowMs: WINDOW_MS
  }
}

function rolloutEvidence () {
  return {
    schemaVersion: 2,
    status: 'verified',
    target: { tag: TARGET, sha: TARGET_SHA, channel: 'canary' },
    publicGateway: { window: { complete: true } }
  }
}

function operatorContractPublication () {
  return {
    relay: 'stable-1',
    operatorId: 'operator-a',
    registrableDomain: 'operator.example',
    suffix: 'hive.operator.example',
    operatorContractSha256: 'd'.repeat(64),
    certificateSpkiSha256: 'e'.repeat(64),
    expectedAddresses: ['8.8.8.8', '2606:4700:4700::1111'],
    checkedAt: '2026-07-12T00:00:00.000Z',
    evidenceSha256: 'f'.repeat(64)
  }
}

async function privateJson (file) {
  await chmod(file, 0o600)
  return JSON.parse(await readFile(file, 'utf8'))
}