mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-09-16 17:46:20 +00:00
* feat(chats): add archive and restore * test(chats): observe archive search requests in pagination e2e * docs(gateway): move thread lifecycle details out of inherited guidance * docs(chats): add concise archive and restore RFC * docs(chats): move archive RFC discussion to issue 5237
184 lines
5.8 KiB
TypeScript
184 lines
5.8 KiB
TypeScript
import { expect, test } from "@playwright/test";
|
|
|
|
import { mockLangGraphAPI } from "./utils/mock-api";
|
|
|
|
const CHAT = "00000000-0000-0000-0000-000000000901";
|
|
const OTHER = "00000000-0000-0000-0000-000000000902";
|
|
|
|
test("archive keeps the open conversation, supports undo and restores from the archive", async ({
|
|
page,
|
|
}, testInfo) => {
|
|
mockLangGraphAPI(page, {
|
|
threads: [
|
|
{
|
|
thread_id: CHAT,
|
|
title: "Finished report",
|
|
updated_at: "2026-07-04T10:00:00Z",
|
|
metadata: { deerflow_pinned: true },
|
|
},
|
|
{
|
|
thread_id: OTHER,
|
|
title: "Current work",
|
|
updated_at: "2026-07-05T10:00:00Z",
|
|
},
|
|
],
|
|
});
|
|
await page.goto(`/workspace/chats/${CHAT}`, {
|
|
waitUntil: "domcontentloaded",
|
|
});
|
|
const sidebarLink = page.locator(
|
|
`a[data-sidebar="menu-button"][href="/workspace/chats/${CHAT}"]`,
|
|
);
|
|
const archive = async () => {
|
|
await sidebarLink.hover();
|
|
await sidebarLink
|
|
.locator("xpath=..")
|
|
.getByRole("button", { name: "More" })
|
|
.click();
|
|
await page
|
|
.getByRole("menuitem", { name: "Archive chat", exact: true })
|
|
.click();
|
|
};
|
|
await expect(sidebarLink).toBeVisible();
|
|
await archive();
|
|
await expect(sidebarLink).toHaveCount(0);
|
|
await expect(page).toHaveURL(new RegExp(CHAT));
|
|
await expect(
|
|
page.getByRole("button", { name: "Restore chat", exact: true }),
|
|
).toBeVisible();
|
|
await page.getByRole("button", { name: "Undo", exact: true }).click();
|
|
await expect(sidebarLink).toBeVisible();
|
|
await archive();
|
|
await expect(sidebarLink).toHaveCount(0);
|
|
await page.reload({ waitUntil: "domcontentloaded" });
|
|
await expect(
|
|
page.getByRole("button", { name: "Restore chat", exact: true }),
|
|
).toBeVisible();
|
|
await page.goto("/workspace/chats", { waitUntil: "domcontentloaded" });
|
|
await expect(
|
|
page.locator("main").getByText("Current work", { exact: true }),
|
|
).toBeVisible();
|
|
await page.getByRole("tab", { name: "Archived", exact: true }).click();
|
|
await expect(
|
|
page.locator("main").getByText("Finished report", { exact: true }),
|
|
).toBeVisible();
|
|
await expect(
|
|
page.getByRole("tab", { name: "Archived", exact: true }),
|
|
).toHaveAttribute("aria-selected", "true");
|
|
await page.screenshot({
|
|
path: testInfo.outputPath("archived-list.png"),
|
|
animations: "disabled",
|
|
});
|
|
await page
|
|
.locator("main")
|
|
.getByRole("button", { name: "Restore chat", exact: true })
|
|
.click();
|
|
await expect(
|
|
page.getByText("No archived chats", { exact: true }),
|
|
).toBeVisible();
|
|
await expect(sidebarLink).toBeVisible();
|
|
});
|
|
|
|
test("failed archive keeps the chat visible", async ({ page }) => {
|
|
mockLangGraphAPI(page, { threads: [{ thread_id: CHAT, title: "Keep me" }] });
|
|
await page.route(`**/api/threads/${CHAT}`, (route) =>
|
|
route.request().method() === "PATCH"
|
|
? route.fulfill({
|
|
status: 500,
|
|
contentType: "application/json",
|
|
body: JSON.stringify({ detail: "Unavailable" }),
|
|
})
|
|
: route.fallback(),
|
|
);
|
|
await page.goto("/workspace/chats/new");
|
|
const link = page.locator(
|
|
`a[data-sidebar="menu-button"][href="/workspace/chats/${CHAT}"]`,
|
|
);
|
|
await link.hover();
|
|
await link.locator("xpath=..").getByRole("button", { name: "More" }).click();
|
|
await page
|
|
.getByRole("menuitem", { name: "Archive chat", exact: true })
|
|
.click();
|
|
await expect(
|
|
page.getByText("Failed to update archived chat", { exact: true }),
|
|
).toBeVisible();
|
|
await expect(link).toBeVisible();
|
|
});
|
|
|
|
test("active list includes legacy chats beyond a full page of archived chats", async ({
|
|
page,
|
|
}) => {
|
|
mockLangGraphAPI(page, {
|
|
threads: [
|
|
...Array.from({ length: 55 }, (_, index) => ({
|
|
thread_id: `archived-${index}`,
|
|
title: `Archived ${index}`,
|
|
updated_at: new Date(
|
|
Date.UTC(2026, 7, 1) - index * 60000,
|
|
).toISOString(),
|
|
metadata: { deerflow_archived: true },
|
|
})),
|
|
{
|
|
thread_id: CHAT,
|
|
title: "Legacy chat",
|
|
updated_at: "2020-01-01T00:00:00Z",
|
|
},
|
|
],
|
|
});
|
|
await page.goto("/workspace/chats", { waitUntil: "domcontentloaded" });
|
|
await expect(
|
|
page.locator("main").getByText("Legacy chat", { exact: true }),
|
|
).toBeVisible();
|
|
await expect(
|
|
page.locator("main").getByText("Archived 0", { exact: true }),
|
|
).toHaveCount(0);
|
|
await page.getByRole("tab", { name: "Archived", exact: true }).click();
|
|
await expect(
|
|
page.locator("main").getByText("Archived 0", { exact: true }),
|
|
).toBeVisible();
|
|
});
|
|
|
|
for (const customAgent of [false, true]) {
|
|
test(`archived ${customAgent ? "custom agent" : "default"} chat can be restored from its mobile header`, async ({
|
|
page,
|
|
}, testInfo) => {
|
|
await page.setViewportSize({ width: 390, height: 844 });
|
|
mockLangGraphAPI(page, {
|
|
agents: [{ name: "researcher", description: "Research assistant" }],
|
|
threads: [
|
|
{
|
|
thread_id: CHAT,
|
|
title: "Archived report",
|
|
metadata: {
|
|
deerflow_archived: true,
|
|
...(customAgent ? { agent_name: "researcher" } : {}),
|
|
},
|
|
},
|
|
],
|
|
});
|
|
const url = customAgent
|
|
? `/workspace/agents/researcher/chats/${CHAT}`
|
|
: `/workspace/chats/${CHAT}`;
|
|
await page.goto(url, { waitUntil: "domcontentloaded" });
|
|
const restore = page.getByRole("button", {
|
|
name: "Restore chat",
|
|
exact: true,
|
|
});
|
|
await expect(restore).toBeVisible({ timeout: 15000 });
|
|
await expect
|
|
.poll(() =>
|
|
page
|
|
.getByText("Archived report", { exact: true })
|
|
.evaluate((element) => element.getBoundingClientRect().height),
|
|
)
|
|
.toBeLessThanOrEqual(24);
|
|
await page.screenshot({
|
|
path: testInfo.outputPath("archived-header-mobile.png"),
|
|
animations: "disabled",
|
|
});
|
|
await restore.click();
|
|
await expect(restore).toHaveCount(0);
|
|
await expect(page).toHaveURL(new RegExp(CHAT));
|
|
});
|
|
}
|