Files
deepseek-harness/packages/client/ui-conversation/tests/chat-store.spec.ts
Yichen Jiang b599d1d0af Merge remote-tracking branch 'origin/master' into worktree/web-multimodal-image-input
# Conflicts:
#	.gitignore
#	apps/cli/package.json
#	apps/cli/src/web.ts
#	docs/architecture.i18n.yaml
#	docs/config-catalog.md
#	docs/module-graph.md
#	packages/client/connection/README.md
#	packages/client/connection/src/client/api.ts
#	packages/client/connection/src/client/fixture.ts
#	packages/client/runtime/README.md
#	packages/client/runtime/src/client/sessions/service.ts
#	packages/client/runtime/src/client/sessions/session.ts
#	packages/client/runtime/src/client/workspaces/service.ts
#	packages/client/ui-conversation/src/client/apply.ts
#	packages/client/ui-conversation/src/client/chat/MessageItem.tsx
#	packages/client/ui-conversation/src/client/contract/slots.ts
#	packages/client/ui-conversation/src/client/index.ts
#	packages/client/ui-conversation/src/client/service.ts
#	packages/client/ui-conversation/src/client/skeleton/ConversationRoot.tsx
#	packages/client/ui-conversation/src/client/skeleton/EmptyHero.tsx
#	packages/client/ui-conversation/src/client/skeleton/EmptyState.tsx
#	packages/client/ui-conversation/src/client/skeleton/InputBar.module.css
#	packages/client/ui-conversation/src/client/skeleton/InputBar.tsx
#	packages/client/ui-conversation/tests/apply-inject.spec.tsx
#	packages/client/ui-conversation/tests/input-bar.spec.tsx
#	packages/client/ui-conversation/tests/skeleton.spec.tsx
#	packages/client/ui-trajectory/tests/views.spec.tsx
#	packages/host/apiproxy/src/api-proxy.ts
#	pnpm-lock.yaml
2026-07-27 12:27:30 +08:00

77 lines
3.0 KiB
TypeScript

// @vitest-environment jsdom
/** Chat-store actions, scoped persistence, and instance isolation. */
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.setView('chat')
expect(store.store.getSnapshot().view).toBe('chat')
})
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('')
})
})