mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-09-15 00:19:14 +00:00
* feat(skills): export custom skill packages with revision preview * docs(gateway): keep export guidance within size budget * ci: retry checks after transient uv setup download failure * docs: focus skill export agent guidance on maintenance invariants * fix(skills): handle export disconnects and bound archive transfers * docs(gateway): remove redundant export guidance to fit merged budget * fix(skills): reset export idle deadline after transfer progress
145 lines
4.4 KiB
TypeScript
145 lines
4.4 KiB
TypeScript
import { expect, test } from "@playwright/test";
|
|
|
|
import { mockLangGraphAPI } from "./utils/mock-api";
|
|
|
|
const preview = {
|
|
skill_name: "demo",
|
|
revision: "a".repeat(64),
|
|
can_export: true,
|
|
file_count: 1,
|
|
directory_count: 1,
|
|
total_bytes: 20,
|
|
files: [{ path: "SKILL.md", type: "file", size: 20, executable: false }],
|
|
requirements: {
|
|
compatibility: "Python 3.12",
|
|
allowed_tools: ["bash"],
|
|
required_secrets: [{ name: "DEMO_TOKEN", optional: false }],
|
|
},
|
|
warnings: [],
|
|
blockers: [],
|
|
};
|
|
|
|
test("custom export previews, handles stale content and downloads only after refresh", async ({
|
|
page,
|
|
}) => {
|
|
mockLangGraphAPI(page, {
|
|
skills: [
|
|
{
|
|
name: "demo",
|
|
description: "Synthetic UI fixture",
|
|
category: "custom",
|
|
enabled: false,
|
|
},
|
|
{
|
|
name: "public-demo",
|
|
description: "Public fixture",
|
|
category: "public",
|
|
},
|
|
],
|
|
});
|
|
let previews = 0,
|
|
downloads = 0;
|
|
await page.route("**/api/skills/custom/demo/export-manifest", (route) => {
|
|
previews++;
|
|
return route.fulfill({ json: preview });
|
|
});
|
|
await page.route("**/api/skills/custom/demo/export?*", (route) => {
|
|
downloads++;
|
|
expect(
|
|
new URL(route.request().url()).searchParams.get("expected_revision"),
|
|
).toBe(preview.revision);
|
|
return downloads === 1
|
|
? route.fulfill({
|
|
status: 409,
|
|
json: { detail: { code: "skill_changed", message: "Changed" } },
|
|
})
|
|
: route.fulfill({
|
|
contentType: "application/zip",
|
|
body: Buffer.from("synthetic transport fixture"),
|
|
});
|
|
});
|
|
await page.goto("/workspace/chats/new?settings=skills");
|
|
await expect(
|
|
page.getByRole("button", { name: "Export public-demo" }),
|
|
).toHaveCount(0);
|
|
await page.getByRole("tab", { name: "Custom", exact: true }).click();
|
|
await page.getByRole("button", { name: "Export demo", exact: true }).click();
|
|
const dialog = page.getByRole("dialog", {
|
|
name: "Export skill",
|
|
exact: true,
|
|
});
|
|
await expect(dialog.getByText("DEMO_TOKEN (required)")).toBeVisible();
|
|
await dialog.getByRole("button", { name: "Download .skill" }).click();
|
|
await expect(
|
|
dialog.getByText(
|
|
"The skill changed. Refresh the file list before downloading.",
|
|
),
|
|
).toBeVisible();
|
|
await expect(
|
|
dialog.getByRole("button", { name: "Download .skill" }),
|
|
).toBeDisabled();
|
|
const beforeRefresh = previews;
|
|
await dialog.getByRole("button", { name: "Refresh file list" }).click();
|
|
await expect(
|
|
dialog.getByRole("button", { name: "Download .skill" }),
|
|
).toBeEnabled();
|
|
const download = page.waitForEvent("download");
|
|
await dialog.getByRole("button", { name: "Download .skill" }).click();
|
|
expect((await download).suggestedFilename()).toBe("demo.skill");
|
|
await expect(
|
|
dialog.getByText("File handed to your browser for download."),
|
|
).toBeVisible();
|
|
expect(previews).toBe(beforeRefresh + 1);
|
|
expect(downloads).toBe(2);
|
|
await page.keyboard.press("Escape");
|
|
await expect(dialog).toBeHidden();
|
|
await expect(
|
|
page.getByRole("dialog", { name: "Settings", exact: true }),
|
|
).toBeVisible();
|
|
});
|
|
|
|
test("mobile manifest blockers are readable and cannot download", async ({
|
|
page,
|
|
}) => {
|
|
await page.setViewportSize({ width: 390, height: 844 });
|
|
mockLangGraphAPI(page, {
|
|
skills: [
|
|
{ name: "demo", description: "Synthetic UI fixture", category: "custom" },
|
|
],
|
|
});
|
|
await page.route("**/api/skills/custom/demo/export-manifest", (route) =>
|
|
route.fulfill({
|
|
json: {
|
|
...preview,
|
|
revision: null,
|
|
can_export: false,
|
|
blockers: [
|
|
{
|
|
code: "skill_export_link",
|
|
message: "Linked files or directories cannot be exported.",
|
|
path: "scripts/linked",
|
|
},
|
|
],
|
|
},
|
|
}),
|
|
);
|
|
await page.goto("/workspace/chats/new?settings=skills");
|
|
await page.getByRole("tab", { name: "Custom", exact: true }).click();
|
|
await page.getByRole("button", { name: "Export demo", exact: true }).click();
|
|
const dialog = page.getByRole("dialog", {
|
|
name: "Export skill",
|
|
exact: true,
|
|
});
|
|
await expect(
|
|
dialog.getByText("This package cannot be exported"),
|
|
).toBeVisible();
|
|
await expect(
|
|
dialog.getByRole("button", { name: "Download .skill" }),
|
|
).toHaveCount(0);
|
|
expect(
|
|
await page.evaluate(
|
|
() => document.documentElement.scrollWidth <= innerWidth,
|
|
),
|
|
).toBe(true);
|
|
});
|