mirror of
https://github.com/yaklang/yaklang-chrome-extension.git
synced 2026-09-26 05:01:53 +08:00
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:
@@ -0,0 +1,372 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import type {
|
||||
BrowserRecordingCallArgument,
|
||||
BrowserRecordingEvent,
|
||||
BrowserRecordingLink,
|
||||
} from '@/types/models';
|
||||
import { inferBrowserTransformProfiles } from './inference';
|
||||
import { buildRecordingLinks } from '@/features/browser-recording/timeline';
|
||||
|
||||
function event(overrides: Partial<BrowserRecordingEvent> & Pick<BrowserRecordingEvent, 'id' | 'sequence' | 'kind' | 'operation'>): BrowserRecordingEvent {
|
||||
return {
|
||||
timestamp: 1_000 + overrides.sequence,
|
||||
recordingId: 'recording-1',
|
||||
traceId: 'trace-1',
|
||||
inputs: [],
|
||||
outputs: [],
|
||||
sensitiveCaptured: false,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function link(overrides: Pick<BrowserRecordingLink, 'id' | 'fromEventId' | 'fromPath' | 'toEventId' | 'toPath'>): BrowserRecordingLink {
|
||||
return { traceId: 'trace-1', kind: 'value', confidence: 'exact', ...overrides };
|
||||
}
|
||||
|
||||
const safeArguments: BrowserRecordingCallArgument[] = [
|
||||
{ index: 0, role: 'data', dataType: 'string', byteLength: 52, replaceable: true, retained: true },
|
||||
{ index: 1, role: 'key', dataType: 'Object', byteLength: 16, replaceable: false, retained: true },
|
||||
{
|
||||
index: 2,
|
||||
role: 'options',
|
||||
dataType: 'Object',
|
||||
replaceable: false,
|
||||
retained: true,
|
||||
summary: 'mode=CBC padding=Pkcs7 ivBytes=16',
|
||||
},
|
||||
];
|
||||
|
||||
const cryptoJsAES = {
|
||||
adapterId: 'cryptojs', providerKind: 'library', family: 'symmetric', operation: 'AES.encrypt', algorithm: 'AES.encrypt',
|
||||
} as const;
|
||||
const webCryptoAES = {
|
||||
adapterId: 'webcrypto', providerKind: 'native', family: 'symmetric', operation: 'encrypt', algorithm: 'AES-GCM',
|
||||
} as const;
|
||||
const cryptoJsHmac = {
|
||||
adapterId: 'cryptojs', providerKind: 'library', family: 'mac', operation: 'HmacSHA256', algorithm: 'HmacSHA256',
|
||||
} as const;
|
||||
|
||||
describe('browser profile inference', () => {
|
||||
it('turns a JSEncrypt RSA result mapped to a form field into a ready profile', () => {
|
||||
const rsa = event({
|
||||
id: 'rsa-1', sequence: 1, kind: 'crypto', operation: 'encrypt',
|
||||
crypto: {
|
||||
adapterId: 'jsencrypt', providerKind: 'library', family: 'asymmetric', operation: 'encrypt', algorithm: 'RSA',
|
||||
padding: 'PKCS1-v1_5', inputEncoding: 'utf8', outputEncoding: 'base64',
|
||||
key: { kind: 'public', bits: 1024, fingerprint: 'key-fingerprint' },
|
||||
},
|
||||
callHandleId: 'rsa-handle', callableCapable: true,
|
||||
arguments: [{ index: 0, role: 'data', dataType: 'string', byteLength: 44, replaceable: true, retained: true }],
|
||||
inputs: [{ path: '$input', fingerprint: 'plain-json', encoding: 'text', byteLength: 44 }],
|
||||
outputs: [{ path: '$output', fingerprint: 'rsa-cipher', encoding: 'text', byteLength: 172 }],
|
||||
});
|
||||
const request = event({
|
||||
id: 'request-rsa', sequence: 2, kind: 'fetch', operation: 'request', method: 'POST', url: 'https://example.test/encrypt/rsa.php',
|
||||
inputs: [{ path: '$body:form.data', fingerprint: 'rsa-cipher', encoding: 'text', byteLength: 172 }],
|
||||
});
|
||||
const [candidate] = inferBrowserTransformProfiles({
|
||||
target: { tabId: 7, frameId: 0 },
|
||||
events: [rsa, request],
|
||||
links: [link({ id: 'rsa-link', fromEventId: rsa.id, fromPath: '$output', toEventId: request.id, toPath: '$body:form.data' })],
|
||||
});
|
||||
|
||||
expect(candidate).toMatchObject({
|
||||
status: 'ready',
|
||||
request: {
|
||||
destination: 'body.data',
|
||||
serialization: 'form-field',
|
||||
mappings: [{ sourceEventId: 'rsa-1', destination: 'body.data', serialization: 'form-field' }],
|
||||
},
|
||||
source: {
|
||||
eventId: 'rsa-1',
|
||||
callHandleId: 'rsa-handle',
|
||||
crypto: { adapterId: 'jsencrypt', algorithm: 'RSA', padding: 'PKCS1-v1_5' },
|
||||
},
|
||||
confidence: { level: 'high', score: 100 },
|
||||
});
|
||||
expect(candidate.summary).toContain('JSEncrypt RSA');
|
||||
});
|
||||
|
||||
it('turns an exact CryptoJS-to-JSON-field link into a high-confidence capture candidate', () => {
|
||||
const crypto = event({
|
||||
id: 'crypto-1', sequence: 1, kind: 'crypto', operation: 'AES.encrypt', crypto: cryptoJsAES,
|
||||
callHandleId: 'handle-1', callableCapable: true, arguments: safeArguments,
|
||||
inputs: [{ path: '$input', fingerprint: 'plain', encoding: 'text', byteLength: 52 }],
|
||||
outputs: [{ path: '$output:string', fingerprint: 'cipher', encoding: 'text', byteLength: 88 }],
|
||||
});
|
||||
const request = event({
|
||||
id: 'request-1', sequence: 2, kind: 'fetch', operation: 'request', method: 'POST', url: 'https://example.test/api/login',
|
||||
inputs: [{ path: '$body:json.encryptedData', fingerprint: 'cipher', encoding: 'text', byteLength: 88 }],
|
||||
});
|
||||
const candidates = inferBrowserTransformProfiles({
|
||||
target: { tabId: 7, frameId: 0, documentId: 'document-1' },
|
||||
events: [crypto, request],
|
||||
links: [link({ id: 'link-1', fromEventId: crypto.id, fromPath: '$output:string', toEventId: request.id, toPath: '$body:json.encryptedData' })],
|
||||
});
|
||||
|
||||
expect(candidates).toHaveLength(1);
|
||||
expect(candidates[0]).toMatchObject({
|
||||
status: 'ready',
|
||||
request: { destination: 'body.encryptedData', serialization: 'json-field' },
|
||||
source: { eventId: 'crypto-1', callHandleId: 'handle-1', arguments: safeArguments },
|
||||
confidence: { level: 'high', score: 100 },
|
||||
});
|
||||
expect(candidates[0].evidence.some((item) => item.kind === 'exact-value' && item.strength === 'proven')).toBe(true);
|
||||
expect(candidates[0].aiContext.valuePolicy).toBe('metadata-only');
|
||||
});
|
||||
|
||||
it('follows a bounded exact-value chain through an intermediate encoder', () => {
|
||||
const crypto = event({
|
||||
id: 'crypto-1', sequence: 1, kind: 'crypto', operation: 'encrypt', crypto: webCryptoAES,
|
||||
callHandleId: 'handle-1', callableCapable: true, arguments: safeArguments,
|
||||
outputs: [{ path: '$output', fingerprint: 'raw-cipher', encoding: 'base64', byteLength: 64 }],
|
||||
});
|
||||
const encoder = event({
|
||||
id: 'encode-1', sequence: 2, kind: 'transform', operation: 'base64.encode',
|
||||
inputs: [{ path: '$input', fingerprint: 'raw-cipher', encoding: 'base64', byteLength: 64 }],
|
||||
outputs: [{ path: '$output', fingerprint: 'encoded-cipher', encoding: 'text', byteLength: 88 }],
|
||||
});
|
||||
const request = event({
|
||||
id: 'request-1', sequence: 3, kind: 'xhr', operation: 'request', method: 'POST', url: 'https://example.test/api/login',
|
||||
inputs: [{ path: '$headers.x-signature', fingerprint: 'encoded-cipher', encoding: 'text', byteLength: 88 }],
|
||||
});
|
||||
const candidates = inferBrowserTransformProfiles({
|
||||
target: { tabId: 7, frameId: 0 },
|
||||
events: [crypto, encoder, request],
|
||||
links: [
|
||||
link({ id: 'link-1', fromEventId: crypto.id, fromPath: '$output', toEventId: encoder.id, toPath: '$input' }),
|
||||
link({ id: 'link-2', fromEventId: encoder.id, fromPath: '$output', toEventId: request.id, toPath: '$headers.x-signature' }),
|
||||
],
|
||||
});
|
||||
|
||||
const cryptoCandidate = candidates.find((item) => item.source.eventId === crypto.id);
|
||||
expect(candidates).toHaveLength(1);
|
||||
expect(cryptoCandidate?.request.destination).toBe('header.x-signature');
|
||||
expect(cryptoCandidate?.evidence.filter((item) => item.kind === 'exact-value')).toHaveLength(2);
|
||||
expect(cryptoCandidate?.flow).toContain('1 个中间转换');
|
||||
});
|
||||
|
||||
it.each([
|
||||
['$body:form.encryptedData', 'body.encryptedData'],
|
||||
['$query.signature', 'query.signature'],
|
||||
])('maps generic serialized request evidence %s to %s', (toPath, destination) => {
|
||||
const crypto = event({
|
||||
id: 'crypto-1', sequence: 1, kind: 'crypto', operation: 'AES.encrypt', crypto: cryptoJsAES,
|
||||
callHandleId: 'handle-1', callableCapable: true, arguments: safeArguments,
|
||||
outputs: [{ path: '$output:string', fingerprint: 'cipher', encoding: 'text', byteLength: 88 }],
|
||||
});
|
||||
const request = event({
|
||||
id: 'request-1', sequence: 2, kind: 'fetch', operation: 'request', method: 'POST',
|
||||
url: 'https://example.test/session',
|
||||
inputs: [{ path: toPath, fingerprint: 'cipher', encoding: 'text', byteLength: 88 }],
|
||||
});
|
||||
const [candidate] = inferBrowserTransformProfiles({
|
||||
target: { tabId: 7, frameId: 0 },
|
||||
events: [crypto, request],
|
||||
links: [link({ id: 'link-1', fromEventId: crypto.id, fromPath: '$output:string', toEventId: request.id, toPath })],
|
||||
});
|
||||
|
||||
expect(candidate.request.destination).toBe(destination);
|
||||
expect(candidate.confidence.level).toBe('high');
|
||||
expect(candidate.request.serialization).toBe(toPath.startsWith('$body:form.') ? 'form-field' : 'query');
|
||||
});
|
||||
|
||||
it('keeps a same-trace temporal guess low-confidence and never includes captured values in AI context', () => {
|
||||
const crypto = event({
|
||||
id: 'crypto-1', sequence: 1, kind: 'crypto', operation: 'HmacSHA256', crypto: cryptoJsHmac,
|
||||
inputPreview: 'plain-password', outputPreview: 'secret-signature', arguments: safeArguments,
|
||||
});
|
||||
const request = event({
|
||||
id: 'request-1', sequence: 2, kind: 'fetch', operation: 'request', method: 'POST',
|
||||
url: 'https://example.test/api/login?token=secret-query-value&mode=fast#private-fragment',
|
||||
inputPreview: '{"password":"plain-password"}',
|
||||
});
|
||||
const [candidate] = inferBrowserTransformProfiles({ target: { tabId: 7, frameId: 0 }, events: [crypto, request], links: [] });
|
||||
|
||||
expect(candidate.status).toBe('capture-required');
|
||||
expect(candidate.confidence.level).toBe('low');
|
||||
expect(JSON.stringify(candidate)).not.toContain('plain-password');
|
||||
expect(JSON.stringify(candidate)).not.toContain('secret-signature');
|
||||
expect(JSON.stringify(candidate.aiContext)).not.toContain('secret-query-value');
|
||||
expect(JSON.stringify(candidate.aiContext)).not.toContain('private-fragment');
|
||||
expect(candidate.aiContext.request.url).toBe('https://example.test/api/login?mode&token');
|
||||
expect(candidate.summary).toContain('可继续捕获完整页面业务封装');
|
||||
expect(candidate.missing[0].label).not.toMatch(/更短|重新录制一次|操作太长/);
|
||||
});
|
||||
|
||||
it('continues from an unknown request boundary even when no known crypto library is visible', () => {
|
||||
const request = event({
|
||||
id: 'opaque-request', sequence: 1, kind: 'fetch', operation: 'request', method: 'POST',
|
||||
url: 'https://example.test/opaque', stack: 'at pack (https://example.test/chunk-a.js:1:42)',
|
||||
});
|
||||
const [candidate] = inferBrowserTransformProfiles({
|
||||
target: { tabId: 7, frameId: 0 }, events: [request], links: [],
|
||||
});
|
||||
|
||||
expect(candidate).toMatchObject({ status: 'capture-required', source: { operation: 'unknown-business-envelope' } });
|
||||
expect(candidate.summary).toContain('算法或库未知不影响继续捕获');
|
||||
expect(candidate.aiContext.requiredDecision).toBe('capture-business-callable');
|
||||
expect(candidate.capturePlan).toMatchObject({ matcherEventId: request.id, sourceCount: 1 });
|
||||
});
|
||||
|
||||
it('treats a Worker round trip as correlated evidence rather than an exact value proof', () => {
|
||||
const crypto = event({
|
||||
id: 'crypto', sequence: 1, kind: 'crypto', operation: 'encrypt', crypto: webCryptoAES,
|
||||
outputs: [{ path: '$output', fingerprint: 'plain-to-worker', encoding: 'base64', byteLength: 32 }],
|
||||
});
|
||||
const send = event({
|
||||
id: 'worker-send', sequence: 2, kind: 'worker', operation: 'worker.postMessage', direction: 'send', channelId: 'channel-1',
|
||||
inputs: [{ path: '$message', fingerprint: 'plain-to-worker', encoding: 'base64', byteLength: 32 }],
|
||||
});
|
||||
const receive = event({
|
||||
id: 'worker-receive', sequence: 3, kind: 'worker', operation: 'worker.message', direction: 'receive', channelId: 'channel-1',
|
||||
outputs: [{ path: '$message', fingerprint: 'worker-result', encoding: 'text', byteLength: 64 }],
|
||||
});
|
||||
const request = event({
|
||||
id: 'request', sequence: 4, kind: 'fetch', operation: 'request', method: 'POST', url: 'https://example.test/submit',
|
||||
inputs: [{ path: '$body:json.payload', fingerprint: 'worker-result', encoding: 'text', byteLength: 64 }],
|
||||
});
|
||||
const links: BrowserRecordingLink[] = [
|
||||
link({ id: 'value-in', fromEventId: crypto.id, fromPath: '$output', toEventId: send.id, toPath: '$message' }),
|
||||
{ id: 'channel', traceId: 'trace-1', kind: 'channel', confidence: 'correlated', fromEventId: send.id, fromPath: '$message', toEventId: receive.id, toPath: '$message' },
|
||||
link({ id: 'value-out', fromEventId: receive.id, fromPath: '$message', toEventId: request.id, toPath: '$body:json.payload' }),
|
||||
];
|
||||
const [candidate] = inferBrowserTransformProfiles({
|
||||
target: { tabId: 7, frameId: 0 }, events: [crypto, send, receive, request], links,
|
||||
});
|
||||
|
||||
expect(candidate.status).toBe('capture-required');
|
||||
expect(candidate.request.destination).toBe('body.payload');
|
||||
expect(candidate.evidence.some((item) => item.kind === 'message-boundary' && item.strength === 'supported')).toBe(true);
|
||||
});
|
||||
|
||||
it('groups hybrid AES and RSA outputs into one request graph and refuses unsafe independent replay', () => {
|
||||
const sources = [
|
||||
event({
|
||||
id: 'aes-data', sequence: 1, kind: 'crypto', operation: 'AES.encrypt', crypto: cryptoJsAES,
|
||||
callHandleId: 'aes-handle', callableCapable: true, arguments: safeArguments,
|
||||
stack: 'at aes (https://example.test/vendor/aes.js:1:1)\n at _0xPacket (https://example.test/app.js:40:2)\n at onclick (https://example.test/app.js:90:1)',
|
||||
outputs: [{ path: '$output:string', fingerprint: 'cipher-data', encoding: 'text', byteLength: 88 }],
|
||||
}),
|
||||
event({
|
||||
id: 'rsa-key', sequence: 2, kind: 'crypto', operation: 'encrypt',
|
||||
crypto: { adapterId: 'jsencrypt', providerKind: 'library', family: 'asymmetric', operation: 'encrypt', algorithm: 'RSA' },
|
||||
callHandleId: 'rsa-key-handle', callableCapable: true, arguments: safeArguments.slice(0, 1),
|
||||
stack: 'at rsa (https://example.test/vendor/rsa.js:1:1)\n at _0xPacket (https://example.test/app.js:51:2)\n at onclick (https://example.test/app.js:90:1)',
|
||||
outputs: [{ path: '$output', fingerprint: 'cipher-key', encoding: 'text', byteLength: 172 }],
|
||||
}),
|
||||
event({
|
||||
id: 'rsa-iv', sequence: 3, kind: 'crypto', operation: 'encrypt',
|
||||
crypto: { adapterId: 'jsencrypt', providerKind: 'library', family: 'asymmetric', operation: 'encrypt', algorithm: 'RSA' },
|
||||
callHandleId: 'rsa-iv-handle', callableCapable: true, arguments: safeArguments.slice(0, 1),
|
||||
stack: 'at rsa (https://example.test/vendor/rsa.js:8:1)\n at _0xPacket (https://example.test/app.js:58:2)\n at onclick (https://example.test/app.js:90:1)',
|
||||
outputs: [{ path: '$output', fingerprint: 'cipher-iv', encoding: 'text', byteLength: 172 }],
|
||||
}),
|
||||
];
|
||||
const request = event({
|
||||
id: 'hybrid-request', sequence: 4, kind: 'fetch', operation: 'request', method: 'POST', url: 'https://example.test/encrypt/aesrsa.php',
|
||||
inputs: [
|
||||
{ path: '$body:json.encryptedData', fingerprint: 'cipher-data', encoding: 'text', byteLength: 88 },
|
||||
{ path: '$body:json.encryptedKey', fingerprint: 'cipher-key', encoding: 'text', byteLength: 172 },
|
||||
{ path: '$body:json.encryptedIv', fingerprint: 'cipher-iv', encoding: 'text', byteLength: 172 },
|
||||
],
|
||||
});
|
||||
const links = sources.map((source, index) => link({
|
||||
id: `hybrid-link-${index}`,
|
||||
fromEventId: source.id,
|
||||
fromPath: source.outputs[0].path,
|
||||
toEventId: request.id,
|
||||
toPath: request.inputs[index].path,
|
||||
}));
|
||||
|
||||
const [candidate] = inferBrowserTransformProfiles({ target: { tabId: 7, frameId: 0 }, events: [...sources, request], links });
|
||||
expect(candidate.sources).toHaveLength(3);
|
||||
expect(candidate.request.mappings.map((item) => item.destination)).toEqual([
|
||||
'body.encryptedData', 'body.encryptedKey', 'body.encryptedIv',
|
||||
]);
|
||||
expect(candidate.status).toBe('capture-required');
|
||||
expect(candidate.summary).toContain('3 个密码调用');
|
||||
expect(candidate.missing[0].label).toContain('随机 Key、IV、Nonce');
|
||||
expect(candidate.capturePlan).toMatchObject({
|
||||
matcherEventId: sources[0].id,
|
||||
sourceCount: 3,
|
||||
expectedDestinations: ['body.encryptedData', 'body.encryptedKey', 'body.encryptedIv'],
|
||||
});
|
||||
expect(candidate.capturePlan?.frameHints[0]).toMatchObject({ functionName: '_0xPacket', support: 3 });
|
||||
});
|
||||
|
||||
it('keeps a stateful signature session as one request source and exposes its ordered stages', () => {
|
||||
const crypto = (operation: string, phase: 'create' | 'update' | 'final') => ({
|
||||
adapterId: 'jsrsasign', providerKind: 'library' as const, family: 'signature' as const, operation,
|
||||
algorithm: 'SHA256withRSA',
|
||||
state: { model: 'session' as const, correlationId: 'signature-session-1', phase },
|
||||
});
|
||||
const create = event({
|
||||
id: 'signature-create', sequence: 1, kind: 'crypto', operation: 'Signature.create', crypto: crypto('Signature.create', 'create'),
|
||||
});
|
||||
const update = event({
|
||||
id: 'signature-update', sequence: 2, kind: 'crypto', operation: 'Signature.updateString', crypto: crypto('Signature.updateString', 'update'),
|
||||
});
|
||||
const final = event({
|
||||
id: 'signature-final', sequence: 3, kind: 'crypto', operation: 'Signature.sign', crypto: crypto('Signature.sign', 'final'),
|
||||
outputs: [{ path: '$output', fingerprint: 'signature', encoding: 'hex', byteLength: 64 }],
|
||||
});
|
||||
const request = event({
|
||||
id: 'signed-request', sequence: 4, kind: 'fetch', operation: 'request', method: 'POST', url: 'https://example.test/api/signed',
|
||||
inputs: [{ path: '$headers.x-signature', fingerprint: 'signature', encoding: 'hex', byteLength: 64 }],
|
||||
});
|
||||
const links: BrowserRecordingLink[] = [
|
||||
{ id: 'state-create-update', traceId: 'trace-1', kind: 'state', confidence: 'correlated', fromEventId: create.id, fromPath: '$state.create', toEventId: update.id, toPath: '$state.update' },
|
||||
{ id: 'state-update-final', traceId: 'trace-1', kind: 'state', confidence: 'correlated', fromEventId: update.id, fromPath: '$state.update', toEventId: final.id, toPath: '$state.final' },
|
||||
link({ id: 'signature-request', fromEventId: final.id, fromPath: '$output', toEventId: request.id, toPath: '$headers.x-signature' }),
|
||||
];
|
||||
|
||||
const [candidate] = inferBrowserTransformProfiles({
|
||||
target: { tabId: 7, frameId: 0 }, events: [create, update, final, request], links,
|
||||
});
|
||||
expect(candidate.sources).toHaveLength(1);
|
||||
expect(candidate.source.eventId).toBe(final.id);
|
||||
expect(candidate.request.destination).toBe('header.x-signature');
|
||||
expect(candidate.evidence).toContainEqual(expect.objectContaining({
|
||||
kind: 'state-sequence', eventIds: [create.id, update.id, final.id],
|
||||
}));
|
||||
expect(candidate.capturePlan).toMatchObject({ sourceCount: 3 });
|
||||
});
|
||||
|
||||
it('traces canonical JSON through a signature and Axios into a request header', () => {
|
||||
const canonical = event({
|
||||
id: 'canonical-json', sequence: 1, kind: 'transform', operation: 'JSON.stringify',
|
||||
transform: { category: 'serializer', provider: 'native', phase: 'output' },
|
||||
inputs: [{ path: '$input.account', fingerprint: 'account', encoding: 'text', byteLength: 5 }],
|
||||
outputs: [{ path: '$output', fingerprint: 'canonical', encoding: 'text', byteLength: 42 }],
|
||||
});
|
||||
const signature = event({
|
||||
id: 'signature', sequence: 2, kind: 'crypto', operation: 'HmacSHA256', crypto: cryptoJsHmac,
|
||||
callHandleId: 'signature-handle', callableCapable: true, arguments: safeArguments,
|
||||
inputs: [{ path: '$input', fingerprint: 'canonical', encoding: 'text', byteLength: 42 }],
|
||||
outputs: [{ path: '$output', fingerprint: 'signed', encoding: 'hex', byteLength: 64 }],
|
||||
});
|
||||
const axios = event({
|
||||
id: 'axios', sequence: 3, kind: 'transform', operation: 'axios.request',
|
||||
transform: { category: 'request-builder', provider: 'axios', phase: 'boundary' },
|
||||
inputs: [{ path: '$headers.X-Signature', fingerprint: 'signed', encoding: 'hex', byteLength: 64 }],
|
||||
outputs: [{ path: '$headers.X-Signature', fingerprint: 'signed', encoding: 'hex', byteLength: 64 }],
|
||||
});
|
||||
const request = event({
|
||||
id: 'request', sequence: 4, kind: 'xhr', operation: 'request', method: 'POST', url: 'https://example.test/api/order',
|
||||
inputs: [{ path: '$headers.x-signature', fingerprint: 'signed', encoding: 'hex', byteLength: 64 }],
|
||||
});
|
||||
const events = [canonical, signature, axios, request];
|
||||
const [candidate] = inferBrowserTransformProfiles({
|
||||
target: { tabId: 7, frameId: 0 }, events, links: buildRecordingLinks(events),
|
||||
});
|
||||
|
||||
expect(candidate.sources).toHaveLength(1);
|
||||
expect(candidate.request).toMatchObject({ destination: 'header.x-signature', serialization: 'header' });
|
||||
expect(candidate.evidence).toContainEqual(expect.objectContaining({
|
||||
kind: 'transform-lineage', strength: 'proven', eventIds: [canonical.id, signature.id],
|
||||
}));
|
||||
expect(candidate.flow).toContain('1 个输入准备步骤');
|
||||
expect(candidate.flow).toContain('1 个中间转换');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user