route.host.spec.ts 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107
  1. import { Context } from '@deepseek-ai/cordis'
  2. import { HostConnectionService } from '@deepseek-ai/dsh-client-connection'
  3. import type { BrowserAuth } from '@deepseek-ai/dsh-client-connection/src/browser-auth.ts'
  4. import { SESSION_FORMAT_VERSION } from '@deepseek-ai/dsh-session'
  5. import type { SessionHeader, SessionId } from '@deepseek-ai/dsh-session'
  6. import type { SessionHandle } from '@deepseek-ai/dsh-session-persistence'
  7. import { strFromU8, unzipSync } from 'fflate'
  8. import { describe, expect, it } from 'vitest'
  9. import {
  10. Config,
  11. SESSION_LOG_FILENAME,
  12. SESSION_LOG_EXPORT_PATH,
  13. apply,
  14. inject,
  15. } from '../src/index.ts'
  16. const sid = (value: string): SessionId => value as SessionId
  17. function readHandle(id: string): SessionHandle {
  18. const header: SessionHeader = {
  19. version: SESSION_FORMAT_VERSION,
  20. id: sid(id),
  21. createdAt: 1,
  22. isSeeded: false,
  23. cwd: '/workspace',
  24. delegationDepth: 0,
  25. }
  26. return {
  27. id: header.id,
  28. header,
  29. access: 'read',
  30. read: async () => ({ eventState: 'detached', events: [] }),
  31. close: async () => {},
  32. } as unknown as SessionHandle
  33. }
  34. async function mounted(withServices: boolean): Promise<{
  35. readonly connection: HostConnectionService
  36. readonly dispose: () => Promise<void>
  37. }> {
  38. const ctx = new Context()
  39. ctx.provide('commands', { register: () => () => {} } as never)
  40. if (withServices) {
  41. ctx.provide('sessionQuery', {
  42. traceSession: async () => ({ descendants: [] }),
  43. } as never)
  44. ctx.provide('sessionPersistence', {
  45. stat: async (id: SessionId) => ({ header: readHandle(String(id)).header }),
  46. open: async (id: SessionId) => readHandle(String(id)),
  47. } as never)
  48. ctx.provide('attachments', {
  49. readImage: async () => { throw new Error('fixture has no images') },
  50. } as never)
  51. }
  52. const connection = new HostConnectionService(ctx, [], {} as BrowserAuth)
  53. const fiber = ctx.plugin({ inject: [...inject], apply })
  54. await fiber
  55. return { connection, dispose: () => fiber.dispose() }
  56. }
  57. describe('Session log export Fetch route', () => {
  58. it('registers one GET/HEAD route and removes it with the plugin fiber', async () => {
  59. const { connection, dispose } = await mounted(true)
  60. const shared = connection.createSharedFetchHandler('/api')
  61. const response = await shared.fetch(new Request(
  62. `http://host${SESSION_LOG_EXPORT_PATH}?sessionId=session-1`,
  63. ))
  64. expect(response.status).toBe(200)
  65. expect(response.headers.get('content-type')).toBe('application/zip')
  66. const files = unzipSync(new Uint8Array(await response.arrayBuffer()))
  67. expect(strFromU8(files[SESSION_LOG_FILENAME] as Uint8Array)).toContain('"id":"session-1"')
  68. const head = await shared.fetch(new Request(
  69. `http://host${SESSION_LOG_EXPORT_PATH}?sessionId=session-1`, { method: 'HEAD' },
  70. ))
  71. expect(head.status).toBe(200)
  72. expect(head.body).toBeNull()
  73. await dispose()
  74. expect((await shared.fetch(new Request(
  75. `http://host${SESSION_LOG_EXPORT_PATH}?sessionId=session-1`,
  76. ))).status).toBe(404)
  77. })
  78. it('validates the query before reporting missing export services', async () => {
  79. const { connection, dispose } = await mounted(false)
  80. const shared = connection.createSharedFetchHandler('/api')
  81. expect((await shared.fetch(new Request(`http://host${SESSION_LOG_EXPORT_PATH}`))).status).toBe(400)
  82. expect((await shared.fetch(new Request(
  83. `http://host${SESSION_LOG_EXPORT_PATH}?sessionId=session-1&includeDescendants=1`,
  84. ))).status).toBe(400)
  85. expect((await shared.fetch(new Request(
  86. `http://host${SESSION_LOG_EXPORT_PATH}?sessionId=session-1`,
  87. ))).status).toBe(500)
  88. await dispose()
  89. })
  90. it('validates the compression level', () => {
  91. expect(Config({})).toEqual({ compressionLevel: 6 })
  92. expect(Config({ compressionLevel: 0 })).toEqual({ compressionLevel: 0 })
  93. expect(Config({ compressionLevel: 9 })).toEqual({ compressionLevel: 9 })
  94. for (const compressionLevel of [-1, 10, 1.5]) {
  95. expect(() => Config({ compressionLevel } as never)).toThrow()
  96. }
  97. })
  98. })