const { runInitialization, inspectRequiredConfiguration } = require("../../../src/scripts/deploy-init"); const { spawnSync } = require("node:child_process"); const path = require("node:path"); const fs = require("node:fs"); const YAML = require("yaml"); const os = require("node:os"); const checks = [ { code: "expense_types", count: 2 }, { code: "approval_rules", count: 1 }, { code: "reimbursement_workflow", count: 1 }, ]; describe("deployment init mode separation", () => { it.each(["valid", "missing", "unsafe_default", "wrong_service"])("enforces the init-only environment contract: %s", (scenario) => { const root = path.resolve(__dirname, "../../../.."); const directory = fs.mkdtempSync(path.join(os.tmpdir(), "hyb-seed-env-")); try { let content = fs.readFileSync(path.join(root, "deploy/trialrun/docker-compose.yml"), "utf8"); if (scenario === "missing") content = content.replace(' DEPLOY_DATA_MODE: ${DEPLOY_DATA_MODE:-}\n', ""); if (scenario === "unsafe_default") content = content.replace('${DEPLOY_DATA_MODE:-}', '${DEPLOY_DATA_MODE:-initialize}'); if (scenario === "wrong_service") content = content.replace(" NODE_ENV: production", " NODE_ENV: production\n DEPLOY_DATA_MODE: existing"); const file = path.join(directory, "compose.yml"); fs.writeFileSync(file, content); const result = spawnSync(process.execPath, [path.join(root, "deploy/trialrun/validate-env-contract.js")], { env: { ...process.env, ENV_CONTRACT_COMPOSE_PATH: file }, encoding: "utf8", }); expect(result.status).toBe(scenario === "valid" ? 0 : 1); if (scenario !== "valid") expect(result.stdout + result.stderr).toContain("DEPLOY_DATA_MODE"); } finally { fs.rmSync(directory, { recursive: true, force: true }); } }); it.each(["existing", "initialize"])("executes the complete %s path in order", async (mode) => { const calls = []; const report = vi.fn(); await runInitialization({ mode, report, runCommand: async (command, args) => calls.push(args.join(" ")), checkConfiguration: async () => { calls.push("check"); return checks; }, }); expect(calls).toEqual([ '-e require("./src/config").getConfig()', "run prisma:migrate:deploy", ...(mode === "initialize" ? ["run seed:config", "run seed:trialrun"] : []), "check", ]); expect(report).toHaveBeenLastCalledWith({ status: "INIT_PASS", mode, checks }); }); it.each([undefined, "", "repair", " existing", "INITIALIZE"])("rejects invalid mode %s before any access", async (mode) => { const runCommand = vi.fn(); const checkConfiguration = vi.fn(); await expect(runInitialization({ mode, runCommand, checkConfiguration })).rejects.toThrow("deployment.data_mode_required"); expect(runCommand).not.toHaveBeenCalled(); expect(checkConfiguration).not.toHaveBeenCalled(); }); it.each([0, 1, 2, 3])("stops initialize when subcommand %s fails", async (failureIndex) => { const runCommand = vi.fn().mockImplementation(async () => { if (runCommand.mock.calls.length === failureIndex + 1) throw new Error("synthetic_failure"); }); const checkConfiguration = vi.fn(); const report = vi.fn(); await expect(runInitialization({ mode: "initialize", runCommand, checkConfiguration, report })).rejects.toThrow("synthetic_failure"); expect(runCommand).toHaveBeenCalledTimes(failureIndex + 1); expect(checkConfiguration).not.toHaveBeenCalled(); expect(report.mock.calls.some(([value]) => value.status === "INIT_PASS")).toBe(false); }); it.each(checks.map((check) => check.code))("stops for missing %s without repair", async (missingCode) => { const runCommand = vi.fn(); const report = vi.fn(); await expect(runInitialization({ mode: "existing", runCommand, report, checkConfiguration: async () => checks.map((check) => ({ ...check, count: check.code === missingCode ? 0 : check.count })), })).rejects.toThrow("deployment.required_configuration_missing"); expect(runCommand).toHaveBeenCalledTimes(2); expect(report.mock.calls.some(([value]) => value.status === "INIT_PASS")).toBe(false); }); it("does not succeed or seed on a read failure", async () => { const runCommand = vi.fn(); const report = vi.fn(); await expect(runInitialization({ mode: "existing", runCommand, report, checkConfiguration: async () => { throw new Error("query_failure"); }, })).rejects.toThrow("query_failure"); expect(runCommand).toHaveBeenCalledTimes(2); expect(report.mock.calls.some(([value]) => value.status === "INIT_PASS")).toBe(false); }); it("uses a bounded read-only transaction and the existing route predicates", async () => { const transaction = { $executeRaw: vi.fn().mockResolvedValue(0), expenseType: { count: vi.fn().mockResolvedValue(4) }, approvalRule: { count: vi.fn().mockResolvedValue(2) }, workflowDefinition: { count: vi.fn().mockResolvedValue(1) }, }; const prisma = { $transaction: vi.fn(async (callback) => callback(transaction)) }; const result = await inspectRequiredConfiguration(prisma); expect(transaction.$executeRaw.mock.calls[0][0].join("")).toBe("SET TRANSACTION READ ONLY"); expect(transaction.expenseType.count).toHaveBeenCalledWith({ where: { enabled: true } }); expect(transaction.approvalRule.count).toHaveBeenCalledWith({ where: { enabled: true } }); expect(transaction.workflowDefinition.count).toHaveBeenCalledWith({ where: { entity_type: "reimbursement", is_active: true } }); expect(prisma.$transaction.mock.calls[0][1]).toEqual({ isolationLevel: "RepeatableRead", maxWait: 5000, timeout: 10000 }); expect(result.map((check) => check.count)).toEqual([4, 2, 1]); }); it("shares one entry with Compose and has no implicit initialize default", () => { const root = path.resolve(__dirname, "../../../.."); const compose = YAML.parse(fs.readFileSync(path.join(root, "deploy/trialrun/docker-compose.yml"), "utf8")); expect(compose.services.init.command).toEqual(["cd /app/server && npm run deploy:init"]); expect(compose.services.init.environment.DEPLOY_DATA_MODE).toBe("${DEPLOY_DATA_MODE:-}"); const result = spawnSync(process.execPath, ["src/scripts/deploy-init.js"], { cwd: path.join(root, "server"), env: { PATH: process.env.PATH, DEPLOY_DATA_MODE: "" }, encoding: "utf8", }); expect(result.status).toBe(1); expect(JSON.parse(result.stderr).code).toBe("deployment.data_mode_required"); expect(result.stdout).toBe(""); }); it("passes the whole Compose command to the shell and preserves child failure", () => { const root = path.resolve(__dirname, "../../../.."); const compose = YAML.parse(fs.readFileSync(path.join(root, "deploy/trialrun/docker-compose.yml"), "utf8")); const directory = fs.mkdtempSync(path.join(os.tmpdir(), "hyb-seed-argv-")); try { fs.writeFileSync(path.join(directory, "npm"), '#!/bin/sh\n[ "$1" = run ] && [ "$2" = deploy:init ] && [ "$DEPLOY_DATA_MODE" = existing ] || exit 99\nprintf ENTRY_REACHED\nexit 17\n', { mode: 0o700 }); const command = compose.services.init.command.map((argument) => argument.replace("/app/server", directory)); const [executable, ...argumentsBeforeCommand] = compose.services.init.entrypoint; const result = spawnSync(executable, [...argumentsBeforeCommand, ...command], { env: { PATH: directory + ":" + process.env.PATH, DEPLOY_DATA_MODE: "existing" }, encoding: "utf8", }); expect(result.stdout).toBe("ENTRY_REACHED"); expect(result.status).toBe(17); } finally { fs.rmSync(directory, { recursive: true, force: true }); } }); });