文件
hyb-oa/scripts/qa/tests/release-gate-result.test.mjs
T
wangsli 1f8d5b658a 完善借款、编号、付款与预算并发及验收(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

140 行
13 KiB
JavaScript

import assert from 'node:assert/strict';
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync, symlinkSync } from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { spawnSync } from 'node:child_process';
import test from 'node:test';
import { captureReleaseSource, verifyReleaseEvidence, validateSkipRequest, confinedPath } from '../release-gate-result.mjs';
import { coveragePlan, localSpecArgs } from '../full-local-coverage.mjs';
import { FULL_LOCAL_STAGES, newStageRecord, updateStageRecord, validateStages } from '../qa-stage-record.mjs';
const time = seconds => new Date(Date.UTC(2026,8,18,0,0,seconds)).toISOString();
function stages(ids, profile, start=1) {
return {schemaVersion:1,profile,status:'PASS',exitCode:0,startedAt:time(start),endedAt:time(start+ids.length*2+1),stages:ids.map((id,index)=>({id,status:'PASS',exitCode:0,startedAt:time(start+index*2),endedAt:time(start+index*2+1)}))};
}
function fixture(context) {
const root=mkdtempSync(path.join(os.tmpdir(),'hyb-release-evidence-'));
context.after(()=>rmSync(root,{recursive:true,force:true}));
mkdirSync(path.join(root,'client/tests/e2e'),{recursive:true});
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}));
for(const row of rows) writeFileSync(path.join(root,row.file),'synthetic');
const source={commit:'a'.repeat(40),tree:'b'.repeat(40),digest:'c'.repeat(64),untrackedDigest:'d'.repeat(64),dirty:false,repository:{trackedCoverageComplete:true,trackedDigest:'e'.repeat(64),indexDigest:'f'.repeat(64),porcelainV1Z:''}};
const policy={digest:'policy',environmentDigest:'environment',runnerDigest:'runner',probeDigest:'probe',toolchain:{node:process.version,playwright:'1.58.2',browserRegistry:'browser',browserVersions:{chromium:'synthetic',webkit:'synthetic'},platform:'linux',arch:'x64',ci:false,workers:4}};
const facts={root,source,policy,contract:{schemaVersion:1,approvalReference:'synthetic',exceptions:[]},manifest:{localCandidateFlows:rows.map((row,i)=>({bundle:`flow-${i}`,witnessTag:`@local-candidate:flow-${i}`,witnessSpec:row.file})),acceptanceSpecs:rows.map(row=>row.file)}};
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:4,version:'1.58.2'};
execution.stats={startTime:time(26),duration:500,expected:10,unexpected:0,flaky:0,skipped:0};
const environment={schemaVersion:1,profile:'isolated-online-v1',mode:'full-local',policyDigest:'environment',platform:'linux',status:'PASS',exitCode:0,childExit:0,childSignal:null,interrupted:null,startedAt:time(3),endedAt:time(30),sourceBefore:structuredClone(source),sourceAfter:structuredClone(source),sourceUnchanged:true,repositoryUnchanged:true,candidateIntegrityStatus:'UNCHANGED',cleanup:{containerOwnerVerified:true,containerRemoved:true,scratchRemoved:true},environment:{schemaVersion:1,profile:'isolated-online-v1',policyDigest:'environment',platform:'linux',runnerDigest:'runner',probeDigest:'probe',supervisorObservation:{source:'live-supervisor-unix-ipc',sampledAt:time(3),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 record={schemaVersion:1,profile:'release-predeploy',mode:'verify-candidate',runId:'a'.repeat(24),startedAt:time(0),finishedAt:time(33),executionExit:0,policy:structuredClone(policy),sourceBefore:structuredClone(source),sourceAfter:structuredClone(source),phases:stages(['static','full-local'],'release-predeploy-stages')};
record.phases.endedAt=time(32);record.phases.stages[1].endedAt=time(31);
const bundle={environment,phases:stages(FULL_LOCAL_STAGES,'full-local-stages',4),discovery,execution,plan:coveragePlan(root,discovery,facts.contract,source)};
bundle.selection={schemaVersion:1,profile:'full-local-selection',specArgs:localSpecArgs(bundle.plan),additionalArgs:[],retries:0,forbidOnly:true,workers:4,mode:'local'};
return {root,record,bundle,facts,verify:()=>verifyReleaseEvidence(record,bundle,facts)};
}
test('完整同源分阶段结果获得发布前资格,但公网交付仍未完成',context=>{
const f=fixture(context), result=f.verify();assert.equal(result.status,'PASS',JSON.stringify(result.issues));assert.equal(result.deploymentEligible,true);assert.equal(result.deliveryStatus,'INCOMPLETE');assert.equal(result.localCases.length,10);assert.equal(result.witnessSummary.passed,10);
});
test('dirty 开发树可完成本地检查,但始终 PARTIAL/无发布资格',context=>{
const f=fixture(context);
for(const source of [f.facts.source,f.record.sourceBefore,f.record.sourceAfter,f.bundle.environment.sourceBefore,f.bundle.environment.sourceAfter]){source.dirty=true;source.repository.porcelainV1Z=' M client/file\0';}
const result=f.verify();assert.equal(result.localChecksPassed,true,JSON.stringify(result.issues));assert.equal(result.status,'PARTIAL');assert.equal(result.deploymentEligible,false);
});
const mutations={
'missing-receipt':f=>{f.record={};},
'old-schema':f=>{f.record.schemaVersion=0;},
'focused-profile':f=>{f.record.profile='local-candidate';},
'no-finish':f=>{f.record.finishedAt=null;},
'failed-run':f=>{f.record.executionExit=17;},
'interrupted':f=>{f.record.interrupted='SIGTERM';},
'phase-missing':f=>{f.record.phases.stages.pop();},
'phase-skipped':f=>{f.record.phases.stages[0].status='SKIP';},
'phase-failed':f=>{f.record.phases.stages[0].exitCode=3;},
'phase-reversed':f=>{f.record.phases.stages.reverse();},
'phase-after-completion':f=>{f.record.phases.endedAt=time(40);},
'inner-phase-missing':f=>{f.bundle.phases.stages.pop();},
'inner-phase-failed':f=>{f.bundle.phases.stages[2].status='FAIL';},
'inner-profile-invalid':f=>{f.bundle.phases.profile='diagnostic';},
'inner-time-outside-run':f=>{f.bundle.phases.startedAt=time(0);},
'changed-commit':f=>{f.facts.source.commit='1'.repeat(40);},
'changed-source':f=>{f.facts.source.digest='1'.repeat(64);},
'changed-index':f=>{f.facts.source.repository.indexDigest='1'.repeat(64);},
'changed-untracked-input':f=>{f.facts.source.untrackedDigest='1'.repeat(64);},
'source-unknown':f=>{f.record.sourceBefore.repository.trackedCoverageComplete=false;},
'policy-drift':f=>{f.record.policy.digest='old';},
'toolchain-drift':f=>{f.record.policy.toolchain.playwright='old';},
'environment-missing':f=>{delete f.bundle.environment;},
'environment-focused':f=>{f.bundle.environment.mode='retained-local';},
'environment-child-failed':f=>{f.bundle.environment.childExit=2;},
'environment-exit-failed':f=>{f.bundle.environment.exitCode=2;},
'environment-source-mismatch':f=>{f.bundle.environment.sourceBefore.digest='1'.repeat(64);},
'environment-self-claimed':f=>{delete f.bundle.environment.environment.namespace;},
'native-offline':f=>{f.bundle.environment.environment.nativeOnlineProbe.online=false;},
'webkit-version-drift':f=>{f.bundle.environment.environment.nativeOnlineProbe.engines[1].version='old';},
'webkit-native-offline':f=>{f.bundle.environment.environment.nativeOnlineProbe.engines[1].online=false;},
'guest-refresh':f=>{f.bundle.environment.environment.bootstrapObservation.refreshRequestCount=1;},
'guest-mock':f=>{f.bundle.environment.environment.bootstrapObservation.noRouteMocks=false;},
'runner-digest-drift':f=>{f.bundle.environment.environment.runnerDigest='old';},
'cleanup-missing':f=>{f.bundle.environment.cleanup.containerRemoved=false;},
'discovery-truncated':f=>{f.bundle.discovery.suites.pop();},
'plan-truncated':f=>{f.bundle.plan.localCases.pop();},
'execution-missing':f=>{f.bundle.execution.suites.pop();},
'execution-duplicate':f=>{f.bundle.execution.suites.push(f.bundle.execution.suites[0]);},
'execution-unknown':f=>{f.bundle.execution.suites[0].specs[0].id='unknown';},
'execution-skipped':f=>{f.bundle.execution.suites[0].specs[0].tests[0].results[0].status='skipped';},
'execution-retried':f=>{f.bundle.execution.suites[0].specs[0].tests[0].results[0].retry=1;},
'execution-xfail':f=>{f.bundle.execution.suites[0].specs[0].tests[0].expectedStatus='failed';},
'execution-global-error':f=>{f.bundle.execution.errors.push({message:'synthetic'});},
'selection-grep':f=>{f.bundle.selection.additionalArgs=['--grep=one'];},
'selection-missing':f=>{delete f.bundle.selection;},
'config-missing':f=>{delete f.bundle.execution.config;},
'projects-missing':f=>{delete f.bundle.execution.config.projects;},
'only-allowed':f=>{f.bundle.execution.config.forbidOnly=false;},
'sharded':f=>{f.bundle.execution.config.shard={current:1,total:2};},
'serialized':f=>{f.bundle.execution.config.workers=1;},
'budget-increased':f=>{f.bundle.execution.config.projects[0].timeout=360000;},
'stats-false-pass':f=>{f.bundle.execution.stats.expected=11;},
'browser-time-mismatch':f=>{f.bundle.execution.stats.startTime=time(0);},
'acceptance-missing':f=>{f.facts.manifest.acceptanceSpecs.push('client/tests/e2e/absent.spec.ts');},
'witness-missing':f=>{f.bundle.execution.suites[0].specs[0].title='missing';},
};
for(const [name,mutate] of Object.entries(mutations))test(`拒绝伪通过:${name}`,context=>{
const f=fixture(context);mutate(f);const r=verifyReleaseEvidence(f.record,f.bundle,f.facts);assert.equal(r.deploymentEligible,false);assert.ok(r.issues.length);
});
test('阶段记录保留真实失败、未跑阶段与清理失败,不吞早期错误',()=>{
const r=newStageRecord(['a','b'],'fixture');assert.throws(()=>updateStageRecord(r,'start','b'));
updateStageRecord(r,'start','a');updateStageRecord(r,'finish',null,23);
assert.equal(r.status,'FAIL');assert.equal(r.exitCode,23);assert.equal(r.stages[0].exitCode,23);assert.equal(r.stages[1].status,'NOT_RUN');assert.throws(()=>updateStageRecord(r,'start','b'));
const complete=newStageRecord(['a'],'fixture');updateStageRecord(complete,'start','a');updateStageRecord(complete,'end','a',0);updateStageRecord(complete,'finish',null,31);assert.ok(validateStages(complete,['a'],'fixture').length);
});
test('skip 精确值和批准记录,仅诊断 PARTIAL,普通发布前入口拒绝',()=>{
assert.equal(validateSkipRequest('verify-candidate',{}),null);
for(const value of ['true','yes','01','', '2'])assert.throws(()=>validateSkipRequest('project-wide',{PROJECT_WIDE_SKIP_VISUAL:value}));
const env={PROJECT_WIDE_SKIP_VISUAL:'1',PROJECT_WIDE_SKIP_REASON:'synthetic',PROJECT_WIDE_SKIP_OPERATOR:'fixture',PROJECT_WIDE_SKIP_APPROVAL_REF:'fixture-only'};
assert.throws(()=>validateSkipRequest('verify-candidate',env));
for(const key of ['PROJECT_WIDE_SKIP_REASON','PROJECT_WIDE_SKIP_OPERATOR','PROJECT_WIDE_SKIP_APPROVAL_REF'])assert.throws(()=>validateSkipRequest('project-wide',{...env,[key]:''}));
const r=validateSkipRequest('project-wide',env);assert.equal(r.status,'PARTIAL');assert.equal(r.deploymentEligible,false);assert.deepEqual(r.affectedStages,['project-wide-visual']);
});
test('回执路径拒绝越界、现存或断裂符号链接',context=>{
const f=fixture(context);
for(const input of ['../outside','/absolute','a/../b','a//b','a\\b'])assert.throws(()=>confinedPath(f.root,input));
symlinkSync('/nonexistent-synthetic-target',path.join(f.root,'link'));
assert.throws(()=>confinedPath(f.root,'link'));
symlinkSync(f.root,path.join(f.root,'directory'));
assert.throws(()=>confinedPath(f.root,'directory/file'));
});
test('原有未跟踪部署文件内容变化,即使 Git status 相同也使来源变化',context=>{
const f=fixture(context);
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:f.root}).status,0);
mkdirSync(path.join(f.root,'deploy'));writeFileSync(path.join(f.root,'deploy/new.sh'),'first');
const before=captureReleaseSource(f.root);writeFileSync(path.join(f.root,'deploy/new.sh'),'second');const after=captureReleaseSource(f.root);
assert.equal(before.repository.porcelainV1Z,after.repository.porcelainV1Z);assert.notEqual(before.untrackedDigest,after.untrackedDigest);
});