Files
2026-07-13 12:58:18 +08:00

136 lines
4.7 KiB
TypeScript

import { mkdtempSync, readFileSync, readdirSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { runRedeploy } from "../redeploy-env";
import { SERVICES } from "../railway-envs";
// A.4: per-service JSON summary contract.
//
// Shape (cross-workstream contract, consumed by showcase_deploy.yml's
// `enforce-redeploy-gate` job in A.7):
// Array<{ service: string; status: "ok" | "error"; error?: string }>
//
// When REDEPLOY_SUMMARY_JSON env var is set, runRedeploy MUST write the
// records array to that path atomically (stage to .tmp, rename). When
// unset, no JSON is written.
//
// PR #5093's exit-code contract MUST be preserved verbatim:
// - staging: exitCode === 0 even when all per-service redeploys fail
// - prod: exitCode === 1 on any per-service failure
// The JSON write happens BEFORE the exit-code computation; it never
// changes exit semantics on disk hiccups (write failures are warn-only).
describe("redeploy-env per-service JSON summary", () => {
let dir: string;
let summaryPath: string;
beforeEach(() => {
dir = mkdtempSync(join(tmpdir(), "redeploy-summary-"));
summaryPath = join(dir, "summary.json");
process.env.REDEPLOY_SUMMARY_JSON = summaryPath;
});
afterEach(() => {
delete process.env.REDEPLOY_SUMMARY_JSON;
rmSync(dir, { recursive: true, force: true });
});
it("writes one JSON record per attempted service with status", async () => {
const ag2ServiceId = SERVICES["showcase-ag2"].serviceId;
const summary = await runRedeploy({
env: "staging",
services: ["showcase-mastra", "showcase-ag2"],
appendSummary: () => {},
redeploy: async (serviceId) => {
if (serviceId === ag2ServiceId) {
return { ok: false, error: "boom" };
}
return { ok: true };
},
});
expect(summary.exitCode).toBe(0); // staging contract intact
const records = JSON.parse(readFileSync(summaryPath, "utf8")) as Array<{
service: string;
status: "ok" | "error";
error?: string;
}>;
expect(records).toEqual(
expect.arrayContaining([
{ service: "showcase-ag2", status: "error", error: "boom" },
{ service: "showcase-mastra", status: "ok" },
]),
);
expect(records).toHaveLength(2);
});
it("does NOT write JSON when REDEPLOY_SUMMARY_JSON is unset", async () => {
delete process.env.REDEPLOY_SUMMARY_JSON;
await runRedeploy({
env: "staging",
services: ["showcase-mastra"],
appendSummary: () => {},
redeploy: async () => ({ ok: true }),
});
// The temp dir we created is empty — assert nothing was written.
// (No path was given; the function had nowhere to write.)
expect(readdirSync(dir)).toEqual([]);
});
it("preserves PR #5093 contract: staging exits 0 even when all fail", async () => {
const summary = await runRedeploy({
env: "staging",
services: ["showcase-mastra", "showcase-ag2"],
appendSummary: () => {},
redeploy: async () => ({ ok: false, error: "boom" }),
});
expect(summary.exitCode).toBe(0);
expect(summary.failed).toBe(2);
const records = JSON.parse(readFileSync(summaryPath, "utf8")) as Array<{
service: string;
status: "ok" | "error";
error?: string;
}>;
expect(records.every((r) => r.status === "error")).toBe(true);
expect(records).toHaveLength(2);
});
it("preserves PR #5093 contract: prod exits 1 on any per-service failure", async () => {
const summary = await runRedeploy({
env: "prod",
services: ["showcase-mastra"],
appendSummary: () => {},
redeploy: async () => ({ ok: false, error: "boom" }),
});
expect(summary.exitCode).toBe(1);
const records = JSON.parse(readFileSync(summaryPath, "utf8")) as Array<{
service: string;
status: "ok" | "error";
error?: string;
}>;
expect(records).toEqual([
{ service: "showcase-mastra", status: "error", error: "boom" },
]);
});
it("records caught throws as status:error with the thrown message", async () => {
const summary = await runRedeploy({
env: "staging",
services: ["showcase-mastra"],
appendSummary: () => {},
redeploy: async () => {
throw new Error("kaboom");
},
});
expect(summary.exitCode).toBe(0);
expect(summary.failed).toBe(1);
const records = JSON.parse(readFileSync(summaryPath, "utf8")) as Array<{
service: string;
status: "ok" | "error";
error?: string;
}>;
expect(records).toEqual([
{ service: "showcase-mastra", status: "error", error: "kaboom" },
]);
});
});