api-proxy-host.spec.ts 2.9 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576
  1. import { homedir } from 'node:os'
  2. import { afterEach, describe, expect, it } from 'vitest'
  3. import { Context } from '@deepseek-ai/cordis'
  4. import AgentRegistry from '@deepseek-ai/dsh-agent'
  5. import type { RpcRequest } from '@deepseek-ai/dsh-host-apiproxy/api/rpc'
  6. import { RpcId } from '@deepseek-ai/dsh-host-apiproxy/api/rpc'
  7. import { createApiProxy } from '../src/api-proxy.ts'
  8. let nextRpc = 1
  9. const contexts: Context[] = []
  10. afterEach(async () => {
  11. await Promise.all(contexts.splice(0).map(ctx => ctx.fiber.dispose()))
  12. })
  13. function request<P>(payload: P): RpcRequest<P> {
  14. return { rpcId: RpcId(`host-${String(nextRpc++)}`), payload }
  15. }
  16. function expectOk<T>(response: { readonly result: { readonly ok: true; readonly value: T } | { readonly ok: false } }): T {
  17. expect(response.result.ok).toBe(true)
  18. if (!response.result.ok) throw new Error('unreachable')
  19. return response.result.value
  20. }
  21. async function harness(
  22. extras: {
  23. openPath?: (path: string, signal: AbortSignal) => Promise<void>
  24. canOpenPath?: () => boolean
  25. } = {},
  26. ) {
  27. const ctx = new Context()
  28. contexts.push(ctx)
  29. await ctx.plugin(AgentRegistry)
  30. const api = createApiProxy(ctx, {
  31. defaultModelSelection: () => ({ provider: 'test', model: 'test-model' }),
  32. cwd: '/tmp/dsh-apiproxy-host',
  33. ...extras.openPath === undefined ? {} : { openPath: extras.openPath },
  34. ...extras.canOpenPath === undefined ? {} : { canOpenPath: extras.canOpenPath },
  35. })
  36. return { api }
  37. }
  38. describe('host.openPath', () => {
  39. it('describes whether the deployment can reach a native desktop', async () => {
  40. const visible = await harness({ canOpenPath: () => true })
  41. const headless = await harness({ canOpenPath: () => false })
  42. expect(expectOk(await visible.api.host.describe(request({}))).canOpenPath).toBe(true)
  43. expect(expectOk(await headless.api.host.describe(request({}))).canOpenPath).toBe(false)
  44. expect(expectOk(await visible.api.host.describe(request({}))).home).toBe(homedir())
  45. })
  46. it('opens through the injected native boundary', async () => {
  47. const opened: string[] = []
  48. const { api } = await harness({
  49. openPath: async (path) => { opened.push(path) },
  50. })
  51. expect((await api.host.openPath(
  52. request({ path: '/tmp/a.txt' }),
  53. new AbortController().signal,
  54. )).result).toEqual({ ok: true, value: { opened: true } })
  55. expect(opened).toEqual(['/tmp/a.txt'])
  56. })
  57. it('propagates abort into the native boundary as a cancelled RPC error', async () => {
  58. const { api } = await harness({
  59. openPath: (_path, signal) => new Promise((_resolve, reject) => {
  60. signal.addEventListener('abort', () => { reject(new Error('aborted')) }, { once: true })
  61. }),
  62. })
  63. const abort = new AbortController()
  64. const pending = api.host.openPath(request({ path: '/tmp/a.txt' }), abort.signal)
  65. abort.abort()
  66. expect((await pending).result).toMatchObject({ ok: false, error: { code: 'cancelled' } })
  67. })
  68. })