| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462 |
- // @vitest-environment jsdom
- import { Context } from '@deepseek-ai/cordis'
- import { afterEach, describe, expect, it, vi } from 'vitest'
- import { apply } from '../src/client/index.ts'
- import { ClientRealmSource } from '../src/client/inspection/realm.ts'
- import type { InspectorClientBootstrap } from '../src/shared/bridge/messages/control.ts'
- class FakeWebSocket extends EventTarget {
- static readonly CONNECTING = 0
- static readonly OPEN = 1
- static readonly CLOSING = 2
- static readonly CLOSED = 3
- static readonly sockets: FakeWebSocket[] = []
- readonly sent: string[] = []
- readonly url: string
- readonly protocol: string
- readyState = FakeWebSocket.CONNECTING
- bufferedAmount = 0
- constructor(url: string | URL, protocols?: string | string[]) {
- super()
- this.url = String(url)
- this.protocol = typeof protocols === 'string' ? protocols : protocols?.[0] ?? ''
- FakeWebSocket.sockets.push(this)
- }
- send(data: string): void {
- this.sent.push(data)
- }
- close(): void {
- if (this.readyState === FakeWebSocket.CLOSED) return
- this.readyState = FakeWebSocket.CLOSED
- this.dispatchEvent(new Event('close'))
- }
- open(): void {
- this.readyState = FakeWebSocket.OPEN
- this.dispatchEvent(new Event('open'))
- }
- receive(value: unknown): void {
- this.dispatchEvent(new MessageEvent('message', { data: JSON.stringify(value) }))
- }
- }
- const bootstrap: InspectorClientBootstrap = {
- endpoint: 'ws://127.0.0.1:9230/ingest',
- protocol: 'dsh-inspector-v0-token',
- maxQueuedRecords: 16,
- maxQueuedBytes: 16_384,
- maxRecordsPerFrame: 8,
- maxFrameBytes: 32_768,
- reconnectBaseMs: 10,
- reconnectMaxMs: 20,
- queryTimeoutMs: 100,
- maxRuntimeObjectsPerSession: 100,
- maxRuntimePropertiesPerResult: 100,
- maxClientSourceBytes: 1_048_576,
- maxCordisNodes: 100,
- }
- describe('experimental Inspector Client plugin', () => {
- const nativeWebSocket = globalThis.WebSocket
- const nativeFetch = globalThis.fetch
- afterEach(() => {
- vi.restoreAllMocks()
- FakeWebSocket.sockets.length = 0
- globalThis.WebSocket = nativeWebSocket
- globalThis.fetch = nativeFetch
- sessionStorage.clear()
- delete globalThis.__DSH_INSPECTOR__
- Reflect.deleteProperty(globalThis, '__DSH_BOOT__')
- })
- it('provides ctx.inspector and sends observations after the Worker accepts the source', async () => {
- globalThis.WebSocket = FakeWebSocket as unknown as typeof WebSocket
- globalThis.__DSH_INSPECTOR__ = bootstrap
- const ctx = new Context()
- const fiber = ctx.plugin({ apply })
- await fiber.await()
- const socket = FakeWebSocket.sockets[0]!
- expect(socket.url).toBe(bootstrap.endpoint)
- expect(socket.protocol).toBe(bootstrap.protocol)
- socket.open()
- const open = JSON.parse(socket.sent[0]!) as {
- source: { sourceId: string; generation: string }
- }
- socket.receive({
- v: 0,
- t: 'source/accepted',
- sourceId: open.source.sourceId,
- generation: open.source.generation,
- })
- expect(JSON.parse(socket.sent[1]!) as unknown).toMatchObject({
- t: 'source/replace',
- records: [{ topic: 'cordis/tree', payload: { schemaVersion: 0, truncated: false } }],
- })
- const treePromise = ctx.inspector.cordis.getTree()
- const treeRequest = socket.sent.map(value => JSON.parse(value) as { t: string; requestId?: string })
- .find(frame => frame.t === 'query/request')
- expect(treeRequest?.requestId).toBeTypeOf('string')
- socket.receive({
- v: 0,
- t: 'query/response',
- sourceId: open.source.sourceId,
- generation: open.source.generation,
- requestId: treeRequest!.requestId,
- outcome: {
- ok: true,
- result: { op: 'cordis-tree/get', tree: { schemaVersion: 0, host: null, clients: [] } },
- },
- })
- await expect(treePromise).resolves.toEqual({ schemaVersion: 0, host: null, clients: [] })
- ctx.inspector.publish('client/probe', { ready: true }, 7)
- const append = socket.sent.map(value => JSON.parse(value) as {
- t: string
- records: Array<{ topic: string; monotonicMs: number; payload: unknown }>
- }).find(frame => frame.t === 'source/append'
- && frame.records.some(record => record.topic === 'client/probe'))
- expect(append).toMatchObject({
- t: 'source/append',
- records: [{ topic: 'client/probe', monotonicMs: 7, payload: { ready: true } }],
- })
- document.title = 'Inspector Client Realm'
- socket.receive({
- v: 0,
- t: 'client-runtime/request',
- sourceId: open.source.sourceId,
- generation: open.source.generation,
- sessionId: 'devtools-1',
- requestId: 'runtime-1',
- command: { op: 'evaluate', expression: 'document.title', returnByValue: true },
- })
- await vi.waitFor(() => {
- const response = socket.sent.map(value => JSON.parse(value) as { requestId?: string })
- .find(frame => frame.requestId === 'runtime-1')
- expect(response).toMatchObject({
- t: 'client-runtime/response',
- sessionId: 'devtools-1',
- requestId: 'runtime-1',
- outcome: {
- ok: true,
- result: { op: 'evaluate', completion: { result: { descriptor: { value: 'Inspector Client Realm' } } } },
- },
- })
- })
- await fiber.dispose()
- expect(JSON.parse(socket.sent.at(-1)!)).toMatchObject({ t: 'source/close' })
- expect(socket.readyState).toBe(FakeWebSocket.CLOSED)
- })
- it('keeps the realm source id and rotates the transport generation on reconnect', async () => {
- globalThis.WebSocket = FakeWebSocket as unknown as typeof WebSocket
- globalThis.__DSH_INSPECTOR__ = bootstrap
- const ctx = new Context()
- const fiber = ctx.plugin({ apply })
- await fiber.await()
- const firstSocket = FakeWebSocket.sockets[0]!
- firstSocket.open()
- const firstOpen = JSON.parse(firstSocket.sent[0]!) as {
- source: { sourceId: string; generation: string }
- }
- firstSocket.close()
- await vi.waitFor(() => { expect(FakeWebSocket.sockets).toHaveLength(2) })
- const secondSocket = FakeWebSocket.sockets[1]!
- secondSocket.open()
- const secondOpen = JSON.parse(secondSocket.sent[0]!) as {
- source: { sourceId: string; generation: string }
- }
- expect(secondOpen.source.sourceId).toBe(firstOpen.source.sourceId)
- expect(secondOpen.source.generation).not.toBe(firstOpen.source.generation)
- await fiber.dispose()
- })
- it('keeps the logical source id when the Client plugin is recreated after a page refresh', async () => {
- globalThis.WebSocket = FakeWebSocket as unknown as typeof WebSocket
- globalThis.__DSH_INSPECTOR__ = bootstrap
- const firstContext = new Context()
- const firstFiber = firstContext.plugin({ apply })
- await firstFiber.await()
- const firstSocket = FakeWebSocket.sockets[0]!
- firstSocket.open()
- const firstOpen = JSON.parse(firstSocket.sent[0]!) as {
- source: { sourceId: string; generation: string }
- }
- await firstFiber.dispose()
- const secondContext = new Context()
- const secondFiber = secondContext.plugin({ apply })
- await secondFiber.await()
- const secondSocket = FakeWebSocket.sockets[1]!
- secondSocket.open()
- const secondOpen = JSON.parse(secondSocket.sent[0]!) as {
- source: { sourceId: string; generation: string }
- }
- expect(secondOpen.source.sourceId).toBe(firstOpen.source.sourceId)
- expect(secondOpen.source.generation).not.toBe(firstOpen.source.generation)
- await secondFiber.dispose()
- })
- it('rotates a copied session identity while its original page remains live', async () => {
- const descriptor = Object.getOwnPropertyDescriptor(navigator, 'locks')
- const held = new Set<string>()
- const request = async (
- name: string,
- _options: LockOptions,
- callback: (lock: Lock | null) => unknown,
- ): Promise<unknown> => {
- const acquired = !held.has(name)
- if (acquired) held.add(name)
- try {
- return await callback(acquired ? { name, mode: 'exclusive' } : null)
- } finally {
- if (acquired) held.delete(name)
- }
- }
- Object.defineProperty(navigator, 'locks', {
- configurable: true,
- value: { request },
- })
- let first: ClientRealmSource | undefined
- let duplicate: ClientRealmSource | undefined
- let refreshed: ClientRealmSource | undefined
- try {
- first = await ClientRealmSource.claim('first')
- duplicate = await ClientRealmSource.claim('duplicate')
- expect(duplicate.sourceId).not.toBe(first.sourceId)
- first.close()
- await vi.waitFor(() => { expect(held.size).toBe(1) })
- sessionStorage.setItem('dsh.experimental-inspector.client-source-id.v0', first.sourceId)
- refreshed = await ClientRealmSource.claim('refreshed')
- expect(refreshed.sourceId).toBe(first.sourceId)
- } finally {
- first?.close()
- duplicate?.close()
- refreshed?.close()
- if (descriptor === undefined) Reflect.deleteProperty(navigator, 'locks')
- else Object.defineProperty(navigator, 'locks', descriptor)
- }
- })
- it('falls back to a page-lifetime source id when session storage is unavailable', async () => {
- globalThis.WebSocket = FakeWebSocket as unknown as typeof WebSocket
- globalThis.__DSH_INSPECTOR__ = bootstrap
- vi.spyOn(Storage.prototype, 'getItem').mockImplementation(() => {
- throw new DOMException('storage disabled', 'SecurityError')
- })
- const ctx = new Context()
- const fiber = ctx.plugin({ apply })
- await fiber.await()
- const socket = FakeWebSocket.sockets[0]!
- socket.open()
- const open = JSON.parse(socket.sent[0]!) as { source: { sourceId: string } }
- expect(open.source.sourceId).toMatch(/^client-/u)
- await fiber.dispose()
- })
- it('cancels an outstanding Client Runtime operation without sending a late response', async () => {
- globalThis.WebSocket = FakeWebSocket as unknown as typeof WebSocket
- globalThis.__DSH_INSPECTOR__ = bootstrap
- const ctx = new Context()
- const fiber = ctx.plugin({ apply })
- await fiber.await()
- const socket = FakeWebSocket.sockets[0]!
- socket.open()
- const open = JSON.parse(socket.sent[0]!) as {
- source: { sourceId: string; generation: string }
- }
- socket.receive({
- v: 0,
- t: 'source/accepted',
- sourceId: open.source.sourceId,
- generation: open.source.generation,
- })
- socket.receive({
- v: 0,
- t: 'client-runtime/request',
- sourceId: open.source.sourceId,
- generation: open.source.generation,
- sessionId: 'devtools-cancel',
- requestId: 'runtime-cancel',
- command: { op: 'evaluate', expression: 'new Promise(() => {})', awaitPromise: true },
- })
- socket.receive({
- v: 0,
- t: 'client-runtime/cancel',
- sourceId: open.source.sourceId,
- generation: open.source.generation,
- sessionId: 'devtools-cancel',
- requestId: 'runtime-cancel',
- })
- await new Promise(resolve => setTimeout(resolve, 0))
- expect(socket.sent.map(value => JSON.parse(value) as { requestId?: string })
- .some(frame => frame.requestId === 'runtime-cancel')).toBe(false)
- socket.receive({
- v: 0,
- t: 'client-runtime/request',
- sourceId: open.source.sourceId,
- generation: open.source.generation,
- sessionId: 'devtools-cancel',
- requestId: 'runtime-after-cancel',
- command: { op: 'evaluate', expression: '42', returnByValue: true },
- })
- await vi.waitFor(() => {
- expect(socket.sent.map(value => JSON.parse(value) as { requestId?: string })
- .some(frame => frame.requestId === 'runtime-after-cancel')).toBe(true)
- })
- await fiber.dispose()
- })
- it('does not report queue loss again after a replacement absorbs it', async () => {
- globalThis.WebSocket = FakeWebSocket as unknown as typeof WebSocket
- globalThis.__DSH_INSPECTOR__ = { ...bootstrap, maxQueuedRecords: 1 }
- const ctx = new Context()
- const fiber = ctx.plugin({ apply })
- await fiber.await()
- const socket = FakeWebSocket.sockets[0]!
- ctx.inspector.publish('client/first', { ordinal: 1 })
- ctx.inspector.publish('client/second', { ordinal: 2 })
- socket.open()
- const open = JSON.parse(socket.sent[0]!) as {
- source: { sourceId: string; generation: string }
- }
- socket.receive({
- v: 0,
- t: 'source/accepted',
- sourceId: open.source.sourceId,
- generation: open.source.generation,
- })
- const replacement = JSON.parse(socket.sent[1]!) as { nextSequence: number }
- const append = JSON.parse(socket.sent[2]!) as {
- firstSequence: number
- droppedBefore: number
- records: Array<{ topic: string }>
- }
- expect(append).toMatchObject({
- firstSequence: replacement.nextSequence,
- droppedBefore: 0,
- records: [{ topic: 'client/second' }],
- })
- await fiber.dispose()
- })
- it('discovers and serves its built Client bundle through the source protocol', async () => {
- globalThis.WebSocket = FakeWebSocket as unknown as typeof WebSocket
- globalThis.__DSH_INSPECTOR__ = bootstrap
- Reflect.set(globalThis, '__DSH_BOOT__', {
- rev: 'graph',
- entries: [{
- id: '@deepseek-ai/dsh-experimental-inspector',
- url: '/plugins/@deepseek-ai/dsh-experimental-inspector/client.js?rev=bundle-rev',
- rev: 'bundle-rev',
- }],
- })
- const source = 'const clientBundleMarker = "你好"\n'
- const sourceMap = '{"version":3,"sources":["client/index.ts"]}'
- globalThis.fetch = vi.fn(async (input: string | URL | Request) => {
- const url = typeof input === 'string' ? input : input instanceof URL ? input.href : input.url
- return new Response(url.includes('.js.map') ? sourceMap : source)
- })
- const ctx = new Context()
- const fiber = ctx.plugin({ apply })
- await fiber.await()
- const socket = FakeWebSocket.sockets[0]!
- socket.open()
- const open = JSON.parse(socket.sent[0]!) as {
- source: { sourceId: string; generation: string; capabilities: Array<{ type: string }> }
- }
- expect(open.source.capabilities).toEqual(expect.arrayContaining([{ type: 'client-sources' }]))
- socket.receive({
- v: 0,
- t: 'source/accepted',
- sourceId: open.source.sourceId,
- generation: open.source.generation,
- })
- socket.receive({
- v: 0,
- t: 'client-sources/request',
- sourceId: open.source.sourceId,
- generation: open.source.generation,
- sessionId: 'source-session-1',
- requestId: 'source-request-1',
- command: { op: 'list-scripts' },
- })
- let scriptKey: string | undefined
- await vi.waitFor(() => {
- const response = socket.sent.map(value => JSON.parse(value) as {
- requestId?: string
- outcome?: { result?: { scripts?: Array<{ scriptKey: string; url: string; sourceMapUrl: string }> } }
- }).find(frame => frame.requestId === 'source-request-1')
- const script = response?.outcome?.result?.scripts?.[0]
- expect(script?.url).toContain('/plugins/@deepseek-ai/dsh-experimental-inspector/client.js?rev=bundle-rev')
- expect(script?.sourceMapUrl)
- .toContain('/plugins/@deepseek-ai/dsh-experimental-inspector/client.js.map?rev=bundle-rev')
- scriptKey = script?.scriptKey
- })
- socket.receive({
- v: 0,
- t: 'client-sources/request',
- sourceId: open.source.sourceId,
- generation: open.source.generation,
- sessionId: 'source-session-1',
- requestId: 'source-request-2',
- command: { op: 'get-content-chunk', scriptKey, content: 'source', offset: 0, maxBytes: 1_024 },
- })
- await vi.waitFor(() => {
- const response = socket.sent.map(value => JSON.parse(value) as {
- requestId?: string
- outcome?: { result?: { data?: string; eof?: boolean } }
- }).find(frame => frame.requestId === 'source-request-2')
- expect(response?.outcome?.result?.eof).toBe(true)
- const bytes = Uint8Array.from(atob(response?.outcome?.result?.data ?? ''), character => character.charCodeAt(0))
- expect(new TextDecoder().decode(bytes)).toBe(source)
- })
- await fiber.dispose()
- })
- it('fails loud when the Host did not inject a bootstrap', async () => {
- globalThis.WebSocket = FakeWebSocket as unknown as typeof WebSocket
- const ctx = new Context()
- const fiber = ctx.plugin({ apply })
- await expect(fiber).rejects.toThrow('Host bootstrap is missing')
- await fiber.dispose()
- })
- it('closes the Client source when a later plugin registration fails', async () => {
- globalThis.WebSocket = FakeWebSocket as unknown as typeof WebSocket
- globalThis.__DSH_INSPECTOR__ = bootstrap
- const ctx = new Context()
- ctx.provide('inspector', {
- publish: () => undefined,
- cordis: { getTree: () => Promise.reject(new Error('unused test service')) },
- })
- const fiber = ctx.plugin({ apply })
- await expect(fiber.await()).rejects.toThrow('service "inspector" has been registered')
- expect(FakeWebSocket.sockets).toHaveLength(1)
- expect(FakeWebSocket.sockets[0]?.readyState).toBe(FakeWebSocket.CLOSED)
- await fiber.dispose()
- })
- })
|