docs: enterprise refactor plan with ralph specs

This commit is contained in:
debian
2026-03-04 16:17:03 -05:00
parent 4c92712d20
commit f8191133c8
204 changed files with 32722 additions and 422 deletions

View File

@@ -0,0 +1,147 @@
import { AnomalyDetector } from '../../src/core/AnomalyDetector';
import { IObservation, IAction } from '../../src/core/interfaces';
function makeObservation(overrides: Partial<IObservation> = {}): IObservation {
return {
id: 'obs-1',
actionId: 'act-1',
newStateId: 'state-2',
httpResponses: [],
consoleErrors: [],
jsExceptions: [],
timestamp: Date.now(),
...overrides,
};
}
function makeAction(id = 'a1'): IAction {
return { id, type: 'click', selector: '#btn', timestamp: 1000, seed: 42, stateId: 's1' };
}
describe('AnomalyDetector', () => {
let detector: AnomalyDetector;
const trace = [makeAction()];
beforeEach(() => {
detector = new AnomalyDetector();
});
describe('checkHttpErrors', () => {
it('returns null when no HTTP errors', () => {
const obs = makeObservation({
httpResponses: [{ url: '/api', status: 200, method: 'GET', durationMs: 10 }],
});
expect(detector.checkHttpErrors(obs, trace)).toBeNull();
});
it('detects a 4xx error as medium severity', () => {
const obs = makeObservation({
httpResponses: [{ url: '/api', status: 404, method: 'GET', durationMs: 10 }],
});
const anomaly = detector.checkHttpErrors(obs, trace);
expect(anomaly).not.toBeNull();
expect(anomaly!.type).toBe('http_error');
expect(anomaly!.severity).toBe('medium');
});
it('detects a 5xx error as high severity', () => {
const obs = makeObservation({
httpResponses: [{ url: '/api', status: 500, method: 'POST', durationMs: 100 }],
});
const anomaly = detector.checkHttpErrors(obs, trace);
expect(anomaly).not.toBeNull();
expect(anomaly!.severity).toBe('high');
});
it('includes http log in evidence', () => {
const obs = makeObservation({
httpResponses: [{ url: '/api', status: 500, method: 'POST', durationMs: 100 }],
});
const anomaly = detector.checkHttpErrors(obs, trace)!;
expect(anomaly.evidence.httpLog).toHaveLength(1);
expect(anomaly.evidence.httpLog![0].status).toBe(500);
});
it('includes the full action trace', () => {
const obs = makeObservation({
httpResponses: [{ url: '/api', status: 503, method: 'GET', durationMs: 50 }],
});
const multiTrace = [makeAction('a1'), makeAction('a2')];
const anomaly = detector.checkHttpErrors(obs, multiTrace)!;
expect(anomaly.actionTrace).toHaveLength(2);
});
});
describe('checkJsExceptions', () => {
it('returns null when no JS exceptions', () => {
expect(detector.checkJsExceptions(makeObservation(), trace)).toBeNull();
});
it('detects a JS exception as high severity', () => {
const obs = makeObservation({ jsExceptions: ['ReferenceError: foo is not defined'] });
const anomaly = detector.checkJsExceptions(obs, trace);
expect(anomaly).not.toBeNull();
expect(anomaly!.type).toBe('js_exception');
expect(anomaly!.severity).toBe('high');
});
it('includes all exceptions in raw errors', () => {
const obs = makeObservation({ jsExceptions: ['Error A', 'Error B'] });
const anomaly = detector.checkJsExceptions(obs, trace)!;
expect(anomaly.evidence.rawErrors).toEqual(['Error A', 'Error B']);
});
});
describe('checkConsoleErrors', () => {
it('returns null when no console errors', () => {
expect(detector.checkConsoleErrors(makeObservation(), trace)).toBeNull();
});
it('detects console errors as low severity', () => {
const obs = makeObservation({ consoleErrors: ['Failed to load resource'] });
const anomaly = detector.checkConsoleErrors(obs, trace);
expect(anomaly).not.toBeNull();
expect(anomaly!.type).toBe('console_error');
expect(anomaly!.severity).toBe('low');
});
});
describe('detect (combined)', () => {
it('returns empty array when no anomalies', () => {
const result = detector.detect(makeObservation(), trace);
expect(result).toHaveLength(0);
});
it('returns multiple anomalies when multiple rules trigger', () => {
const obs = makeObservation({
httpResponses: [{ url: '/api', status: 500, method: 'POST', durationMs: 10 }],
jsExceptions: ['TypeError: Cannot read property'],
consoleErrors: ['Network error'],
});
const result = detector.detect(obs, trace);
expect(result).toHaveLength(3);
const types = result.map((a) => a.type);
expect(types).toContain('http_error');
expect(types).toContain('js_exception');
expect(types).toContain('console_error');
});
it('each anomaly has a unique id', () => {
const obs = makeObservation({
httpResponses: [{ url: '/api', status: 500, method: 'POST', durationMs: 10 }],
jsExceptions: ['TypeError'],
});
const anomalies = detector.detect(obs, trace);
const ids = anomalies.map((a) => a.id);
expect(new Set(ids).size).toBe(ids.length);
});
it('anomalies are JSON-serializable', () => {
const obs = makeObservation({
httpResponses: [{ url: '/api', status: 404, method: 'GET', durationMs: 5 }],
});
const anomalies = detector.detect(obs, trace);
expect(() => JSON.stringify(anomalies)).not.toThrow();
});
});
});