import assert from "node:assert/strict"; import { createHash } from "node:crypto"; import { spawnSync } from "node:child_process"; import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, symlinkSync, writeFileSync } from "node:fs"; import os from "node:os"; import path from "node:path"; import { fileURLToPath } from "node:url"; import test from "node:test"; const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../.."); for (const [flag, protectedLine] of [ ["FAIL_ENVIRONMENT_VERIFY", ' node "$ROOT_DIR/scripts/qa/full-local-environment.mjs" verify'], ["FAIL_ENVIRONMENT_PROBE", ' node "$ROOT_DIR/scripts/qa/browser-environment-probe.mjs" guest "$QA_BASE_URL"'], ["FAIL_ENVIRONMENT_CONSUME", ' node "$ROOT_DIR/scripts/qa/full-local-environment.mjs" report-guest > "$QA_FULL_REPORT_DIR/environment-before-browser.json"'], ]) test(`环境保护拒绝并对真实源码删除敏感:${flag}`, context => { const assertBlocked = result => { assert.notEqual(result.status, 0); assert.ok(!result.commands.some(command => command.args.includes("test:e2e:full"))); }; const owned = fixture(context, { [flag]: "1", QA_ENVIRONMENT_CONTEXT: "synthetic-reference", ENVIRONMENT_PASSED: "1", ONLINE: "1" }); assertBlocked(owned.run()); const source = path.join(owned.root, "scripts/qa/run-web-gates.sh"); const original = readFileSync(source, "utf8"); assert.ok(original.includes(protectedLine)); writeFileSync(source, original.replaceAll(protectedLine, ":")); assert.throws(() => assertBlocked(owned.run()), assert.AssertionError); }); function fixture(context, overrides = {}) { const root = mkdtempSync(path.join(os.tmpdir(), "hyb-full-local-test-")); context.after(() => rmSync(root, { recursive: true, force: true })); for (const directory of ["bin", "tmp", "scripts/qa", "server/scripts", "client/dist", "client/tests/e2e", "output"]) mkdirSync(path.join(root, directory), { recursive: true }); writeFileSync(path.join(root, "output/archive.png"), "historical evidence"); for (const file of ["run-web-gates.sh", "run-pwa-cross-version-upgrade.sh", "pwa-cross-version-fixture.sh", "verify-full-local-discovery.mjs", "full-local-diagnostic.mjs", "full-local-diagnostic-stage.sh", "full-local-coverage.mjs", "qa-stage-record.mjs"]) copyFileSync(path.join(projectRoot, "scripts/qa", file), path.join(root, "scripts/qa", file)); for (const file of ["pwa-cross-version-upgrade.spec.ts", "ordinary.spec.ts"]) writeFileSync(path.join(root, "client/tests/e2e", file), ""); const exceptions = []; if (overrides.LAYERED_FIXTURE) { for (const [file, kind] of [["live.spec.ts", "post-deploy"], ["frozen.spec.ts", "frozen"]]) { writeFileSync(path.join(root, "client/tests/e2e", file), "preserved"); exceptions.push({ file: `client/tests/e2e/${file}`, id: file, title: file, projectName: "", repeatEachIndex: 0, kind, sourceSha256: createHash("sha256").update("preserved").digest("hex"), reason: "fixture", entry: "fixture", responsibilityRole: "fixture", requiredEvidence: "fixture", replacementFile: "client/tests/e2e/ordinary.spec.ts", replacementTitle: "ordinary.spec.ts" }); } } writeFileSync(path.join(root, "scripts/qa/full-local-coverage-contract.json"), JSON.stringify({ schemaVersion: 1, approvalReference: "fixture", exceptions })); writeFileSync(path.join(root, "client/dist/keep.txt"), "original-dist"); for (const [file, stage] of [["regression-workflow-actions.sh", "workflow-actions"], ["regression-all.sh", "server-regressions"]]) { writeFileSync(path.join(root, "server/scripts", file), `#!${process.execPath} const fs = require('node:fs'); fs.appendFileSync(process.env.HYB_QA_FIXTURE_ROOT+'/commands.jsonl',JSON.stringify({command:'regression',stage:'${stage}',args:process.argv.slice(2),database:process.env.QA_DB_NAME})+'\\n'); process.exit((process.env.FAIL_STAGES??'').split(',').includes('${stage}') ? 31 : 0); `, { mode: 0o755 }); } const fake = `#!${process.execPath} const fs = require('node:fs'); const path = require('node:path'); const { spawnSync } = require('node:child_process'); const root = process.env.HYB_QA_FIXTURE_ROOT; if (!root || !path.basename(root).startsWith('hyb-full-local-test-') || path.dirname(process.argv[1]) !== path.join(root, 'bin')) throw Error('fixture_only'); const command = path.basename(process.argv[1]); const args = process.argv.slice(2); fs.appendFileSync(path.join(root, 'commands.jsonl'), JSON.stringify({command,args,nodeEnv:process.env.NODE_ENV,imNotifierEnabled:process.env.IM_NOTIFIER_ENABLED,ocrProvider:process.env.OCR_PROVIDER,jobsEnabled:process.env.JOBS_ENABLED,ocrMock:process.env.PLAYWRIGHT_OCR_MOCK,syntheticInitialPresent:Boolean(process.env.QA_SYNTHETIC_INITIAL_PASSWORD),seedRolesPrepared:['CEO','ADMIN','MANAGER','EMPLOYEE','FINANCE','FINANCE_MGR','HR','ADMINISTRATION'].every(role=>Boolean(process.env['SEED_'+role+'_PASSWORD'])),visualOutput:process.env.QA_VISUAL_OUTPUT_DIR,stage:process.env.QA_DIAGNOSTIC_STAGE,database:process.env.QA_DB_NAME,sha:process.env.VITE_BUILD_SHA,inputs:[process.env.PWA_BUILD_A_DIR,process.env.PWA_BUILD_B_DIR,process.env.PWA_BUILD_A_SHA,process.env.PWA_BUILD_B_SHA]})+'\\n'); if (command === 'node' && args[0]?.endsWith('full-local-environment.mjs')) { if (process.env.FAIL_ENVIRONMENT) process.exit(41); if (process.env.FAIL_ENVIRONMENT_VERIFY && args[1] === 'verify') process.exit(41); if (process.env.FAIL_ENVIRONMENT_CONSUME && args[1] === 'report-guest') process.exit(41); if (['full-local','full-local-diagnostic','retained-server-local'].includes(args[1])) { const result = spawnSync('bash',[path.join(root,'scripts/qa/run-web-gates.sh'),args[1]],{env:{...process.env,QA_ENVIRONMENT_CONTEXT:'synthetic-reference'},stdio:'inherit'}); process.exit(result.status ?? 1); } console.log(JSON.stringify({bootstrapObservation:{valid:true}})); process.exit(0); } if (command === 'node' && args[0]?.endsWith('prepare-local-e2e-accounts.mjs')) process.exit(process.env.FAIL_ACCOUNT_PREPARATION ? 43 : 0); if (command === 'node' && args[0]?.endsWith('browser-environment-probe.mjs')) process.exit(process.env.FAIL_ENVIRONMENT_PROBE ? 42 : 0); if (command === 'node' && (args[0]?.endsWith('verify-full-local-discovery.mjs') || args[0]?.endsWith('full-local-diagnostic.mjs') || args[0]?.endsWith('full-local-coverage.mjs') || args[0]?.endsWith('qa-stage-record.mjs') || args[0] === '-e')) { const result = spawnSync(process.execPath, args, {env:process.env, stdio:'inherit'}); process.exit(result.status ?? 1); } if (command === 'npx' && args[0] === 'vite' && args.includes('--outDir')) { if (process.env.FAIL_BUILD_SHA === process.env.VITE_BUILD_SHA) process.exit(23); const directory = args[args.indexOf('--outDir')+1]; fs.mkdirSync(directory, {recursive:true}); fs.writeFileSync(path.join(directory,'index.html'),'synthetic'); if (!process.env.OMIT_SW) fs.writeFileSync(path.join(directory,'sw.js'),'synthetic'); } if (command === 'npx' && args.includes('--list')) { for (const name of ['PWA_BUILD_A_DIR','PWA_BUILD_B_DIR','PWA_BUILD_A_SHA','PWA_BUILD_B_SHA']) if (!process.env[name]) process.exit(24); if (process.env.FAIL_DISCOVERY) process.exit(25); let files = fs.readdirSync(path.join(root,'client/tests/e2e')); if (process.env.OMIT_PWA) files = files.filter(file=>!file.startsWith('pwa-')); const report = {errors:[],suites:files.map(file=>({file,specs:[{file,id:file,title:file,line:1,tests:[{projectName:''}]}]}))}; fs.writeFileSync(process.env.PLAYWRIGHT_JSON_OUTPUT_FILE,JSON.stringify(report)); } if (command === 'psql' && args.some(arg=>arg.includes('count(*)'))) console.log('0'); if (command === 'dropdb' && process.env.FAIL_CLEANUP_STAGE && process.env.FAIL_CLEANUP_STAGE === process.env.QA_DIAGNOSTIC_STAGE) process.exit(33); if (command === 'createdb' && process.env.FAIL_PREPARE_STAGE && process.env.FAIL_PREPARE_STAGE === process.env.QA_DIAGNOSTIC_STAGE) process.exit(34); const failStage = (process.env.FAIL_STAGES??'').split(',').includes(process.env.QA_DIAGNOSTIC_STAGE); if (command === 'npm' && args[0] === 'test' && failStage) process.exit(32); if (command === 'npx' && args[0] === 'tsc' && failStage) process.exit(32); if (command === 'npm' && args.includes('test:e2e:full')) { if (process.env.MUTATE_ARCHIVE) fs.writeFileSync(path.join(root,'output/archive.png'),'overwritten'); if (process.env.PLAYWRIGHT_JSON_OUTPUT_FILE) { const discovery = JSON.parse(fs.readFileSync(path.join(root,process.env.QA_DIAGNOSTIC_STAGE?'report/full-local-report/discovery.json':'report/discovery.json'))); if (process.env.LAYERED_FIXTURE) discovery.suites = discovery.suites.filter(suite => args.some(arg => {try { return new RegExp(arg).test('tests/e2e/'+suite.file); } catch { return false; }})); if (process.env.OMIT_EXECUTION_CASE) discovery.suites.pop(); for (const suite of discovery.suites) for (const spec of suite.specs) for (const test of spec.tests) Object.assign(test,{status:process.env.FAIL_BROWSER?'unexpected':'expected',expectedStatus:'passed',results:[{status:process.env.FAIL_BROWSER?'failed':'passed',retry:0,duration:1}]}); if (process.env.SKIP_EXECUTION_CASE) discovery.suites[0].specs[0].tests[0].results[0].status = 'skipped'; fs.writeFileSync(process.env.PLAYWRIGHT_JSON_OUTPUT_FILE,JSON.stringify(discovery)); } if (process.env.FAIL_BROWSER) process.exit(26); } if (!['node','npm','npx','curl','psql','createdb','dropdb'].includes(command)) throw Error('unexpected_fixture_command'); `; writeFileSync(path.join(root, "bin/fake"), fake, { mode: 0o755 }); for (const command of ["node", "npm", "npx", "curl", "psql", "createdb", "dropdb"]) symlinkSync("fake", path.join(root, "bin", command)); const env = { PATH: `${path.join(root, "bin")}:${process.env.PATH}`, HOME: root, LANG: "C.UTF-8", HYB_QA_FIXTURE_ROOT: root, TMPDIR: path.join(root, "tmp"), QA_DB_CLIENT: "host", QA_DB_NAME: "hyb_oa_webgate_fixture", QA_HOST: "127.0.0.1", QA_FULL_REPORT_DIR: path.join(root, "report"), PWA_REPORT_DIR: path.join(root, "report"), QA_SERVER_LOG: path.join(root, "server.log"), QA_SERVER_PID_FILE: path.join(root, "server.pid"), ...overrides, }; writeFileSync(path.join(root, ".gitignore"), "bin/\ntmp/\nreport/\noutput/\ncommands.jsonl\n*.log\n*.pid\n"); for (const args of [["init", "-q"], ["add", "."], ["add", "-f", "output/archive.png"], ["-c", "user.name=QA Fixture", "-c", "user.email=qa@example.invalid", "commit", "-qm", "synthetic fixture"]]) { assert.equal(spawnSync("git", args, { cwd: root, encoding: "utf8" }).status, 0); } return { root, run(script = "run-web-gates.sh", args = ["full-local"]) { // Each mutation invocation owns a fresh report; do not fail on a prior // invocation's exclusive output before reaching the mutated guard. rmSync(path.join(root, "report"), { recursive: true, force: true }); rmSync(path.join(root, "commands.jsonl"), { force: true }); const result = spawnSync("bash", [path.join(root, "scripts/qa", script), ...args], { env, cwd: root, encoding: "utf8", timeout: 30000 }); return { ...result, commands: existsSync(path.join(root, "commands.jsonl")) ? readFileSync(path.join(root, "commands.jsonl"), "utf8").trim().split("\n").map((line) => JSON.parse(line)) : [] }; }, }; } for (const ci of [undefined, "1"]) { test(`full-local 准备 A/B、保留并发、CI=${ci ?? "unset"} 仍强制零重试`, (context) => { const owned = fixture(context, ci ? { CI: ci } : {}); const result = owned.run(); assert.equal(result.status, 0, result.stderr); const builds = result.commands.filter((command) => command.args.includes("--outDir")); assert.deepEqual(builds.map((command) => command.sha), ["111111111111", "222222222222"]); assert.ok(builds.every(command => command.nodeEnv === "production")); const discovery = result.commands.find((command) => command.args.includes("--list")); const browser = result.commands.find((command) => command.args.includes("test:e2e:full")); assert.equal(browser.imNotifierEnabled, "true"); assert.ok(!browser.args.some(argument => argument.startsWith("--trace"))); for (const command of [discovery, browser]) { assert.ok(command); assert.ok(command.args.includes("--retries=0")); assert.ok(command.args.includes("--forbid-only")); assert.ok(!command.args.some((argument) => argument.startsWith("--workers") || argument.startsWith("--grep") || argument.startsWith("--shard"))); assert.ok(command.inputs.every(Boolean)); assert.equal(command.visualOutput, path.join(owned.root, "report/visual")); assert.notEqual(command.inputs[0], command.inputs[1]); assert.notEqual(command.inputs[2], command.inputs[3]); } assert.ok(result.commands.indexOf(discovery) < result.commands.findIndex((command) => command.command === "createdb")); const environment = result.commands.find(command => command.args[0]?.endsWith("full-local-environment.mjs") && command.args[1] === "verify"); const probe = result.commands.find(command => command.args[0]?.endsWith("browser-environment-probe.mjs")); const consume = result.commands.find(command => command.args[0]?.endsWith("full-local-environment.mjs") && command.args[1] === "report-guest"); assert.ok(result.commands.indexOf(environment) < result.commands.indexOf(discovery)); assert.ok(result.commands.indexOf(probe) < result.commands.indexOf(consume)); assert.ok(result.commands.indexOf(consume) < result.commands.indexOf(browser)); assert.ok(result.commands.filter((command) => command.command === "npm" && !command.args.includes("test:e2e:full")).every((command) => command.inputs.every((value) => value === null))); assert.equal(readFileSync(path.join(owned.root, "client/dist/keep.txt"), "utf8"), "original-dist"); assert.deepEqual(readdirSync(path.join(owned.root, "tmp")), []); assert.ok(existsSync(path.join(owned.root, "report/pwa-build-a.log"))); }); } for (const overrides of [{ FAIL_BUILD_SHA: "111111111111" }, { FAIL_BUILD_SHA: "222222222222" }, { OMIT_SW: "1" }, { FAIL_DISCOVERY: "1" }, { OMIT_PWA: "1" }]) { test(`输入或完整发现失败阻止后续阶段 ${JSON.stringify(overrides)}`, (context) => { const owned = fixture(context, overrides); const result = owned.run(); assert.notEqual(result.status, 0); assert.ok(!result.commands.some((command) => command.command === "createdb" || command.args.includes("test:e2e:full"))); assert.deepEqual(readdirSync(path.join(owned.root, "tmp")), []); assert.ok(existsSync(path.join(owned.root, "report/pwa-build-a.log"))); }); } test("浏览器失败保留非零并清理本轮输入", (context) => { const owned = fixture(context, { FAIL_BROWSER: "1" }); const result = owned.run(); assert.equal(result.status, 26); assert.ok(result.commands.some((command) => command.command === "dropdb")); assert.deepEqual(readdirSync(path.join(owned.root, "tmp")), []); assert.ok(existsSync(path.join(owned.root, "report/discovery-coverage.json"))); }); test("原专用 runner 共用输入逻辑且保留自身单 worker", (context) => { const owned = fixture(context); const result = owned.run("run-pwa-cross-version-upgrade.sh", []); assert.equal(result.status, 0, result.stderr); const browser = result.commands.find((command) => command.args.includes("tests/e2e/pwa-cross-version-upgrade.spec.ts")); assert.ok(browser.args.includes("--workers=1")); assert.ok(browser.args.includes("--retries=0")); assert.ok(browser.inputs.every(Boolean)); assert.deepEqual(readdirSync(path.join(owned.root, "tmp")), []); }); test("恢复静默排除 PWA 的真实源码变异必须使 full-local 失败", (context) => { const owned = fixture(context); const source = path.join(owned.root, "scripts/qa/run-web-gates.sh"); const original = readFileSync(source, "utf8"); const mutated = original.replace("npx playwright test --list --retries=0", "OMIT_PWA=1 npx playwright test --list --retries=0"); assert.notEqual(mutated, original); writeFileSync(source, mutated); assert.notEqual(owned.run().status, 0); }); const diagnosticApproval = { QA_DIAGNOSTIC_REASON: "synthetic coverage inventory", QA_DIAGNOSTIC_OPERATOR: "fixture", QA_DIAGNOSTIC_APPROVAL_REF: "synthetic-test-approval", }; function runDiagnostic(context, overrides = {}) { const owned = fixture(context, { ...diagnosticApproval, ...overrides }); const result = owned.run("run-web-gates.sh", ["full-local-diagnostic"]); const reportFile = path.join(owned.root, "report/diagnostic-results.json"); return { owned, result, report: existsSync(reportFile) ? JSON.parse(readFileSync(reportFile, "utf8")) : null }; } test("普通 full-local 遇 workflow 失败仍立即停止", context => { const owned = fixture(context, { FAIL_STAGES: "workflow-actions" }); const result = owned.run(); assert.equal(result.status, 31, result.stderr); assert.ok(!result.commands.some(command => command.stage === "server-regressions" || command.args.includes("test:e2e:full"))); }); test("诊断全绿仍 PARTIAL/2,每个写库阶段使用全新独占库并清理", context => { const { result, report, owned } = runDiagnostic(context, { CI: "1", GATE_PASSED: "1" }); assert.equal(result.status, 2, result.stderr); assert.equal(report.status, "PARTIAL"); assert.ok(report.stages.every(stage => stage.status === "PASS"), JSON.stringify(report.stages)); assert.equal(report.sourceUnchanged, true); assert.equal(report.repositoryUnchanged, true); assert.equal(report.cleanAtStart, true); assert.equal(report.cleanAtEnd, true); assert.equal(report.browser.cases.length, 2); assert.ok(report.browser.cases.every(entry => entry.status === "passed")); const created = result.commands.filter(command => command.command === "createdb"); assert.equal(new Set(created.map(command => command.database)).size, 3); assert.equal(result.commands.filter(command => command.command === "dropdb").length, 3); const browser = result.commands.find(command => command.args.includes("test:e2e:full")); assert.ok(browser.args.includes("--retries=0")); assert.deepEqual(browser.args.filter(argument => argument.startsWith("--trace")), ["--trace=on"]); assert.ok(!browser.args.some(argument => /^(--workers|--grep|--shard)/.test(argument))); assert.deepEqual(result.commands.find(command => command.command === "regression" && command.stage === "server-regressions").args, []); assert.deepEqual(readdirSync(path.join(owned.root, "tmp")), []); }); test("诊断跨过独立测试失败,浏览器失败与来源分别留痕", context => { const { result, report } = runDiagnostic(context, { FAIL_STAGES: "server-unit,workflow-actions,server-regressions", FAIL_BROWSER: "1" }); assert.equal(result.status, 2, result.stderr); for (const id of ["server-unit", "workflow-actions", "server-regressions", "browser"]) { const stage = report.stages.find(stage => stage.id === id); assert.equal(stage.status, "FAIL"); assert.equal(stage.failureKind, "test"); } assert.ok(report.browser.cases.every(entry => entry.status === "failed")); }); test("环境有效与 tracked output 被改写必须独立记录", context => { const { result, report } = runDiagnostic(context, { MUTATE_ARCHIVE: "1" }); assert.equal(result.status, 2); assert.equal(report.environmentQualified, true); assert.equal(report.sourceUnchanged, true); assert.equal(report.repositoryUnchanged, false); assert.equal(report.cleanAtStart, true); assert.equal(report.cleanAtEnd, false); assert.equal(report.candidateIntegrityStatus, "CHANGED"); assert.ok(report.sourceAfter.repository.porcelainV1Z.includes("output/archive.png\0")); }); for (const missing of Object.keys(diagnosticApproval)) { test(`诊断缺授权记录拒绝 ${missing}`, context => { const { result, report } = runDiagnostic(context, { [missing]: " " }); assert.notEqual(result.status, 0); assert.equal(report, null); assert.ok(!result.commands.some(command => command.command === "createdb" || command.command === "npx")); }); } test("PWA 前置失败只阻断浏览器依赖,其他测试仍执行", context => { const { result, report } = runDiagnostic(context, { FAIL_BUILD_SHA: "111111111111" }); assert.equal(result.status, 2); assert.equal(report.stages.find(stage => stage.id === "inputs").status, "FAIL"); assert.equal(report.stages.find(stage => stage.id === "server-unit").status, "PASS"); assert.equal(report.stages.find(stage => stage.id === "browser").status, "NOT_RUN"); assert.ok(!result.commands.some(command => command.args.includes("test:e2e:full"))); }); test("建库失败不执行该套件,后续独立新库仍可验证", context => { const { result, report } = runDiagnostic(context, { FAIL_PREPARE_STAGE: "workflow-actions" }); assert.equal(result.status, 2); assert.equal(report.stages.find(stage => stage.id === "workflow-actions").failureKind, "environment"); assert.ok(!result.commands.some(command => command.command === "regression" && command.stage === "workflow-actions")); assert.equal(report.stages.find(stage => stage.id === "browser").status, "PASS"); }); test("清理失败必须记录,不能被测试退出 0 吞掉", context => { const { result, report } = runDiagnostic(context, { FAIL_CLEANUP_STAGE: "workflow-actions" }); assert.equal(result.status, 2); assert.equal(report.stages.find(stage => stage.id === "workflow-actions").failureKind, "cleanup"); assert.equal(report.scratchCleanup, "PASS"); }); test("必要构建失败明确阻断写库及浏览器,不把缺环境记为产品红项", context => { const { result, report } = runDiagnostic(context, { FAIL_STAGES: "client-build" }); assert.equal(result.status, 2); assert.equal(report.stages.find(stage => stage.id === "client-build").failureKind, "environment"); for (const id of ["workflow-actions", "server-regressions", "browser"]) { assert.equal(report.stages.find(stage => stage.id === id).status, "NOT_RUN"); } assert.ok(report.browser.cases.every(entry => entry.status === "NOT_RUN")); assert.ok(!result.commands.some(command => command.command === "createdb")); }); test("移除诊断固定非零保护的源码变异可观测", context => { const owned = fixture(context, diagnosticApproval); const source = path.join(owned.root, "scripts/qa/full-local-diagnostic.mjs"); const original = readFileSync(source, "utf8"); const mutated = original.replace("process.exitCode = 2;", "process.exitCode = 0;"); assert.notEqual(mutated, original); writeFileSync(source, mutated); const result = owned.run("run-web-gates.sh", ["full-local-diagnostic"]); assert.equal(result.status, 0, result.stderr); assert.throws(() => assert.equal(result.status, 2)); }); test("移除诊断 trace 实参的源码变异会被真实命令捕获拒绝", context => { const owned = fixture(context, diagnosticApproval); const source = path.join(owned.root, "scripts/qa/full-local-diagnostic-stage.sh"); const original = readFileSync(source, "utf8"); const mutated = original.replace("qa_run_full_browser --trace=on", "qa_run_full_browser"); assert.notEqual(mutated, original); writeFileSync(source, mutated); const result = owned.run("run-web-gates.sh", ["full-local-diagnostic"]); assert.equal(result.status, 2, result.stderr); const browser = result.commands.find(command => command.args.includes("test:e2e:full")); assert.ok(browser); assert.ok(!browser.args.includes("--trace=on")); assert.throws(() => assert.deepEqual(browser.args.filter(argument => argument.startsWith("--trace")), ["--trace=on"])); }); test("普通 full-local 在回归前串行准备账号,随后不传播初始密码", context => { const owned = fixture(context); const result = owned.run(); assert.equal(result.status, 0, result.stderr); const seed = result.commands.find(c => c.args.includes("seed:dev")); const preparations = result.commands.filter(c => c.args[0]?.endsWith("prepare-local-e2e-accounts.mjs")); assert.equal(preparations.length, 1); assert.equal(seed.seedRolesPrepared, true); assert.equal(preparations[0].syntheticInitialPresent, true); assert.ok(result.commands.indexOf(seed) < result.commands.indexOf(preparations[0])); const regression = result.commands.find(c => c.command === "regression"); assert.ok(result.commands.indexOf(preparations[0]) < result.commands.indexOf(regression)); const browser = result.commands.find(c => c.args.includes("test:e2e:full")); assert.equal(browser.syntheticInitialPresent, false); assert.equal(browser.seedRolesPrepared, false); }); test("普通 full-local 账号准备失败阻断回归及浏览器,删前置反例会被抓到", context => { const owned = fixture(context, { FAIL_ACCOUNT_PREPARATION: "1" }); const verify = result => { assert.equal(result.status, 43); assert.ok(!result.commands.some(c => c.command === "regression" || c.args.includes("test:e2e:full"))); assert.ok(result.commands.some(c => c.command === "dropdb")); }; verify(owned.run()); const file = path.join(owned.root, "scripts/qa/run-web-gates.sh"); const source = readFileSync(file, "utf8"); writeFileSync(file, source.replace("qa_full_stage accounts qa_prepare_synthetic_accounts", "qa_full_stage accounts : # removed preparation")); assert.throws(() => verify(owned.run()), assert.AssertionError); }); for (const suite of ["workflow-actions", "server-regressions"]) { test(`受管限定服务端只运行已选套件并保持无发布资格 ${suite}`, context => { const owned = fixture(context, { QA_RETAINED_SERVER_SUITE: suite }); const result = owned.run("run-web-gates.sh", ["retained-server-local"]); assert.equal(result.status, 0, result.stderr); assert.deepEqual(result.commands.filter(c => c.command === "regression").map(c => c.stage), [suite]); assert.ok(!result.commands.some(c => c.args.includes("--list") || c.args.includes("test:e2e:full"))); const selection = JSON.parse(readFileSync(path.join(owned.root, "report/selection.json"))); assert.equal(selection.normalReleaseEligible, false); assert.equal(selection.scope, "focused-server-regression"); assert.equal(selection.suite, suite); const regressionIndex = result.commands.findIndex(c => c.command === "regression"); const accountPreparation = result.commands.find(c => c.args[0]?.endsWith("prepare-local-e2e-accounts.mjs")); assert.equal(accountPreparation.imNotifierEnabled, suite === "server-regressions" ? "true" : "false"); for (const script of ["browser-environment-probe.mjs", "prepare-local-e2e-accounts.mjs"]) { const index = result.commands.findIndex(c => c.args[0]?.endsWith(script)); assert.ok(index >= 0 && index < regressionIndex); } assert.ok(result.commands.some(c => c.command === "dropdb")); }); } for (const suite of [undefined, "../workflow-actions", "workflow-actions; true"]) { test(`限定服务端拒绝缺失或任意命令选择 ${suite}`, context => { const owned = fixture(context, suite ? { QA_RETAINED_SERVER_SUITE: suite } : {}); const result = owned.run("run-web-gates.sh", ["retained-server-local"]); assert.notEqual(result.status, 0); assert.ok(!result.commands.some(c => c.command === "createdb" || c.command === "regression")); }); } for (const flag of ["FAIL_ENVIRONMENT_PROBE", "FAIL_ACCOUNT_PREPARATION"]) { test(`限定服务端前置失败不进入回归 ${flag}`, context => { const owned = fixture(context, { QA_RETAINED_SERVER_SUITE: "workflow-actions", [flag]: "1" }); const result = owned.run("run-web-gates.sh", ["retained-server-local"]); assert.notEqual(result.status, 0); assert.ok(!result.commands.some(c => c.command === "regression")); assert.ok(result.commands.some(c => c.command === "dropdb")); }); } for (const [overrides, enabled] of [ [{}, true], [{ PLAYWRIGHT_OCR_MOCK: "0" }, true], [{ PLAYWRIGHT_OCR_MOCK: "1", JOBS_ENABLED_FOR_E2E: "false" }, false], [{ PLAYWRIGHT_OCR_MOCK: "1", OCR_PROVIDER: "external-fixture" }, false], ]) { test(`OCR 修正能力绑定已启动服务而非调用者标志 ${JSON.stringify(overrides)}`, context => { const owned = fixture(context, overrides); const result = owned.run(); assert.equal(result.status, 0, result.stderr); const browser = result.commands.find(c => c.args.includes("test:e2e:full")); assert.equal(browser.ocrMock, enabled ? "1" : "0"); assert.equal(browser.ocrMock === "1", browser.ocrProvider === "mock" && browser.jobsEnabled === "true"); const capabilities = readFileSync(path.join(owned.root, "report/server-capabilities.jsonl"), "utf8").trim().split("\n").map(JSON.parse); assert.equal(capabilities.at(-1).mockCorrectionEnabled, enabled); assert.equal(capabilities.at(-1).source, "owned-server-start"); }); } test("分层 full-local 真实入口从总发现派生集合并保留公网与冻结义务", context => { const owned = fixture(context, { LAYERED_FIXTURE: "1" }); const result = owned.run(); assert.equal(result.status, 0, result.stderr); const plan = JSON.parse(readFileSync(path.join(owned.root, "report/coverage-plan.json"))); const receipt = JSON.parse(readFileSync(path.join(owned.root, "report/local-coverage-result.json"))); assert.equal(plan.totalCases, 4); assert.equal(plan.localCases.length, 2); assert.deepEqual(receipt.obligations.map(row => row.status), ["NOT_RUN_LOCAL", "FROZEN_PROTECTION_PASS"]); assert.equal(receipt.deliveryStatus, "INCOMPLETE"); assert.equal(receipt.normalReleaseEligible, false); const browser = result.commands.find(command => command.args.includes("test:e2e:full")); assert.ok(!browser.args.some(arg => arg.includes("live.spec") || arg.includes("frozen.spec"))); }); for (const flag of ["OMIT_EXECUTION_CASE", "SKIP_EXECUTION_CASE"]) test(`即使浏览器退出零也拒绝漏例或跳过:${flag}`, context => { const owned = fixture(context, { LAYERED_FIXTURE: "1", [flag]: "1" }); const result = owned.run(); assert.notEqual(result.status, 0); assert.equal(JSON.parse(readFileSync(path.join(owned.root, "report/local-coverage-result.json"))).status, "FAIL"); }); test("删除真实覆盖消费必须使跳过反例失去阻断", context => { const owned = fixture(context, { LAYERED_FIXTURE: "1", SKIP_EXECUTION_CASE: "1" }); const assertBlocked = result => assert.notEqual(result.status, 0); assertBlocked(owned.run()); const source = path.join(owned.root, "scripts/qa/run-web-gates.sh"); const original = readFileSync(source, "utf8"); const protectedCall = 'node "$ROOT_DIR/scripts/qa/full-local-coverage.mjs" verify-local "$QA_FULL_REPORT_DIR"'; assert.ok(original.includes(protectedCall)); writeFileSync(source, original.replace(protectedCall, ':')); assert.throws(() => assertBlocked(owned.run()), assert.AssertionError); });