mirror of
https://github.com/murashit/codex-panel.git
synced 2026-07-22 06:57:10 +00:00
546 lines
20 KiB
TypeScript
546 lines
20 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
|
|
import { slashCommandHelpLines, slashCommandHelpSections } from "../../../../src/features/chat/composer/slash-commands";
|
|
import type { Thread } from "../../../../src/generated/app-server/v2/Thread";
|
|
import { executeSlashCommand, type SlashCommandExecutionContext } from "../../../../src/features/chat/slash-commands";
|
|
|
|
function context(overrides: Partial<SlashCommandExecutionContext> = {}): SlashCommandExecutionContext {
|
|
return {
|
|
activeThreadId: "thread-1",
|
|
busy: false,
|
|
listedThreads: [thread({ id: "thread-1", name: "Current" })],
|
|
startNewThread: vi.fn().mockResolvedValue(undefined),
|
|
resumeThread: vi.fn().mockResolvedValue(undefined),
|
|
referThread: vi.fn().mockResolvedValue({
|
|
input: [{ type: "text", text: "referenced", text_elements: [] }],
|
|
referencedThread: { threadId: "thread-2", title: "Referenced", includedTurns: 1, turnLimit: 20 },
|
|
}),
|
|
forkThread: vi.fn().mockResolvedValue(undefined),
|
|
rollbackThread: vi.fn().mockResolvedValue(undefined),
|
|
compactThread: vi.fn().mockResolvedValue(undefined),
|
|
archiveThread: vi.fn().mockResolvedValue(undefined),
|
|
renameThread: vi.fn().mockResolvedValue(undefined),
|
|
reconnect: vi.fn().mockResolvedValue(undefined),
|
|
toggleFastMode: vi.fn(),
|
|
toggleCollaborationMode: vi.fn(),
|
|
toggleAutoReview: vi.fn(),
|
|
addSystemMessage: vi.fn(),
|
|
addStructuredSystemMessage: vi.fn(),
|
|
setStatus: vi.fn(),
|
|
setRequestedModel: vi.fn(),
|
|
setRequestedReasoningEffort: vi.fn(),
|
|
statusSummaryLines: () => ["status"],
|
|
connectionDiagnosticDetails: () => [{ title: "Process", rows: [{ key: "connection", value: "connected" }] }],
|
|
mcpStatusLines: vi.fn().mockResolvedValue(["mcp"]),
|
|
modelStatusLines: () => ["model"],
|
|
effortStatusLines: () => ["effort"],
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function thread(overrides: Partial<Thread> = {}): Thread {
|
|
return {
|
|
id: "thread-1",
|
|
sessionId: "session-1",
|
|
forkedFromId: null,
|
|
parentThreadId: null,
|
|
preview: "Preview",
|
|
ephemeral: false,
|
|
modelProvider: "openai",
|
|
createdAt: 1,
|
|
updatedAt: 1,
|
|
status: "idle",
|
|
path: null,
|
|
cwd: "/vault",
|
|
cliVersion: "0.130.0",
|
|
source: "appServer",
|
|
threadSource: null,
|
|
agentNickname: null,
|
|
agentRole: null,
|
|
gitInfo: null,
|
|
name: null,
|
|
turns: [],
|
|
...overrides,
|
|
} as Thread;
|
|
}
|
|
|
|
describe("slash commands", () => {
|
|
it("clears the current panel for /clear", async () => {
|
|
const ctx = context();
|
|
|
|
await executeSlashCommand("clear", "", ctx);
|
|
|
|
expect(ctx.startNewThread).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it("rejects /clear arguments", async () => {
|
|
const ctx = context();
|
|
|
|
await executeSlashCommand("clear", "最初の依頼です", ctx);
|
|
|
|
expect(ctx.startNewThread).not.toHaveBeenCalled();
|
|
expect(ctx.addSystemMessage).toHaveBeenCalledWith("/clear does not take arguments. Usage: /clear");
|
|
});
|
|
|
|
it("resumes the latest listed thread for bare /resume", async () => {
|
|
const ctx = context({
|
|
listedThreads: [thread({ id: "latest", name: "Latest" }), thread({ id: "older", name: "Older" })],
|
|
});
|
|
|
|
await executeSlashCommand("resume", "", ctx);
|
|
|
|
expect(ctx.resumeThread).toHaveBeenCalledWith("latest");
|
|
});
|
|
|
|
it("reconnects the panel for /reconnect", async () => {
|
|
const ctx = context();
|
|
|
|
await executeSlashCommand("reconnect", "", ctx);
|
|
|
|
expect(ctx.reconnect).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it("resumes a thread by id argument", async () => {
|
|
const ctx = context({
|
|
listedThreads: [thread({ id: "thread-alpha", name: "Alpha" }), thread({ id: "thread-beta", name: "Beta" })],
|
|
});
|
|
|
|
await executeSlashCommand("resume", "thread-beta", ctx);
|
|
|
|
expect(ctx.resumeThread).toHaveBeenCalledWith("thread-beta");
|
|
});
|
|
|
|
it("reports ambiguous resume matches", async () => {
|
|
const ctx = context({
|
|
listedThreads: [thread({ id: "thread-alpha", name: "Draft" }), thread({ id: "thread-beta", name: "Draft notes" })],
|
|
});
|
|
|
|
await executeSlashCommand("resume", "Draft", ctx);
|
|
|
|
expect(ctx.resumeThread).not.toHaveBeenCalled();
|
|
expect(ctx.addSystemMessage).toHaveBeenCalledWith("Multiple matching threads: Draft, Draft notes");
|
|
});
|
|
|
|
it("returns referenced input for /refer", async () => {
|
|
const target = thread({ id: "thread-alpha", name: "Alpha" });
|
|
const input = [{ type: "text" as const, text: "context\n質問です", text_elements: [] }];
|
|
const referencedThread = { threadId: "thread-alpha", title: "Alpha", includedTurns: 2, turnLimit: 20 };
|
|
const ctx = context({
|
|
listedThreads: [thread({ id: "thread-current", name: "Current" }), target],
|
|
referThread: vi.fn().mockResolvedValue({ input, referencedThread }),
|
|
});
|
|
|
|
const result = await executeSlashCommand("refer", "thread-alpha 質問です", ctx);
|
|
|
|
expect(ctx.referThread).toHaveBeenCalledWith(target, "質問です");
|
|
expect(result).toEqual({ sendText: "質問です", sendInput: input, referencedThread });
|
|
});
|
|
|
|
it("rejects /refer without both thread and message", async () => {
|
|
const ctx = context();
|
|
|
|
await executeSlashCommand("refer", "thread-2", ctx);
|
|
|
|
expect(ctx.referThread).not.toHaveBeenCalled();
|
|
expect(ctx.addSystemMessage).toHaveBeenCalledWith("/refer requires a thread and a message. Usage: /refer <thread> <message>");
|
|
});
|
|
|
|
it("rejects /refer for the active thread", async () => {
|
|
const ctx = context({
|
|
activeThreadId: "thread-1",
|
|
listedThreads: [thread({ id: "thread-1", name: "Current" })],
|
|
});
|
|
|
|
await executeSlashCommand("refer", "thread-1 続きです", ctx);
|
|
|
|
expect(ctx.referThread).not.toHaveBeenCalled();
|
|
expect(ctx.addSystemMessage).toHaveBeenCalledWith("Use the current thread directly instead of referencing it.");
|
|
});
|
|
|
|
it("forks the active thread for /fork", async () => {
|
|
const ctx = context({ activeThreadId: "active-thread" });
|
|
|
|
await executeSlashCommand("fork", "", ctx);
|
|
|
|
expect(ctx.forkThread).toHaveBeenCalledWith("active-thread");
|
|
});
|
|
|
|
it("rejects /fork arguments", async () => {
|
|
const ctx = context();
|
|
|
|
await executeSlashCommand("fork", "anything", ctx);
|
|
|
|
expect(ctx.forkThread).not.toHaveBeenCalled();
|
|
expect(ctx.addSystemMessage).toHaveBeenCalledWith("/fork does not take arguments. Usage: /fork");
|
|
});
|
|
|
|
it("rolls back the active thread for /rollback", async () => {
|
|
const ctx = context({ activeThreadId: "active-thread" });
|
|
|
|
await executeSlashCommand("rollback", "", ctx);
|
|
|
|
expect(ctx.rollbackThread).toHaveBeenCalledWith("active-thread");
|
|
});
|
|
|
|
it("rejects /rollback without an active thread", async () => {
|
|
const ctx = context({ activeThreadId: null });
|
|
|
|
await executeSlashCommand("rollback", "", ctx);
|
|
|
|
expect(ctx.rollbackThread).not.toHaveBeenCalled();
|
|
expect(ctx.addSystemMessage).toHaveBeenCalledWith("No active thread to roll back.");
|
|
});
|
|
|
|
it("rejects /rollback while a turn is running", async () => {
|
|
const ctx = context({ busy: true });
|
|
|
|
await executeSlashCommand("rollback", "", ctx);
|
|
|
|
expect(ctx.rollbackThread).not.toHaveBeenCalled();
|
|
expect(ctx.addSystemMessage).toHaveBeenCalledWith("Interrupt the current turn before rolling back.");
|
|
});
|
|
|
|
it("rejects /rollback arguments", async () => {
|
|
const ctx = context();
|
|
|
|
await executeSlashCommand("rollback", "2", ctx);
|
|
|
|
expect(ctx.rollbackThread).not.toHaveBeenCalled();
|
|
expect(ctx.addSystemMessage).toHaveBeenCalledWith("/rollback does not take arguments. Usage: /rollback");
|
|
});
|
|
|
|
it("toggles Plan mode without sending text for bare /plan", async () => {
|
|
const ctx = context();
|
|
|
|
const result = await executeSlashCommand("plan", "", ctx);
|
|
|
|
expect(ctx.toggleCollaborationMode).toHaveBeenCalledOnce();
|
|
expect(result).toBeUndefined();
|
|
});
|
|
|
|
it("returns message text after toggling Plan mode for /plan arguments", async () => {
|
|
const ctx = context();
|
|
|
|
const result = await executeSlashCommand("plan", "OK、実装してください", ctx);
|
|
|
|
expect(ctx.toggleCollaborationMode).toHaveBeenCalledOnce();
|
|
expect(result).toEqual({ sendText: "OK、実装してください" });
|
|
});
|
|
|
|
it("toggles auto-review without sending text for bare /auto-review", async () => {
|
|
const ctx = context();
|
|
|
|
const result = await executeSlashCommand("auto-review", "", ctx);
|
|
|
|
expect(ctx.toggleAutoReview).toHaveBeenCalledOnce();
|
|
expect(result).toBeUndefined();
|
|
});
|
|
|
|
it("rejects /auto-review arguments", async () => {
|
|
const ctx = context();
|
|
|
|
await executeSlashCommand("auto-review", "この依頼からお願いします", ctx);
|
|
|
|
expect(ctx.toggleAutoReview).not.toHaveBeenCalled();
|
|
expect(ctx.addSystemMessage).toHaveBeenCalledWith("/auto-review does not take arguments. Usage: /auto-review");
|
|
});
|
|
|
|
it("rejects /compact arguments", async () => {
|
|
const ctx = context();
|
|
|
|
await executeSlashCommand("compact", "ignored for now", ctx);
|
|
|
|
expect(ctx.compactThread).not.toHaveBeenCalled();
|
|
expect(ctx.addSystemMessage).toHaveBeenCalledWith("/compact does not take arguments. Usage: /compact");
|
|
});
|
|
|
|
it("rejects bare /archive", async () => {
|
|
const ctx = context({ activeThreadId: "active-thread" });
|
|
|
|
await executeSlashCommand("archive", "", ctx);
|
|
|
|
expect(ctx.archiveThread).not.toHaveBeenCalled();
|
|
expect(ctx.addSystemMessage).toHaveBeenCalledWith("/archive requires a thread. Usage: /archive <thread>");
|
|
});
|
|
|
|
it("archives a selected thread by id argument", async () => {
|
|
const ctx = context({
|
|
listedThreads: [thread({ id: "thread-alpha", name: "Alpha" }), thread({ id: "thread-beta", name: "Beta" })],
|
|
});
|
|
|
|
await executeSlashCommand("archive", "thread-beta", ctx);
|
|
|
|
expect(ctx.archiveThread).toHaveBeenCalledWith("thread-beta");
|
|
});
|
|
|
|
it("rejects /archive without a thread before active-thread checks", async () => {
|
|
const ctx = context({ activeThreadId: null });
|
|
|
|
await executeSlashCommand("archive", "", ctx);
|
|
|
|
expect(ctx.archiveThread).not.toHaveBeenCalled();
|
|
expect(ctx.addSystemMessage).toHaveBeenCalledWith("/archive requires a thread. Usage: /archive <thread>");
|
|
});
|
|
|
|
it("rejects /archive while a turn is running", async () => {
|
|
const ctx = context({ busy: true });
|
|
|
|
await executeSlashCommand("archive", "thread-1", ctx);
|
|
|
|
expect(ctx.archiveThread).not.toHaveBeenCalled();
|
|
expect(ctx.addSystemMessage).toHaveBeenCalledWith("Finish or interrupt the current turn before archiving threads.");
|
|
});
|
|
|
|
it("reports ambiguous archive matches", async () => {
|
|
const ctx = context({
|
|
listedThreads: [thread({ id: "thread-alpha", name: "Draft" }), thread({ id: "thread-beta", name: "Draft notes" })],
|
|
});
|
|
|
|
await executeSlashCommand("archive", "Draft", ctx);
|
|
|
|
expect(ctx.archiveThread).not.toHaveBeenCalled();
|
|
expect(ctx.addSystemMessage).toHaveBeenCalledWith("Multiple matching threads: Draft, Draft notes");
|
|
});
|
|
|
|
it("renames a selected thread by id argument", async () => {
|
|
const ctx = context({
|
|
listedThreads: [thread({ id: "thread-alpha", name: "Alpha" }), thread({ id: "thread-beta", name: "Beta" })],
|
|
});
|
|
|
|
await executeSlashCommand("rename", "thread-beta New Beta Name", ctx);
|
|
|
|
expect(ctx.renameThread).toHaveBeenCalledWith("thread-beta", "New Beta Name");
|
|
});
|
|
|
|
it("trims /rename names before saving", async () => {
|
|
const ctx = context({
|
|
listedThreads: [thread({ id: "thread-beta", name: "Beta" })],
|
|
});
|
|
|
|
await executeSlashCommand("rename", "thread-beta New Beta Name ", ctx);
|
|
|
|
expect(ctx.renameThread).toHaveBeenCalledWith("thread-beta", "New Beta Name");
|
|
});
|
|
|
|
it("rejects /rename without a thread and name", async () => {
|
|
const ctx = context();
|
|
|
|
await executeSlashCommand("rename", "thread-1", ctx);
|
|
|
|
expect(ctx.renameThread).not.toHaveBeenCalled();
|
|
expect(ctx.addSystemMessage).toHaveBeenCalledWith("/rename requires a thread and a name. Usage: /rename <thread> <name>");
|
|
});
|
|
|
|
it("rejects blank /rename names after trimming", async () => {
|
|
const ctx = context();
|
|
|
|
await executeSlashCommand("rename", "thread-1 ", ctx);
|
|
|
|
expect(ctx.renameThread).not.toHaveBeenCalled();
|
|
expect(ctx.addSystemMessage).toHaveBeenCalledWith("/rename requires a thread and a name. Usage: /rename <thread> <name>");
|
|
});
|
|
|
|
it("reports ambiguous rename matches", async () => {
|
|
const ctx = context({
|
|
listedThreads: [thread({ id: "thread-alpha", name: "Draft" }), thread({ id: "thread-beta", name: "Draft notes" })],
|
|
});
|
|
|
|
await executeSlashCommand("rename", "Draft New name", ctx);
|
|
|
|
expect(ctx.renameThread).not.toHaveBeenCalled();
|
|
expect(ctx.addSystemMessage).toHaveBeenCalledWith("Multiple matching threads: Draft, Draft notes");
|
|
});
|
|
|
|
it("documents archive", () => {
|
|
expect(slashCommandHelpLines().find((line) => line.startsWith("/archive"))).toBe(
|
|
"/archive <thread> - Archive the selected Codex thread.",
|
|
);
|
|
});
|
|
|
|
it("shows slash command help as a structured system result", async () => {
|
|
const ctx = context();
|
|
|
|
await executeSlashCommand("help", "", ctx);
|
|
|
|
expect(ctx.addSystemMessage).not.toHaveBeenCalled();
|
|
expect(ctx.addStructuredSystemMessage).toHaveBeenCalledWith("Available slash commands", slashCommandHelpSections());
|
|
});
|
|
|
|
it("groups slash command help by command surface", () => {
|
|
expect(slashCommandHelpSections()).toEqual([
|
|
{
|
|
title: "Panel actions",
|
|
rows: expect.arrayContaining([
|
|
{ key: "/clear", value: "Clear the current panel and start a fresh Codex thread." },
|
|
{ key: "/reconnect", value: "Reconnect to Codex app-server and resume the active thread." },
|
|
{ key: "/archive <thread>", value: "Archive the selected Codex thread." },
|
|
{ key: "/rename <thread> <name>", value: "Rename the selected Codex thread." },
|
|
]),
|
|
},
|
|
{
|
|
title: "Thread settings",
|
|
rows: expect.arrayContaining([
|
|
{ key: "/plan [message]", value: "Toggle Plan mode, optionally with a message." },
|
|
{ key: "/model [model|default]", value: "Show or set the model for subsequent turns." },
|
|
]),
|
|
},
|
|
{
|
|
title: "Diagnostics",
|
|
rows: expect.arrayContaining([
|
|
{ key: "/status", value: "Show current thread, context, and usage limits." },
|
|
{ key: "/help", value: "Show available Codex slash commands." },
|
|
]),
|
|
},
|
|
{
|
|
title: "Composition",
|
|
rows: [{ key: "/refer <thread> <message>", value: "Send a message with recent turns from another non-archived thread." }],
|
|
},
|
|
]);
|
|
});
|
|
|
|
it("rejects /help arguments", async () => {
|
|
const ctx = context();
|
|
|
|
await executeSlashCommand("help", "anything", ctx);
|
|
|
|
expect(ctx.addStructuredSystemMessage).not.toHaveBeenCalled();
|
|
expect(ctx.addSystemMessage).toHaveBeenCalledWith("/help does not take arguments. Usage: /help");
|
|
});
|
|
|
|
it("shows status as a structured system result", async () => {
|
|
const ctx = context({ statusSummaryLines: () => ["Thread status", "Thread: thread-1", "Usage limits", "5h: 42%"] });
|
|
|
|
await executeSlashCommand("status", "", ctx);
|
|
|
|
expect(ctx.addSystemMessage).not.toHaveBeenCalled();
|
|
expect(ctx.addStructuredSystemMessage).toHaveBeenCalledWith("Thread status", [
|
|
{
|
|
rows: [
|
|
{ key: "Thread", value: "thread-1" },
|
|
{ key: "message", value: "Usage limits" },
|
|
{ key: "5h", value: "42%" },
|
|
],
|
|
},
|
|
]);
|
|
});
|
|
|
|
it("rejects /status arguments", async () => {
|
|
const ctx = context();
|
|
|
|
await executeSlashCommand("status", "anything", ctx);
|
|
|
|
expect(ctx.addStructuredSystemMessage).not.toHaveBeenCalled();
|
|
expect(ctx.addSystemMessage).toHaveBeenCalledWith("/status does not take arguments. Usage: /status");
|
|
});
|
|
|
|
it("shows doctor diagnostics as shared structured sections", async () => {
|
|
const details = [{ title: "Process", rows: [{ key: "connection", value: "connected" }] }];
|
|
const ctx = context({ connectionDiagnosticDetails: () => details });
|
|
|
|
await executeSlashCommand("doctor", "", ctx);
|
|
|
|
expect(ctx.addSystemMessage).not.toHaveBeenCalled();
|
|
expect(ctx.addStructuredSystemMessage).toHaveBeenCalledWith("Connection diagnostics", details);
|
|
});
|
|
|
|
it("rejects /doctor arguments", async () => {
|
|
const details = [{ title: "Process", rows: [{ key: "connection", value: "connected" }] }];
|
|
const ctx = context({ connectionDiagnosticDetails: () => details });
|
|
|
|
await executeSlashCommand("doctor", "anything", ctx);
|
|
|
|
expect(ctx.addStructuredSystemMessage).not.toHaveBeenCalled();
|
|
expect(ctx.addSystemMessage).toHaveBeenCalledWith("/doctor does not take arguments. Usage: /doctor");
|
|
});
|
|
|
|
it("documents that /plan can take a message", () => {
|
|
expect(slashCommandHelpLines().find((line) => line.startsWith("/plan"))).toBe(
|
|
"/plan [message] - Toggle Plan mode, optionally with a message.",
|
|
);
|
|
});
|
|
|
|
it("documents auto-review", () => {
|
|
expect(slashCommandHelpLines().find((line) => line.startsWith("/auto-review"))).toBe("/auto-review - Toggle approval auto-review.");
|
|
});
|
|
|
|
it("documents rollback", () => {
|
|
expect(slashCommandHelpLines().find((line) => line.startsWith("/rollback"))).toBe(
|
|
"/rollback - Roll back the latest turn and restore its prompt.",
|
|
);
|
|
});
|
|
|
|
it("documents rename", () => {
|
|
expect(slashCommandHelpLines().find((line) => line.startsWith("/rename"))).toBe(
|
|
"/rename <thread> <name> - Rename the selected Codex thread.",
|
|
);
|
|
});
|
|
|
|
it("documents refer history size", () => {
|
|
expect(slashCommandHelpLines().find((line) => line.startsWith("/refer"))).toBe(
|
|
"/refer <thread> <message> - Send a message with recent turns from another non-archived thread.",
|
|
);
|
|
});
|
|
|
|
it("documents status and doctor as separate commands", () => {
|
|
expect(slashCommandHelpLines().find((line) => line.startsWith("/status"))).toBe(
|
|
"/status - Show current thread, context, and usage limits.",
|
|
);
|
|
expect(slashCommandHelpLines().find((line) => line.startsWith("/doctor"))).toBe(
|
|
"/doctor - Show Codex CLI and Codex App Server diagnostics.",
|
|
);
|
|
});
|
|
|
|
it("rejects unsupported reasoning effort with usage", async () => {
|
|
const ctx = context();
|
|
|
|
await executeSlashCommand("reasoning", "extreme", ctx);
|
|
|
|
expect(ctx.setRequestedReasoningEffort).not.toHaveBeenCalled();
|
|
expect(ctx.addSystemMessage).toHaveBeenCalledWith("Unsupported reasoning level: extreme. Usage: /reasoning [level|default]");
|
|
});
|
|
|
|
it("does not announce model or effort changes when applying them fails", async () => {
|
|
const ctx = context({
|
|
setRequestedModel: vi.fn().mockResolvedValue(false),
|
|
setRequestedReasoningEffort: vi.fn().mockResolvedValue(false),
|
|
});
|
|
|
|
await executeSlashCommand("model", "gpt-5.5", ctx);
|
|
await executeSlashCommand("reasoning", "high", ctx);
|
|
|
|
expect(ctx.setRequestedModel).toHaveBeenCalledWith("gpt-5.5");
|
|
expect(ctx.setRequestedReasoningEffort).toHaveBeenCalledWith("high");
|
|
expect(ctx.addSystemMessage).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("shows MCP server status", async () => {
|
|
const ctx = context();
|
|
|
|
await executeSlashCommand("mcp", "", ctx);
|
|
|
|
expect(ctx.mcpStatusLines).toHaveBeenCalledOnce();
|
|
expect(ctx.addStructuredSystemMessage).toHaveBeenCalledWith("MCP servers", [{ rows: [] }]);
|
|
});
|
|
|
|
it("rejects /mcp arguments", async () => {
|
|
const ctx = context();
|
|
|
|
await executeSlashCommand("mcp", "enable github", ctx);
|
|
|
|
expect(ctx.mcpStatusLines).not.toHaveBeenCalled();
|
|
expect(ctx.addSystemMessage).toHaveBeenCalledWith("/mcp does not take arguments. Usage: /mcp");
|
|
});
|
|
|
|
it("rejects /fast arguments before toggling", async () => {
|
|
const ctx = context();
|
|
|
|
await executeSlashCommand("fast", "now", ctx);
|
|
|
|
expect(ctx.toggleFastMode).not.toHaveBeenCalled();
|
|
expect(ctx.addSystemMessage).toHaveBeenCalledWith("/fast does not take arguments. Usage: /fast");
|
|
});
|
|
|
|
it("documents MCP status", () => {
|
|
expect(slashCommandHelpLines().find((line) => line.startsWith("/mcp"))).toBe("/mcp - Show MCP servers reported by Codex App Server.");
|
|
});
|
|
});
|