mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-06-15 11:56:01 +00:00
fix(frontend): reset active chat after deletion (#3519)
This commit is contained in:
@@ -65,6 +65,36 @@ test.describe("Thread history", () => {
|
||||
).toBeVisible({ timeout: 15_000 });
|
||||
});
|
||||
|
||||
test("deleting an inactive chat keeps the current chat open", async ({
|
||||
page,
|
||||
}) => {
|
||||
mockLangGraphAPI(page, { threads: THREADS });
|
||||
|
||||
await page.goto(`/workspace/chats/${MOCK_THREAD_ID}`);
|
||||
await expect(
|
||||
page.getByText("Response in thread First conversation"),
|
||||
).toBeVisible({ timeout: 15_000 });
|
||||
|
||||
const sidebar = page.locator("[data-sidebar='sidebar']");
|
||||
const inactiveThreadItem = sidebar
|
||||
.locator("[data-sidebar='menu-item']")
|
||||
.filter({
|
||||
has: page.getByRole("button", { name: /more/i }),
|
||||
hasText: "Second conversation",
|
||||
})
|
||||
.first();
|
||||
await expect(inactiveThreadItem).toBeVisible();
|
||||
await inactiveThreadItem.hover();
|
||||
await inactiveThreadItem.getByRole("button", { name: /more/i }).click();
|
||||
await page.getByRole("menuitem", { name: /delete/i }).click();
|
||||
|
||||
await expect(page).toHaveURL(new RegExp(MOCK_THREAD_ID));
|
||||
await expect(
|
||||
page.getByText("Response in thread First conversation"),
|
||||
).toBeVisible();
|
||||
await expect(sidebar.getByText("Second conversation")).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("new chat does not show previous thread messages after client-side navigation", async ({
|
||||
page,
|
||||
}) => {
|
||||
@@ -100,7 +130,9 @@ test.describe("Thread history", () => {
|
||||
timeout: 15_000,
|
||||
});
|
||||
|
||||
await page.getByRole("link", { name: /new chat/i }).click();
|
||||
await page
|
||||
.locator("[data-sidebar='sidebar'] a[href='/workspace/chats/new']")
|
||||
.click();
|
||||
await page.waitForURL("**/workspace/chats/new");
|
||||
|
||||
await expect(page.getByText(SVG_PROMPT_MARKER)).toBeHidden();
|
||||
@@ -161,13 +193,64 @@ test.describe("Thread history", () => {
|
||||
await page.waitForURL(`**/workspace/chats/${MOCK_THREAD_ID_2}`);
|
||||
await expect(page.getByText(OPTIMISTIC_PROMPT_MARKER)).toHaveCount(0);
|
||||
|
||||
await page.getByRole("link", { name: /new chat/i }).click();
|
||||
await page
|
||||
.locator("[data-sidebar='sidebar'] a[href='/workspace/chats/new']")
|
||||
.click();
|
||||
await page.waitForURL("**/workspace/chats/new");
|
||||
|
||||
await expect(page.getByText(OPTIMISTIC_PROMPT_MARKER)).toHaveCount(0);
|
||||
await expect(page.getByPlaceholder(/how can i assist you/i)).toBeVisible();
|
||||
});
|
||||
|
||||
test("deleting the active newly created chat returns to the new chat screen", async ({
|
||||
page,
|
||||
}) => {
|
||||
mockLangGraphAPI(page);
|
||||
await page.route(/\/api\/threads\/[^/]+$/, (route) => {
|
||||
if (route.request().method() === "DELETE") {
|
||||
return route.fulfill({
|
||||
status: 500,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({ detail: "Local cleanup failed" }),
|
||||
});
|
||||
}
|
||||
return route.fallback();
|
||||
});
|
||||
|
||||
await page.goto("/workspace/chats/new");
|
||||
const textarea = page.getByPlaceholder(/how can i assist you/i);
|
||||
await expect(textarea).toBeVisible({ timeout: 15_000 });
|
||||
await textarea.fill("What should disappear after deletion?");
|
||||
await textarea.press("Enter");
|
||||
|
||||
await expect(page.getByText("Hello from DeerFlow!")).toBeVisible({
|
||||
timeout: 15_000,
|
||||
});
|
||||
|
||||
const sidebar = page.locator("[data-sidebar='sidebar']");
|
||||
const recentThreadItem = sidebar
|
||||
.locator("[data-sidebar='menu-item']")
|
||||
.filter({
|
||||
has: page.getByRole("button", { name: /more/i }),
|
||||
hasText: "New Chat",
|
||||
})
|
||||
.first();
|
||||
await expect(recentThreadItem).toBeVisible();
|
||||
await recentThreadItem.hover();
|
||||
await recentThreadItem.getByRole("button", { name: /more/i }).click();
|
||||
await page.getByRole("menuitem", { name: /delete/i }).click();
|
||||
|
||||
await expect(page).toHaveURL(/\/workspace\/chats\/new$/);
|
||||
await expect(page.getByText("Previous question")).toHaveCount(0);
|
||||
await expect(page.getByText("Hello from DeerFlow!")).toHaveCount(0);
|
||||
await expect(page.getByPlaceholder(/how can i assist you/i)).toBeVisible();
|
||||
|
||||
await page.goto(`/workspace/chats/${MOCK_THREAD_ID}`);
|
||||
await page.waitForURL("**/workspace/chats/new");
|
||||
await expect(page.getByText("Hello from DeerFlow!")).toHaveCount(0);
|
||||
await expect(page.getByPlaceholder(/how can i assist you/i)).toBeVisible();
|
||||
});
|
||||
|
||||
test("mock thread does not load real backend run history", async ({
|
||||
page,
|
||||
}) => {
|
||||
|
||||
@@ -16,6 +16,13 @@ export const MOCK_THREAD_ID = "00000000-0000-0000-0000-000000000001";
|
||||
export const MOCK_THREAD_ID_2 = "00000000-0000-0000-0000-000000000002";
|
||||
export const MOCK_RUN_ID = "00000000-0000-0000-0000-000000000099";
|
||||
|
||||
const MOCK_AUTH_USER = {
|
||||
id: "default",
|
||||
email: "default@test.local",
|
||||
system_role: "admin",
|
||||
needs_setup: false,
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -71,6 +78,21 @@ const DEFAULT_SKILLS: MockSkill[] = [
|
||||
},
|
||||
];
|
||||
|
||||
function mockStreamMessages() {
|
||||
return [
|
||||
{
|
||||
type: "human",
|
||||
id: "msg-human-1",
|
||||
content: [{ type: "text", text: "Hello" }],
|
||||
},
|
||||
{
|
||||
type: "ai",
|
||||
id: "msg-ai-1",
|
||||
content: "Hello from DeerFlow!",
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// mockLangGraphAPI
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -81,23 +103,84 @@ const DEFAULT_SKILLS: MockSkill[] = [
|
||||
* for a real backend.
|
||||
*/
|
||||
export function mockLangGraphAPI(page: Page, options?: MockAPIOptions) {
|
||||
const threads = options?.threads ?? [];
|
||||
let threads = [...(options?.threads ?? [])];
|
||||
const agents = options?.agents ?? [];
|
||||
const skills = options?.skills ?? DEFAULT_SKILLS;
|
||||
|
||||
const upsertThread = (thread: MockThread) => {
|
||||
threads = [
|
||||
thread,
|
||||
...threads.filter((existing) => existing.thread_id !== thread.thread_id),
|
||||
];
|
||||
};
|
||||
|
||||
const threadSearchResult = (thread: MockThread) => ({
|
||||
thread_id: thread.thread_id,
|
||||
created_at: "2025-01-01T00:00:00Z",
|
||||
updated_at: thread.updated_at ?? "2025-01-01T00:00:00Z",
|
||||
metadata: {
|
||||
...(thread.metadata ?? {}),
|
||||
...(thread.agent_name ? { agent_name: thread.agent_name } : {}),
|
||||
},
|
||||
status: "idle",
|
||||
values: { title: thread.title ?? "Untitled" },
|
||||
});
|
||||
|
||||
// Auth — keep workspace tests independent from a real gateway session.
|
||||
void page.route("**/api/v1/auth/me", (route) => {
|
||||
if (route.request().method() === "GET") {
|
||||
return route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify(MOCK_AUTH_USER),
|
||||
});
|
||||
}
|
||||
return route.fallback();
|
||||
});
|
||||
|
||||
void page.route("**/api/v1/auth/setup-status", (route) => {
|
||||
if (route.request().method() === "GET") {
|
||||
return route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({ needs_setup: false }),
|
||||
});
|
||||
}
|
||||
return route.fallback();
|
||||
});
|
||||
|
||||
void page.route("**/api/v1/auth/logout", (route) => {
|
||||
if (route.request().method() === "POST") {
|
||||
return route.fulfill({ status: 204 });
|
||||
}
|
||||
return route.fallback();
|
||||
});
|
||||
|
||||
void page.route("**/api/channels/providers", (route) => {
|
||||
if (route.request().method() === "GET") {
|
||||
return route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({ enabled: false, providers: [] }),
|
||||
});
|
||||
}
|
||||
return route.fallback();
|
||||
});
|
||||
|
||||
void page.route("**/api/channels/connections", (route) => {
|
||||
if (route.request().method() === "GET") {
|
||||
return route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({ connections: [] }),
|
||||
});
|
||||
}
|
||||
return route.fallback();
|
||||
});
|
||||
|
||||
// Thread search — sidebar thread list & chats list page
|
||||
void page.route("**/api/langgraph/threads/search", async (route) => {
|
||||
const body = threads.map((t) => ({
|
||||
thread_id: t.thread_id,
|
||||
created_at: "2025-01-01T00:00:00Z",
|
||||
updated_at: t.updated_at ?? "2025-01-01T00:00:00Z",
|
||||
metadata: {
|
||||
...(t.metadata ?? {}),
|
||||
...(t.agent_name ? { agent_name: t.agent_name } : {}),
|
||||
},
|
||||
status: "idle",
|
||||
values: { title: t.title ?? "Untitled" },
|
||||
}));
|
||||
const body = threads.map(threadSearchResult);
|
||||
|
||||
let limit: number | undefined;
|
||||
let offset = 0;
|
||||
@@ -131,6 +214,12 @@ export function mockLangGraphAPI(page: Page, options?: MockAPIOptions) {
|
||||
// Thread create — called when user sends first message in a new chat
|
||||
void page.route("**/api/langgraph/threads", (route) => {
|
||||
if (route.request().method() === "POST") {
|
||||
upsertThread({
|
||||
thread_id: MOCK_THREAD_ID,
|
||||
title: "New Chat",
|
||||
updated_at: new Date().toISOString(),
|
||||
messages: mockStreamMessages(),
|
||||
});
|
||||
return route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
@@ -149,6 +238,26 @@ export function mockLangGraphAPI(page: Page, options?: MockAPIOptions) {
|
||||
|
||||
// Thread update (PATCH) — metadata update after creation
|
||||
void page.route("**/api/langgraph/threads/*", (route) => {
|
||||
const threadId = decodeURIComponent(
|
||||
new URL(route.request().url()).pathname.split("/").at(-1) ?? "",
|
||||
);
|
||||
const matchingThread = threads.find(
|
||||
(thread) => thread.thread_id === threadId,
|
||||
);
|
||||
if (route.request().method() === "GET") {
|
||||
if (!matchingThread) {
|
||||
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(threadSearchResult(matchingThread)),
|
||||
});
|
||||
}
|
||||
if (route.request().method() === "PATCH") {
|
||||
return route.fulfill({
|
||||
status: 200,
|
||||
@@ -156,6 +265,21 @@ export function mockLangGraphAPI(page: Page, options?: MockAPIOptions) {
|
||||
body: JSON.stringify({ thread_id: MOCK_THREAD_ID }),
|
||||
});
|
||||
}
|
||||
if (route.request().method() === "DELETE") {
|
||||
threads = threads.filter((thread) => thread.thread_id !== threadId);
|
||||
return route.fulfill({
|
||||
status: 204,
|
||||
});
|
||||
}
|
||||
return route.fallback();
|
||||
});
|
||||
|
||||
void page.route(/\/api\/threads\/[^/]+$/, (route) => {
|
||||
if (route.request().method() === "DELETE") {
|
||||
return route.fulfill({
|
||||
status: 204,
|
||||
});
|
||||
}
|
||||
return route.fallback();
|
||||
});
|
||||
|
||||
@@ -299,8 +423,21 @@ export function mockLangGraphAPI(page: Page, options?: MockAPIOptions) {
|
||||
);
|
||||
|
||||
// Run stream — returns a minimal SSE response with an AI message
|
||||
void page.route("**/api/langgraph/runs/stream", handleRunStream);
|
||||
void page.route("**/api/langgraph/threads/*/runs/stream", handleRunStream);
|
||||
const handleMockRunStream = (route: Route) => {
|
||||
upsertThread({
|
||||
thread_id: MOCK_THREAD_ID,
|
||||
title: "New Chat",
|
||||
updated_at: new Date().toISOString(),
|
||||
messages: mockStreamMessages(),
|
||||
});
|
||||
return handleRunStream(route);
|
||||
};
|
||||
|
||||
void page.route("**/api/langgraph/runs/stream", handleMockRunStream);
|
||||
void page.route(
|
||||
"**/api/langgraph/threads/*/runs/stream",
|
||||
handleMockRunStream,
|
||||
);
|
||||
|
||||
// Models list — model picker dropdown
|
||||
void page.route("**/api/models", (route) => {
|
||||
@@ -391,18 +528,7 @@ export function handleRunStream(route: Route) {
|
||||
{
|
||||
event: "values",
|
||||
data: {
|
||||
messages: [
|
||||
{
|
||||
type: "human",
|
||||
id: "msg-human-1",
|
||||
content: [{ type: "text", text: "Hello" }],
|
||||
},
|
||||
{
|
||||
type: "ai",
|
||||
id: "msg-ai-1",
|
||||
content: "Hello from DeerFlow!",
|
||||
},
|
||||
],
|
||||
messages: mockStreamMessages(),
|
||||
},
|
||||
},
|
||||
{ event: "end", data: {} },
|
||||
|
||||
Reference in New Issue
Block a user