Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
167 changes: 167 additions & 0 deletions site/src/api/queries/chats.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,18 +58,21 @@ import {
promoteChatQueuedMessage,
proposeChatTitle,
removeChatEntity,
removeChatFromChatsByWorkspace,
removeChildFromParentInCache,
reorderPinnedChat,
setChatGroupRole,
setChatUserRole,
shouldInvalidateChatSearches,
shouldInvalidateChatsByWorkspace,
TERMINAL_RUN_STATUSES,
toChatListParams,
unarchiveChat,
unpinChat,
updateChatAdvisorConfig,
updateChatPlanMode,
updateChatTitle,
updateChatWorkspace,
updateChildInParentCache,
updateInfiniteChatsCache,
} from "./chats";
Expand Down Expand Up @@ -1607,6 +1610,97 @@ describe("mutation invalidation scope", () => {
"chat search entry should be invalidated",
).toBe(true);
});

it.each<{
name: string;
settle: (queryClient: QueryClient) => unknown;
}>([
{
name: "archiveChat onSettled",
settle: (queryClient) =>
archiveChat(queryClient).onSettled(undefined, undefined, "chat-1"),
},
{
name: "unarchiveChat onSettled",
settle: (queryClient) =>
unarchiveChat(queryClient).onSettled(undefined, undefined, "chat-1"),
},
{
name: "updateChatWorkspace onSettled",
settle: (queryClient) =>
updateChatWorkspace(queryClient).onSettled(undefined, undefined, {
chatId: "chat-1",
workspaceId: "ws-1",
}),
},
{
name: "createChat onSuccess",
settle: (queryClient) => createChat(queryClient).onSuccess(),
},
])("$name invalidates chats by workspace", async ({ settle }) => {
const queryClient = createTestQueryClient();
queryClient.setQueryData(chatsByWorkspace(["ws-1"]).queryKey, {
"ws-1": "chat-1",
});

settle(queryClient);
await new Promise((r) => setTimeout(r, 0));

expect(
queryClient.getQueryState(chatsByWorkspace(["ws-1"]).queryKey)
?.isInvalidated,
"by-workspace entry should be invalidated",
).toBe(true);
});

it("archiveChat onSuccess synchronously removes the chat's by-workspace mappings", () => {
const queryClient = createTestQueryClient();
queryClient.setQueryData(chatsByWorkspace(["ws-1"]).queryKey, {
"ws-1": "chat-1",
"ws-2": "chat-2",
});

archiveChat(queryClient).onSuccess(undefined, "chat-1");

// Assert before any timer flush: the archived chat must be gone
// from the mapping without waiting for the onSettled refetch.
expect(
queryClient.getQueryData(chatsByWorkspace(["ws-1"]).queryKey),
).toEqual({ "ws-2": "chat-2" });
});

it.each<{
name: string;
settle: (queryClient: QueryClient) => unknown;
}>([
{
name: "updateChatTitle onSettled",
settle: (queryClient) =>
updateChatTitle(queryClient).onSettled(undefined, undefined, {
chatId: "chat-1",
title: "New",
}),
},
{
name: "createChatMessage onSuccess",
settle: (queryClient) =>
createChatMessage(queryClient, "chat-1").onSuccess?.(),
},
])("$name does not invalidate chats by workspace", async ({ settle }) => {
const queryClient = createTestQueryClient();
queryClient.setQueryData(chatsByWorkspace(["ws-1"]).queryKey, {
"ws-1": "chat-1",
});

settle(queryClient);
await new Promise((r) => setTimeout(r, 0));

expect(
queryClient.getQueryState(chatsByWorkspace(["ws-1"]).queryKey)
?.isInvalidated,
"by-workspace entry should NOT be invalidated",
).not.toBe(true);
});
});

describe("chatListKey shape", () => {
Expand Down Expand Up @@ -3088,6 +3182,26 @@ describe("semantic cache operations: prefix invalidations", () => {
).not.toBe(true);
});

describe(shouldInvalidateChatsByWorkspace.name, () => {
// created/deleted have their own watch branches; title, summary,
// diff, and context events do not move updated_at ordering.
const expectedByKind: Record<TypesGen.ChatWatchEventKind, boolean> = {
action_required: true,
chat_summary_change: false,
context_dirty: false,
created: false,
deleted: false,
diff_status_change: false,
status_change: true,
summary_change: false,
title_change: false,
};

it.each(ChatWatchEventKinds)("%s", (kind) => {
expect(shouldInvalidateChatsByWorkspace(kind)).toBe(expectedByKind[kind]);
});
});

it("invalidateChatDebugRuns touches the runs list and run details only", async () => {
const queryClient = createTestQueryClient();
queryClient.setQueryData(chatDebugRunsKey("chat-1"), []);
Expand Down Expand Up @@ -3291,4 +3405,57 @@ describe("semantic cache operations: removal and patching", () => {

expect(queryClient.getQueryData(chatMessagesKey("chat-1"))).toBe(before);
});

it("removeChatFromChatsByWorkspace removes only mappings pointing at the chat", () => {
const queryClient = createTestQueryClient();
queryClient.setQueryData(chatsByWorkspace(["ws-1"]).queryKey, {
"ws-1": "chat-1",
"ws-2": "chat-2",
});
queryClient.setQueryData(chatsByWorkspace(["ws-3"]).queryKey, {
"ws-3": "chat-1",
});
seedInfiniteChats(queryClient, [makeChat("chat-1")]);
queryClient.setQueryData(chatEntityKey("chat-1"), makeChat("chat-1"));
queryClient.setQueryData(chatSearch({ q: "alpha" }).queryKey, []);

removeChatFromChatsByWorkspace(queryClient, "chat-1");

expect(
queryClient.getQueryData(chatsByWorkspace(["ws-1"]).queryKey),
).toEqual({ "ws-2": "chat-2" });
expect(
queryClient.getQueryData(chatsByWorkspace(["ws-3"]).queryKey),
).toEqual({});
for (const [label, key] of [
["chat list", infiniteChatsTestKey],
["chat detail", chatEntityKey("chat-1")],
["chat search", chatSearch({ q: "alpha" }).queryKey],
] as const) {
expect(
queryClient.getQueryData(key),
`${label} entry should survive removeChatFromChatsByWorkspace`,
).toBeDefined();
expect(
queryClient.getQueryState(key)?.isInvalidated,
`${label} entry should NOT be invalidated`,
).not.toBe(true);
}
});

it("removeChatFromChatsByWorkspace preserves the previous reference when the chat is absent", () => {
const queryClient = createTestQueryClient();
queryClient.setQueryData(chatsByWorkspace(["ws-1"]).queryKey, {
"ws-1": "chat-2",
});
const before = queryClient.getQueryData(
chatsByWorkspace(["ws-1"]).queryKey,
);

removeChatFromChatsByWorkspace(queryClient, "chat-1");

expect(queryClient.getQueryData(chatsByWorkspace(["ws-1"]).queryKey)).toBe(
before,
);
});
});
29 changes: 29 additions & 0 deletions site/src/api/queries/chats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -643,6 +643,15 @@ export const invalidateChatListQueries = (queryClient: QueryClient) =>
queryKey: chatListFamilyKey,
});

