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