86 行
11 KiB
JavaScript
86 行
11 KiB
JavaScript
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';
|
|
|
|
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'};
|
|
export 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));
|
|
for(const file of readdirSync(path.join(projectRoot,'deploy/trialrun')).filter(file=>/\.(mjs|sh|yml)$/.test(file)))copyFileSync(path.join(projectRoot,'deploy/trialrun',file),path.join(root,'deploy/trialrun',file));
|
|
for(const dir of ['docs/trialrun','server/src/scripts','bin/home/.ssh'])mkdirSync(path.join(root,dir),{recursive:true});
|
|
writeFileSync(path.join(root,'bin/home/.ssh/config'),'Host fixture-refresh\n');
|
|
writeFileSync(path.join(root,'docs/trialrun/OPS7运维交接Runbook.md'),'curl -fsS https://fixture.invalid/ready\n');
|
|
for(const file of ['smoke-trialrun-runtime.js','smoke-external-deps.js'])writeFileSync(path.join(root,'server/src/scripts',file),'// synthetic');
|
|
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');
|
|
// Private child environment: no real HOME/config, credentials or fallback PATH.
|
|
const env={PATH:path.join(root,'bin'),HOME:path.join(root,'bin/home'),GIT_CONFIG_NOSYSTEM:'1',GIT_CONFIG_GLOBAL:'/dev/null',GIT_CONFIG_SYSTEM:'/dev/null',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}\nrequire('./deploy-fake.cjs');\n`;
|
|
writeFileSync(path.join(root,'bin/fake'),fake,{mode:0o755});
|
|
for(const name of ['node','npm','bash','python3','git','ssh','rsync','curl','docker'])symlinkSync('fake',path.join(root,'bin',name));
|
|
for(const name of ['dirname','date','mkdir','mktemp','tar','rm'])symlinkSync('/usr/bin/'+name,path.join(root,'bin',name));
|
|
copyFileSync(path.join(projectRoot,'scripts/qa/tests/fixtures/release-deploy-fake.cjs'),path.join(root,'bin/deploy-fake.cjs'));
|
|
copyFileSync(path.join(projectRoot,'scripts/qa/tests/fixtures/release-fs-guard.cjs'),path.join(root,'bin/fs-guard.cjs'));
|
|
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('/usr/bin/git',args,{cwd:root,env}).status,0);
|
|
const commands=()=>existsSync(path.join(root,'artifacts/commands.jsonl'))?readFileSync(path.join(root,'artifacts/commands.jsonl'),'utf8').trim().split('\n').filter(Boolean).map(JSON.parse):[];
|
|
return {root,env,commands,commit() {
|
|
for(const args of [['add','.'],['-c','user.name=QA Fixture','-c','[email protected]','commit','-qm','synthetic mutation']])assert.equal(spawnSync('/usr/bin/git',args,{cwd:root,env}).status,0);
|
|
},invoke(entry,overrides={}) {
|
|
rmSync(path.join(root,'artifacts/commands.jsonl'),{force:true});
|
|
const script=entry==='bootstrap'?'deploy/trialrun/production-identity-bootstrap.mjs':entry==='existing'?'deploy/trialrun/deploy-existing.sh':entry==='raw'?'deploy/trialrun/deploy.sh':'scripts/qa/run-release-gate.sh';
|
|
const argv=[path.join(root,script),...(['ci-release','all','deploy'].includes(entry)?[entry]:[])];
|
|
const childEnv={...env,...overrides};
|
|
const executable=entry==='bootstrap'?process.execPath:'/bin/bash';
|
|
if(entry==='bootstrap')argv.unshift('--require',path.join(root,'bin/fs-guard.cjs'));
|
|
const result=spawnSync(executable,argv,{cwd:root,env:childEnv,encoding:'utf8',timeout:30000});
|
|
return {...result,commands:commands()};
|
|
},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});}};
|
|
}
|