167 lines
6.6 KiB
JavaScript
167 lines
6.6 KiB
JavaScript
|
|
import assert from 'node:assert/strict'
|
||
|
|
import crypto from 'node:crypto'
|
||
|
|
import fs from 'node:fs'
|
||
|
|
import os from 'node:os'
|
||
|
|
import path from 'node:path'
|
||
|
|
import test from 'node:test'
|
||
|
|
|
||
|
|
import { activateRelease, verifyReleaseBundle } from '../server/release-broadcast/operator.mjs'
|
||
|
|
import { loadRuntimeState } from '../server/release-broadcast/server.mjs'
|
||
|
|
|
||
|
|
const sha256 = (bytes) => crypto.createHash('sha256').update(bytes).digest('hex')
|
||
|
|
const writeJson = (file, value) => fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 })
|
||
|
|
|
||
|
|
function buildOperatorFixture() {
|
||
|
|
const fixtureRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'hololake-release-operator-fixture-'))
|
||
|
|
const source = path.join(fixtureRoot, 'source')
|
||
|
|
const approvals = path.join(fixtureRoot, 'approvals')
|
||
|
|
const stateRoot = path.join(fixtureRoot, 'state')
|
||
|
|
for (const directory of [source, approvals, stateRoot]) fs.mkdirSync(directory, { mode: 0o700 })
|
||
|
|
const packageName = 'HoloLake.app.tar.gz'
|
||
|
|
const packageBytes = Buffer.from('signed-notarized-updater-package')
|
||
|
|
const sourceCommit = '2'.repeat(40)
|
||
|
|
fs.writeFileSync(path.join(source, packageName), packageBytes, { mode: 0o600 })
|
||
|
|
writeJson(path.join(source, 'HOLOLAKE-CODESIGN.json'), {
|
||
|
|
schema: 'hololake.platform-code-signature-receipt/v1',
|
||
|
|
state: 'DEVELOPER_ID_SIGNATURE_STRICT_AND_GATEKEEPER_ACCEPTED',
|
||
|
|
sourceCommit,
|
||
|
|
})
|
||
|
|
writeJson(path.join(source, 'HOLOLAKE-NOTARIZATION.json'), {
|
||
|
|
schema: 'hololake.apple-notarization-receipt/v1',
|
||
|
|
state: 'APPLE_NOTARIZATION_ACCEPTED_AND_STAPLED',
|
||
|
|
sourceCommit,
|
||
|
|
})
|
||
|
|
const broadcast = {
|
||
|
|
schema: 'hololake.release-broadcast/v1',
|
||
|
|
releaseId: 'GH-HOLOLAKE-RELEASE-0.2.0',
|
||
|
|
version: '0.2.0',
|
||
|
|
pub_date: '2026-08-13T10:00:00.000Z',
|
||
|
|
notes: 'Signed release',
|
||
|
|
platforms: {
|
||
|
|
'darwin-aarch64': {
|
||
|
|
url: `https://guanghulab.com/hololake/releases/0.2.0/${packageName}`,
|
||
|
|
signature: 'trusted-updater-signature',
|
||
|
|
size: packageBytes.length,
|
||
|
|
sha256: sha256(packageBytes),
|
||
|
|
platformCodeSignatureReceipt: 'HOLOLAKE-CODESIGN',
|
||
|
|
notarizationReceipt: 'HOLOLAKE-NOTARIZATION',
|
||
|
|
},
|
||
|
|
},
|
||
|
|
hololake: {
|
||
|
|
features: ['Stage one'],
|
||
|
|
fixes: [],
|
||
|
|
compatibility: { minimumVersion: '0.1.0', dataMigrationRequired: false },
|
||
|
|
restart: { required: true, automaticAllowed: false },
|
||
|
|
rollback: { supported: true, healthReceiptRequired: true, previousVersion: '0.1.0' },
|
||
|
|
},
|
||
|
|
}
|
||
|
|
const broadcastPath = path.join(source, 'latest.json')
|
||
|
|
writeJson(broadcastPath, broadcast)
|
||
|
|
const broadcastSha256 = sha256(fs.readFileSync(broadcastPath))
|
||
|
|
writeJson(path.join(source, 'pipeline-receipt.json'), {
|
||
|
|
schema: 'hololake.signed-release-pipeline-receipt/v1',
|
||
|
|
state: 'SIGNED_NOTARIZED_RELEASE_BROADCAST_READY_FOR_JD_CONTROLLER_UPLOAD',
|
||
|
|
sourceCommit,
|
||
|
|
broadcastSha256,
|
||
|
|
automaticUpload: false,
|
||
|
|
automaticActivation: false,
|
||
|
|
})
|
||
|
|
const approval = path.join(approvals, 'approval.json')
|
||
|
|
writeJson(approval, {
|
||
|
|
schema: 'hololake.release-broadcast-human-approval/v1',
|
||
|
|
state: 'HUMAN_APPROVED_EXACT_SIGNED_NOTARIZED_RELEASE',
|
||
|
|
approvalId: 'GH-HUMAN-RELEASE-APPROVAL-002',
|
||
|
|
releaseId: broadcast.releaseId,
|
||
|
|
version: broadcast.version,
|
||
|
|
broadcastSha256,
|
||
|
|
})
|
||
|
|
return {
|
||
|
|
fixtureRoot,
|
||
|
|
source,
|
||
|
|
approval,
|
||
|
|
stateRoot,
|
||
|
|
expected: { releaseId: broadcast.releaseId, version: broadcast.version, sourceCommit, broadcastSha256 },
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
test('verify reconstructs and validates an exact private candidate without mutating state', () => {
|
||
|
|
const fixture = buildOperatorFixture()
|
||
|
|
try {
|
||
|
|
const result = verifyReleaseBundle(fixture.source, fixture.approval)
|
||
|
|
assert.equal(result.state, 'EXACT_SIGNED_NOTARIZED_HUMAN_APPROVED_BUNDLE_VERIFIED')
|
||
|
|
assert.equal(result.broadcastSha256, fixture.expected.broadcastSha256)
|
||
|
|
assert.equal(result.artifacts.some((artifact) => artifact.name === 'HoloLake.app.tar.gz'), true)
|
||
|
|
assert.deepEqual(fs.readdirSync(fixture.stateRoot), [])
|
||
|
|
} finally {
|
||
|
|
fs.rmSync(fixture.fixtureRoot, { recursive: true, force: true })
|
||
|
|
}
|
||
|
|
})
|
||
|
|
|
||
|
|
test('activation repeats expected facts, commits atomically, and still requires explicit restart', () => {
|
||
|
|
const fixture = buildOperatorFixture()
|
||
|
|
try {
|
||
|
|
const receipt = activateRelease({
|
||
|
|
sourceDirectory: fixture.source,
|
||
|
|
humanApprovalPath: fixture.approval,
|
||
|
|
stateRoot: fixture.stateRoot,
|
||
|
|
expected: fixture.expected,
|
||
|
|
enforceRootOwner: false,
|
||
|
|
})
|
||
|
|
assert.equal(receipt.state, 'ACTIVATED_EXPLICIT_SERVICE_RESTART_REQUIRED')
|
||
|
|
assert.equal(receipt.automaticUpload, false)
|
||
|
|
assert.equal(receipt.automaticActivation, false)
|
||
|
|
assert.equal(receipt.automaticRestart, false)
|
||
|
|
assert.equal(loadRuntimeState(fixture.stateRoot).state, 'READY_SIGNED_NOTARIZED_BROADCAST')
|
||
|
|
assert.equal(fs.existsSync(path.join(fixture.stateRoot, '.operator-lock')), false)
|
||
|
|
} finally {
|
||
|
|
fs.rmSync(fixture.fixtureRoot, { recursive: true, force: true })
|
||
|
|
}
|
||
|
|
})
|
||
|
|
|
||
|
|
test('expected-fact mismatch performs no activation or release staging', () => {
|
||
|
|
const fixture = buildOperatorFixture()
|
||
|
|
try {
|
||
|
|
assert.throws(() => activateRelease({
|
||
|
|
sourceDirectory: fixture.source,
|
||
|
|
humanApprovalPath: fixture.approval,
|
||
|
|
stateRoot: fixture.stateRoot,
|
||
|
|
expected: { ...fixture.expected, broadcastSha256: '3'.repeat(64) },
|
||
|
|
enforceRootOwner: false,
|
||
|
|
}), /HOLOLAKE_RELEASE_OPERATOR_EXPECTED_BROADCAST_SHA256_MISMATCH/)
|
||
|
|
assert.deepEqual(fs.readdirSync(fixture.stateRoot), [])
|
||
|
|
} finally {
|
||
|
|
fs.rmSync(fixture.fixtureRoot, { recursive: true, force: true })
|
||
|
|
}
|
||
|
|
})
|
||
|
|
|
||
|
|
test('human approval cannot be reused after broadcast bytes change', () => {
|
||
|
|
const fixture = buildOperatorFixture()
|
||
|
|
try {
|
||
|
|
const latestPath = path.join(fixture.source, 'latest.json')
|
||
|
|
const latest = JSON.parse(fs.readFileSync(latestPath, 'utf8'))
|
||
|
|
latest.notes = 'Changed after approval'
|
||
|
|
writeJson(latestPath, latest)
|
||
|
|
assert.throws(
|
||
|
|
() => verifyReleaseBundle(fixture.source, fixture.approval),
|
||
|
|
/HOLOLAKE_RELEASE_OPERATOR_PIPELINE_RECEIPT_INVALID/,
|
||
|
|
)
|
||
|
|
} finally {
|
||
|
|
fs.rmSync(fixture.fixtureRoot, { recursive: true, force: true })
|
||
|
|
}
|
||
|
|
})
|
||
|
|
|
||
|
|
test('symlinked package inputs are rejected instead of followed', () => {
|
||
|
|
const fixture = buildOperatorFixture()
|
||
|
|
try {
|
||
|
|
const packagePath = path.join(fixture.source, 'HoloLake.app.tar.gz')
|
||
|
|
const moved = path.join(fixture.fixtureRoot, 'moved-package')
|
||
|
|
fs.renameSync(packagePath, moved)
|
||
|
|
fs.symlinkSync(moved, packagePath)
|
||
|
|
assert.throws(
|
||
|
|
() => verifyReleaseBundle(fixture.source, fixture.approval),
|
||
|
|
/HOLOLAKE_RELEASE_OPERATOR_FILE_INVALID/,
|
||
|
|
)
|
||
|
|
} finally {
|
||
|
|
fs.rmSync(fixture.fixtureRoot, { recursive: true, force: true })
|
||
|
|
}
|
||
|
|
})
|