| 1 | import { Miniflare, convertV4MiniflareOptions } from 'miniflare'; |
| 2 | import { readFileSync, readdirSync } from 'node:fs'; |
| 3 | import { resolve } from 'node:path'; |
| 4 | |
| 5 | // Local-only integration fixture: real built API/Work Workers and D1. |
| 6 | // Authentication, repository lookup and event transport are deterministic |
| 7 | // fixtures. No production credentials or remote service bindings are used. |
| 8 | const root = resolve(import.meta.dirname, '..'); |
| 9 | const user = { id: 'usr_test', username: 'tester', verified: true, workspaces: [] }; |
| 10 | const repo = { id: 'rep_test', namespace: 'tester', name: 'rehearsal', description: null, isPrivate: false, ownerId: user.id, defaultBranch: 'main', forkOf: null, createdAt: '2026-10-14T00:00:00Z' }; |
| 11 | const stub = `const user=${JSON.stringify(user)}; const repo=${JSON.stringify(repo)}; |
| 12 | export default { async fetch(request) { |
| 13 | const method = new URL(request.url).pathname.split('/').pop(); |
| 14 | const body = await request.json(); |
| 15 | if (method === 'user_for_access_token') return Response.json(body.token === 'local-rehearsal' ? user : body.token === 'local-other' ? {...user,id:'usr_other',username:'other'} : null); |
| 16 | if (method === 'get') return Response.json({ok:true,value:repo}); |
| 17 | if (method === 'publish') return Response.json(null); |
| 18 | if (method === 'audit_record') return Response.json(1); |
| 19 | if (method === 'resolve_slug' || method === 'resolve_path') return Response.json(null); |
| 20 | if (method === 'display_usernames') return Response.json({}); |
| 21 | console.error('Unimplemented local fixture RPC: '+method); return new Response('Unimplemented local fixture RPC: '+method,{status:500}); |
| 22 | }};`; |
| 23 | const serviceNames = ['IDENTITY','REPOS','EVENTS','RUNNER','BILLING','INTEGRATIONS','WEBHOOKS','ACTIONS','SEARCH','SECURITY','PROJECTS','DEPLOYMENTS','TELEMETRY','PACKAGES','ARTIFACTS']; |
| 24 | const bindings = Object.fromEntries(serviceNames.map(name => [name, 'fixture'])); |
| 25 | const worker = (name, path, extra) => ({ name, modules: [{type:'ESModule',path:resolve(root,path,'build/index.js')},{type:'CompiledWasm',path:resolve(root,path,'build/index_bg.wasm')}], compatibilityDate:'2026-09-26', ...extra }); |
| 26 | export async function startLocal({ port = 8799, persist = false } = {}) { |
| 27 | const mf = new Miniflare(convertV4MiniflareOptions({ host:'127.0.0.1', port, resourcePersistencePath: persist || undefined, workers:[ |
| 28 | worker('api','apps/api',{serviceBindings:{...bindings,WORK:'work'}, bindings:{MCP_URL:`http://127.0.0.1:${port}/mcp`,API_URL:`http://127.0.0.1:${port}`,SITE_URL:`http://127.0.0.1:${port}`}}), |
| 29 | worker('work','services/work',{serviceBindings:bindings,d1Databases:{DB:'local-coordination'}}), |
| 30 | {name:'fixture',modules:true,script:stub,compatibilityDate:'2026-09-26'} |
| 31 | ]})); |
| 32 | const ready = await mf.ready; |
| 33 | const db = await mf.getD1Database('DB','work'); |
| 34 | const initialized = await db.prepare("SELECT name FROM sqlite_master WHERE name = 'sessions'").first(); |
| 35 | if (!initialized) { |
| 36 | for (const file of readdirSync(resolve(root,'services/work/migrations')).filter(f=>f.endsWith('.sql')).sort()) { |
| 37 | try { |
| 38 | // These migrations contain no triggers. Keep each complete statement on |
| 39 | // one line for D1.exec, whose API treats newlines as statement boundaries. |
| 40 | const sql = readFileSync(resolve(root,'services/work/migrations',file),'utf8') |
| 41 | .replace(/--[^\n]*/g,'').replace(/\r?\n/g,' ').split(';').map(s=>s.trim()).filter(Boolean).join(';\n'); |
| 42 | await db.exec(sql); |
| 43 | } |
| 44 | catch(error) { await mf.dispose(); throw new Error(`${file}: ${error.message}`); } |
| 45 | } |
| 46 | } |
| 47 | return {mf, db, url:ready.origin}; |
| 48 | } |
| 49 | if (process.argv[1] && resolve(process.argv[1]) === resolve(import.meta.filename)) { |
| 50 | const {mf,url} = await startLocal({persist:resolve(root,'target/submission-audit/local-coordination')}); |
| 51 | console.log(`Local rehearsal ready: ${url}; MCP: ${url}/mcp; token: local-rehearsal (fixture only)`); |
| 52 | for (const signal of ['SIGINT','SIGTERM']) process.on(signal,async()=>{await mf.dispose();process.exit(0);}); |
| 53 | } |