chat-store.spec.ts 3.8 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394
  1. // @vitest-environment jsdom
  2. /**
  3. * createChatStore unit account (slot terminal design §4): the declared
  4. * actions write set, persist round-trip through the scope-suffixed key, and
  5. * factory purity (every create() is an independent instance; the factory
  6. * itself holds no singleton state).
  7. */
  8. import { beforeEach, describe, expect, it } from 'vitest'
  9. import { createChatStore } from '../src/client/stores.ts'
  10. const KEY = 'dsh.conversation.chat'
  11. beforeEach(() => {
  12. localStorage.clear()
  13. })
  14. describe('createChatStore', () => {
  15. it('init shape: empty selection/draft/view', () => {
  16. const store = createChatStore().create()
  17. expect(store.store.getSnapshot()).toEqual({ selection: null, draft: '', view: null })
  18. })
  19. it('actions cover the declared write set', () => {
  20. const store = createChatStore().create()
  21. store.actions.select({ turnSeq: 3, callId: 'c1', toolName: 'bash' })
  22. expect(store.store.getSnapshot().selection).toEqual({ turnSeq: 3, callId: 'c1', toolName: 'bash' })
  23. store.actions.select(null)
  24. expect(store.store.getSnapshot().selection).toBeNull()
  25. store.actions.setDraft('hello')
  26. expect(store.store.getSnapshot().draft).toBe('hello')
  27. store.actions.clearDraft()
  28. expect(store.store.getSnapshot().draft).toBe('')
  29. store.actions.setView('chat')
  30. expect(store.store.getSnapshot().view).toBe('chat')
  31. })
  32. it('restoreDraft only fills an empty draft (optimistic-send rollback contract)', () => {
  33. const store = createChatStore().create()
  34. // Rollback path: draft was cleared by send, nothing typed since.
  35. store.actions.restoreDraft('failed text')
  36. expect(store.store.getSnapshot().draft).toBe('failed text')
  37. // The user typed something new before the failure landed: keep theirs.
  38. store.actions.setDraft('newer input')
  39. store.actions.restoreDraft('stale text')
  40. expect(store.store.getSnapshot().draft).toBe('newer input')
  41. })
  42. it('persists per scope key and rehydrates a fresh instance', () => {
  43. const handle = createChatStore()
  44. const s1 = handle.create('sess-1')
  45. s1.actions.setDraft('draft for one')
  46. s1.actions.select({ turnSeq: 1 })
  47. // Scope-suffixed key: each session persists separately.
  48. expect(localStorage.getItem(`${KEY}.sess-1`)).not.toBeNull()
  49. expect(localStorage.getItem(`${KEY}.sess-2`)).toBeNull()
  50. // A rebuilt instance under the same scope key rehydrates the state.
  51. const again = createChatStore().create('sess-1')
  52. expect(again.store.getSnapshot().draft).toBe('draft for one')
  53. expect(again.store.getSnapshot().selection).toEqual({ turnSeq: 1 })
  54. // A sibling scope starts clean.
  55. const other = createChatStore().create('sess-2')
  56. expect(other.store.getSnapshot().draft).toBe('')
  57. })
  58. it('clearPersisted removes the scope entry (session-death cleanup hook)', () => {
  59. const store = createChatStore().create('sess-9')
  60. store.actions.setDraft('doomed')
  61. expect(localStorage.getItem(`${KEY}.sess-9`)).not.toBeNull()
  62. store.clearPersisted()
  63. expect(localStorage.getItem(`${KEY}.sess-9`)).toBeNull()
  64. })
  65. it('every create() is an independent instance; the factory holds no singleton', () => {
  66. const handle = createChatStore()
  67. const a = handle.create()
  68. const b = handle.create()
  69. a.actions.setDraft('only in a')
  70. expect(b.store.getSnapshot().draft).toBe('')
  71. // Two factory calls likewise share no LIVE state (identity is per handle
  72. // VALUE, not per module — the sharing contract lives in the framework's
  73. // handle x scope-key resolution, not in module state). Persistence is the
  74. // one sanctioned cross-instance channel: clear it so this assertion sees
  75. // memory identity, not rehydration (covered by the persist case above).
  76. localStorage.clear()
  77. const c = createChatStore().create()
  78. expect(c.store.getSnapshot().draft).toBe('')
  79. })
  80. })