import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; import { JSONExporter } from '../../../src/plugins/exporters/JSONExporter'; import { IAnomaly, IAction } from '../../../src/core/interfaces'; function makeAnomaly(id = 'anom-001'): IAnomaly { const action: IAction = { id: 'act-1', type: 'click', selector: '#submit', timestamp: 1700000000000, seed: 42, stateId: 's1', }; return { id, type: 'http_error', severity: 'high', observationId: 'obs-1', actionTrace: [action], description: 'HTTP 500 on form submit', evidence: { httpLog: [{ url: '/api/register', status: 500, method: 'POST', durationMs: 234 }], rawErrors: ['POST /api/register → 500'], }, timestamp: 1700000000000, }; } describe('JSONExporter', () => { let tmpDir: string; beforeEach(() => { tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'abe-json-')); }); afterEach(() => { fs.rmSync(tmpDir, { recursive: true, force: true }); }); it('creates report.json in the output directory', async () => { const exporter = new JSONExporter('http://localhost:3000'); const outputDir = path.join(tmpDir, 'anom-001'); const filePath = await exporter.export(makeAnomaly(), outputDir); expect(fs.existsSync(filePath)).toBe(true); expect(filePath.endsWith('report.json')).toBe(true); }); it('produces valid JSON', async () => { const exporter = new JSONExporter('http://localhost:3000'); const outputDir = path.join(tmpDir, 'anom-001'); const filePath = await exporter.export(makeAnomaly(), outputDir); const content = fs.readFileSync(filePath, 'utf8'); expect(() => JSON.parse(content)).not.toThrow(); }); it('includes all required top-level fields', async () => { const exporter = new JSONExporter('http://localhost:3000'); const outputDir = path.join(tmpDir, 'anom-001'); const filePath = await exporter.export(makeAnomaly(), outputDir); const report = JSON.parse(fs.readFileSync(filePath, 'utf8')); expect(report.version).toBe('1.0'); expect(report.generated_at).toBeTruthy(); expect(report.environment).toBeDefined(); expect(report.anomaly).toBeDefined(); expect(report.reproduction).toBeDefined(); expect(report.evidence).toBeDefined(); }); it('includes anomaly details', async () => { const exporter = new JSONExporter(); const anomaly = makeAnomaly('test-id'); const outputDir = path.join(tmpDir, 'test-id'); const filePath = await exporter.export(anomaly, outputDir); const report = JSON.parse(fs.readFileSync(filePath, 'utf8')); expect(report.anomaly.id).toBe('test-id'); expect(report.anomaly.type).toBe('http_error'); expect(report.anomaly.severity).toBe('high'); }); it('includes reproduction steps', async () => { const exporter = new JSONExporter(); const outputDir = path.join(tmpDir, 'steps-test'); const filePath = await exporter.export(makeAnomaly(), outputDir); const report = JSON.parse(fs.readFileSync(filePath, 'utf8')); expect(report.reproduction.steps).toHaveLength(1); expect(report.reproduction.steps[0].step).toBe(1); expect(report.reproduction.steps[0].action_type).toBe('click'); }); it('includes HTTP log in evidence', async () => { const exporter = new JSONExporter(); const outputDir = path.join(tmpDir, 'http-test'); const filePath = await exporter.export(makeAnomaly(), outputDir); const report = JSON.parse(fs.readFileSync(filePath, 'utf8')); expect(report.evidence.http_log).toHaveLength(1); expect(report.evidence.http_log[0].status).toBe(500); }); it('has format = json', () => { expect(new JSONExporter().format).toBe('json'); }); it('creates output directory if it does not exist', async () => { const exporter = new JSONExporter(); const nestedDir = path.join(tmpDir, 'nested', 'deep', 'anom-001'); await exporter.export(makeAnomaly(), nestedDir); expect(fs.existsSync(nestedDir)).toBe(true); }); });