mirror of
https://github.com/yaklang/yaklang-chrome-extension.git
synced 2026-09-25 20:51:52 +08:00
feat: advance browser agent integration workflows
This commit is contained in:
@@ -0,0 +1,203 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import type {
|
||||
BrowserPageCallable,
|
||||
BrowserTransformProfile,
|
||||
} from '@/types/models';
|
||||
import { compileGuidedTransform, defaultGuidedTransform } from './guided';
|
||||
import {
|
||||
BROWSER_TRANSFORM_RECOVERY_MAX_BYTES,
|
||||
compileBrowserTransformRecoveryDirections,
|
||||
createBrowserTransformRecoveryPlan,
|
||||
staleBrowserTransformProfile,
|
||||
} from './recovery-plan';
|
||||
|
||||
function callable(
|
||||
id: string,
|
||||
output: BrowserPageCallable['output'] = {
|
||||
dataType: 'string',
|
||||
encoding: 'utf8',
|
||||
shape: 'value',
|
||||
paths: [],
|
||||
},
|
||||
transaction?: BrowserPageCallable['transaction'],
|
||||
): BrowserPageCallable {
|
||||
return {
|
||||
id,
|
||||
name: '页面 AES 加密',
|
||||
kind: transaction ? 'request-transaction' : 'recorded-call',
|
||||
operation: 'CryptoJS.AES.encrypt',
|
||||
origin: 'http://127.0.0.1:82',
|
||||
target: { tabId: 9, frameId: 0, documentId: 'document-old' },
|
||||
lifecycle: 'document',
|
||||
execution: { resultMode: 'auto', timeoutMs: 8_000 },
|
||||
inputSlots: [{
|
||||
id: 'arg-0',
|
||||
name: 'payload',
|
||||
index: 0,
|
||||
role: 'data',
|
||||
dataType: 'string',
|
||||
required: true,
|
||||
retained: false,
|
||||
}],
|
||||
output,
|
||||
transaction,
|
||||
provenance: {
|
||||
sourceUrl: 'http://127.0.0.1:82/assets/app.random.js?access_token=source-secret#private',
|
||||
functionName: 'sendEncrypted',
|
||||
lineNumber: 42,
|
||||
businessFrameHints: [{
|
||||
functionName: 'sendEncrypted',
|
||||
url: 'http://127.0.0.1:82/assets/app.random.js?session=frame-secret',
|
||||
support: 1,
|
||||
averageDepth: 0,
|
||||
}],
|
||||
},
|
||||
createdAt: 1,
|
||||
};
|
||||
}
|
||||
|
||||
function profile(primary: BrowserPageCallable): BrowserTransformProfile {
|
||||
return {
|
||||
id: 'profile-1',
|
||||
name: 'AES 明文网关',
|
||||
enabled: true,
|
||||
target: { ...primary.target },
|
||||
isolationContextId: 'browser-profile:store-1',
|
||||
cookieStoreId: 'store-1',
|
||||
origin: primary.origin,
|
||||
match: { methods: ['POST'], urlPattern: '*/encrypt/random.php' },
|
||||
request: compileGuidedTransform({
|
||||
...defaultGuidedTransform(primary, { outputKind: 'form-field', outputField: 'ciphertext' }),
|
||||
outputKind: 'form-field',
|
||||
outputField: 'ciphertext',
|
||||
setFormContentType: true,
|
||||
}, primary),
|
||||
response: { enabled: false, nodes: [] },
|
||||
failMode: 'closed',
|
||||
maxConcurrency: 2,
|
||||
createdAt: 1,
|
||||
updatedAt: 1,
|
||||
};
|
||||
}
|
||||
|
||||
describe('browser transform document recovery plan', () => {
|
||||
it('stores only capture semantics and disables a stale document binding', () => {
|
||||
const original = profile(callable('call-old'));
|
||||
const recovery = createBrowserTransformRecoveryPlan(original, [callable('call-old')], 10);
|
||||
|
||||
expect(recovery).toMatchObject({
|
||||
contractVersion: 1,
|
||||
state: 'ready',
|
||||
desiredEnabled: true,
|
||||
capture: {
|
||||
kind: 'request',
|
||||
method: 'POST',
|
||||
url: 'http://127.0.0.1:82/encrypt/random.php',
|
||||
urlPattern: '/encrypt/random.php',
|
||||
bodyFormat: 'form',
|
||||
expectedDestinations: ['body.ciphertext'],
|
||||
automatic: true,
|
||||
},
|
||||
binding: {
|
||||
callableId: 'call-old',
|
||||
operation: 'CryptoJS.AES.encrypt',
|
||||
},
|
||||
});
|
||||
const serialized = JSON.stringify(recovery);
|
||||
expect(new TextEncoder().encode(serialized).byteLength).toBeLessThanOrEqual(BROWSER_TRANSFORM_RECOVERY_MAX_BYTES);
|
||||
expect(serialized).not.toContain('source-secret');
|
||||
expect(serialized).not.toContain('frame-secret');
|
||||
expect(serialized).not.toContain('access_token');
|
||||
expect(serialized).not.toContain('session=');
|
||||
|
||||
const stale = staleBrowserTransformProfile({ ...original, recovery }, '页面已刷新', 20);
|
||||
expect(stale.enabled).toBe(false);
|
||||
expect(stale.recovery).toMatchObject({
|
||||
state: 'stale',
|
||||
desiredEnabled: true,
|
||||
reason: '页面已刷新',
|
||||
});
|
||||
});
|
||||
|
||||
it('recompiles a value field gateway into one full request transaction without double wrapping', () => {
|
||||
const originalCallable = callable('call-old');
|
||||
const original = profile(originalCallable);
|
||||
const recovery = createBrowserTransformRecoveryPlan(original, [originalCallable], 10);
|
||||
expect(recovery).toBeDefined();
|
||||
|
||||
const transaction = {
|
||||
version: 2 as const,
|
||||
prerequisites: [],
|
||||
request: {
|
||||
boundary: 'fetch' as const,
|
||||
method: 'POST',
|
||||
url: 'http://127.0.0.1:82/encrypt/random.php',
|
||||
expectedDestinations: ['body.ciphertext'],
|
||||
bodyFormat: 'form' as const,
|
||||
},
|
||||
inputMode: 'auto' as const,
|
||||
};
|
||||
const captured = callable('call-new', {
|
||||
dataType: 'object',
|
||||
encoding: 'utf8',
|
||||
shape: 'envelope',
|
||||
paths: ['body.ciphertext'],
|
||||
}, transaction);
|
||||
const directions = compileBrowserTransformRecoveryDirections(original, recovery!, captured);
|
||||
|
||||
expect(directions.request.nodes.filter((node) => node.kind === 'page.call')).toHaveLength(1);
|
||||
expect(directions.request.nodes.find((node) => node.kind === 'page.call')).toMatchObject({
|
||||
callableId: 'call-new',
|
||||
});
|
||||
expect(directions.request.nodes.some((node) => node.kind === 'builtin' && node.operation === 'form.compose')).toBe(false);
|
||||
expect(directions.request.nodes.some((node) => node.kind === 'builtin' && node.operation === 'form.serialize')).toBe(true);
|
||||
expect(directions.request.nodes.find((node) => node.kind === 'output.write' && node.destination === 'body')).toMatchObject({
|
||||
encoding: 'text',
|
||||
});
|
||||
});
|
||||
|
||||
it('fails closed when multiple page functions make automatic rebinding ambiguous', () => {
|
||||
const first = callable('call-a');
|
||||
const second = callable('call-b');
|
||||
const original = profile(first);
|
||||
original.request.nodes.splice(-1, 0, {
|
||||
id: 'second-call',
|
||||
name: '第二个页面调用',
|
||||
kind: 'page.call',
|
||||
callableId: second.id,
|
||||
arguments: [],
|
||||
});
|
||||
|
||||
const recovery = createBrowserTransformRecoveryPlan(original, [first, second], 10);
|
||||
expect(recovery?.capture.automatic).toBe(false);
|
||||
expect(recovery?.capture.reason).toContain('多个页面函数');
|
||||
});
|
||||
|
||||
it('does not persist a sensitive request query or arm an inexact recovery boundary', () => {
|
||||
const transaction = callable('call-sensitive', {
|
||||
dataType: 'object',
|
||||
encoding: 'utf8',
|
||||
shape: 'envelope',
|
||||
paths: ['body.ciphertext'],
|
||||
}, {
|
||||
version: 2,
|
||||
prerequisites: [],
|
||||
request: {
|
||||
boundary: 'fetch',
|
||||
method: 'POST',
|
||||
url: 'http://127.0.0.1:82/encrypt/random.php?access_token=request-secret',
|
||||
expectedDestinations: ['body.ciphertext'],
|
||||
bodyFormat: 'form',
|
||||
},
|
||||
inputMode: 'auto',
|
||||
});
|
||||
const recovery = createBrowserTransformRecoveryPlan(profile(transaction), [transaction], 10);
|
||||
const serialized = JSON.stringify(recovery);
|
||||
|
||||
expect(recovery?.capture.automatic).toBe(false);
|
||||
expect(recovery?.capture.reason).toContain('精确路由');
|
||||
expect(recovery?.binding.transaction?.request.url).toBe('http://127.0.0.1:82/encrypt/random.php');
|
||||
expect(serialized).not.toContain('request-secret');
|
||||
expect(serialized).not.toContain('access_token');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user