projection.spec.ts 3.6 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576
  1. /**
  2. * The `title` projection unit: mounting the title service beside the
  3. * projection registry serves the current normalized title (last-wins over
  4. * session/title events, the same events foldSessionTitle consumes) — null
  5. * before the first title — through the registry snapshot and the change
  6. * feed; compositions without the registry are unaffected; unmounting the
  7. * service removes the key (HMR safety). The bespoke session/title mux frame
  8. * is untouched by this unit (its retirement is the client value-store
  9. * migration's concern).
  10. */
  11. import { describe, expect, it } from 'vitest'
  12. import { Context } from '@deepseek-ai/cordis'
  13. import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
  14. import type { Session } from '@deepseek-ai/dsh-session'
  15. import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
  16. import SessionTitleService from '@deepseek-ai/dsh-session-title'
  17. const CONFIG = { fallbackMaxWords: 8, fallbackMaxBytes: 64, maxTitleBytes: 256 }
  18. async function harness(withTitleService: boolean): Promise<{ ctx: Context; session: Session }> {
  19. const ctx = new Context()
  20. await ctx.plugin(SessionStore)
  21. await ctx.plugin(SessionProjectionRegistry)
  22. if (withTitleService) await ctx.plugin(SessionTitleService, CONFIG)
  23. return { ctx, session: ctx.sessions.create(SessionId('titled')) }
  24. }
  25. /** Append one session/title event directly (the replay-plane shape the unit folds). */
  26. function appendTitle(session: Session, title: string): number {
  27. return session.append('session/title', { title, messageSeqs: [1], source: { kind: 'fallback' } }).seq
  28. }
  29. describe('title projection unit', () => {
  30. it('serves null before the first title event', async () => {
  31. const { ctx, session } = await harness(true)
  32. const snapshot = ctx.sessionProjections.snapshot(session)
  33. expect(snapshot.values.title).toBeNull()
  34. })
  35. it('serves the latest title last-wins and notifies the change feed with the causing seq', async () => {
  36. const { ctx, session } = await harness(true)
  37. const changes: { key: string; value: unknown; seq: number }[] = []
  38. ctx.sessionProjections.onChanged((_session, key, value, seq) => {
  39. changes.push({ key, value, seq })
  40. })
  41. const firstSeq = appendTitle(session, 'First title')
  42. const secondSeq = appendTitle(session, 'Second title')
  43. // Unrelated event: same-reference apply, no notification.
  44. session.append('turn/start', { turn: 1 })
  45. expect(changes).toEqual([
  46. { key: 'title', value: 'First title', seq: firstSeq },
  47. { key: 'title', value: 'Second title', seq: secondSeq },
  48. ])
  49. const snapshot = ctx.sessionProjections.snapshot(session)
  50. expect(snapshot.values.title).toBe('Second title')
  51. expect(snapshot.asOfSeq).toBe(session.seq - 1)
  52. })
  53. it('folds titles already in the log when the service mounts late (lazy cell build)', async () => {
  54. const { ctx, session } = await harness(false)
  55. appendTitle(session, 'Pre-mount title')
  56. await ctx.plugin(SessionTitleService, CONFIG)
  57. expect(ctx.sessionProjections.snapshot(session).values.title).toBe('Pre-mount title')
  58. })
  59. it('has no title key without the title service, and drops it when the service unloads (HMR safety)', async () => {
  60. const { ctx, session } = await harness(false)
  61. expect('title' in ctx.sessionProjections.snapshot(session).values).toBe(false)
  62. const fiber = await ctx.plugin(SessionTitleService, CONFIG)
  63. appendTitle(session, 'Ephemeral')
  64. expect(ctx.sessionProjections.snapshot(session).values.title).toBe('Ephemeral')
  65. await fiber.dispose()
  66. expect('title' in ctx.sessionProjections.snapshot(session).values).toBe(false)
  67. })
  68. })