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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
  1. import { Context } from '@deepseek-ai/cordis'
  2. import AgentRegistry from '@deepseek-ai/dsh-agent'
  3. import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
  4. import { describe, expect, it, vi } from 'vitest'
  5. import {
  6. createSessionTestController,
  7. createSessionTestRemote,
  8. testSessionPersistence,
  9. } from './test-remote.ts'
  10. async function context(): Promise<Context> {
  11. const ctx = new Context()
  12. await ctx.plugin(SessionStore)
  13. await ctx.plugin(AgentRegistry)
  14. return ctx
  15. }
  16. describe('session/openWorkspacePath', () => {
  17. it('resolves a relative path against the attached Session cwd', async () => {
  18. const ctx = await context()
  19. const sessionId = SessionId('open-relative')
  20. ctx.sessions.create(sessionId, { meta: { cwd: '/workspace/project' } })
  21. const openPath = vi.fn((_path: string, _signal: AbortSignal) => Promise.resolve())
  22. const remote = createSessionTestRemote(ctx, {
  23. defaultModelSelection: () => ({ provider: 'p', model: 'm' }),
  24. cwd: '/default',
  25. openPath,
  26. })
  27. const signal = new AbortController().signal
  28. await expect(remote.openWorkspacePath({ sessionId, path: 'src/a.ts' }, signal))
  29. .resolves.toEqual({ ok: true, value: { opened: true } })
  30. expect(openPath).toHaveBeenCalledWith('/workspace/project/src/a.ts', signal)
  31. expect(ctx.agents.list()).toEqual([])
  32. })
  33. it('preserves absolute paths and cwd-less Session paths', async () => {
  34. const ctx = await context()
  35. const withCwd = SessionId('open-absolute')
  36. const withoutCwd = SessionId('open-without-cwd')
  37. ctx.sessions.create(withCwd, { meta: { cwd: '/workspace/project' } })
  38. ctx.sessions.create(withoutCwd)
  39. const openPath = vi.fn((_path: string, _signal: AbortSignal) => Promise.resolve())
  40. const remote = createSessionTestRemote(ctx, {
  41. defaultModelSelection: () => ({ provider: 'p', model: 'm' }),
  42. cwd: '/default',
  43. openPath,
  44. })
  45. await remote.openWorkspacePath({ sessionId: withCwd, path: '/tmp/result.html' })
  46. await remote.openWorkspacePath({ sessionId: withoutCwd, path: 'result.html' })
  47. expect(openPath.mock.calls.map(call => call[0])).toEqual(['/tmp/result.html', 'result.html'])
  48. })
  49. it('rejects empty paths and missing Sessions before opening anything', async () => {
  50. const ctx = await context()
  51. const sessionId = SessionId('open-validation')
  52. ctx.provide('sessionPersistence', testSessionPersistence(ctx, {
  53. list: () => Promise.resolve([]),
  54. inspect: () => Promise.resolve(undefined),
  55. }) as never)
  56. const openPath = vi.fn((_path: string, _signal: AbortSignal) => Promise.resolve())
  57. const remote = createSessionTestRemote(ctx, {
  58. defaultModelSelection: () => ({ provider: 'p', model: 'm' }),
  59. cwd: '/default',
  60. openPath,
  61. })
  62. await expect(remote.openWorkspacePath({ sessionId, path: '' }))
  63. .resolves.toMatchObject({ ok: false, error: { code: 'bad-request' } })
  64. await expect(remote.openWorkspacePath({ sessionId, path: 'result.html' }))
  65. .resolves.toMatchObject({ ok: false, error: { code: 'session-not-found' } })
  66. expect(openPath).not.toHaveBeenCalled()
  67. })
  68. it('preserves native opener failure and cancellation results', async () => {
  69. const ctx = await context()
  70. const sessionId = SessionId('open-failure')
  71. ctx.sessions.create(sessionId, { meta: { cwd: '/workspace/project' } })
  72. const openPath = vi.fn((_path: string, _signal: AbortSignal) =>
  73. Promise.reject(new Error('desktop unavailable')))
  74. const remote = createSessionTestRemote(ctx, {
  75. defaultModelSelection: () => ({ provider: 'p', model: 'm' }),
  76. cwd: '/default',
  77. openPath,
  78. })
  79. await expect(remote.openWorkspacePath({ sessionId, path: 'result.html' }))
  80. .resolves.toMatchObject({
  81. ok: false,
  82. error: { code: 'internal', message: 'path open failed: desktop unavailable' },
  83. })
  84. const aborted = new AbortController()
  85. aborted.abort(new Error('cancelled'))
  86. await expect(remote.openWorkspacePath({ sessionId, path: 'result.html' }, aborted.signal))
  87. .resolves.toMatchObject({ ok: false, error: { code: 'cancelled' } })
  88. })
  89. it('classifies inspection cancellation and non-session failures', async () => {
  90. const ctx = await context()
  91. const controller = createSessionTestController(ctx, {
  92. defaultModelSelection: () => ({ provider: 'p', model: 'm' }),
  93. cwd: '/default',
  94. })
  95. const inspect = vi.spyOn(controller, 'inspect')
  96. const aborted = new AbortController()
  97. inspect.mockImplementationOnce(async () => {
  98. aborted.abort(new Error('cancelled'))
  99. throw new Error('inspection stopped')
  100. })
  101. await expect(controller.openWorkspacePath({
  102. sessionId: SessionId('inspection-cancelled'), path: 'result.html',
  103. }, aborted.signal)).rejects.toMatchObject({ failure: { code: 'cancelled' } })
  104. inspect.mockRejectedValueOnce('storage offline')
  105. const failed = controller.openWorkspacePath({
  106. sessionId: SessionId('inspection-failed'), path: 'result.html',
  107. }, new AbortController().signal)
  108. await expect(failed).rejects.toMatchObject({ failure: { code: 'internal' } })
  109. await expect(failed).rejects.toThrow('storage offline')
  110. })
  111. it('classifies opener cancellation and non-Error failures', async () => {
  112. const ctx = await context()
  113. const sessionId = SessionId('open-error-kinds')
  114. ctx.sessions.create(sessionId, { meta: { cwd: '/workspace/project' } })
  115. const aborted = new AbortController()
  116. const openPath = vi.fn()
  117. .mockImplementationOnce(async () => {
  118. aborted.abort(new Error('cancelled'))
  119. throw new Error('opening stopped')
  120. })
  121. .mockRejectedValueOnce('desktop unavailable')
  122. const controller = createSessionTestController(ctx, {
  123. defaultModelSelection: () => ({ provider: 'p', model: 'm' }),
  124. cwd: '/default',
  125. openPath,
  126. })
  127. await expect(controller.openWorkspacePath({ sessionId, path: 'first.html' }, aborted.signal))
  128. .rejects.toMatchObject({ failure: { code: 'cancelled' } })
  129. await expect(controller.openWorkspacePath({
  130. sessionId, path: 'second.html',
  131. }, new AbortController().signal)).rejects.toMatchObject({
  132. failure: { code: 'internal', message: 'path open failed: desktop unavailable' },
  133. })
  134. })
  135. })