| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394 |
- // @vitest-environment jsdom
- /**
- * createChatStore unit account (slot terminal design §4): the declared
- * actions write set, persist round-trip through the scope-suffixed key, and
- * factory purity (every create() is an independent instance; the factory
- * itself holds no singleton state).
- */
- import { beforeEach, describe, expect, it } from 'vitest'
- import { createChatStore } from '../src/client/stores.ts'
- const KEY = 'dsh.conversation.chat'
- beforeEach(() => {
- localStorage.clear()
- })
- describe('createChatStore', () => {
- it('init shape: empty selection/draft/view', () => {
- const store = createChatStore().create()
- expect(store.store.getSnapshot()).toEqual({ selection: null, draft: '', view: null })
- })
- it('actions cover the declared write set', () => {
- const store = createChatStore().create()
- store.actions.select({ turnSeq: 3, callId: 'c1', toolName: 'bash' })
- expect(store.store.getSnapshot().selection).toEqual({ turnSeq: 3, callId: 'c1', toolName: 'bash' })
- store.actions.select(null)
- expect(store.store.getSnapshot().selection).toBeNull()
- store.actions.setDraft('hello')
- expect(store.store.getSnapshot().draft).toBe('hello')
- store.actions.clearDraft()
- expect(store.store.getSnapshot().draft).toBe('')
- store.actions.setView('chat')
- expect(store.store.getSnapshot().view).toBe('chat')
- })
- it('restoreDraft only fills an empty draft (optimistic-send rollback contract)', () => {
- const store = createChatStore().create()
- // Rollback path: draft was cleared by send, nothing typed since.
- store.actions.restoreDraft('failed text')
- expect(store.store.getSnapshot().draft).toBe('failed text')
- // The user typed something new before the failure landed: keep theirs.
- store.actions.setDraft('newer input')
- store.actions.restoreDraft('stale text')
- expect(store.store.getSnapshot().draft).toBe('newer input')
- })
- it('persists per scope key and rehydrates a fresh instance', () => {
- const handle = createChatStore()
- const s1 = handle.create('sess-1')
- s1.actions.setDraft('draft for one')
- s1.actions.select({ turnSeq: 1 })
- // Scope-suffixed key: each session persists separately.
- expect(localStorage.getItem(`${KEY}.sess-1`)).not.toBeNull()
- expect(localStorage.getItem(`${KEY}.sess-2`)).toBeNull()
- // A rebuilt instance under the same scope key rehydrates the state.
- const again = createChatStore().create('sess-1')
- expect(again.store.getSnapshot().draft).toBe('draft for one')
- expect(again.store.getSnapshot().selection).toEqual({ turnSeq: 1 })
- // A sibling scope starts clean.
- const other = createChatStore().create('sess-2')
- expect(other.store.getSnapshot().draft).toBe('')
- })
- it('clearPersisted removes the scope entry (session-death cleanup hook)', () => {
- const store = createChatStore().create('sess-9')
- store.actions.setDraft('doomed')
- expect(localStorage.getItem(`${KEY}.sess-9`)).not.toBeNull()
- store.clearPersisted()
- expect(localStorage.getItem(`${KEY}.sess-9`)).toBeNull()
- })
- it('every create() is an independent instance; the factory holds no singleton', () => {
- const handle = createChatStore()
- const a = handle.create()
- const b = handle.create()
- a.actions.setDraft('only in a')
- expect(b.store.getSnapshot().draft).toBe('')
- // Two factory calls likewise share no LIVE state (identity is per handle
- // VALUE, not per module — the sharing contract lives in the framework's
- // handle x scope-key resolution, not in module state). Persistence is the
- // one sanctioned cross-instance channel: clear it so this assertion sees
- // memory identity, not rehydration (covered by the persist case above).
- localStorage.clear()
- const c = createChatStore().create()
- expect(c.store.getSnapshot().draft).toBe('')
- })
- })
|