Files
yaklang-chrome-extension/src/features/browser-recording/main-world/trace.test.ts
T

159 lines
5.9 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import { createRecordingTraceRuntime } from './trace';
function environment() {
let active = true;
let recordingId: string | undefined = 'recording-1';
let captureValues = false;
let maxEntries = 3;
let parentEventId: string | undefined;
let uniqueSequence = 0;
let currentTime = 1_000;
const runtime = createRecordingTraceRuntime({
active: () => active,
recordingId: () => recordingId,
captureValues: () => captureValues,
maxEntries: () => maxEntries,
parentEventId: () => parentEventId,
unique: (prefix) => `${prefix}-${++uniqueSequence}`,
}, () => currentTime);
return {
runtime,
setActive(value: boolean) {
active = value;
},
setRecordingId(value: string | undefined) {
recordingId = value;
},
setCaptureValues(value: boolean) {
captureValues = value;
},
setMaxEntries(value: number) {
maxEntries = value;
},
setParentEventId(value: string | undefined) {
parentEventId = value;
},
advance(milliseconds: number) {
currentTime += milliseconds;
},
};
}
describe('recording trace runtime', () => {
it('inherits unique value provenance across a later interaction without guessing shared values', () => {
const { runtime, setMaxEntries } = environment();
setMaxEntries(20);
const value = { path: '$body', fingerprint: 'ciphertext', encoding: 'text' as const, byteLength: 32 };
const key = { ...value, fingerprint: 'shared-key' };
runtime.bindContext({ traceId: 'B' });
runtime.record({ kind: 'fetch', operation: 'response', outputs: [value, key] }, { traceId: 'A' });
runtime.record({ kind: 'fetch', operation: 'response', outputs: [key] }, { traceId: 'C' });
expect(runtime.record({ kind: 'crypto', operation: 'decrypt', inputs: [value, key] })?.traceId).toBe('A');
runtime.record({ kind: 'fetch', operation: 'response', outputs: [value] }, { traceId: 'C' });
expect(runtime.record({ kind: 'crypto', operation: 'decrypt', inputs: [value] })?.traceId).toBe('B');
});
it('does not create events or trace state while recording is inactive', () => {
const environmentState = environment();
environmentState.setActive(false);
expect(environmentState.runtime.record({ kind: 'interaction', operation: 'click' })).toBeUndefined();
expect(environmentState.runtime.observe(() => {
throw new Error('factory must not run');
})).toBeUndefined();
expect(environmentState.runtime.currentContext()).toBeUndefined();
expect(environmentState.runtime.snapshot(10)).toEqual({
count: 0,
droppedCount: 0,
sequence: 0,
events: [],
});
});
it('binds interaction context and preserves parent and sensitivity metadata', () => {
const environmentState = environment();
environmentState.setCaptureValues(true);
environmentState.setParentEventId('crypto-parent');
environmentState.runtime.bindContext({ traceId: 'trace-login', interactionId: 'interaction-submit' });
const event = environmentState.runtime.record({
kind: 'fetch',
operation: 'POST',
inputs: [],
outputs: [],
});
expect(event).toEqual(expect.objectContaining({
id: 'event-1',
recordingId: 'recording-1',
traceId: 'trace-login',
interactionId: 'interaction-submit',
parentEventId: 'crypto-parent',
sensitiveCaptured: true,
sequence: 1,
}));
});
it('reuses an active trace and creates a new trace after the idle window', () => {
const environmentState = environment();
const first = environmentState.runtime.record({ kind: 'transform', operation: 'first' });
environmentState.advance(4_999);
const second = environmentState.runtime.record({ kind: 'transform', operation: 'second' });
environmentState.advance(5_001);
const third = environmentState.runtime.record({ kind: 'transform', operation: 'third' });
expect(second?.traceId).toBe(first?.traceId);
expect(third?.traceId).not.toBe(first?.traceId);
});
it('keeps sequence monotonic while evicting bounded history', () => {
const environmentState = environment();
environmentState.setMaxEntries(2);
environmentState.runtime.record({ kind: 'interaction', operation: 'one' });
environmentState.runtime.record({ kind: 'interaction', operation: 'two' });
environmentState.runtime.record({ kind: 'interaction', operation: 'three' });
const snapshot = environmentState.runtime.snapshot(10);
expect(snapshot.count).toBe(2);
expect(snapshot.droppedCount).toBe(1);
expect(snapshot.sequence).toBe(3);
expect(snapshot.events.map((event) => event.operation)).toEqual(['two', 'three']);
});
it('isolates observer errors and supports reset and resumed sequence starts', () => {
const environmentState = environment();
environmentState.runtime.record({ kind: 'interaction', operation: 'before-reset' });
expect(environmentState.runtime.observe(() => {
throw new Error('broken observer');
})).toBeUndefined();
expect(environmentState.runtime.snapshot(10).droppedCount).toBe(1);
environmentState.runtime.reset(8);
environmentState.runtime.advanceSequenceStart(12);
const event = environmentState.runtime.record({ kind: 'navigation', operation: 'navigate' });
expect(event?.sequence).toBe(13);
expect(environmentState.runtime.snapshot(0)).toEqual({
count: 1,
droppedCount: 0,
sequence: 13,
events: [],
});
});
it('does not evaluate an observer factory without a recording identity', () => {
const environmentState = environment();
environmentState.setRecordingId(undefined);
let evaluated = false;
environmentState.runtime.observe(() => {
evaluated = true;
return { kind: 'interaction', operation: 'click' };
});
expect(evaluated).toBe(false);
expect(environmentState.runtime.snapshot(10).events).toEqual([]);
});
});