Files

41 lines
4.4 KiB
TypeScript

import { readFile, mkdir, writeFile, rm, readdir, stat } from 'node:fs/promises';
import { createHash } from 'node:crypto';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { upstream58e2204Operations } from '../../operation-registry/src/upstream-58e2204.ts';
import { normalizeConfig } from '../../../server/config/schema.js';
import { collectOne, selectReadonlyOperations, validateInstanceOrigin } from './collector.ts';
const here=path.dirname(fileURLToPath(import.meta.url));const pkg=path.resolve(here,'..');const repo=path.resolve(pkg,'../..');
export function loadInstances(config:any){
if(!config||!Array.isArray(config.instances)||config.instances.length!==2)throw new Error('config must contain exactly two instances');
for(const raw of config.instances){
if(!raw||typeof raw!=='object'||typeof raw.url!=='string')throw new Error('invalid collector instance configuration');
validateInstanceOrigin(raw.url);
if(raw.password||raw.auth?.password||!['none',undefined].includes(raw.auth?.mode))throw new Error('collector instances must be password-free');
}
const normalized=normalizeConfig(config,{HOST:'127.0.0.1',PORT:'8788'});
if(normalized.instances.length!==2)throw new Error('config must contain exactly two instances');
return normalized.instances.map((raw:any,i:number)=>{
if(raw.auth.mode!=='none'||raw.auth.password)throw new Error(`instance-${i+1} must be password-free`);
return {origin:validateInstanceOrigin(raw.url),alias:`instance-${i+1}`};
});
}
async function mapLimit<T,R>(xs:T[],limit:number,fn:(x:T)=>Promise<R>){const out:R[]=[];let n=0;async function worker(){while(n<xs.length){const i=n++;out[i]=await fn(xs[i]);}}await Promise.all(Array.from({length:Math.min(limit,xs.length)},worker));return out;}
async function allJson(dir:string):Promise<string[]>{const out:string[]=[];for(const e of await readdir(dir,{withFileTypes:true})){const p=path.join(dir,e.name);if(e.isDirectory())out.push(...await allJson(p));else if(e.name.endsWith('.json'))out.push(p);}return out;}
function safeName(s:string){return s.replace(/[^a-zA-Z0-9_-]/g,'-');}
export async function main(argv=process.argv.slice(2)){
if(argv.length!==1||!['--dry-run','--capture'].includes(argv[0]))throw new Error('usage: collect-readonly-fixtures.ts --dry-run|--capture');
const config=JSON.parse(await readFile(path.join(repo,'config.json'),'utf8'));const instances=loadInstances(config);const {selected,denied}=selectReadonlyOperations(upstream58e2204Operations);
if(argv[0]==='--dry-run'){for(const op of selected)console.log(`registry ${op.operationId}`);console.log(`selected=${selected.length} denied=${denied.length}`);return;}
const shapePath=path.join(pkg,'src/response-shapes-58e2204.json');
// Separately reviewed: capture attests this baseline but never creates it.
const shapeBytes=await readFile(shapePath);
const root=path.join(pkg,'src/simadmin');await rm(root,{recursive:true,force:true});await rm(path.join(pkg,'src/manifest.json'),{force:true});await mkdir(root,{recursive:true});
const jobs=instances.flatMap(instance=>selected.map(op=>({instance,op})));const fixtures=await mapLimit(jobs,2,async({instance,op})=>{const f=await collectOne(instance,op);console.log(`${instance.alias} ${op.operationId} ${f.statusCategory}`);return {f,domain:op.upstreamDomain};});
for(const {f,domain} of fixtures){const dir=path.join(root,safeName(domain));await mkdir(dir,{recursive:true});await writeFile(path.join(dir,`${f.sourceInstanceAlias}--${f.operationId}.json`),JSON.stringify(f,null,2)+'\n',{flag:'wx'});}
const files=[];const coverage:Record<string,number>={};for(const full of await allJson(root)){const text=await readFile(full);const rel=path.relative(pkg,full);const domain=path.basename(path.dirname(full));coverage[domain]=(coverage[domain]||0)+1;files.push({path:rel,size:(await stat(full)).size,sha256:createHash('sha256').update(text).digest('hex')});}
files.sort((a,b)=>a.path.localeCompare(b.path));const shapeBaseline={path:'src/response-shapes-58e2204.json',size:shapeBytes.length,sha256:createHash('sha256').update(shapeBytes).digest('hex')};await writeFile(path.join(pkg,'src/manifest.json'),JSON.stringify({schemaVersion:1,upstreamBaseline:'58e2204',realFixtureCount:files.length,syntheticFixtureCount:0,domainCoverage:Object.fromEntries(Object.entries(coverage).sort()),shapeBaseline,files},null,2)+'\n');
}
if(process.argv[1]===fileURLToPath(import.meta.url))main().catch(()=>{console.error('collector failed');process.exitCode=1;});