mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-11-08 03:30:34 +00:00
* feat(frontend): add token usage dashboard over console APIs * fix(frontend): address token usage dashboard review feedback * fix(frontend): route usage runs to their custom agent chats * docs(frontend): keep agent guidance within the module budget
201 lines
7.0 KiB
TypeScript
201 lines
7.0 KiB
TypeScript
import { expect, test } from "@playwright/test";
|
|
|
|
import { mockLangGraphAPI } from "./utils/mock-api";
|
|
|
|
test.use({ timezoneId: "Asia/Shanghai" });
|
|
|
|
function summary(days: number) {
|
|
const tokens = days === 7 ? 3000 : 10000;
|
|
return {
|
|
total_tokens: tokens,
|
|
total_runs: 3,
|
|
total_cost: 0.012,
|
|
currency: "USD",
|
|
days: Array.from({ length: days }, (_, index) => {
|
|
const weight = [0.2, 0.3, 0.5][index - (days - 3)] ?? 0;
|
|
const totalTokens = tokens * weight;
|
|
const outputTokens = index === days - 1 ? 100 : 0;
|
|
return {
|
|
date: new Date(Date.UTC(2026, 9, 25 - days + index))
|
|
.toISOString()
|
|
.slice(0, 10),
|
|
total_tokens: totalTokens,
|
|
input_tokens: totalTokens - outputTokens,
|
|
output_tokens: outputTokens,
|
|
runs: weight > 0 ? 1 : 0,
|
|
cost: 0.012 * weight,
|
|
};
|
|
}),
|
|
by_model: {
|
|
"deepseek-flash": {
|
|
tokens,
|
|
runs: 3,
|
|
cost: 0.012,
|
|
input_tokens: tokens - 100,
|
|
cache_read_tokens: 500,
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
test("switches the usage period, sends browser offset and exposes daily details", async ({
|
|
page,
|
|
}, testInfo) => {
|
|
await page.setViewportSize({ width: 1440, height: 1200 });
|
|
mockLangGraphAPI(page, { threads: [] });
|
|
const offsets: string[] = [];
|
|
await page.route("**/api/console/usage?*", async (route) => {
|
|
const query = new URL(route.request().url()).searchParams;
|
|
offsets.push(query.get("tz_offset_minutes") ?? "");
|
|
await route.fulfill({ json: summary(Number(query.get("days"))) });
|
|
});
|
|
await page.route("**/api/console/runs?*", (route) =>
|
|
route.fulfill({ json: { runs: [], has_more: false } }),
|
|
);
|
|
await page.goto("/workspace/chats/new");
|
|
await page
|
|
.getByRole("link", { name: "Token usage", exact: true })
|
|
.first()
|
|
.click();
|
|
await expect(page.getByTestId("usage-total-tokens")).toHaveText("3,000");
|
|
await expect(page.getByTestId("usage-total-cost")).toHaveCount(0);
|
|
await page.getByRole("button", { name: "Last 30 days" }).click();
|
|
await expect(page.getByTestId("usage-total-tokens")).toHaveText("10,000");
|
|
await page.getByText("View daily details", { exact: true }).click();
|
|
await expect(
|
|
page.getByRole("table", { name: "Daily token usage", exact: true }),
|
|
).toBeVisible();
|
|
expect(offsets.length).toBeGreaterThan(1);
|
|
expect(offsets.every((value) => value === "480")).toBe(true);
|
|
await expect(
|
|
page.getByRole("link", { name: "Token usage", exact: true }).first(),
|
|
).toBeVisible();
|
|
await page.getByText("View daily details", { exact: true }).click();
|
|
await page
|
|
.getByRole("heading", { name: "Token usage", exact: true })
|
|
.scrollIntoViewIfNeeded();
|
|
await testInfo.attach("usage-desktop", {
|
|
body: await page.screenshot({
|
|
path: testInfo.outputPath("usage-desktop.png"),
|
|
fullPage: true,
|
|
}),
|
|
contentType: "image/png",
|
|
});
|
|
});
|
|
|
|
test("recovers from reporting unavailability and shows token counts", async ({
|
|
page,
|
|
}) => {
|
|
mockLangGraphAPI(page, { threads: [] });
|
|
let available = false;
|
|
await page.route("**/api/console/usage?*", (route) => {
|
|
return !available
|
|
? route.fulfill({ status: 503, json: { detail: "database unavailable" } })
|
|
: route.fulfill({ json: summary(7) });
|
|
});
|
|
await page.route("**/api/console/runs?*", (route) =>
|
|
route.fulfill({ json: { runs: [], has_more: false } }),
|
|
);
|
|
await page.goto("/workspace/usage");
|
|
await expect(
|
|
page.getByRole("alert").filter({ hasText: "SQLite or PostgreSQL" }),
|
|
).toBeVisible();
|
|
await expect(page.getByTestId("usage-total-cost")).toHaveCount(0);
|
|
available = true;
|
|
await page.getByRole("button", { name: "Retry", exact: true }).click();
|
|
await expect(page.getByTestId("usage-total-tokens")).toHaveText("3,000");
|
|
});
|
|
|
|
test("pages run history and resets its offset when filtering failures", async ({
|
|
page,
|
|
}) => {
|
|
mockLangGraphAPI(page, { threads: [] });
|
|
await page.route("**/api/console/usage?*", (route) =>
|
|
route.fulfill({ json: summary(7) }),
|
|
);
|
|
const requests: Array<{ offset: string; status: string | null }> = [];
|
|
await page.route("**/api/console/runs?*", (route) => {
|
|
const query = new URL(route.request().url()).searchParams;
|
|
const offset = query.get("offset") ?? "0";
|
|
const status = query.get("status");
|
|
requests.push({ offset, status });
|
|
return route.fulfill({
|
|
json: {
|
|
has_more: offset === "0",
|
|
runs: [
|
|
{
|
|
run_id: `run-${offset}-${status ?? "all"}`,
|
|
thread_id: "thread/a #?",
|
|
thread_title: offset === "0" ? "Latest report" : "Older report",
|
|
assistant_id: null,
|
|
status: status ?? "success",
|
|
model_name: "deepseek-flash",
|
|
created_at: "2026-10-25T08:00:00Z",
|
|
updated_at: null,
|
|
duration_seconds: 6.2,
|
|
total_tokens: 100,
|
|
message_count: 2,
|
|
cost: 0.012,
|
|
error: status === "error" ? "An upstream request failed" : null,
|
|
},
|
|
],
|
|
},
|
|
});
|
|
});
|
|
await page.goto("/workspace/usage");
|
|
await expect(
|
|
page.getByRole("link", { name: "Latest report" }),
|
|
).toHaveAttribute("href", "/workspace/chats/thread%2Fa%20%23%3F");
|
|
await page.getByRole("button", { name: "Next page" }).click();
|
|
await expect(page.getByRole("link", { name: "Older report" })).toBeVisible();
|
|
await page
|
|
.getByRole("combobox", { name: "Run status" })
|
|
.selectOption("error");
|
|
await expect(page.getByText("Page 1", { exact: true })).toBeVisible();
|
|
await expect(page.getByText("Failed", { exact: true }).last()).toBeVisible();
|
|
await page.getByText("Error details", { exact: true }).click();
|
|
await expect(page.getByText("An upstream request failed")).toBeVisible();
|
|
expect(requests).toContainEqual({ offset: "20", status: null });
|
|
expect(requests).toContainEqual({ offset: "0", status: "error" });
|
|
});
|
|
|
|
test("renders Chinese on mobile without widening the document", async ({
|
|
page,
|
|
context,
|
|
baseURL,
|
|
}, testInfo) => {
|
|
await page.setViewportSize({ width: 390, height: 844 });
|
|
await context.addCookies([
|
|
{ name: "locale", value: "zh-CN", url: baseURL ?? "http://localhost:3000" },
|
|
]);
|
|
mockLangGraphAPI(page, { threads: [] });
|
|
await page.route("**/api/console/usage?*", (route) =>
|
|
route.fulfill({ json: summary(7) }),
|
|
);
|
|
await page.route("**/api/console/runs?*", (route) =>
|
|
route.fulfill({ json: { runs: [], has_more: false } }),
|
|
);
|
|
await page.goto("/workspace/usage");
|
|
await expect(
|
|
page.getByRole("heading", { name: "Token 用量", exact: true }),
|
|
).toBeVisible();
|
|
await expect(page.getByTestId("usage-total-cost")).toHaveCount(0);
|
|
await expect(
|
|
page.getByRole("button", { name: "近 30 天", exact: true }),
|
|
).toBeVisible();
|
|
await expect
|
|
.poll(() =>
|
|
page.evaluate(
|
|
() => document.documentElement.scrollWidth <= window.innerWidth,
|
|
),
|
|
)
|
|
.toBe(true);
|
|
await testInfo.attach("usage-mobile-zh", {
|
|
body: await page.screenshot({
|
|
path: testInfo.outputPath("usage-mobile-zh.png"),
|
|
fullPage: true,
|
|
}),
|
|
contentType: "image/png",
|
|
});
|
|
});
|