汇总已落盘的借款本金与还款、兼容历史编号的周期化编号、付款责任与回单核验、报销进度和排序、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项公网义务仍开放。生产编号未启用、差旅仍冻结;不推送、不部署,不宣称发布资格。
140 行
13 KiB
JavaScript
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);
|
|
});
|