session-open-workspace-path.host.spec.ts 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  1. import { Context } from '@deepseek-ai/cordis'
  2. import AgentRegistry from '@deepseek-ai/dsh-agent'
  3. import SessionStore from '@deepseek-ai/dsh-session'
  4. import { describe, expect, it, vi } from 'vitest'
  5. import {
  6. createSessionTestController,
  7. createSessionTestRemote,
  8. } from './test-remote.ts'
  9. async function context(): Promise<Context> {
  10. const ctx = new Context()
  11. await ctx.plugin(SessionStore)
  12. await ctx.plugin(AgentRegistry)
  13. return ctx
  14. }
  15. describe('session/openWorkspacePath', () => {
  16. it('reports the deployment opener capability independently of a Session', async () => {
  17. const ctx = await context()
  18. const remote = createSessionTestRemote(ctx, {
  19. defaultModelSelection: () => ({ provider: 'p', model: 'm' }),
  20. cwd: '/default',
  21. canOpenPath: () => false,
  22. })
  23. await expect(remote.canOpenWorkspacePath()).resolves.toEqual({ ok: true, value: false })
  24. })
  25. it('derives opener availability from config, an injected opener, or the platform probe', async () => {
  26. const configured = createSessionTestRemote(await context(), {
  27. defaultModelSelection: () => ({ provider: 'p', model: 'm' }),
  28. cwd: '/default',
  29. nativeOpen: false,
  30. })
  31. await expect(configured.canOpenWorkspacePath()).resolves.toEqual({ ok: true, value: false })
  32. const injected = createSessionTestRemote(await context(), {
  33. defaultModelSelection: () => ({ provider: 'p', model: 'm' }),
  34. cwd: '/default',
  35. openPath: () => Promise.resolve(),
  36. })
  37. await expect(injected.canOpenWorkspacePath()).resolves.toEqual({ ok: true, value: true })
  38. const detected = createSessionTestRemote(await context(), {
  39. defaultModelSelection: () => ({ provider: 'p', model: 'm' }),
  40. cwd: '/default',
  41. })
  42. await expect(detected.canOpenWorkspacePath()).resolves.toMatchObject({ ok: true })
  43. })
  44. it('hands a Client-resolved workspace path to the Host opener unchanged', async () => {
  45. const ctx = await context()
  46. const openPath = vi.fn((_path: string, _signal: AbortSignal) => Promise.resolve())
  47. const remote = createSessionTestRemote(ctx, {
  48. defaultModelSelection: () => ({ provider: 'p', model: 'm' }),
  49. cwd: '/default',
  50. openPath,
  51. })
  52. const signal = new AbortController().signal
  53. await expect(remote.openWorkspacePath({ path: '/workspace/project/src/a.ts' }, signal))
  54. .resolves.toEqual({ ok: true, value: { opened: true } })
  55. expect(openPath).toHaveBeenCalledWith('/workspace/project/src/a.ts', signal)
  56. expect(ctx.agents.list()).toEqual([])
  57. })
  58. it('preserves relative and absolute Host-resolvable paths', async () => {
  59. const ctx = await context()
  60. const openPath = vi.fn((_path: string, _signal: AbortSignal) => Promise.resolve())
  61. const remote = createSessionTestRemote(ctx, {
  62. defaultModelSelection: () => ({ provider: 'p', model: 'm' }),
  63. cwd: '/default',
  64. openPath,
  65. })
  66. await remote.openWorkspacePath({ path: '/tmp/result.html' })
  67. await remote.openWorkspacePath({ path: 'result.html' })
  68. expect(openPath.mock.calls.map(call => call[0])).toEqual(['/tmp/result.html', 'result.html'])
  69. })
  70. it('rejects empty paths before opening anything', async () => {
  71. const ctx = await context()
  72. const openPath = vi.fn((_path: string, _signal: AbortSignal) => Promise.resolve())
  73. const remote = createSessionTestRemote(ctx, {
  74. defaultModelSelection: () => ({ provider: 'p', model: 'm' }),
  75. cwd: '/default',
  76. openPath,
  77. })
  78. await expect(remote.openWorkspacePath({ path: '' }))
  79. .resolves.toMatchObject({ ok: false, error: { code: 'gateway/bad-request' } })
  80. expect(openPath).not.toHaveBeenCalled()
  81. })
  82. it('preserves native opener failure and cancellation results', async () => {
  83. const ctx = await context()
  84. const openPath = vi.fn((_path: string, _signal: AbortSignal) =>
  85. Promise.reject(new Error('desktop unavailable')))
  86. const remote = createSessionTestRemote(ctx, {
  87. defaultModelSelection: () => ({ provider: 'p', model: 'm' }),
  88. cwd: '/default',
  89. openPath,
  90. })
  91. await expect(remote.openWorkspacePath({ path: 'result.html' }))
  92. .resolves.toMatchObject({
  93. ok: false,
  94. error: { code: 'gateway/internal', message: 'path open failed: desktop unavailable' },
  95. })
  96. const aborted = new AbortController()
  97. aborted.abort(new Error('gateway/cancelled'))
  98. await expect(remote.openWorkspacePath({ path: 'result.html' }, aborted.signal))
  99. .resolves.toMatchObject({ ok: false, error: { code: 'gateway/cancelled' } })
  100. })
  101. it('classifies opener cancellation and non-Error failures', async () => {
  102. const ctx = await context()
  103. const aborted = new AbortController()
  104. const openPath = vi.fn()
  105. .mockImplementationOnce(async () => {
  106. aborted.abort(new Error('gateway/cancelled'))
  107. throw new Error('opening stopped')
  108. })
  109. .mockRejectedValueOnce('desktop unavailable')
  110. const controller = createSessionTestController(ctx, {
  111. defaultModelSelection: () => ({ provider: 'p', model: 'm' }),
  112. cwd: '/default',
  113. openPath,
  114. })
  115. await expect(controller.openWorkspacePath({ path: 'first.html' }, aborted.signal))
  116. .rejects.toMatchObject({ code: 'gateway/cancelled' })
  117. await expect(controller.openWorkspacePath({
  118. path: 'second.html',
  119. }, new AbortController().signal)).rejects.toMatchObject({
  120. code: 'gateway/internal', message: 'path open failed: desktop unavailable',
  121. })
  122. })
  123. })