persistence.spec.ts 2.8 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283
  1. import { createUserMessage } from '@deepseek-ai/dsh-llm'
  2. import { afterEach, describe, expect, it } from 'vitest'
  3. import { Context } from '@deepseek-ai/cordis'
  4. import { mkdtemp, rm } from 'node:fs/promises'
  5. import { tmpdir } from 'node:os'
  6. import { join } from 'node:path'
  7. import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
  8. import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
  9. import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
  10. import SessionTitleService, { foldSessionTitle } from '@deepseek-ai/dsh-session-title'
  11. const CONFIG = {
  12. fallbackMaxWords: 5,
  13. fallbackMaxBytes: 40,
  14. maxTitleBytes: 80,
  15. } as const
  16. const roots: string[] = []
  17. afterEach(async () => {
  18. for (const root of roots.splice(0)) await rm(root, { recursive: true, force: true })
  19. })
  20. async function appendPersistedTitle(ctx: Context, id: ReturnType<typeof SessionId>): Promise<void> {
  21. const session = ctx.sessions.create(id)
  22. const handle = await ctx.sessionPersistence.create(session.header)
  23. try {
  24. session.append('turn/start', {
  25. turn: 1,
  26. })
  27. session.append('user/message', createUserMessage({
  28. content: [{ type: 'text', text: 'Persist this session title' }],
  29. source: { kind: 'user' },
  30. }), { surfaceOp: 'append' })
  31. session.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
  32. await ctx.sessionTitle.refresh(session)
  33. } finally {
  34. await handle.close()
  35. }
  36. }
  37. async function expectPersistedTitle(ctx: Context, id: ReturnType<typeof SessionId>): Promise<void> {
  38. const handle = await ctx.sessionPersistence.open(id, 'read')
  39. try {
  40. const { events } = await handle.read()
  41. expect(foldSessionTitle(events)).toMatchObject({
  42. title: 'Persist this session title',
  43. messageSeqs: [1],
  44. source: { kind: 'fallback' },
  45. eventSeq: 3,
  46. })
  47. expect(events.map(event => event.type)).toEqual([
  48. 'turn/start',
  49. 'user/message',
  50. 'turn/end',
  51. 'session/title',
  52. ])
  53. } finally {
  54. await handle.close()
  55. }
  56. }
  57. describe('session title persistence round trips', () => {
  58. it('round-trips through a remounted JSONL backend', async () => {
  59. const root = await mkdtemp(join(tmpdir(), 'dsh-title-jsonl-'))
  60. roots.push(root)
  61. const id = SessionId('title-jsonl')
  62. const writer = new Context()
  63. await writer.plugin(SessionStore)
  64. await writer.plugin(SessionProjectionRegistry)
  65. await writer.plugin(JsonlSessionPersistence, { root, compression: 'none' })
  66. await writer.plugin(SessionTitleService, CONFIG)
  67. await appendPersistedTitle(writer, id)
  68. await writer.fiber.dispose()
  69. const reader = new Context()
  70. await reader.plugin(SessionStore)
  71. await reader.plugin(SessionProjectionRegistry)
  72. await reader.plugin(JsonlSessionPersistence, { root, compression: 'none' })
  73. await expectPersistedTitle(reader, id)
  74. await reader.fiber.dispose()
  75. })
  76. })