mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-08-29 04:26:38 +00:00
248 lines
10 KiB
TypeScript
248 lines
10 KiB
TypeScript
/** Client-face Runtime behavior. */
|
|
|
|
import { afterEach, describe, expect, it } from 'vitest'
|
|
import { ClientRuntimeExecutor } from '../src/client/cdp/runtime.ts'
|
|
import type {
|
|
ClientRuntimeCommand,
|
|
ClientRuntimeRequestFrame,
|
|
ClientRuntimeResult,
|
|
} from '../src/shared/bridge/messages/runtime/index.ts'
|
|
import {
|
|
inspectorId,
|
|
} from '../src/shared/bridge/ids.ts'
|
|
|
|
const sourceId = inspectorId<'InspectorSourceId'>('client-test', 'sourceId')
|
|
const generation = inspectorId<'InspectorSourceGeneration'>('generation-test', 'generation')
|
|
const sessionId = inspectorId<'ClientRuntimeSessionId'>('session-test', 'sessionId')
|
|
const secondSessionId = inspectorId<'ClientRuntimeSessionId'>('session-second', 'sessionId')
|
|
|
|
describe('Client Runtime executor', () => {
|
|
afterEach(() => {
|
|
Reflect.deleteProperty(globalThis, '__clientRuntimeFixture')
|
|
Reflect.deleteProperty(globalThis, '__clientRuntimeGetterCalls')
|
|
})
|
|
|
|
it('retains RemoteObjects, reads descriptors lazily, calls functions, and releases groups', async () => {
|
|
Reflect.set(globalThis, '__clientRuntimeGetterCalls', 0)
|
|
const fixture = {
|
|
value: 4,
|
|
get dangerous(): number {
|
|
const calls = Number(Reflect.get(globalThis, '__clientRuntimeGetterCalls'))
|
|
Reflect.set(globalThis, '__clientRuntimeGetterCalls', calls + 1)
|
|
return 99
|
|
},
|
|
}
|
|
Object.defineProperty(fixture, Symbol.toStringTag, {
|
|
get() {
|
|
const calls = Number(Reflect.get(globalThis, '__clientRuntimeGetterCalls'))
|
|
Reflect.set(globalThis, '__clientRuntimeGetterCalls', calls + 1)
|
|
return 'DangerousTag'
|
|
},
|
|
})
|
|
Reflect.set(globalThis, '__clientRuntimeFixture', fixture)
|
|
const runtime = new ClientRuntimeExecutor({
|
|
maxObjectsPerSession: 100,
|
|
maxPropertiesPerResult: 100,
|
|
maxResponseBytes: 32_768,
|
|
})
|
|
|
|
const evaluated = success(await runtime.execute(frame({
|
|
op: 'evaluate',
|
|
expression: 'globalThis.__clientRuntimeFixture',
|
|
objectGroup: 'console',
|
|
generatePreview: true,
|
|
})), 'evaluate')
|
|
const handle = evaluated.completion.result.object?.handle
|
|
if (handle === undefined) throw new Error('evaluate did not return a Client object handle')
|
|
|
|
const properties = success(await runtime.execute(frame({
|
|
op: 'get-properties',
|
|
handle,
|
|
ownProperties: true,
|
|
})), 'get-properties')
|
|
const valueProperty = properties.properties.find(property => property.name === 'value')
|
|
const getterProperty = properties.properties.find(property => property.name === 'dangerous')
|
|
expect(valueProperty?.value).toMatchObject({ descriptor: { type: 'number', value: 4 } })
|
|
expect(getterProperty?.get).toMatchObject({ descriptor: { type: 'function' } })
|
|
expect(Reflect.get(globalThis, '__clientRuntimeGetterCalls')).toBe(0)
|
|
|
|
const called = success(await runtime.execute(frame({
|
|
op: 'call-function',
|
|
functionDeclaration: 'function (increment) { return this.value + increment }',
|
|
receiver: handle,
|
|
arguments: [{ kind: 'value', value: 3 }],
|
|
returnByValue: true,
|
|
})), 'call-function')
|
|
expect(called.completion.result).toMatchObject({ descriptor: { type: 'number', value: 7 } })
|
|
|
|
success(await runtime.execute(frame({ op: 'release-object-group', objectGroup: 'console' })), 'release-object-group')
|
|
const released = await runtime.execute(frame({ op: 'get-properties', handle }))
|
|
expect(released.outcome).toEqual({
|
|
ok: false,
|
|
error: { code: 'object-not-found', message: 'Client RemoteObject was released' },
|
|
})
|
|
})
|
|
|
|
it('keeps evaluated exceptions separate from transport failures', async () => {
|
|
const runtime = new ClientRuntimeExecutor({
|
|
maxObjectsPerSession: 100,
|
|
maxPropertiesPerResult: 100,
|
|
maxResponseBytes: 32_768,
|
|
})
|
|
const result = success(await runtime.execute(frame({
|
|
op: 'evaluate',
|
|
expression: 'throw new TypeError("bad value")',
|
|
})), 'evaluate')
|
|
expect(result.completion.exceptionDetails).toMatchObject({
|
|
text: 'Uncaught',
|
|
exception: { descriptor: { type: 'object', subtype: 'error' } },
|
|
})
|
|
expect(result.completion.result).toMatchObject({ descriptor: { type: 'object', subtype: 'error' } })
|
|
})
|
|
|
|
it('preserves non-JSON primitives and reports bounded async execution failures', async () => {
|
|
const runtime = new ClientRuntimeExecutor({
|
|
maxObjectsPerSession: 100,
|
|
maxPropertiesPerResult: 100,
|
|
maxResponseBytes: 32_768,
|
|
})
|
|
const values = [
|
|
['NaN', { descriptor: { type: 'number', unserializableValue: 'NaN' } }],
|
|
['-0', { descriptor: { type: 'number', unserializableValue: '-0' } }],
|
|
['12n', { descriptor: { type: 'bigint', unserializableValue: '12n' } }],
|
|
['null', { descriptor: { type: 'object', subtype: 'null', value: null } }],
|
|
] as const
|
|
for (const [expression, expected] of values) {
|
|
const result = success(await runtime.execute(frame({ op: 'evaluate', expression })), 'evaluate')
|
|
expect(result.completion.result).toMatchObject(expected)
|
|
}
|
|
const fn = success(await runtime.execute(frame({
|
|
op: 'evaluate',
|
|
expression: '(value) => value',
|
|
generatePreview: true,
|
|
})), 'evaluate')
|
|
expect(fn.completion.result).toMatchObject({ descriptor: { type: 'function' } })
|
|
expect(fn.completion.result.descriptor.preview).toBeUndefined()
|
|
|
|
const timedOut = await runtime.execute(frame({
|
|
op: 'evaluate',
|
|
expression: 'new Promise(() => {})',
|
|
awaitPromise: true,
|
|
timeoutMs: 1,
|
|
}))
|
|
expect(timedOut.outcome).toMatchObject({ ok: false, error: { code: 'timeout' } })
|
|
})
|
|
|
|
it('rolls back only objects allocated by the failing concurrent request', async () => {
|
|
const runtime = new ClientRuntimeExecutor({
|
|
maxObjectsPerSession: 100,
|
|
maxPropertiesPerResult: 100,
|
|
maxResponseBytes: 32_768,
|
|
})
|
|
const blocked = runtime.execute(frame({
|
|
op: 'evaluate',
|
|
expression: 'new Promise(() => {})',
|
|
awaitPromise: true,
|
|
timeoutMs: 10,
|
|
}))
|
|
const completed = success(await runtime.execute(frame({
|
|
op: 'evaluate',
|
|
expression: '({ retainedByConcurrentRequest: true })',
|
|
})), 'evaluate')
|
|
const handle = completed.completion.result.object?.handle
|
|
if (handle === undefined) throw new Error('concurrent evaluation did not retain an object')
|
|
|
|
await expect(blocked).resolves.toMatchObject({ outcome: { ok: false, error: { code: 'timeout' } } })
|
|
const properties = success(await runtime.execute(frame({
|
|
op: 'get-properties',
|
|
handle,
|
|
ownProperties: true,
|
|
})), 'get-properties')
|
|
expect(properties.properties.find(property => property.name === 'retainedByConcurrentRequest')?.value)
|
|
.toMatchObject({ descriptor: { value: true } })
|
|
})
|
|
|
|
it('rejects oversized by-value results before they enter the source transport', async () => {
|
|
const runtime = new ClientRuntimeExecutor({
|
|
maxObjectsPerSession: 100,
|
|
maxPropertiesPerResult: 100,
|
|
maxResponseBytes: 256,
|
|
})
|
|
const response = await runtime.execute(frame({
|
|
op: 'evaluate',
|
|
expression: '"x".repeat(1000)',
|
|
returnByValue: true,
|
|
}))
|
|
expect(response.outcome).toMatchObject({ ok: false, error: { code: 'result-too-large' } })
|
|
})
|
|
|
|
it('drops every retained handle when its DevTools Runtime session closes', async () => {
|
|
const runtime = new ClientRuntimeExecutor({
|
|
maxObjectsPerSession: 100,
|
|
maxPropertiesPerResult: 100,
|
|
maxResponseBytes: 32_768,
|
|
})
|
|
const evaluated = success(await runtime.execute(frame({
|
|
op: 'evaluate',
|
|
expression: '({ retained: true })',
|
|
})), 'evaluate')
|
|
const handle = evaluated.completion.result.object?.handle
|
|
if (handle === undefined) throw new Error('evaluate did not return a Client object handle')
|
|
|
|
runtime.closeSession(sessionId)
|
|
const response = await runtime.execute(frame({ op: 'get-properties', handle }))
|
|
expect(response.outcome).toMatchObject({ ok: false, error: { code: 'object-not-found' } })
|
|
})
|
|
|
|
it('serializes Console objects into isolated DevTools sessions', async () => {
|
|
const runtime = new ClientRuntimeExecutor({
|
|
maxObjectsPerSession: 100,
|
|
maxPropertiesPerResult: 100,
|
|
maxResponseBytes: 32_768,
|
|
})
|
|
const value = { owner: 'console' }
|
|
const first = runtime.consoleEvent(sessionId, 'log', [value], 12)
|
|
const second = runtime.consoleEvent(secondSessionId, 'log', [value], 12)
|
|
if (first?.type !== 'console-api' || second?.type !== 'console-api') {
|
|
throw new Error('Console event was unexpectedly dropped')
|
|
}
|
|
const firstHandle = first.event.arguments[0]?.object?.handle
|
|
const secondHandle = second.event.arguments[0]?.object?.handle
|
|
if (firstHandle === undefined || secondHandle === undefined) throw new Error('Console object was not retained')
|
|
|
|
runtime.releaseObjectGroup(sessionId, 'console')
|
|
expect((await runtime.execute(frame({ op: 'get-properties', handle: firstHandle }))).outcome)
|
|
.toMatchObject({ ok: false, error: { code: 'object-not-found' } })
|
|
const properties = success(await runtime.execute(
|
|
frame({ op: 'get-properties', handle: secondHandle }, secondSessionId),
|
|
), 'get-properties').properties
|
|
expect(properties.find(property => property.name === 'owner')?.value?.descriptor.value).toBe('console')
|
|
})
|
|
})
|
|
|
|
let nextRequestId = 0
|
|
|
|
function frame(
|
|
command: ClientRuntimeCommand,
|
|
owner: ClientRuntimeRequestFrame['sessionId'] = sessionId,
|
|
): ClientRuntimeRequestFrame {
|
|
return {
|
|
v: 0,
|
|
t: 'client-runtime/request',
|
|
sourceId,
|
|
generation,
|
|
sessionId: owner,
|
|
requestId: inspectorId<'ClientRuntimeRequestId'>(`request-${String(++nextRequestId)}`, 'requestId'),
|
|
command,
|
|
}
|
|
}
|
|
|
|
function success<Operation extends ClientRuntimeResult['op']>(
|
|
response: Awaited<ReturnType<ClientRuntimeExecutor['execute']>>,
|
|
operation: Operation,
|
|
): Extract<ClientRuntimeResult, { op: Operation }> {
|
|
if (!response.outcome.ok) throw new Error(response.outcome.error.message)
|
|
if (response.outcome.result.op !== operation) throw new Error('unexpected Client Runtime result')
|
|
return response.outcome.result as Extract<ClientRuntimeResult, { op: Operation }>
|
|
}
|