threadViews.test.tsx4.1 KBView on GitHub import { DEFAULT_THREAD_ID } from '@/modules/cedar-os/src/store/messages/MessageTypes';
import type { MessageThread } from '@/modules/cedar-os/src/store/messages/MessageTypes';
import { useCedarStore } from '@/modules/cedar-os/src/store/CedarStore';
/**
* Phase 2 (design: chat-thread-store) — derived views + selection/navigation.
* All pure-over-threadMap; exercised headlessly against the real slice.
*/
const thread = (over: Partial<MessageThread> & Pick<MessageThread, 'id'>): MessageThread => ({
messages: [],
...over,
});
describe('agentThreadsSlice — derived views', () => {
beforeEach(() => {
useCedarStore.setState((state) => ({
...state,
threadMap: {
// A real, recently-active streaming thread
t1: thread({
id: 't1',
name: 'Adobe QBR',
messages: [{ id: 'm1', role: 'user', type: 'text', content: 'hi' }],
status: 'streaming',
lastActiveAt: '2026-07-06T10:00:00.000Z',
pinned: true,
}),
// A real, older idle thread
t2: thread({
id: 't2',
name: 'Numeral deal',
messages: [{ id: 'm2', role: 'user', type: 'text', content: 'yo' }],
status: 'idle',
lastActiveAt: '2026-07-03T10:00:00.000Z',
}),
// An empty placeholder — excluded from 'recent'
[DEFAULT_THREAD_ID]: thread({ id: DEFAULT_THREAD_ID, name: 'New chat' }),
},
mainThreadId: 't2',
activeThreadId: 't2',
messages: [],
}));
});
it("'recent' excludes placeholders and sorts by recency desc", () => {
const recent = useCedarStore.getState().getThreadsByView('recent');
expect(recent.map((t) => t.id)).toEqual(['t1', 't2']);
});
it("'active' returns only streaming threads", () => {
const active = useCedarStore.getState().getThreadsByView('active');
expect(active.map((t) => t.id)).toEqual(['t1']);
});
it("'pinned' returns only pinned threads", () => {
const pinned = useCedarStore.getState().getThreadsByView('pinned');
expect(pinned.map((t) => t.id)).toEqual(['t1']);
});
});
describe('agentThreadsSlice — selection + navigation', () => {
beforeEach(() => {
useCedarStore.setState((state) => ({
...state,
threadMap: {
t1: thread({
id: 't1',
name: 'A',
messages: [{ id: 'm1', role: 'user', type: 'text', content: 'a' }],
lastActiveAt: '2026-07-06T10:00:00.000Z',
}),
t2: thread({
id: 't2',
name: 'B',
messages: [{ id: 'm2', role: 'user', type: 'text', content: 'b' }],
lastActiveAt: '2026-07-05T10:00:00.000Z',
}),
t3: thread({
id: 't3',
name: 'C',
messages: [{ id: 'm3', role: 'user', type: 'text', content: 'c' }],
lastActiveAt: '2026-07-04T10:00:00.000Z',
}),
},
mainThreadId: 't2',
activeThreadId: 't2',
messages: [],
}));
});
it('selectThread updates both activeThreadId and mainThreadId', () => {
useCedarStore.getState().selectThread('t3');
expect(useCedarStore.getState().activeThreadId).toBe('t3');
expect(useCedarStore.getState().mainThreadId).toBe('t3');
});
it('navigateToNextChatThread moves down the recent view (t2 → t3)', () => {
// recent order: t1, t2, t3 (desc by lastActiveAt); active = t2
useCedarStore.getState().navigateToNextChatThread();
expect(useCedarStore.getState().activeThreadId).toBe('t3');
});
it('navigateToPreviousChatThread moves up the recent view (t2 → t1)', () => {
useCedarStore.getState().navigateToPreviousChatThread();
expect(useCedarStore.getState().activeThreadId).toBe('t1');
});
it('navigation clamps at the ends (no wrap)', () => {
useCedarStore.getState().selectThread('t1'); // first
useCedarStore.getState().navigateToPreviousChatThread();
expect(useCedarStore.getState().activeThreadId).toBe('t1');
useCedarStore.getState().selectThread('t3'); // last
useCedarStore.getState().navigateToNextChatThread();
expect(useCedarStore.getState().activeThreadId).toBe('t3');
});
});