mirror of
https://github.com/yaklang/yaklang-chrome-extension.git
synced 2026-09-24 12:11:53 +08:00
217 lines
10 KiB
TypeScript
217 lines
10 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import {
|
|
BRIDGE_MAX_MESSAGE_BYTES, BRIDGE_PROTOCOL_VERSION, parseBridgeEnvelope, parseBridgePairingEnvelope, parseCapabilityParams,
|
|
} from './bridge';
|
|
|
|
describe('Bridge v3 protocol', () => {
|
|
it('accepts an identified hello_ack', () => {
|
|
expect(parseBridgeEnvelope({
|
|
type: 'hello_ack', protocolVersion: BRIDGE_PROTOCOL_VERSION, version: 'test', capabilities: [],
|
|
sessionId: 'session-1', engineIdentityId: 'engine-identity-1', engineInstanceId: 'engine-1', connectionId: 'connection-1', resumed: true,
|
|
})).toMatchObject({ type: 'hello_ack', resumed: true });
|
|
});
|
|
|
|
it('rejects mismatched versions and missing identities', () => {
|
|
expect(() => parseBridgeEnvelope({ type: 'hello_ack', protocolVersion: 1, capabilities: [] })).toThrow('不兼容');
|
|
expect(() => parseBridgeEnvelope({ type: 'hello_ack', protocolVersion: BRIDGE_PROTOCOL_VERSION, capabilities: [] })).toThrow('engineIdentityId');
|
|
});
|
|
|
|
it('validates engine challenges and pairing responses', () => {
|
|
const publicKey = { kty: 'EC', crv: 'P-256', x: 'x-coordinate', y: 'y-coordinate' } as const;
|
|
expect(parseBridgeEnvelope({
|
|
type: 'challenge', protocolVersion: BRIDGE_PROTOCOL_VERSION, engineIdentityId: 'identity-1', engineInstanceId: 'instance-1',
|
|
challenge: 'challenge-1', signature: 'signature-1', timestamp: Date.now(), publicKey,
|
|
})).toMatchObject({ type: 'challenge', engineIdentityId: 'identity-1' });
|
|
expect(parseBridgePairingEnvelope({
|
|
type: 'pair_pending', protocolVersion: BRIDGE_PROTOCOL_VERSION, requestId: 'request-1', serverNonce: 'server-nonce',
|
|
engineIdentityId: 'identity-1', code: '123456', expiresAt: Date.now() + 60_000, publicKey,
|
|
})).toMatchObject({ type: 'pair_pending', code: '123456' });
|
|
expect(() => parseBridgePairingEnvelope({
|
|
type: 'pair_pending', protocolVersion: BRIDGE_PROTOCOL_VERSION, requestId: 'request-1', serverNonce: 'server-nonce',
|
|
engineIdentityId: 'identity-1', code: '123456', expiresAt: Date.now() + 6 * 60_000, publicKey,
|
|
})).toThrow('有效期异常');
|
|
});
|
|
|
|
it('rejects undeclared bridge and pairing fields', () => {
|
|
expect(() => parseBridgeEnvelope({ type: 'response', id: 'r1', result: {}, legacy: true })).toThrow('$.legacy');
|
|
expect(() => parseBridgeEnvelope({
|
|
type: 'response', id: 'r1', error: { code: 'failed', message: 'failed', legacy: true },
|
|
})).toThrow('$.error.legacy');
|
|
expect(() => parseBridgePairingEnvelope({ type: 'pair_error', message: 'failed', legacy: true })).toThrow('$.legacy');
|
|
});
|
|
|
|
it('validates heartbeat and chunk boundaries', () => {
|
|
expect(parseBridgeEnvelope({ type: 'pong', id: 'p1', sequence: 3, timestamp: 100 })).toMatchObject({ sequence: 3 });
|
|
expect(() => parseBridgeEnvelope({ type: 'ping' })).toThrow('心跳');
|
|
expect(parseBridgeEnvelope({
|
|
type: 'chunk', transferId: 't1', index: 0, total: 2, data: 'eA==', originalBytes: 2,
|
|
})).toMatchObject({ transferId: 't1' });
|
|
expect(() => parseBridgeEnvelope({
|
|
type: 'chunk', transferId: 't1', index: 2, total: 2, data: 'eA==', originalBytes: 2,
|
|
})).toThrow('序号');
|
|
});
|
|
|
|
it('requires explicit Eval mode and caps raw payloads', () => {
|
|
expect(parseCapabilityParams('browser.eval', { mode: 'expression', code: 'document.title' })).toMatchObject({ mode: 'expression' });
|
|
expect(() => parseCapabilityParams('browser.eval', { code: 'document.title' })).toThrow('mode');
|
|
expect(() => parseBridgeEnvelope('x'.repeat(BRIDGE_MAX_MESSAGE_BYTES + 1))).toThrow('16 MiB');
|
|
});
|
|
|
|
it('opens only HTTP(S) pages in the attached browser instance', () => {
|
|
expect(parseCapabilityParams('browser.tab.open', { url: 'https://www.baidu.com/' }))
|
|
.toEqual({ url: 'https://www.baidu.com/' });
|
|
expect(() => parseCapabilityParams('browser.tab.open', { url: 'chrome://settings' })).toThrow('HTTP(S)');
|
|
});
|
|
|
|
it('accepts exact Worker boundary handles for remote deep capture', () => {
|
|
expect(parseCapabilityParams('browser.deep_capture.start', {
|
|
matcher: {
|
|
kind: 'boundary', eventKind: 'worker', operation: 'worker.postMessage', wrapperHandleId: 'boundary-wrapper-1',
|
|
},
|
|
})).toMatchObject({ matcher: { kind: 'boundary', eventKind: 'worker' } });
|
|
});
|
|
|
|
it('validates browser identity isolation capabilities without accepting an unbounded tab query', () => {
|
|
expect(parseCapabilityParams('browser.isolation.inspect', {
|
|
tabIds: [12, 13],
|
|
})).toEqual({ tabIds: [12, 13] });
|
|
expect(parseCapabilityParams('browser.isolation.proof', {
|
|
leftTabId: 12,
|
|
rightTabId: 13,
|
|
})).toEqual({ leftTabId: 12, rightTabId: 13 });
|
|
expect(parseCapabilityParams('browser.isolation.incognito.open', {
|
|
url: 'https://example.test/login',
|
|
})).toEqual({ url: 'https://example.test/login' });
|
|
expect(() => parseCapabilityParams('browser.isolation.inspect', { tabIds: [] })).toThrow();
|
|
expect(() => parseCapabilityParams('browser.isolation.proof', {
|
|
leftTabId: 12,
|
|
rightTabId: 12,
|
|
})).toThrow();
|
|
expect(() => parseCapabilityParams('browser.isolation.incognito.open', {
|
|
url: 'chrome://extensions',
|
|
})).toThrow();
|
|
});
|
|
|
|
it('accepts automatic selected-frame capture and rejects the legacy expression contract', () => {
|
|
expect(parseCapabilityParams('browser.callable.create', {
|
|
source: 'recording', callHandleId: 'call-1', name: 'Dynamic decrypt',
|
|
dynamicInputPaths: ['$input', '$input.key', '$input.iv'],
|
|
})).toMatchObject({ dynamicInputPaths: ['$input', '$input.key', '$input.iv'] });
|
|
expect(parseCapabilityParams('browser.callable.create', {
|
|
source: 'deep-capture', strategy: 'selected-frame', callFrameId: 'frame-1', name: 'Envelope',
|
|
candidateId: 'candidate-envelope',
|
|
})).toMatchObject({ strategy: 'selected-frame', callFrameId: 'frame-1', candidateId: 'candidate-envelope' });
|
|
expect(() => parseCapabilityParams('browser.callable.create', {
|
|
source: 'deep-capture', callFrameId: 'frame-1', name: 'Envelope', functionExpression: 'buildEnvelope',
|
|
})).toThrow();
|
|
});
|
|
|
|
it('accepts only an evidence candidate reference for request-transaction capture', () => {
|
|
expect(parseCapabilityParams('browser.callable.create', {
|
|
tabId: 12,
|
|
frameId: 0,
|
|
source: 'deep-capture',
|
|
strategy: 'request-transaction',
|
|
callFrameId: 'frame-1',
|
|
name: 'Login envelope',
|
|
candidateId: 'candidate-login',
|
|
})).toMatchObject({
|
|
strategy: 'request-transaction',
|
|
candidateId: 'candidate-login',
|
|
});
|
|
expect(() => parseCapabilityParams('browser.callable.create', {
|
|
source: 'deep-capture', strategy: 'request-transaction', callFrameId: 'frame-1',
|
|
candidateId: 'candidate-login', transaction: {},
|
|
})).toThrow();
|
|
});
|
|
|
|
it('validates the bounded browser crypto Agent tool contracts', () => {
|
|
const packet = {
|
|
method: 'POST',
|
|
url: 'https://example.test/login',
|
|
headers: [{ name: 'Content-Type', value: 'application/json' }],
|
|
bodyBase64: 'e30=',
|
|
};
|
|
expect(parseCapabilityParams('browser.recording.trace.list', {
|
|
tabId: 12, frameId: 0, limit: 20,
|
|
})).toMatchObject({ limit: 20 });
|
|
expect(parseCapabilityParams('browser.crypto.inspect', {
|
|
tabId: 12, captureId: 'capture-1', nodeId: 'n1', settleMs: 2_000,
|
|
})).toMatchObject({ captureId: 'capture-1', nodeId: 'n1', settleMs: 2_000 });
|
|
expect(() => parseCapabilityParams('browser.crypto.inspect', {
|
|
captureId: 'capture-1', nodeId: 'n1', settleMs: 30_000,
|
|
})).toThrow();
|
|
expect(parseCapabilityParams('browser.recording.evidence.inspect', {
|
|
tabId: 12, traceId: 'trace-1', includeValues: false,
|
|
})).toMatchObject({ traceId: 'trace-1', includeValues: false });
|
|
expect(parseCapabilityParams('browser.callable.inspect', {
|
|
tabId: 12, callableId: 'callable-1',
|
|
})).toMatchObject({ callableId: 'callable-1' });
|
|
expect(parseCapabilityParams('browser.callable.replay', {
|
|
tabId: 12, callableId: 'callable-1', args: [{ username: 'admin' }],
|
|
})).toMatchObject({ callableId: 'callable-1' });
|
|
expect(parseCapabilityParams('browser.packet.compare', {
|
|
tabId: 12, actual: packet, expected: packet, mode: 'structure',
|
|
})).toMatchObject({ mode: 'structure' });
|
|
expect(parseCapabilityParams('browser.profile.propose', {
|
|
tabId: 12,
|
|
candidateId: 'candidate-1',
|
|
callableId: 'callable-1',
|
|
inputPaths: ['body'],
|
|
})).toMatchObject({ candidateId: 'candidate-1', callableId: 'callable-1' });
|
|
expect(parseCapabilityParams('browser.profile.validation.latest', {
|
|
tabId: 12, frameId: 0, documentId: 'document-1',
|
|
})).toMatchObject({ tabId: 12, documentId: 'document-1' });
|
|
expect(parseCapabilityParams('browser.profile.validate', {
|
|
tabId: 12,
|
|
candidateId: 'candidate-1',
|
|
callableId: 'callable-1',
|
|
inputPaths: ['body'],
|
|
packet,
|
|
})).toMatchObject({
|
|
tabId: 12,
|
|
candidateId: 'candidate-1',
|
|
callableId: 'callable-1',
|
|
});
|
|
expect(parseCapabilityParams('browser.transform.prepare', {
|
|
tabId: 12,
|
|
candidateId: 'candidate-1',
|
|
inputPaths: ['body'],
|
|
packet,
|
|
})).toMatchObject({ candidateId: 'candidate-1', packet });
|
|
expect(parseCapabilityParams('browser.transform.recovery.start', {
|
|
id: 'profile-1',
|
|
})).toMatchObject({ id: 'profile-1' });
|
|
expect(parseCapabilityParams('browser.transform.recovery.capture', {
|
|
id: 'profile-1',
|
|
tabId: 12,
|
|
frameId: 0,
|
|
documentId: 'document-2',
|
|
callFrameId: 'frame-1',
|
|
strategy: 'request-transaction',
|
|
})).toMatchObject({
|
|
id: 'profile-1',
|
|
documentId: 'document-2',
|
|
strategy: 'request-transaction',
|
|
});
|
|
expect(parseCapabilityParams('browser.transform.recovery.validate', {
|
|
id: 'profile-1',
|
|
packet,
|
|
})).toMatchObject({ id: 'profile-1' });
|
|
expect(parseCapabilityParams('browser.transform.recovery.confirm', {
|
|
id: 'profile-1',
|
|
validationId: 'validation-1',
|
|
})).toMatchObject({ validationId: 'validation-1' });
|
|
expect(parseCapabilityParams('browser.transform.validation.execute', {
|
|
validationId: 'validation-1',
|
|
direction: 'request',
|
|
packet,
|
|
})).toMatchObject({ validationId: 'validation-1', direction: 'request' });
|
|
expect(() => parseCapabilityParams('browser.profile.validate', {
|
|
tabId: 12,
|
|
profile: {},
|
|
packet,
|
|
})).toThrow();
|
|
});
|
|
});
|