完善借款、编号、付款与预算并发及验收(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项公网义务仍开放。生产编号未启用、差旅仍冻结;不推送、不部署,不宣称发布资格。
这个提交包含在:
2026-09-19 13:11:04 +08:00
父节点 729d90576f
当前提交 1f8d5b658a
共修改 367 个文件,包含 21079 行新增和 12287 行删除
@@ -0,0 +1,135 @@
import assert from 'node:assert/strict';
import { spawnSync } from 'node:child_process';
import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
import path from 'node:path';
import os from 'node:os';
import { fileURLToPath } from 'node:url';
import test from 'node:test';
const projectRoot=path.resolve(path.dirname(fileURLToPath(import.meta.url)),'../../..');
const receiptFiles={environment:'environment/result.json',phases:'results/full-local-stages.json',discovery:'results/discovery.json',plan:'results/coverage-plan.json',execution:'results/playwright-results.json',selection:'results/execution-selection.json'};
function fixture(context,overrides={}) {
const root=mkdtempSync(path.join(os.tmpdir(),'hyb-release-shell-'));
context.after(()=>rmSync(root,{recursive:true,force:true}));
for(const dir of ['scripts/qa','deploy/trialrun','client/tests/e2e','server','bin','client/node_modules/@playwright/test','client/node_modules/playwright-core'])mkdirSync(path.join(root,dir),{recursive:true});
for(const file of readdirSync(path.join(projectRoot,'scripts/qa')).filter(file=>file.endsWith('.mjs')||file.endsWith('.sh')||file.endsWith('.py')))copyFileSync(path.join(projectRoot,'scripts/qa',file),path.join(root,'scripts/qa',file));
const rows=Array.from({length:10},(_,i)=>({file:`client/tests/e2e/flow-${i}.spec.ts`,id:`flow-${i}`,title:`flow-${i} @local-candidate:flow-${i}`,line:1}));
rows.push({file:'client/tests/e2e/ordinary.spec.ts',id:'ordinary',title:'ordinary',line:1});
for(const row of rows)writeFileSync(path.join(root,row.file),'synthetic');
const manifest={localCandidateFlows:rows.slice(0,10).map((row,i)=>({bundle:`flow-${i}`,witnessTag:`@local-candidate:flow-${i}`,witnessSpec:row.file})),acceptanceSpecs:rows.map(row=>row.file)};
const contract={schemaVersion:1,approvalReference:'synthetic',exceptions:[]};
writeFileSync(path.join(root,'scripts/qa/route-manifest.json'),JSON.stringify(manifest));
writeFileSync(path.join(root,'scripts/qa/full-local-coverage-contract.json'),JSON.stringify(contract));
for(const file of ['client/package.json','client/package-lock.json','server/package.json','server/package-lock.json','client/node_modules/playwright-core/browsers.json'])writeFileSync(path.join(root,file),'{}');
writeFileSync(path.join(root,'client/node_modules/playwright-core/browsers.json'),JSON.stringify({browsers:['chromium','webkit'].map(name=>({name,browserVersion:'synthetic'}))}));
writeFileSync(path.join(root,'client/node_modules/@playwright/test/package.json'),JSON.stringify({version:'1.58.2'}));
writeFileSync(path.join(root,'client/playwright.config.ts'),'// synthetic policy');
writeFileSync(path.join(root,'deploy/trialrun/validate-env-contract.js'),'// fake tooling owns this command');
writeFileSync(path.join(root,'.gitignore'),'artifacts/\nbin/\nnode_modules/\n');
const env={PATH:`${path.join(root,'bin')}:${process.env.PATH}`,LANG:'C.UTF-8',HYB_RELEASE_FIXTURE_ROOT:root,RELEASE_GATE_ARTIFACT_DIR:path.join(root,'artifacts/release-gate/run'),...overrides};
const fake=`#!${process.execPath}
const fs=require('node:fs'),path=require('node:path'),{spawnSync}=require('node:child_process');
const root=process.env.HYB_RELEASE_FIXTURE_ROOT,args=process.argv.slice(2),name=path.basename(process.argv[1]);
fs.mkdirSync(path.join(root,'artifacts'),{recursive:true});fs.appendFileSync(path.join(root,'artifacts/commands.jsonl'),JSON.stringify({name,args})+'\\n');
if(name==='node' && args[0]?.endsWith('release-gate-result.mjs')){const result=spawnSync(process.execPath,args,{env:process.env,stdio:'inherit'});process.exit(result.status??1);}
if(name==='node' && args[0]?.endsWith('validate-env-contract.js') && process.env.FAIL_STATIC)process.exit(23);
if(name==='bash' && args[0]?.endsWith('run-web-gates.sh') && args[1]==='full-local'){if(process.env.OMIT_EVIDENCE)process.exit(0);const result=spawnSync(process.execPath,[path.join(root,'bin/evidence.mjs')],{env:process.env,stdio:'inherit'});process.exit(result.status??1);}
if(name==='bash' && args[0]?.includes('deploy/trialrun'))process.exit(91);
if(!['node','npm','bash','python3'].includes(name))throw Error('unexpected command');
`;
writeFileSync(path.join(root,'bin/fake'),fake,{mode:0o755});
for(const name of ['node','npm','bash','python3'])symlinkSync('fake',path.join(root,'bin',name));
writeFileSync(path.join(root,'bin/evidence.mjs'),`
import {mkdirSync,writeFileSync,readFileSync} from 'node:fs';
import path from 'node:path';
import {captureReleaseSource,currentPolicy} from '../scripts/qa/release-gate-result.mjs';
import {FULL_LOCAL_STAGES,newStageRecord,updateStageRecord} from '../scripts/qa/qa-stage-record.mjs';
import {coveragePlan,localSpecArgs} from '../scripts/qa/full-local-coverage.mjs';
const root=process.env.HYB_RELEASE_FIXTURE_ROOT,base=process.env.QA_FULL_REPORT_DIR,now=()=>new Date().toISOString();
const source=captureReleaseSource(root),policy=currentPolicy(root),startedAt=now();
const phases=newStageRecord(FULL_LOCAL_STAGES,'full-local-stages');
const rows=${JSON.stringify(rows)},contract=${JSON.stringify(contract)};
const report=executed=>({errors:[],suites:rows.map(row=>({file:row.file,specs:[{...row,tests:[{projectName:'',repeatEachIndex:0,...(executed?{expectedStatus:'passed',status:'expected',results:[{status:'passed',retry:0}]}:{})}]}]}))});
const discovery=report(false),execution=report(true);
execution.config={forbidOnly:true,projects:[{name:'',retries:0,repeatEach:1,timeout:180000,testIgnore:['**/manual/**']}],shard:null,maxFailures:0,workers:policy.toolchain.workers,version:policy.toolchain.playwright};
for(const stage of FULL_LOCAL_STAGES){updateStageRecord(phases,'start',stage);if(stage==='browser')execution.stats={startTime:now(),duration:0,expected:rows.length,unexpected:0,flaky:0,skipped:0};updateStageRecord(phases,'end',stage,0);}updateStageRecord(phases,'finish',null,0);
const environment={schemaVersion:1,profile:'isolated-online-v1',mode:'full-local',policyDigest:policy.environmentDigest,platform:process.platform,status:'PASS',exitCode:0,childExit:0,childSignal:null,interrupted:null,startedAt,endedAt:now(),sourceBefore:source,sourceAfter:source,sourceUnchanged:true,repositoryUnchanged:true,candidateIntegrityStatus:'UNCHANGED',cleanup:{containerOwnerVerified:true,containerRemoved:true,scratchRemoved:true},environment:{schemaVersion:1,profile:'isolated-online-v1',policyDigest:policy.environmentDigest,platform:process.platform,runnerDigest:policy.runnerDigest,probeDigest:policy.probeDigest,supervisorObservation:{source:'live-supervisor-unix-ipc',sampledAt:now(),namespace:'parent'},namespace:{inheritedRunner:'parent',ownedChild:'child',runnerCreated:true,currentMatchesOwnedChild:true,topologyMatched:true},nativeOnlineProbe:{valid:true,online:true,browserNamespace:'child',browserMatchesOwnedChild:true,noNavigatorOverride:true,engines:['chromium','webkit'].map(name=>({name,version:'synthetic',online:true,valid:true,noNavigatorOverride:true,browserNamespace:'child',browserMatchesOwnedChild:true}))},bootstrapObservation:{valid:true,observationPoint:'guest-entry-black-box',freshContext:true,initialSessionHint:false,listenerBeforeNavigation:true,noRouteMocks:true,loginFormVisible:true,refreshRequestCount:0,loginRequestCount:0,nativeOnlineSamples:{beforeNavigation:true,documentStart:true,afterForm:true},browserNamespace:'child',browserMatchesOwnedChild:true,noNavigatorOverride:true,documentStatus:200,stayedOnLogin:true}}};
const bundle={environment,phases,discovery,execution,plan:coveragePlan(root,discovery,contract,source)};
bundle.selection={schemaVersion:1,profile:'full-local-selection',specArgs:localSpecArgs(bundle.plan),additionalArgs:[],retries:0,forbidOnly:true,workers:policy.toolchain.workers,mode:'local'};
if(process.env.BAD_PROFILE)environment.mode='retained-local';
if(process.env.BAD_SOURCE)environment.sourceBefore={...source,digest:'0'.repeat(64)};
if(process.env.BAD_COVERAGE)execution.suites.pop();
if(process.env.BAD_CLEANUP)environment.cleanup.containerRemoved=false;
if(process.env.BAD_STAGE)phases.stages.splice(2,1);
for(const [key,relative] of Object.entries(${JSON.stringify(receiptFiles)})){const file=path.join(base,relative);mkdirSync(path.dirname(file),{recursive:true});writeFileSync(file,JSON.stringify(bundle[key]));}
if(process.env.FAIL_FULL)process.exit(17);
`);
for(const args of [['init','-q'],['add','.'],['-c','user.name=QA Fixture','-c','[email protected]','commit','-qm','synthetic fixture']])assert.equal(spawnSync('git',args,{cwd:root}).status,0);
return {root,env,run(mode='verify-candidate') {
rmSync(path.join(root,'artifacts'),{recursive:true,force:true});
const result=spawnSync('/bin/bash',[path.join(root,'scripts/qa/run-release-gate.sh'),mode],{cwd:root,env,encoding:'utf8',timeout:30000});
const file=path.join(env.RELEASE_GATE_ARTIFACT_DIR,'release-result.json');
return {...result,record:existsSync(file)?JSON.parse(readFileSync(file)):null,commands:existsSync(path.join(root,'artifacts/commands.jsonl'))?readFileSync(path.join(root,'artifacts/commands.jsonl'),'utf8').trim().split('\n').map(JSON.parse):[]};
},verify() {return spawnSync(process.execPath,[path.join(root,'scripts/qa/release-gate-result.mjs'),'verify',env.RELEASE_GATE_ARTIFACT_DIR],{cwd:root,env,encoding:'utf8',timeout:30000});}};
}
function noDeploy(result) {assert.ok(!result.commands.some(c=>c.args.some(arg=>arg.includes('deploy/trialrun/deploy'))));}
test('真实 shell 复用 static/full-local、回执离线重验、部署命令零调用',context=>{
const f=fixture(context),r=f.run();assert.equal(r.status,0,r.stdout+r.stderr);assert.equal(r.record.status,'PASS');assert.equal(r.record.verification.localChecksPassed,true);assert.deepEqual(r.record.phases.stages.map(s=>s.id),['static','full-local']);noDeploy(r);assert.equal(f.verify().status,0);
});
for(const flag of ['FAIL_STATIC','FAIL_FULL','OMIT_EVIDENCE','BAD_COVERAGE','BAD_CLEANUP','BAD_STAGE'])test(`shell 假工具退出零也不能绕过:${flag}`,context=>{
const f=fixture(context,{[flag]:'1'}),r=f.run();assert.notEqual(r.status,0,r.stdout+r.stderr);assert.equal(r.record.deploymentEligible,false);noDeploy(r);
if(flag==='FAIL_STATIC'){assert.equal(r.status,23);assert.equal(r.record.phases.stages[0].exitCode,23);assert.equal(r.record.phases.stages[1].status,'NOT_RUN');assert.ok(!r.commands.some(c=>c.name==='bash'));}
if(flag==='FAIL_FULL')assert.equal(r.status,17);
});
test('dirty 本地树执行完整阶段但 PARTIAL/退出2',context=>{
const f=fixture(context);writeFileSync(path.join(f.root,'deploy/untracked.sh'),'synthetic');const r=f.run();assert.equal(r.status,2,r.stdout+r.stderr);assert.equal(r.record.verification.localChecksPassed,true);assert.equal(r.record.status,'PARTIAL');assert.equal(f.verify().status,2);noDeploy(r);
});
test('实际回执拒绝文件摘要篡改、截断、删除和链接',context=>{
const f=fixture(context),r=f.run();assert.equal(r.status,0,r.stdout+r.stderr);
const file=path.join(f.root,r.record.fullLocalDirectory,'results/playwright-results.json'),raw=readFileSync(file);
for(const mutate of [()=>writeFileSync(file,'{'),()=>writeFileSync(file,raw.toString()+' '),()=>rmSync(file),()=>{rmSync(file);symlinkSync('/outside-synthetic',file); }]){
writeFileSync(file,raw);mutate();assert.notEqual(f.verify().status,0);rmSync(file,{force:true});
}
});
for(const flags of [{PROJECT_WIDE_SKIP_VISUAL:'1'},{PROJECT_WIDE_SKIP_BUSINESS_FLOW:'1'},{PROJECT_WIDE_SKIP_VISUAL:'1',PROJECT_WIDE_SKIP_BUSINESS_FLOW:'1'}])test(`跳过有批准元数据时只限诊断 PARTIAL/2:${JSON.stringify(flags)}`,context=>{
const f=fixture(context,{...flags,PROJECT_WIDE_SKIP_REASON:'synthetic',PROJECT_WIDE_SKIP_OPERATOR:'fixture',PROJECT_WIDE_SKIP_APPROVAL_REF:'fixture-only'});
const rejected=f.run();assert.notEqual(rejected.status,0);assert.ok(!rejected.commands.some(c=>c.name==='npm'||c.name==='bash'));noDeploy(rejected);
const r=f.run('project-wide');assert.equal(r.status,2,r.stdout+r.stderr);assert.doesNotMatch(r.stdout,/\[PASS\]/);noDeploy(r);
const receipt=JSON.parse(readFileSync(path.join(f.env.RELEASE_GATE_ARTIFACT_DIR,'skip-receipt.json')));assert.equal(receipt.status,'PARTIAL');assert.equal(receipt.deploymentEligible,false);
});
test('删掉真实 skip 前置校验会使反例失败',context=>{
const f=fixture(context,{PROJECT_WIDE_SKIP_VISUAL:'yes'});
const blocked=r=>{assert.notEqual(r.status,0);assert.ok(!r.commands.some(c=>c.name==='npm'));};blocked(f.run('project-wide'));
const file=path.join(f.root,'scripts/qa/run-release-gate.sh'),original=readFileSync(file,'utf8'),mutated=original.replace(' node "$ROOT_DIR/scripts/qa/release-gate-result.mjs" skip "$ARTIFACT_ROOT" "$MODE"',' : # removed skip validation');assert.notEqual(original,mutated);writeFileSync(file,mutated);
assert.throws(()=>blocked(f.run('project-wide')),assert.AssertionError);
});
test('删除最终回执消费会使缺证据反例失败',context=>{
const f=fixture(context,{OMIT_EVIDENCE:'1'});const blocked=r=>assert.notEqual(r.status,0);blocked(f.run());
const file=path.join(f.root,'scripts/qa/run-release-gate.sh'),original=readFileSync(file,'utf8'),mutated=original.replace(' node "$ROOT_DIR/scripts/qa/release-gate-result.mjs" finish "$ARTIFACT_ROOT" "$original_status" || receipt_status=$?',' receipt_status=0 # removed verification');assert.notEqual(original,mutated);writeFileSync(file,mutated);
assert.throws(()=>blocked(f.run()),assert.AssertionError);
});
test('放宽真实阶段验证使缺阶段反例失败',context=>{
const f=fixture(context,{BAD_STAGE:'1'});const blocked=r=>assert.notEqual(r.status,0);blocked(f.run());
const file=path.join(f.root,'scripts/qa/qa-stage-record.mjs'),original=readFileSync(file,'utf8'),mutated=original.replace('export function validateStages(record, expected, profile) {','export function validateStages(record, expected, profile) { return [];');assert.notEqual(original,mutated);writeFileSync(file,mutated);
const r=f.run();assert.equal(r.status,2,r.stdout+r.stderr); // mutation makes source dirty, still no release eligibility
assert.equal(r.record.verification.localChecksPassed,true);
});
for(const [flag,from,to] of [
['BAD_PROFILE', "env?.mode!=='full-local' || ", ''],
['BAD_SOURCE', "issues.push('managed_source_mismatch');", "; // removed source validation"],
['BAD_COVERAGE', 'issues.push(...coverage.issues);', '; // removed coverage validation'],
])test(`移除实际 profile/source/coverage 保护使反例失效:${flag}`,context=>{
const f=fixture(context,{[flag]:'1'}),first=f.run();
assert.notEqual(first.status,0);assert.equal(first.record.verification.localChecksPassed,false);noDeploy(first);
const file=path.join(f.root,'scripts/qa/release-gate-result.mjs'),original=readFileSync(file,'utf8'),mutated=original.replace(from,to);
assert.notEqual(original,mutated);writeFileSync(file,mutated);
const result=f.run();assert.equal(result.status,2,result.stdout+result.stderr);
assert.equal(result.record.verification.localChecksPassed,true);noDeploy(result);
});
test('候选路径越界在建目录和静态执行前拒绝',context=>{
const f=fixture(context);f.env.RELEASE_GATE_ARTIFACT_DIR=path.join(f.root,'unapproved-directory');
const r=f.run();assert.notEqual(r.status,0);assert.equal(existsSync(f.env.RELEASE_GATE_ARTIFACT_DIR),false);assert.ok(!r.commands.some(c=>c.name==='npm'));noDeploy(r);
});