import { Context } from 'cordis' import { describe, expect, it, vi } from 'vitest' import { z } from 'zod' import type { ConnectionHandle } from '@deepseek-ai/dsh-client-connection/client' import type { InvocationDescriptor, TypeRTContext, TypeRTRemoteContextApi, TypeRTRemoteNamespace, } from '@deepseek-ai/dsh-type-meta' import TypertRegistry from '@deepseek-ai/dsh-typert-registry' import { apply, inject } from '../src/client/index.ts' declare module '@deepseek-ai/dsh-type-meta' { interface TypeRTContextMap { fixture: TypeRTContext } interface TypeRTRemoteMap { 'goals/create': ( agentId: string, request: { readonly objective: string }, signal?: AbortSignal, ) => Promise<{ readonly ref: string }> } interface TypeRTRemoteContextMap { 'fixture:goals/create': ( request: { readonly objective: string }, signal?: AbortSignal, ) => Promise<{ readonly ref: string }> 'fixture:goals/rename': (request: { readonly objective: string }) => Promise<{ readonly renamed: boolean }> } interface TypeRTRemoteNamespaceMap { goals: TypeRTRemoteNamespace<'goals'> } } type FixtureContext = Context & TypeRTRemoteContextApi<'fixture'> const idSchema = z.string().min(1) const requestSchema = z.object({ objective: z.string().min(1) }) const createResultSchema = z.object({ ref: z.string().min(1) }) const renameResultSchema = z.object({ renamed: z.boolean() }) function directDescriptor(): InvocationDescriptor { return { id: '@fixture/goals#goals/create', service: 'goals', namespace: 'goals', method: 'create', invocation: { kind: 'direct' }, scope: { context: 'fixture', wire: 'agentId' }, parameters: [{ name: 'agent', wire: 'agentId', source: 'lookup', lookup: 'fixture', codec: { mode: 'strict', typeSymbol: '@fixture#AgentId', schema: idSchema }, }, { name: 'request', wire: 'request', source: 'json', codec: { mode: 'strict', typeSymbol: '@fixture#CreateRequest', schema: requestSchema }, }], cancellation: { parameter: 'signal' }, result: { mode: 'strict', typeSymbol: '@fixture#CreateResult', schema: createResultSchema }, } } function contextDescriptor(): InvocationDescriptor { return { id: '@fixture/goals#goals/rename', service: 'goals', namespace: 'goals', method: 'rename', invocation: { kind: 'context', context: 'fixture', wire: 'agentId', codec: { mode: 'strict', typeSymbol: '@fixture#AgentId', schema: idSchema }, }, parameters: [{ name: 'request', wire: 'request', source: 'json', codec: { mode: 'strict', typeSymbol: '@fixture#RenameRequest', schema: requestSchema }, }], result: { mode: 'strict', typeSymbol: '@fixture#RenameResult', schema: renameResultSchema }, } } async function bench(call: ConnectionHandle['rpc']['call']): Promise { const ctx = new Context() await ctx.plugin(TypertRegistry) ctx.provide('connection', { rpc: { call } } as unknown as ConnectionHandle) await ctx.plugin({ inject, apply }) return ctx } describe('Client TypeRT API', () => { it('mounts concrete direct methods, validates both boundaries, and withdraws retained handles', async () => { const call = vi.fn() .mockResolvedValue({ ok: true, value: { ref: 'goal-1' } }) const ctx = await bench(call) let retained: typeof ctx.api.goals.create | undefined const assembly = ctx.plugin(Object.assign( (scope: Context) => { scope.api.mount({ package: '@fixture/goals', descriptors: [directDescriptor()] }) retained = scope.api.goals.create }, { inject: ['api'] }, )) await assembly await expect(ctx.api.goals.create('agent-1', { objective: 'ship' })).resolves.toEqual({ ref: 'goal-1' }) expect(call).toHaveBeenCalledWith( '/api', 'goals/create', { args: { agentId: 'agent-1', request: { objective: 'ship' } } }, expect.any(AbortSignal), ) const callerAbort = new AbortController() await expect(ctx.api.goals.create( 'agent-1', { objective: 'cancel me' }, callerAbort.signal, )).resolves.toEqual({ ref: 'goal-1' }) const combinedSignal = call.mock.calls.at(-1)?.[3] expect(combinedSignal).toBeInstanceOf(AbortSignal) expect(combinedSignal).not.toBe(callerAbort.signal) const cancellation = new Error('caller cancelled') callerAbort.abort(cancellation) expect(combinedSignal?.aborted).toBe(true) expect(combinedSignal?.reason).toBe(cancellation) await expect(ctx.api.goals.create('', { objective: 'ship' })).rejects.toThrow('rejected "agentId"') call.mockResolvedValueOnce({ ok: true, value: { ref: 1 } }) await expect(ctx.api.goals.create('agent-1', { objective: 'ship' })).rejects.toThrow('rejected "result"') await assembly.dispose() expect((ctx.api as unknown as Record).goals).toBeUndefined() expect(ctx.get('goals')).toBeUndefined() expect(ctx.typert.remotes.list()).toEqual([]) await expect(retained?.('agent-1', { objective: 'ship' })).rejects.toThrow('no longer mounted') }) it('projects one direct lookup descriptor onto an Agent-scoped alias', async () => { const call = vi.fn() .mockResolvedValue({ ok: true, value: { ref: 'goal-2' } }) const ctx = await bench(call) const agentCtx = ctx.extend({ fixtureId: 'agent-2' }) as FixtureContext ctx.typert.contexts.registerClient('fixture', { identity: candidate => (candidate as Context & { fixtureId?: string }).fixtureId, }) const assembly = ctx.plugin(Object.assign( (scope: Context) => { scope.api.mount({ package: '@fixture/goals', descriptors: [directDescriptor()] }) }, { inject: ['api'] }, )) await assembly await expect(agentCtx.goals.create({ objective: 'ship scoped' })).resolves.toEqual({ ref: 'goal-2' }) expect(call).toHaveBeenCalledWith( '/api', 'goals/create', { args: { agentId: 'agent-2', request: { objective: 'ship scoped' } } }, expect.any(AbortSignal), ) await expect((ctx as FixtureContext).goals.create({ objective: 'wrong scope' })) .rejects.toThrow('requires a "fixture" Context') await assembly.dispose() expect((ctx.api as unknown as Record).goals).toBeUndefined() expect(ctx.get('goals')).toBeUndefined() }) it('uses the caller Context identity for scoped namespace methods', async () => { const call = vi.fn() .mockResolvedValue({ ok: true, value: { renamed: true } }) const ctx = await bench(call) const agentCtx = ctx.extend({ fixtureId: 'agent-2' }) as FixtureContext ctx.typert.contexts.registerClient('fixture', { identity: candidate => (candidate as Context & { fixtureId?: string }).fixtureId, }) const assembly = ctx.plugin(Object.assign( (scope: Context) => { scope.api.mount({ package: '@fixture/goals', descriptors: [contextDescriptor()] }) }, { inject: ['api'] }, )) await assembly await expect(agentCtx.goals.rename({ objective: 'land' })).resolves.toEqual({ renamed: true }) expect(call).toHaveBeenCalledWith( '/api', 'goals/rename', { args: { agentId: 'agent-2', request: { objective: 'land' } } }, expect.any(AbortSignal), ) await expect((ctx as FixtureContext).goals.rename({ objective: 'land' })) .rejects.toThrow('requires a "fixture" Context') await assembly.dispose() expect(ctx.get('goals')).toBeUndefined() }) it('rejects weak descriptors and namespace collisions before registration', async () => { const ctx = await bench(vi.fn()) const weak: InvocationDescriptor = { ...directDescriptor(), result: { mode: 'src-json' }, } expect(() => ctx.api.mount({ package: '@fixture/weak', descriptors: [weak] })) .toThrow('has no strict codec') expect(() => ctx.api.mount({ package: '@fixture/conflict', descriptors: [{ ...directDescriptor(), namespace: 'mount' }], })).toThrow('conflicts with the API service') expect(ctx.typert.remotes.list()).toEqual([]) }) it('rejects duplicate, live, scoped-service, and Context namespace collisions', async () => { const call = vi.fn() .mockResolvedValue({ ok: true, value: { renamed: true } }) const ctx = await bench(call) const agentCtx = ctx.extend({ fixtureId: 'agent-remounted' }) as FixtureContext ctx.typert.contexts.registerClient('fixture', { identity: candidate => (candidate as Context & { fixtureId?: string }).fixtureId, }) const direct = directDescriptor() const context = contextDescriptor() expect(() => ctx.api.mount({ package: '@fixture/direct-duplicates', descriptors: [direct, { ...direct, id: '@fixture/goals#goals/create-again' }], })).toThrow('repeats direct method') expect(() => ctx.api.mount({ package: '@fixture/scoped-duplicates', descriptors: [context, { ...context, id: '@fixture/goals#goals/rename-again' }], })).toThrow('repeats scoped method') const disposeDirect = ctx.api.mount({ package: '@fixture/direct-live', descriptors: [direct] }) expect(() => ctx.api.mount({ package: '@fixture/direct-conflict', descriptors: [{ ...direct, id: '@fixture/other#goals/create' }], })).toThrow('direct method goals/create is already mounted') await disposeDirect() const disposeScoped = ctx.api.mount({ package: '@fixture/scoped-live', descriptors: [context] }) expect(() => ctx.api.mount({ package: '@fixture/scoped-conflict', descriptors: [{ ...context, id: '@fixture/other#goals/rename' }], })).toThrow('scoped method goals/rename is already mounted') expect(() => ctx.api.mount({ package: '@fixture/service-method-conflict', descriptors: [{ ...context, id: '@fixture/goals#goals/remove', method: 'remove' }], })).toThrow('conflicts with its namespace service') await disposeScoped() expect(() => ctx.api.mount({ package: '@fixture/context-property-conflict', descriptors: [{ ...context, namespace: 'typert' }], })).toThrow('conflicts with an existing Context property') const disposeMultipleScoped = ctx.api.mount({ package: '@fixture/multiple-scoped', descriptors: [directDescriptor(), contextDescriptor()], }) await expect(agentCtx.goals.rename({ objective: 'remounted' })).resolves.toEqual({ renamed: true }) expect(call).toHaveBeenLastCalledWith( '/api', 'goals/rename', { args: { agentId: 'agent-remounted', request: { objective: 'remounted' } } }, expect.any(AbortSignal), ) await disposeMultipleScoped() }) it('rejects weak parameter and Context codecs plus malformed scope projections', async () => { const ctx = await bench(vi.fn()) const direct = directDescriptor() const context = contextDescriptor() expect(() => ctx.api.mount({ package: '@fixture/weak-parameter', descriptors: [{ ...direct, parameters: direct.parameters.map((parameter, index) => index === 0 ? { ...parameter, codec: { mode: 'src-json' } } : parameter), }], })).toThrow('has no strict codec') expect(() => ctx.api.mount({ package: '@fixture/weak-context', descriptors: [{ ...context, invocation: { ...context.invocation, codec: { mode: 'src-json' } }, } as InvocationDescriptor], })).toThrow('has no strict codec') expect(() => ctx.api.mount({ package: '@fixture/malformed-scope', descriptors: [{ ...direct, scope: { context: 'fixture', wire: 'missingId' } }], })).toThrow('scope must select its only lookup parameter') expect(() => ctx.api.mount({ package: '@fixture/ambiguous-scope', descriptors: [{ ...direct, parameters: [...direct.parameters, { name: 'other', wire: 'otherId', source: 'lookup', lookup: 'fixture', codec: { mode: 'strict', typeSymbol: '@fixture#AgentId', schema: idSchema }, }], }], })).toThrow('scope must select its only lookup parameter') }) it('validates invocation arity, required binders, live Connection, and mutable descriptor codecs', async () => { const call = vi.fn() .mockResolvedValue({ ok: true, value: { ref: 'goal-1' } }) const ctx = await bench(call) const descriptor = directDescriptor() const dispose = ctx.api.mount({ package: '@fixture/goals', descriptors: [descriptor, contextDescriptor()], }) const create = ctx.api.goals.create as unknown as (...args: unknown[]) => Promise const goals = (ctx as FixtureContext).goals const rename = goals.rename as unknown as (...args: unknown[]) => Promise await expect(create('agent-1')).rejects.toThrow('expected 2 business argument(s) plus an optional AbortSignal, got 1') await expect(create('agent-1', { objective: 'ship' }, undefined, 'extra')) .rejects.toThrow('got 4') await expect(rename.call(goals)).rejects.toThrow('expected 1 argument(s), got 0') await expect((ctx as FixtureContext).goals.create({ objective: 'ship' })) .rejects.toThrow('no Client Context binder') ;(descriptor.parameters[0] as { codec: { mode: string } }).codec.mode = 'src-json' await expect(ctx.api.goals.create('agent-1', { objective: 'ship' })).rejects.toThrow('has no strict codec') ;(descriptor.parameters[0] as { codec: { mode: string } }).codec.mode = 'strict' ctx.set('connection', undefined) await expect(ctx.api.goals.create('agent-1', { objective: 'ship' })).rejects.toThrow('no active Connection') await dispose() }) it('withdraws a pending invocation and preserves a direct namespace until its last method leaves', async () => { let resolveCall!: (result: Awaited>) => void const pending = new Promise>>((resolve) => { resolveCall = resolve }) const call = vi.fn().mockReturnValue(pending) const ctx = await bench(call) const { scope: _scope, ...first } = directDescriptor() const second: InvocationDescriptor = { ...first, id: '@fixture/goals#goals/archive', method: 'archive', } const dispose = ctx.api.mount({ package: '@fixture/goals', descriptors: [first, second] }) const invocation = ctx.api.goals.create('agent-1', { objective: 'ship' }) await vi.waitFor(() => { expect(call).toHaveBeenCalledTimes(1) }) await dispose() resolveCall({ ok: true, value: { ref: 'goal-1' } }) await expect(invocation).rejects.toThrow('withdrawn during invocation') expect((ctx.api as unknown as Record).goals).toBeUndefined() }) it('rolls back Remote registration when concrete method installation fails', async () => { const ctx = await bench(vi.fn()) const defineProperty = Object.defineProperty const spy = vi.spyOn(Object, 'defineProperty').mockImplementation((target, key, attributes) => { if (key === 'goals') throw new Error('fixture installation failure') return defineProperty(target, key, attributes) }) try { expect(() => ctx.api.mount({ package: '@fixture/goals', descriptors: [directDescriptor()] })) .toThrow('fixture installation failure') await vi.waitFor(() => { expect(ctx.typert.remotes.list()).toEqual([]) }) } finally { spy.mockRestore() } }) it('throws RPC failures with the structured error as its cause', async () => { const rpcError = { code: 'internal' as const, message: 'host failed', details: {} } const ctx = await bench(vi.fn().mockResolvedValue({ ok: false, error: rpcError })) ctx.api.mount({ package: '@fixture/goals', descriptors: [directDescriptor()] }) let failure: unknown try { await ctx.api.goals.create('agent-1', { objective: 'ship' }) } catch (error) { failure = error } expect(failure).toBeInstanceOf(Error) if (!(failure instanceof Error)) throw new Error('expected Client API invocation to fail') expect(failure.message).toContain('internal: host failed') expect(failure.cause).toBe(rpcError) }) })