docs: enterprise refactor plan with ralph specs
This commit is contained in:
398
dist/server/SessionStore.js
vendored
Normal file
398
dist/server/SessionStore.js
vendored
Normal file
@@ -0,0 +1,398 @@
|
||||
"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;
|
||||
Reference in New Issue
Block a user