draftChatOnArrival.test.ts3.8 KBView on GitHub
import { useCedarStore } from '@/modules/store';

/**
 * The DRAFT chat — the resting state of `/home` (and its `/agent` alias).
 *
 * Arriving on the agent home used to mint a chat thread: the route layout seeded one, the chat
 * panel seeded another, and the storage sync landed you in yesterday's chat if neither had won
 * yet. Every visit that went nowhere left an empty "New chat" behind in the rail.
 *
 * The rule now: nothing is active until something needs a thread. `?chat=` stays off the URL
 * (LayoutUrlSync writes `activeThreadId || null`), no row is written (the backend inserts the
 * thread with the first persisted message), and `ensureOpenChatThread` mints the id on first use.
 */
describe('the draft chat', () => {
  beforeEach(() => {
    useCedarStore.setState({ threadMap: {}, mainThreadId: '', activeThreadId: '', messages: [] });
  });

  it('hydrating chat history does not put you in one of those chats', async () => {
    useCedarStore.getState().setMessageStorageAdapter({
      type: 'custom',
      adapter: {
        async listThreads() {
          return [
            { id: 'yesterday', title: 'Adobe QBR', updatedAt: '2026-08-29T10:00:00Z' },
            { id: 'older', title: 'Renewal', updatedAt: '2026-08-28T10:00:00Z' },
          ];
        },
        async createThread(_userId, threadId, meta) {
          return { ...meta, id: threadId };
        },
        async loadMessages() {
          return { messages: [], hasMore: false };
        },
      },
    });
    useCedarStore.setState({ userId: 'user_1' });

    await useCedarStore.getState().initializeChat({ userId: 'user_1' });

    // The history is there to open from the rail…
    expect(Object.keys(useCedarStore.getState().threadMap).sort()).toEqual(['older', 'yesterday']);
    // …but you are in none of it.
    expect(useCedarStore.getState().mainThreadId).toBe('');
    expect(useCedarStore.getState().activeThreadId).toBe('');
  });

  it('mints a fresh id on first use, and leaves the chat you came from alone', () => {
    const store = useCedarStore.getState();
    const previous = store.createThread('previous', 'Adobe QBR');
    store.switchThread(previous);

    useCedarStore.getState().startFreshChat();
    expect(useCedarStore.getState().mainThreadId).toBe('');

    const minted = useCedarStore.getState().ensureOpenChatThread();
    expect(minted).not.toBe(previous);
    expect(useCedarStore.getState().mainThreadId).toBe(minted);
    expect(useCedarStore.getState().threadMap[minted]?.messages).toEqual([]);
    expect(useCedarStore.getState().threadMap[previous]).toBeDefined();
  });

  /**
   * An id still gets minted short of a message: opening a conversation needs a thread to hang
   * its display pointer on. So the pile-up this change exists to stop can come back the long
   * way round — home, open a deal, home, open another — unless stepping off an untouched chat
   * closes it. The rail and the tab strip are both views over `pinned`.
   */
  it('closes the untouched chat it steps off, and keeps the used one open', () => {
    const store = useCedarStore.getState();

    const used = store.createThread('used', 'Adobe QBR');
    store.switchThread(used);
    // What the chat column does for whatever thread is active (see EmbeddedCedarChat).
    store.pinThread(used);
    store.addMessage({ role: 'user', type: 'text', content: 'hello' }, true, used);
    useCedarStore.getState().startFreshChat();
    expect(useCedarStore.getState().threadMap[used]?.pinned).toBe(true);

    // Opening a deal from the draft mints + pins a chat that never gets a message.
    const untouched = useCedarStore.getState().ensureOpenChatThread();
    expect(useCedarStore.getState().threadMap[untouched]?.pinned).toBe(true);

    useCedarStore.getState().startFreshChat();
    expect(useCedarStore.getState().threadMap[untouched]?.pinned).toBe(false);
  });
});