test: finish the Remote-result and picker-split test migrations
Every generated Remote method resolves to `RemoteResult<T>`, so the Gateway client spec asserts the ok and error branches instead of the unwrapped value and a throw, and the generator fixtures declare the wrapper in the consumer face they typecheck. The RPC-failure test splits into the Host error carried verbatim in the error branch plus a transport throw folded into it. The runtime client, ui-command and ui-plan benches answer the generated commands Remote through its result branches and provide the `remote.commands` namespace their plugins now inject; the ui-command bench also serves the `$on` the service subscribes on construction. The directory-picker chooser mounts a backend and its surface as a pair, so the real-Loader composition serves both surface packages and asserts each entry arrives and leaves with its backend.
This commit is contained in:
@@ -5,6 +5,7 @@ import { z } from 'zod'
|
|||||||
import type { ConnectionHandle } from '@deepseek-ai/dsh-client-connection/client'
|
import type { ConnectionHandle } from '@deepseek-ai/dsh-client-connection/client'
|
||||||
import type {
|
import type {
|
||||||
InvocationDescriptor,
|
InvocationDescriptor,
|
||||||
|
RemoteResult,
|
||||||
TypeRTClientRemote,
|
TypeRTClientRemote,
|
||||||
TypeRTContext,
|
TypeRTContext,
|
||||||
TypeRTRemoteScopeApi,
|
TypeRTRemoteScopeApi,
|
||||||
@@ -46,16 +47,18 @@ declare module '@deepseek-ai/dsh-type-meta' {
|
|||||||
agentId: string,
|
agentId: string,
|
||||||
request: { readonly objective: string },
|
request: { readonly objective: string },
|
||||||
signal?: AbortSignal,
|
signal?: AbortSignal,
|
||||||
) => Promise<{ readonly ref: string }>
|
) => Promise<RemoteResult<{ readonly ref: string }>>
|
||||||
'probe/maybe': (value: string | null | undefined) => Promise<string | null | undefined>
|
'probe/maybe': (value: string | null | undefined) => Promise<RemoteResult<string | null | undefined>>
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TypeRTRemoteScopeMap {
|
interface TypeRTRemoteScopeMap {
|
||||||
'fixture:probe/create': (
|
'fixture:probe/create': (
|
||||||
request: { readonly objective: string },
|
request: { readonly objective: string },
|
||||||
signal?: AbortSignal,
|
signal?: AbortSignal,
|
||||||
) => Promise<{ readonly ref: string }>
|
) => Promise<RemoteResult<{ readonly ref: string }>>
|
||||||
'fixture:probe/rename': (request: { readonly objective: string }) => Promise<{ readonly renamed: boolean }>
|
'fixture:probe/rename': (
|
||||||
|
request: { readonly objective: string },
|
||||||
|
) => Promise<RemoteResult<{ readonly renamed: boolean }>>
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TypeRTRemoteNamespaceMap {
|
interface TypeRTRemoteNamespaceMap {
|
||||||
@@ -182,7 +185,8 @@ describe('Client TypeRT API', () => {
|
|||||||
await assembly
|
await assembly
|
||||||
const retained = ctx.remote.probe.create
|
const retained = ctx.remote.probe.create
|
||||||
|
|
||||||
await expect(ctx.remote.probe.create('agent-1', { objective: 'ship' })).resolves.toEqual({ ref: 'goal-1' })
|
await expect(ctx.remote.probe.create('agent-1', { objective: 'ship' }))
|
||||||
|
.resolves.toEqual({ ok: true, value: { ref: 'goal-1' } })
|
||||||
expect(call).toHaveBeenCalledWith(
|
expect(call).toHaveBeenCalledWith(
|
||||||
'/api',
|
'/api',
|
||||||
'probe/create',
|
'probe/create',
|
||||||
@@ -194,7 +198,7 @@ describe('Client TypeRT API', () => {
|
|||||||
'agent-1',
|
'agent-1',
|
||||||
{ objective: 'cancel me' },
|
{ objective: 'cancel me' },
|
||||||
callerAbort.signal,
|
callerAbort.signal,
|
||||||
)).resolves.toEqual({ ref: 'goal-1' })
|
)).resolves.toEqual({ ok: true, value: { ref: 'goal-1' } })
|
||||||
const combinedSignal = call.mock.calls.at(-1)?.[3]
|
const combinedSignal = call.mock.calls.at(-1)?.[3]
|
||||||
expect(combinedSignal).toBeInstanceOf(AbortSignal)
|
expect(combinedSignal).toBeInstanceOf(AbortSignal)
|
||||||
expect(combinedSignal).not.toBe(callerAbort.signal)
|
expect(combinedSignal).not.toBe(callerAbort.signal)
|
||||||
@@ -205,14 +209,20 @@ describe('Client TypeRT API', () => {
|
|||||||
await expect(ctx.remote.probe.create('', { objective: 'ship' })).rejects.toThrow('rejected "agentId"')
|
await expect(ctx.remote.probe.create('', { objective: 'ship' })).rejects.toThrow('rejected "agentId"')
|
||||||
|
|
||||||
call.mockResolvedValueOnce({ ok: true, value: { ref: 1 } })
|
call.mockResolvedValueOnce({ ok: true, value: { ref: 1 } })
|
||||||
await expect(ctx.remote.probe.create('agent-1', { objective: 'ship' })).rejects.toThrow('rejected "result"')
|
await expect(ctx.remote.probe.create('agent-1', { objective: 'ship' })).resolves.toMatchObject({
|
||||||
|
ok: false,
|
||||||
|
error: { code: 'internal', message: expect.stringContaining('rejected "result"') },
|
||||||
|
})
|
||||||
|
|
||||||
await assembly.dispose()
|
await assembly.dispose()
|
||||||
expect((ctx.remote as unknown as Record<string, unknown>).probe).toBeUndefined()
|
expect((ctx.remote as unknown as Record<string, unknown>).probe).toBeUndefined()
|
||||||
expect(ctx.get('remote.probe')).toBeUndefined()
|
expect(ctx.get('remote.probe')).toBeUndefined()
|
||||||
expect(ctx.get('probe')).toBe(businessProbe)
|
expect(ctx.get('probe')).toBe(businessProbe)
|
||||||
expect(ctx.typert.remotes.list()).toEqual([])
|
expect(ctx.typert.remotes.list()).toEqual([])
|
||||||
await expect(retained?.('agent-1', { objective: 'ship' })).rejects.toThrow('no longer mounted')
|
await expect(retained?.('agent-1', { objective: 'ship' })).resolves.toMatchObject({
|
||||||
|
ok: false,
|
||||||
|
error: { code: 'internal', message: expect.stringContaining('no longer mounted') },
|
||||||
|
})
|
||||||
disposeBusinessProbe()
|
disposeBusinessProbe()
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -226,7 +236,7 @@ describe('Client TypeRT API', () => {
|
|||||||
descriptors: [maybeDescriptor()],
|
descriptors: [maybeDescriptor()],
|
||||||
})
|
})
|
||||||
|
|
||||||
await expect(ctx.remote.probe.maybe(undefined)).resolves.toBeUndefined()
|
await expect(ctx.remote.probe.maybe(undefined)).resolves.toStrictEqual({ ok: true, value: undefined })
|
||||||
expect(call).toHaveBeenNthCalledWith(
|
expect(call).toHaveBeenNthCalledWith(
|
||||||
1,
|
1,
|
||||||
'/api',
|
'/api',
|
||||||
@@ -234,7 +244,7 @@ describe('Client TypeRT API', () => {
|
|||||||
{ args: {} },
|
{ args: {} },
|
||||||
expect.any(AbortSignal),
|
expect.any(AbortSignal),
|
||||||
)
|
)
|
||||||
await expect(ctx.remote.probe.maybe(null)).resolves.toBeNull()
|
await expect(ctx.remote.probe.maybe(null)).resolves.toStrictEqual({ ok: true, value: null })
|
||||||
expect(call).toHaveBeenNthCalledWith(
|
expect(call).toHaveBeenNthCalledWith(
|
||||||
2,
|
2,
|
||||||
'/api',
|
'/api',
|
||||||
@@ -260,7 +270,8 @@ describe('Client TypeRT API', () => {
|
|||||||
))
|
))
|
||||||
await assembly
|
await assembly
|
||||||
|
|
||||||
await expect(agentCtx.remote.probe.create({ objective: 'ship scoped' })).resolves.toEqual({ ref: 'goal-2' })
|
await expect(agentCtx.remote.probe.create({ objective: 'ship scoped' }))
|
||||||
|
.resolves.toEqual({ ok: true, value: { ref: 'goal-2' } })
|
||||||
expect(call).toHaveBeenCalledWith(
|
expect(call).toHaveBeenCalledWith(
|
||||||
'/api',
|
'/api',
|
||||||
'probe/create',
|
'probe/create',
|
||||||
@@ -289,7 +300,8 @@ describe('Client TypeRT API', () => {
|
|||||||
))
|
))
|
||||||
await assembly
|
await assembly
|
||||||
|
|
||||||
await expect(agentCtx.remote.probe.rename({ objective: 'land' })).resolves.toEqual({ renamed: true })
|
await expect(agentCtx.remote.probe.rename({ objective: 'land' }))
|
||||||
|
.resolves.toEqual({ ok: true, value: { renamed: true } })
|
||||||
expect(call).toHaveBeenCalledWith(
|
expect(call).toHaveBeenCalledWith(
|
||||||
'/api',
|
'/api',
|
||||||
'probe/rename',
|
'probe/rename',
|
||||||
@@ -373,7 +385,8 @@ describe('Client TypeRT API', () => {
|
|||||||
package: '@fixture/multiple-scoped',
|
package: '@fixture/multiple-scoped',
|
||||||
descriptors: [directDescriptor(), contextDescriptor()],
|
descriptors: [directDescriptor(), contextDescriptor()],
|
||||||
})
|
})
|
||||||
await expect(agentCtx.remote.probe.rename({ objective: 'remounted' })).resolves.toEqual({ renamed: true })
|
await expect(agentCtx.remote.probe.rename({ objective: 'remounted' }))
|
||||||
|
.resolves.toEqual({ ok: true, value: { renamed: true } })
|
||||||
expect(call).toHaveBeenLastCalledWith(
|
expect(call).toHaveBeenLastCalledWith(
|
||||||
'/api',
|
'/api',
|
||||||
'probe/rename',
|
'probe/rename',
|
||||||
@@ -523,7 +536,10 @@ describe('Client TypeRT API', () => {
|
|||||||
await dispose()
|
await dispose()
|
||||||
resolveCall({ ok: true, value: { ref: 'goal-1' } })
|
resolveCall({ ok: true, value: { ref: 'goal-1' } })
|
||||||
|
|
||||||
await expect(invocation).rejects.toThrow('withdrawn during invocation')
|
await expect(invocation).resolves.toMatchObject({
|
||||||
|
ok: false,
|
||||||
|
error: { code: 'internal', message: expect.stringContaining('no longer mounted') },
|
||||||
|
})
|
||||||
expect((ctx.remote as unknown as Record<string, unknown>).probe).toBeUndefined()
|
expect((ctx.remote as unknown as Record<string, unknown>).probe).toBeUndefined()
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -560,7 +576,7 @@ describe('Client TypeRT API', () => {
|
|||||||
const dispose = await ctx.remote.$mount({ package: '@fixture/prototype', descriptors: [descriptor] })
|
const dispose = await ctx.remote.$mount({ package: '@fixture/prototype', descriptors: [descriptor] })
|
||||||
|
|
||||||
const method = (ctx.remote.probe as unknown as Record<string, (...args: unknown[]) => Promise<unknown>>).prototype
|
const method = (ctx.remote.probe as unknown as Record<string, (...args: unknown[]) => Promise<unknown>>).prototype
|
||||||
await expect(method?.('wire-value')).resolves.toEqual({ ref: 'goal-1' })
|
await expect(method?.('wire-value')).resolves.toEqual({ ok: true, value: { ref: 'goal-1' } })
|
||||||
const payload = call.mock.calls[0]?.[2] as { readonly args: Record<string, unknown> }
|
const payload = call.mock.calls[0]?.[2] as { readonly args: Record<string, unknown> }
|
||||||
expect(Object.getPrototypeOf(payload.args)).toBeNull()
|
expect(Object.getPrototypeOf(payload.args)).toBeNull()
|
||||||
expect(Object.hasOwn(payload.args, '__proto__')).toBe(true)
|
expect(Object.hasOwn(payload.args, '__proto__')).toBe(true)
|
||||||
@@ -649,21 +665,26 @@ describe('Client TypeRT API', () => {
|
|||||||
await disposeReplacement()
|
await disposeReplacement()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('throws RPC failures with the structured error as its cause', async () => {
|
it('delivers an RPC failure in the error branch with the Host error verbatim', async () => {
|
||||||
const rpcError = { code: 'internal' as const, message: 'host failed', details: {} }
|
const rpcError = { code: 'internal' as const, message: 'host failed', details: {} }
|
||||||
const ctx = await bench(vi.fn<ConnectionHandle['rpc']['call']>().mockResolvedValue({ ok: false, error: rpcError }))
|
const ctx = await bench(vi.fn<ConnectionHandle['rpc']['call']>().mockResolvedValue({ ok: false, error: rpcError }))
|
||||||
await ctx.remote.$mount({ package: '@fixture/probe', descriptors: [directDescriptor()] })
|
await ctx.remote.$mount({ package: '@fixture/probe', descriptors: [directDescriptor()] })
|
||||||
|
|
||||||
let failure: unknown
|
const outcome = await ctx.remote.probe.create('agent-1', { objective: 'ship' })
|
||||||
try {
|
expect(outcome.ok).toBe(false)
|
||||||
await ctx.remote.probe.create('agent-1', { objective: 'ship' })
|
if (outcome.ok) throw new Error('expected the Client API invocation to report a failure')
|
||||||
} catch (error) {
|
expect(outcome.error).toBe(rpcError)
|
||||||
failure = error
|
})
|
||||||
}
|
|
||||||
expect(failure).toBeInstanceOf(Error)
|
it('folds a transport throw into the error branch', async () => {
|
||||||
if (!(failure instanceof Error)) throw new Error('expected Client API invocation to fail')
|
const ctx = await bench(vi.fn<ConnectionHandle['rpc']['call']>()
|
||||||
expect(failure.message).toContain('internal: host failed')
|
.mockRejectedValue(new Error('carrier offline')))
|
||||||
expect(failure.cause).toBe(rpcError)
|
await ctx.remote.$mount({ package: '@fixture/probe', descriptors: [directDescriptor()] })
|
||||||
|
|
||||||
|
await expect(ctx.remote.probe.create('agent-1', { objective: 'ship' })).resolves.toMatchObject({
|
||||||
|
ok: false,
|
||||||
|
error: { code: 'internal', message: expect.stringContaining('carrier offline') },
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('owns each $on subscription in the calling fiber', async () => {
|
it('owns each $on subscription in the calling fiber', async () => {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import type { ConversationNodeDefinition } from '../src/client/contract/conversa
|
|||||||
import { Session } from '../src/client/sessions/session.ts'
|
import { Session } from '../src/client/sessions/session.ts'
|
||||||
import type { SessionsService } from '../src/client/sessions/service.ts'
|
import type { SessionsService } from '../src/client/sessions/service.ts'
|
||||||
import type { WorkspacesService } from '../src/client/workspaces/service.ts'
|
import type { WorkspacesService } from '../src/client/workspaces/service.ts'
|
||||||
import { FakeApiClient, ok } from './fake-api.ts'
|
import { FakeApiClient, fakeRemote, ok } from './fake-api.ts'
|
||||||
|
|
||||||
interface Bench {
|
interface Bench {
|
||||||
ctx: Context
|
ctx: Context
|
||||||
@@ -45,6 +45,7 @@ async function mount(): Promise<Bench> {
|
|||||||
}
|
}
|
||||||
ctx.reflect.provide('connection', handle)
|
ctx.reflect.provide('connection', handle)
|
||||||
ctx.reflect.provide('remote', {})
|
ctx.reflect.provide('remote', {})
|
||||||
|
ctx.reflect.provide('remote.commands', fakeRemote().commands)
|
||||||
await ctx.plugin(RuntimeClient).await()
|
await ctx.plugin(RuntimeClient).await()
|
||||||
return bench
|
return bench
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import TypertRegistry from '@deepseek-ai/dsh-typert-registry'
|
|||||||
// key face and per-event listener signatures.
|
// key face and per-event listener signatures.
|
||||||
import type {} from '@deepseek-ai/dsh-api-remotes/client'
|
import type {} from '@deepseek-ai/dsh-api-remotes/client'
|
||||||
import * as RuntimeClient from '../src/client/index.ts'
|
import * as RuntimeClient from '../src/client/index.ts'
|
||||||
import { FakeApiClient } from './fake-api.ts'
|
import { FakeApiClient, fakeRemote } from './fake-api.ts'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compile-time face of `ctx.remote.$on`, asserted by type-checking this file
|
* Compile-time face of `ctx.remote.$on`, asserted by type-checking this file
|
||||||
@@ -74,6 +74,7 @@ async function mount(): Promise<Bench> {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
ctx.reflect.provide('connection', handle)
|
ctx.reflect.provide('connection', handle)
|
||||||
|
ctx.reflect.provide('remote.commands', fakeRemote().commands)
|
||||||
await ctx.plugin(RuntimeClient).await()
|
await ctx.plugin(RuntimeClient).await()
|
||||||
return bench
|
return bench
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,9 @@ async function bench() {
|
|||||||
scopeOf: (c: Context) => scopeOf(c),
|
scopeOf: (c: Context) => scopeOf(c),
|
||||||
})
|
})
|
||||||
const commandsRemote = { list: () => Promise.resolve([]) }
|
const commandsRemote = { list: () => Promise.resolve([]) }
|
||||||
ctx.provide('remote', { commands: commandsRemote })
|
// The service subscribes its cache-invalidation events on construction, so
|
||||||
|
// the Remote face needs `$on` even where this spec dispatches none.
|
||||||
|
ctx.provide('remote', { commands: commandsRemote, $on: () => () => {} })
|
||||||
ctx.provide('remote.commands', commandsRemote)
|
ctx.provide('remote.commands', commandsRemote)
|
||||||
await ctx.plugin(SlotsService).await()
|
await ctx.plugin(SlotsService).await()
|
||||||
ctx.slots.register({
|
ctx.slots.register({
|
||||||
|
|||||||
@@ -40,6 +40,28 @@ interface BenchOptions {
|
|||||||
addressed?: SessionId
|
addressed?: SessionId
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fold one programmed answer into the generated Remote face's outcome: a
|
||||||
|
* resolved value is the ok branch, a rejection is the transport failure the
|
||||||
|
* carrier reports in the error branch instead of throwing at the caller.
|
||||||
|
* @param produce - the scripted answer for one Remote method.
|
||||||
|
* @returns the carried result the service reads.
|
||||||
|
*/
|
||||||
|
async function carried<T>(produce: () => Promise<T>) {
|
||||||
|
try {
|
||||||
|
return { ok: true as const, value: await produce() }
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
ok: false as const,
|
||||||
|
error: {
|
||||||
|
code: 'internal',
|
||||||
|
message: error instanceof Error ? error.message : String(error),
|
||||||
|
details: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function bench(opts: BenchOptions = {}) {
|
async function bench(opts: BenchOptions = {}) {
|
||||||
const ctx = new Context()
|
const ctx = new Context()
|
||||||
const registered = new Map<string, SlashSource>()
|
const registered = new Map<string, SlashSource>()
|
||||||
@@ -50,21 +72,22 @@ async function bench(opts: BenchOptions = {}) {
|
|||||||
const commandsRemote = {
|
const commandsRemote = {
|
||||||
list: async (sessionId: SessionId) => {
|
list: async (sessionId: SessionId) => {
|
||||||
listCalls.push({ sessionId })
|
listCalls.push({ sessionId })
|
||||||
const value = await (opts.commands ?? (p => Promise.resolve({
|
return await carried(async () => {
|
||||||
commands: p.sessionId === sid('s2') ? S2_CMDS : S1_CMDS,
|
const value = await (opts.commands ?? (p => Promise.resolve({
|
||||||
})))({ sessionId })
|
commands: p.sessionId === sid('s2') ? S2_CMDS : S1_CMDS,
|
||||||
return { ok: true as const, value: value.commands }
|
})))({ sessionId })
|
||||||
|
return value.commands
|
||||||
|
})
|
||||||
},
|
},
|
||||||
execute: async (sessionId: SessionId, line: string) => {
|
execute: async (sessionId: SessionId, line: string) => {
|
||||||
executeCalls.push({ sessionId, line })
|
executeCalls.push({ sessionId, line })
|
||||||
const fallback = (): Promise<ExecuteValue> => Promise.resolve({ matched: true })
|
return await carried(async () => {
|
||||||
const value = await (opts.execute ?? fallback)({ sessionId, line })
|
const fallback = (): Promise<ExecuteValue> => Promise.resolve({ matched: true })
|
||||||
return {
|
const value = await (opts.execute ?? fallback)({ sessionId, line })
|
||||||
ok: true as const,
|
return value.matched
|
||||||
value: value.matched
|
|
||||||
? { commandId: value.commandId ?? 'fake-command', result: { kind: 'success' as const } }
|
? { commandId: value.commandId ?? 'fake-command', result: { kind: 'success' as const } }
|
||||||
: undefined,
|
: undefined
|
||||||
}
|
})
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
ctx.provide('slash', {
|
ctx.provide('slash', {
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ async function bench() {
|
|||||||
children: { 'conversation.input.plan': { kind: 'single', scope: 'session' } },
|
children: { 'conversation.input.plan': { kind: 'single', scope: 'session' } },
|
||||||
} as never, () => null)
|
} as never, () => null)
|
||||||
const execute = vi.fn((_sessionId: SessionId, _line: string) =>
|
const execute = vi.fn((_sessionId: SessionId, _line: string) =>
|
||||||
Promise.resolve({ commandId: 'c1', result: { kind: 'success' as const } }))
|
Promise.resolve({ ok: true, value: { commandId: 'c1', result: { kind: 'success' as const } } }))
|
||||||
const commandsRemote = { execute }
|
const commandsRemote = { execute }
|
||||||
ctx.provide('remote', { commands: commandsRemote })
|
ctx.provide('remote', { commands: commandsRemote })
|
||||||
ctx.provide('remote.commands', commandsRemote)
|
ctx.provide('remote.commands', commandsRemote)
|
||||||
@@ -71,14 +71,15 @@ describe('ui-plan browser apply', () => {
|
|||||||
expect(b.execute).toHaveBeenLastCalledWith(SID, '/plan off')
|
expect(b.execute).toHaveBeenLastCalledWith(SID, '/plan off')
|
||||||
|
|
||||||
// Business failure folds to the composer-visible line: the generated method
|
// Business failure folds to the composer-visible line: the generated method
|
||||||
// throws with the RPC failure as its cause.
|
// reports the RPC failure in its error branch.
|
||||||
b.execute.mockRejectedValueOnce(new Error('client api: commands/execute failed', {
|
b.execute.mockResolvedValueOnce({
|
||||||
cause: { code: 'session-not-found', message: 'gone', details: {} },
|
ok: false,
|
||||||
}))
|
error: { code: 'session-not-found', message: 'gone', details: {} },
|
||||||
|
} as never)
|
||||||
await expect(injected.exitPlanMode()).resolves.toBe('gone (session-not-found)')
|
await expect(injected.exitPlanMode()).resolves.toBe('gone (session-not-found)')
|
||||||
|
|
||||||
// Unmatched admission (plan-mode not composed host-side) is also a failure line.
|
// Unmatched admission (plan-mode not composed host-side) is also a failure line.
|
||||||
b.execute.mockResolvedValueOnce(undefined as never)
|
b.execute.mockResolvedValueOnce({ ok: true, value: undefined } as never)
|
||||||
await expect(injected.exitPlanMode()).resolves.toBe('unknown command: /plan off')
|
await expect(injected.exitPlanMode()).resolves.toBe('unknown command: /plan off')
|
||||||
|
|
||||||
await fiber.dispose()
|
await fiber.dispose()
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
/**
|
/**
|
||||||
* REAL-composition coverage: a test-only cordis.yml booted through the
|
* REAL-composition coverage: a test-only cordis.yml booted through the
|
||||||
* vendored Loader mounts the webserver row plus the adaptive chooser, and the
|
* vendored Loader mounts the webserver row plus the adaptive chooser, and the
|
||||||
* assertions observe the durable outcome — which backend entry the chooser
|
* assertions observe the durable outcome — which backend and surface entries
|
||||||
* mounted into the Loader store, the capability the seam then serves, and
|
* the chooser mounted into the Loader store, the capability the seam then
|
||||||
* that disposing the chooser removes the mounted entry again (HMR safety),
|
* serves, and that disposing the chooser removes both mounted entries again
|
||||||
* joining the backend's own teardown before the disposer settles.
|
* (HMR safety), joining the backend's own teardown before the disposer settles.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { chmodSync, mkdtempSync, writeFileSync } from 'node:fs'
|
import { chmodSync, mkdtempSync, writeFileSync } from 'node:fs'
|
||||||
@@ -20,6 +20,8 @@ import HttpServer from '@deepseek-ai/dsh-host-webserver'
|
|||||||
import type { DirectoryPicker } from '@deepseek-ai/dsh-host-directory-picker'
|
import type { DirectoryPicker } from '@deepseek-ai/dsh-host-directory-picker'
|
||||||
import BrowseDirectoryPicker from '@deepseek-ai/dsh-host-directory-picker-browse'
|
import BrowseDirectoryPicker from '@deepseek-ai/dsh-host-directory-picker-browse'
|
||||||
import NativeDirectoryPicker from '@deepseek-ai/dsh-host-directory-picker-native'
|
import NativeDirectoryPicker from '@deepseek-ai/dsh-host-directory-picker-native'
|
||||||
|
import * as BrowseSurface from '@deepseek-ai/dsh-client-ui-directory-picker'
|
||||||
|
import * as NativeSurface from '@deepseek-ai/dsh-client-ui-directory-picker-native'
|
||||||
import * as DirectoryPickerAuto from '../src/index.ts'
|
import * as DirectoryPickerAuto from '../src/index.ts'
|
||||||
|
|
||||||
const renameControl = vi.hoisted(() => ({
|
const renameControl = vi.hoisted(() => ({
|
||||||
@@ -48,6 +50,8 @@ vi.mock('node:fs/promises', async (importOriginal) => {
|
|||||||
const AUTO = '@deepseek-ai/dsh-host-directory-picker-auto'
|
const AUTO = '@deepseek-ai/dsh-host-directory-picker-auto'
|
||||||
const NATIVE = '@deepseek-ai/dsh-host-directory-picker-native'
|
const NATIVE = '@deepseek-ai/dsh-host-directory-picker-native'
|
||||||
const BROWSE = '@deepseek-ai/dsh-host-directory-picker-browse'
|
const BROWSE = '@deepseek-ai/dsh-host-directory-picker-browse'
|
||||||
|
const NATIVE_SURFACE = '@deepseek-ai/dsh-client-ui-directory-picker-native'
|
||||||
|
const BROWSE_SURFACE = '@deepseek-ai/dsh-client-ui-directory-picker'
|
||||||
|
|
||||||
let root: string | undefined
|
let root: string | undefined
|
||||||
let fakeBin: string | undefined
|
let fakeBin: string | undefined
|
||||||
@@ -92,6 +96,8 @@ async function loadComposition(bindHost: '127.0.0.1' | '0.0.0.0'): Promise<{ ctx
|
|||||||
[AUTO, DirectoryPickerAuto],
|
[AUTO, DirectoryPickerAuto],
|
||||||
[NATIVE, NativeDirectoryPicker],
|
[NATIVE, NativeDirectoryPicker],
|
||||||
[BROWSE, BrowseDirectoryPicker],
|
[BROWSE, BrowseDirectoryPicker],
|
||||||
|
[NATIVE_SURFACE, NativeSurface],
|
||||||
|
[BROWSE_SURFACE, BrowseSurface],
|
||||||
])
|
])
|
||||||
context.loader.internal = {
|
context.loader.internal = {
|
||||||
version: 'v2',
|
version: 'v2',
|
||||||
@@ -142,7 +148,9 @@ describe('real Loader composition', () => {
|
|||||||
.map(entry => entry.options.name)
|
.map(entry => entry.options.name)
|
||||||
expect(unloaded).toEqual([])
|
expect(unloaded).toEqual([])
|
||||||
expect(entryNames(ctx)).toContain(NATIVE)
|
expect(entryNames(ctx)).toContain(NATIVE)
|
||||||
|
expect(entryNames(ctx)).toContain(NATIVE_SURFACE)
|
||||||
expect(entryNames(ctx)).not.toContain(BROWSE)
|
expect(entryNames(ctx)).not.toContain(BROWSE)
|
||||||
|
expect(entryNames(ctx)).not.toContain(BROWSE_SURFACE)
|
||||||
const picker = ctx.get('directoryPicker') as DirectoryPicker
|
const picker = ctx.get('directoryPicker') as DirectoryPicker
|
||||||
expect(picker.capability().kind).toBe('native')
|
expect(picker.capability().kind).toBe('native')
|
||||||
// The mounted row lives in the Loader's in-memory root tree only — the
|
// The mounted row lives in the Loader's in-memory root tree only — the
|
||||||
@@ -155,6 +163,7 @@ describe('real Loader composition', () => {
|
|||||||
const autoEntry = [...ctx.loader.entries()].find(entry => entry.options.name === AUTO)!
|
const autoEntry = [...ctx.loader.entries()].find(entry => entry.options.name === AUTO)!
|
||||||
await autoEntry.fiber!.dispose()
|
await autoEntry.fiber!.dispose()
|
||||||
expect(entryNames(ctx)).not.toContain(NATIVE)
|
expect(entryNames(ctx)).not.toContain(NATIVE)
|
||||||
|
expect(entryNames(ctx)).not.toContain(NATIVE_SURFACE)
|
||||||
expect(ctx.get('directoryPicker')).toBeUndefined()
|
expect(ctx.get('directoryPicker')).toBeUndefined()
|
||||||
// Self-disposing an include-tree entry persists `disabled: true` (loader
|
// Self-disposing an include-tree entry persists `disabled: true` (loader
|
||||||
// behavior, not the chooser's); await that debounced write so it cannot
|
// behavior, not the chooser's); await that debounced write so it cannot
|
||||||
@@ -170,7 +179,9 @@ describe('real Loader composition', () => {
|
|||||||
const { ctx } = await loadComposition('127.0.0.1')
|
const { ctx } = await loadComposition('127.0.0.1')
|
||||||
|
|
||||||
expect(entryNames(ctx)).toContain(BROWSE)
|
expect(entryNames(ctx)).toContain(BROWSE)
|
||||||
|
expect(entryNames(ctx)).toContain(BROWSE_SURFACE)
|
||||||
expect(entryNames(ctx)).not.toContain(NATIVE)
|
expect(entryNames(ctx)).not.toContain(NATIVE)
|
||||||
|
expect(entryNames(ctx)).not.toContain(NATIVE_SURFACE)
|
||||||
const picker = ctx.get('directoryPicker') as DirectoryPicker
|
const picker = ctx.get('directoryPicker') as DirectoryPicker
|
||||||
expect(picker.capability().kind).toBe('browse')
|
expect(picker.capability().kind).toBe('browse')
|
||||||
})
|
})
|
||||||
@@ -180,7 +191,9 @@ describe('real Loader composition', () => {
|
|||||||
const { ctx } = await loadComposition('0.0.0.0')
|
const { ctx } = await loadComposition('0.0.0.0')
|
||||||
|
|
||||||
expect(entryNames(ctx)).toContain(BROWSE)
|
expect(entryNames(ctx)).toContain(BROWSE)
|
||||||
|
expect(entryNames(ctx)).toContain(BROWSE_SURFACE)
|
||||||
expect(entryNames(ctx)).not.toContain(NATIVE)
|
expect(entryNames(ctx)).not.toContain(NATIVE)
|
||||||
|
expect(entryNames(ctx)).not.toContain(NATIVE_SURFACE)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('tolerates the mounted entry being removed by the tree before the chooser unloads', { timeout: 60_000 }, async () => {
|
it('tolerates the mounted entry being removed by the tree before the chooser unloads', { timeout: 60_000 }, async () => {
|
||||||
@@ -193,6 +206,7 @@ describe('real Loader composition', () => {
|
|||||||
renameControl.remainingFailures = 1
|
renameControl.remainingFailures = 1
|
||||||
await expect(autoEntry.fiber!.dispose()).resolves.not.toThrow()
|
await expect(autoEntry.fiber!.dispose()).resolves.not.toThrow()
|
||||||
expect(entryNames(ctx)).not.toContain(NATIVE)
|
expect(entryNames(ctx)).not.toContain(NATIVE)
|
||||||
|
expect(entryNames(ctx)).not.toContain(NATIVE_SURFACE)
|
||||||
// Same self-dispose persistence as above: let the write land before teardown.
|
// Same self-dispose persistence as above: let the write land before teardown.
|
||||||
await expect.poll(async () => await readFile(configPath, 'utf8')).toContain('disabled: true')
|
await expect.poll(async () => await readFile(configPath, 'utf8')).toContain('disabled: true')
|
||||||
expect(renameControl.injectedFailures).toBe(1)
|
expect(renameControl.injectedFailures).toBe(1)
|
||||||
|
|||||||
@@ -13,6 +13,16 @@ declare module '@deepseek-ai/dsh-type-meta' {
|
|||||||
export interface TypeRTRemoteMap {}
|
export interface TypeRTRemoteMap {}
|
||||||
export interface TypeRTRemoteScopeMap {}
|
export interface TypeRTRemoteScopeMap {}
|
||||||
|
|
||||||
|
export interface RemoteFailure {
|
||||||
|
readonly code: string
|
||||||
|
readonly message: string
|
||||||
|
readonly details: object
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RemoteResult<T> =
|
||||||
|
| { readonly ok: true; readonly value: T }
|
||||||
|
| { readonly ok: false; readonly error: RemoteFailure }
|
||||||
|
|
||||||
export type TypeRTRemoteNamespace<Namespace extends string> = {
|
export type TypeRTRemoteNamespace<Namespace extends string> = {
|
||||||
[Endpoint in keyof TypeRTRemoteMap as Endpoint extends `${Namespace}/${infer Method}`
|
[Endpoint in keyof TypeRTRemoteMap as Endpoint extends `${Namespace}/${infer Method}`
|
||||||
? Method
|
? Method
|
||||||
|
|||||||
@@ -114,15 +114,15 @@ describe('Remote model generation', { timeout: 60_000 }, () => {
|
|||||||
|
|
||||||
expect(artifact?.js).toContain('invocations: [')
|
expect(artifact?.js).toContain('invocations: [')
|
||||||
expect(artifact?.remote?.dts).toContain(
|
expect(artifact?.remote?.dts).toContain(
|
||||||
"'goals/create': (agentId: AgentId, request: CreateGoalRequest, signal?: AbortSignal) => Promise<CreateGoalResult>",
|
"'goals/create': (agentId: AgentId, request: CreateGoalRequest, signal?: AbortSignal) => Promise<RemoteResult<CreateGoalResult>>",
|
||||||
)
|
)
|
||||||
expect(artifact?.remote?.dts).toContain('interface TypeRTRemoteNamespace$676f616c73 {\n create:')
|
expect(artifact?.remote?.dts).toContain('interface TypeRTRemoteNamespace$676f616c73 {\n create:')
|
||||||
expect(artifact?.remote?.dts).toContain("'goals': TypeRTRemoteNamespace$676f616c73")
|
expect(artifact?.remote?.dts).toContain("'goals': TypeRTRemoteNamespace$676f616c73")
|
||||||
expect(artifact?.remote?.dts).toContain(
|
expect(artifact?.remote?.dts).toContain(
|
||||||
"'agent:goals/create': (request: CreateGoalRequest, signal?: AbortSignal) => Promise<CreateGoalResult>",
|
"'agent:goals/create': (request: CreateGoalRequest, signal?: AbortSignal) => Promise<RemoteResult<CreateGoalResult>>",
|
||||||
)
|
)
|
||||||
expect(artifact?.remote?.dts).toContain(
|
expect(artifact?.remote?.dts).toContain(
|
||||||
"'agent:goals/rename': (request: RenameGoalRequest) => Promise<RenameGoalResult>",
|
"'agent:goals/rename': (request: RenameGoalRequest) => Promise<RemoteResult<RenameGoalResult>>",
|
||||||
)
|
)
|
||||||
|
|
||||||
const remoteJs = artifact?.remote?.js
|
const remoteJs = artifact?.remote?.js
|
||||||
@@ -172,13 +172,13 @@ export type {`,
|
|||||||
|
|
||||||
const [artifact] = new WorkspaceTypertGenerator(root).generate()
|
const [artifact] = new WorkspaceTypertGenerator(root).generate()
|
||||||
expect(artifact?.remote?.dts).toContain(
|
expect(artifact?.remote?.dts).toContain(
|
||||||
"'goals/maybe': (value: string | undefined) => Promise<string | undefined>",
|
"'goals/maybe': (value: string | undefined) => Promise<RemoteResult<string | undefined>>",
|
||||||
)
|
)
|
||||||
expect(artifact?.remote?.dts).toContain("'goals/clear': () => Promise<void>")
|
expect(artifact?.remote?.dts).toContain("'goals/clear': () => Promise<RemoteResult<void>>")
|
||||||
// An explicit `T | undefined` stays a required argument; only authored
|
// An explicit `T | undefined` stays a required argument; only authored
|
||||||
// optionality lets a consumer omit the field.
|
// optionality lets a consumer omit the field.
|
||||||
expect(artifact?.remote?.dts).not.toContain('value?: string')
|
expect(artifact?.remote?.dts).not.toContain('value?: string')
|
||||||
expect(artifact?.remote?.dts).toContain("'goals/labelled': (id: string, label?: string) => Promise<string>")
|
expect(artifact?.remote?.dts).toContain("'goals/labelled': (id: string, label?: string) => Promise<RemoteResult<string>>")
|
||||||
|
|
||||||
const remoteJs = artifact?.remote?.js
|
const remoteJs = artifact?.remote?.js
|
||||||
if (remoteJs === undefined) throw new Error('undefined Remote fixture emitted no Host-for-Client JavaScript')
|
if (remoteJs === undefined) throw new Error('undefined Remote fixture emitted no Host-for-Client JavaScript')
|
||||||
@@ -256,7 +256,7 @@ export type GenericResult = {
|
|||||||
|
|
||||||
const [artifact] = new WorkspaceTypertGenerator(root).generate()
|
const [artifact] = new WorkspaceTypertGenerator(root).generate()
|
||||||
expect(artifact?.remote?.dts).toContain(
|
expect(artifact?.remote?.dts).toContain(
|
||||||
"'goals/dispatch': (request: GenericRequest) => Promise<GenericResult>",
|
"'goals/dispatch': (request: GenericRequest) => Promise<RemoteResult<GenericResult>>",
|
||||||
)
|
)
|
||||||
const remoteJs = artifact?.remote?.js
|
const remoteJs = artifact?.remote?.js
|
||||||
if (remoteJs === undefined) throw new Error('generic Remote fixture emitted no Host-for-Client JavaScript')
|
if (remoteJs === undefined) throw new Error('generic Remote fixture emitted no Host-for-Client JavaScript')
|
||||||
@@ -306,7 +306,7 @@ export interface BoxPayload {
|
|||||||
|
|
||||||
const [artifact] = new WorkspaceTypertGenerator(root).generate()
|
const [artifact] = new WorkspaceTypertGenerator(root).generate()
|
||||||
expect(artifact?.remote?.dts).toMatch(/import type \{ [^}]*Box[^}]*BoxPayload[^}]* \} from '@fixture\/remote\/types'/)
|
expect(artifact?.remote?.dts).toMatch(/import type \{ [^}]*Box[^}]*BoxPayload[^}]* \} from '@fixture\/remote\/types'/)
|
||||||
expect(artifact?.remote?.dts).toContain('box: (request: Box<BoxPayload>) => Promise<Box<BoxPayload>>')
|
expect(artifact?.remote?.dts).toContain('box: (request: Box<BoxPayload>) => Promise<RemoteResult<Box<BoxPayload>>>')
|
||||||
assertRemoteConsumerTypechecks(artifact?.remote?.dts, artifact?.remote?.dtsMap, root)
|
assertRemoteConsumerTypechecks(artifact?.remote?.dts, artifact?.remote?.dtsMap, root)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -326,7 +326,7 @@ export interface BoxPayload {
|
|||||||
))
|
))
|
||||||
|
|
||||||
const [artifact] = new WorkspaceTypertGenerator(root).generate()
|
const [artifact] = new WorkspaceTypertGenerator(root).generate()
|
||||||
expect(artifact?.remote?.dts).toContain("'create-goal': (request: CreateGoalRequest) => Promise<CreateGoalResult>")
|
expect(artifact?.remote?.dts).toContain("'create-goal': (request: CreateGoalRequest) => Promise<RemoteResult<CreateGoalResult>>")
|
||||||
assertRemoteConsumerTypechecks(artifact?.remote?.dts, artifact?.remote?.dtsMap, root)
|
assertRemoteConsumerTypechecks(artifact?.remote?.dts, artifact?.remote?.dtsMap, root)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -636,6 +636,7 @@ function assertRemoteConsumerTypechecks(
|
|||||||
const consumerSource = `
|
const consumerSource = `
|
||||||
import remote from '@fixture/remote/remote'
|
import remote from '@fixture/remote/remote'
|
||||||
import type {
|
import type {
|
||||||
|
RemoteResult,
|
||||||
TypeRTRemoteContribution,
|
TypeRTRemoteContribution,
|
||||||
TypeRTRemoteScopeMap,
|
TypeRTRemoteScopeMap,
|
||||||
TypeRTRemoteMap,
|
TypeRTRemoteMap,
|
||||||
@@ -647,12 +648,12 @@ const contribution: TypeRTRemoteContribution = remote
|
|||||||
declare const create: TypeRTRemoteMap['goals/create']
|
declare const create: TypeRTRemoteMap['goals/create']
|
||||||
declare const createScoped: TypeRTRemoteScopeMap['agent:goals/create']
|
declare const createScoped: TypeRTRemoteScopeMap['agent:goals/create']
|
||||||
declare const rename: TypeRTRemoteScopeMap['agent:goals/rename']
|
declare const rename: TypeRTRemoteScopeMap['agent:goals/rename']
|
||||||
const created: Promise<CreateGoalResult> = create('agent-1', { title: 'ship' })
|
const created: Promise<RemoteResult<CreateGoalResult>> = create('agent-1', { title: 'ship' })
|
||||||
const cancellable: Promise<CreateGoalResult> = create('agent-1', { title: 'ship' }, new AbortController().signal)
|
const cancellable: Promise<RemoteResult<CreateGoalResult>> = create('agent-1', { title: 'ship' }, new AbortController().signal)
|
||||||
const createdScoped: Promise<CreateGoalResult> = createScoped({ title: 'ship' })
|
const createdScoped: Promise<RemoteResult<CreateGoalResult>> = createScoped({ title: 'ship' })
|
||||||
const renamed: Promise<RenameGoalResult> = rename({ ref: 'goal-1', title: 'land' })
|
const renamed: Promise<RemoteResult<RenameGoalResult>> = rename({ ref: 'goal-1', title: 'land' })
|
||||||
declare const ctx: { remote: TypeRTRemoteNamespaceMap }
|
declare const ctx: { remote: TypeRTRemoteNamespaceMap }
|
||||||
const navigated: Promise<CreateGoalResult> = ctx.remote.goals.create('agent-1', { title: 'navigate' })
|
const navigated: Promise<RemoteResult<CreateGoalResult>> = ctx.remote.goals.create('agent-1', { title: 'navigate' })
|
||||||
void contribution
|
void contribution
|
||||||
void created
|
void created
|
||||||
void cancellable
|
void cancellable
|
||||||
|
|||||||
Reference in New Issue
Block a user