provider.spec.ts 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226
  1. /** Native SDK lifecycle and catalog behavior without desktop access. */
  2. import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
  3. import { Context } from '@deepseek-ai/cordis'
  4. import ComputerUseRegistry from '@deepseek-ai/dsh-computer-use'
  5. import { ComputerUseProviderName } from '@deepseek-ai/dsh-computer-use/brand'
  6. import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
  7. import ToolRuntime from '@deepseek-ai/dsh-tools'
  8. import { ToolCallId } from '@deepseek-ai/dsh-llm'
  9. import * as NativeProvider from '../src/index.ts'
  10. import { catalog, fixture, resetFixture } from './fixtures/cua-driver.ts'
  11. vi.mock('@trycua/cua-driver', async () => import('./fixtures/cua-driver.ts'))
  12. let ctx: Context
  13. beforeEach(async () => {
  14. resetFixture()
  15. ctx = new Context()
  16. await ctx.plugin(ComputerUseRegistry)
  17. await ctx.plugin(SystemPrompt)
  18. await ctx.plugin(ToolRuntime)
  19. })
  20. afterEach(async () => {
  21. await ctx.fiber.dispose()
  22. })
  23. function execute(rawName: string, args: Record<string, unknown> = {}) {
  24. return ctx.tools.execute({
  25. name: `cua_driver_native__${rawName}`,
  26. callId: ToolCallId('native-test'),
  27. arguments: args,
  28. signal: new AbortController().signal,
  29. })
  30. }
  31. describe('Cua Driver native provider', () => {
  32. it('registers the upstream catalog and calls its raw names', async () => {
  33. const fiber = ctx.plugin(NativeProvider)
  34. await fiber
  35. expect(ctx.computerUse.providerName).toBe('cua-driver-native')
  36. expect(ctx.tools.schemas().map(tool => tool.name)).toEqual(catalog.tools.map(tool => `cua_driver_native__${tool.name}`))
  37. const result = await execute('click', { pid: 9, window_id: 7 })
  38. expect(result.isError).toBe(false)
  39. expect(fixture.calls).toMatchObject([{ name: 'click', args: { pid: 9, window_id: 7 } }])
  40. await fiber.dispose()
  41. expect(ctx.tools.schemas()).toEqual([])
  42. expect(ctx.computerUse.providerName).toBeUndefined()
  43. expect(fixture.shutdowns).toBe(1)
  44. expect(fixture.destroys).toBe(1)
  45. })
  46. it('rejects another provider before importing or creating a native runtime', async () => {
  47. const release = ctx.computerUse.register(ComputerUseProviderName('another-driver'))
  48. await expect(ctx.plugin(NativeProvider)).rejects.toThrow('already registered')
  49. expect(fixture.creates).toBe(0)
  50. expect(ctx.computerUse.providerName).toBe('another-driver')
  51. await release()
  52. })
  53. it('rolls back a failed native constructor', async () => {
  54. fixture.createError = new Error('native library initialization failed')
  55. await expect(ctx.plugin(NativeProvider)).rejects.toThrow('native library initialization failed')
  56. expect(ctx.computerUse.providerName).toBeUndefined()
  57. expect(ctx.tools.schemas()).toEqual([])
  58. expect(fixture.shutdowns).toBe(0)
  59. })
  60. it('accepts upstream tools whose optional description is absent', async () => {
  61. fixture.list = async () => JSON.stringify({ tools: [{ name: 'check_permissions', inputSchema: { type: 'object', properties: {} } }] })
  62. await ctx.plugin(NativeProvider)
  63. expect(ctx.tools.schemas()[0]?.description).toBe('')
  64. })
  65. it.each([
  66. ['invalid JSON', '{'],
  67. ['missing catalog', '{}'],
  68. ['duplicate names', JSON.stringify({ tools: [catalog.tools[0], catalog.tools[0]] })],
  69. ['invalid function name', JSON.stringify({ tools: [{ ...catalog.tools[0], name: 'x'.repeat(70) }] })],
  70. ])('rolls back the native runtime after %s', async (_label, response) => {
  71. fixture.list = async () => response
  72. await expect(ctx.plugin(NativeProvider)).rejects.toThrow()
  73. expect(ctx.computerUse.providerName).toBeUndefined()
  74. expect(ctx.tools.schemas()).toEqual([])
  75. expect(fixture.shutdowns).toBe(1)
  76. expect(fixture.destroys).toBe(1)
  77. })
  78. it('preserves native tool refusals as model-visible errors', async () => {
  79. fixture.call = async () => ({ isError: true, content: [{ type: 'text', text: 'background_unavailable' }] })
  80. await ctx.plugin(NativeProvider)
  81. const result = await execute('click', { pid: 9, window_id: 7 })
  82. expect(result.isError).toBe(true)
  83. expect(result.content).toEqual([{ type: 'text', text: 'Error: background_unavailable' }])
  84. })
  85. it('leaves an unrelated tool running when the native provider unloads', async () => {
  86. const started = Promise.withResolvers<AbortSignal>()
  87. const settled = Promise.withResolvers<boolean>()
  88. const controller = new AbortController()
  89. const fiber = ctx.plugin(NativeProvider)
  90. await fiber
  91. ctx.tools.register({
  92. name: 'unrelated', description: 'Independent tool.', parameters: { type: 'object' },
  93. output: { schema: { type: 'boolean' }, render: () => [{ type: 'text', text: 'Done.' }] },
  94. execute(_args, exec) {
  95. started.resolve(exec.signal)
  96. return settled.promise
  97. },
  98. })
  99. const result = ctx.tools.execute({
  100. name: 'unrelated', callId: ToolCallId('unrelated'), arguments: {}, signal: controller.signal,
  101. })
  102. try {
  103. const signal = await started.promise
  104. await fiber.dispose()
  105. expect(signal).toBe(controller.signal)
  106. expect(signal.aborted).toBe(false)
  107. expect(fixture.shutdowns).toBe(1)
  108. expect(fixture.destroys).toBe(1)
  109. expect(ctx.tools.schemas().map(tool => tool.name)).toEqual(['unrelated'])
  110. } finally {
  111. settled.resolve(true)
  112. await result
  113. }
  114. expect((await result).isError).toBe(false)
  115. })
  116. it('retains the reservation until aborted calls and native shutdown settle', async () => {
  117. const called: PromiseWithResolvers<void> = Promise.withResolvers()
  118. const callSettled = Promise.withResolvers<unknown>()
  119. const shutdownStarted: PromiseWithResolvers<void> = Promise.withResolvers()
  120. const shutdownSettled: PromiseWithResolvers<void> = Promise.withResolvers()
  121. fixture.call = async () => {
  122. called.resolve()
  123. return callSettled.promise
  124. }
  125. fixture.shutdown = async () => {
  126. shutdownStarted.resolve()
  127. await shutdownSettled.promise
  128. }
  129. const fiber = ctx.plugin(NativeProvider)
  130. await fiber
  131. const result = execute('click', { pid: 9, window_id: 7 })
  132. await called.promise
  133. const disposal = fiber.dispose()
  134. try {
  135. await vi.waitFor(() => {
  136. expect(fixture.calls[0]?.signal?.aborted).toBe(true)
  137. expect(ctx.tools.schemas()).toEqual([])
  138. })
  139. expect(ctx.computerUse.providerName).toBe('cua-driver-native')
  140. expect(() => ctx.computerUse.register(ComputerUseProviderName('replacement'))).toThrow('already registered')
  141. callSettled.resolve({ content: [{ type: 'text', text: 'late native completion' }] })
  142. await shutdownStarted.promise
  143. expect(ctx.computerUse.providerName).toBe('cua-driver-native')
  144. expect(fixture.destroys).toBe(0)
  145. } finally {
  146. callSettled.resolve({ content: [] })
  147. shutdownSettled.resolve()
  148. await disposal
  149. }
  150. expect((await result).isError).toBe(true)
  151. expect(ctx.computerUse.providerName).toBeUndefined()
  152. expect(fixture.destroys).toBe(1)
  153. })
  154. it.each([['tools', ToolRuntime], ['systemPrompt', SystemPrompt]] as const)('closes the old native runtime before %s restarts it', async (_name, service) => {
  155. const shutdownStarted: PromiseWithResolvers<void> = Promise.withResolvers()
  156. const shutdownSettled: PromiseWithResolvers<void> = Promise.withResolvers()
  157. fixture.shutdown = async () => {
  158. shutdownStarted.resolve()
  159. await shutdownSettled.promise
  160. }
  161. const fiber = ctx.plugin(NativeProvider)
  162. await fiber
  163. const dependency = [...ctx.registry.get(service)!.fibers][0]!
  164. const restart = dependency.restart()
  165. try {
  166. await shutdownStarted.promise
  167. expect(fixture.creates).toBe(1)
  168. expect(fixture.destroys).toBe(0)
  169. expect(ctx.computerUse.providerName).toBe('cua-driver-native')
  170. } finally {
  171. shutdownSettled.resolve()
  172. await restart
  173. await fiber
  174. }
  175. await vi.waitFor(() => { expect(fixture.creates).toBe(2) })
  176. expect(fixture.shutdowns).toBe(1)
  177. expect(fixture.destroys).toBe(1)
  178. expect((await execute('check_permissions')).isError).toBe(false)
  179. await fiber.dispose()
  180. expect(fixture.shutdowns).toBe(2)
  181. expect(fixture.destroys).toBe(2)
  182. })
  183. it('interrupts discovery when unloaded during startup', async () => {
  184. const started: PromiseWithResolvers<void> = Promise.withResolvers()
  185. fixture.list = signal => new Promise((_resolve, reject) => {
  186. started.resolve()
  187. signal?.addEventListener('abort', () => { reject(new Error('Native discovery aborted')) }, { once: true })
  188. })
  189. const fiber = ctx.plugin(NativeProvider)
  190. const readiness = Promise.resolve(fiber).catch(() => undefined)
  191. await started.promise
  192. await fiber.dispose()
  193. await readiness
  194. expect(ctx.computerUse.providerName).toBeUndefined()
  195. expect(ctx.tools.schemas()).toEqual([])
  196. expect(fixture.shutdowns).toBe(1)
  197. expect(fixture.destroys).toBe(1)
  198. })
  199. it('keeps the reservation when native shutdown cannot prove completion', async () => {
  200. fixture.shutdown = async () => { throw new Error('Native shutdown failed') }
  201. const fiber = ctx.plugin(NativeProvider)
  202. await fiber
  203. await fiber.dispose()
  204. expect(ctx.tools.schemas()).toEqual([])
  205. expect(ctx.computerUse.providerName).toBe('cua-driver-native')
  206. expect(fixture.destroys).toBe(0)
  207. expect(() => ctx.computerUse.register(ComputerUseProviderName('replacement'))).toThrow('already registered')
  208. })
  209. })