'use strict'; const assert = require('node:assert/strict'); const crypto = require('node:crypto'); const test = require('node:test'); const { SCHEMA, auditDeploymentReadinessReceipt, createDeploymentReadinessReceipt, parseArguments, validateDeploymentReadinessReceipt, } = require('../../scripts/ql3-release-deployment-readiness-contract.cjs'); const VERSION = '3.0.0-alpha.0'; const REVISION = 'd'.repeat(40); const OWNER = 'qinglong-release'; const SOURCE_REPOSITORY = `${OWNER}/qinglong`; const MANIFEST_DIGEST = `sha256:${'a'.repeat(64)}`; const RELEASE_SET_DIGEST = `sha256:${'b'.repeat(64)}`; function sha256(value) { return `sha256:${crypto.createHash('sha256').update(value).digest('hex')}`; } function identity(scope) { return { version: VERSION, sourceRevision: REVISION, sourceRef: `refs/tags/v${VERSION}`, releaseScope: scope, repositoryOwner: OWNER, sourceRepository: SOURCE_REPOSITORY, }; } function consumption(scope, marker) { return { compatible: true, releaseScope: scope, sourceRepository: SOURCE_REPOSITORY, workflowIdentity: `https://github.com/${SOURCE_REPOSITORY}/.github/workflows/ql3-image-release.yml@refs/tags/v${VERSION}`, releaseSetDigest: RELEASE_SET_DIGEST, catalogManifestDigest: MANIFEST_DIGEST, immutableReference: `ghcr.io/${OWNER}/qinglong3-release-catalog@${MANIFEST_DIGEST}`, imageCount: scope === 'all' ? 5 : scope === 'local' ? 1 : 4, discoveryTagAuthority: 'none', externalToolResultsReplayed: false, deploymentMutation: false, contentDigest: sha256(marker), releaseSet: { release: { version: VERSION, sourceRevision: REVISION, sourceRef: `refs/tags/v${VERSION}`, scope, }, releaseSetDigest: RELEASE_SET_DIGEST, }, }; } function reportEntry(value) { return { value, digest: sha256(`${JSON.stringify(value)}\n`), }; } function localReport(scope, catalog, profile) { return reportEntry({ schemaVersion: 1, profile, generation: 2, exactRepoDigest: true, composeMerge: true, rolloutActive: true, durableReceipt: true, sqliteWriteContract: 37, sqliteBackup: true, sqliteWriteObservation: { committed: true }, sqliteRestorePrepared: true, sqliteRestoreCommitted: true, sqliteRestoreRolloutRecovered: true, sqliteRestoreReplayUnchanged: true, sqliteEvidenceCollected: true, sqliteCollectedRolloutReplayUnchanged: true, gracefulCleanup: true, releaseAuthority: { mode: 'verified_release_catalog', sourceRevision: REVISION, sourceRef: `refs/tags/v${VERSION}`, scope, releaseSetDigest: RELEASE_SET_DIGEST, catalogManifestDigest: MANIFEST_DIGEST, catalogConsumptionDigest: catalog.contentDigest, selectionDigest: sha256('selection'), }, compatible: true, }); } function clusterReport(scope, catalog) { return reportEntry({ schemaVersion: 1, schema: 'qinglong/kubernetes-deployment-live-evidence@v1', kubernetes: { serverVersion: 'v1.34.3+k3s1', architecture: 'linux/amd64', nodeCount: 3, clusterUid: 'fixture-cluster', }, deployment: { namespace: 'qinglong3-system', resourceCount: 7, deploymentCount: 4, replicas: 0, fieldManager: 'qinglong3-catalog-lock', immutableImages: true, headName: 'ql3-deployment-head', headPhase: 'committed', headGeneration: 2, deploymentDigest: sha256('deployment'), resourceInventoryCount: 6, }, releaseAuthority: { mode: 'verified_release_catalog', version: VERSION, sourceRevision: REVISION, sourceRef: `refs/tags/v${VERSION}`, scope, releaseSetDigest: RELEASE_SET_DIGEST, catalogManifestDigest: MANIFEST_DIGEST, catalogConsumptionDigest: catalog.contentDigest, immutableReference: catalog.immutableReference, }, preflightDigest: sha256('preflight'), receiptDigest: sha256('receipt'), receiptAuditCompatible: true, retirement: { preflightDigest: sha256('retirement-preflight'), receiptDigest: sha256('retirement-receipt'), receiptAuditCompatible: true, targetCount: 1, targetAbsent: true, uidResourceVersionDeletePreconditions: true, deploymentHeadCas: true, inventoryCount: 6, unixSocketProxy: true, }, serverSideDryRun: true, serverSideApply: true, convergenceRead: true, deploymentHeadCas: true, resourceInventoryClosed: true, crossResourceAtomicity: false, cleanupComplete: true, }); } function fixture(scope = 'all') { const finalizerConsumption = consumption(scope, 'finalizer'); const input = { identity: identity(scope), finalizerConsumption, }; if (scope !== 'cluster') { const localConsumption = consumption(scope, 'local'); input.local = { consumption: localConsumption, edge: localReport(scope, localConsumption, 'edge'), standalone: localReport(scope, localConsumption, 'standalone'), }; } if (scope !== 'local') { const clusterConsumption = consumption(scope, 'cluster'); input.cluster = { consumption: clusterConsumption, report: clusterReport(scope, clusterConsumption), }; } return { input, receipt: createDeploymentReadinessReceipt(input) }; } test('requires the exact deployment families for Local, Cluster and All', () => { for (const [scope, families, reportCount] of [ ['local', ['local'], 2], ['cluster', ['cluster'], 1], ['all', ['local', 'cluster'], 3], ]) { const { input, receipt } = fixture(scope); assert.equal(receipt.schema, SCHEMA); assert.deepEqual(receipt.requiredDeploymentFamilies, families); assert.equal( auditDeploymentReadinessReceipt(receipt, input).reportCount, reportCount, ); assert.equal( validateDeploymentReadinessReceipt(receipt, { release: receipt.release, sourceRepository: SOURCE_REPOSITORY, releaseSetDigest: RELEASE_SET_DIGEST, catalogManifestDigest: MANIFEST_DIGEST, immutableReference: receipt.catalog.immutableReference, }), receipt, ); } }); test('rejects missing, extra and cross-scope deployment evidence', () => { const local = fixture('local').input; assert.throws( () => createDeploymentReadinessReceipt({ ...local, local: undefined, }), /local deployment catalog consumption is invalid/, ); assert.throws( () => createDeploymentReadinessReceipt({ ...local, cluster: fixture('cluster').input.cluster, }), /cluster deployment evidence is forbidden/, ); const all = fixture('all').input; assert.throws( () => createDeploymentReadinessReceipt({ ...all, cluster: undefined, }), /cluster deployment catalog consumption is invalid/, ); }); test('rejects reports detached from the independently consumed catalog', () => { const current = fixture('all').input; const drifted = structuredClone(current); drifted.local.consumption.catalogManifestDigest = sha256('other-manifest'); drifted.local.consumption.immutableReference = `ghcr.io/${OWNER}/qinglong3-release-catalog@${drifted.local.consumption.catalogManifestDigest}`; drifted.local.edge.value.releaseAuthority.catalogManifestDigest = drifted.local.consumption.catalogManifestDigest; drifted.local.standalone.value.releaseAuthority.catalogManifestDigest = drifted.local.consumption.catalogManifestDigest; assert.throws( () => createDeploymentReadinessReceipt(drifted), /detached from the finalizer catalog/, ); }); test('rejects synthetic, incomplete and unclean deployment reports', () => { for (const mutate of [ (input) => { input.local.edge.value.releaseAuthority.mode = 'synthetic_live_fixture'; }, (input) => { input.local.standalone.value.gracefulCleanup = false; }, (input) => { input.cluster.report.value.receiptAuditCompatible = false; }, (input) => { input.cluster.report.value.cleanupComplete = false; }, ]) { const input = structuredClone(fixture('all').input); mutate(input); assert.throws( () => createDeploymentReadinessReceipt(input), /deployment evidence is invalid/, ); } }); test('rejects tampered, recomputed or authority-detached receipts', () => { const { receipt } = fixture('all'); const tampered = structuredClone(receipt); tampered.receiptDigest = sha256('tampered'); assert.throws( () => validateDeploymentReadinessReceipt(tampered), /receipt digest is invalid/, ); const recomputed = structuredClone(receipt); recomputed.evidence[0].unexpected = true; delete recomputed.receiptDigest; recomputed.receiptDigest = sha256(JSON.stringify(recomputed)); assert.throws( () => validateDeploymentReadinessReceipt(recomputed), /evidence summary is invalid/, ); assert.throws( () => validateDeploymentReadinessReceipt(receipt, { releaseSetDigest: sha256('another-release-set'), }), /authority is detached/, ); }); test('accepts only closed scope-specific CLI argument sets', () => { const common = [ '--mode=create', `--version=${VERSION}`, `--source-revision=${REVISION}`, `--source-ref=refs/tags/v${VERSION}`, '--release-scope=local', `--repository-owner=${OWNER}`, `--source-repository=${SOURCE_REPOSITORY}`, '--finalizer-consumption-bundle=/tmp/finalizer', '--local-consumption-bundle=/tmp/local', '--edge-report=/tmp/edge', '--standalone-report=/tmp/standalone', '--output=/tmp/receipt', ]; assert.equal(parseArguments(common).releaseScope, 'local'); assert.throws( () => parseArguments([...common, '--cluster-report=/tmp/cluster']), /arguments are invalid/, ); assert.throws( () => parseArguments(common.filter((entry) => !entry.startsWith('--edge'))), /arguments are invalid/, ); });