mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
`CommandService.list` and `execute` carry the wire contract directly through `@Remote`, and the Client assembly mounts the generated commands contribution. The legacy API Proxy route, its schemas, the map rows, the generated client methods and the fixture's command domain are removed, so the catalog and the admission call have one owner again. `Session.command()` keeps a result-shaped public face for parity with the prompt, cancel and attachment neighbours it sits beside, and reads the generated namespace through one `SessionRemotes` parameter. The Session cluster declares that face against the owning business package rather than the generated contribution: the Host compiler aggregate builds this package, and it runs before any contribution is emitted. Migrated calls lose the `title-invalid` class of protocol-only error codes and report `internal`; no production caller branched on them.
156 lines
6.3 KiB
TypeScript
156 lines
6.3 KiB
TypeScript
/**
|
|
* Runtime plugin browser-half apply: slots + object services mounting over the
|
|
* connection handle, stream-loop sink wiring into the object layer, and the
|
|
* fiber-scoped loop teardown.
|
|
*/
|
|
import { Context } from '@deepseek-ai/cordis'
|
|
import { describe, expect, it, vi } from 'vitest'
|
|
import type { ConnectionHandle } from '@deepseek-ai/dsh-api-remotes/client'
|
|
import type { ConnectionSinks } from '@deepseek-ai/dsh-api-remotes/client'
|
|
import { SESSION_SEARCH_RESULT_LIMIT } from '@deepseek-ai/dsh-host-apiproxy/api'
|
|
import TypertRegistry from '@deepseek-ai/dsh-typert-registry'
|
|
import * as RuntimeClient from '../src/client/index.ts'
|
|
import type { ConversationNodeDefinition } from '../src/client/contract/conversation.ts'
|
|
import { Session } from '../src/client/sessions/session.ts'
|
|
import type { SessionsService } from '../src/client/sessions/service.ts'
|
|
import type { WorkspacesService } from '../src/client/workspaces/service.ts'
|
|
import { FakeApiClient, ok } from './fake-api.ts'
|
|
|
|
interface Bench {
|
|
ctx: Context
|
|
api: FakeApiClient
|
|
sinks: ConnectionSinks | undefined
|
|
stopped: number
|
|
}
|
|
|
|
async function mount(): Promise<Bench> {
|
|
const ctx = new Context()
|
|
await ctx.plugin(TypertRegistry)
|
|
const api = new FakeApiClient()
|
|
const bench: Bench = { ctx, api, sinks: undefined, stopped: 0 }
|
|
const handle: ConnectionHandle = {
|
|
api,
|
|
isLoopback: true,
|
|
hostDescription: {
|
|
getSnapshot: () => undefined,
|
|
subscribe: () => () => {},
|
|
},
|
|
rpc: {
|
|
call: () => Promise.reject(new Error('unexpected generic RPC call')),
|
|
},
|
|
start: (sinks) => {
|
|
bench.sinks = sinks
|
|
return { stop: () => { bench.stopped += 1 } }
|
|
},
|
|
}
|
|
ctx.reflect.provide('connection', handle)
|
|
ctx.reflect.provide('remote', {})
|
|
await ctx.plugin(RuntimeClient).await()
|
|
return bench
|
|
}
|
|
|
|
async function flushMicrotasks(): Promise<void> {
|
|
for (let i = 0; i < 12; i++) await Promise.resolve()
|
|
}
|
|
|
|
describe('runtime client apply', () => {
|
|
it('mounts slots, Sessions, and Workspaces and fans host frames into both managers', async () => {
|
|
const bench = await mount()
|
|
expect(bench.ctx.get('slots') !== undefined).toBe(true)
|
|
// The built-in 'root' declaration ships with this package's SlotsService
|
|
// (the SlotMap 'root' merge lives here).
|
|
expect(bench.ctx.slots.spec('root')).toEqual({ kind: 'single', scope: 'root' })
|
|
const sessions = bench.ctx.get('sessions')
|
|
const workspaces = bench.ctx.get('workspaces')
|
|
expect(sessions !== undefined).toBe(true)
|
|
expect(workspaces !== undefined).toBe(true)
|
|
// The bound the wire schema enforces, not a per-connection negotiation.
|
|
expect((sessions as SessionsService).searchResultLimit).toBe(SESSION_SEARCH_RESULT_LIMIT)
|
|
if (workspaces === undefined) throw new Error('WorkspacesService missing after runtime apply')
|
|
expect(bench.sinks).toBeDefined()
|
|
|
|
// Frame sinks reach the object layer: a host session-added lands in the list store.
|
|
bench.sinks?.onHostEnvelope?.({
|
|
rpcId: 'r1' as never,
|
|
payload: { type: 'host/session-added', blank: true, sessionId: 's-new' } as never,
|
|
})
|
|
await Promise.resolve()
|
|
expect((sessions as { list: { getSnapshot(): { ids: string[] } } }).list.getSnapshot().ids).toContain('s-new')
|
|
bench.sinks?.onHostEnvelope?.({
|
|
rpcId: 'r-workspace' as never,
|
|
payload: {
|
|
type: 'host/workspace-changed',
|
|
workspace: {
|
|
workspaceId: 'w-new', path: '/w/new', title: 'new', sessionIds: [],
|
|
createdAt: '2026-01-01T00:00:00.000Z', updatedAt: '2026-01-01T00:00:00.000Z',
|
|
},
|
|
} as never,
|
|
})
|
|
await Promise.resolve()
|
|
expect(workspaces.list.getSnapshot().items[0]?.workspaceId).toBe('w-new')
|
|
// Mux sink and onConnected route without throwing (manager semantics own the behavior).
|
|
bench.sinks?.onMuxEnvelope?.({ rpcId: 'r2' as never, payload: { type: 'stream/error', message: 'x' } as never })
|
|
bench.sinks?.onConnected?.({ version: '0', cwd: '/f', attachedSessions: 0, canOpenPath: true })
|
|
})
|
|
|
|
it('selects the recent Workspace once when the first baselines have no current session', async () => {
|
|
const bench = await mount()
|
|
bench.api.onWorkspaceList = () => Promise.resolve(ok({
|
|
items: [{
|
|
workspaceId: 'w-recent', path: '/w/recent', title: 'recent', sessionIds: [],
|
|
createdAt: '2026-01-01T00:00:00.000Z', updatedAt: '2026-01-01T00:00:00.000Z',
|
|
}] as never[],
|
|
}))
|
|
bench.api.onList = () => Promise.resolve(ok({ items: [] }))
|
|
|
|
bench.sinks?.onConnected?.({ version: '0', cwd: '/f', attachedSessions: 0, canOpenPath: true })
|
|
await flushMicrotasks()
|
|
|
|
const sessions = bench.ctx.get('sessions') as SessionsService
|
|
const workspaces = bench.ctx.get('workspaces') as WorkspacesService
|
|
expect(bench.api.callsOf('session.create')).toEqual([{ workspaceId: 'w-recent' }])
|
|
expect(sessions.list.getSnapshot().current).toBe('fk-new')
|
|
|
|
sessions.clear()
|
|
await workspaces.refresh()
|
|
await flushMicrotasks()
|
|
expect(sessions.list.getSnapshot().current).toBeUndefined()
|
|
expect(bench.api.callsOf('session.create')).toHaveLength(1)
|
|
})
|
|
|
|
it('wires registry changes into resident Sessions during the runtime apply pass', async () => {
|
|
const bench = await mount()
|
|
const sessions = bench.ctx.get('sessions') as SessionsService
|
|
bench.sinks?.onHostEnvelope?.({
|
|
rpcId: 'r-registry' as never,
|
|
payload: { type: 'host/session-added', blank: true, sessionId: 's-registry' } as never,
|
|
})
|
|
await flushMicrotasks()
|
|
expect(sessions.binding('s-registry' as never)).toBeDefined()
|
|
const rebuild = vi.spyOn(Session.prototype, 'rebuildConversationRegistry')
|
|
const definition: ConversationNodeDefinition<null> = {
|
|
kind: 'registry-probe',
|
|
target: 'chat',
|
|
match: () => null,
|
|
start: () => null,
|
|
update: context => context.state,
|
|
buildViewNode: () => null,
|
|
}
|
|
|
|
bench.ctx.conversationEvents.register(definition)
|
|
await flushMicrotasks()
|
|
|
|
expect(rebuild).toHaveBeenCalledOnce()
|
|
rebuild.mockRestore()
|
|
})
|
|
|
|
it('stops the stream loop when the plugin fiber unloads', async () => {
|
|
const bench = await mount()
|
|
const fiber = [...bench.ctx.registry.values()].find(f => f.name?.includes('client'))
|
|
// Dispose the whole tree: the ctx.effect teardown must call loop.stop exactly once.
|
|
await bench.ctx.fiber.dispose()
|
|
expect(bench.stopped).toBe(1)
|
|
void fiber
|
|
})
|
|
})
|