完善 readiness、发布追溯与敏感治理(07-15-production-trust-closeout-cleanup)
这个提交包含在:
@@ -0,0 +1,85 @@
|
||||
const path = require("node:path");
|
||||
const { pathToFileURL } = require("node:url");
|
||||
|
||||
let pendingOcr;
|
||||
|
||||
describe("pending OCR readiness contract", () => {
|
||||
beforeAll(async () => {
|
||||
const scriptPath = path.resolve(__dirname, "../../../../scripts/qa/check-pending-ocr.mjs");
|
||||
pendingOcr = await import(pathToFileURL(scriptPath).href);
|
||||
});
|
||||
|
||||
it("normalizes unique positive thresholds", () => {
|
||||
expect(pendingOcr.parseThresholds(["--thresholds=10,2,5,2,0,invalid"])).toEqual([2, 5, 10]);
|
||||
});
|
||||
|
||||
it("reports a non-clean SKIP when the database target is unavailable", async () => {
|
||||
const summary = await pendingOcr.buildPendingOcrSummary({
|
||||
databaseReady: false,
|
||||
thresholds: [2, 5, 10],
|
||||
countPending: vi.fn(),
|
||||
now: new Date("2026-07-15T00:00:00.000Z"),
|
||||
});
|
||||
|
||||
expect(summary).toMatchObject({
|
||||
status: "SKIP",
|
||||
findingCount: 1,
|
||||
findings: [{ code: "database_not_configured", severity: "manual_or_config" }],
|
||||
thresholds: [],
|
||||
});
|
||||
});
|
||||
|
||||
it("reports PASS when no pending OCR rows exceed any threshold", async () => {
|
||||
const countPending = vi.fn(async () => 0);
|
||||
const summary = await pendingOcr.buildPendingOcrSummary({
|
||||
databaseReady: true,
|
||||
thresholds: [2, 5],
|
||||
countPending,
|
||||
now: new Date("2026-07-15T00:00:00.000Z"),
|
||||
});
|
||||
|
||||
expect(summary).toMatchObject({ status: "PASS", findingCount: 0, findings: [] });
|
||||
expect(summary.thresholds).toEqual([
|
||||
{ thresholdMinutes: 2, count: 0 },
|
||||
{ thresholdMinutes: 5, count: 0 },
|
||||
]);
|
||||
expect(countPending).toHaveBeenNthCalledWith(1, {
|
||||
status: "pending_ocr",
|
||||
created_at: { lte: new Date("2026-07-14T23:58:00.000Z") },
|
||||
});
|
||||
});
|
||||
|
||||
it("reports PASS_WITH_FINDINGS using the unique pending row upper bound", async () => {
|
||||
const counts = [2, 4, 7];
|
||||
const summary = await pendingOcr.buildPendingOcrSummary({
|
||||
databaseReady: true,
|
||||
thresholds: [2, 5, 10],
|
||||
countPending: vi.fn(async () => counts.shift()),
|
||||
now: new Date("2026-07-15T00:00:00.000Z"),
|
||||
});
|
||||
|
||||
expect(summary).toMatchObject({
|
||||
status: "PASS_WITH_FINDINGS",
|
||||
findingCount: 7,
|
||||
findings: [{ code: "pending_ocr_detected", severity: "finding", count: 7 }],
|
||||
});
|
||||
});
|
||||
|
||||
it("constructs and closes the shared Prisma 7 adapter without opening a database connection", async () => {
|
||||
const previousDatabaseUrl = process.env.DATABASE_URL;
|
||||
process.env.DATABASE_URL = "postgresql://local-user:[email protected]:1/hyb_oa_test";
|
||||
|
||||
try {
|
||||
await expect(pendingOcr.executePendingOcrCheck({
|
||||
databaseReady: true,
|
||||
thresholds: [],
|
||||
})).resolves.toMatchObject({ status: "PASS", findingCount: 0 });
|
||||
} finally {
|
||||
if (previousDatabaseUrl === undefined) {
|
||||
delete process.env.DATABASE_URL;
|
||||
} else {
|
||||
process.env.DATABASE_URL = previousDatabaseUrl;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,77 @@
|
||||
const path = require("node:path");
|
||||
const { pathToFileURL } = require("node:url");
|
||||
|
||||
const ROOT_DIR = path.resolve(__dirname, "../../../..");
|
||||
let scanner;
|
||||
|
||||
describe("current fact-source sensitive content gate", () => {
|
||||
beforeAll(async () => {
|
||||
const scriptPath = path.join(ROOT_DIR, "scripts/qa/check-sensitive-content.mjs");
|
||||
scanner = await import(pathToFileURL(scriptPath).href);
|
||||
});
|
||||
|
||||
it("reports only redacted metadata for secret, account, path and infrastructure matches", () => {
|
||||
const source = [
|
||||
"SERVICE_PASSWORD=do-not-disclose-value",
|
||||
"[email protected]",
|
||||
"phone=13800000000",
|
||||
"workspace=/home/developer/private/project",
|
||||
"server=203.0.113.10",
|
||||
].join("\n");
|
||||
|
||||
const findings = scanner.scanSensitiveText(source, "fixture.md");
|
||||
|
||||
expect(findings.map((item) => item.category)).toEqual(expect.arrayContaining([
|
||||
"assigned_secret",
|
||||
"email_account",
|
||||
"phone_account",
|
||||
"personal_workspace_path",
|
||||
"public_ipv4",
|
||||
]));
|
||||
expect(JSON.stringify(findings)).not.toMatch(/do-not-disclose|person@|13800000000|203\.0\.113\.10/);
|
||||
});
|
||||
|
||||
it("allows explicit placeholders, environment interpolation and loopback addresses", () => {
|
||||
const source = [
|
||||
"SERVICE_PASSWORD=<managed-secret>",
|
||||
"DATABASE_URL=${LOCAL_DATABASE_URL}",
|
||||
"admin=<admin-email>",
|
||||
"workspace=<repo-root>/server",
|
||||
"server=127.0.0.1",
|
||||
].join("\n");
|
||||
|
||||
expect(scanner.scanSensitiveText(source, "fixture.md")).toEqual([]);
|
||||
});
|
||||
|
||||
it("derives current docs from the status matrix without pulling historical issue rows", () => {
|
||||
const matrix = [
|
||||
"| ID | Contract | Type | Purpose | Status |",
|
||||
"| --- | --- | --- | --- | --- |",
|
||||
"| `CURRENT-DOC` | `docs/current.md` | fact | current | `CURRENT` |",
|
||||
"| `HISTORY-DOC` | `docs/history.md` | evidence | old | `EVIDENCE` |",
|
||||
"| `CURRENT-ISSUE` | `issues/current.csv` | `CURRENT` | issue only |",
|
||||
].join("\n");
|
||||
|
||||
expect(scanner.extractCurrentDocumentFiles(matrix)).toEqual(["docs/current.md"]);
|
||||
});
|
||||
|
||||
it("includes every CURRENT docs contract in the default scan set", () => {
|
||||
const files = scanner.resolveDefaultScanFiles(ROOT_DIR);
|
||||
|
||||
expect(files).toEqual(expect.arrayContaining([
|
||||
"docs/测试规约与回归门禁.md",
|
||||
"docs/需求-资产管理规约.md",
|
||||
"docs/管理员手册.md",
|
||||
"docs/员工快速上手.md",
|
||||
]));
|
||||
});
|
||||
|
||||
it("passes the registered current fact sources and deployment entrypoints", () => {
|
||||
expect(scanner.scanSensitiveContent({ rootDir: ROOT_DIR })).toMatchObject({
|
||||
status: "PASS",
|
||||
findingCount: 0,
|
||||
filesScanned: expect.any(Number),
|
||||
findings: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,157 @@
|
||||
const { execFile } = require("node:child_process");
|
||||
const { mkdtemp, rm, writeFile } = require("node:fs/promises");
|
||||
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 provenance;
|
||||
let repoDir;
|
||||
|
||||
async function git(args, cwd = repoDir) {
|
||||
return execFileAsync("git", args, { cwd });
|
||||
}
|
||||
|
||||
async function createCommit(message = "initial") {
|
||||
await git(["add", "tracked.txt"]);
|
||||
await git(["commit", "-m", message]);
|
||||
}
|
||||
|
||||
describe("deployment source provenance", () => {
|
||||
beforeAll(async () => {
|
||||
const scriptPath = path.join(ROOT_DIR, "deploy/trialrun/check-source-provenance.mjs");
|
||||
provenance = await import(pathToFileURL(scriptPath).href);
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
repoDir = await mkdtemp(path.join(os.tmpdir(), "hyb-oa-provenance-test-"));
|
||||
await git(["init", "--initial-branch=main"]);
|
||||
await git(["config", "user.name", "Test User"]);
|
||||
await git(["config", "user.email", "[email protected]"]);
|
||||
await writeFile(path.join(repoDir, "tracked.txt"), "initial\n", "utf8");
|
||||
await createCommit();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await rm(repoDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("requires an upstream by default and permits an explicit unpublished override", () => {
|
||||
expect(() => provenance.collectSourceProvenance({ rootDir: repoDir })).toThrowError(
|
||||
expect.objectContaining({ code: "upstream_missing" }),
|
||||
);
|
||||
|
||||
expect(provenance.collectSourceProvenance({ rootDir: repoDir, allowUnpublished: true })).toMatchObject({
|
||||
status: "PASS",
|
||||
published: false,
|
||||
overrideUsed: true,
|
||||
branch: "main",
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects tracked and untracked working-tree changes before any deployment", async () => {
|
||||
await writeFile(path.join(repoDir, "tracked.txt"), "changed\n", "utf8");
|
||||
await writeFile(path.join(repoDir, "untracked.txt"), "untracked\n", "utf8");
|
||||
|
||||
expect(() => provenance.collectSourceProvenance({ rootDir: repoDir, allowUnpublished: true })).toThrowError(
|
||||
expect.objectContaining({ code: "dirty_worktree" }),
|
||||
);
|
||||
});
|
||||
|
||||
it("accepts a clean commit that is contained in its upstream", async () => {
|
||||
const remoteDir = await mkdtemp(path.join(os.tmpdir(), "hyb-oa-provenance-remote-"));
|
||||
try {
|
||||
await execFileAsync("git", ["init", "--bare", remoteDir]);
|
||||
await git(["remote", "add", "origin", remoteDir]);
|
||||
await git(["push", "--set-upstream", "origin", "main"]);
|
||||
|
||||
expect(provenance.collectSourceProvenance({ rootDir: repoDir })).toMatchObject({
|
||||
status: "PASS",
|
||||
published: true,
|
||||
overrideUsed: false,
|
||||
upstream: "origin/main",
|
||||
});
|
||||
} finally {
|
||||
await rm(remoteDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects a clean local commit that has not reached upstream", async () => {
|
||||
const remoteDir = await mkdtemp(path.join(os.tmpdir(), "hyb-oa-provenance-remote-"));
|
||||
try {
|
||||
await execFileAsync("git", ["init", "--bare", remoteDir]);
|
||||
await git(["remote", "add", "origin", remoteDir]);
|
||||
await git(["push", "--set-upstream", "origin", "main"]);
|
||||
await writeFile(path.join(repoDir, "tracked.txt"), "ahead\n", "utf8");
|
||||
await createCommit("ahead");
|
||||
|
||||
expect(() => provenance.collectSourceProvenance({ rootDir: repoDir })).toThrowError(
|
||||
expect.objectContaining({ code: "commit_not_in_upstream" }),
|
||||
);
|
||||
expect(provenance.collectSourceProvenance({ rootDir: repoDir, allowUnpublished: true })).toMatchObject({
|
||||
published: false,
|
||||
overrideUsed: true,
|
||||
});
|
||||
} finally {
|
||||
await rm(remoteDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("refreshes the remote-tracking ref before deciding whether a commit is still published", async () => {
|
||||
const remoteDir = await mkdtemp(path.join(os.tmpdir(), "hyb-oa-provenance-remote-"));
|
||||
const replacementDir = await mkdtemp(path.join(os.tmpdir(), "hyb-oa-provenance-replacement-"));
|
||||
try {
|
||||
await execFileAsync("git", ["init", "--bare", remoteDir]);
|
||||
await git(["remote", "add", "origin", remoteDir]);
|
||||
await git(["push", "--set-upstream", "origin", "main"]);
|
||||
expect(provenance.collectSourceProvenance({ rootDir: repoDir })).toMatchObject({ published: true });
|
||||
|
||||
await execFileAsync("git", ["init", "--initial-branch=main"], { cwd: replacementDir });
|
||||
await execFileAsync("git", ["config", "user.name", "Replacement"], { cwd: replacementDir });
|
||||
await execFileAsync("git", ["config", "user.email", "[email protected]"], { cwd: replacementDir });
|
||||
await writeFile(path.join(replacementDir, "replacement.txt"), "replacement\n", "utf8");
|
||||
await execFileAsync("git", ["add", "replacement.txt"], { cwd: replacementDir });
|
||||
await execFileAsync("git", ["commit", "-m", "replacement"], { cwd: replacementDir });
|
||||
await execFileAsync("git", ["remote", "add", "origin", remoteDir], { cwd: replacementDir });
|
||||
await execFileAsync("git", ["push", "--force", "origin", "main"], { cwd: replacementDir });
|
||||
|
||||
expect(provenance.collectSourceProvenance({ rootDir: repoDir })).toMatchObject({ published: true });
|
||||
expect(provenance.refreshUpstreamRef({ rootDir: repoDir })).toMatchObject({
|
||||
refreshed: true,
|
||||
upstream: "origin/main",
|
||||
remote: "origin",
|
||||
});
|
||||
expect(() => provenance.collectSourceProvenance({ rootDir: repoDir })).toThrowError(
|
||||
expect.objectContaining({ code: "commit_not_in_upstream" }),
|
||||
);
|
||||
} finally {
|
||||
await rm(remoteDir, { recursive: true, force: true });
|
||||
await rm(replacementDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("requires an allowlisted audit reason for the unpublished override", async () => {
|
||||
const script = path.join(ROOT_DIR, "deploy/trialrun/check-source-provenance.mjs");
|
||||
|
||||
await expect(execFileAsync("node", [script, `--root=${repoDir}`], {
|
||||
env: { ...process.env, DEPLOY_ALLOW_UNPUBLISHED_COMMIT: "1" },
|
||||
})).rejects.toMatchObject({ code: 1 });
|
||||
|
||||
const { stdout } = await execFileAsync("node", [script, `--root=${repoDir}`], {
|
||||
env: {
|
||||
...process.env,
|
||||
DEPLOY_ALLOW_UNPUBLISHED_COMMIT: "1",
|
||||
DEPLOY_UNPUBLISHED_REASON: "offline-disaster-recovery",
|
||||
},
|
||||
});
|
||||
expect(JSON.parse(stdout)).toMatchObject({
|
||||
published: false,
|
||||
remoteVerified: false,
|
||||
overrideUsed: true,
|
||||
overrideRequested: true,
|
||||
overrideReason: "offline-disaster-recovery",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
const path = require("node:path");
|
||||
const { pathToFileURL } = require("node:url");
|
||||
|
||||
let governance;
|
||||
|
||||
describe("Trellis context manifest governance", () => {
|
||||
beforeAll(async () => {
|
||||
const scriptPath = path.resolve(__dirname, "../../../../scripts/qa/check-trellis-governance.mjs");
|
||||
governance = await import(pathToFileURL(scriptPath).href);
|
||||
});
|
||||
|
||||
it("does not require sub-agent manifests in Codex inline mode", () => {
|
||||
expect(governance.contextManifestPolicy("inline")).toEqual({
|
||||
required: false,
|
||||
requireRealEntries: false,
|
||||
rejectSeedRows: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps strict manifest validation for sub-agent dispatch", () => {
|
||||
expect(governance.contextManifestPolicy("sub-agent")).toEqual({
|
||||
required: true,
|
||||
requireRealEntries: true,
|
||||
rejectSeedRows: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
在新工单中引用
屏蔽一个用户