From 149e379c16a92046aab391bb1497bd4ba6aeb29c Mon Sep 17 00:00:00 2001 From: Nikolas de Hor Date: Wed, 25 Feb 2026 17:06:40 -0300 Subject: [PATCH 1/2] test: add 54 unit tests for IdeationEngine and all 5 Analyzers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cobre IdeationEngine (orquestrador) + PerformanceAnalyzer, SecurityAnalyzer, CodeQualityAnalyzer, UXAnalyzer e ArchitectureAnalyzer. Testes incluem: - calculatePriority: quick-wins, effort multipliers, defaults - isKnownGotcha: word overlap detection, edge cases - countByArea: aggregação por área - formatSuggestion/formatMarkdown: output formatting - ideate: orquestração, filtragem de gotchas, save - Analyzers: detecção de sync ops, secrets, eval, vulns, etc. NOTA: Requer --transform='{}' por falta de babel.config no projeto. --- tests/core/ideation/ideation-engine.test.js | 621 ++++++++++++++++++++ 1 file changed, 621 insertions(+) create mode 100644 tests/core/ideation/ideation-engine.test.js diff --git a/tests/core/ideation/ideation-engine.test.js b/tests/core/ideation/ideation-engine.test.js new file mode 100644 index 0000000000..9efdea9a8b --- /dev/null +++ b/tests/core/ideation/ideation-engine.test.js @@ -0,0 +1,621 @@ +/** + * @jest-environment node + * + * Testes unitários para o módulo ideation-engine + * + * Testa IdeationEngine (orquestrador) e os 5 Analyzers individuais + * com mocks para execSync e fs, garantindo isolamento de I/O. + */ + +const child_process = require('child_process'); +const fs = require('fs'); + +// Mock gotchas-memory antes de carregar ideation-engine +// (ideation-engine tenta `new GotchasMemory()` mas o export é { GotchasMemory: class }) +jest.mock('../../../.aios-core/core/memory/gotchas-memory', () => null); + +const execSyncSpy = jest.spyOn(child_process, 'execSync').mockReturnValue(''); +jest.spyOn(fs, 'existsSync').mockReturnValue(true); +jest.spyOn(fs, 'mkdirSync').mockImplementation(() => {}); +jest.spyOn(fs, 'writeFileSync').mockImplementation(() => {}); + +const IdeationEngine = require('../../../.aios-core/core/ideation/ideation-engine.js'); +const { + PerformanceAnalyzer, + SecurityAnalyzer, + CodeQualityAnalyzer, + UXAnalyzer, + ArchitectureAnalyzer, +} = IdeationEngine; + +describe('IdeationEngine', () => { + let engine; + + beforeEach(() => { + jest.clearAllMocks(); + execSyncSpy.mockReturnValue(''); + engine = new IdeationEngine({ + rootPath: '/fake/project', + gotchasMemory: null, + }); + }); + + // ============================================================ + // Constructor + // ============================================================ + describe('constructor', () => { + test('usa rootPath fornecido', () => { + expect(engine.rootPath).toBe('/fake/project'); + }); + + test('usa process.cwd() como padrão', () => { + const e = new IdeationEngine(); + expect(e.rootPath).toBe(process.cwd()); + }); + + test('inicializa 5 áreas padrão', () => { + expect(engine.areas).toEqual([ + 'performance', 'security', 'codeQuality', 'ux', 'architecture', + ]); + }); + + test('aceita áreas customizadas', () => { + const e = new IdeationEngine({ areas: ['security'] }); + expect(e.areas).toEqual(['security']); + }); + + test('inicializa 5 analyzers', () => { + expect(engine.analyzers.performance).toBeInstanceOf(PerformanceAnalyzer); + expect(engine.analyzers.security).toBeInstanceOf(SecurityAnalyzer); + expect(engine.analyzers.codeQuality).toBeInstanceOf(CodeQualityAnalyzer); + expect(engine.analyzers.ux).toBeInstanceOf(UXAnalyzer); + expect(engine.analyzers.architecture).toBeInstanceOf(ArchitectureAnalyzer); + }); + }); + + // ============================================================ + // calculatePriority + // ============================================================ + describe('calculatePriority', () => { + test('quick-win: low effort + impact >= 0.7 → prioridade alta', () => { + const finding = { effort: 'low', impact: 0.8 }; + const priority = engine.calculatePriority(finding); + expect(priority).toBe(0.8 * 1.5); // 1.2 + expect(finding.category).toBe('quick-win'); + }); + + test('quick-win: low effort + impact 0.7 exato', () => { + const finding = { effort: 'low', impact: 0.7 }; + engine.calculatePriority(finding); + expect(finding.category).toBe('quick-win'); + }); + + test('low effort + impact < 0.7 → NÃO é quick-win', () => { + const finding = { effort: 'low', impact: 0.6 }; + const priority = engine.calculatePriority(finding); + // low effort multiplier = 1.5, mas não é quick-win + expect(priority).toBe(0.6 * 1.5); + expect(finding.category).toBeUndefined(); + }); + + test('medium effort usa multiplicador 1.0', () => { + const finding = { effort: 'medium', impact: 0.5 }; + expect(engine.calculatePriority(finding)).toBe(0.5); + }); + + test('high effort usa multiplicador 0.6', () => { + const finding = { effort: 'high', impact: 0.8 }; + expect(engine.calculatePriority(finding)).toBeCloseTo(0.48); + }); + + test('effort desconhecido usa multiplicador 1.0', () => { + const finding = { effort: 'unknown', impact: 0.5 }; + expect(engine.calculatePriority(finding)).toBe(0.5); + }); + + test('impact padrão é 0.5 quando não fornecido', () => { + const finding = { effort: 'medium' }; + expect(engine.calculatePriority(finding)).toBe(0.5); + }); + }); + + // ============================================================ + // isKnownGotcha + // ============================================================ + describe('isKnownGotcha', () => { + test('retorna false quando knownIssues é null', () => { + const suggestion = { title: 'Test', description: 'desc' }; + expect(engine.isKnownGotcha(suggestion, null)).toBe(false); + }); + + test('retorna false quando knownIssues é vazio', () => { + const suggestion = { title: 'Test', description: 'desc' }; + expect(engine.isKnownGotcha(suggestion, [])).toBe(false); + }); + + test('detecta gotcha com 3+ palavras em comum (>4 chars)', () => { + const suggestion = { + title: 'Synchronous operations blocking event loop', + description: 'Multiple readFileSync calls detected', + }; + const knownIssues = [{ + pattern: 'readFileSync blocking event', + description: 'synchronous operations cause performance issues', + }]; + expect(engine.isKnownGotcha(suggestion, knownIssues)).toBe(true); + }); + + test('não detecta gotcha com <3 palavras em comum', () => { + const suggestion = { + title: 'Missing aria labels', + description: 'Interactive elements need accessibility', + }; + const knownIssues = [{ + pattern: 'database timeout', + description: 'connection pooling issues', + }]; + expect(engine.isKnownGotcha(suggestion, knownIssues)).toBe(false); + }); + + test('ignora palavras curtas (<=4 chars) na comparação', () => { + const suggestion = { + title: 'the and for but not with', + description: 'small words only here and there', + }; + const knownIssues = [{ + pattern: 'the and for but not with', + description: 'small words also matching', + }]; + // Nenhuma palavra >4 chars → matches.length = 0 < 3 + expect(engine.isKnownGotcha(suggestion, knownIssues)).toBe(false); + }); + + test('gotcha sem pattern/description não causa erro', () => { + const suggestion = { title: 'Test', description: 'desc' }; + const knownIssues = [{ id: 'gotcha-1' }]; + expect(engine.isKnownGotcha(suggestion, knownIssues)).toBe(false); + }); + }); + + // ============================================================ + // countByArea + // ============================================================ + describe('countByArea', () => { + test('conta sugestões por área', () => { + const suggestions = [ + { area: 'security' }, + { area: 'performance' }, + { area: 'security' }, + { area: 'ux' }, + ]; + expect(engine.countByArea(suggestions)).toEqual({ + security: 2, + performance: 1, + ux: 1, + }); + }); + + test('retorna objeto vazio para array vazio', () => { + expect(engine.countByArea([])).toEqual({}); + }); + }); + + // ============================================================ + // formatSuggestion + // ============================================================ + describe('formatSuggestion', () => { + test('formata sugestão básica em markdown', () => { + const md = engine.formatSuggestion({ + title: 'Fix sync ops', + impact: 0.7, + effort: 'low', + area: 'performance', + description: 'Convert to async.', + }); + expect(md).toContain('### Fix sync ops'); + expect(md).toContain('**Impact:** 70%'); + expect(md).toContain('**Effort:** low'); + expect(md).toContain('**Area:** performance'); + expect(md).toContain('Convert to async.'); + }); + + test('inclui location quando presente', () => { + const md = engine.formatSuggestion({ + title: 'T', impact: 0.5, effort: 'low', area: 'security', + description: 'd', + location: { file: 'src/app.js', lines: '10-20' }, + }); + expect(md).toContain('`src/app.js`'); + expect(md).toContain('(lines 10-20)'); + }); + + test('inclui suggestedFix quando presente', () => { + const md = engine.formatSuggestion({ + title: 'T', impact: 0.5, effort: 'low', area: 'x', + description: 'd', + suggestedFix: 'Use async/await', + }); + expect(md).toContain('**Suggested Fix:** Use async/await'); + }); + + test('omite location e suggestedFix quando ausentes', () => { + const md = engine.formatSuggestion({ + title: 'T', impact: 0.5, effort: 'low', area: 'x', + description: 'd', + }); + expect(md).not.toContain('Location'); + expect(md).not.toContain('Suggested Fix'); + }); + }); + + // ============================================================ + // formatMarkdown + // ============================================================ + describe('formatMarkdown', () => { + const baseResult = { + generatedAt: '2026-02-25T12:00:00Z', + projectId: 'test-project', + duration: 150, + summary: { totalSuggestions: 2, quickWins: 1, highImpact: 1 }, + quickWins: [{ + title: 'QW1', impact: 0.8, effort: 'low', area: 'performance', + description: 'Quick win desc', + }], + highImpact: [{ + title: 'HI1', impact: 0.9, effort: 'high', area: 'security', + description: 'High impact desc', + }], + allSuggestions: [ + { title: 'QW1', impact: 0.8, effort: 'low', area: 'performance', description: 'qw' }, + { title: 'HI1', impact: 0.9, effort: 'high', area: 'security', description: 'hi' }, + ], + }; + + test('inclui cabeçalho com metadata', () => { + const md = engine.formatMarkdown(baseResult); + expect(md).toContain('# Ideation Report'); + expect(md).toContain('2026-02-25T12:00:00Z'); + expect(md).toContain('test-project'); + expect(md).toContain('150ms'); + }); + + test('inclui tabela de sumário', () => { + const md = engine.formatMarkdown(baseResult); + expect(md).toContain('Total Suggestions | 2'); + expect(md).toContain('Quick Wins | 1'); + expect(md).toContain('High Impact | 1'); + }); + + test('inclui seção Quick Wins', () => { + const md = engine.formatMarkdown(baseResult); + expect(md).toContain('Quick Wins'); + expect(md).toContain('QW1'); + }); + + test('inclui seção High Impact', () => { + const md = engine.formatMarkdown(baseResult); + expect(md).toContain('High Impact'); + expect(md).toContain('HI1'); + }); + + test('omite seção Quick Wins quando vazio', () => { + const result = { ...baseResult, quickWins: [] }; + const md = engine.formatMarkdown(result); + // A seção Quick Wins usa emoji 🎯 + expect(md).not.toContain('🎯'); + }); + + test('omite High Impact quando vazio', () => { + const result = { ...baseResult, highImpact: [] }; + const md = engine.formatMarkdown(result); + expect(md).not.toContain('🚀 High Impact'); + }); + + test('limita Quick Wins a 5 itens', () => { + const qws = Array.from({ length: 8 }, (_, i) => ({ + title: `QW${i}`, impact: 0.8, effort: 'low', area: 'perf', description: 'd', + })); + const md = engine.formatMarkdown({ ...baseResult, quickWins: qws }); + expect(md).toContain('QW4'); + expect(md).not.toContain('QW5'); + }); + + test('inclui rodapé', () => { + const md = engine.formatMarkdown(baseResult); + expect(md).toContain('Generated by AIOS Ideation Engine'); + }); + }); + + // ============================================================ + // ideate (orquestração) + // ============================================================ + describe('ideate', () => { + test('retorna resultado com structure correta', async () => { + const result = await engine.ideate({ save: false }); + expect(result).toHaveProperty('generatedAt'); + expect(result).toHaveProperty('projectId', 'project'); + expect(result).toHaveProperty('duration'); + expect(result).toHaveProperty('summary'); + expect(result).toHaveProperty('quickWins'); + expect(result).toHaveProperty('highImpact'); + expect(result).toHaveProperty('allSuggestions'); + }); + + test('filtra por área de foco', async () => { + // Mock security analyzer to return findings + engine.analyzers.security.analyze = jest.fn().mockResolvedValue([{ + title: 'Hardcoded secret', description: 'Found API key', + impact: 0.95, effort: 'low', + }]); + + const result = await engine.ideate({ focus: 'security', save: false }); + expect(result.allSuggestions.length).toBe(1); + expect(result.allSuggestions[0].area).toBe('security'); + }); + + test('aceita focus como array', async () => { + engine.analyzers.security.analyze = jest.fn().mockResolvedValue([{ + title: 'S1', description: 'd', impact: 0.9, effort: 'low', + }]); + engine.analyzers.ux.analyze = jest.fn().mockResolvedValue([{ + title: 'U1', description: 'd', impact: 0.5, effort: 'low', + }]); + + const result = await engine.ideate({ + focus: ['security', 'ux'], save: false, + }); + expect(result.allSuggestions.length).toBe(2); + }); + + test('ignora analyzers inválidos sem erro', async () => { + const result = await engine.ideate({ focus: 'invalid_area', save: false }); + expect(result.allSuggestions.length).toBe(0); + }); + + test('captura erros de analyzers sem propagar', async () => { + engine.analyzers.security.analyze = jest.fn().mockRejectedValue( + new Error('analysis failed'), + ); + + const result = await engine.ideate({ focus: 'security', save: false }); + expect(result.allSuggestions.length).toBe(0); + }); + + test('ordena por prioridade decrescente', async () => { + engine.analyzers.security.analyze = jest.fn().mockResolvedValue([ + { title: 'Low', description: 'd', impact: 0.3, effort: 'high' }, + { title: 'High', description: 'd', impact: 0.95, effort: 'low' }, + { title: 'Med', description: 'd', impact: 0.6, effort: 'medium' }, + ]); + + const result = await engine.ideate({ focus: 'security', save: false }); + const priorities = result.allSuggestions.map(s => s.priority); + expect(priorities[0]).toBeGreaterThan(priorities[1]); + expect(priorities[1]).toBeGreaterThan(priorities[2]); + }); + + test('filtra gotchas conhecidas quando gotchasMemory disponível', async () => { + const mockMemory = { + getAll: jest.fn().mockResolvedValue([{ + pattern: 'synchronous operations blocking event', + description: 'known performance issue with readFileSync', + }]), + }; + engine.gotchasMemory = mockMemory; + + engine.analyzers.performance.analyze = jest.fn().mockResolvedValue([{ + title: 'Synchronous operations blocking event loop', + description: 'Found readFileSync calls in production code', + impact: 0.7, effort: 'medium', + }]); + + const result = await engine.ideate({ focus: 'performance', save: false }); + // Deve filtrar a sugestão que é um gotcha conhecido + expect(result.allSuggestions.length).toBe(0); + }); + + test('salva resultado quando save !== false', async () => { + await engine.ideate(); + expect(fs.writeFileSync).toHaveBeenCalledTimes(2); // JSON + MD + }); + + test('não salva quando save é false', async () => { + await engine.ideate({ save: false }); + expect(fs.writeFileSync).not.toHaveBeenCalled(); + }); + + test('summary.byArea conta corretamente', async () => { + engine.analyzers.security.analyze = jest.fn().mockResolvedValue([ + { title: 'S1', description: 'd', impact: 0.9, effort: 'low' }, + { title: 'S2', description: 'd', impact: 0.8, effort: 'medium' }, + ]); + + const result = await engine.ideate({ focus: 'security', save: false }); + expect(result.summary.byArea).toEqual({ security: 2 }); + }); + }); + + // ============================================================ + // save + // ============================================================ + describe('save', () => { + test('cria diretório de output se não existe', async () => { + fs.existsSync.mockReturnValue(false); + await engine.save({ + generatedAt: 'now', projectId: 'p', duration: 0, + summary: { totalSuggestions: 0, quickWins: 0, highImpact: 0 }, + quickWins: [], highImpact: [], allSuggestions: [], + }); + expect(fs.mkdirSync).toHaveBeenCalledWith( + expect.stringContaining('ideation'), + { recursive: true }, + ); + }); + + test('salva JSON e markdown', async () => { + const result = { + generatedAt: 'now', projectId: 'p', duration: 100, + summary: { totalSuggestions: 0, quickWins: 0, highImpact: 0 }, + quickWins: [], highImpact: [], allSuggestions: [], + }; + await engine.save(result); + + expect(fs.writeFileSync).toHaveBeenCalledTimes(2); + const jsonCall = fs.writeFileSync.mock.calls.find(c => c[0].endsWith('.json')); + const mdCall = fs.writeFileSync.mock.calls.find(c => c[0].endsWith('.md')); + expect(jsonCall).toBeDefined(); + expect(mdCall).toBeDefined(); + }); + }); +}); + +// ============================================================ +// Analyzers individuais (com execSync mockado) +// ============================================================ +describe('PerformanceAnalyzer', () => { + let analyzer; + + beforeEach(() => { + jest.clearAllMocks(); + analyzer = new PerformanceAnalyzer('/fake/project'); + }); + + test('detecta operações síncronas excessivas (>10)', async () => { + execSyncSpy.mockReturnValue( + Array(15).fill('src/app.js:10: readFileSync(file)').join('\n'), + ); + const findings = await analyzer.analyze(); + const syncFinding = findings.find(f => f.title.includes('synchronous')); + expect(syncFinding).toBeDefined(); + expect(syncFinding.impact).toBe(0.7); + }); + + test('não reporta quando <10 operações síncronas', async () => { + execSyncSpy.mockReturnValue('src/app.js:1: readFileSync\nsrc/app.js:2: writeFileSync'); + const findings = await analyzer.analyze(); + expect(findings.find(f => f.title.includes('synchronous'))).toBeUndefined(); + }); + + test('ignora erros de execSync', async () => { + execSyncSpy.mockImplementation(() => { throw new Error('command failed'); }); + const findings = await analyzer.analyze(); + expect(findings).toEqual([]); + }); +}); + +describe('SecurityAnalyzer', () => { + let analyzer; + + beforeEach(() => { + jest.clearAllMocks(); + analyzer = new SecurityAnalyzer('/fake/project'); + }); + + test('detecta secrets hardcoded', async () => { + execSyncSpy.mockImplementation((cmd) => { + if (cmd.includes('password')) { + return 'src/config.js:5: password = "secret123"'; + } + return ''; + }); + const findings = await analyzer.analyze(); + const secretFinding = findings.find(f => f.title.includes('secrets')); + expect(secretFinding).toBeDefined(); + expect(secretFinding.impact).toBe(0.95); + }); + + test('detecta uso de eval()', async () => { + execSyncSpy.mockImplementation((cmd) => { + if (cmd.includes('eval')) { + return 'src/utils.js:42: eval(userInput)'; + } + return ''; + }); + const findings = await analyzer.analyze(); + const evalFinding = findings.find(f => f.title.includes('eval')); + expect(evalFinding).toBeDefined(); + expect(evalFinding.impact).toBe(0.9); + }); + + test('detecta vulnerabilidades de dependências', async () => { + execSyncSpy.mockImplementation((cmd) => { + if (cmd.includes('npm audit')) { + return JSON.stringify({ + metadata: { vulnerabilities: { critical: 2, high: 3 } }, + }); + } + return ''; + }); + const findings = await analyzer.analyze(); + const depsFinding = findings.find(f => f.title.includes('Vulnerable')); + expect(depsFinding).toBeDefined(); + expect(depsFinding.impact).toBe(0.95); // critical > 0 + }); +}); + +describe('CodeQualityAnalyzer', () => { + let analyzer; + + beforeEach(() => { + jest.clearAllMocks(); + analyzer = new CodeQualityAnalyzer('/fake/project'); + }); + + test('detecta arquivos grandes (>500 linhas)', async () => { + execSyncSpy.mockImplementation((cmd) => { + if (cmd.includes('wc -l')) { + return ' 750 src/huge.js\n 600 src/big.js\n'; + } + return ''; + }); + const findings = await analyzer.analyze(); + const longFinding = findings.find(f => f.title.includes('Large')); + expect(longFinding).toBeDefined(); + }); + + test('detecta console.log excessivo (>20)', async () => { + execSyncSpy.mockImplementation((cmd) => { + if (cmd.includes('console')) { + return Array(25).fill('src/app.js:1: console.log("debug")').join('\n'); + } + return ''; + }); + const findings = await analyzer.analyze(); + const consoleFinding = findings.find(f => f.title.includes('console')); + expect(consoleFinding).toBeDefined(); + }); +}); + +describe('ArchitectureAnalyzer', () => { + let analyzer; + + beforeEach(() => { + jest.clearAllMocks(); + analyzer = new ArchitectureAnalyzer('/fake/project'); + }); + + test('detecta dependências circulares', async () => { + execSyncSpy.mockImplementation((cmd) => { + if (cmd.includes('madge')) { + return 'Circular dependency found:\n a.js → b.js → c.js → a.js'; + } + return ''; + }); + const findings = await analyzer.analyze(); + const circularFinding = findings.find(f => f.title.includes('Circular')); + expect(circularFinding).toBeDefined(); + expect(circularFinding.impact).toBe(0.7); + }); + + test('detecta violações de camada', async () => { + execSyncSpy.mockImplementation((cmd) => { + if (cmd.includes('from.*infrastructure')) { + return 'src/components/App.tsx:5: import { db } from "infrastructure/db"'; + } + return ''; + }); + const findings = await analyzer.analyze(); + const layerFinding = findings.find(f => f.title.includes('layer')); + expect(layerFinding).toBeDefined(); + }); +}); From c496dd66335eb6b421a44c125c06de4f5ca0c59e Mon Sep 17 00:00:00 2001 From: Nikolas de Hor Date: Wed, 25 Feb 2026 20:04:41 -0300 Subject: [PATCH 2/2] fix: resolver feedback do CodeRabbit no test suite do ideation-engine MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Mock gotchas-memory com shape de constructor em vez de null — funciona com import atual (sem destructuring) e com fix #519 (com destructuring). Captura mismatch de import/export. 2. Reconfigurar spies de fs no beforeEach (existsSync, mkdirSync, writeFileSync) para evitar leak de estado entre testes após clearAllMocks limpar as implementações. 3. Suprimir console.warn no teste de analyzer rejection para evitar ruído no output do CI. --- tests/core/ideation/ideation-engine.test.js | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/tests/core/ideation/ideation-engine.test.js b/tests/core/ideation/ideation-engine.test.js index 9efdea9a8b..47d9321d54 100644 --- a/tests/core/ideation/ideation-engine.test.js +++ b/tests/core/ideation/ideation-engine.test.js @@ -10,9 +10,16 @@ const child_process = require('child_process'); const fs = require('fs'); -// Mock gotchas-memory antes de carregar ideation-engine -// (ideation-engine tenta `new GotchasMemory()` mas o export é { GotchasMemory: class }) -jest.mock('../../../.aios-core/core/memory/gotchas-memory', () => null); +// Mock gotchas-memory com shape de constructor. +// O módulo retornado funciona como constructor diretamente (import atual sem +// destructuring) E como named export { GotchasMemory } (pós-fix #519). +jest.mock('../../../.aios-core/core/memory/gotchas-memory', () => { + const MockGotchasMemory = jest.fn().mockImplementation(() => ({ + getAll: jest.fn().mockResolvedValue([]), + })); + MockGotchasMemory.GotchasMemory = MockGotchasMemory; + return MockGotchasMemory; +}); const execSyncSpy = jest.spyOn(child_process, 'execSync').mockReturnValue(''); jest.spyOn(fs, 'existsSync').mockReturnValue(true); @@ -33,7 +40,11 @@ describe('IdeationEngine', () => { beforeEach(() => { jest.clearAllMocks(); + // Reconfigurar retornos padrão para evitar leak entre testes execSyncSpy.mockReturnValue(''); + fs.existsSync.mockReturnValue(true); + fs.mkdirSync.mockImplementation(() => {}); + fs.writeFileSync.mockImplementation(() => {}); engine = new IdeationEngine({ rootPath: '/fake/project', gotchasMemory: null, @@ -373,12 +384,14 @@ describe('IdeationEngine', () => { }); test('captura erros de analyzers sem propagar', async () => { + const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}); engine.analyzers.security.analyze = jest.fn().mockRejectedValue( new Error('analysis failed'), ); const result = await engine.ideate({ focus: 'security', save: false }); expect(result.allSuggestions.length).toBe(0); + warnSpy.mockRestore(); }); test('ordena por prioridade decrescente', async () => {