persistence.spec.ts 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990
  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 JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
  9. import SqliteSessionPersistence from '@deepseek-ai/dsh-session-persistence-sqlite'
  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. session.append('turn/start', {
  23. turn: 1,
  24. })
  25. session.append('user/message', createUserMessage({
  26. content: [{ type: 'text', text: 'Persist this session title' }],
  27. source: { kind: 'user' },
  28. }), { surfaceOp: 'append' })
  29. session.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
  30. await ctx.sessionTitle.refresh(session)
  31. }
  32. async function expectPersistedTitle(ctx: Context, id: ReturnType<typeof SessionId>): Promise<void> {
  33. const loaded = await ctx.sessionPersistence.load(id)
  34. expect(foldSessionTitle(loaded.events)).toMatchObject({
  35. title: 'Persist this session title',
  36. messageSeqs: [1],
  37. source: { kind: 'fallback' },
  38. eventSeq: 3,
  39. })
  40. expect(loaded.events.map(event => event.type)).toEqual([
  41. 'turn/start',
  42. 'user/message',
  43. 'turn/end',
  44. 'session/title',
  45. ])
  46. }
  47. describe('session title persistence round trips', () => {
  48. it('round-trips through a remounted JSONL backend', async () => {
  49. const root = await mkdtemp(join(tmpdir(), 'dsh-title-jsonl-'))
  50. roots.push(root)
  51. const id = SessionId('title-jsonl')
  52. const writer = new Context()
  53. await writer.plugin(SessionStore)
  54. await writer.plugin(JsonlSessionPersistence, { root, compression: 'none' })
  55. await writer.plugin(SessionTitleService, CONFIG)
  56. await appendPersistedTitle(writer, id)
  57. await writer.fiber.dispose()
  58. const reader = new Context()
  59. await reader.plugin(SessionStore)
  60. await reader.plugin(JsonlSessionPersistence, { root, compression: 'none' })
  61. await expectPersistedTitle(reader, id)
  62. await reader.fiber.dispose()
  63. })
  64. it('round-trips through a remounted SQLite backend', async () => {
  65. const root = await mkdtemp(join(tmpdir(), 'dsh-title-sqlite-'))
  66. roots.push(root)
  67. const path = join(root, 'sessions.db')
  68. const id = SessionId('title-sqlite')
  69. const writer = new Context()
  70. await writer.plugin(SessionStore)
  71. await writer.plugin(SqliteSessionPersistence, { path })
  72. await writer.plugin(SessionTitleService, CONFIG)
  73. await appendPersistedTitle(writer, id)
  74. await writer.fiber.dispose()
  75. const reader = new Context()
  76. await reader.plugin(SessionStore)
  77. await reader.plugin(SqliteSessionPersistence, { path })
  78. await expectPersistedTitle(reader, id)
  79. await reader.fiber.dispose()
  80. })
  81. })