Files
gitmost/apps/client/src/features/ai-chat/utils/adopt-chat-id.test.ts
claude code agent 227 580f3442b8 fix(ai-chat): prevent duplicate chat row on first-turn error; add adoption tests
Addresses the PR #138 review.

Blocker 1 — duplicate chat row: a brand-new chat whose first turn errors BEFORE
the SSE 'start' chunk never receives the authoritative chatId, so metadata
adoption can't run; a retry then sent chatId:null and the server inserted a
SECOND chat row, orphaning the first turn. Keep metadata adoption as the primary
path (resolveAdoptedChatId) and add a bounded, unambiguous fallback: on a
new-chat finish with no server id, snapshot the known chat ids and, once the
list refetch lands, adopt the SINGLE newly-appeared id (pickNewlyCreatedChatId).
Zero or >1 new ids (e.g. two tabs racing) → no adoption — no items[0] guessing,
so #137 stays fixed. The wait-for-refetch guard compares set membership (robust
to a concurrent delete), and the diff dedupes so a repeated id from a paginated
list never reads as ambiguous.

Blocker 2 — tests: new adopt-chat-id.test.ts covers both pure helpers (adopt
decision + newly-created-id diff incl. dedupe/reorder); the server
messageMetadata callback is extracted to chatStreamStartMetadata and unit-tested
(start -> {chatId}, otherwise undefined).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 01:17:30 +03:00

44 lines
1.5 KiB
TypeScript

import { describe, it, expect } from "vitest";
import { resolveAdoptedChatId, pickNewlyCreatedChatId } from "./adopt-chat-id";
describe("resolveAdoptedChatId", () => {
it("adopts the server id for a brand-new chat (activeChatId null + id)", () => {
expect(resolveAdoptedChatId(null, "chat-1")).toBe("chat-1");
});
it("returns null for an existing chat even with a server id", () => {
expect(resolveAdoptedChatId("chat-existing", "chat-1")).toBeNull();
});
it("returns null for a new chat with no server id", () => {
expect(resolveAdoptedChatId(null, undefined)).toBeNull();
expect(resolveAdoptedChatId(null, null)).toBeNull();
});
});
describe("pickNewlyCreatedChatId", () => {
it("returns the single newly-appeared id", () => {
expect(pickNewlyCreatedChatId(["a", "b"], ["c", "a", "b"])).toBe("c");
});
it("returns null when no new id appeared", () => {
expect(pickNewlyCreatedChatId(["a", "b"], ["a", "b"])).toBeNull();
});
it("returns null when more than one new id appeared (ambiguous)", () => {
expect(pickNewlyCreatedChatId(["a"], ["a", "b", "c"])).toBeNull();
});
it("returns the single after id when before is empty", () => {
expect(pickNewlyCreatedChatId([], ["only"])).toBe("only");
});
it("treats a duplicated new id as one (deduped, not ambiguous)", () => {
expect(pickNewlyCreatedChatId(["a"], ["a", "new", "new"])).toBe("new");
});
it("returns null when membership is unchanged but reordered", () => {
expect(pickNewlyCreatedChatId(["a", "b"], ["b", "a"])).toBeNull();
});
});