Enhance architecture documentation and update project dependencies. Introduce new features for browser recording, page callables, and transform capabilities. Improve build scripts and permissions for better functionality.

This commit is contained in:
go0p
2026-07-23 15:16:50 +08:00
parent 726a97849b
commit c7891a6a9d
125 changed files with 25018 additions and 1675 deletions
@@ -0,0 +1,176 @@
import { describe, expect, it } from 'vitest';
import type { BrowserRecordingEvent } from '@/types/models';
import {
buildRecordingLinks,
buildRecordingTraces,
mergeRecordingEvents,
nextRecordingSequence,
} from './timeline';
function event(
id: string,
sequence: number,
traceId: string,
overrides: Partial<BrowserRecordingEvent> = {},
): BrowserRecordingEvent {
return {
id,
sequence,
timestamp: 1_000 + sequence * 10,
recordingId: 'session-1',
traceId,
kind: 'interaction',
operation: 'click',
inputs: [],
outputs: [],
sensitiveCaptured: false,
...overrides,
};
}
describe('browser recording timeline', () => {
it('keeps traces and their steps in top-to-bottom chronological order', () => {
const events = [
event('request', 3, 'trace-login', { kind: 'fetch', operation: 'request', method: 'POST', url: 'https://example.test/login' }),
event('second-click', 5, 'trace-search', { label: '查询' }),
event('first-click', 1, 'trace-login', { label: '登录' }),
event('crypto', 2, 'trace-login', { kind: 'crypto', operation: 'AES.encrypt', crypto: { adapterId: 'cryptojs', providerKind: 'library', family: 'symmetric', operation: 'AES.encrypt' } }),
event('navigation', 4, 'trace-login', {
kind: 'navigation',
operation: 'navigation.document',
navigation: { phase: 'completed', kind: 'document', toUrl: 'https://example.test/success', sameDocument: false },
}),
];
const traces = buildRecordingTraces(events, []);
expect(traces.map((trace) => trace.label)).toEqual(['登录', '查询']);
expect(traces[0].eventIds).toEqual(['first-click', 'crypto', 'request', 'navigation']);
expect(traces[0]).toMatchObject({ requestCount: 1, cryptoCount: 1, navigationCount: 1 });
});
it('updates one navigation boundary instead of duplicating its lifecycle phases', () => {
const started = event('navigation', 2, 'trace-login', {
kind: 'navigation',
operation: 'navigation.document',
navigation: { phase: 'started', kind: 'document', fromUrl: 'https://example.test/', toUrl: 'https://example.test/success', sameDocument: false },
});
const completed = {
...started,
durationMs: 84,
navigation: { ...started.navigation!, phase: 'completed' as const, documentId: 'document-2' },
};
const merged = mergeRecordingEvents([[started], [completed]]);
expect(merged).toHaveLength(1);
expect(merged[0].navigation?.phase).toBe('completed');
expect(merged[0].durationMs).toBe(84);
expect(nextRecordingSequence(merged)).toBe(3);
expect(buildRecordingTraces(merged, [])[0]).toMatchObject({ startedAt: 1_020, endedAt: 1_104 });
});
it('preserves sensitive previews and the most advanced navigation phase across status snapshots', () => {
const sensitive = event('crypto', 1, 'trace-login', {
kind: 'crypto',
operation: 'AES.encrypt',
crypto: { adapterId: 'cryptojs', providerKind: 'library', family: 'symmetric', operation: 'AES.encrypt' },
sensitiveCaptured: true,
inputPreview: 'plain-value',
inputs: [{ path: '$input', fingerprint: 'plain', encoding: 'text', byteLength: 11, preview: 'plain-value' }],
});
const metadataOnly = {
...sensitive,
sensitiveCaptured: false,
inputPreview: undefined,
inputs: sensitive.inputs.map(({ preview: _preview, ...item }) => item),
};
const completedNavigation = event('navigation', 2, 'trace-login', {
kind: 'navigation',
operation: 'navigation.document',
durationMs: 48,
navigation: { phase: 'completed', kind: 'document', toUrl: 'https://example.test/success', sameDocument: false },
});
const pageNavigation = {
...completedNavigation,
durationMs: undefined,
navigation: { ...completedNavigation.navigation!, phase: 'started' as const },
};
const merged = mergeRecordingEvents([[sensitive, completedNavigation], [metadataOnly, pageNavigation]]);
expect(merged[0]).toMatchObject({ sensitiveCaptured: true, inputPreview: 'plain-value' });
expect(merged[0].inputs[0].preview).toBe('plain-value');
expect(merged[1].navigation?.phase).toBe('completed');
expect(merged[1].durationMs).toBe(48);
});
it('keeps exact value links separate from chronological navigation edges', () => {
const crypto = event('crypto', 1, 'trace-login', {
kind: 'crypto',
operation: 'AES.encrypt',
crypto: { adapterId: 'cryptojs', providerKind: 'library', family: 'symmetric', operation: 'AES.encrypt' },
outputs: [{ path: '$output', fingerprint: 'cipher', encoding: 'text', byteLength: 32 }],
});
const request = event('request', 2, 'trace-login', {
kind: 'fetch',
operation: 'request',
inputs: [{ path: '$body.encryptedData', fingerprint: 'cipher', encoding: 'text', byteLength: 32 }],
});
const navigation = event('navigation', 3, 'trace-login', {
kind: 'navigation',
operation: 'navigation.document',
navigation: { phase: 'completed', kind: 'document', toUrl: 'https://example.test/success', sameDocument: false },
});
const links = buildRecordingLinks([crypto, request, navigation]);
expect(links).toHaveLength(1);
expect(links[0]).toMatchObject({ fromEventId: 'crypto', toEventId: 'request' });
});
it('correlates delayed Worker replies with the originating channel without claiming value equality', () => {
const send = event('send', 1, 'trace-worker', {
kind: 'worker', operation: 'worker.postMessage', direction: 'send', channelId: 'worker-1',
});
const receive = event('receive', 2, 'trace-worker', {
kind: 'worker', operation: 'worker.message', direction: 'receive', channelId: 'worker-1',
});
const links = buildRecordingLinks([send, receive]);
expect(links).toEqual([expect.objectContaining({
kind: 'channel', confidence: 'correlated', fromEventId: 'send', toEventId: 'receive',
})]);
expect(buildRecordingTraces([send, receive], links)[0]).toMatchObject({ messageCount: 2, linkedValueCount: 0 });
});
it('links ordered constructor and session stages without claiming value equality', () => {
const state = (phase: 'create' | 'update' | 'final') => ({
adapterId: 'jsrsasign',
providerKind: 'library' as const,
family: 'signature' as const,
operation: phase === 'final' ? 'sign' : phase,
state: { model: 'session' as const, correlationId: 'signature-session-1', phase },
});
const create = event('create', 1, 'trace-sign', {
kind: 'crypto', operation: 'Signature.create', crypto: state('create'),
});
const update = event('update', 2, 'trace-sign', {
kind: 'crypto', operation: 'Signature.updateString', crypto: state('update'),
});
const final = event('final', 3, 'trace-sign', {
kind: 'crypto', operation: 'Signature.sign', crypto: state('final'),
outputs: [{ path: '$output', fingerprint: 'signature', encoding: 'hex', byteLength: 64 }],
});
const request = event('request', 4, 'trace-sign', {
kind: 'fetch', operation: 'request',
inputs: [{ path: '$headers.x-signature', fingerprint: 'signature', encoding: 'hex', byteLength: 64 }],
});
const links = buildRecordingLinks([create, update, final, request]);
expect(links.filter((link) => link.kind === 'state')).toEqual([
expect.objectContaining({ fromEventId: 'create', toEventId: 'update', confidence: 'correlated' }),
expect.objectContaining({ fromEventId: 'update', toEventId: 'final', confidence: 'correlated' }),
]);
expect(links.filter((link) => link.kind === 'value')).toEqual([
expect.objectContaining({ fromEventId: 'final', toEventId: 'request', confidence: 'exact' }),
]);
expect(buildRecordingTraces([create, update, final, request], links)[0].linkedValueCount).toBe(1);
});
});