diff --git a/frontend/src/app/workspace/agents/[agent_name]/chats/[thread_id]/page.tsx b/frontend/src/app/workspace/agents/[agent_name]/chats/[thread_id]/page.tsx
index 4f90f85205..62e2560261 100644
--- a/frontend/src/app/workspace/agents/[agent_name]/chats/[thread_id]/page.tsx
+++ b/frontend/src/app/workspace/agents/[agent_name]/chats/[thread_id]/page.tsx
@@ -275,7 +275,11 @@ export default function AgentChatPage() {
{!isNewThread &&
diff --git a/frontend/src/components/workspace/chats/chat-page.tsx b/frontend/src/components/workspace/chats/chat-page.tsx
index a6c650dded..2c5b6e2878 100644
--- a/frontend/src/components/workspace/chats/chat-page.tsx
+++ b/frontend/src/components/workspace/chats/chat-page.tsx
@@ -288,7 +288,11 @@ export default function ChatPage() {
>
{!isMock &&
}
-
+
{!isNewThread &&
diff --git a/frontend/src/components/workspace/thread-title.tsx b/frontend/src/components/workspace/thread-title.tsx
index bfc469e876..11412f01f6 100644
--- a/frontend/src/components/workspace/thread-title.tsx
+++ b/frontend/src/components/workspace/thread-title.tsx
@@ -7,21 +7,27 @@ import type { AgentThreadState } from "@/core/threads";
import { useThreadChat } from "./chats";
import { FlipDisplay } from "./flip-display";
-export function ThreadTitle({
- threadId,
- thread,
-}: {
+export type ThreadTitleProps = {
className?: string;
threadId: string;
thread: BaseStream
;
-}) {
+ canonicalTitle?: string;
+};
+
+export function ThreadTitle({
+ threadId,
+ thread,
+ canonicalTitle,
+}: ThreadTitleProps) {
const { t } = useI18n();
const { isNewThread } = useThreadChat();
+ const title = canonicalTitle?.length ? canonicalTitle : thread.values?.title;
+
useEffect(() => {
let _title = t.pages.untitled;
- if (thread.values?.title) {
- _title = thread.values.title;
+ if (title) {
+ _title = title;
} else if (isNewThread) {
_title = t.pages.newChat;
}
@@ -36,15 +42,11 @@ export function ThreadTitle({
t.pages.untitled,
t.pages.appName,
thread.isThreadLoading,
- thread.values,
+ title,
]);
- if (!thread.values?.title) {
+ if (!title) {
return null;
}
- return (
-
- {thread.values.title ?? "Untitled"}
-
- );
+ return {title};
}
diff --git a/frontend/src/core/threads/hooks.ts b/frontend/src/core/threads/hooks.ts
index 8a00a98f8a..b3752ed6a9 100644
--- a/frontend/src/core/threads/hooks.ts
+++ b/frontend/src/core/threads/hooks.ts
@@ -4,6 +4,7 @@ import { useStream } from "@langchain/langgraph-sdk/react";
import {
type QueryClient,
type InfiniteData,
+ type QueryFilters,
useInfiniteQuery,
useMutation,
useQuery,
@@ -1809,47 +1810,10 @@ export function useThreadStream({
);
for (const update of updates) {
if (update && "title" in update && update.title) {
- void queryClient.setQueriesData(
- {
- queryKey: ["threads", "search"],
- exact: false,
- },
- (oldData: Array | undefined) => {
- return oldData?.map((t) => {
- if (t.thread_id === threadIdRef.current) {
- return {
- ...t,
- values: {
- ...t.values,
- title: update.title,
- },
- };
- }
- return t;
- });
- },
- );
- const nextTitle: string = update.title;
- void queryClient.setQueriesData(
- {
- queryKey: INFINITE_THREADS_QUERY_KEY_PREFIX,
- exact: false,
- },
- (oldData: InfiniteData | undefined) =>
- mapInfiniteThreadsCache(
- oldData,
- (t): AgentThread =>
- t.thread_id === threadIdRef.current
- ? {
- ...t,
- values: {
- ...t.values,
- title: nextTitle,
- },
- }
- : t,
- ),
- );
+ const currentThreadId = threadIdRef.current;
+ if (currentThreadId) {
+ setThreadTitleInCaches(queryClient, currentThreadId, update.title);
+ }
}
}
},
@@ -2922,46 +2886,82 @@ function mergeThreadMetadata(
};
}
-function setThreadMetadataInCaches(
- queryClient: QueryClient,
- threadId: string,
- metadata: ThreadMetadataPatch,
-) {
- queryClient.setQueriesData(
- {
+function mergeThreadTitle(thread: AgentThread, title: string): AgentThread {
+ return {
+ ...thread,
+ values: {
+ ...thread.values,
+ title,
+ },
+ };
+}
+
+function getThreadSnapshotCacheFilters(threadId: string) {
+ return {
+ search: {
queryKey: ["threads", "search"],
exact: false,
},
+ infiniteSearch: {
+ queryKey: INFINITE_THREADS_QUERY_KEY_PREFIX,
+ exact: false,
+ },
+ metadata: {
+ queryKey: ["thread", "metadata", threadId],
+ exact: false,
+ },
+ } as const;
+}
+
+function setThreadInCaches(
+ queryClient: QueryClient,
+ threadId: string,
+ mapper: (thread: AgentThread) => AgentThread,
+): void {
+ const filters = getThreadSnapshotCacheFilters(threadId);
+
+ queryClient.setQueriesData(
+ filters.search,
(oldData: Array | undefined) => {
if (!oldData) {
return oldData;
}
return oldData.map((thread) =>
- thread.thread_id === threadId
- ? mergeThreadMetadata(thread, metadata)
- : thread,
+ thread.thread_id === threadId ? mapper(thread) : thread,
);
},
);
queryClient.setQueriesData(
- {
- queryKey: INFINITE_THREADS_QUERY_KEY_PREFIX,
- exact: false,
- },
+ filters.infiniteSearch,
(oldData: InfiniteData | undefined) =>
mapInfiniteThreadsCache(oldData, (thread) =>
- thread.thread_id === threadId
- ? mergeThreadMetadata(thread, metadata)
- : thread,
+ thread.thread_id === threadId ? mapper(thread) : thread,
),
);
queryClient.setQueriesData(
- {
- queryKey: ["thread", "metadata", threadId],
- exact: false,
- },
+ filters.metadata,
(oldData: AgentThread | null | undefined) =>
- oldData ? mergeThreadMetadata(oldData, metadata) : oldData,
+ oldData ? mapper(oldData) : oldData,
+ );
+}
+
+function setThreadMetadataInCaches(
+ queryClient: QueryClient,
+ threadId: string,
+ metadata: ThreadMetadataPatch,
+) {
+ setThreadInCaches(queryClient, threadId, (thread) =>
+ mergeThreadMetadata(thread, metadata),
+ );
+}
+
+export function setThreadTitleInCaches(
+ queryClient: QueryClient,
+ threadId: string,
+ title: string,
+): void {
+ setThreadInCaches(queryClient, threadId, (thread) =>
+ mergeThreadTitle(thread, title),
);
}
@@ -3320,45 +3320,19 @@ export function useRenameThread() {
values: { title },
});
},
- onSuccess(_, { threadId, title }) {
- queryClient.setQueriesData(
- {
- queryKey: ["threads", "search"],
- exact: false,
- },
- (oldData: Array) => {
- return oldData.map((t) => {
- if (t.thread_id === threadId) {
- return {
- ...t,
- values: {
- ...t.values,
- title,
- },
- };
- }
- return t;
- });
- },
+ async onSuccess(_, { threadId, title }) {
+ const filters: QueryFilters[] = Object.values(
+ getThreadSnapshotCacheFilters(threadId),
);
- queryClient.setQueriesData(
- {
- queryKey: INFINITE_THREADS_QUERY_KEY_PREFIX,
- exact: false,
- },
- (oldData: InfiniteData | undefined) =>
- mapInfiniteThreadsCache(oldData, (t) =>
- t.thread_id === threadId
- ? {
- ...t,
- values: {
- ...t.values,
- title,
- },
- }
- : t,
- ),
+
+ // Prevent pre-rename snapshot requests from restoring the stale title.
+ await Promise.all(
+ filters.map((filter) => queryClient.cancelQueries(filter)),
);
+ setThreadTitleInCaches(queryClient, threadId, title);
+ for (const filter of filters) {
+ void queryClient.invalidateQueries(filter);
+ }
},
});
}
diff --git a/frontend/tests/e2e/thread-title-sync.spec.ts b/frontend/tests/e2e/thread-title-sync.spec.ts
new file mode 100644
index 0000000000..fde30f4dd4
--- /dev/null
+++ b/frontend/tests/e2e/thread-title-sync.spec.ts
@@ -0,0 +1,188 @@
+import { expect, test } from "@playwright/test";
+
+import { mockLangGraphAPI } from "./utils/mock-api";
+
+const THREAD_ID = "00000000-0000-0000-0000-000000000321";
+const ORIGINAL_TITLE = "Original title";
+const RENAMED_TITLE = "Renamed title";
+
+test("renaming a thread updates the sidebar, header, and document title", async ({
+ page,
+}) => {
+ mockLangGraphAPI(page, {
+ threads: [
+ {
+ thread_id: THREAD_ID,
+ title: ORIGINAL_TITLE,
+ updated_at: "2026-07-05T10:00:00Z",
+ },
+ ],
+ });
+
+ await page.goto(`/workspace/chats/${THREAD_ID}`);
+ await expect(page.getByText(ORIGINAL_TITLE).first()).toBeVisible({
+ timeout: 15_000,
+ });
+ await expect(page).toHaveTitle(`${ORIGINAL_TITLE} - DeerFlow`);
+
+ const threadItem = page
+ .locator(
+ `a[data-sidebar="menu-button"][href="/workspace/chats/${THREAD_ID}"]`,
+ )
+ .locator("xpath=..");
+ await threadItem.hover();
+ await threadItem.getByRole("button", { name: "More" }).click();
+ await page.getByRole("menuitem", { name: "Rename" }).click();
+
+ const dialog = page.getByRole("dialog");
+ await dialog.getByRole("textbox").fill(RENAMED_TITLE);
+ await dialog.getByRole("button", { name: "Save" }).click();
+
+ await expect(dialog).toBeHidden();
+ await expect(threadItem).toContainText(RENAMED_TITLE);
+ await expect(page.locator("header").getByText(RENAMED_TITLE)).toBeVisible();
+ await expect(page).toHaveTitle(`${RENAMED_TITLE} - DeerFlow`);
+
+ await page.reload();
+ await expect(threadItem).toContainText(RENAMED_TITLE);
+ await expect(page.locator("header").getByText(RENAMED_TITLE)).toBeVisible({
+ timeout: 15_000,
+ });
+ await expect(page).toHaveTitle(`${RENAMED_TITLE} - DeerFlow`);
+});
+
+test("a stale metadata response cannot restore the old title after rename", async ({
+ page,
+}) => {
+ mockLangGraphAPI(page, {
+ threads: [
+ {
+ thread_id: THREAD_ID,
+ title: ORIGINAL_TITLE,
+ updated_at: "2026-07-05T10:00:00Z",
+ },
+ ],
+ });
+
+ let releaseStaleMetadataResponse!: () => void;
+ const staleMetadataResponseGate = new Promise((resolve) => {
+ releaseStaleMetadataResponse = resolve;
+ });
+ let markStaleMetadataRequestStarted!: () => void;
+ const staleMetadataRequestStarted = new Promise((resolve) => {
+ markStaleMetadataRequestStarted = resolve;
+ });
+ let markStaleMetadataResponseCompleted!: () => void;
+ const staleMetadataResponseCompleted = new Promise((resolve) => {
+ markStaleMetadataResponseCompleted = resolve;
+ });
+ let releaseFreshMetadataResponse!: () => void;
+ const freshMetadataResponseGate = new Promise((resolve) => {
+ releaseFreshMetadataResponse = resolve;
+ });
+ let markFreshMetadataRequestStarted!: () => void;
+ const freshMetadataRequestStarted = new Promise((resolve) => {
+ markFreshMetadataRequestStarted = resolve;
+ });
+ let markFreshMetadataResponseCompleted!: () => void;
+ const freshMetadataResponseCompleted = new Promise((resolve) => {
+ markFreshMetadataResponseCompleted = resolve;
+ });
+ let delayMetadataResponses = false;
+ let metadataRequestCount = 0;
+
+ await page.route("**/api/langgraph/threads/*", async (route) => {
+ const url = new URL(route.request().url());
+ if (
+ route.request().method() !== "GET" ||
+ url.pathname !== `/api/langgraph/threads/${THREAD_ID}`
+ ) {
+ return route.fallback();
+ }
+
+ if (!delayMetadataResponses) {
+ return route.fallback();
+ }
+
+ metadataRequestCount += 1;
+ if (metadataRequestCount > 1) {
+ markFreshMetadataRequestStarted();
+ await freshMetadataResponseGate;
+ await route.fallback();
+ markFreshMetadataResponseCompleted();
+ return;
+ }
+
+ markStaleMetadataRequestStarted();
+ await staleMetadataResponseGate;
+ await route.fulfill({
+ status: 200,
+ contentType: "application/json",
+ body: JSON.stringify({
+ thread_id: THREAD_ID,
+ created_at: "2026-07-05T10:00:00Z",
+ updated_at: "2026-07-05T10:00:00Z",
+ metadata: {},
+ status: "idle",
+ values: { title: ORIGINAL_TITLE, goal: null },
+ }),
+ });
+ markStaleMetadataResponseCompleted();
+ });
+
+ await page.goto(`/workspace/chats/${THREAD_ID}`);
+ await expect(page.locator("header").getByText(ORIGINAL_TITLE)).toBeVisible({
+ timeout: 15_000,
+ });
+
+ delayMetadataResponses = true;
+ await page.getByRole("link", { name: "Chats", exact: true }).click();
+ await expect(page).toHaveURL(/\/workspace\/chats$/);
+ await page
+ .locator(
+ `a[data-sidebar="menu-button"][href="/workspace/chats/${THREAD_ID}"]`,
+ )
+ .click();
+ await staleMetadataRequestStarted;
+
+ const threadItem = page
+ .locator(
+ `a[data-sidebar="menu-button"][href="/workspace/chats/${THREAD_ID}"]`,
+ )
+ .locator("xpath=..");
+ await expect(threadItem).toContainText(ORIGINAL_TITLE);
+ await threadItem.hover();
+ await threadItem.getByRole("button", { name: "More" }).click();
+ await page.getByRole("menuitem", { name: "Rename" }).click();
+
+ const dialog = page.getByRole("dialog");
+ await dialog.getByRole("textbox").fill(RENAMED_TITLE);
+ await dialog.getByRole("button", { name: "Save" }).click();
+ await expect(dialog).toBeHidden();
+ await expect(threadItem).toContainText(RENAMED_TITLE);
+ await expect(page.locator("header").getByText(RENAMED_TITLE)).toBeVisible();
+ await expect(page).toHaveTitle(`${RENAMED_TITLE} - DeerFlow`);
+
+ releaseStaleMetadataResponse();
+ await staleMetadataResponseCompleted;
+ await page.evaluate(
+ () =>
+ new Promise((resolve) => {
+ requestAnimationFrame(() => requestAnimationFrame(() => resolve()));
+ }),
+ );
+
+ // The stale response has completed while the fresh refetch is still blocked.
+ // The title must remain renamed without relying on the refetch to repair it.
+ await expect(threadItem).toContainText(RENAMED_TITLE);
+ await expect(page.locator("header").getByText(RENAMED_TITLE)).toBeVisible();
+ await expect(page).toHaveTitle(`${RENAMED_TITLE} - DeerFlow`);
+
+ await freshMetadataRequestStarted;
+ releaseFreshMetadataResponse();
+ await freshMetadataResponseCompleted;
+
+ await expect(threadItem).toContainText(RENAMED_TITLE);
+ await expect(page.locator("header").getByText(RENAMED_TITLE)).toBeVisible();
+ await expect(page).toHaveTitle(`${RENAMED_TITLE} - DeerFlow`);
+});
diff --git a/frontend/tests/e2e/utils/mock-api.ts b/frontend/tests/e2e/utils/mock-api.ts
index f75f3bb41c..5401dce255 100644
--- a/frontend/tests/e2e/utils/mock-api.ts
+++ b/frontend/tests/e2e/utils/mock-api.ts
@@ -375,6 +375,18 @@ export function mockLangGraphAPI(page: Page, options?: MockAPIOptions) {
return updated;
};
+ const patchThreadTitle = (threadId: string, title: string) => {
+ let updated: MockThread | undefined;
+ threads = threads.map((thread) => {
+ if (thread.thread_id !== threadId) {
+ return thread;
+ }
+ updated = { ...thread, title };
+ return updated;
+ });
+ return updated;
+ };
+
// Auth — keep workspace tests independent from a real gateway session.
void page.route("**/api/v1/auth/me", (route) => {
if (route.request().method() === "GET") {
@@ -1054,9 +1066,13 @@ export function mockLangGraphAPI(page: Page, options?: MockAPIOptions) {
// Thread state — getState for individual thread
void page.route("**/api/langgraph/threads/*/state", (route) => {
+ const url = new URL(route.request().url());
+ const threadId = decodeURIComponent(url.pathname.split("/").at(-2) ?? "");
+ const matchingThread = threads.find(
+ (thread) => thread.thread_id === threadId,
+ );
+
if (route.request().method() === "GET") {
- const url = route.request().url();
- const matchingThread = threads.find((t) => url.includes(t.thread_id));
return route.fulfill({
status: 200,
contentType: "application/json",
@@ -1086,6 +1102,33 @@ export function mockLangGraphAPI(page: Page, options?: MockAPIOptions) {
}),
});
}
+ if (route.request().method() === "POST") {
+ const body = route.request().postDataJSON() as {
+ values?: { title?: unknown };
+ };
+ const updated =
+ typeof body.values?.title === "string"
+ ? patchThreadTitle(threadId, body.values.title)
+ : matchingThread;
+ if (!updated) {
+ return route.fulfill({
+ status: 404,
+ contentType: "application/json",
+ body: JSON.stringify({ detail: "Thread not found" }),
+ });
+ }
+ return route.fulfill({
+ status: 200,
+ contentType: "application/json",
+ body: JSON.stringify({
+ configurable: {
+ thread_id: threadId,
+ checkpoint_ns: "",
+ checkpoint_id: "mock-checkpoint",
+ },
+ }),
+ });
+ }
return route.fallback();
});
diff --git a/frontend/tests/unit/components/workspace/thread-title.dom.test.tsx b/frontend/tests/unit/components/workspace/thread-title.dom.test.tsx
new file mode 100644
index 0000000000..f95df2e63a
--- /dev/null
+++ b/frontend/tests/unit/components/workspace/thread-title.dom.test.tsx
@@ -0,0 +1,74 @@
+import type { BaseStream } from "@langchain/langgraph-sdk";
+import { afterEach, expect, rs, test } from "@rstest/core";
+import { cleanup, render } from "@testing-library/react";
+import type { ReactNode } from "react";
+
+import { ThreadTitle } from "@/components/workspace/thread-title";
+import type { AgentThreadState } from "@/core/threads";
+
+rs.mock("@/core/i18n/hooks", () => ({
+ useI18n: () => ({
+ t: {
+ pages: {
+ appName: "DeerFlow",
+ newChat: "New chat",
+ untitled: "Untitled",
+ },
+ },
+ }),
+}));
+
+rs.mock("@/components/workspace/chats", () => ({
+ useThreadChat: () => ({ isNewThread: false }),
+}));
+
+rs.mock("@/components/workspace/flip-display", () => ({
+ FlipDisplay: ({ children }: { children: ReactNode }) => {children}
,
+}));
+
+function makeThread(title: string): BaseStream {
+ return {
+ isThreadLoading: false,
+ values: { title },
+ } as unknown as BaseStream;
+}
+
+afterEach(() => {
+ cleanup();
+ document.title = "";
+});
+
+test("prefers the canonical metadata title over a stale stream title", () => {
+ const { container } = render(
+ ,
+ );
+
+ expect(container.textContent).toBe("Renamed title");
+ expect(document.title).toBe("Renamed title - DeerFlow");
+});
+
+test("falls back to the stream title when metadata is unavailable", () => {
+ const { container } = render(
+ ,
+ );
+
+ expect(container.textContent).toBe("Stream title");
+ expect(document.title).toBe("Stream title - DeerFlow");
+});
+
+test("falls back to the stream title when metadata title is empty", () => {
+ const { container } = render(
+ ,
+ );
+
+ expect(container.textContent).toBe("Stream title");
+ expect(document.title).toBe("Stream title - DeerFlow");
+});
diff --git a/frontend/tests/unit/core/threads/rename-thread.dom.test.tsx b/frontend/tests/unit/core/threads/rename-thread.dom.test.tsx
new file mode 100644
index 0000000000..d3b2f47421
--- /dev/null
+++ b/frontend/tests/unit/core/threads/rename-thread.dom.test.tsx
@@ -0,0 +1,170 @@
+import { afterEach, beforeEach, expect, rs, test } from "@rstest/core";
+import {
+ type InfiniteData,
+ QueryClient,
+ QueryClientProvider,
+} from "@tanstack/react-query";
+import { act, cleanup, renderHook } from "@testing-library/react";
+import type { PropsWithChildren } from "react";
+
+const apiMocks = rs.hoisted(() => ({
+ updateState: rs.fn(),
+}));
+
+rs.mock("@/core/api", () => ({
+ getAPIClient: () => ({
+ threads: {
+ updateState: apiMocks.updateState,
+ },
+ }),
+}));
+
+import {
+ INFINITE_THREADS_QUERY_KEY_PREFIX,
+ useRenameThread,
+} from "@/core/threads/hooks";
+import { DEFAULT_THREAD_SEARCH_PARAMS } from "@/core/threads/thread-search-query";
+import type { AgentThread } from "@/core/threads/types";
+
+const THREAD_ID = "thread-1";
+const ORIGINAL_TITLE = "Original title";
+const RENAMED_TITLE = "Renamed title";
+
+function makeThread(title: string): AgentThread {
+ return {
+ thread_id: THREAD_ID,
+ created_at: "2026-01-01T00:00:00Z",
+ updated_at: "2026-01-01T00:00:00Z",
+ metadata: {},
+ status: "idle",
+ values: { title },
+ } as unknown as AgentThread;
+}
+
+function createDelayedResult(value: T) {
+ let markStarted!: () => void;
+ const started = new Promise((resolve) => {
+ markStarted = resolve;
+ });
+ let release!: () => void;
+ const gate = new Promise((resolve) => {
+ release = resolve;
+ });
+ let markCompleted!: () => void;
+ const completed = new Promise((resolve) => {
+ markCompleted = resolve;
+ });
+
+ return {
+ completed,
+ queryFn: async () => {
+ markStarted();
+ await gate;
+ markCompleted();
+ return value;
+ },
+ release,
+ started,
+ };
+}
+
+function readSearchTitle(
+ queryClient: QueryClient,
+ queryKey: readonly unknown[],
+) {
+ return queryClient.getQueryData(queryKey)?.[0]?.values.title;
+}
+
+function readInfiniteSearchTitle(
+ queryClient: QueryClient,
+ queryKey: readonly unknown[],
+) {
+ return queryClient.getQueryData>(queryKey)
+ ?.pages[0]?.[0]?.values.title;
+}
+
+test("stale list responses cannot restore the old title after rename", async () => {
+ const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: { retry: false },
+ mutations: { retry: false },
+ },
+ });
+ const wrapper = ({ children }: PropsWithChildren) => (
+ {children}
+ );
+ const infiniteParams = {
+ sortBy: "updated_at",
+ sortOrder: "desc",
+ select: ["thread_id", "updated_at", "values", "metadata"],
+ };
+ const searchKey = [
+ "threads",
+ "search",
+ DEFAULT_THREAD_SEARCH_PARAMS,
+ ] as const;
+ const infiniteSearchKey = [
+ ...INFINITE_THREADS_QUERY_KEY_PREFIX,
+ infiniteParams,
+ ] as const;
+ const originalThread = makeThread(ORIGINAL_TITLE);
+ const staleSearch = createDelayedResult([originalThread]);
+ const staleInfiniteSearch = createDelayedResult([originalThread]);
+
+ queryClient.setQueryData(searchKey, [originalThread]);
+ queryClient.setQueryData>(infiniteSearchKey, {
+ pages: [[originalThread]],
+ pageParams: [0],
+ });
+
+ const searchFetch = queryClient
+ .fetchQuery({
+ queryKey: searchKey,
+ queryFn: staleSearch.queryFn,
+ })
+ .catch(() => undefined);
+ const infiniteSearchFetch = queryClient
+ .fetchInfiniteQuery({
+ queryKey: infiniteSearchKey,
+ initialPageParam: 0,
+ queryFn: staleInfiniteSearch.queryFn,
+ getNextPageParam: () => undefined,
+ })
+ .catch(() => undefined);
+
+ await Promise.all([staleSearch.started, staleInfiniteSearch.started]);
+ apiMocks.updateState.mockResolvedValue(undefined);
+ const { result } = renderHook(() => useRenameThread(), { wrapper });
+
+ await act(async () => {
+ await result.current.mutateAsync({
+ threadId: THREAD_ID,
+ title: RENAMED_TITLE,
+ });
+ });
+
+ expect(readSearchTitle(queryClient, searchKey)).toBe(RENAMED_TITLE);
+ expect(readInfiniteSearchTitle(queryClient, infiniteSearchKey)).toBe(
+ RENAMED_TITLE,
+ );
+
+ staleSearch.release();
+ staleInfiniteSearch.release();
+ await Promise.all([staleSearch.completed, staleInfiniteSearch.completed]);
+ await Promise.all([searchFetch, infiniteSearchFetch]);
+
+ expect(readSearchTitle(queryClient, searchKey)).toBe(RENAMED_TITLE);
+ expect(readInfiniteSearchTitle(queryClient, infiniteSearchKey)).toBe(
+ RENAMED_TITLE,
+ );
+
+ queryClient.clear();
+});
+
+beforeEach(() => {
+ apiMocks.updateState.mockReset();
+});
+
+afterEach(() => {
+ cleanup();
+});