/* Unit tests for deploy helpers covering deploy-helpers; resolveArtifactHash; makeR2ConfigFromEnv. It also guards the documented public contract and regression-sensitive edge cases. Not a contract source of truth — the module under test owns the authoritative behavior; fixtures are minimal scaffolding only. RFC-1311: Compass header sweep — declares the deploy suite's contract for test-file scaffold coverage. */ import { describe, it, expect, beforeEach, afterEach } from "vitest"; import fs from "node:fs/promises"; import path from "node:path"; import os from "node:os"; import { authorizeAndDeploy, resolveArtifactHash, makeR2ConfigFromEnv, buildEffectRecord, writeDeploymentEffectRecord, } from "../deploy-helpers.ts"; import type { Sha256Digest } from "@warpgogol/werkstatt-shared/fingerprint"; const validGateDecision = { schema: "werkstatt/gate-decision@1", decisionId: "dec-001", candidateId: "test-candidate", policyBundleRoot: "sha256:" + "c".repeat(64), gate: "dev", evaluationCut: 1, selectedEvidence: [], status: "pass", coverage: { schema: "werkstatt/coverage-report@1", totalRequirements: 9, coveredRequirements: 9, uncoveredRequirements: [], }, reasons: ["all requirements passed"], actionPackRef: null, decidedAt: "2026-01-01T00:00:00Z", }; const _validMainVerificationDecision = { schema: "werkstatt/main-verification-decision@1", decisionId: "dec-main-001", candidateId: "test-candidate", policyBundleRoot: "sha256:" + "c".repeat(64), gate: "main", evaluationCut: 1, selectedEvidence: [], status: "pass", coverage: { schema: "werkstatt/coverage-report@1", totalRequirements: 9, coveredRequirements: 9, uncoveredRequirements: [], }, reasons: ["all requirements passed"], actionPackRef: null, rootDossierRef: "sha256:" + "b".repeat(64), priorOperationRef: null, decidedAt: "2026-01-01T00:00:00Z", }; const testArtifactHash = ("sha256:" + "a".repeat(64)) as Sha256Digest; describe("deploy-helpers", () => { let tmpDir: string; beforeEach(async () => { tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "deploy-test-")); }); afterEach(async () => { await fs.rm(tmpDir, { recursive: true, force: true }); }); describe("resolveArtifactHash", () => { it("returns hash from flag when valid", async () => { const { hash, source } = await resolveArtifactHash(testArtifactHash, undefined); expect(hash).toBe(testArtifactHash); expect(source).toBe("flag"); }); it("throws when flag is invalid and no release dir", async () => { await expect(resolveArtifactHash("invalid", undefined)).rejects.toThrow( "artifact hash not provided", ); }); it("computes hash from artifact.tar.gz in release dir", async () => { const releaseDir = path.join(tmpDir, "releases", "rel-001"); await fs.mkdir(releaseDir, { recursive: true }); const artifactPath = path.join(releaseDir, "artifact.tar.gz"); await fs.writeFile(artifactPath, "test artifact content"); const { hash, source, path: resolvedPath } = await resolveArtifactHash(undefined, releaseDir); expect(hash).toMatch(/^sha256:[0-9a-f]{64}$/); expect(source).toBe("artifact.tar.gz"); expect(resolvedPath).toBe(artifactPath); }); it("reads distTreeHash from release.yaml when artifact.tar.gz is absent", async () => { const releaseDir = path.join(tmpDir, "releases", "rel-002"); await fs.mkdir(releaseDir, { recursive: true }); const releaseYamlPath = path.join(releaseDir, "release.yaml"); await fs.writeFile(releaseYamlPath, `distTreeHash: ${testArtifactHash}\n`); const { hash, source, path: resolvedPath } = await resolveArtifactHash(undefined, releaseDir); expect(hash).toBe(testArtifactHash); expect(source).toBe("release.yaml"); expect(resolvedPath).toBe(releaseYamlPath); }); }); describe("makeR2ConfigFromEnv", () => { it("returns null when any required env var is missing", () => { expect(makeR2ConfigFromEnv({})).toBeNull(); expect(makeR2ConfigFromEnv({ R2_ACCOUNT_ID: "a" })).toBeNull(); expect( makeR2ConfigFromEnv({ R2_ACCOUNT_ID: "a", R2_ACCESS_KEY_ID: "b", R2_SECRET_ACCESS_KEY: "c", }), ).toBeNull(); }); it("returns config when all env vars are present", () => { const config = makeR2ConfigFromEnv({ R2_ACCOUNT_ID: "account", R2_ACCESS_KEY_ID: "key", R2_SECRET_ACCESS_KEY: "secret", R2_BUCKET_NAME: "bucket", }); expect(config).toEqual({ accountId: "account", accessKeyId: "key", secretAccessKey: "secret", bucketName: "bucket", }); }); }); describe("authorizeAndDeploy", () => { it("authorizes when gate decision passes", async () => { const gateDecisionPath = path.join(tmpDir, "gate-decision.json"); await fs.writeFile(gateDecisionPath, JSON.stringify(validGateDecision)); const result = await authorizeAndDeploy({ gateDecisionPath, artifactHash: testArtifactHash, candidateId: "test-candidate", gate: "dev-deploy", durableSyncVerified: false, artifactReadinessVerified: true, forceRequested: false, skipRequested: false, waiverRequested: false, graceRequested: false, }); expect(result.ok).toBe(true); if (result.ok) { expect(result.outcome.authorized).toBe(true); expect(result.outcome.gate).toBe("dev-deploy"); expect(result.outcome.channel).toBe("dev"); } }); it("fails when gate decision file is missing", async () => { await expect( authorizeAndDeploy({ gateDecisionPath: path.join(tmpDir, "nonexistent.json"), artifactHash: testArtifactHash, candidateId: "test-candidate", gate: "dev-deploy", durableSyncVerified: false, artifactReadinessVerified: true, forceRequested: false, skipRequested: false, waiverRequested: false, graceRequested: false, }), ).rejects.toThrow("gate decision file not found"); }); }); describe("buildEffectRecord", () => { it("creates a record with correct schema", () => { const record = buildEffectRecord( "op-001", "candidate-001", "dev-deploy", "dev", testArtifactHash, "dec-001", false, null, "authorized", "2026-01-01T00:00:00Z", ); expect(record.schema).toBe("werkstatt/deployment-effect-record@1"); expect(record.operationId).toBe("op-001"); expect(record.candidateId).toBe("candidate-001"); expect(record.gate).toBe("dev-deploy"); expect(record.channel).toBe("dev"); expect(record.artifactHash).toBe(testArtifactHash); expect(record.decisionId).toBe("dec-001"); expect(record.durableSyncVerified).toBe(false); expect(record.mainVerificationDecisionId).toBeNull(); expect(record.state).toBe("authorized"); expect(record.effectHash).toMatch(/^sha256:[0-9a-f]{64}$/); }); }); describe("writeDeploymentEffectRecord", () => { it("writes record to deployment-operations directory", async () => { const record = buildEffectRecord( "op-001", "candidate-001", "dev-deploy", "dev", testArtifactHash, "dec-001", false, null, "authorized", "2026-01-01T00:00:00Z", ); const filePath = await writeDeploymentEffectRecord(tmpDir, record); expect(filePath).toContain("deployment-operations"); expect(filePath).toContain("op-001.json"); const content = await fs.readFile(filePath, "utf8"); const parsed = JSON.parse(content); expect(parsed.operationId).toBe("op-001"); }); }); });