murashit_codex-panel/tests/features/chat/application/conversation/slash-command-executor.test.ts
2026-07-01 17:19:56 +09:00

182 lines
6.2 KiB
TypeScript

import { describe, expect, it, vi } from "vitest";
import type { CodexInput } from "../../../../../src/domain/chat/input";
import type { Thread } from "../../../../../src/domain/threads/model";
import {
executeSlashCommandWithState,
type SlashCommandExecutorHost,
} from "../../../../../src/features/chat/application/conversation/slash-command-executor";
import { createChatState } from "../../../../../src/features/chat/application/state/root-reducer";
import { createChatStateStore } from "../../../../../src/features/chat/application/state/store";
const textInput = (text: string): CodexInput => [{ type: "text", text }];
function thread(id: string, name: string | null = null): Thread {
return {
id,
preview: "",
createdAt: 0,
updatedAt: 0,
name,
archived: false,
};
}
type SlashCommandExecutorHostOverrides = Partial<SlashCommandExecutorHost>;
function createHost(overrides: SlashCommandExecutorHostOverrides = {}) {
const stateStore = createChatStateStore(createChatState());
const compactThread = vi.fn().mockResolvedValue(undefined);
const referThread = vi.fn().mockResolvedValue(null);
const host: SlashCommandExecutorHost = {
stateStore,
connectionAvailable: () => true,
referThread,
startNewThread: vi.fn().mockResolvedValue(undefined),
startThreadForGoal: vi.fn().mockResolvedValue("thread-new"),
resumeThread: vi.fn().mockResolvedValue(undefined),
threadActions: {
forkThread: vi.fn().mockResolvedValue(undefined),
rollbackThread: vi.fn().mockResolvedValue(undefined),
compactThread,
archiveThread: vi.fn().mockResolvedValue(undefined),
renameThread: vi.fn().mockResolvedValue(true),
},
reconnect: vi.fn().mockResolvedValue(undefined),
runtimeSettings: {
toggleFastMode: vi.fn(),
toggleCollaborationMode: vi.fn(),
toggleAutoReview: vi.fn(),
requestModel: vi.fn(),
resetModelToConfig: vi.fn(),
requestReasoningEffort: vi.fn(),
resetReasoningEffortToConfig: vi.fn(),
},
goals: {
activeGoal: vi.fn(() => stateStore.getState().activeThread.goal),
setObjective: vi.fn().mockResolvedValue(true),
setStatus: vi.fn().mockResolvedValue(true),
clear: vi.fn().mockResolvedValue(true),
},
addSystemMessage: vi.fn(),
addStructuredSystemMessage: vi.fn(),
setStatus: vi.fn(),
statusSummaryLines: () => [],
permissionDetails: () => [],
connectionDiagnosticDetails: () => [],
toolInventoryDetails: vi.fn(() => []),
modelStatusLines: () => [],
effortStatusLines: () => [],
...overrides,
};
return { compactThread, host, referThread, stateStore };
}
describe("executeSlashCommandWithState", () => {
it("executes slash commands against the current chat state", async () => {
const { host } = createHost();
const result = await executeSlashCommandWithState(host, "clear", "");
expect(host.startNewThread).toHaveBeenCalledOnce();
expect(result).toBeUndefined();
});
it("routes compact through the shared thread action port", async () => {
const { compactThread, host, stateStore } = createHost();
stateStore.dispatch({
type: "thread-list/applied",
threads: [thread("thread", "Thread")],
});
stateStore.dispatch({
type: "active-thread/resumed",
approvalPolicy: null,
sandboxPolicy: null,
activePermissionProfile: null,
thread: thread("thread", "Thread"),
cwd: "/vault",
model: null,
reasoningEffort: null,
serviceTier: null,
approvalsReviewer: null,
});
await executeSlashCommandWithState(host, "compact", "");
expect(compactThread).toHaveBeenCalledWith("thread");
});
it("routes compact through the shared thread action port before a client is connected", async () => {
const { compactThread, host, stateStore } = createHost({ connectionAvailable: () => false });
stateStore.dispatch({
type: "active-thread/resumed",
approvalPolicy: null,
sandboxPolicy: null,
activePermissionProfile: null,
thread: thread("thread", "Thread"),
cwd: "/vault",
model: null,
reasoningEffort: null,
serviceTier: null,
approvalsReviewer: null,
});
await executeSlashCommandWithState(host, "compact", "");
expect(compactThread).toHaveBeenCalledWith("thread");
});
it("starts an empty panel before setting a slash command goal", async () => {
const { host } = createHost();
await executeSlashCommandWithState(host, "goal", "set Ship this");
expect(host.startThreadForGoal).toHaveBeenCalledWith("Ship this");
expect(host.goals.setObjective).toHaveBeenCalledWith("thread-new", "Ship this", null);
});
it("runs reconnect even when there is no current app-server client", async () => {
const { host } = createHost({ connectionAvailable: () => false });
await executeSlashCommandWithState(host, "reconnect", "");
expect(host.reconnect).toHaveBeenCalledOnce();
});
it("reports unreadable referenced threads", async () => {
const { host, referThread, stateStore } = createHost();
stateStore.dispatch({
type: "thread-list/applied",
threads: [thread("other", "Other")],
});
const result = await executeSlashCommandWithState(host, "refer", "Other summarize");
expect(referThread).toHaveBeenCalledWith(expect.objectContaining({ id: "other" }), "summarize");
expect(result).toBeUndefined();
});
it("forwards readable referenced thread input to turn submission", async () => {
const { host, referThread, stateStore } = createHost();
stateStore.dispatch({
type: "thread-list/applied",
threads: [thread("019abcde-0000-7000-8000-000000000001", "Other")],
});
referThread.mockResolvedValue({
text: "prepared summarize",
input: textInput("referenced summarize"),
referencedThread: {
threadId: "019abcde-0000-7000-8000-000000000001",
title: "Other",
includedTurns: 1,
turnLimit: 20,
},
});
const result = await executeSlashCommandWithState(host, "refer", "Other summarize");
expect(result?.sendText).toBe("prepared summarize");
expect(result?.sendInput).toEqual(textInput("referenced summarize"));
expect(result?.referencedThread?.title).toBe("Other");
});
});