test-remote.ts 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  1. /** Test-only direct Remote face over the Session Controller's internal controllers. */
  2. import type { Context } from '@deepseek-ai/cordis'
  3. import type { ModelSelection as AgentModelSelection } from '@deepseek-ai/dsh-agent'
  4. import { vi } from 'vitest'
  5. import {
  6. TypertRemoteFailure,
  7. type RemoteResult,
  8. } from '@deepseek-ai/dsh-typert-protocol'
  9. import SessionController from '../src/index.ts'
  10. import type {
  11. SessionAttachmentRequest,
  12. SessionAttachmentValue,
  13. SessionCancelRequest,
  14. SessionCancelValue,
  15. SessionControlFrame,
  16. SessionCreateRequest,
  17. SessionCreateValue,
  18. SessionForkRequest,
  19. SessionForkValue,
  20. SessionListRequest,
  21. SessionListValue,
  22. SessionModels,
  23. SessionModelsRequest,
  24. SessionPage,
  25. SessionPageRequest,
  26. SessionPromptRequest,
  27. SessionPromptValue,
  28. SessionRenameRequest,
  29. SessionRenameValue,
  30. SessionSearchRequest,
  31. SessionSearchValue,
  32. SessionSelectModelRequest,
  33. SessionSelectModelValue,
  34. SessionUpdateQueueRequest,
  35. SessionUpdateQueueValue,
  36. } from '../src/types.ts'
  37. /** Direct test face matching the generated `ctx.remote.session` unary methods. */
  38. export interface TestSessionRemote {
  39. list(request: SessionListRequest, signal?: AbortSignal): Promise<RemoteResult<SessionListValue>>
  40. search(request: SessionSearchRequest, signal?: AbortSignal): Promise<RemoteResult<SessionSearchValue>>
  41. create(request: SessionCreateRequest): Promise<RemoteResult<SessionCreateValue>>
  42. models(request: SessionModelsRequest): Promise<RemoteResult<SessionModels>>
  43. selectModel(request: SessionSelectModelRequest): Promise<RemoteResult<SessionSelectModelValue>>
  44. rename(request: SessionRenameRequest): Promise<RemoteResult<SessionRenameValue>>
  45. fork(request: SessionForkRequest): Promise<RemoteResult<SessionForkValue>>
  46. prompt(request: SessionPromptRequest, signal?: AbortSignal): Promise<RemoteResult<SessionPromptValue>>
  47. attachment(request: SessionAttachmentRequest): Promise<RemoteResult<SessionAttachmentValue>>
  48. updateQueue(request: SessionUpdateQueueRequest): Promise<RemoteResult<SessionUpdateQueueValue>>
  49. cancel(request: SessionCancelRequest): Promise<RemoteResult<SessionCancelValue>>
  50. page(request: SessionPageRequest, signal?: AbortSignal): Promise<RemoteResult<SessionPage>>
  51. control(signal?: AbortSignal): AsyncIterable<SessionControlFrame>
  52. }
  53. /** Dependencies and policy supplied by a Session Controller unit harness. */
  54. export interface TestSessionRemoteDefaults {
  55. readonly defaultModelSelection: () => AgentModelSelection
  56. readonly cwd: string
  57. readonly coldBlankProbeMaxBytes?: number
  58. readonly saveDefaultModelSelection?: (selection: AgentModelSelection) => void | Promise<void>
  59. }
  60. const installed = new WeakMap<Context, SessionController>()
  61. function installControllers(
  62. ctx: Context,
  63. defaults: TestSessionRemoteDefaults,
  64. ): SessionController {
  65. const found = installed.get(ctx)
  66. if (found !== undefined) return found
  67. if (ctx.get('typert') === undefined) {
  68. const dispose = (): void => {}
  69. ctx.provide('typert', {
  70. lookups: { configure: () => dispose },
  71. contexts: { configureHost: () => dispose },
  72. } as never)
  73. }
  74. if (ctx.get('agentDefaultModel') === undefined) {
  75. ctx.provide('agentDefaultModel', {
  76. currentSelection: defaults.defaultModelSelection,
  77. saveSelection: async (selection: AgentModelSelection) => {
  78. await defaults.saveDefaultModelSelection?.(selection)
  79. },
  80. } as never)
  81. }
  82. if (ctx.get('llm') === undefined) {
  83. ctx.provide('llm', {
  84. listProviders: () => {
  85. const selection = defaults.defaultModelSelection()
  86. return [{ id: selection.provider, name: selection.provider }]
  87. },
  88. } as never)
  89. }
  90. if (ctx.get('userQuestions') === undefined) {
  91. ctx.provide('userQuestions', {
  92. registerProvider: () => (): void => {},
  93. } as never)
  94. }
  95. const cwd = vi.spyOn(process, 'cwd').mockReturnValue(defaults.cwd)
  96. let controller: SessionController
  97. try {
  98. controller = new SessionController(ctx, defaults.coldBlankProbeMaxBytes === undefined
  99. ? {}
  100. : { coldBlankProbeMaxBytes: defaults.coldBlankProbeMaxBytes })
  101. } finally {
  102. cwd.mockRestore()
  103. }
  104. installed.set(ctx, controller)
  105. return controller
  106. }
  107. /** Build or return the production Session Controller for a direct unit harness. */
  108. export function createSessionTestController(
  109. ctx: Context,
  110. defaults: TestSessionRemoteDefaults,
  111. ): SessionController {
  112. return installControllers(ctx, defaults)
  113. }
  114. function remoteResult<T>(
  115. operation: () => T | Promise<T>,
  116. signal?: AbortSignal,
  117. ): Promise<RemoteResult<T>> {
  118. return Promise.resolve()
  119. .then(operation)
  120. .then(value => ({ ok: true as const, value }))
  121. .catch((error: unknown) => ({
  122. ok: false as const,
  123. error: signal?.aborted === true
  124. ? { code: 'cancelled', message: 'request was aborted', details: {} }
  125. : error instanceof TypertRemoteFailure
  126. ? error.failure
  127. : {
  128. code: 'internal',
  129. message: error instanceof Error ? error.message : String(error),
  130. details: {},
  131. },
  132. }))
  133. }
  134. /** Build the generated Session Remote's unary result semantics without a carrier. */
  135. export function createSessionTestRemote(
  136. ctx: Context,
  137. defaults: TestSessionRemoteDefaults,
  138. ): TestSessionRemote {
  139. const direct = createSessionTestController(ctx, defaults)
  140. return {
  141. list: (request, signal = new AbortController().signal) => remoteResult(
  142. () => direct.list(request, signal),
  143. signal,
  144. ),
  145. search: (request, signal = new AbortController().signal) => remoteResult(
  146. () => direct.search(request, signal),
  147. signal,
  148. ),
  149. create: request => remoteResult(() => direct.create(request)),
  150. models: request => remoteResult(() => direct.models(request)),
  151. selectModel: request => remoteResult(() => direct.selectModel(request)),
  152. rename: request => remoteResult(() => direct.rename(request)),
  153. fork: request => remoteResult(() => direct.fork(request)),
  154. prompt: (request, signal = new AbortController().signal) => remoteResult(
  155. () => direct.prompt(request, signal),
  156. signal,
  157. ),
  158. attachment: request => remoteResult(() => direct.attachment(request)),
  159. updateQueue: request => remoteResult(() => direct.updateQueue(request)),
  160. cancel: request => remoteResult(() => direct.cancel(request)),
  161. page: (request, signal = new AbortController().signal) => remoteResult(
  162. () => direct.page(request, signal),
  163. signal,
  164. ),
  165. control: (signal = new AbortController().signal) => direct.control(signal),
  166. }
  167. }