fix: 补齐候选诊断环境与证据保护(09-06-release-gate-integrity)
这个提交包含在:
@@ -0,0 +1,129 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { copyFileSync, mkdirSync, readFileSync, rmSync, writeFileSync, unlinkSync, symlinkSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import test from "node:test";
|
||||
import { validateExactProductionIdentity } from "../../../deploy/trialrun/production-preflight.mjs";
|
||||
import { browserInventory, createDiagnosticScratch, sourceSnapshot, compareSnapshots } from "../full-local-diagnostic.mjs";
|
||||
|
||||
test("全仓快照发现已跟踪 PNG 覆盖、dirty 二次改写、index-only 和删除", context => {
|
||||
const root = createDiagnosticScratch();
|
||||
context.after(() => rmSync(root, { recursive: true }));
|
||||
const git = args => { const result = spawnSync("git", args, { cwd: root, encoding: "utf8" }); assert.equal(result.status, 0, result.stderr); };
|
||||
mkdirSync(path.join(root, "client")); mkdirSync(path.join(root, "output"));
|
||||
writeFileSync(path.join(root, ".gitignore"), "output/\n");
|
||||
writeFileSync(path.join(root, "client/source.ts"), "export {};\n");
|
||||
const image = path.join(root, "output/archive.png"); writeFileSync(image, "archived");
|
||||
git(["init", "-q"]); git(["add", "."]); git(["add", "-f", "output/archive.png"]);
|
||||
git(["-c", "user.name=Fixture", "-c", "[email protected]", "-c", "core.hooksPath=/dev/null", "commit", "-qm", "fixture"]);
|
||||
const before = sourceSnapshot(root);
|
||||
assert.equal(before.dirty, false);
|
||||
writeFileSync(path.join(root, "output/ignored.png"), "new run");
|
||||
assert.equal(compareSnapshots(before, sourceSnapshot(root)).repositoryUnchanged, true);
|
||||
writeFileSync(image, "first overwrite"); const first = sourceSnapshot(root);
|
||||
assert.equal(compareSnapshots(before, first).sourceUnchanged, true);
|
||||
assert.equal(compareSnapshots(before, first).repositoryUnchanged, false);
|
||||
assert.ok(first.repository.porcelainV1Z.includes("output/archive.png\0"));
|
||||
writeFileSync(image, "second overwrite"); const second = sourceSnapshot(root);
|
||||
assert.equal(first.dirty, true); assert.equal(second.dirty, true);
|
||||
assert.equal(first.repository.porcelainV1Z, second.repository.porcelainV1Z);
|
||||
assert.equal(compareSnapshots(first, second).repositoryUnchanged, false);
|
||||
git(["add", "-f", "output/archive.png"]); const staged = sourceSnapshot(root);
|
||||
assert.equal(second.repository.trackedDigest, staged.repository.trackedDigest);
|
||||
assert.notEqual(second.repository.indexDigest, staged.repository.indexDigest);
|
||||
assert.equal(compareSnapshots(second, staged).repositoryUnchanged, false);
|
||||
git(["update-index", "--assume-unchanged", "output/archive.png"]);
|
||||
const flagged = sourceSnapshot(root);
|
||||
assert.equal(staged.repository.trackedDigest, flagged.repository.trackedDigest);
|
||||
assert.notEqual(staged.repository.indexDigest, flagged.repository.indexDigest);
|
||||
assert.equal(compareSnapshots(staged, flagged).repositoryUnchanged, false);
|
||||
git(["update-index", "--no-assume-unchanged", "output/archive.png"]);
|
||||
unlinkSync(image); const deleted = sourceSnapshot(root);
|
||||
assert.equal(deleted.repository.trackedFiles.find(entry => entry.file === "output/archive.png").kind, "missing");
|
||||
assert.equal(compareSnapshots(staged, deleted).repositoryUnchanged, false);
|
||||
symlinkSync("absent-target", image); const linked = sourceSnapshot(root);
|
||||
assert.equal(linked.repository.trackedFiles.find(entry => entry.file === "output/archive.png").kind, "symlink");
|
||||
assert.equal(compareSnapshots(deleted, linked).repositoryUnchanged, false);
|
||||
assert.equal(compareSnapshots(null, linked).repositoryUnchanged, false);
|
||||
assert.equal(compareSnapshots(null, linked).candidateIntegrityStatus, "UNKNOWN");
|
||||
const incomplete = structuredClone(linked);
|
||||
delete incomplete.repository.trackedDigest;
|
||||
assert.equal(compareSnapshots(linked, incomplete).candidateIntegrityStatus, "UNKNOWN");
|
||||
});
|
||||
|
||||
test("诊断临时根不污染原生产身份测试的合成合法路径", context => {
|
||||
const scratch = createDiagnosticScratch();
|
||||
context.after(() => rmSync(scratch, { recursive: true }));
|
||||
const identity = {
|
||||
deployServer: "production-node",
|
||||
composeProjectName: "production-project",
|
||||
remoteDir: path.join(scratch, "server-unit", "hyb-oa-deploy-safety-fixture", "app"),
|
||||
};
|
||||
const validate = remoteDir => validateExactProductionIdentity({
|
||||
...identity,
|
||||
remoteDir,
|
||||
expectedDeployServer: identity.deployServer,
|
||||
expectedComposeProjectName: identity.composeProjectName,
|
||||
expectedRemoteDir: remoteDir,
|
||||
});
|
||||
assert.doesNotThrow(() => validate(identity.remoteDir));
|
||||
assert.throws(() => validate(path.join(scratch, "hyb-full-local-diagnostic-fixture", "app")), /REMOTE_DIR_NON_PRODUCTION_IDENTITY/);
|
||||
});
|
||||
|
||||
test("原聚合器在 workflow 失败后停止,同库继续参数会破坏该边界", context => {
|
||||
const scratch = createDiagnosticScratch();
|
||||
context.after(() => rmSync(scratch, { recursive: true }));
|
||||
mkdirSync(path.join(scratch, "lib"));
|
||||
copyFileSync(new URL("../../../server/scripts/regression-all.sh", import.meta.url), path.join(scratch, "regression-all.sh"));
|
||||
copyFileSync(new URL("../../../server/scripts/lib/regression-report.sh", import.meta.url), path.join(scratch, "lib/regression-report.sh"));
|
||||
const suites = ["smoke-test", "regression-dedup", "regression-workflow-actions", "regression-procurement", "regression-assets", "regression-audit", "regression-ocr", "regression-im-notifier"];
|
||||
for (const suite of suites) {
|
||||
writeFileSync(path.join(scratch, `${suite}.sh`), `#!/usr/bin/env bash\nprintf '%s\\n' '${suite}' >> "$QA_AGGREGATE_FIXTURE_LOG"\nexit ${suite === "regression-workflow-actions" ? 31 : 0}\n`, { mode: 0o755 });
|
||||
}
|
||||
const log = path.join(scratch, "invocations.log");
|
||||
const environment = { PATH: process.env.PATH, HOME: scratch, TMPDIR: scratch, QA_AGGREGATE_FIXTURE_LOG: log };
|
||||
const run = args => spawnSync("bash", [path.join(scratch, "regression-all.sh"), ...args], { cwd: scratch, env: environment, encoding: "utf8", timeout: 15000 });
|
||||
const normal = run([]);
|
||||
assert.equal(normal.status, 31, normal.stderr);
|
||||
assert.deepEqual(readFileSync(log, "utf8").trim().split("\n"), suites.slice(0, 3));
|
||||
writeFileSync(log, "");
|
||||
const continued = run(["--json-report", path.join(scratch, "continued.json")]);
|
||||
assert.equal(continued.status, 1, continued.stderr);
|
||||
assert.deepEqual(readFileSync(log, "utf8").trim().split("\n"), suites);
|
||||
});
|
||||
|
||||
const discovery = { cases: [
|
||||
{ file: "client/tests/e2e/ordinary.spec.ts", id: "first", projectName: "", repeatEachIndex: 0 },
|
||||
{ file: "client/tests/e2e/ordinary.spec.ts", id: "second", projectName: "", repeatEachIndex: 0 },
|
||||
] };
|
||||
|
||||
test("缺浏览器回执不能从枚举伪造执行通过", () => {
|
||||
const result = browserInventory(discovery, null);
|
||||
assert.equal(result.reportPresent, false);
|
||||
assert.deepEqual(result.cases.map(entry => entry.status), ["NOT_RUN", "NOT_RUN"]);
|
||||
});
|
||||
|
||||
test("保留失败、serial 跳过、预期状态及重试事实", () => {
|
||||
const execution = { errors: [], suites: [{ specs: [
|
||||
{ id: "first", tests: [{ projectName: "", expectedStatus: "passed", status: "unexpected", results: [{ status: "failed", retry: 0, duration: 100 }] }] },
|
||||
{ id: "second", tests: [{ projectName: "", expectedStatus: "passed", status: "skipped", results: [{ status: "skipped", retry: 0, duration: 0 }] }] },
|
||||
] }] };
|
||||
const result = browserInventory(discovery, execution);
|
||||
assert.deepEqual(result.cases.map(entry => entry.status), ["failed", "skipped"]);
|
||||
assert.equal(result.cases[1].reportedStatus, "skipped");
|
||||
assert.equal(result.cases[0].results[0].retry, 0);
|
||||
});
|
||||
|
||||
test("部分回执只为实际返回的用例计结果", () => {
|
||||
const result = browserInventory(discovery, { errors: [{}], suites: [{ specs: [{ id: "first", tests: [{ results: [{ status: "passed" }] }] }] }] });
|
||||
assert.deepEqual(result.cases.map(entry => entry.status), ["passed", "NOT_RUN"]);
|
||||
assert.equal(result.executionErrors, 1);
|
||||
});
|
||||
|
||||
test("重复和未知用例作为报告问题单列", () => {
|
||||
const result = browserInventory(discovery, { suites: [{ specs: [
|
||||
{ id: "unknown", tests: [{}, {}] },
|
||||
] }] });
|
||||
assert.deepEqual(result.issues, ["duplicate_case", "unknown_cases"]);
|
||||
assert.ok(result.cases.every(entry => entry.status === "NOT_RUN"));
|
||||
});
|
||||
在新工单中引用
屏蔽一个用户