embeddedTabs.test.tsx3.6 KBView on GitHub import type { MessageThread } from '@/modules/cedar-os/src/store/messages/MessageTypes';
import { useCedarStore } from '@/modules/cedar-os/src/store/CedarStore';
/**
* Phase 4 (design: chat-thread-store) — EmbeddedCedarChat's tab bar is now derived
* purely from the store: openTabIds = getThreadsByView('pinned') ∪ activeThread, and
* per-tab processing/finished come from thread.status. Rendering the full 1300-line
* component in jsdom would need the entire tRPC/canvas/portal provider stack, so we
* assert the real derivation the component performs — the logic that actually changed.
*/
const thread = (over: Partial<MessageThread> & Pick<MessageThread, 'id'>): MessageThread => ({
messages: [],
...over,
});
// Mirrors EmbeddedCedarChat's openTabIds useMemo.
const deriveOpenTabIds = (pinnedIds: string[], mainThreadId: string): string[] => {
const ids = [...pinnedIds];
if (mainThreadId && !ids.includes(mainThreadId)) ids.push(mainThreadId);
return ids;
};
describe('EmbeddedCedarChat tab model — derived from views + status', () => {
beforeEach(() => {
useCedarStore.setState((state) => ({
...state,
threadMap: {
t1: thread({ id: 't1', name: 'Adobe', pinned: true, status: 'streaming' }),
t2: thread({ id: 't2', name: 'Numeral', pinned: true, status: 'finished' }),
// An unpinned, active thread — should still appear as a tab via the union.
t3: thread({ id: 't3', name: 'Scratch', status: 'idle' }),
},
mainThreadId: 't3',
activeThreadId: 't3',
messages: [],
}));
});
it('openTabIds = pinned view unioned with the active thread', () => {
const pinnedIds = useCedarStore.getState().getThreadsByView('pinned').map((t) => t.id);
const openTabIds = deriveOpenTabIds(pinnedIds, useCedarStore.getState().mainThreadId);
expect(openTabIds).toEqual(['t1', 't2', 't3']);
});
it('active pinned thread is not duplicated in the tab set', () => {
useCedarStore.getState().selectThread('t1'); // t1 is pinned + now active
const pinnedIds = useCedarStore.getState().getThreadsByView('pinned').map((t) => t.id);
const openTabIds = deriveOpenTabIds(pinnedIds, useCedarStore.getState().mainThreadId);
expect(openTabIds).toEqual(['t1', 't2']);
});
it('per-tab processing/finished markers come from thread.status (finished hidden on active tab)', () => {
const { threadMap, mainThreadId } = useCedarStore.getState();
const markerFor = (tid: string) => {
const status = threadMap[tid]?.status;
return {
isTabProcessing: status === 'streaming',
isTabFinished: status === 'finished' && tid !== mainThreadId,
};
};
// t1 streaming → processing
expect(markerFor('t1')).toEqual({ isTabProcessing: true, isTabFinished: false });
// t2 finished and NOT active → finished badge
expect(markerFor('t2')).toEqual({ isTabProcessing: false, isTabFinished: true });
// t3 idle active → neither
expect(markerFor('t3')).toEqual({ isTabProcessing: false, isTabFinished: false });
});
it('selecting a finished tab clears its badge (setThreadStatus idle)', () => {
// Simulate onValueChange: switch + clear finished
useCedarStore.getState().switchThread('t2');
useCedarStore.getState().setThreadStatus('t2', 'idle');
expect(useCedarStore.getState().threadMap['t2'].status).toBe('idle');
});
it('close tab = unpinThread (drops it from the pinned view)', () => {
useCedarStore.getState().unpinThread('t1');
const pinnedIds = useCedarStore.getState().getThreadsByView('pinned').map((t) => t.id);
expect(pinnedIds).toEqual(['t2']);
});
});