fix: 修复迁移预检状态误判(09-06-release-gate-integrity)

这个提交包含在:
2026-09-09 12:47:29 +08:00
父节点 b9185308a6
当前提交 81102c26cf
共修改 6 个文件,包含 189 行新增和 7 行删除
@@ -1,4 +1,4 @@
const { execFile, execFileSync } = require("node:child_process");
const { execFile, execFileSync, spawnSync } = require("node:child_process");
const { createHash } = require("node:crypto");
const { chmod, mkdtemp, mkdir, rm, symlink, writeFile, readFile } = require("node:fs/promises");
const os = require("node:os");
@@ -17,6 +17,95 @@ const PRODUCTION_PREFLIGHT_REMOTE = path.join(ROOT_DIR, "deploy/trialrun/product
const PRODUCTION_IDENTITY_BOOTSTRAP = path.join(ROOT_DIR, "deploy/trialrun/production-identity-bootstrap.mjs");
const PRODUCTION_IDENTITY_BOOTSTRAP_REMOTE = path.join(ROOT_DIR, "deploy/trialrun/production-identity-bootstrap-remote.sh");
describe("migration status predicate fail-closed regression", () => {
const known = "20260807193000_contract_registry_termination_workspace";
const unknown = "20260807193002_unknown_migration";
function runStatus(text, { statusCode = 0, grepFailure = false, oldPredicate = false } = {}) {
const source = require("node:fs").readFileSync(PRODUCTION_PREFLIGHT_REMOTE, "utf8");
const start = source.indexOf("\nstatus_matches() {");
const end = source.indexOf("\nmigration_rows=", start);
expect(start).toBeGreaterThan(0);
expect(end).toBeGreaterThan(start);
let parsing = source.slice(start, end);
if (oldPredicate) {
const target = "if status_matches 'have not yet been applied'; then";
expect(parsing.split(target)).toHaveLength(2);
parsing = parsing.replace(target, "if printf '%s' \"$prisma_status_output\" | grep -Eiq 'have not yet been applied'; then");
}
const guardStart = source.indexOf('if [[ "${#status_pending_ids[@]}" -gt 0 ]]; then', end);
const guardEnd = source.indexOf("\npending_ids=()", guardStart);
expect(guardStart).toBeGreaterThan(end);
expect(guardEnd).toBeGreaterThan(guardStart);
const script = [
"set -euo pipefail",
'fail() { echo "$1"; exit 1; }',
'prisma_status_output="$(cat)"',
"prisma_status_code=" + statusCode,
"declare -A candidate_set=([" + known + "]=1)",
'printf() { builtin printf "$@"; if [[ "$1" == "%s" && "${2:-}" == "$prisma_status_output" ]]; then return 141; fi; }',
"matcher_calls=0",
grepFailure ? 'grep() { matcher_calls=$((matcher_calls + 1)); if [[ "$matcher_calls" -eq ' + grepFailure + ' ]]; then return 2; fi; command grep "$@"; }' : "",
parsing,
source.slice(guardStart, guardEnd),
"echo ACCEPTED",
].join("\n");
return spawnSync("bash", ["-c", script], {
input: text, encoding: "utf8", timeout: 10000, maxBuffer: 1024 * 1024,
env: { PATH: process.env.PATH },
});
}
it.each(["Database schema is up to date!", "No pending migrations", "DATABASE SCHEMA IS UP TO DATE!"])("accepts recognized safe status: %s", (text) => {
const result = runStatus(text);
expect(result.status).toBe(0);
expect(result.stdout).toContain("ACCEPTED");
});
it("accepts an allowlisted pending migration with Prisma exit one", () => {
const result = runStatus("Following migration have not yet been applied:\n" + known, { statusCode: 1 });
expect(result.status).toBe(0);
});
it.each([0, 96 * 1024])("rejects unknown pending despite producer 141 with padding %s", (padding) => {
const text = "Following migration have not yet been applied:\n" + unknown + "\n" + "x".repeat(padding);
const result = runStatus(text, { statusCode: 1 });
expect(result.status).toBe(1);
expect(result.stdout).toContain("PROD_MIGRATION_STATUS_UNKNOWN_PENDING");
expect(result.stdout).not.toContain("ACCEPTED");
});
it("demonstrates the old predicate mutation loses unknown-pending rejection", () => {
const text = "Following migration have not yet been applied:\n" + unknown;
const result = runStatus(text, { statusCode: 1, oldPredicate: true });
expect(result.status).toBe(0);
expect(result.stdout).toContain("ACCEPTED");
expect(runStatus(text, { statusCode: 1 }).stdout).toContain("PROD_MIGRATION_STATUS_UNKNOWN_PENDING");
});
it.each([
["Database schema is up to date!\nmigration failed", "PROD_MIGRATION_STATUS_UNSAFE"],
["unrecognized status", "PROD_MIGRATION_STATUS_UNREADABLE"],
["Following migration have not yet been applied:", "PROD_MIGRATION_STATUS_PENDING_IDS_UNREADABLE"],
])("fails closed on %s", (text, code) => {
const result = runStatus(text);
expect(result.status).toBe(1);
expect(result.stdout).toContain(code);
});
it.each([1, 2, 3, 4])("fails closed on matcher error at predicate %s", (grepFailure) => {
const result = runStatus("Following migration have not yet been applied:\n" + unknown, { grepFailure, statusCode: 1 });
expect(result.status).toBe(1);
expect(result.stdout).toContain("PROD_MIGRATION_STATUS_UNREADABLE");
});
it("rejects a nonzero status without pending migrations", () => {
const result = runStatus("Database schema is up to date!", { statusCode: 1 });
expect(result.status).toBe(1);
expect(result.stdout).toContain("PROD_MIGRATION_STATUS_UNREADABLE");
});
});
describe("deployment safety contract", () => {
let fixtureRoot;
let remoteDir;
@@ -724,7 +813,7 @@ describe("deployment safety contract", () => {
await expect(runRemote("happy", "", { VITE_BUILD_SHA: "b".repeat(12) })).rejects.toMatchObject({
stderr: expect.stringContaining("PROD_TARGET_BUILD_SHA_MISMATCH"),
});
});
}, 15000);
it("aggregates preflight checks and keeps the root helper self-contained", async () => {
const aggregate = await require("node:fs/promises").readFile(AGGREGATE_PREFLIGHT, "utf8");