deer-flow/frontend/tests/unit/components/workspace/agents/agent-display-name.dom.test.tsx
yang rui ac2b6415ea
feat(agents): support Unicode display names for custom agents (#5324)
* feat(agents): support Unicode display names for custom agents

* fix(agents): preserve and validate Unicode display names

* fix(agents): tolerate invalid stored labels and reject invisible names

* fix(agents): identify agent in invalid display name warning

* style(frontend): format agent display name fallback

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-09-12 11:09:41 +08:00

116 lines
3.8 KiB
TypeScript

import { afterEach, describe, expect, it, rs } from "@rstest/core";
import {
cleanup,
fireEvent,
render,
screen,
waitFor,
} from "@testing-library/react";
import { toast } from "sonner";
import { AgentWelcome } from "@/components/workspace/agent-welcome";
import { AgentSettingsDialog } from "@/components/workspace/agents/agent-settings-dialog";
import type { Agent } from "@/core/agents";
import { enUS } from "@/core/i18n/locales/en-US";
const { mutateAsync } = rs.hoisted(() => ({
mutateAsync: rs.fn().mockResolvedValue({}),
}));
rs.mock("@/core/agents", () => ({
useUpdateAgent: () => ({ mutateAsync, isPending: false }),
}));
rs.mock("@/core/models/hooks", () => ({ useModels: () => ({ models: [] }) }));
rs.mock("@/core/subagents", () => ({
useSubagents: () => ({ subagents: [] }),
}));
rs.mock("@/core/i18n/hooks", () => ({ useI18n: () => ({ t: enUS }) }));
rs.mock("sonner", () => ({ toast: { success: rs.fn(), error: rs.fn() } }));
const agent: Agent = {
name: "reviewer",
display_name: "代码审查助手",
description: "",
model: null,
tool_groups: null,
skills: null,
};
afterEach(() => {
cleanup();
mutateAsync.mockClear();
});
describe("custom agent display names", () => {
it("accepts 100 astral code points without an HTML code-unit limit", async () => {
render(<AgentSettingsDialog agent={agent} open onOpenChange={rs.fn()} />);
const input = screen.getByLabelText("Display name");
expect(input.hasAttribute("maxlength")).toBe(false);
fireEvent.change(input, { target: { value: "🦌".repeat(100) } });
fireEvent.click(screen.getByRole("button", { name: "Save" }));
await waitFor(() =>
expect(mutateAsync).toHaveBeenCalledWith(
expect.objectContaining({
request: expect.objectContaining({ display_name: "🦌".repeat(100) }),
}),
),
);
});
it("rejects 101 code points before saving", () => {
render(<AgentSettingsDialog agent={agent} open onOpenChange={rs.fn()} />);
fireEvent.change(screen.getByLabelText("Display name"), {
target: { value: "🦌".repeat(101) },
});
fireEvent.click(screen.getByRole("button", { name: "Save" }));
expect(mutateAsync).not.toHaveBeenCalled();
expect(toast.error).toHaveBeenCalled();
});
it("shows Unicode names and falls back for legacy or cleared names", () => {
const { rerender } = render(
<AgentWelcome agent={agent} agentName="reviewer" />,
);
expect(screen.getByText("代码审查助手")).toBeTruthy();
for (const display_name of [undefined, null, ""]) {
rerender(
<AgentWelcome
agent={{ ...agent, display_name }}
agentName="reviewer"
/>,
);
expect(screen.getByText("reviewer")).toBeTruthy();
}
});
it("saves a display name using the stable identifier", async () => {
render(<AgentSettingsDialog agent={agent} open onOpenChange={rs.fn()} />);
fireEvent.change(screen.getByLabelText("Display name"), {
target: { value: "审查员 🦌" },
});
fireEvent.click(screen.getByRole("button", { name: "Save" }));
await waitFor(() =>
expect(mutateAsync).toHaveBeenCalledWith(
expect.objectContaining({
name: "reviewer",
request: expect.objectContaining({ display_name: "审查员 🦌" }),
}),
),
);
});
it("clears a blank name without renaming the agent", async () => {
render(<AgentSettingsDialog agent={agent} open onOpenChange={rs.fn()} />);
fireEvent.change(screen.getByLabelText("Display name"), {
target: { value: " " },
});
fireEvent.click(screen.getByRole("button", { name: "Save" }));
await waitFor(() =>
expect(mutateAsync).toHaveBeenCalledWith(
expect.objectContaining({
name: "reviewer",
request: expect.objectContaining({ display_name: null }),
}),
),
);
});
});