forked from EduCraft/curriculum-project-hub
feat: add slash command help
This commit is contained in:
@@ -0,0 +1,274 @@
|
||||
import type { PrismaClient } from "@prisma/client";
|
||||
import type { FastifyBaseLogger } from "fastify";
|
||||
import type { ModelRegistry, RoleEntry } from "../agent/models.js";
|
||||
import type { RuntimeSettings } from "../settings/runtime.js";
|
||||
import { sendText, type FeishuRuntime } from "./client.js";
|
||||
import type { TriggerQueue } from "./triggerQueue.js";
|
||||
|
||||
export interface SlashInvocation {
|
||||
readonly name: string;
|
||||
readonly args: readonly string[];
|
||||
}
|
||||
|
||||
export interface SlashCommandRunContext {
|
||||
readonly invocation: SlashInvocation;
|
||||
readonly projectId: string;
|
||||
readonly chatId: string;
|
||||
readonly rt: FeishuRuntime;
|
||||
}
|
||||
|
||||
export interface SlashCommandDefinition {
|
||||
readonly name: string;
|
||||
readonly usage: string;
|
||||
readonly summary: string;
|
||||
readonly details: readonly string[];
|
||||
run(context: SlashCommandRunContext): Promise<void>;
|
||||
}
|
||||
|
||||
export interface SlashCommandRegistryDeps {
|
||||
readonly prisma: PrismaClient;
|
||||
readonly settings: RuntimeSettings;
|
||||
readonly logger: FastifyBaseLogger;
|
||||
readonly triggerQueue: TriggerQueue;
|
||||
}
|
||||
|
||||
export function parseSlashInvocation(prompt: string): SlashInvocation | null {
|
||||
const trimmed = prompt.trim();
|
||||
if (!trimmed.startsWith("/")) return null;
|
||||
const tokens = trimmed.split(/\s+/);
|
||||
const rawCommand = tokens[0];
|
||||
if (rawCommand === undefined || rawCommand.length <= 1) return null;
|
||||
return {
|
||||
name: rawCommand.slice(1),
|
||||
args: tokens.slice(1),
|
||||
};
|
||||
}
|
||||
|
||||
export function parseSlashHelpSubcommand(invocation: SlashInvocation): string | null {
|
||||
if (invocation.name === "help") return null;
|
||||
return invocation.args.length === 1 && invocation.args[0] === "help"
|
||||
? invocation.name
|
||||
: null;
|
||||
}
|
||||
|
||||
export function createSlashCommandRegistry(deps: SlashCommandRegistryDeps): ReadonlyMap<string, SlashCommandDefinition> {
|
||||
const commands = new Map<string, SlashCommandDefinition>();
|
||||
const add = (command: SlashCommandDefinition): void => {
|
||||
if (commands.has(command.name)) {
|
||||
throw new Error(`duplicate slash command definition: /${command.name}`);
|
||||
}
|
||||
commands.set(command.name, command);
|
||||
};
|
||||
|
||||
add({
|
||||
name: "help",
|
||||
usage: "/help [command]",
|
||||
summary: "查看可用 slash 命令或单个命令说明。",
|
||||
details: [
|
||||
"不创建 agent run,也不改变当前会话。",
|
||||
"支持 /help new 和 /new help 两种写法。",
|
||||
],
|
||||
run: async ({ invocation, projectId, chatId, rt }) => {
|
||||
const registry = await deps.settings.modelRegistry({ projectId });
|
||||
await sendText(rt, chatId, formatHelpCommandInvocation(invocation, registry, commands));
|
||||
},
|
||||
});
|
||||
|
||||
add({
|
||||
name: "new",
|
||||
usage: "/new",
|
||||
summary: "开新会话,下次 @bot 将从头开始。",
|
||||
details: [
|
||||
"归档当前未归档的 agent session。",
|
||||
"不会清空当前项目的等待队列。",
|
||||
],
|
||||
run: async ({ projectId, chatId, rt }) => {
|
||||
await deps.prisma.agentSession.updateMany({
|
||||
where: { projectId, archivedAt: null },
|
||||
data: { archivedAt: new Date() },
|
||||
});
|
||||
await sendText(rt, chatId, "已开新会话,下次 @bot 将从头开始。");
|
||||
},
|
||||
});
|
||||
|
||||
add({
|
||||
name: "resume",
|
||||
usage: "/resume",
|
||||
summary: "恢复最近一次已归档的会话。",
|
||||
details: [
|
||||
"只恢复当前项目最近归档的 agent session。",
|
||||
"没有可恢复会话时只回复提示,不会创建 agent run。",
|
||||
],
|
||||
run: async ({ projectId, chatId, rt }) => {
|
||||
const latest = await deps.prisma.agentSession.findFirst({
|
||||
where: { projectId, archivedAt: { not: null } },
|
||||
orderBy: { archivedAt: "desc" },
|
||||
select: { id: true },
|
||||
});
|
||||
if (latest === null) {
|
||||
await sendText(rt, chatId, "没有可恢复的会话。");
|
||||
return;
|
||||
}
|
||||
await deps.prisma.agentSession.update({
|
||||
where: { id: latest.id },
|
||||
data: { archivedAt: null },
|
||||
});
|
||||
await sendText(rt, chatId, "已恢复上一个会话。");
|
||||
},
|
||||
});
|
||||
|
||||
add({
|
||||
name: "reset",
|
||||
usage: "/reset",
|
||||
summary: "重置当前会话并清空等待队列。",
|
||||
details: [
|
||||
"归档当前未归档的 agent session。",
|
||||
"清空当前项目已经排队、尚未开始的触发请求。",
|
||||
],
|
||||
run: async ({ projectId, chatId, rt }) => {
|
||||
await deps.prisma.agentSession.updateMany({
|
||||
where: { projectId, archivedAt: null },
|
||||
data: { archivedAt: new Date() },
|
||||
});
|
||||
const cleared = deps.triggerQueue.clear(projectId);
|
||||
if (cleared > 0) {
|
||||
deps.logger.info({ projectId, cleared }, "feishu trigger: cleared queued triggers on reset");
|
||||
}
|
||||
await sendText(rt, chatId, "已重置,下次 @bot 将从头开始。");
|
||||
},
|
||||
});
|
||||
|
||||
return commands;
|
||||
}
|
||||
|
||||
function formatHelpCommandInvocation(
|
||||
invocation: SlashInvocation,
|
||||
registry: ModelRegistry,
|
||||
slashCommands: ReadonlyMap<string, SlashCommandDefinition>,
|
||||
): string {
|
||||
if (invocation.args.length > 1) {
|
||||
const helpCommand = slashCommands.get("help");
|
||||
if (helpCommand === undefined) {
|
||||
throw new Error("slash command registry is missing /help");
|
||||
}
|
||||
return [
|
||||
"用法错误: /help 只接受一个命令名。",
|
||||
"",
|
||||
formatBuiltinSlashCommandHelp(helpCommand),
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
const target = invocation.args[0];
|
||||
if (target === undefined) return formatSlashOverview(registry, slashCommands);
|
||||
const normalizedTarget = normalizeHelpTarget(target);
|
||||
if (normalizedTarget === "") return formatSlashOverview(registry, slashCommands);
|
||||
return formatSlashHelpTarget(normalizedTarget, registry, slashCommands);
|
||||
}
|
||||
|
||||
export function formatSlashHelpTarget(
|
||||
target: string,
|
||||
registry: ModelRegistry,
|
||||
slashCommands: ReadonlyMap<string, SlashCommandDefinition>,
|
||||
): string {
|
||||
const normalizedTarget = normalizeHelpTarget(target);
|
||||
if (normalizedTarget === "") return formatSlashOverview(registry, slashCommands);
|
||||
return formatSlashCommandHelp(normalizedTarget, registry, slashCommands) ?? formatUnknownSlashHelp(normalizedTarget, registry, slashCommands);
|
||||
}
|
||||
|
||||
function normalizeHelpTarget(target: string): string {
|
||||
return target.trim().replace(/^\/+/, "");
|
||||
}
|
||||
|
||||
function formatSlashOverview(
|
||||
registry: ModelRegistry,
|
||||
slashCommands: ReadonlyMap<string, SlashCommandDefinition>,
|
||||
): string {
|
||||
const lines = [
|
||||
"可用 slash 命令:",
|
||||
...[...slashCommands.values()].map((command) => `/${command.name} - ${command.summary}`),
|
||||
];
|
||||
const roles = visibleRoleCommands(registry, slashCommands);
|
||||
if (roles.length > 0) {
|
||||
lines.push("", "角色命令:");
|
||||
for (const role of roles) {
|
||||
lines.push(`/${role.id} <需求> - 使用“${role.label}”角色发起请求。`);
|
||||
}
|
||||
} else {
|
||||
lines.push("", "当前没有配置角色命令。");
|
||||
}
|
||||
lines.push("", "查看单个命令: /help new 或 /new help。");
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
function formatSlashCommandHelp(
|
||||
commandName: string,
|
||||
registry: ModelRegistry,
|
||||
slashCommands: ReadonlyMap<string, SlashCommandDefinition>,
|
||||
): string | null {
|
||||
const normalizedName = normalizeHelpTarget(commandName);
|
||||
const slashCommand = slashCommands.get(normalizedName);
|
||||
if (slashCommand !== undefined) return formatBuiltinSlashCommandHelp(slashCommand);
|
||||
|
||||
const role = registry.role(normalizedName);
|
||||
if (role === undefined) return null;
|
||||
if (slashCommands.has(role.id)) return null;
|
||||
return formatRoleSlashCommandHelp(role);
|
||||
}
|
||||
|
||||
function formatBuiltinSlashCommandHelp(command: SlashCommandDefinition): string {
|
||||
const helpUsage = command.name === "help"
|
||||
? "帮助: /help help"
|
||||
: `帮助: /help ${command.name} 或 /${command.name} help`;
|
||||
return [
|
||||
`/${command.name}`,
|
||||
command.summary,
|
||||
"",
|
||||
`用法: ${command.usage}`,
|
||||
...command.details.map((detail) => `- ${detail}`),
|
||||
"",
|
||||
helpUsage,
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function formatRoleSlashCommandHelp(role: RoleEntry): string {
|
||||
const lines = [
|
||||
`/${role.id}`,
|
||||
`使用“${role.label}”角色发起一次 agent run。`,
|
||||
"",
|
||||
`用法: /${role.id} <需求>`,
|
||||
"- 真正运行时仍会按当前项目的 role.trigger 授权检查。",
|
||||
];
|
||||
if (role.defaultModel !== undefined) {
|
||||
lines.push(`- 默认模型: ${role.defaultModel}`);
|
||||
}
|
||||
lines.push(`- 工具范围: ${roleToolsDescription(role)}`, "", `帮助: /help ${role.id} 或 /${role.id} help`);
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
function roleToolsDescription(role: RoleEntry): string {
|
||||
if (role.tools === undefined) return "全部已注册工具";
|
||||
if (role.tools.length === 0) return "无";
|
||||
return role.tools.join(", ");
|
||||
}
|
||||
|
||||
function formatUnknownSlashHelp(
|
||||
commandName: string,
|
||||
registry: ModelRegistry,
|
||||
slashCommands: ReadonlyMap<string, SlashCommandDefinition>,
|
||||
): string {
|
||||
const normalizedName = normalizeHelpTarget(commandName);
|
||||
return [
|
||||
`未知 slash 命令 /${normalizedName}。`,
|
||||
"",
|
||||
formatSlashOverview(registry, slashCommands),
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function visibleRoleCommands(
|
||||
registry: ModelRegistry,
|
||||
slashCommands: ReadonlyMap<string, SlashCommandDefinition>,
|
||||
): readonly RoleEntry[] {
|
||||
return registry
|
||||
.listRoles()
|
||||
.filter((role) => !slashCommands.has(role.id));
|
||||
}
|
||||
+26
-43
@@ -38,6 +38,7 @@ import { MessageBatcher, messageBatchKey, type MessageBatcherOptions } from "./m
|
||||
import { ApprovalManager } from "./approval.js";
|
||||
import { SenderNameCache } from "./senderCache.js";
|
||||
import { TriggerQueue, triggerQueue as defaultTriggerQueue, type QueuedTrigger } from "./triggerQueue.js";
|
||||
import { createSlashCommandRegistry, formatSlashHelpTarget, parseSlashHelpSubcommand, parseSlashInvocation } from "./slashCommands.js";
|
||||
|
||||
export { ApprovalManager } from "./approval.js";
|
||||
export type { ApprovalResult, PendingApproval } from "./approval.js";
|
||||
@@ -87,6 +88,12 @@ export function makeTriggerHandler(deps: TriggerDeps): TriggerHandler {
|
||||
const runAgent = deps.runAgent ?? defaultRunAgent;
|
||||
const approvalManager = new ApprovalManager();
|
||||
const triggerQueue = deps.triggerQueue ?? defaultTriggerQueue;
|
||||
const slashCommands = createSlashCommandRegistry({
|
||||
prisma: deps.prisma,
|
||||
settings: deps.settings,
|
||||
logger: deps.logger,
|
||||
triggerQueue,
|
||||
});
|
||||
const batchContexts = new Map<string, TriggerRunContext>();
|
||||
// runId → AbortController for live runs. Registered when a run starts,
|
||||
// used by the interrupt card action to abort the SDK query, cleared in the
|
||||
@@ -676,48 +683,23 @@ export function makeTriggerHandler(deps: TriggerDeps): TriggerHandler {
|
||||
// batcher. Session commands bypass the lock because they don't create runs;
|
||||
// role/unknown slash prompts still use the normal run path and queue if locked.
|
||||
if (cleanPrompt.startsWith("/")) {
|
||||
const cmd = cleanPrompt.split(/\s+/)[0];
|
||||
switch (cmd) {
|
||||
case "/new": {
|
||||
await deps.prisma.agentSession.updateMany({
|
||||
where: { projectId, archivedAt: null },
|
||||
data: { archivedAt: new Date() },
|
||||
});
|
||||
await sendText(rt, chatId, "已开新会话,下次 @bot 将从头开始。");
|
||||
return;
|
||||
}
|
||||
case "/resume": {
|
||||
const latest = await deps.prisma.agentSession.findFirst({
|
||||
where: { projectId, archivedAt: { not: null } },
|
||||
orderBy: { archivedAt: "desc" },
|
||||
select: { id: true },
|
||||
});
|
||||
if (latest === null) {
|
||||
await sendText(rt, chatId, "没有可恢复的会话。");
|
||||
return;
|
||||
}
|
||||
await deps.prisma.agentSession.update({
|
||||
where: { id: latest.id },
|
||||
data: { archivedAt: null },
|
||||
});
|
||||
await sendText(rt, chatId, "已恢复上一个会话。");
|
||||
return;
|
||||
}
|
||||
case "/reset": {
|
||||
await deps.prisma.agentSession.updateMany({
|
||||
where: { projectId, archivedAt: null },
|
||||
data: { archivedAt: new Date() },
|
||||
});
|
||||
const cleared = triggerQueue.clear(projectId);
|
||||
if (cleared > 0) {
|
||||
deps.logger.info({ projectId, cleared }, "feishu trigger: cleared queued triggers on reset");
|
||||
}
|
||||
await sendText(rt, chatId, "已重置,下次 @bot 将从头开始。");
|
||||
return;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
const invocation = parseSlashInvocation(cleanPrompt);
|
||||
const helpSubcommandTarget = invocation === null ? null : parseSlashHelpSubcommand(invocation);
|
||||
if (helpSubcommandTarget !== null) {
|
||||
// Help is generated on demand because role/tool configuration is runtime data.
|
||||
const models = await deps.settings.modelRegistry({ projectId });
|
||||
await sendText(rt, chatId, formatSlashHelpTarget(helpSubcommandTarget, models, slashCommands));
|
||||
return;
|
||||
}
|
||||
|
||||
if (invocation !== null) {
|
||||
const slashCommand = slashCommands.get(invocation.name);
|
||||
if (slashCommand !== undefined) {
|
||||
await slashCommand.run({ invocation, projectId, chatId, rt });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await startAgentRun(runContext, cleanPrompt);
|
||||
return;
|
||||
}
|
||||
@@ -1037,11 +1019,12 @@ async function downloadPostMessageFiles(args: {
|
||||
await downloadMessageFile(args.rt, args.msg.message_id, fileKey, savePath);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a leading `/<role>` command from a prompt (e.g. `/draft 帮我写教案`).
|
||||
* Returns the role id and the remaining prompt with the command stripped.
|
||||
* Control commands already handled upstream (`/new`, `/resume`, `/reset`) are
|
||||
* ignored here — they never reach this function.
|
||||
* Control/help commands already handled upstream are ignored here — they never
|
||||
* reach this function.
|
||||
*
|
||||
* Unknown `/foo` that isn't a registered role: returns `null` role and the
|
||||
* original prompt unchanged (the slash is treated as literal text). Role
|
||||
|
||||
@@ -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 } });
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { parseSlashHelpSubcommand, parseSlashInvocation } from "../../src/feishu/slashCommands.js";
|
||||
|
||||
describe("slash command parser", () => {
|
||||
it("parses a slash invocation without resolving it", () => {
|
||||
expect(parseSlashInvocation("/new")).toEqual({ name: "new", args: [] });
|
||||
expect(parseSlashInvocation("/review 看看这节")).toEqual({ name: "review", args: ["看看这节"] });
|
||||
expect(parseSlashInvocation("写教案")).toBeNull();
|
||||
});
|
||||
|
||||
it("parses help subcommands only in the exact /<command> help form", () => {
|
||||
expect(parseSlashHelpSubcommand({ name: "new", args: ["help"] })).toBe("new");
|
||||
expect(parseSlashHelpSubcommand({ name: "unknown", args: ["help"] })).toBe("unknown");
|
||||
expect(parseSlashHelpSubcommand({ name: "new", args: ["help", "please"] })).toBeNull();
|
||||
expect(parseSlashHelpSubcommand({ name: "help", args: ["new"] })).toBeNull();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user