fix: 修复迁移预检状态误判(09-06-release-gate-integrity)
这个提交包含在:
@@ -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");
|
||||
|
||||
在新工单中引用
屏蔽一个用户