Files
Autonomous-Bug-Explorer/dist/server/SessionStore.js

399 lines
18 KiB
JavaScript

"use strict";
/**
* SessionStore — manages active sessions and persists to SQLite.
* In-memory map for running engines; DB for durable storage.
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.SessionStore = void 0;
const path_1 = __importDefault(require("path"));
const ExplorationEngine_1 = require("../core/ExplorationEngine");
const StateGraph_1 = require("../core/StateGraph");
const PlaywrightAgent_1 = require("../plugins/agents/PlaywrightAgent");
const ScreenshotCollector_1 = require("../plugins/collectors/ScreenshotCollector");
const NetworkCollector_1 = require("../plugins/collectors/NetworkCollector");
const DOMSnapshotCollector_1 = require("../plugins/collectors/DOMSnapshotCollector");
const MarkdownExporter_1 = require("../plugins/exporters/MarkdownExporter");
const JSONExporter_1 = require("../plugins/exporters/JSONExporter");
const PlaywrightReproducer_1 = require("../plugins/reproducers/PlaywrightReproducer");
const FuzzingEngine_1 = require("../plugins/fuzzers/FuzzingEngine");
const VisualRegressionCollector_1 = require("../plugins/collectors/VisualRegressionCollector");
const AccessibilityCollector_1 = require("../plugins/collectors/AccessibilityCollector");
const PerformanceCollector_1 = require("../plugins/collectors/PerformanceCollector");
class SessionStore {
constructor(outputDir = './reports', sessionRepo, anomalyRepo, maxConcurrentSessions = 3, notificationService, visualRepo) {
this.sessions = new Map();
this.emitter = () => undefined;
/** In-memory performance metrics keyed by sessionId */
this.performanceMetrics = new Map();
this.outputDir = outputDir;
this.sessionRepo = sessionRepo ?? null;
this.anomalyRepo = anomalyRepo ?? null;
this.maxConcurrentSessions = maxConcurrentSessions;
this.notificationService = notificationService ?? null;
this.visualRepo = visualRepo ?? null;
}
getPerformanceMetrics(sessionId) {
return this.performanceMetrics.get(sessionId) ?? [];
}
getMaxConcurrent() {
return this.maxConcurrentSessions;
}
setEmitter(emitter) {
this.emitter = emitter;
}
getAllSessions() {
if (this.sessionRepo) {
const rows = this.sessionRepo.findAll();
return rows.map((r) => {
const live = this.sessions.get(r.id);
return {
sessionId: r.id,
url: r.url,
seed: r.seed,
maxStates: r.max_states,
status: r.status,
startedAt: new Date(r.started_at).toISOString(),
finishedAt: r.finished_at ? new Date(r.finished_at).toISOString() : undefined,
statesVisited: r.states_visited,
anomaliesFound: r.anomalies_found,
anomalies: live?.anomalies ?? [],
engine: live?.engine,
};
});
}
return Array.from(this.sessions.values());
}
getSession(sessionId) {
if (this.sessionRepo) {
const r = this.sessionRepo.findById(sessionId);
if (!r)
return undefined;
const live = this.sessions.get(sessionId);
return {
sessionId: r.id,
url: r.url,
seed: r.seed,
maxStates: r.max_states,
status: r.status,
startedAt: new Date(r.started_at).toISOString(),
finishedAt: r.finished_at ? new Date(r.finished_at).toISOString() : undefined,
statesVisited: r.states_visited,
anomaliesFound: r.anomalies_found,
anomalies: live?.anomalies ?? [],
engine: live?.engine,
};
}
return this.sessions.get(sessionId);
}
getAllAnomalies(sessionId, severity) {
if (this.anomalyRepo) {
return this.anomalyRepo.findAll({ sessionId, severity });
}
const all = Array.from(this.sessions.values()).flatMap((s) => s.anomalies);
return all
.filter((a) => !sessionId || this.findSessionForAnomaly(a.id) === sessionId)
.filter((a) => !severity || a.severity === severity);
}
getAnomaly(anomalyId) {
if (this.anomalyRepo) {
return this.anomalyRepo.findById(anomalyId) ?? undefined;
}
for (const session of this.sessions.values()) {
const found = session.anomalies.find((a) => a.id === anomalyId);
if (found)
return found;
}
return undefined;
}
findSessionForAnomaly(anomalyId) {
if (this.anomalyRepo) {
const a = this.anomalyRepo.findById(anomalyId);
return a?.sessionId;
}
for (const session of this.sessions.values()) {
if (session.anomalies.some((a) => a.id === anomalyId))
return session.sessionId;
}
return undefined;
}
screenshotPath(anomalyId) {
const anomaly = this.getAnomaly(anomalyId);
if (!anomaly?.evidence.screenshotPath)
return undefined;
const sessionId = this.findSessionForAnomaly(anomalyId);
if (!sessionId)
return undefined;
return path_1.default.resolve(this.outputDir, anomalyId, anomaly.evidence.screenshotPath);
}
stopSession(sessionId) {
const record = this.sessions.get(sessionId);
if (!record || record.status !== 'running')
return false;
record.engine?.stop();
record.status = 'stopped';
record.finishedAt = new Date().toISOString();
this.sessionRepo?.update(sessionId, { status: 'stopped', finishedAt: Date.now() });
return true;
}
getStats() {
if (this.sessionRepo && this.anomalyRepo) {
const rows = this.sessionRepo.findAll();
return {
totalSessions: rows.length,
totalAnomalies: this.anomalyRepo.count(),
criticalHighCount: this.anomalyRepo.countBySeverity(['high', 'critical']),
runningSessions: rows.filter((r) => r.status === 'running').length,
};
}
const sessions = Array.from(this.sessions.values());
const anomalies = sessions.flatMap((s) => s.anomalies);
return {
totalSessions: sessions.length,
totalAnomalies: anomalies.length,
criticalHighCount: anomalies.filter((a) => a.severity === 'high' || a.severity === 'critical').length,
runningSessions: sessions.filter((s) => s.status === 'running').length,
};
}
async startSession(params) {
const sessionId = `sess_${Date.now()}_${params.seed}`;
const startedAt = new Date().toISOString();
const startedAtMs = Date.now();
const record = {
sessionId,
url: params.url,
seed: params.seed,
maxStates: params.maxStates,
status: 'running',
startedAt,
statesVisited: 0,
anomaliesFound: 0,
anomalies: [],
};
this.sessions.set(sessionId, record);
this.sessionRepo?.create({
id: sessionId,
url: params.url,
seed: params.seed,
maxStates: params.maxStates,
startedAt: startedAtMs,
configJson: params.explorationConfig ? JSON.stringify(params.explorationConfig) : '{}',
});
this.emitter('session:started', { sessionId, url: params.url });
const graph = new StateGraph_1.StateGraph();
const agent = new PlaywrightAgent_1.PlaywrightAgent({
seed: params.seed,
explorationConfig: params.explorationConfig,
});
const fuzzingEnabled = params.explorationConfig?.fuzzingEnabled !== false;
const fuzzingIntensity = params.explorationConfig?.fuzzingIntensity ?? 'medium';
const fuzzingPlugin = fuzzingEnabled
? new FuzzingEngine_1.FuzzingEngine({ intensity: fuzzingIntensity, seed: params.seed })
: undefined;
// Build state hooks for visual regression, accessibility, and performance
const stateHooks = [];
// Visual regression hook
if (params.explorationConfig?.visualRegression?.enabled && this.visualRepo) {
const visualCollector = new VisualRegressionCollector_1.VisualRegressionCollector(this.outputDir, this.visualRepo, params.explorationConfig.visualRegression);
stateHooks.push(async (state, agentInstance, sid, actionTrace) => {
const pw = agentInstance;
if (!pw.getPage)
return [];
// Take screenshot for visual comparison
const screenshotPath = path_1.default.join(this.outputDir, sid, `visual_${state.id}.png`);
try {
const fs_mod = await Promise.resolve().then(() => __importStar(require('fs/promises')));
await fs_mod.mkdir(path_1.default.dirname(screenshotPath), { recursive: true });
await pw.getPage().screenshot({ path: screenshotPath });
const anomaly = await visualCollector.processScreenshot(screenshotPath, state, sid, actionTrace);
return anomaly ? [anomaly] : [];
}
catch {
return [];
}
});
}
// Accessibility hook
if (params.explorationConfig?.accessibility?.enabled !== false) {
const a11yCollector = new AccessibilityCollector_1.AccessibilityCollector(params.explorationConfig?.accessibility);
stateHooks.push(async (state, agentInstance, sid, actionTrace) => {
const pw = agentInstance;
if (!pw.getPage)
return [];
return a11yCollector.collect(pw.getPage(), state.id, sid, actionTrace);
});
}
// Performance hook
if (params.explorationConfig?.performance?.enabled !== false) {
const perfCollector = new PerformanceCollector_1.PerformanceCollector(params.explorationConfig?.performance);
this.performanceMetrics.set(sessionId, []);
stateHooks.push(async (state, agentInstance, sid, actionTrace) => {
const pw = agentInstance;
if (!pw.getPage)
return [];
const { metrics, anomalies } = await perfCollector.collect(pw.getPage(), state.id, sid, actionTrace);
const existing = this.performanceMetrics.get(sid) ?? [];
existing.push(metrics);
this.performanceMetrics.set(sid, existing);
return anomalies;
});
}
// Mobile layout hook (only when a mobile device is emulated)
if (params.explorationConfig?.mobileDevice && params.explorationConfig.mobileDevice !== 'none') {
stateHooks.push(async (state, agentInstance, sid, actionTrace) => {
const pw = agentInstance;
if (!pw.detectMobileLayoutIssues)
return [];
return pw.detectMobileLayoutIssues(state.id, sid, actionTrace);
});
}
const engineConfig = {
graph,
agent,
seed: params.seed,
url: params.url,
maxSteps: params.maxStates,
explorationConfig: params.explorationConfig,
outputDir: this.outputDir,
sessionId,
fuzzingPlugin,
stateHooks,
collectors: [
new ScreenshotCollector_1.ScreenshotCollector(this.outputDir),
new NetworkCollector_1.NetworkCollector(),
new DOMSnapshotCollector_1.DOMSnapshotCollector(this.outputDir),
],
exporters: [new MarkdownExporter_1.MarkdownExporter(), new JSONExporter_1.JSONExporter()],
reproducer: new PlaywrightReproducer_1.PlaywrightReproducer(),
events: {
onSessionStarted: (sid, url) => {
this.emitter('session:started', { sessionId: sid, url });
},
onStateDiscovered: (sid, stateId, url, title) => {
record.statesVisited += 1;
this.sessionRepo?.update(sid, { statesVisited: record.statesVisited });
this.emitter('state:discovered', { sessionId: sid, stateId, url, title });
},
onActionExecuted: (sid, actionType, selector, timestamp) => {
this.emitter('action:executed', { sessionId: sid, actionType, selector, timestamp });
},
onAnomalyDetected: (sid, anomaly) => {
record.anomalies.push(anomaly);
record.anomaliesFound = record.anomalies.length;
this.sessionRepo?.update(sid, { anomaliesFound: record.anomaliesFound });
this.anomalyRepo?.create(anomaly, sid);
if (this.notificationService) {
void this.notificationService.notify(anomaly, sid, params.url);
}
this.emitter('anomaly:detected', {
sessionId: sid,
anomalyId: anomaly.id,
type: anomaly.type,
severity: anomaly.severity,
description: anomaly.description,
});
},
onSessionCompleted: (sid, statesVisited, anomaliesFound) => {
if (record.status === 'running') {
record.status = 'completed';
}
record.finishedAt = new Date().toISOString();
record.statesVisited = statesVisited;
record.anomaliesFound = anomaliesFound;
this.sessionRepo?.update(sid, {
status: record.status,
statesVisited,
anomaliesFound,
finishedAt: Date.now(),
});
this.emitter('session:completed', { sessionId: sid, statesVisited, anomaliesFound });
},
onSessionError: (sid, error) => {
record.status = 'error';
record.finishedAt = new Date().toISOString();
this.sessionRepo?.update(sid, { status: 'error', finishedAt: Date.now() });
this.emitter('session:error', { sessionId: sid, error });
},
},
};
const engine = new ExplorationEngine_1.ExplorationEngine(engineConfig);
record.engine = engine;
// Run in background — do not await
engine.run().catch((err) => {
if (record.status === 'running') {
record.status = 'error';
record.finishedAt = new Date().toISOString();
const msg = err instanceof Error ? err.message : String(err);
this.sessionRepo?.update(sessionId, { status: 'error', finishedAt: Date.now() });
this.emitter('session:error', { sessionId, error: msg });
}
});
return record;
}
async replayAnomaly(anomalyId) {
const replayId = `replay_${Date.now()}`;
const anomaly = this.getAnomaly(anomalyId);
if (!anomaly)
throw new Error(`Anomaly ${anomalyId} not found`);
const sessionId = this.findSessionForAnomaly(anomalyId);
const session = this.getSession(sessionId);
const reproducer = new PlaywrightReproducer_1.PlaywrightReproducer();
const script = reproducer.generateScript(anomaly.actionTrace);
const replayDir = path_1.default.join(this.outputDir, anomalyId, 'replays');
setImmediate(async () => {
try {
const fs_mod = await Promise.resolve().then(() => __importStar(require('fs/promises')));
await fs_mod.mkdir(replayDir, { recursive: true });
const scriptPath = path_1.default.join(replayDir, `${replayId}.ts`);
await fs_mod.writeFile(scriptPath, script, 'utf8');
const agent = new PlaywrightAgent_1.PlaywrightAgent({ seed: session.seed });
await agent.launch(session.url);
for (const action of anomaly.actionTrace) {
await agent.executeAction(action).catch(() => undefined);
}
await agent.close();
}
catch {
// replay errors are non-fatal
}
});
return replayId;
}
}
exports.SessionStore = SessionStore;