mirror of
https://github.com/murashit/codex-panel.git
synced 2026-07-22 06:57:10 +00:00
200 lines
6.9 KiB
TypeScript
200 lines
6.9 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 { createChatState } from "../../../../../src/features/chat/application/state/root-reducer";
|
|
import { createChatStateStore } from "../../../../../src/features/chat/application/state/store";
|
|
import {
|
|
executeSlashCommandWithState,
|
|
type SlashCommandExecutorHost,
|
|
} from "../../../../../src/features/chat/application/turns/slash-command-executor";
|
|
|
|
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,
|
|
provenance: { kind: "interactive" },
|
|
};
|
|
}
|
|
|
|
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 clipUrl = vi.fn().mockResolvedValue(null);
|
|
const host: SlashCommandExecutorHost = {
|
|
stateStore,
|
|
connectionAvailable: () => true,
|
|
referThread,
|
|
clipUrl,
|
|
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(),
|
|
requestPermissionProfile: vi.fn(),
|
|
resetPermissionProfileToConfig: 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(),
|
|
statusDetails: () => [],
|
|
permissionDetails: () => [],
|
|
connectionDiagnosticDetails: () => [],
|
|
toolInventoryDetails: vi.fn(() => []),
|
|
modelStatusDetails: () => [],
|
|
effortStatusDetails: () => [],
|
|
...overrides,
|
|
};
|
|
return { clipUrl, 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",
|
|
approvalPolicyKnown: true,
|
|
sandboxPolicyKnown: true,
|
|
permissionProfileKnown: true,
|
|
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",
|
|
approvalPolicyKnown: true,
|
|
sandboxPolicyKnown: true,
|
|
permissionProfileKnown: true,
|
|
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("does not reference threads without a captured input snapshot", 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).not.toHaveBeenCalled();
|
|
expect(host.addSystemMessage).toHaveBeenCalledWith("Cannot reference a thread without composer input context.");
|
|
expect(result).toBeUndefined();
|
|
});
|
|
|
|
it("forwards readable referenced thread input to turn submission", async () => {
|
|
const { host, referThread, stateStore } = createHost();
|
|
const inputSnapshot = { sourcePath: "snapshot.md" } as never;
|
|
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", inputSnapshot);
|
|
|
|
expect(referThread).toHaveBeenCalledWith(
|
|
expect.objectContaining({ id: "019abcde-0000-7000-8000-000000000001" }),
|
|
"summarize",
|
|
inputSnapshot,
|
|
);
|
|
expect(result?.sendText).toBe("prepared summarize");
|
|
expect(result?.sendInput).toEqual(textInput("referenced summarize"));
|
|
expect(result?.referencedThread?.title).toBe("Other");
|
|
});
|
|
});
|