完善借款、编号、付款与预算并发及验收(09-06-release-gate-integrity)
汇总已落盘的借款本金与还款、兼容历史编号的周期化编号、付款责任与回单核验、报销进度和排序、PWA/采购事务、错误原因与日期校验及ERP退役。 统一共享预算池锁序和锁后判断,恢复差旅原预算选择,补原因码固定夹具及反向完整性检查。 纳入配套迁移、测试、受管QA、分层验收合同、规约及任务记录;原始证据留在ignored artifacts。 关联任务:09-17-company-loan-principal-repayment、09-17-erp-exclusive-retirement、09-18-reimbursement-periodic-numbering、09-08-reimbursement-payment-closeout、09-18-local-concurrency-product-fixes、09-19-budget-pool-concurrency、09-05-error-explainability-payment-readiness。 预算补审由Claude定向验证2文件928项通过,原4446/28及r6结果仍仅对应各自历史输入;本轮未重跑测试。 完整新候选门禁、R1真实入口接入与12项公网义务仍开放。生产编号未启用、差旅仍冻结;不推送、不部署,不宣称发布资格。
这个提交包含在:
@@ -1,4 +1,5 @@
|
||||
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";
|
||||
@@ -31,8 +32,16 @@ function fixture(context, overrides = {}) {
|
||||
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"]) copyFileSync(path.join(projectRoot, "scripts/qa", file), path.join(root, "scripts/qa", file));
|
||||
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}
|
||||
@@ -49,20 +58,21 @@ 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,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');
|
||||
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'].includes(args[1])) {
|
||||
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] === '-e')) {
|
||||
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);
|
||||
}
|
||||
@@ -91,7 +101,10 @@ 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);
|
||||
@@ -115,6 +128,10 @@ if (!['node','npm','npx','curl','psql','createdb','dropdb'].includes(command)) t
|
||||
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)) : [] };
|
||||
},
|
||||
@@ -128,12 +145,15 @@ for (const ci of [undefined, "1"]) {
|
||||
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"));
|
||||
@@ -330,3 +350,132 @@ test("移除诊断 trace 实参的源码变异会被真实命令捕获拒绝", c
|
||||
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);
|
||||
});
|
||||
|
||||
在新工单中引用
屏蔽一个用户