OverviewFilesCommitsIssuesDocsReleasesPeers

P2P-Hiverelay / test / unit / official-umbrel-pr-evidence.test.js

756 lines · javascript ref:
import test from 'brittle'
import { execFile } from 'node:child_process'
import { createHash } from 'node:crypto'
import { mkdir, mkdtemp, readFile, rm, symlink, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import path from 'node:path'

const HEAD_SHA = 'a'.repeat(40)
const REPOSITORY = 'bigdestiny2/P2P-Hiverelay'
const RELEASE_BASE = `https://github.com/${REPOSITORY}/releases/download/v9.9.9`
const STARTOS_REGISTRY_PACKAGE_URL = 'https://registry.start9.com/startos/blindspark'
const ISO_TIMESTAMP_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/
const WRITE_OFFICIAL_UMBREL_PR_EVIDENCE_SCRIPT = path.join(process.cwd(), 'scripts/write-official-umbrel-pr-evidence.mjs')
const IMAGE_MANIFEST_BYTES = '{"kind":"release-image-manifest"}\n'
const IMAGE_SMOKE_BYTES = '{"kind":"release-image-smoke"}\n'
const UMBREL_SMOKE_BYTES = '{"kind":"umbrel-package-smoke"}\n'
const FLEET_ROLLOUT_BYTES = '{"kind":"fleet-rollout"}\n'
const STARTOS_REGISTRY_BYTES = '{"kind":"startos-registry-publication"}\n'
const STARTOS_PACKAGE_BYTES = 's9pk\n'
const IMAGE_MANIFEST_SHA = sha256(IMAGE_MANIFEST_BYTES)
const IMAGE_SMOKE_SHA = sha256(IMAGE_SMOKE_BYTES)
const UMBREL_SMOKE_SHA = sha256(UMBREL_SMOKE_BYTES)
const FLEET_ROLLOUT_SHA = sha256(FLEET_ROLLOUT_BYTES)
const STARTOS_REGISTRY_SHA = sha256(STARTOS_REGISTRY_BYTES)
const STARTOS_PACKAGE_SHA = sha256(STARTOS_PACKAGE_BYTES)
const LINKED_EVIDENCE_FILES = [
  'release-evidence.json',
  'release-image-manifest-evidence.json',
  'release-image-smoke-evidence.json',
  'umbrel-package-smoke-evidence.json',
  'fleet-rollout-evidence.json',
  'startos-registry-evidence.json'
]

async function runWriter (outFile, env, opts = {}) {
  const cwd = opts.cwd || path.dirname(path.resolve(outFile))
  if (opts.linkedArtifacts !== false) await writeLinkedArtifacts(cwd)
  return new Promise((resolve, reject) => {
    execFile(process.execPath, [WRITE_OFFICIAL_UMBREL_PR_EVIDENCE_SCRIPT, '--out', outFile], {
      cwd,
      env: { ...process.env, ...env },
      timeout: 10000
    }, (err, stdout, stderr) => {
      if (err) {
        err.stdout = stdout
        err.stderr = stderr
        reject(err)
      } else {
        resolve({ stdout, stderr })
      }
    })
  })
}

async function writeLinkedArtifacts (dir, overrides = {}) {
  const artifacts = {
    'release-evidence.json': releaseEvidenceBytes(),
    'release-image-manifest-evidence.json': IMAGE_MANIFEST_BYTES,
    'release-image-smoke-evidence.json': IMAGE_SMOKE_BYTES,
    'umbrel-package-smoke-evidence.json': UMBREL_SMOKE_BYTES,
    'fleet-rollout-evidence.json': FLEET_ROLLOUT_BYTES,
    'startos-registry-evidence.json': STARTOS_REGISTRY_BYTES,
    'startos/blindspark.s9pk': STARTOS_PACKAGE_BYTES
  }
  await mkdir(path.join(dir, 'startos'), { recursive: true })
  for (const file of LINKED_EVIDENCE_FILES) {
    if (overrides[file] === false) continue
    await writeFile(path.join(dir, file), bytesFor(file, artifacts, overrides))
  }
  if (overrides['startos/blindspark.s9pk'] !== false) {
    await writeFile(path.join(dir, 'startos/blindspark.s9pk'), bytesFor('startos/blindspark.s9pk', artifacts, overrides))
  }
}

function bytesFor (file, artifacts, overrides) {
  return Object.prototype.hasOwnProperty.call(overrides, file) ? overrides[file] : artifacts[file]
}

function env (overrides = {}) {
  return {
    GITHUB_SERVER_URL: 'https://github.com',
    GITHUB_REPOSITORY: REPOSITORY,
    GITHUB_RUN_ID: '12345',
    GITHUB_RUN_ATTEMPT: '2',
    HIVERELAY_RELEASE_VERSION: 'v9.9.9',
    HIVERELAY_RELEASE_SEMVER: '9.9.9',
    HIVERELAY_UMBREL_OFFICIAL_PR_URL: 'https://github.com/getumbrel/umbrel-apps/pull/123',
    HIVERELAY_UMBREL_OFFICIAL_PR_HEAD: 'bigdestiny2:blindspark-v9.9.9',
    HIVERELAY_UMBREL_OFFICIAL_PR_HEAD_SHA: HEAD_SHA,
    HIVERELAY_UMBREL_OFFICIAL_PR_STATE: 'OPEN',
    HIVERELAY_UMBREL_OFFICIAL_PR_DRAFT: 'true',
    HIVERELAY_UMBREL_OFFICIAL_PR_BASE: 'master',
    HIVERELAY_UMBREL_OFFICIAL_PR_HEAD_OWNER: 'bigdestiny2',
    HIVERELAY_UMBREL_OFFICIAL_PR_HEAD_REF: 'blindspark-v9.9.9',
    HIVERELAY_UMBREL_OFFICIAL_PR_HEAD_OID: HEAD_SHA,
    HIVERELAY_STARTOS_REGISTRY_PACKAGE_URL: STARTOS_REGISTRY_PACKAGE_URL,
    ...overrides
  }
}

function releaseEvidenceBytes (overrides = {}) {
  return JSON.stringify(deepMerge({
    schemaVersion: 1,
    release: {
      version: 'v9.9.9',
      semver: '9.9.9',
      workflow: {
        status: 'success',
        repository: REPOSITORY,
        runId: '12345',
        runAttempt: '2',
        runUrl: `https://github.com/${REPOSITORY}/actions/runs/12345`
      }
    },
    artifacts: {
      startosPackage: {
        path: 'startos/blindspark.s9pk',
        sha256: STARTOS_PACKAGE_SHA
      }
    },
    gates: {
      imageManifestEvidence: {
        path: 'release-image-manifest-evidence.json',
        sha256: IMAGE_MANIFEST_SHA
      },
      pushedImageSmokeEvidence: {
        path: 'release-image-smoke-evidence.json',
        sha256: IMAGE_SMOKE_SHA
      },
      umbrelPackageSmokeEvidence: {
        path: 'umbrel-package-smoke-evidence.json',
        sha256: UMBREL_SMOKE_SHA
      }
    },
    surfaces: {
      fleetRolloutEvidence: {
        path: 'fleet-rollout-evidence.json',
        sha256: FLEET_ROLLOUT_SHA
      },
      startosRegistryPackageUrl: STARTOS_REGISTRY_PACKAGE_URL,
      startosRegistryEvidence: {
        path: 'startos-registry-evidence.json',
        sha256: STARTOS_REGISTRY_SHA
      },
      umbrelOfficial: {
        status: 'draft-pr-ready',
        prUrl: 'https://github.com/getumbrel/umbrel-apps/pull/123',
        head: 'bigdestiny2:blindspark-v9.9.9',
        headSha: HEAD_SHA,
        state: 'OPEN',
        isDraft: true,
        base: 'master',
        headOwner: 'bigdestiny2',
        headRef: 'blindspark-v9.9.9',
        headOid: HEAD_SHA
      }
    }
  }, overrides), null, 2) + '\n'
}

function deepMerge (target, overrides) {
  for (const [key, value] of Object.entries(overrides)) {
    if (isPlainObject(value) && isPlainObject(target[key])) {
      deepMerge(target[key], value)
    } else {
      target[key] = value
    }
  }
  return target
}

function isPlainObject (value) {
  return value != null && typeof value === 'object' && !Array.isArray(value)
}

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

test('official Umbrel PR evidence writer keeps a closed public schema', async (t) => {
  const script = await readFile('scripts/write-official-umbrel-pr-evidence.mjs', 'utf8')

  t.ok(script.includes('assertOfficialUmbrelPrEvidenceSchema(body)'))
  t.ok(script.includes('function assertOfficialUmbrelPrEvidenceSchema'))
  t.ok(script.includes("requireOnlyKeys('official Umbrel PR evidence'"))
  t.ok(script.includes('has unsupported fields'))
})

test('official Umbrel PR evidence writer records public release links', async (t) => {
  const dir = await mkdtemp(path.join(tmpdir(), 'hiverelay-official-pr-evidence-'))
  t.teardown(async () => {
    await rm(dir, { recursive: true, force: true })
  })

  const outFile = path.join(dir, 'official-umbrel-pr-evidence.json')
  await runWriter(outFile, env())

  const body = JSON.parse(await readFile(outFile, 'utf8'))
  t.is(body.schemaVersion, 1)
  t.ok(ISO_TIMESTAMP_PATTERN.test(body.generatedAt), 'official Umbrel PR evidence generatedAt is an ISO timestamp')
  t.is(body.kind, 'official-umbrel-pr')
  t.is(body.status, 'updated')
  t.is(body.release.version, 'v9.9.9')
  t.is(body.pr.url, 'https://github.com/getumbrel/umbrel-apps/pull/123')
  t.is(body.pr.number, '123')
  t.is(body.pr.head, 'bigdestiny2:blindspark-v9.9.9')
  t.is(body.pr.headSha, HEAD_SHA)
  t.is(body.pr.state, 'OPEN')
  t.is(body.pr.isDraft, true)
  t.is(body.pr.base, 'master')
  t.is(body.pr.headOwner, 'bigdestiny2')
  t.is(body.pr.headRef, 'blindspark-v9.9.9')
  t.is(body.pr.headOid, HEAD_SHA)
  t.is(body.workflow.runUrl, `https://github.com/${REPOSITORY}/actions/runs/12345`)
  t.is(body.runtimeReview.status, 'pending-real-device-review')
  t.is(body.runtimeReview.evidenceFile, 'umbrel-runtime-review-evidence.json')
  t.is(body.runtimeReview.verifier, 'npm run umbrel:verify-runtime-review')
  t.is(body.evidenceLinks.releaseEvidence, `${RELEASE_BASE}/release-evidence.json`)
  t.is(body.evidenceLinks.releaseImageManifest, `${RELEASE_BASE}/release-image-manifest-evidence.json`)
  t.is(body.evidenceLinks.releaseImageSmoke, `${RELEASE_BASE}/release-image-smoke-evidence.json`)
  t.is(body.evidenceLinks.umbrelPackageSmoke, `${RELEASE_BASE}/umbrel-package-smoke-evidence.json`)
  t.is(body.evidenceLinks.fleetRollout, `${RELEASE_BASE}/fleet-rollout-evidence.json`)
  t.is(body.evidenceLinks.startosPackage, `${RELEASE_BASE}/blindspark.s9pk`)
  t.is(body.evidenceLinks.startosRegistryPackage, STARTOS_REGISTRY_PACKAGE_URL)
  t.is(body.evidenceLinks.startosRegistry, `${RELEASE_BASE}/startos-registry-evidence.json`)
})

test('official Umbrel PR evidence writer records gateway rollout as deferred without a false fleet link', async (t) => {
  const dir = await mkdtemp(path.join(tmpdir(), 'hiverelay-official-pr-evidence-'))
  t.teardown(async () => {
    await rm(dir, { recursive: true, force: true })
  })

  const releaseBytes = releaseEvidenceBytes({
    release: {
      channel: 'none',
      publicGateway: {
        enabled: true,
        manifestStatus: 'enabled',
        manifestPath: 'fleet/public-hive-gateway-release.json',
        manifestSha256: '4'.repeat(64),
        releaseTarget: 'v9.9.9',
        commitSha: HEAD_SHA,
        admissionProfile: 'blind-substrate-public-v1',
        cohortSize: 3
      }
    },
    surfaces: {
      fleetRollout: 'deferred-gateway-canary-gated',
      fleetRolloutEvidence: { path: '', sha256: '' }
    }
  })
  await writeLinkedArtifacts(dir, {
    'release-evidence.json': releaseBytes,
    'fleet-rollout-evidence.json': false
  })
  const outFile = path.join(dir, 'official-umbrel-pr-evidence.json')
  await runWriter(outFile, env({
    HIVERELAY_PUBLIC_GATEWAY_RELEASE_ENABLED: 'true'
  }), { cwd: dir, linkedArtifacts: false })

  const body = JSON.parse(await readFile(outFile, 'utf8'))
  t.is(body.evidenceLinks.fleetRollout, '')
})

test('official Umbrel PR evidence writer requires linked release artifacts before write', async (t) => {
  const dir = await mkdtemp(path.join(tmpdir(), 'hiverelay-official-pr-evidence-'))
  t.teardown(async () => {
    await rm(dir, { recursive: true, force: true })
  })

  const missingFile = path.join(dir, 'missing-artifacts.json')
  let missingErr = null
  try {
    await runWriter(missingFile, env(), { cwd: dir, linkedArtifacts: false })
  } catch (e) {
    missingErr = e
  }
  t.ok(missingErr)
  t.ok(missingErr.stderr.includes('linked evidence artifact is required before writing official Umbrel PR evidence: release-evidence.json'))

  await writeLinkedArtifacts(dir, { 'release-image-smoke-evidence.json': false })
  await symlink('release-evidence.json', path.join(dir, 'release-image-smoke-evidence.json'))
  const symlinkFile = path.join(dir, 'symlink-artifact.json')
  let symlinkErr = null
  try {
    await runWriter(symlinkFile, env(), { cwd: dir, linkedArtifacts: false })
  } catch (e) {
    symlinkErr = e
  }
  t.ok(symlinkErr)
  t.ok(symlinkErr.stderr.includes('linked evidence artifact must not be a symlink: release-image-smoke-evidence.json'))

  const missingPackageDir = path.join(dir, 'missing-package')
  await writeLinkedArtifacts(missingPackageDir, { 'startos/blindspark.s9pk': false })
  const missingPackageFile = path.join(missingPackageDir, 'missing-package.json')
  let missingPackageErr = null
  try {
    await runWriter(missingPackageFile, env(), { cwd: missingPackageDir, linkedArtifacts: false })
  } catch (e) {
    missingPackageErr = e
  }
  t.ok(missingPackageErr)
  t.ok(missingPackageErr.stderr.includes('StartOS package is required before writing official Umbrel PR evidence: startos/blindspark.s9pk'))

  await rm(path.join(dir, 'release-image-smoke-evidence.json'), { force: true })
  await writeLinkedArtifacts(dir, { 'startos-registry-evidence.json': 'x'.repeat(2 * 1024 * 1024 + 1) })
  const oversizedFile = path.join(dir, 'oversized-artifact.json')
  let oversizedErr = null
  try {
    await runWriter(oversizedFile, env(), { cwd: dir, linkedArtifacts: false })
  } catch (e) {
    oversizedErr = e
  }
  t.ok(oversizedErr)
  t.ok(oversizedErr.stderr.includes('linked evidence artifact startos-registry-evidence.json must be 2097152 bytes or smaller'))
})

test('official Umbrel PR evidence writer rejects linked artifact hash drift before write', async (t) => {
  const dir = await mkdtemp(path.join(tmpdir(), 'hiverelay-official-pr-evidence-'))
  t.teardown(async () => {
    await rm(dir, { recursive: true, force: true })
  })

  await writeLinkedArtifacts(dir, {
    'release-image-smoke-evidence.json': '{"kind":"stale-release-image-smoke"}\n'
  })

  const staleImageSmokeFile = path.join(dir, 'stale-image-smoke.json')
  let staleImageSmokeErr = null
  try {
    await runWriter(staleImageSmokeFile, env(), { cwd: dir, linkedArtifacts: false })
  } catch (e) {
    staleImageSmokeErr = e
  }
  t.ok(staleImageSmokeErr)
  t.ok(staleImageSmokeErr.stderr.includes('linked release image smoke evidence SHA-256 does not match release-image-smoke-evidence.json'))

  const packageDir = path.join(dir, 'stale-package')
  await writeLinkedArtifacts(packageDir, {
    'startos/blindspark.s9pk': 'stale package\n'
  })

  const stalePackageFile = path.join(packageDir, 'stale-package.json')
  let stalePackageErr = null
  try {
    await runWriter(stalePackageFile, env(), { cwd: packageDir, linkedArtifacts: false })
  } catch (e) {
    stalePackageErr = e
  }
  t.ok(stalePackageErr)
  t.ok(stalePackageErr.stderr.includes('linked StartOS package SHA-256 does not match startos/blindspark.s9pk'))
})

test('official Umbrel PR evidence writer rejects release evidence drift before write', async (t) => {
  const dir = await mkdtemp(path.join(tmpdir(), 'hiverelay-official-pr-evidence-'))
  t.teardown(async () => {
    await rm(dir, { recursive: true, force: true })
  })

  await writeLinkedArtifacts(dir, {
    'release-evidence.json': releaseEvidenceBytes({
      surfaces: {
        umbrelOfficial: {
          headOid: 'b'.repeat(40)
        }
      }
    })
  })

  const outFile = path.join(dir, 'release-pr-drift.json')
  let err = null
  try {
    await runWriter(outFile, env(), { cwd: dir, linkedArtifacts: false })
  } catch (e) {
    err = e
  }

  t.ok(err)
  t.ok(err.stderr.includes('release evidence official Umbrel PR head OID'))
})

test('official Umbrel PR evidence writer rejects unsafe StartOS registry package URLs', async (t) => {
  const dir = await mkdtemp(path.join(tmpdir(), 'hiverelay-official-pr-evidence-'))
  t.teardown(async () => {
    await rm(dir, { recursive: true, force: true })
  })

  const outFile = path.join(dir, 'unsafe-startos-registry-package-url.json')
  let err = null
  try {
    await runWriter(outFile, env({
      HIVERELAY_STARTOS_REGISTRY_PACKAGE_URL: 'http://registry.start9.com/startos/blindspark'
    }))
  } catch (e) {
    err = e
  }

  t.ok(err)
  t.ok(err.stderr.includes('StartOS registry package link'))
})

test('official Umbrel PR evidence writer rejects non-canonical release repositories', async (t) => {
  const dir = await mkdtemp(path.join(tmpdir(), 'hiverelay-official-pr-evidence-'))
  t.teardown(async () => {
    await rm(dir, { recursive: true, force: true })
  })

  const outFile = path.join(dir, 'official-umbrel-pr-evidence.json')
  let err = null
  try {
    await runWriter(outFile, env({
      GITHUB_REPOSITORY: 'attacker/P2P-Hiverelay'
    }))
  } catch (e) {
    err = e
  }

  t.ok(err)
  t.ok(err.stderr.includes(`workflow repository must be "${REPOSITORY}"`))
})

test('official Umbrel PR evidence writer rejects stale PR state facts', async (t) => {
  const dir = await mkdtemp(path.join(tmpdir(), 'hiverelay-official-pr-evidence-'))
  t.teardown(async () => {
    await rm(dir, { recursive: true, force: true })
  })

  const closedFile = path.join(dir, 'closed-pr.json')
  let stateErr = null
  try {
    await runWriter(closedFile, env({
      HIVERELAY_UMBREL_OFFICIAL_PR_STATE: 'CLOSED'
    }))
  } catch (e) {
    stateErr = e
  }
  t.ok(stateErr)
  t.ok(stateErr.stderr.includes('official Umbrel PR state'))

  const nonDraftFile = path.join(dir, 'non-draft-pr.json')
  let draftErr = null
  try {
    await runWriter(nonDraftFile, env({
      HIVERELAY_UMBREL_OFFICIAL_PR_DRAFT: 'false'
    }))
  } catch (e) {
    draftErr = e
  }
  t.ok(draftErr)
  t.ok(draftErr.stderr.includes('official Umbrel PR draft'))

  const baseFile = path.join(dir, 'wrong-base-pr.json')
  let baseErr = null
  try {
    await runWriter(baseFile, env({
      HIVERELAY_UMBREL_OFFICIAL_PR_BASE: 'main'
    }))
  } catch (e) {
    baseErr = e
  }
  t.ok(baseErr)
  t.ok(baseErr.stderr.includes('official Umbrel PR base'))

  const headRefFile = path.join(dir, 'wrong-head-ref-pr.json')
  let headRefErr = null
  try {
    await runWriter(headRefFile, env({
      HIVERELAY_UMBREL_OFFICIAL_PR_HEAD_REF: 'wrong-branch'
    }))
  } catch (e) {
    headRefErr = e
  }
  t.ok(headRefErr)
  t.ok(headRefErr.stderr.includes('official Umbrel PR head ref matches head branch'))

  const headOwnerFile = path.join(dir, 'wrong-head-owner-pr.json')
  let headOwnerErr = null
  try {
    await runWriter(headOwnerFile, env({
      HIVERELAY_UMBREL_OFFICIAL_PR_HEAD_OWNER: 'attacker'
    }))
  } catch (e) {
    headOwnerErr = e
  }
  t.ok(headOwnerErr)
  t.ok(headOwnerErr.stderr.includes('official Umbrel PR head owner matches head owner'))

  const headOidFile = path.join(dir, 'wrong-head-oid-pr.json')
  let headOidErr = null
  try {
    await runWriter(headOidFile, env({
      HIVERELAY_UMBREL_OFFICIAL_PR_HEAD_OID: 'b'.repeat(40)
    }))
  } catch (e) {
    headOidErr = e
  }
  t.ok(headOidErr)
  t.ok(headOidErr.stderr.includes('official Umbrel PR head OID matches head SHA'))
})

test('official Umbrel PR evidence writer rejects non-upstream PR URLs', async (t) => {
  const dir = await mkdtemp(path.join(tmpdir(), 'hiverelay-official-pr-evidence-'))
  t.teardown(async () => {
    await rm(dir, { recursive: true, force: true })
  })

  const outFile = path.join(dir, 'official-umbrel-pr-evidence.json')
  let err = null
  try {
    await runWriter(outFile, env({
      HIVERELAY_UMBREL_OFFICIAL_PR_URL: 'https://github.com/example/umbrel-apps/pull/123'
    }))
  } catch (e) {
    err = e
  }

  t.ok(err)
  t.ok(err.stderr.includes('official Umbrel PR URL'))

  const zeroFile = path.join(dir, 'zero-official-pr.json')
  let zeroErr = null
  try {
    await runWriter(zeroFile, env({
      HIVERELAY_UMBREL_OFFICIAL_PR_URL: 'https://github.com/getumbrel/umbrel-apps/pull/0'
    }))
  } catch (e) {
    zeroErr = e
  }

  t.ok(zeroErr)
  t.ok(zeroErr.stderr.includes('official Umbrel PR URL'))
})

test('official Umbrel PR evidence writer rejects malformed GitHub owner names', async (t) => {
  const dir = await mkdtemp(path.join(tmpdir(), 'hiverelay-official-pr-evidence-'))
  t.teardown(async () => {
    await rm(dir, { recursive: true, force: true })
  })

  for (const owner of ['bad_owner', 'bad.owner', '-bad', 'getumbrel', 'GetUmbrel']) {
    let err = null
    try {
      await runWriter(path.join(dir, `${owner}-pr.json`), env({
        HIVERELAY_UMBREL_OFFICIAL_PR_HEAD: `${owner}:blindspark-v9.9.9`,
        HIVERELAY_UMBREL_OFFICIAL_PR_HEAD_OWNER: owner
      }))
    } catch (e) {
      err = e
    }
    t.ok(err, owner)
    t.ok(err.stderr.includes('official Umbrel PR head owner must be a normal GitHub owner name'), owner)
  }
})

test('official Umbrel PR evidence writer rejects malformed GitHub head refs', async (t) => {
  const dir = await mkdtemp(path.join(tmpdir(), 'hiverelay-official-pr-evidence-'))
  t.teardown(async () => {
    await rm(dir, { recursive: true, force: true })
  })

  for (const ref of ['bad..ref', '.bad-ref', 'bad/ref.lock']) {
    let err = null
    try {
      await runWriter(path.join(dir, `${ref.replace(/\//g, '-')}-pr.json`), env({
        HIVERELAY_UMBREL_OFFICIAL_PR_HEAD: `bigdestiny2:${ref}`,
        HIVERELAY_UMBREL_OFFICIAL_PR_HEAD_REF: ref
      }))
    } catch (e) {
      err = e
    }
    t.ok(err, ref)
    t.ok(err.stderr.includes('official Umbrel PR head ref must be a normal GitHub branch name'), ref)
  }
})

test('official Umbrel PR evidence writer ignores release base overrides', async (t) => {
  const dir = await mkdtemp(path.join(tmpdir(), 'hiverelay-official-pr-evidence-'))
  t.teardown(async () => {
    await rm(dir, { recursive: true, force: true })
  })

  const outFile = path.join(dir, 'official-umbrel-pr-evidence.json')
  await runWriter(outFile, env({
    HIVERELAY_RELEASE_BASE_URL: 'https://github.com/attacker/fake/releases/download/v9.9.9'
  }))

  const body = JSON.parse(await readFile(outFile, 'utf8'))
  t.is(body.evidenceLinks.releaseEvidence, `${RELEASE_BASE}/release-evidence.json`)
  t.is(body.evidenceLinks.releaseImageManifest, `${RELEASE_BASE}/release-image-manifest-evidence.json`)
  t.is(body.evidenceLinks.releaseImageSmoke, `${RELEASE_BASE}/release-image-smoke-evidence.json`)
  t.is(body.evidenceLinks.umbrelPackageSmoke, `${RELEASE_BASE}/umbrel-package-smoke-evidence.json`)
  t.is(body.evidenceLinks.fleetRollout, `${RELEASE_BASE}/fleet-rollout-evidence.json`)
  t.is(body.evidenceLinks.startosPackage, `${RELEASE_BASE}/blindspark.s9pk`)
  t.is(body.evidenceLinks.startosRegistryPackage, STARTOS_REGISTRY_PACKAGE_URL)
  t.is(body.evidenceLinks.startosRegistry, `${RELEASE_BASE}/startos-registry-evidence.json`)
})

test('official Umbrel PR evidence writer rejects non-numeric run attempts', async (t) => {
  const dir = await mkdtemp(path.join(tmpdir(), 'hiverelay-official-pr-evidence-'))
  t.teardown(async () => {
    await rm(dir, { recursive: true, force: true })
  })

  const outFile = path.join(dir, 'official-umbrel-pr-evidence.json')
  let err = null
  try {
    await runWriter(outFile, env({
      GITHUB_RUN_ATTEMPT: 'not-a-number'
    }))
  } catch (e) {
    err = e
  }

  t.ok(err)
  t.ok(err.stderr.includes('workflow run attempt'))

  const zeroAttemptFile = path.join(dir, 'zero-attempt.json')
  let zeroAttemptErr = null
  try {
    await runWriter(zeroAttemptFile, env({
      GITHUB_RUN_ATTEMPT: '0'
    }))
  } catch (e) {
    zeroAttemptErr = e
  }

  t.ok(zeroAttemptErr)
  t.ok(zeroAttemptErr.stderr.includes('workflow run attempt'))

  const zeroRunIdFile = path.join(dir, 'zero-run-id.json')
  let zeroRunIdErr = null
  try {
    await runWriter(zeroRunIdFile, env({
      GITHUB_RUN_ID: '0'
    }))
  } catch (e) {
    zeroRunIdErr = e
  }

  t.ok(zeroRunIdErr)
  t.ok(zeroRunIdErr.stderr.includes('workflow run id'))

  const badServerFile = path.join(dir, 'bad-workflow-server.json')
  let badServerErr = null
  try {
    await runWriter(badServerFile, env({
      GITHUB_SERVER_URL: 'https://github.com/attacker'
    }))
  } catch (e) {
    badServerErr = e
  }

  t.ok(badServerErr)
  t.ok(badServerErr.stderr.includes('workflow URL'))
})

test('official Umbrel PR evidence writer rejects whitespace-normalized metadata before write', async (t) => {
  const dir = await mkdtemp(path.join(tmpdir(), 'hiverelay-official-pr-evidence-'))
  t.teardown(async () => {
    await rm(dir, { recursive: true, force: true })
  })

  const cases = [
    ['run-id', { GITHUB_RUN_ID: ' 12345' }, 'workflow run id'],
    ['run-attempt', { GITHUB_RUN_ATTEMPT: '2 ' }, 'workflow run attempt'],
    ['server-url', { GITHUB_SERVER_URL: 'https://github.com ' }, 'workflow URL'],
    ['pr-url', { HIVERELAY_UMBREL_OFFICIAL_PR_URL: ' https://github.com/getumbrel/umbrel-apps/pull/123' }, 'official Umbrel PR URL'],
    ['head-ref', {
      HIVERELAY_UMBREL_OFFICIAL_PR_HEAD: 'bigdestiny2:blindspark-v9.9.9 ',
      HIVERELAY_UMBREL_OFFICIAL_PR_HEAD_REF: 'blindspark-v9.9.9 '
    }, 'official Umbrel PR head']
  ]

  for (const [name, overrides, message] of cases) {
    const outFile = path.join(dir, `${name}.json`)
    let err = null
    try {
      await runWriter(outFile, env(overrides))
    } catch (e) {
      err = e
    }

    t.ok(err, name)
    t.ok(err.stderr.includes(message), name)

    let evidenceErr = null
    try {
      await readFile(outFile, 'utf8')
    } catch (e) {
      evidenceErr = e
    }
    t.ok(evidenceErr, `${name} does not write public evidence`)
  }
})

test('official Umbrel PR evidence writer rejects malformed head SHA', async (t) => {
  const dir = await mkdtemp(path.join(tmpdir(), 'hiverelay-official-pr-evidence-'))
  t.teardown(async () => {
    await rm(dir, { recursive: true, force: true })
  })

  const outFile = path.join(dir, 'official-umbrel-pr-evidence.json')
  let err = null
  try {
    await runWriter(outFile, env({
      HIVERELAY_UMBREL_OFFICIAL_PR_HEAD_SHA: 'not-a-sha'
    }))
  } catch (e) {
    err = e
  }

  t.ok(err)
  t.ok(err.stderr.includes('official Umbrel PR head SHA'))
})

test('official Umbrel PR evidence writer rejects secret-looking public values before write', async (t) => {
  const dir = await mkdtemp(path.join(tmpdir(), 'hiverelay-official-pr-evidence-'))
  t.teardown(async () => {
    await rm(dir, { recursive: true, force: true })
  })

  const outFile = path.join(dir, 'official-umbrel-pr-evidence.json')
  let err = null
  try {
    await runWriter(outFile, env({
      GITHUB_RUN_ATTEMPT: 'Bearer abcdefghijklmnopqrstuvwxyz'
    }))
  } catch (e) {
    err = e
  }

  t.ok(err)
  t.ok(err.stderr.includes('bearer token') || err.stderr.includes('workflow run attempt'))
})

test('official Umbrel PR evidence writer rejects control-character public values before write', async (t) => {
  const dir = await mkdtemp(path.join(tmpdir(), 'hiverelay-official-pr-evidence-'))
  t.teardown(async () => {
    await rm(dir, { recursive: true, force: true })
  })

  const outFile = path.join(dir, 'official-umbrel-pr-evidence.json')
  let err = null
  try {
    await runWriter(outFile, env({
      GITHUB_RUN_ATTEMPT: '2\n3'
    }))
  } catch (e) {
    err = e
  }

  t.ok(err)
  t.ok(err.stderr.includes('official Umbrel PR evidence must not contain control characters'))
})