import { ExplorationEngine } from '../../src/core/ExplorationEngine'; import { StateGraph } from '../../src/core/StateGraph'; import { NullLogger } from '../../src/core/Logger'; import { IState, IAction, IObservation } from '../../src/core/interfaces'; import { IInteractionAgent } from '../../src/plugins/interfaces'; function makeState(id: string, url: string): IState { return { id, url, title: `Page ${id}`, timestamp: 1000, domSnapshot: '', visitCount: 0 }; } function makeAction(id: string, stateId: string): IAction { return { id, type: 'click', selector: '#btn', timestamp: 2000, seed: 42, stateId }; } function makeObservation(id: string, actionId: string, newStateId: string): IObservation { return { id, actionId, newStateId, httpResponses: [], consoleErrors: [], jsExceptions: [], timestamp: 3000, }; } /** Creates a minimal mock IInteractionAgent */ function createMockAgent(states: IState[], actions: IAction[], observations: IObservation[]): IInteractionAgent { let callCount = 0; return { launch: jest.fn().mockResolvedValue(undefined), close: jest.fn().mockResolvedValue(undefined), captureState: jest.fn().mockImplementation(() => { const state = states[Math.min(callCount, states.length - 1)]; callCount++; return Promise.resolve(state); }), discoverActions: jest.fn().mockResolvedValue(actions), executeAction: jest.fn().mockImplementation((_action: IAction) => { return Promise.resolve(observations[0]); }), }; } describe('ExplorationEngine', () => { it('launches and closes the agent', async () => { const graph = new StateGraph(); const state = makeState('s1', 'http://localhost/'); const obs = makeObservation('o1', 'a1', 's1'); const agent = createMockAgent([state], [], [obs]); const engine = new ExplorationEngine({ graph, agent, seed: 42, url: 'http://localhost/', maxSteps: 0, logger: new NullLogger(), }); await engine.run(); expect(agent.launch).toHaveBeenCalledWith('http://localhost/'); expect(agent.close).toHaveBeenCalled(); }); it('captures initial state and adds it to the graph', async () => { const graph = new StateGraph(); const state = makeState('s1', 'http://localhost/'); const agent = createMockAgent([state], [], []); const engine = new ExplorationEngine({ graph, agent, seed: 42, url: 'http://localhost/', maxSteps: 0, logger: new NullLogger(), }); await engine.run(); expect(graph.hasState('s1')).toBe(true); }); it('executes actions and records transitions', async () => { const graph = new StateGraph(); const s1 = makeState('s1', '/'); const s2 = makeState('s2', '/about'); const action = makeAction('a1', 's1'); const obs = makeObservation('o1', 'a1', 's2'); const agent = createMockAgent([s1, s2], [action], [obs]); const engine = new ExplorationEngine({ graph, agent, seed: 42, url: 'http://localhost/', maxSteps: 1, logger: new NullLogger(), }); const result = await engine.run(); expect(result.statesVisited).toBeGreaterThanOrEqual(1); expect(graph.getTransitions()).toHaveLength(1); }); it('terminates when maxSteps is reached', async () => { const graph = new StateGraph(); const state = makeState('s1', '/'); const action = makeAction('a1', 's1'); const obs = makeObservation('o1', 'a1', 's1'); const agent = createMockAgent([state], [action], [obs]); const engine = new ExplorationEngine({ graph, agent, seed: 42, url: 'http://localhost/', maxSteps: 3, logger: new NullLogger(), }); const result = await engine.run(); expect((agent.executeAction as jest.Mock).mock.calls.length).toBeLessThanOrEqual(3); expect(result).toBeDefined(); }); it('terminates when no more states to explore', async () => { const graph = new StateGraph(); const state = makeState('s1', '/'); // No actions to discover → loop exits immediately const agent = createMockAgent([state], [], []); const engine = new ExplorationEngine({ graph, agent, seed: 42, url: 'http://localhost/', maxSteps: 100, logger: new NullLogger(), }); const result = await engine.run(); expect(result.anomaliesFound).toBe(0); }); it('detects anomalies from HTTP errors', async () => { const graph = new StateGraph(); const s1 = makeState('s1', '/'); const action = makeAction('a1', 's1'); const obs: IObservation = { id: 'o1', actionId: 'a1', newStateId: 's1', httpResponses: [{ url: '/api', status: 500, method: 'POST', durationMs: 100 }], consoleErrors: [], jsExceptions: [], timestamp: 3000, }; const agent = createMockAgent([s1], [action], [obs]); const engine = new ExplorationEngine({ graph, agent, seed: 42, url: 'http://localhost/', maxSteps: 1, logger: new NullLogger(), }); const result = await engine.run(); expect(result.anomaliesFound).toBe(1); expect(result.anomalies[0].type).toBe('http_error'); }); it('logs all key events via the logger', async () => { const graph = new StateGraph(); const state = makeState('s1', '/'); const logger = new NullLogger(); const agent = createMockAgent([state], [], []); const engine = new ExplorationEngine({ graph, agent, seed: 42, url: 'http://localhost/', maxSteps: 0, logger, }); await engine.run(); const eventTypes = logger.events.map((e) => e.event); expect(eventTypes).toContain('session_start'); expect(eventTypes).toContain('state_discovered'); expect(eventTypes).toContain('session_end'); }); });