完善 readiness、发布追溯与敏感治理(07-15-production-trust-closeout-cleanup)
这个提交包含在:
@@ -0,0 +1,178 @@
|
||||
const { execFile } = require("node:child_process");
|
||||
const { mkdtemp, readFile, rm } = require("node:fs/promises");
|
||||
const http = require("node:http");
|
||||
const os = require("node:os");
|
||||
const path = require("node:path");
|
||||
const { promisify } = require("node:util");
|
||||
const { pathToFileURL } = require("node:url");
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
const ROOT_DIR = path.resolve(__dirname, "../../../..");
|
||||
let readinessReport;
|
||||
|
||||
function passSteps() {
|
||||
return Object.fromEntries(
|
||||
readinessReport.READINESS_STEP_NAMES.map((name) => [name, {
|
||||
status: "PASS",
|
||||
kind: "required",
|
||||
findingCount: 0,
|
||||
logRef: `artifacts/qa-flow/test/${name}.log`,
|
||||
}]),
|
||||
);
|
||||
}
|
||||
|
||||
function listen(server) {
|
||||
return new Promise((resolve, reject) => {
|
||||
server.once("error", reject);
|
||||
server.listen(0, "127.0.0.1", () => {
|
||||
server.off("error", reject);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function close(server) {
|
||||
return new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
|
||||
}
|
||||
|
||||
const EXPECTED_BUILD_SHA = "abcdef123456";
|
||||
|
||||
describe("live smoke readiness aggregation", () => {
|
||||
beforeAll(async () => {
|
||||
const scriptPath = path.join(ROOT_DIR, "scripts/qa/live-smoke-readiness-report.mjs");
|
||||
readinessReport = await import(pathToFileURL(scriptPath).href);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["missing database", "pendingOcr"],
|
||||
["missing login credentials", "liveSmoke"],
|
||||
["missing OCR sample", "liveOcr"],
|
||||
])("propagates %s as PASS_WITH_FINDINGS", (_label, stepName) => {
|
||||
const steps = passSteps();
|
||||
steps[stepName] = {
|
||||
...steps[stepName],
|
||||
status: "SKIP",
|
||||
findingCount: 1,
|
||||
};
|
||||
|
||||
const result = readinessReport.evaluateReadinessSteps(steps);
|
||||
|
||||
expect(result.status).toBe("PASS_WITH_FINDINGS");
|
||||
expect(result.findingCount).toBeGreaterThan(0);
|
||||
expect(result.findings).toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({ source: stepName, severity: "manual_or_config" }),
|
||||
]));
|
||||
});
|
||||
|
||||
it("reports clean PASS only when every prerequisite and step is ready", () => {
|
||||
expect(readinessReport.evaluateReadinessSteps(passSteps())).toMatchObject({
|
||||
status: "PASS",
|
||||
findingCount: 0,
|
||||
findings: [],
|
||||
});
|
||||
});
|
||||
|
||||
it("treats a required command failure as a blocker", () => {
|
||||
const steps = passSteps();
|
||||
steps.publicReady = { ...steps.publicReady, status: "FAIL", findingCount: 1 };
|
||||
|
||||
expect(readinessReport.evaluateReadinessSteps(steps)).toMatchObject({
|
||||
status: "FAIL",
|
||||
findingCount: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it("runs the shell parent against a local public stub and writes a redacted non-clean summary", async () => {
|
||||
const reportDir = await mkdtemp(path.join(os.tmpdir(), "hyb-oa-readiness-test-"));
|
||||
const server = http.createServer((request, response) => {
|
||||
response.setHeader("content-type", "application/json; charset=utf-8");
|
||||
if (request.url === "/ready") {
|
||||
response.end(JSON.stringify({ data: { ok: true, buildSha: EXPECTED_BUILD_SHA } }));
|
||||
return;
|
||||
}
|
||||
if (request.url === "/api/auth/wecom/config") {
|
||||
response.end(JSON.stringify({
|
||||
data: {
|
||||
status: "ready",
|
||||
entryEnabled: true,
|
||||
oauthEnabled: true,
|
||||
defaultReturnTo: "/portal",
|
||||
loginUrl: "/api/auth/wecom/login",
|
||||
},
|
||||
}));
|
||||
return;
|
||||
}
|
||||
if (request.url === "/api/auth/capabilities") {
|
||||
response.end(JSON.stringify({
|
||||
data: {
|
||||
emailDelivery: { ready: true, statusReasons: [] },
|
||||
emailLoginCode: { enabled: true },
|
||||
passwordReset: { enabled: true },
|
||||
},
|
||||
}));
|
||||
return;
|
||||
}
|
||||
if (request.url === "/") {
|
||||
response.setHeader("content-type", "text/html; charset=utf-8");
|
||||
response.end('<!doctype html><html><body>OA<script src="/assets/index-test.js"></script></body></html>');
|
||||
return;
|
||||
}
|
||||
if (request.url === "/assets/index-test.js") {
|
||||
response.setHeader("content-type", "application/javascript; charset=utf-8");
|
||||
response.end(`document.documentElement.dataset.buildSha="${EXPECTED_BUILD_SHA}";`);
|
||||
return;
|
||||
}
|
||||
response.statusCode = 404;
|
||||
response.end(JSON.stringify({ error: "not_found" }));
|
||||
});
|
||||
|
||||
try {
|
||||
await listen(server);
|
||||
const address = server.address();
|
||||
const env = { ...process.env };
|
||||
for (const key of [
|
||||
"DATABASE_URL",
|
||||
"LIVE_SMOKE_ADMIN_IDENTIFIER",
|
||||
"LIVE_SMOKE_ADMIN_PASSWORD",
|
||||
"LIVE_SMOKE_EMPLOYEE_IDENTIFIER",
|
||||
"LIVE_SMOKE_EMPLOYEE_PASSWORD",
|
||||
"PLAYWRIGHT_OCR_LIVE_IDENTIFIER",
|
||||
"PLAYWRIGHT_OCR_LIVE_PASSWORD",
|
||||
"PLAYWRIGHT_OCR_LIVE_FILE",
|
||||
"PLAYWRIGHT_OCR_LIVE_DIR",
|
||||
"LIVE_OCR_INVOICE_FILE",
|
||||
]) {
|
||||
delete env[key];
|
||||
}
|
||||
env.LIVE_SMOKE_BASE_URL = `http://127.0.0.1:${address.port}`;
|
||||
env.LIVE_SMOKE_READINESS_DIR = reportDir;
|
||||
env.LIVE_SMOKE_EXPECTED_BUILD_SHA = EXPECTED_BUILD_SHA;
|
||||
env.QA_ISSUE_ID = "READINESS-TEST";
|
||||
|
||||
const result = await execFileAsync("bash", [path.join(ROOT_DIR, "scripts/qa/run-live-smoke-readiness.sh")], {
|
||||
cwd: ROOT_DIR,
|
||||
env,
|
||||
maxBuffer: 2 * 1024 * 1024,
|
||||
});
|
||||
const summaryText = await readFile(path.join(reportDir, "summary.json"), "utf8");
|
||||
const summary = JSON.parse(summaryText);
|
||||
|
||||
expect(result.stdout).toContain("[live-readiness][PASS_WITH_FINDINGS]");
|
||||
expect(summary.status).toBe("PASS_WITH_FINDINGS");
|
||||
expect(summary.findingCount).toBeGreaterThan(0);
|
||||
expect(summary.steps.pendingOcr.status).toBe("SKIP");
|
||||
expect(summary.steps.liveSmoke.status).toBe("SKIP");
|
||||
expect(summary.steps.liveOcr.status).toBe("SKIP");
|
||||
expect(summary.steps.privacy.status).toBe("PASS");
|
||||
expect(summary.expectedBuildSha).toBe(EXPECTED_BUILD_SHA);
|
||||
expect(summary.observedBuildSha).toBe(EXPECTED_BUILD_SHA);
|
||||
expect(summary.buildShaMatches).toBe(true);
|
||||
expect(summary.childReports.pendingOcr.findingCount).toBeGreaterThan(0);
|
||||
expect(summaryText).not.toContain(ROOT_DIR);
|
||||
expect(summaryText).not.toMatch(/DATABASE_URL|LIVE_SMOKE_ADMIN_PASSWORD|PLAYWRIGHT_OCR_LIVE_FILE/);
|
||||
} finally {
|
||||
await close(server);
|
||||
await rm(reportDir, { recursive: true, force: true });
|
||||
}
|
||||
}, 30_000);
|
||||
});
|
||||
在新工单中引用
屏蔽一个用户