fix(ai-chat): disarm pending adoption on New chat/select; drop dead helper
Closes the 3rd PR #138 review. Warning fix: the render-phase reconciler only disarms the error-path adoption fallback when activeChatId actually changes. Pressing 'New chat' while ALREADY in a new chat keeps activeChatId === null (a no-op atom write), so the reconciler never fired and a stale armed fallback could adopt the just-failed chat from a late refetch, yanking the user out of their fresh chat. useChatSession now returns cancelPendingAdoption(); the window calls it from startNewChat AND selectChat. (The hook call moved above those callbacks so they can reference it.) Added a hook test that fails without the explicit disarm, plus a test for the existing-chat onTurnFinished branch (no adoption + per-chat invalidation). Cleanups: removed the dead pickNewlyCreatedChatId (the fallback effect uses newlyAddedChatIds directly with the 0/1/>1 decision inline) and its tests/doc mention; inlined the two invalidation closures (onTurnFinished is read live by useChat's onFinish, never in an effect dep array, so memoizing them was needless ceremony). Verified: tsc clean, 127 ai-chat tests green; live (z.ai glm-5.2) new chat + 2nd turn recalled the number in the SAME row (1 chat / 4 messages), no page errors. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -114,6 +114,37 @@ describe("useChatSession", () => {
|
||||
expect(setActiveChatId).not.toHaveBeenCalledWith("new");
|
||||
});
|
||||
|
||||
it("startNewChat while already in a new chat: cancelPendingAdoption stops a late refetch adopting the failed chat", () => {
|
||||
// The Warning path the render-phase reconciler can't catch: pressing "New
|
||||
// chat" while already in a new chat keeps activeChatId === null (a no-op for
|
||||
// the atom), so only the explicit cancelPendingAdoption() disarms.
|
||||
const { result, rerender, setActiveChatId } = setup({
|
||||
activeChatId: null,
|
||||
chats: { items: [{ id: "x" }] },
|
||||
});
|
||||
result.current.onTurnFinished(undefined); // first turn failed → arm (before=["x"])
|
||||
result.current.cancelPendingAdoption(); // window calls this from startNewChat
|
||||
// The just-failed row lands in a late refetch; it must NOT be adopted.
|
||||
rerender({
|
||||
activeChatId: null,
|
||||
chats: { items: [{ id: "x" }, { id: "failed" }] },
|
||||
});
|
||||
expect(setActiveChatId).not.toHaveBeenCalledWith("failed");
|
||||
});
|
||||
|
||||
it("onTurnFinished for an existing chat: no adoption, invalidates that chat's messages", () => {
|
||||
const {
|
||||
result,
|
||||
setActiveChatId,
|
||||
onInvalidateChatList,
|
||||
onInvalidateChatMessages,
|
||||
} = setup({ activeChatId: "chat-1", chats: { items: [{ id: "chat-1" }] } });
|
||||
result.current.onTurnFinished("chat-1");
|
||||
expect(setActiveChatId).not.toHaveBeenCalled(); // existing chat is never re-adopted
|
||||
expect(onInvalidateChatList).toHaveBeenCalled();
|
||||
expect(onInvalidateChatMessages).toHaveBeenCalledWith("chat-1");
|
||||
});
|
||||
|
||||
it("in-place adopt keeps threadKey stable; an external switch remounts", () => {
|
||||
const chats = { items: [{ id: "B" }] };
|
||||
const { result, rerender } = setup({ activeChatId: null, chats });
|
||||
|
||||
Reference in New Issue
Block a user