mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-10-26 21:26:39 +00:00
* fix(artifacts): preserve account-scoped drafts across navigation * docs(artifacts): keep draft lifecycle guidance within its budget * fix(artifacts): retain drafts through unknown auth and isolate typing updates * fix(artifacts): preserve drafts when visiting public showcases * fix(artifacts): preserve edits after pending save
127 lines
4.3 KiB
TypeScript
127 lines
4.3 KiB
TypeScript
import { createHash } from "node:crypto";
|
|
|
|
import { expect, test, type Page } from "@playwright/test";
|
|
|
|
import { mockLangGraphAPI } from "./utils/mock-api";
|
|
|
|
const threadA = "00000000-0000-0000-0000-000000006696";
|
|
const threadB = "00000000-0000-0000-0000-000000006697";
|
|
const filepath = "/mnt/user-data/outputs/navigation-report.txt";
|
|
const original = "Original saved report";
|
|
const draft = "My draft survives chat navigation";
|
|
|
|
async function navigateToChat(page: Page, threadId: string) {
|
|
const link = page.locator(`a[href="/workspace/chats/${threadId}"]`).first();
|
|
if (!(await link.isVisible())) {
|
|
await page.getByRole("button", { name: "Toggle Sidebar" }).first().click();
|
|
}
|
|
await link.click();
|
|
await expect(page).toHaveURL(new RegExp(`/workspace/chats/${threadId}$`));
|
|
}
|
|
|
|
test("retains drafts through chat navigation and keeps Save and Discard final", async ({
|
|
page,
|
|
}) => {
|
|
mockLangGraphAPI(page, {
|
|
threads: [
|
|
{
|
|
thread_id: threadA,
|
|
title: "Draft navigation A",
|
|
artifacts: [filepath],
|
|
messages: [
|
|
{ type: "human", id: "draft-human", content: "Write a report" },
|
|
{
|
|
type: "ai",
|
|
id: "draft-ai",
|
|
content: "Here is the report",
|
|
tool_calls: [
|
|
{
|
|
id: "draft-present",
|
|
name: "present_files",
|
|
args: { filepaths: [filepath] },
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
{ thread_id: threadB, title: "Draft navigation B", messages: [] },
|
|
],
|
|
});
|
|
let saved = original;
|
|
let putCount = 0;
|
|
const sha256 = () => createHash("sha256").update(saved).digest("hex");
|
|
await page.route(
|
|
`**/api/threads/${threadA}/artifacts${filepath}`,
|
|
(route) => {
|
|
if (route.request().method() === "PUT") {
|
|
const body = route.request().postDataJSON() as {
|
|
content: string;
|
|
expected_sha256: string;
|
|
};
|
|
expect(body.expected_sha256).toBe(sha256());
|
|
saved = body.content;
|
|
putCount++;
|
|
return route.fulfill({
|
|
contentType: "application/json",
|
|
body: JSON.stringify({
|
|
path: filepath,
|
|
sha256: sha256(),
|
|
size: Buffer.byteLength(saved),
|
|
}),
|
|
});
|
|
}
|
|
return route.fulfill({
|
|
contentType: "text/plain; charset=utf-8",
|
|
headers: { ETag: `"${sha256()}"` },
|
|
body: saved,
|
|
});
|
|
},
|
|
);
|
|
|
|
await page.goto(`/workspace/chats/${threadA}`);
|
|
await page
|
|
.getByText("navigation-report.txt", { exact: true })
|
|
.first()
|
|
.click();
|
|
const panel = page.locator("#artifacts");
|
|
await expect(panel.locator(".cm-content")).toHaveText(original);
|
|
await panel.getByRole("button", { name: "Edit", exact: true }).click();
|
|
await panel.locator('.cm-content[contenteditable="true"]').fill(draft);
|
|
await expect(panel.getByText("Unsaved", { exact: true })).toHaveCount(1);
|
|
await page.screenshot({
|
|
path: "output/playwright/artifact-draft-before-navigation.png",
|
|
});
|
|
|
|
await navigateToChat(page, threadB);
|
|
await navigateToChat(page, threadA);
|
|
await expect(panel.locator(".cm-content")).toHaveText(draft);
|
|
await expect(panel.getByText("Unsaved", { exact: true })).toHaveCount(1);
|
|
await panel.getByRole("button", { name: "Edit", exact: true }).click();
|
|
await panel.getByRole("button", { name: "Save", exact: true }).click();
|
|
await expect(
|
|
panel.getByRole("button", { name: "Save", exact: true }),
|
|
).toBeDisabled();
|
|
expect(putCount).toBe(1);
|
|
expect(saved).toBe(draft);
|
|
|
|
await navigateToChat(page, threadB);
|
|
await navigateToChat(page, threadA);
|
|
await expect(panel.locator(".cm-content")).toHaveText(draft);
|
|
await expect(panel.getByText("Unsaved", { exact: true })).toHaveCount(0);
|
|
await panel.getByRole("button", { name: "Edit", exact: true }).click();
|
|
await panel
|
|
.locator('.cm-content[contenteditable="true"]')
|
|
.fill("Discard this edit");
|
|
page.once("dialog", (dialog) => dialog.accept());
|
|
await panel
|
|
.getByRole("button", { name: "Discard changes", exact: true })
|
|
.click();
|
|
await navigateToChat(page, threadB);
|
|
await navigateToChat(page, threadA);
|
|
await expect(panel.locator(".cm-content")).toHaveText(draft);
|
|
await expect(panel.getByText("Unsaved", { exact: true })).toHaveCount(0);
|
|
await page.screenshot({
|
|
path: "output/playwright/artifact-draft-after-save-return.png",
|
|
});
|
|
});
|