Browse Source

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.
imccyu 2 months ago
parent
commit
92e0e3377f

+ 47 - 26
packages/api/gateway/tests/client.spec.ts

@@ -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 }>
-    'probe/maybe': (value: string | null | undefined) => Promise<string | null | undefined>
+    ) => Promise<RemoteResult<{ readonly ref: string }>>
+    '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 }>
-    'fixture:probe/rename': (request: { readonly objective: string }) => Promise<{ readonly renamed: boolean }>
+    ) => Promise<RemoteResult<{ readonly ref: string }>>
+    '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
-    try {
-      await ctx.remote.probe.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)
+    const outcome = await ctx.remote.probe.create('agent-1', { objective: 'ship' })
+    expect(outcome.ok).toBe(false)
+    if (outcome.ok) throw new Error('expected the Client API invocation to report a failure')
+    expect(outcome.error).toBe(rpcError)
+  })
+
+  it('folds a transport throw into the error branch', async () => {
+    const ctx = await bench(vi.fn<ConnectionHandle['rpc']['call']>()
+      .mockRejectedValue(new Error('carrier offline')))
+    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 () => {

+ 2 - 1
packages/client/runtime/tests/client-apply.spec.ts

@@ -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
 }
 }

+ 2 - 1
packages/client/runtime/tests/wire-events.spec.ts

@@ -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
 }
 }

+ 3 - 1
packages/client/ui-command/tests/browser-plugin.spec.ts

@@ -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({

+ 34 - 11
packages/client/ui-command/tests/service.spec.ts

@@ -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({
-        commands: p.sessionId === sid('s2') ? S2_CMDS : S1_CMDS,
-      })))({ sessionId })
-      return { ok: true as const, value: value.commands }
+      return await carried(async () => {
+        const value = await (opts.commands ?? (p => Promise.resolve({
+          commands: p.sessionId === sid('s2') ? S2_CMDS : S1_CMDS,
+        })))({ 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 })
-      const value = await (opts.execute ?? fallback)({ sessionId, line })
-      return {
-        ok: true as const,
-        value: value.matched
+      return await carried(async () => {
+        const fallback = (): Promise<ExecuteValue> => Promise.resolve({ matched: true })
+        const value = await (opts.execute ?? fallback)({ sessionId, line })
+        return 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', {

+ 7 - 6
packages/client/ui-plan/tests/browser-plugin.spec.ts

@@ -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.
-    b.execute.mockRejectedValueOnce(new Error('client api: commands/execute failed', {
-      cause: { code: 'session-not-found', message: 'gone', details: {} },
-    }))
+    // reports the RPC failure in its error branch.
+    b.execute.mockResolvedValueOnce({
+      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()

+ 18 - 4
packages/host/directory-picker-auto/tests/loader-composition.spec.ts

@@ -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
- * mounted into the Loader store, the capability the seam then serves, and
- * that disposing the chooser removes the mounted entry again (HMR safety),
- * joining the backend's own teardown before the disposer settles.
+ * assertions observe the durable outcome — which backend and surface entries
+ * the chooser mounted into the Loader store, the capability the seam then
+ * serves, and that disposing the chooser removes both mounted entries again
+ * (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)

+ 10 - 0
packages/typert/generator/tests/fixtures/remote-model/type-meta.d.ts

@@ -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

+ 15 - 14
packages/typert/generator/tests/remote-model.spec.ts

@@ -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 cancellable: Promise<CreateGoalResult> = create('agent-1', { title: 'ship' }, new AbortController().signal)
-const createdScoped: Promise<CreateGoalResult> = createScoped({ title: 'ship' })
-const renamed: Promise<RenameGoalResult> = rename({ ref: 'goal-1', title: 'land' })
+const created: Promise<RemoteResult<CreateGoalResult>> = create('agent-1', { title: 'ship' })
+const cancellable: Promise<RemoteResult<CreateGoalResult>> = create('agent-1', { title: 'ship' }, new AbortController().signal)
+const createdScoped: Promise<RemoteResult<CreateGoalResult>> = createScoped({ title: 'ship' })
+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