"use strict"; const fs = require("node:fs/promises"); const os = require("node:os"); const path = require("node:path"); const crypto = require("node:crypto"); const { run } = require("./process-runner.cjs"); class ProductionAcceptanceHarness { constructor(root) { this.root = root; this.paths = { remote: path.join(root, "gitea", "owner", "app.git"), source: path.join(root, "workspace", "app"), server: path.join(root, "server", "appdata", "app"), releases: path.join(root, "releases"), config: path.join(root, "user-data", "forgeflow-config.json"), keys: path.join(root, "keys"), }; this.state = { installed: false, version: null, tokenVersion: 1, auth: null, liveSha: null, previousSha: null, healthy: false, deployment: null, recovery: null, hostFingerprint: "SHA256:fixture-host", keyReadOnly: true }; } static async create() { const root = await fs.mkdtemp(path.join(os.tmpdir(), "forgeflow-production-acceptance-")); const harness = new ProductionAcceptanceHarness(root); await harness.provision(); return harness; } async provision() { await Promise.all(Object.values(this.paths).filter((value) => !path.extname(value)).map((directory) => fs.mkdir(directory, { recursive: true }))); await fs.mkdir(path.dirname(this.paths.remote), { recursive: true }); await run("git", ["init", "--bare", this.paths.remote], { cwd: this.root, timeout: 30_000 }); await fs.mkdir(this.paths.source, { recursive: true }); await run("git", ["init", "-b", "main"], { cwd: this.paths.source, timeout: 30_000 }); await run("git", ["config", "user.name", "ForgeFlow Acceptance"], { cwd: this.paths.source }); await run("git", ["config", "user.email", "acceptance@example.invalid"], { cwd: this.paths.source }); await fs.writeFile(path.join(this.paths.source, "compose.yml"), "services:\n app:\n image: forgeflow-fixture:latest\n", "utf8"); await fs.writeFile(path.join(this.paths.source, "README.md"), "# Acceptance fixture\n", "utf8"); await run("git", ["add", "."], { cwd: this.paths.source }); await run("git", ["commit", "-m", "feat: initial fixture"], { cwd: this.paths.source }); await run("git", ["remote", "add", "origin", this.paths.remote], { cwd: this.paths.source }); await run("git", ["push", "-u", "origin", "main"], { cwd: this.paths.source, timeout: 30_000 }); this.initialSha = (await run("git", ["rev-parse", "HEAD"], { cwd: this.paths.source })).stdout.trim(); await fs.mkdir(this.paths.releases, { recursive: true }); await fs.mkdir(path.dirname(this.paths.config), { recursive: true }); await fs.mkdir(this.paths.keys, { recursive: true }); await fs.writeFile(path.join(this.paths.keys, "deploy_key.pub"), "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFixture forgeflow-acceptance\n", "utf8"); } async cleanup() { await fs.rm(this.root, { recursive: true, force: true }); } async install(version = "0.10.0", mode = "installed") { this.state.installed = true; this.state.version = version; this.state.mode = mode; await this.saveConfig({ schemaVersion: 12, version, mode }); return structuredClone(this.state); } async migrate(targetVersion = "1.0.0") { if (!this.state.installed) throw new Error("Clean installation is required before migration."); const previous = JSON.parse(await fs.readFile(this.paths.config, "utf8")); await fs.writeFile(`${this.paths.config}.backup`, JSON.stringify(previous, null, 2), "utf8"); this.state.version = targetVersion; await this.saveConfig({ ...previous, schemaVersion: 13, version: targetVersion, migratedAt: new Date().toISOString() }); return { previousVersion: previous.version, version: targetVersion, backup: `${this.paths.config}.backup` }; } async saveConfig(data) { await fs.writeFile(this.paths.config, `${JSON.stringify(data, null, 2)}\n`, "utf8"); } rotateToken() { this.state.tokenVersion += 1; return { tokenVersion: this.state.tokenVersion }; } authenticate(type, options = {}) { if (!['password', 'ssh-key'].includes(type)) throw new Error("Unsupported authentication fixture."); if (type === 'ssh-key' && options.hostFingerprint !== this.state.hostFingerprint) throw new Error("SSH host fingerprint changed."); this.state.auth = type; return { authenticated: true, type }; } setKeyAccess(readOnly) { this.state.keyReadOnly = readOnly; } async createCommit(message = "fix: acceptance change") { const target = path.join(this.paths.source, "fixture.txt"); await fs.writeFile(target, `${crypto.randomUUID()}\n`, "utf8"); await run("git", ["add", "fixture.txt"], { cwd: this.paths.source }); await run("git", ["commit", "-m", message], { cwd: this.paths.source }); await run("git", ["push", "origin", "main"], { cwd: this.paths.source }); return (await run("git", ["rev-parse", "HEAD"], { cwd: this.paths.source })).stdout.trim(); } plan(sha, mode = "server-git") { return { id: crypto.randomUUID(), evidenceHash: crypto.createHash("sha256").update(JSON.stringify({ sha, mode, liveSha: this.state.liveSha, keyReadOnly: this.state.keyReadOnly })).digest("hex"), sha, mode, previousSha: this.state.liveSha }; } async deploy(plan, fault = null) { if (!this.state.auth) throw new Error("Server authentication is required."); if (plan.mode === "server-git" && !this.state.keyReadOnly) throw new Error("Writable deploy key rejected."); if (this.plan(plan.sha, plan.mode).evidenceHash !== plan.evidenceHash) throw new Error("Stale reconciliation plan."); this.state.recovery = structuredClone(this.state); this.state.deployment = { id: crypto.randomUUID(), sha: plan.sha, mode: plan.mode, status: "running" }; if (fault === "fetch-network") return this.fail("Network interrupted during fetch", false); await fs.mkdir(this.paths.server, { recursive: true }); await fs.writeFile(path.join(this.paths.server, "compose.yml"), await fs.readFile(path.join(this.paths.source, "compose.yml"))); if (fault === "activation-network") return this.fail("Network interrupted during activation", true); if (fault === "shutdown") { this.state.deployment.status = "interrupted"; return structuredClone(this.state.deployment); } this.state.previousSha = this.state.liveSha; this.state.liveSha = plan.sha; this.state.healthy = fault !== "unhealthy"; this.state.deployment.status = this.state.healthy ? "success" : "failed"; return structuredClone(this.state.deployment); } fail(message, partial) { this.state.deployment.status = "failed"; this.state.deployment.failure = { message, partial }; return structuredClone(this.state.deployment); } recover() { if (this.state.deployment?.status !== "interrupted") throw new Error("No interrupted deployment to recover."); this.state.deployment.status = this.state.liveSha === this.state.deployment.sha && this.state.healthy ? "success" : "failed"; return structuredClone(this.state.deployment); } rollback(targetSha) { if (!targetSha || targetSha !== this.state.previousSha) throw new Error("Rollback target is not the exact recorded previous SHA."); [this.state.liveSha, this.state.previousSha] = [targetSha, this.state.liveSha]; this.state.healthy = true; return { status: "rolled-back", liveSha: this.state.liveSha }; } adoptExisting(sha = this.initialSha) { this.state.liveSha = sha; this.state.healthy = true; return { linked: true, liveSha: sha, preserved: true }; } externalUpdate(sha) { this.state.liveSha = sha; this.state.healthy = true; return { reconciled: true, liveSha: sha }; } rotateDeployKey() { if (!this.state.keyReadOnly) throw new Error("Candidate deploy key is writable."); this.state.keyVersion = (this.state.keyVersion || 1) + 1; return { rotated: true, keyVersion: this.state.keyVersion }; } revokeDeployKey() { this.state.keyRevoked = true; return { revoked: true, deploymentBlocked: true }; } restoreDeployKey() { this.state.keyRevoked = false; this.state.keyReadOnly = true; return { restored: true }; } inventory(count = 20, partial = false) { return { workloads: Array.from({ length: count }, (_, index) => ({ id: `workload-${index + 1}`, classification: index === 1 ? "duplicate" : "active" })), partial, warnings: partial ? ["One scan root was unavailable"] : [] }; } async publishRelease(version, options = {}) { const binary = Buffer.from(options.binary || "MZ-forgeflow-acceptance-binary"); const name = `ForgeFlow-Portable-${version}-win-x64.exe`; const checksum = crypto.createHash("sha256").update(binary).digest("hex"); const manifest = { version, draft: options.draft === true, assets: options.missingAsset ? [] : [{ name, sha256: options.badChecksum ? "0".repeat(64) : checksum }], provenance: { commitSha: options.commitSha || this.initialSha }, sbom: { bomFormat: "CycloneDX" } }; await fs.writeFile(path.join(this.paths.releases, `${version}.json`), JSON.stringify(manifest, null, 2)); if (!options.missingAsset) await fs.writeFile(path.join(this.paths.releases, name), binary); return manifest; } async verifyRelease(version) { const manifest = JSON.parse(await fs.readFile(path.join(this.paths.releases, `${version}.json`), "utf8")); if (manifest.draft) throw new Error("Incomplete draft release rejected."); const asset = manifest.assets[0]; if (!asset) throw new Error("Required release asset is missing."); const binary = await fs.readFile(path.join(this.paths.releases, asset.name)); if (crypto.createHash("sha256").update(binary).digest("hex") !== asset.sha256) throw new Error("Release checksum mismatch."); if (!manifest.provenance?.commitSha || manifest.sbom?.bomFormat !== "CycloneDX") throw new Error("Release provenance or SBOM is missing."); return { verified: true, version, asset: asset.name }; } } module.exports = { ProductionAcceptanceHarness };