197 lines
5.8 KiB
TypeScript
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');
|
|
});
|
|
});
|