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", "user.email=fixture@example.invalid", "-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")); });