mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-06-09 17:12:01 +00:00
* support slash skill activation * format slash skill activation * Preserve slash skill activation with uploads * Address slash skill review feedback * Address slash skill follow-up review * Fix lazy slash skill storage resolution * Keep slash skill activation out of system prompt * Address slash skill review issues * fix: harden slash skill command handling * feat(frontend): add slash skill autocomplete * fix: address slash skill review feedback * fix: preserve slash skill text for IM uploads
313 lines
9.3 KiB
TypeScript
313 lines
9.3 KiB
TypeScript
import { expect, test } from "@playwright/test";
|
|
|
|
import { handleRunStream, mockLangGraphAPI } from "./utils/mock-api";
|
|
|
|
test.describe("Chat workspace", () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
mockLangGraphAPI(page);
|
|
});
|
|
|
|
test("new chat page loads with input box", async ({ page }) => {
|
|
await page.goto("/workspace/chats/new");
|
|
|
|
const textarea = page.getByPlaceholder(/how can i assist you/i);
|
|
await expect(textarea).toBeVisible({ timeout: 15_000 });
|
|
});
|
|
|
|
test("can type a message in the input box", async ({ page }) => {
|
|
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("Hello, DeerFlow!");
|
|
await expect(textarea).toHaveValue("Hello, DeerFlow!");
|
|
});
|
|
|
|
test("suggests matching skills after a leading slash", async ({ page }) => {
|
|
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("/dat");
|
|
await expect(
|
|
page.getByRole("option", { name: /data-analysis/i }),
|
|
).toBeVisible();
|
|
await expect(
|
|
page.getByRole("option", { name: /disabled-skill/i }),
|
|
).toBeHidden();
|
|
|
|
await textarea.press("Enter");
|
|
|
|
await expect(textarea).toHaveValue("/data-analysis ");
|
|
});
|
|
|
|
test("keeps Shift+Enter as newline while skill suggestions are visible", async ({
|
|
page,
|
|
}) => {
|
|
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("/dat");
|
|
await expect(
|
|
page.getByRole("option", { name: /data-analysis/i }),
|
|
).toBeVisible();
|
|
|
|
await textarea.press("Shift+Enter");
|
|
|
|
await expect(textarea).toHaveValue("/dat\n");
|
|
await expect(
|
|
page.getByRole("option", { name: /data-analysis/i }),
|
|
).toBeHidden();
|
|
});
|
|
|
|
test("does not suggest skills for slash text away from the prompt start", async ({
|
|
page,
|
|
}) => {
|
|
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("please /dat");
|
|
|
|
await expect(
|
|
page.getByRole("option", { name: /data-analysis/i }),
|
|
).toBeHidden();
|
|
});
|
|
|
|
test("sending a message triggers API call and shows response", async ({
|
|
page,
|
|
}) => {
|
|
let streamCalled = false;
|
|
await page.route("**/runs/stream", (route) => {
|
|
streamCalled = true;
|
|
return handleRunStream(route);
|
|
});
|
|
|
|
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("Hello");
|
|
await textarea.press("Enter");
|
|
|
|
await expect.poll(() => streamCalled, { timeout: 10_000 }).toBeTruthy();
|
|
|
|
// The AI response should appear in the chat
|
|
await expect(page.getByText("Hello from DeerFlow!")).toBeVisible({
|
|
timeout: 10_000,
|
|
});
|
|
});
|
|
|
|
test("slash skill command is submitted as normal chat text", async ({
|
|
page,
|
|
}) => {
|
|
const slashCommand = "/data-analysis analyze uploads/foo.csv";
|
|
let submittedText: string | undefined;
|
|
await page.route("**/runs/stream", (route) => {
|
|
const body = route.request().postDataJSON() as {
|
|
input?: { messages?: Array<{ content?: unknown }> };
|
|
};
|
|
const content = body.input?.messages?.at(-1)?.content;
|
|
if (typeof content === "string") {
|
|
submittedText = content;
|
|
} else if (Array.isArray(content)) {
|
|
submittedText = content
|
|
.map((block) =>
|
|
typeof block === "object" &&
|
|
block !== null &&
|
|
"text" in block &&
|
|
typeof block.text === "string"
|
|
? block.text
|
|
: "",
|
|
)
|
|
.join("");
|
|
}
|
|
return handleRunStream(route);
|
|
});
|
|
|
|
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(slashCommand);
|
|
await textarea.press("Enter");
|
|
|
|
await expect
|
|
.poll(() => submittedText, { timeout: 10_000 })
|
|
.toBe(slashCommand);
|
|
await expect(page.getByText("Hello from DeerFlow!")).toBeVisible({
|
|
timeout: 10_000,
|
|
});
|
|
});
|
|
|
|
test("slash skill command with attachment preserves command text and file metadata", async ({
|
|
page,
|
|
}) => {
|
|
const slashCommand = "/data-analysis analyze report.docx";
|
|
let uploadCalled = false;
|
|
let submittedText: string | undefined;
|
|
let submittedFiles:
|
|
| Array<{ filename?: string; path?: string; status?: string }>
|
|
| undefined;
|
|
|
|
await page.route("**/api/threads/*/uploads", async (route) => {
|
|
uploadCalled = true;
|
|
return route.fulfill({
|
|
status: 200,
|
|
contentType: "application/json",
|
|
body: JSON.stringify({
|
|
success: true,
|
|
message: "Uploaded",
|
|
files: [
|
|
{
|
|
filename: "report.docx",
|
|
size: 12,
|
|
path: "report.docx",
|
|
virtual_path: "/mnt/user-data/uploads/report.docx",
|
|
artifact_url: "/api/threads/test/uploads/report.docx",
|
|
extension: ".docx",
|
|
},
|
|
],
|
|
}),
|
|
});
|
|
});
|
|
|
|
await page.route("**/runs/stream", (route) => {
|
|
const body = route.request().postDataJSON() as {
|
|
input?: {
|
|
messages?: Array<{
|
|
content?: unknown;
|
|
additional_kwargs?: {
|
|
files?: Array<{
|
|
filename?: string;
|
|
path?: string;
|
|
status?: string;
|
|
}>;
|
|
};
|
|
}>;
|
|
};
|
|
};
|
|
const message = body.input?.messages?.at(-1);
|
|
const content = message?.content;
|
|
if (typeof content === "string") {
|
|
submittedText = content;
|
|
} else if (Array.isArray(content)) {
|
|
submittedText = content
|
|
.map((block) =>
|
|
typeof block === "object" &&
|
|
block !== null &&
|
|
"text" in block &&
|
|
typeof block.text === "string"
|
|
? block.text
|
|
: "",
|
|
)
|
|
.join("");
|
|
}
|
|
submittedFiles = message?.additional_kwargs?.files;
|
|
return handleRunStream(route);
|
|
});
|
|
|
|
await page.goto("/workspace/chats/new");
|
|
|
|
const textarea = page.getByPlaceholder(/how can i assist you/i);
|
|
await expect(textarea).toBeVisible({ timeout: 15_000 });
|
|
|
|
await page.getByLabel("Upload files").setInputFiles({
|
|
name: "report.docx",
|
|
mimeType:
|
|
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
buffer: Buffer.from("fake docx"),
|
|
});
|
|
|
|
await textarea.fill(slashCommand);
|
|
await textarea.press("Enter");
|
|
|
|
await expect.poll(() => uploadCalled, { timeout: 10_000 }).toBeTruthy();
|
|
await expect
|
|
.poll(() => submittedText, { timeout: 10_000 })
|
|
.toBe(slashCommand);
|
|
await expect
|
|
.poll(() => submittedFiles, { timeout: 10_000 })
|
|
.toEqual([
|
|
{
|
|
filename: "report.docx",
|
|
size: 12,
|
|
path: "/mnt/user-data/uploads/report.docx",
|
|
status: "uploaded",
|
|
},
|
|
]);
|
|
await expect(page.getByText("Hello from DeerFlow!")).toBeVisible({
|
|
timeout: 10_000,
|
|
});
|
|
});
|
|
|
|
test("keeps attachments visible while upload submit is pending", async ({
|
|
page,
|
|
}) => {
|
|
let releaseUpload!: () => void;
|
|
const uploadCanFinish = new Promise<void>((resolve) => {
|
|
releaseUpload = resolve;
|
|
});
|
|
let uploadStarted!: () => void;
|
|
const uploadStartedPromise = new Promise<void>((resolve) => {
|
|
uploadStarted = resolve;
|
|
});
|
|
|
|
await page.route("**/api/threads/*/uploads", async (route) => {
|
|
uploadStarted();
|
|
await uploadCanFinish;
|
|
return route.fulfill({
|
|
status: 200,
|
|
contentType: "application/json",
|
|
body: JSON.stringify({
|
|
success: true,
|
|
message: "Uploaded",
|
|
files: [
|
|
{
|
|
filename: "report.docx",
|
|
size: 12,
|
|
path: "report.docx",
|
|
virtual_path: "/mnt/user-data/uploads/report.docx",
|
|
artifact_url: "/api/threads/test/uploads/report.docx",
|
|
extension: ".docx",
|
|
},
|
|
],
|
|
}),
|
|
});
|
|
});
|
|
|
|
await page.goto("/workspace/chats/new");
|
|
|
|
const textarea = page.getByPlaceholder(/how can i assist you/i);
|
|
await expect(textarea).toBeVisible({ timeout: 15_000 });
|
|
const promptForm = page.locator("form").filter({ has: textarea });
|
|
|
|
await page.getByLabel("Upload files").setInputFiles({
|
|
name: "report.docx",
|
|
mimeType:
|
|
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
buffer: Buffer.from("fake docx"),
|
|
});
|
|
await expect(promptForm.getByText("report.docx")).toBeVisible();
|
|
|
|
await textarea.fill("Summarize this document");
|
|
await textarea.press("Enter");
|
|
|
|
await uploadStartedPromise;
|
|
await expect(promptForm.getByText("report.docx")).toBeVisible();
|
|
|
|
releaseUpload();
|
|
await expect(page.getByText("Hello from DeerFlow!")).toBeVisible({
|
|
timeout: 10_000,
|
|
});
|
|
await expect(promptForm.getByText("report.docx")).toBeHidden();
|
|
});
|
|
});
|