文件
hyb-oa/scripts/qa/tests/release-gate-shell.test.mjs

68 行
6.2 KiB
JavaScript

import assert from 'node:assert/strict';
import { existsSync, readFileSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
import path from 'node:path';
import test from 'node:test';
import { fixture } from './fixtures/release-gate-fixture.mjs';
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);
});