feat: add slash command help

This commit is contained in:
2026-07-09 19:43:13 +08:00
parent f260195439
commit f1d29176d3
4 changed files with 432 additions and 43 deletions
+115
View File
@@ -168,6 +168,121 @@ describe("trigger full lifecycle (integration)", () => {
expect(await prisma.agentRun.findMany()).toHaveLength(0);
});
it("/help lists control and role commands without creating a run", async () => {
await seedProject("proj-help", "chat-help");
const trigger = makeTriggerHandler({ prisma, settings, logger: silentLogger, runAgent, messageBatcherOptions: { maxMessages: 1 } });
await trigger(makeEvent("chat-help", "@_user_1 /help"), rt);
const helpText = rt.sentTexts.at(-1) ?? "";
expect(helpText).toContain("可用 slash 命令");
expect(helpText).toContain("/new");
expect(helpText).toContain("/reset");
expect(helpText).toContain("/draft <需求>");
expect(helpText).toContain("/review <需求>");
expect(runAgentCalls).toHaveLength(0);
expect(await prisma.agentRun.findMany()).toHaveLength(0);
});
it("/help is built from the current registry on each request", async () => {
await seedProject("proj-help-live", "chat-help-live");
let currentModels = new InMemoryModelRegistry(
[{ id: "mock-model", label: "Mock", toolCapable: true }],
[{ id: "draft", label: "草稿", defaultModel: "mock-model", systemPrompt: undefined, tools: undefined }],
);
const dynamicSettings: RuntimeSettings = {
async provider(providerId, scope) {
return settings.provider(providerId, scope);
},
async modelRegistry() {
return currentModels;
},
async runPolicy(input) {
return settings.runPolicy(input);
},
};
const trigger = makeTriggerHandler({ prisma, settings: dynamicSettings, logger: silentLogger, runAgent, messageBatcherOptions: { maxMessages: 1 } });
await trigger(makeEvent("chat-help-live", "@_user_1 /help"), rt);
expect(rt.sentTexts.at(-1) ?? "").not.toContain("/coach <需求>");
currentModels = new InMemoryModelRegistry(
[{ id: "mock-model", label: "Mock", toolCapable: true }],
[
{ id: "draft", label: "草稿", defaultModel: "mock-model", systemPrompt: undefined, tools: undefined },
{ id: "coach", label: "教练", defaultModel: "mock-model", systemPrompt: undefined, tools: [] },
],
);
await trigger(makeEvent("chat-help-live", "@_user_1 /help"), rt);
const helpText = rt.sentTexts.at(-1) ?? "";
expect(helpText).toContain("/coach <需求>");
expect(runAgentCalls).toHaveLength(0);
expect(await prisma.agentRun.findMany()).toHaveLength(0);
});
it("/help <command> returns command-specific help", async () => {
await seedProject("proj-help-reset", "chat-help-reset");
const trigger = makeTriggerHandler({ prisma, settings, logger: silentLogger, runAgent, messageBatcherOptions: { maxMessages: 1 } });
await trigger(makeEvent("chat-help-reset", "@_user_1 /help reset"), rt);
const helpText = rt.sentTexts.at(-1) ?? "";
expect(helpText).toContain("/reset");
expect(helpText).toContain("清空当前项目已经排队");
expect(helpText).toContain("/reset help");
expect(runAgentCalls).toHaveLength(0);
expect(await prisma.agentRun.findMany()).toHaveLength(0);
});
it("control commands support a help subcommand", async () => {
await seedProject("proj-new-help", "chat-new-help");
const trigger = makeTriggerHandler({ prisma, settings, logger: silentLogger, runAgent, messageBatcherOptions: { maxMessages: 1 } });
await trigger(makeEvent("chat-new-help", "@_user_1 /new help"), rt);
const helpText = rt.sentTexts.at(-1) ?? "";
expect(helpText).toContain("/new");
expect(helpText).toContain("归档当前未归档");
expect(helpText).toContain("/help new");
expect(rt.sentTexts).not.toContain("已开新会话,下次 @bot 将从头开始。");
expect(runAgentCalls).toHaveLength(0);
expect(await prisma.agentRun.findMany()).toHaveLength(0);
});
it("role commands support a help subcommand without consuming role grants", async () => {
await seedProject("proj-role-help", "chat-role-help");
await prisma.roleTriggerGrant.create({
data: { projectId: "proj-role-help", roleId: "review", principalType: "USER", principalId: "ou_other" },
});
const trigger = makeTriggerHandler({ prisma, settings, logger: silentLogger, runAgent, messageBatcherOptions: { maxMessages: 1 } });
await trigger(makeEvent("chat-role-help", "@_user_1 /review help"), rt);
const helpText = rt.sentTexts.at(-1) ?? "";
expect(helpText).toContain("/review");
expect(helpText).toContain("使用“审校”角色");
expect(helpText).toContain("工具范围: read_file");
expect(rt.sentTexts).not.toContain("无权限使用角色 review。");
expect(runAgentCalls).toHaveLength(0);
expect(await prisma.agentRun.findMany()).toHaveLength(0);
});
it("/help unknown and /unknown help report the unknown command", async () => {
await seedProject("proj-help-unknown", "chat-help-unknown");
const trigger = makeTriggerHandler({ prisma, settings, logger: silentLogger, runAgent, messageBatcherOptions: { maxMessages: 1 } });
await trigger(makeEvent("chat-help-unknown", "@_user_1 /help unknown"), rt);
expect(rt.sentTexts.at(-1) ?? "").toContain("未知 slash 命令 /unknown");
expect(runAgentCalls).toHaveLength(0);
await trigger(makeEvent("chat-help-unknown", "@_user_1 /unknown help"), rt);
expect(rt.sentTexts.at(-1) ?? "").toContain("未知 slash 命令 /unknown");
expect(runAgentCalls).toHaveLength(0);
expect(await prisma.agentRun.findMany()).toHaveLength(0);
});
it("rejects a sender without edit grant (ADR-0004)", async () => {
await seedProject("proj-2", "chat-2", { role: "READ" });
const trigger = makeTriggerHandler({ prisma, settings, logger: silentLogger, runAgent, messageBatcherOptions: { maxMessages: 1 } });