Files
Autonomous-Bug-Explorer/tests/core/ExplorationEngine.test.ts

197 lines
5.8 KiB
TypeScript

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: '<body></body>', 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');
});
});