#!/usr/bin/env node /** * E2E 冒烟: 验证 post-edit-snapshot -> validator -> rollback 链路 * 不污染生产 feature-flags (测后原子恢复) */ 'use strict'; const fs = require('fs'); const path = require('path'); const { spawnSync } = require('child_process'); const os = require('os'); const crypto = require('crypto'); const REAL_ROOT = path.resolve(__dirname, '..', '..'); const SNAPSHOT = path.join(REAL_ROOT, 'hooks', 'post-edit-snapshot.js'); // 不能在 ai-delivery-pipeline/ 下 (触发 post-edit-snapshot 的防递归守卫) const SANDBOX = path.join(os.tmpdir(), 'bookworm-e2e-' + Date.now()); const REPORT_DIR = path.join(REAL_ROOT, 'ai-delivery-pipeline', '_poc-sandbox'); const MANIFEST = path.join(REAL_ROOT, 'ai-delivery-pipeline', 'manifest.jsonl'); function setup() { fs.mkdirSync(SANDBOX, { recursive: true }); if (fs.existsSync(MANIFEST)) fs.unlinkSync(MANIFEST); } function tempFlagWarn() { const flagPath = path.join(REAL_ROOT, 'feature-flags.json'); const raw = fs.readFileSync(flagPath, 'utf8'); const json = JSON.parse(raw); const originalMode = json.features['staging-pipeline'].mode; json.features['staging-pipeline'].mode = 'warn'; const tmp = flagPath + '.tmp.' + process.pid; fs.writeFileSync(tmp, JSON.stringify(json, null, 2) + '\n'); fs.renameSync(tmp, flagPath); return function restore() { const r = fs.readFileSync(flagPath, 'utf8'); const j = JSON.parse(r); j.features['staging-pipeline'].mode = originalMode; const t = flagPath + '.tmp.' + process.pid; fs.writeFileSync(t, JSON.stringify(j, null, 2) + '\n'); fs.renameSync(t, flagPath); }; } function invokeSnapshot(filePath, sessionId) { const input = JSON.stringify({ tool_name: 'Edit', tool_input: { file_path: filePath }, session_id: sessionId, }); return spawnSync(process.execPath, [SNAPSHOT], { input, encoding: 'utf8', timeout: 10000, windowsHide: true, }); } function sleepMs(ms) { const s = Date.now() + ms; while (Date.now() < s) {} } function loadManifestEntries() { if (!fs.existsSync(MANIFEST)) return []; return fs.readFileSync(MANIFEST, 'utf8') .split('\n').filter(Boolean) .map(L => { try { return JSON.parse(L); } catch { return null; } }) .filter(Boolean); } function waitForEntry(predicate, timeoutMs) { const t0 = Date.now(); while (Date.now() - t0 < timeoutMs) { const entries = loadManifestEntries(); const match = entries.find(predicate); if (match) return match; sleepMs(50); } return null; } function scenario(name, createFileFn, expectedStagedEvent, expectValidator) { const r = { name }; const testFile = createFileFn(); const sessionId = 'e2e-' + name; const t0 = Date.now(); const snap = invokeSnapshot(testFile, sessionId); r.snapshotExit = snap.status; r.snapshotMs = Date.now() - t0; const staged = waitForEntry(e => e.sessionId === sessionId && e.event === expectedStagedEvent, 2000); r.stagedFound = !!staged; r.stagedEvent = staged ? staged.event : null; if (expectValidator && staged) { const validated = waitForEntry( e => e.hash === staged.hash && (e.event === 'validated-pass' || e.event === 'validated-fail'), 5000 ); r.validatorEvent = validated ? validated.event : 'timeout'; r.validatorMs = validated ? validated.elapsedMs : null; r.validatorFailures = validated ? validated.failures : null; } r.ok = (r.stagedFound) && (!expectValidator || (r.validatorEvent && r.validatorEvent !== 'timeout')); return r; } function makeFakeApiKeyString() { // 运行时拼接, 避免静态字符串触发安全扫描 return 'sk' + '_' + 'live' + '_' + crypto.randomBytes(24).toString('hex'); } function main() { setup(); const restoreFlag = tempFlagWarn(); try { const results = []; results.push(scenario('t1-normal-js', () => { const p = path.join(SANDBOX, 't1.js'); fs.writeFileSync(p, "'use strict';\nfunction foo(){return 1;}\nmodule.exports=foo;\n"); return p; }, 'staged', true)); results.push(scenario('t2-credential-leak', () => { const p = path.join(SANDBOX, 't2.js'); const fake = makeFakeApiKeyString(); fs.writeFileSync(p, "'use strict';\nconst k='" + fake + "';\nmodule.exports=k;\n"); return p; }, 'staged', true)); results.push(scenario('t3-syntax-error', () => { const p = path.join(SANDBOX, 't3.js'); fs.writeFileSync(p, "'use strict';\nfunction bad( { syntax error\n"); return p; }, 'staged', true)); results.push(scenario('t4-oversize', () => { const p = path.join(SANDBOX, 't4.bin'); fs.writeFileSync(p, Buffer.alloc(6 * 1024 * 1024, 0)); return p; }, 'skip-oversize', false)); const report = { hypothesis: 'E2E · staging pipeline 端到端链路', platform: os.platform() + ' ' + os.release(), timestamp: new Date().toISOString(), results, verdict: { total: results.length, passed: results.filter(r => r.ok).length, pass: results.every(r => r.ok), }, }; fs.mkdirSync(REPORT_DIR, { recursive: true }); fs.writeFileSync(path.join(REPORT_DIR, 'e2e-report.json'), JSON.stringify(report, null, 2), 'utf8'); console.log(JSON.stringify(report, null, 2)); } finally { restoreFlag(); } } main();