完善借款、编号、付款与预算并发及验收(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项公网义务仍开放。生产编号未启用、差旅仍冻结;不推送、不部署,不宣称发布资格。
这个提交包含在:
@@ -9,7 +9,7 @@ import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
import { sourceSnapshot, compareSnapshots } from "./full-local-diagnostic.mjs";
|
||||
|
||||
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
||||
const policyFiles = ["scripts/qa/full-local-environment.mjs", "scripts/qa/full-local-environment-inner.sh", "scripts/qa/local-postgres-proxy.py", "scripts/qa/browser-environment-probe.mjs", "scripts/qa/run-web-gates.sh", "scripts/qa/full-local-diagnostic.mjs", "client/playwright.config.ts"];
|
||||
const policyFiles = ["scripts/qa/full-local-environment.mjs", "scripts/qa/full-local-environment-inner.sh", "scripts/qa/local-postgres-proxy.py", "scripts/qa/browser-environment-probe.mjs", "scripts/qa/run-web-gates.sh", "scripts/qa/prepare-local-e2e-accounts.mjs", "scripts/qa/full-local-diagnostic.mjs", "client/playwright.config.ts"];
|
||||
const sha = value => createHash("sha256").update(value).digest("hex");
|
||||
const readJson = file => JSON.parse(readFileSync(file, "utf8"));
|
||||
export function atomicEnvironmentJson(file, value) {
|
||||
@@ -60,17 +60,27 @@ export async function requestSupervisorObservation(context) {
|
||||
});
|
||||
});
|
||||
}
|
||||
export function topologyMatches(addresses, routes, ipv6Routes) {
|
||||
export function topologyMatches(addresses, routes, ipv6Routes, links) {
|
||||
const names = addresses.map(entry => entry.ifname).sort();
|
||||
if (JSON.stringify(names) !== JSON.stringify(["lo", "qaonline", "qapeer"])) return false;
|
||||
for (const [name, address] of [["qaonline", "192.0.2.1"], ["qapeer", "192.0.2.2"]]) {
|
||||
const entry = addresses.find(item => item.ifname === name);
|
||||
if (!entry.flags?.includes("UP") || entry.operstate !== "UP" || !entry.addr_info?.some(item => item.local === address && item.prefixlen === 30)) return false;
|
||||
}
|
||||
return ![...routes, ...ipv6Routes].some(entry => entry.dst === "default" || entry.gateway);
|
||||
if (!Array.isArray(links) || JSON.stringify(links.map(entry => entry.ifname).sort()) !== JSON.stringify(names)) return false;
|
||||
const left = links.find(entry => entry.ifname === "qaonline"), right = links.find(entry => entry.ifname === "qapeer");
|
||||
if (left.ifindex === right.ifindex) return false;
|
||||
for (const [entry, peer] of [[left, right], [right, left]]) {
|
||||
if (!Number.isInteger(entry.ifindex) || entry.ifindex <= 0 || entry.linkinfo?.info_kind !== "veth" || entry.link_netnsid !== undefined || entry.master !== undefined) return false;
|
||||
if (entry.link !== peer.ifname && entry.link_index !== peer.ifindex) return false;
|
||||
}
|
||||
const defaults = routes.filter(entry => entry.dst === "default");
|
||||
if (defaults.length !== 1 || defaults[0].dev !== "qaonline" || defaults[0].scope !== "link" || (defaults[0].table !== undefined && defaults[0].table !== "main")) return false;
|
||||
if (ipv6Routes.some(entry => entry.dst === "default")) return false;
|
||||
return ![...routes, ...ipv6Routes].some(entry => entry.gateway || entry.via || entry.multipath || entry.nexthops || entry.encap || !names.includes(entry.dev));
|
||||
}
|
||||
function topology() {
|
||||
return topologyMatches(JSON.parse(command("ip", ["-j", "address", "show"])), JSON.parse(command("ip", ["-j", "route", "show", "table", "all"])), JSON.parse(command("ip", ["-j", "-6", "route", "show", "table", "all"])));
|
||||
return topologyMatches(JSON.parse(command("ip", ["-j", "address", "show"])), JSON.parse(command("ip", ["-j", "route", "show", "table", "all"])), JSON.parse(command("ip", ["-j", "-6", "route", "show", "table", "all"])), JSON.parse(command("ip", ["-j", "-d", "link", "show"])));
|
||||
}
|
||||
export function validateEnvironmentFacts(context, facts) {
|
||||
const failures = [];
|
||||
@@ -112,6 +122,8 @@ export function validateEnvironmentReceipt(receipt, digest, requireGuest = true)
|
||||
if (!namespace?.inheritedRunner || !namespace.ownedChild || namespace.inheritedRunner === namespace.ownedChild || namespace.runnerCreated !== true || namespace.currentMatchesOwnedChild !== true || namespace.topologyMatched !== true) failures.push("environment_receipt_namespace_invalid");
|
||||
if (receipt?.nativeOnlineProbe?.browserNamespace !== namespace?.ownedChild || receipt?.nativeOnlineProbe?.noNavigatorOverride !== true) failures.push("native_probe_namespace_invalid");
|
||||
if (!receipt?.nativeOnlineProbe?.valid || receipt.nativeOnlineProbe.online !== true || !receipt.nativeOnlineProbe.browserMatchesOwnedChild) failures.push("native_probe_invalid");
|
||||
const engines = receipt?.nativeOnlineProbe?.engines;
|
||||
if (!Array.isArray(engines) || JSON.stringify(engines.map(engine => engine.name)) !== JSON.stringify(["chromium", "webkit"]) || engines.some(engine => engine.online !== true || engine.valid !== true || engine.noNavigatorOverride !== true || engine.browserNamespace !== namespace?.ownedChild || engine.browserMatchesOwnedChild !== true || !engine.version)) failures.push("native_engine_probe_invalid");
|
||||
if (requireGuest) {
|
||||
const observed = receipt?.bootstrapObservation;
|
||||
if (observed?.browserNamespace !== namespace?.ownedChild || observed?.noNavigatorOverride !== true || observed?.loginRequestCount !== 0 || observed?.documentStatus !== 200 || observed?.stayedOnLogin !== true) failures.push("guest_probe_evidence_invalid");
|
||||
@@ -136,7 +148,7 @@ export function environmentReceipt(contextFile = process.env.QA_ENVIRONMENT_CONT
|
||||
return receipt;
|
||||
}
|
||||
async function launch(mode) {
|
||||
if (!["full-local", "full-local-diagnostic"].includes(mode)) throw new Error("environment_mode_invalid");
|
||||
if (!["full-local", "full-local-diagnostic", "retained-local", "retained-server-local"].includes(mode)) throw new Error("environment_mode_invalid");
|
||||
if (process.platform !== "linux") throw new Error("environment_platform_unsupported");
|
||||
if (mode === "full-local-diagnostic") for (const name of ["REASON", "OPERATOR", "APPROVAL_REF"]) {
|
||||
const value = process.env[`QA_DIAGNOSTIC_${name}`]?.trim();
|
||||
@@ -191,7 +203,7 @@ async function launch(mode) {
|
||||
});
|
||||
chmodSync(path.join(scratch, "supervisor.sock"), 0o600);
|
||||
const env = {};
|
||||
for (const name of ["PATH", "HOME", "LANG", "TERM", "CI", "NODE_OPTIONS", "PAYMENT_ORDER_PYTHON_BIN", "QA_DIAGNOSTIC_REASON", "QA_DIAGNOSTIC_OPERATOR", "QA_DIAGNOSTIC_APPROVAL_REF"]) if (process.env[name] !== undefined) env[name] = process.env[name];
|
||||
for (const name of ["PATH", "HOME", "LANG", "TERM", "CI", "NODE_OPTIONS", "PAYMENT_ORDER_PYTHON_BIN", "QA_DIAGNOSTIC_REASON", "QA_DIAGNOSTIC_OPERATOR", "QA_DIAGNOSTIC_APPROVAL_REF", "QA_RETAINED_SPECS", "QA_RETAINED_GREP", "QA_RETAINED_SERVER_SUITE", "DOCKER_CONFIG", "PLAYWRIGHT_BROWSERS_PATH", "npm_config_userconfig", "npm_config_globalconfig"]) if (process.env[name] !== undefined) env[name] = process.env[name];
|
||||
Object.assign(env, { QA_ENVIRONMENT_CONTEXT: contextFile, QA_FULL_REPORT_DIR: path.join(base, "results"), QA_DIAGNOSTIC_REPORT_DIR: path.join(base, "results"), QA_MANAGED_SCRATCH: scratch, QA_MANAGED_SOCKET: socket, QA_HOST: "127.0.0.1", QA_PORT: "23000", QA_DB_HOST: "127.0.0.1", QA_DB_PORT: "25432", QA_DB_CLIENT: "host", QA_DB_USER: "postgres", QA_DB_PASSWORD: "local_qa_only", QA_KEEP_DB: "0", QA_SKIP_CLIENT_BUILD: "0", E2E_MODE: "local", WECOM_ENABLED: "false", EMAIL_ENABLED: "false", OIDC_ENABLED: "false", STORAGE_DRIVER: "local", NODE_ENV: "test", npm_config_offline: "true", NO_COLOR: "1" });
|
||||
const outcome = await new Promise(resolve => {
|
||||
child = spawn("unshare", ["-Urn", "bash", path.join(root, "scripts/qa/full-local-environment-inner.sh"), mode], { cwd: root, env, stdio: "inherit", detached: true });
|
||||
|
||||
在新工单中引用
屏蔽一个用户