源码
tests/desktop/custom-commands.spec.ts
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import type { ElectronApplication } from "@playwright/test";
import { chunk, done, fakeServer, send } from "../fake-server.ts";
import { runCleanup } from "../helpers/cleanup.ts";
import { launchDesktop } from "../helpers/desktop.ts";
import { expect, test } from "../helpers/test.ts";
test("commands settings create, search, persist switches, edit conflicts, and confirm deletion", async () => {
const dir = await mkdtemp(join(tmpdir(), "zpi-commands-settings-"));
const project = join(dir, "project");
await mkdir(project);
const server = await fakeServer((_, response) => done(response));
let app: ElectronApplication | undefined;
try {
app = await launchDesktop({ dir, project, url: server.url });
const page = await app.firstWindow();
await page.getByRole("button", { name: "添加项目", exact: true }).first().click();
await page.getByRole("button", { name: "设置", exact: true }).click();
await page.getByRole("button", { name: "命令", exact: true }).click();
await page.getByRole("button", { name: "新建", exact: true }).first().click();
const breadcrumb = page.getByRole("navigation", { name: "命令导航", exact: true });
await expect(breadcrumb.locator('[aria-current="page"]')).toHaveText("新建命令");
await breadcrumb.getByRole("button", { name: "返回命令列表", exact: true }).click();
await expect(breadcrumb).toHaveCount(0);
await page.getByRole("button", { name: "新建", exact: true }).first().click();
await page.getByLabel("名称", { exact: true }).fill("review");
await page.getByLabel("描述(可选)", { exact: true }).fill("Review selected files");
await page.getByLabel("参数提示(可选)", { exact: true }).fill("<file>");
await page.getByRole("textbox", { name: "提示词", exact: true }).fill("Review $ARGUMENTS");
await page.getByRole("button", { name: "保存", exact: true }).click();
const row = page.locator(".command-row").filter({ hasText: "/review" });
await expect(row).toBeVisible();
expect(
await row.locator(".command-avatar").evaluate((element) => element.getBoundingClientRect().width),
).toBe(36);
await page.getByLabel("搜索命令", { exact: true }).fill("selected");
await expect(row).toBeVisible();
await page.getByLabel("搜索命令", { exact: true }).fill("missing");
await expect(page.getByText("没有匹配的命令", { exact: true })).toBeVisible();
await page.getByLabel("搜索命令", { exact: true }).fill("");
await row.getByRole("checkbox").click();
await expect(row.getByRole("checkbox")).not.toBeChecked();
await page.getByRole("button", { name: "刷新命令", exact: true }).click();
await expect(row.getByRole("checkbox")).not.toBeChecked();
await row.getByRole("button", { name: "编辑命令 /review", exact: true }).click();
await expect(page.getByLabel("名称", { exact: true })).toBeDisabled();
const filePath = await page.evaluate(async () => {
const result = await window.zpi.getCustomCommands();
if (!result.ok) throw new Error(result.error.message);
return result.value.commands[0].filePath;
});
await writeFile(filePath, "EXTERNAL EDIT");
await page.getByRole("textbox", { name: "提示词", exact: true }).fill("STALE SAVE");
await page.getByRole("button", { name: "保存", exact: true }).click();
await expect(page.getByRole("alert")).toBeVisible();
expect(await readFile(filePath, "utf8")).toBe("EXTERNAL EDIT");
await page.getByRole("button", { name: "取消", exact: true }).click();
await page.getByRole("button", { name: "刷新命令", exact: true }).click();
await row.getByRole("button", { name: "编辑命令 /review", exact: true }).click();
await page.getByRole("button", { name: "删除", exact: true }).click();
await page
.getByRole("dialog", { name: "删除命令", exact: true })
.getByRole("button", { name: "取消", exact: true })
.click();
expect(await readFile(filePath, "utf8")).toBe("EXTERNAL EDIT");
await page.getByRole("button", { name: "删除", exact: true }).click();
await page
.getByRole("dialog", { name: "删除命令", exact: true })
.getByRole("button", { name: "删除", exact: true })
.click();
await expect(row).toHaveCount(0);
expect(server.requests).toHaveLength(0);
} finally {
await runCleanup([
() => rm(dir, { recursive: true, force: true }),
() => server.close(),
() => app?.close(),
]);
}
});
test("project commands appear in slash picker, insert without sending, and execute in their session", async () => {
const dir = await mkdtemp(join(tmpdir(), "zpi-commands-composer-"));
const project = join(dir, "project");
await mkdir(join(project, ".zpi", "commands"), { recursive: true });
await writeFile(
join(project, ".zpi", "commands", "review.md"),
"---\ndescription: Review this project\nargument-hint: <file>\n---\nPROJECT REVIEW $ARGUMENTS",
);
const server = await fakeServer((_, response) => {
send(response, chunk({ content: "reviewed" }));
done(response);
});
let app: ElectronApplication | undefined;
try {
app = await launchDesktop({ dir, project, url: server.url });
const page = await app.firstWindow();
await page.getByRole("button", { name: "添加项目", exact: true }).first().click();
const editor = page.getByLabel("消息", { exact: true });
await page.getByRole("button", { name: "收起项目 project", exact: true }).hover();
await page.getByRole("button", { name: "新建任务 project", exact: true }).click();
await editor.fill("/rev");
await expect(page.getByRole("option").filter({ hasText: "/review" })).toBeVisible();
await editor.press("Tab");
await expect(editor.locator(".inline-mention.command")).toHaveText("review");
await expect(editor.locator(".inline-mention.command")).toHaveAttribute("data-markdown", "/review");
expect(server.requests).toHaveLength(0);
await editor.pressSequentially("file.ts");
await editor.press("Enter");
await expect.poll(() => server.requests.length).toBe(1);
expect(JSON.stringify(server.requests[0].messages)).toContain("PROJECT REVIEW file.ts");
await expect(page.getByText("reviewed", { exact: true })).toBeVisible();
} finally {
await runCleanup([
() => rm(dir, { recursive: true, force: true }),
() => server.close(),
() => app?.close(),
]);
}
});
test("subagent slash selection inserts a canonical chip without sending or spawning", async () => {
const dir = await mkdtemp(join(tmpdir(), "zpi-command-subagents-"));
const project = join(dir, "project");
await mkdir(project);
const server = await fakeServer((_, response) => done(response));
let app: ElectronApplication | undefined;
try {
app = await launchDesktop({ dir, project, url: server.url });
const page = await app.firstWindow();
await page.getByRole("button", { name: "添加项目", exact: true }).first().click();
const profileId = await page.evaluate(async () => {
const enabled = await window.zpi.setPluginEnabled("zpi.collaboration", true);
if (!enabled.ok) throw new Error(enabled.error.message);
const result = await window.zpi.saveSubagent({
sessionId: null,
scope: "user",
config: {
name: "Fixture-reviewer",
description: "Temporary slash picker reviewer",
systemPrompt: "Review the task supplied by the parent.",
color: "blue",
injectAgentsMd: false,
tools: ["read"],
},
});
if (!result.ok) throw new Error(result.error.message);
const profile = result.value.agents.find((agent) => agent.name === "Fixture-reviewer");
if (!profile) throw new Error("Missing fixture profile");
return profile.id;
});
await page.reload();
const editor = page.getByLabel("消息", { exact: true });
await editor.fill("/Fixture");
const slash = page.getByRole("listbox", { name: "指令", exact: true });
await expect(slash.locator(".command-section-title")).toHaveText(["指令", "技能", "子智能体"]);
const option = slash.getByRole("option", { name: /^Fixture-reviewer/ });
await expect(option).toBeVisible();
await expect(option).toHaveAttribute("aria-selected", "true");
expect(server.requests).toHaveLength(0);
await editor.press("Tab");
await expect(slash).toBeHidden();
await expect(editor.locator(".inline-mention.subagent")).toHaveText("Fixture-reviewer");
await expect(editor.locator(".inline-mention.subagent")).toHaveAttribute(
"data-markdown",
"@Fixture-reviewer",
);
await expect(editor.locator(".inline-mention.file")).toHaveCount(0);
await expect
.poll(async () =>
page.evaluate(async () => {
const id = localStorage.getItem("zpi.selectedSession");
if (!id) throw new Error("Missing draft session");
const draft = await window.zpi.getDraft(id);
if (!draft.ok) throw new Error(draft.error.message);
return { text: draft.value.text, files: draft.value.fileReferences };
}),
)
.toEqual({ text: "@Fixture-reviewer ", files: [] });
const snapshot = await page.evaluate(async () => {
const id = localStorage.getItem("zpi.selectedSession");
if (!id) throw new Error("Missing draft session");
const result = await window.zpi.getSessionSnapshot(id);
if (!result.ok) throw new Error(result.error.message);
return { runs: result.value.view.runs.length, subagents: result.value.view.subagents ?? [] };
});
expect(snapshot).toEqual({ runs: 0, subagents: [] });
expect(server.requests).toHaveLength(0);
// A disabled profile must disappear, even when its @mention remains in a saved draft.
await page.evaluate(async (id) => {
const result = await window.zpi.setSubagentEnabled(id, false, null);
if (!result.ok) throw new Error(result.error.message);
}, profileId);
await page.reload();
await expect(editor).toContainText("@Fixture-reviewer");
await expect(editor.locator(".inline-mention.subagent")).toHaveCount(0);
await editor.fill("/Fixture");
// Built-in agents can still match the query through their fuzzy description search.
await expect(slash).toBeVisible();
await expect(option).toHaveCount(0);
expect(server.requests).toHaveLength(0);
} finally {
await runCleanup([
() => rm(dir, { recursive: true, force: true }),
() => server.close(),
() => app?.close(),
]);
}
});