import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; import { MarkdownExporter } from '../../../src/plugins/exporters/MarkdownExporter'; import { IAnomaly, IAction } from '../../../src/core/interfaces'; function makeAnomaly(overrides: Partial = {}): IAnomaly { const action: IAction = { id: 'act-1', type: 'navigate', url: 'http://localhost:3000/register', timestamp: 1700000000000, seed: 42, stateId: 's1', }; return { id: 'anom-md-001', type: 'http_error', severity: 'high', observationId: 'obs-1', actionTrace: [action], description: 'Form submission returns HTTP 500 on empty email field', evidence: { screenshotPath: 'anom-md-001/screenshot.png', domSnapshotPath: 'anom-md-001/dom.html', httpLog: [{ url: '/api/register', status: 500, method: 'POST', durationMs: 234 }], rawErrors: ['POST /api/register → 500 (234ms)'], }, timestamp: 1700000000000, ...overrides, }; } describe('MarkdownExporter', () => { let tmpDir: string; beforeEach(() => { tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'abe-md-')); }); afterEach(() => { fs.rmSync(tmpDir, { recursive: true, force: true }); }); it('creates report.md in the output directory', async () => { const exporter = new MarkdownExporter(); const outputDir = path.join(tmpDir, 'anom-md-001'); const filePath = await exporter.export(makeAnomaly(), outputDir); expect(fs.existsSync(filePath)).toBe(true); expect(filePath.endsWith('report.md')).toBe(true); }); it('includes anomaly type and date in title', async () => { const exporter = new MarkdownExporter(); const outputDir = path.join(tmpDir, 'title-test'); const filePath = await exporter.export(makeAnomaly(), outputDir); const content = fs.readFileSync(filePath, 'utf8'); expect(content).toContain('# Bug Report'); expect(content).toContain('http_error'); }); it('includes severity section', async () => { const exporter = new MarkdownExporter(); const outputDir = path.join(tmpDir, 'severity-test'); const filePath = await exporter.export(makeAnomaly(), outputDir); const content = fs.readFileSync(filePath, 'utf8'); expect(content).toContain('## Severity'); expect(content).toContain('high'); }); it('includes reproduction steps with navigate action', async () => { const exporter = new MarkdownExporter(); const outputDir = path.join(tmpDir, 'steps-test'); const filePath = await exporter.export(makeAnomaly(), outputDir); const content = fs.readFileSync(filePath, 'utf8'); expect(content).toContain('## Reproduction Steps'); expect(content).toContain('Navigate to'); expect(content).toContain('http://localhost:3000/register'); }); it('includes seed and replay command', async () => { const exporter = new MarkdownExporter(); const outputDir = path.join(tmpDir, 'seed-test'); const filePath = await exporter.export(makeAnomaly(), outputDir); const content = fs.readFileSync(filePath, 'utf8'); expect(content).toContain('Seed used'); expect(content).toContain('42'); expect(content).toContain('Replay command'); expect(content).toContain('npm run replay'); }); it('includes evidence section with screenshot and dom paths', async () => { const exporter = new MarkdownExporter(); const outputDir = path.join(tmpDir, 'evidence-test'); const filePath = await exporter.export(makeAnomaly(), outputDir); const content = fs.readFileSync(filePath, 'utf8'); expect(content).toContain('## Evidence'); expect(content).toContain('screenshot.png'); expect(content).toContain('dom.html'); }); it('includes HTTP table when responses present', async () => { const exporter = new MarkdownExporter(); const outputDir = path.join(tmpDir, 'http-test'); const filePath = await exporter.export(makeAnomaly(), outputDir); const content = fs.readFileSync(filePath, 'utf8'); expect(content).toContain('500'); expect(content).toContain('/api/register'); expect(content).toContain('POST'); }); it('includes raw errors section', async () => { const exporter = new MarkdownExporter(); const outputDir = path.join(tmpDir, 'errors-test'); const filePath = await exporter.export(makeAnomaly(), outputDir); const content = fs.readFileSync(filePath, 'utf8'); expect(content).toContain('## Raw Errors'); expect(content).toContain('POST /api/register'); }); it('has format = markdown', () => { expect(new MarkdownExporter().format).toBe('markdown'); }); it('handles anomaly with no action trace', async () => { const exporter = new MarkdownExporter(); const outputDir = path.join(tmpDir, 'no-trace-test'); const anomaly = makeAnomaly({ actionTrace: [] }); const filePath = await exporter.export(anomaly, outputDir); const content = fs.readFileSync(filePath, 'utf8'); expect(content).toContain('No steps recorded'); }); });