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');
  });
});