// Event kinds that can change which chat is newest for a workspace.
const BY_WORKSPACE_AFFECTING_EVENT_KINDS = new Set<TypesGen.ChatWatchEventKind>(
["status_change", "action_required"],
);

export const shouldInvalidateChatsByWorkspace = (
eventKind: TypesGen.ChatWatchEventKind,
): boolean => BY_WORKSPACE_AFFECTING_EVENT_KINDS.has(eventKind);

export const invalidateChatsByWorkspace = (queryClient: QueryClient) =>
queryClient.invalidateQueries({
queryKey: chatsByWorkspaceFamilyKey,
Expand Down Expand Up @@ -767,6 +776,25 @@ export const removeChatEntity = (queryClient: QueryClient, chatId: string) =>
exact: true,
});

export const removeChatFromChatsByWorkspace = (
queryClient: QueryClient,
chatId: string,
) =>
queryClient.setQueriesData<Record<string, string>>(
{ queryKey: chatsByWorkspaceFamilyKey },
(prev) => {
if (!prev) {
return prev;
}
const next = Object.fromEntries(
Object.entries(prev).filter(([, id]) => id !== chatId),
);
return Object.keys(next).length === Object.keys(prev).length
? prev
: next;
},
);

export const patchChatEntity = (
queryClient: QueryClient,
chatId: string,
Expand Down Expand Up @@ -1008,6 +1036,7 @@ export const archiveChat = (queryClient: QueryClient) => ({
},
onSuccess: (_data: unknown, chatId: string) => {
applyChatArchiveStateToCaches(queryClient, chatId, true);
removeChatFromChatsByWorkspace(queryClient, chatId);
},
onSettled: (_data: unknown, _error: unknown, chatId: string) => {
void invalidateChatListQueries(queryClient);
Expand Down
10 changes: 10 additions & 0 deletions site/src/pages/AgentsPage/AgentsPageLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,11 @@ import {
proposeChatTitle,
readInfiniteChatsCache,
removeChatEntity,
removeChatFromChatsByWorkspace,
removeChildFromParentInCache,
reorderPinnedChat,
shouldInvalidateChatSearches,
shouldInvalidateChatsByWorkspace,
unarchiveChat,
unpinChat,
updateChatTitle,
Expand Down Expand Up @@ -304,6 +306,7 @@ const AgentsPageLayout: FC = () => {
),
onSuccess: ({ chatId, workspaceId, deleteBuild }) => {
applyChatArchiveStateToCaches(queryClient, chatId, true);
removeChatFromChatsByWorkspace(queryClient, chatId);
clearChatErrorReason(chatId);
clearPersistedSidebarTabId(chatId);
clearPersistedRightPanelState(chatId);
Expand Down Expand Up @@ -619,6 +622,8 @@ const AgentsPageLayout: FC = () => {
);
removeChildFromParentInCache(queryClient, updatedChat.id);
removeChatEntity(queryClient, updatedChat.id);
removeChatFromChatsByWorkspace(queryClient, updatedChat.id);
void invalidateChatsByWorkspace(queryClient);
void invalidateChatSearches(queryClient);
return;
}
Expand Down Expand Up @@ -655,6 +660,7 @@ const AgentsPageLayout: FC = () => {
} else {
prependToInfiniteChatsCache(queryClient, updatedChat);
void invalidateChatListQueries(queryClient);
void invalidateChatsByWorkspace(queryClient);
void invalidateChatSearches(queryClient);
}
} else {
Expand All @@ -668,6 +674,9 @@ const AgentsPageLayout: FC = () => {
if (shouldInvalidateChatSearches(chatEvent.kind)) {
void invalidateChatSearches(queryClient);
}
if (shouldInvalidateChatsByWorkspace(chatEvent.kind)) {
void invalidateChatsByWorkspace(queryClient);
}
const costChatId = chatCostIdToInvalidate(
updatedChat,
chatEvent.kind,
Expand All @@ -690,6 +699,7 @@ const AgentsPageLayout: FC = () => {
},
onOpen() {
void invalidateChatListQueries(queryClient);
void invalidateChatsByWorkspace(queryClient);
void invalidateChatSearches(queryClient);
},
});
Expand Down
Loading
Loading