openclaw/ui/src/ui/chat/slash-command-executor.node...

977 lines
31 KiB
TypeScript

import { describe, expect, it, vi } from "vitest";
import {
createResolvedModelPatch,
createModelCatalog,
DEEPSEEK_CHAT_MODEL,
OPENAI_GPT5_MINI_MODEL,
} from "../chat-model.test-helpers.ts";
import type { GatewayBrowserClient } from "../gateway.ts";
import type { GatewaySessionRow, SessionsListResult } from "../types.ts";
import { executeSlashCommand } from "./slash-command-executor.ts";
function row(key: string, overrides?: Partial<GatewaySessionRow>): GatewaySessionRow {
return {
key,
spawnedBy: overrides?.spawnedBy,
kind: "direct",
updatedAt: null,
...overrides,
};
}
describe("executeSlashCommand /kill", () => {
it("aborts every sub-agent session for /kill all", async () => {
const request = vi.fn(async (method: string, _payload?: unknown) => {
if (method === "sessions.list") {
return {
sessions: [
row("main"),
row("agent:main:subagent:one", { spawnedBy: "main" }),
row("agent:main:subagent:parent", { spawnedBy: "main" }),
row("agent:main:subagent:parent:subagent:child", {
spawnedBy: "agent:main:subagent:parent",
}),
row("agent:other:main"),
],
};
}
if (method === "chat.abort") {
return { ok: true, aborted: true };
}
throw new Error(`unexpected method: ${method}`);
});
const result = await executeSlashCommand(
{ request } as unknown as GatewayBrowserClient,
"agent:main:main",
"kill",
"all",
);
expect(result.content).toBe("Aborted 3 sub-agent sessions.");
expect(request).toHaveBeenNthCalledWith(1, "sessions.list", {});
expect(request).toHaveBeenNthCalledWith(2, "chat.abort", {
sessionKey: "agent:main:subagent:one",
});
expect(request).toHaveBeenNthCalledWith(3, "chat.abort", {
sessionKey: "agent:main:subagent:parent",
});
expect(request).toHaveBeenNthCalledWith(4, "chat.abort", {
sessionKey: "agent:main:subagent:parent:subagent:child",
});
});
it("aborts matching sub-agent sessions for /kill <agentId>", async () => {
const request = vi.fn(async (method: string, _payload?: unknown) => {
if (method === "sessions.list") {
return {
sessions: [
row("agent:main:subagent:one", { spawnedBy: "agent:main:main" }),
row("agent:main:subagent:two", { spawnedBy: "agent:main:main" }),
row("agent:other:subagent:three", { spawnedBy: "agent:other:main" }),
],
};
}
if (method === "chat.abort") {
return { ok: true, aborted: true };
}
throw new Error(`unexpected method: ${method}`);
});
const result = await executeSlashCommand(
{ request } as unknown as GatewayBrowserClient,
"agent:main:main",
"kill",
"main",
);
expect(result.content).toBe("Aborted 2 matching sub-agent sessions for `main`.");
expect(request).toHaveBeenNthCalledWith(1, "sessions.list", {});
expect(request).toHaveBeenNthCalledWith(2, "chat.abort", {
sessionKey: "agent:main:subagent:one",
});
expect(request).toHaveBeenNthCalledWith(3, "chat.abort", {
sessionKey: "agent:main:subagent:two",
});
});
it("does not exact-match a session key outside the current subagent subtree", async () => {
const request = vi.fn(async (method: string, _payload?: unknown) => {
if (method === "sessions.list") {
return {
sessions: [
row("agent:main:subagent:parent", { spawnedBy: "agent:main:main" }),
row("agent:main:subagent:parent:subagent:child", {
spawnedBy: "agent:main:subagent:parent",
}),
row("agent:main:subagent:sibling", { spawnedBy: "agent:main:main" }),
],
};
}
if (method === "chat.abort") {
return { ok: true, aborted: true };
}
throw new Error(`unexpected method: ${method}`);
});
const result = await executeSlashCommand(
{ request } as unknown as GatewayBrowserClient,
"agent:main:subagent:parent",
"kill",
"agent:main:subagent:sibling",
);
expect(result.content).toBe(
"No matching sub-agent sessions found for `agent:main:subagent:sibling`.",
);
expect(request).toHaveBeenCalledTimes(1);
expect(request).toHaveBeenNthCalledWith(1, "sessions.list", {});
});
it("returns a no-op summary when matching sessions have no active runs", async () => {
const request = vi.fn(async (method: string, _payload?: unknown) => {
if (method === "sessions.list") {
return {
sessions: [
row("agent:main:subagent:one", { spawnedBy: "agent:main:main" }),
row("agent:main:subagent:two", { spawnedBy: "agent:main:main" }),
],
};
}
if (method === "chat.abort") {
return { ok: true, aborted: false };
}
throw new Error(`unexpected method: ${method}`);
});
const result = await executeSlashCommand(
{ request } as unknown as GatewayBrowserClient,
"agent:main:main",
"kill",
"all",
);
expect(result.content).toBe("No active sub-agent runs to abort.");
expect(request).toHaveBeenNthCalledWith(1, "sessions.list", {});
expect(request).toHaveBeenNthCalledWith(2, "chat.abort", {
sessionKey: "agent:main:subagent:one",
});
expect(request).toHaveBeenNthCalledWith(3, "chat.abort", {
sessionKey: "agent:main:subagent:two",
});
});
it("treats the legacy main session key as the default agent scope", async () => {
const request = vi.fn(async (method: string, _payload?: unknown) => {
if (method === "sessions.list") {
return {
sessions: [
row("main"),
row("agent:main:subagent:one", { spawnedBy: "agent:main:main" }),
row("agent:main:subagent:two", { spawnedBy: "agent:main:main" }),
row("agent:other:subagent:three", { spawnedBy: "agent:other:main" }),
],
};
}
if (method === "chat.abort") {
return { ok: true, aborted: true };
}
throw new Error(`unexpected method: ${method}`);
});
const result = await executeSlashCommand(
{ request } as unknown as GatewayBrowserClient,
"main",
"kill",
"all",
);
expect(result.content).toBe("Aborted 2 sub-agent sessions.");
expect(request).toHaveBeenNthCalledWith(1, "sessions.list", {});
expect(request).toHaveBeenNthCalledWith(2, "chat.abort", {
sessionKey: "agent:main:subagent:one",
});
expect(request).toHaveBeenNthCalledWith(3, "chat.abort", {
sessionKey: "agent:main:subagent:two",
});
});
it("does not abort unrelated same-agent subagents from another root session", async () => {
const request = vi.fn(async (method: string, _payload?: unknown) => {
if (method === "sessions.list") {
return {
sessions: [
row("agent:main:main"),
row("agent:main:subagent:mine", { spawnedBy: "agent:main:main" }),
row("agent:main:subagent:mine:subagent:child", {
spawnedBy: "agent:main:subagent:mine",
}),
row("agent:main:subagent:other-root", {
spawnedBy: "agent:main:discord:dm:alice",
}),
],
};
}
if (method === "chat.abort") {
return { ok: true, aborted: true };
}
throw new Error(`unexpected method: ${method}`);
});
const result = await executeSlashCommand(
{ request } as unknown as GatewayBrowserClient,
"agent:main:main",
"kill",
"all",
);
expect(result.content).toBe("Aborted 2 sub-agent sessions.");
expect(request).toHaveBeenNthCalledWith(1, "sessions.list", {});
expect(request).toHaveBeenNthCalledWith(2, "chat.abort", {
sessionKey: "agent:main:subagent:mine",
});
expect(request).toHaveBeenNthCalledWith(3, "chat.abort", {
sessionKey: "agent:main:subagent:mine:subagent:child",
});
});
});
describe("executeSlashCommand directives", () => {
it("resolves the legacy main alias for bare /model", async () => {
const request = vi.fn(async (method: string, _payload?: unknown) => {
if (method === "sessions.list") {
return {
defaults: { modelProvider: "openai", model: "default-model" },
sessions: [
row("agent:main:main", {
model: "gpt-4.1-mini",
}),
],
};
}
if (method === "models.list") {
return {
models: [{ id: "gpt-4.1-mini" }, { id: "gpt-4.1" }],
};
}
throw new Error(`unexpected method: ${method}`);
});
const result = await executeSlashCommand(
{ request } as unknown as GatewayBrowserClient,
"main",
"model",
"",
);
expect(result.content).toBe(
"**Current model:** `gpt-4.1-mini`\n**Available:** `gpt-4.1-mini`, `gpt-4.1`",
);
expect(request).toHaveBeenNthCalledWith(1, "sessions.list", {});
expect(request).toHaveBeenNthCalledWith(2, "models.list", {});
});
it("mirrors resolved provider-qualified model refs after /model changes", async () => {
const request = vi.fn(async (method: string, _payload?: unknown) => {
if (method === "sessions.patch") {
return createResolvedModelPatch("gpt-5-mini", "openai");
}
if (method === "models.list") {
return { models: createModelCatalog(OPENAI_GPT5_MINI_MODEL) };
}
if (method === "models.list") {
return { models: [{ id: "gpt-5-mini", name: "gpt-5-mini", provider: "openai" }] };
}
throw new Error(`unexpected method: ${method}`);
});
const result = await executeSlashCommand(
{ request } as unknown as GatewayBrowserClient,
"main",
"model",
"gpt-5-mini",
{
chatModelCatalog: [{ id: "gpt-5-mini", name: "gpt-5-mini", provider: "openai" }],
},
);
expect(request).toHaveBeenCalledWith("sessions.patch", {
key: "main",
model: "gpt-5-mini",
});
expect(result.sessionPatch?.modelOverride).toEqual({
kind: "qualified",
value: "openai/gpt-5-mini",
});
});
it("uses the local model catalog to qualify raw /model overrides when the patch response omits provider", async () => {
const request = vi.fn(async (method: string, _payload?: unknown) => {
if (method === "sessions.patch") {
return {
ok: true,
key: "main",
resolved: {
model: "gpt-5-mini",
},
};
}
throw new Error(`unexpected method: ${method}`);
});
const result = await executeSlashCommand(
{ request } as unknown as GatewayBrowserClient,
"main",
"model",
"gpt-5-mini",
{
chatModelCatalog: [{ id: "gpt-5-mini", name: "GPT-5 Mini", provider: "openai" }],
},
);
expect(result.sessionPatch?.modelOverride).toEqual({
kind: "qualified",
value: "openai/gpt-5-mini",
});
});
it("corrects stale patched providers with the catalog after /model", async () => {
const request = vi.fn(async (method: string, _payload?: unknown) => {
if (method === "sessions.patch") {
return createResolvedModelPatch("deepseek-chat", "zai");
}
if (method === "models.list") {
return { models: createModelCatalog(DEEPSEEK_CHAT_MODEL) };
}
throw new Error(`unexpected method: ${method}`);
});
const result = await executeSlashCommand(
{ request } as unknown as GatewayBrowserClient,
"main",
"model",
"deepseek-chat",
);
expect(result.sessionPatch?.modelOverride).toEqual({
kind: "qualified",
value: "deepseek/deepseek-chat",
});
});
it("falls back to the patched server provider when catalog lookup fails", async () => {
const request = vi.fn(async (method: string, _payload?: unknown) => {
if (method === "sessions.patch") {
return createResolvedModelPatch("gpt-5-mini", "openai");
}
if (method === "models.list") {
throw new Error("models unavailable");
}
throw new Error(`unexpected method: ${method}`);
});
const result = await executeSlashCommand(
{ request } as unknown as GatewayBrowserClient,
"main",
"model",
"gpt-5-mini",
);
expect(result.sessionPatch?.modelOverride).toEqual({
kind: "qualified",
value: "openai/gpt-5-mini",
});
});
it("reuses a provided model catalog for /model updates without refetching", async () => {
const request = vi.fn(async (method: string, _payload?: unknown) => {
if (method === "sessions.patch") {
return createResolvedModelPatch("gpt-5-mini", "openai");
}
throw new Error(`unexpected method: ${method}`);
});
const result = await executeSlashCommand(
{ request } as unknown as GatewayBrowserClient,
"main",
"model",
"gpt-5-mini",
{ modelCatalog: createModelCatalog(OPENAI_GPT5_MINI_MODEL) },
);
expect(result.sessionPatch?.modelOverride).toEqual({
kind: "qualified",
value: "openai/gpt-5-mini",
});
expect(request).toHaveBeenCalledTimes(1);
expect(request).not.toHaveBeenCalledWith("models.list", {});
});
it("resolves the legacy main alias for /usage", async () => {
const request = vi.fn(async (method: string, _payload?: unknown) => {
if (method === "sessions.list") {
return {
sessions: [
row("agent:main:main", {
model: "gpt-4.1-mini",
inputTokens: 1200,
outputTokens: 300,
totalTokens: 1500,
contextTokens: 4000,
}),
],
};
}
throw new Error(`unexpected method: ${method}`);
});
const result = await executeSlashCommand(
{ request } as unknown as GatewayBrowserClient,
"main",
"usage",
"",
);
expect(result.content).toBe(
"**Session Usage**\nInput: **1.2k** tokens\nOutput: **300** tokens\nTotal: **1.5k** tokens\nContext: **30%** of 4k\nModel: `gpt-4.1-mini`",
);
expect(request).toHaveBeenNthCalledWith(1, "sessions.list", {});
});
it("reports the current thinking level for bare /think", async () => {
const request = vi.fn(async (method: string, _payload?: unknown) => {
if (method === "sessions.list") {
return {
sessions: [
row("agent:main:main", {
modelProvider: "openai",
model: "gpt-4.1-mini",
}),
],
};
}
if (method === "models.list") {
return {
models: [{ id: "gpt-4.1-mini", provider: "openai", reasoning: true }],
};
}
throw new Error(`unexpected method: ${method}`);
});
const result = await executeSlashCommand(
{ request } as unknown as GatewayBrowserClient,
"agent:main:main",
"think",
"",
);
expect(result.content).toBe(
"Current thinking level: low.\nOptions: off, minimal, low, medium, high, adaptive.",
);
expect(request).toHaveBeenNthCalledWith(1, "sessions.list", {});
expect(request).toHaveBeenNthCalledWith(2, "models.list", {});
});
it("accepts minimal and xhigh thinking levels", async () => {
const request = vi.fn().mockResolvedValueOnce({ ok: true }).mockResolvedValueOnce({ ok: true });
const minimal = await executeSlashCommand(
{ request } as unknown as GatewayBrowserClient,
"agent:main:main",
"think",
"minimal",
);
const xhigh = await executeSlashCommand(
{ request } as unknown as GatewayBrowserClient,
"agent:main:main",
"think",
"xhigh",
);
expect(minimal.content).toBe("Thinking level set to **minimal**.");
expect(xhigh.content).toBe("Thinking level set to **xhigh**.");
expect(request).toHaveBeenNthCalledWith(1, "sessions.patch", {
key: "agent:main:main",
thinkingLevel: "minimal",
});
expect(request).toHaveBeenNthCalledWith(2, "sessions.patch", {
key: "agent:main:main",
thinkingLevel: "xhigh",
});
});
it("reports the current verbose level for bare /verbose", async () => {
const request = vi.fn(async (method: string, _payload?: unknown) => {
if (method === "sessions.list") {
return {
sessions: [row("agent:main:main", { verboseLevel: "full" })],
};
}
throw new Error(`unexpected method: ${method}`);
});
const result = await executeSlashCommand(
{ request } as unknown as GatewayBrowserClient,
"agent:main:main",
"verbose",
"",
);
expect(result.content).toBe("Current verbose level: full.\nOptions: on, full, off.");
expect(request).toHaveBeenNthCalledWith(1, "sessions.list", {});
});
it("reports the current fast mode for bare /fast", async () => {
const request = vi.fn(async (method: string, _payload?: unknown) => {
if (method === "sessions.list") {
return {
sessions: [row("agent:main:main", { fastMode: true })],
};
}
throw new Error(`unexpected method: ${method}`);
});
const result = await executeSlashCommand(
{ request } as unknown as GatewayBrowserClient,
"agent:main:main",
"fast",
"",
);
expect(result.content).toBe("Current fast mode: on.\nOptions: status, on, off.");
expect(request).toHaveBeenNthCalledWith(1, "sessions.list", {});
});
it("patches fast mode for /fast on", async () => {
const request = vi.fn().mockResolvedValue({ ok: true });
const result = await executeSlashCommand(
{ request } as unknown as GatewayBrowserClient,
"agent:main:main",
"fast",
"on",
);
expect(result.content).toBe("Fast mode enabled.");
expect(request).toHaveBeenCalledWith("sessions.patch", {
key: "agent:main:main",
fastMode: true,
});
});
});
describe("executeSlashCommand /steer (soft inject)", () => {
it("injects into the current session via chat.send with deliver: false", async () => {
const request = vi.fn(async (method: string, _payload?: unknown) => {
if (method === "sessions.list") {
return { sessions: [row("agent:main:main", { status: "running" })] };
}
if (method === "chat.send") {
return { status: "started", runId: "run-1", messageSeq: 2 };
}
throw new Error(`unexpected method: ${method}`);
});
const result = await executeSlashCommand(
{ request } as unknown as GatewayBrowserClient,
"agent:main:main",
"steer",
"try a different approach",
);
expect(result.content).toBe("Steered.");
expect(request).toHaveBeenCalledWith(
"chat.send",
expect.objectContaining({
sessionKey: "agent:main:main",
message: "try a different approach",
deliver: false,
}),
);
});
it("injects into a matching subagent when the first word resolves to one", async () => {
const request = vi.fn(async (method: string, _payload?: unknown) => {
if (method === "sessions.list") {
return {
sessions: [
row("agent:main:main"),
row("agent:main:subagent:researcher", {
spawnedBy: "agent:main:main",
status: "running",
}),
],
};
}
if (method === "chat.send") {
return { status: "started", runId: "run-2", messageSeq: 1 };
}
throw new Error(`unexpected method: ${method}`);
});
const result = await executeSlashCommand(
{ request } as unknown as GatewayBrowserClient,
"agent:main:main",
"steer",
"researcher try a different approach",
);
expect(result.content).toBe("Steered `researcher`.");
expect(request).toHaveBeenCalledWith(
"chat.send",
expect.objectContaining({
sessionKey: "agent:main:subagent:researcher",
message: "try a different approach",
deliver: false,
}),
);
});
it("uses cached sessions to avoid an extra sessions.list round trip", async () => {
const request = vi.fn(async (method: string, _payload?: unknown) => {
if (method === "chat.send") {
return { status: "started", runId: "run-2", messageSeq: 1 };
}
throw new Error(`unexpected method: ${method}`);
});
const result = await executeSlashCommand(
{ request } as unknown as GatewayBrowserClient,
"agent:main:main",
"steer",
"researcher try a different approach",
{
sessionsResult: {
sessions: [
row("agent:main:main"),
row("agent:main:subagent:researcher", {
spawnedBy: "agent:main:main",
status: "running",
}),
],
} as SessionsListResult,
},
);
expect(result.content).toBe("Steered `researcher`.");
expect(request).toHaveBeenCalledTimes(1);
expect(request).toHaveBeenCalledWith(
"chat.send",
expect.objectContaining({
sessionKey: "agent:main:subagent:researcher",
message: "try a different approach",
deliver: false,
}),
);
});
it("matches an explicit full subagent session key", async () => {
const request = vi.fn(async (method: string, _payload?: unknown) => {
if (method === "sessions.list") {
return {
sessions: [
row("agent:main:main"),
row("agent:main:subagent:researcher", {
spawnedBy: "agent:main:main",
status: "running",
}),
],
};
}
if (method === "chat.send") {
return { status: "started", runId: "run-2", messageSeq: 1 };
}
throw new Error(`unexpected method: ${method}`);
});
const result = await executeSlashCommand(
{ request } as unknown as GatewayBrowserClient,
"agent:main:main",
"steer",
"agent:main:subagent:researcher try a different approach",
);
expect(result.content).toBe("Steered `agent:main:subagent:researcher`.");
expect(request).toHaveBeenCalledWith(
"chat.send",
expect.objectContaining({
sessionKey: "agent:main:subagent:researcher",
message: "try a different approach",
deliver: false,
}),
);
});
it("does not treat 'all' as a subagent wildcard", async () => {
const request = vi.fn(async (method: string, _payload?: unknown) => {
if (method === "sessions.list") {
return { sessions: [row("agent:main:main", { status: "running" })] };
}
if (method === "chat.send") {
return { status: "started", runId: "run-3", messageSeq: 1 };
}
throw new Error(`unexpected method: ${method}`);
});
const result = await executeSlashCommand(
{ request } as unknown as GatewayBrowserClient,
"agent:main:main",
"steer",
"all good now",
);
expect(result.content).toBe("Steered.");
expect(request).toHaveBeenCalledWith(
"chat.send",
expect.objectContaining({
sessionKey: "agent:main:main",
message: "all good now",
deliver: false,
}),
);
});
it("does not match agent id as target — treats 'main' as message text", async () => {
const request = vi.fn(async (method: string, _payload?: unknown) => {
if (method === "sessions.list") {
return {
sessions: [
row("agent:main:main", { status: "running" }),
row("agent:main:subagent:researcher", { spawnedBy: "agent:main:main" }),
],
};
}
if (method === "chat.send") {
return { status: "started", runId: "run-4", messageSeq: 1 };
}
throw new Error(`unexpected method: ${method}`);
});
const result = await executeSlashCommand(
{ request } as unknown as GatewayBrowserClient,
"agent:main:main",
"steer",
"main refine the plan",
);
expect(result.content).toBe("Steered.");
expect(request).toHaveBeenCalledWith(
"chat.send",
expect.objectContaining({
sessionKey: "agent:main:main",
message: "main refine the plan",
deliver: false,
}),
);
});
it("keeps ended subagent targets so steer does not fall back to the current session", async () => {
const request = vi.fn(async (method: string, _payload?: unknown) => {
if (method === "sessions.list") {
return {
sessions: [
row("agent:main:main", { status: "running" }),
row("agent:main:subagent:researcher", {
spawnedBy: "agent:main:main",
endedAt: Date.now() - 60_000,
}),
],
};
}
if (method === "chat.send") {
return { status: "started", runId: "run-5", messageSeq: 1 };
}
throw new Error(`unexpected method: ${method}`);
});
const result = await executeSlashCommand(
{ request } as unknown as GatewayBrowserClient,
"agent:main:main",
"steer",
"researcher try again",
);
expect(result.content).toBe("No active run matched `researcher`. Use `/redirect` instead.");
expect(request).toHaveBeenCalledWith("sessions.list", {});
expect(request).not.toHaveBeenCalledWith("chat.send", expect.anything());
});
it("returns a no-op summary when the current session has no active run", async () => {
const request = vi.fn(async (method: string, _payload?: unknown) => {
if (method === "sessions.list") {
return { sessions: [row("agent:main:main", { status: "done", endedAt: Date.now() })] };
}
throw new Error(`unexpected method: ${method}`);
});
const result = await executeSlashCommand(
{ request } as unknown as GatewayBrowserClient,
"agent:main:main",
"steer",
"try again",
);
expect(result.content).toBe("No active run. Use the chat input or `/redirect` instead.");
expect(request).toHaveBeenCalledWith("sessions.list", {});
expect(request).not.toHaveBeenCalledWith("chat.send", expect.anything());
});
it("returns usage when no message is provided", async () => {
const request = vi.fn();
const result = await executeSlashCommand(
{ request } as unknown as GatewayBrowserClient,
"agent:main:main",
"steer",
"",
);
expect(result.content).toBe("Usage: `/steer [id] <message>`");
expect(request).not.toHaveBeenCalled();
});
it("returns error message on RPC failure", async () => {
const request = vi.fn(async (method: string, _payload?: unknown) => {
if (method === "sessions.list") {
return { sessions: [row("agent:main:main", { status: "running" })] };
}
throw new Error("connection lost");
});
const result = await executeSlashCommand(
{ request } as unknown as GatewayBrowserClient,
"agent:main:main",
"steer",
"try again",
);
expect(result.content).toBe("Failed to steer: Error: connection lost");
});
});
describe("executeSlashCommand /redirect (hard kill-and-restart)", () => {
it("calls sessions.steer to abort and restart the current session", async () => {
const request = vi.fn(async (method: string, _payload?: unknown) => {
if (method === "sessions.list") {
return { sessions: [row("agent:main:main")] };
}
if (method === "sessions.steer") {
return { status: "started", runId: "run-1", messageSeq: 2, interruptedActiveRun: true };
}
throw new Error(`unexpected method: ${method}`);
});
const result = await executeSlashCommand(
{ request } as unknown as GatewayBrowserClient,
"agent:main:main",
"redirect",
"start over with a new plan",
);
expect(result.content).toBe("Redirected.");
expect(result.trackRunId).toBe("run-1");
expect(request).toHaveBeenCalledWith("sessions.steer", {
key: "agent:main:main",
message: "start over with a new plan",
});
});
it("redirects a matching subagent when the first word resolves to one", async () => {
const request = vi.fn(async (method: string, _payload?: unknown) => {
if (method === "sessions.list") {
return {
sessions: [
row("agent:main:main"),
row("agent:main:subagent:researcher", { spawnedBy: "agent:main:main" }),
],
};
}
if (method === "sessions.steer") {
return { status: "started", runId: "run-2", messageSeq: 1 };
}
throw new Error(`unexpected method: ${method}`);
});
const result = await executeSlashCommand(
{ request } as unknown as GatewayBrowserClient,
"agent:main:main",
"redirect",
"researcher start over completely",
);
expect(result.content).toBe("Redirected `researcher`.");
// Subagent redirect must NOT set trackRunId — the run belongs to a
// different session so chat events would never clear chatRunId.
expect(result.trackRunId).toBeUndefined();
expect(request).toHaveBeenCalledWith("sessions.steer", {
key: "agent:main:subagent:researcher",
message: "start over completely",
});
});
it("redirects an ended subagent instead of falling back to the current session", async () => {
const request = vi.fn(async (method: string, _payload?: unknown) => {
if (method === "sessions.list") {
return {
sessions: [
row("agent:main:main"),
row("agent:main:subagent:researcher", {
spawnedBy: "agent:main:main",
endedAt: Date.now() - 60_000,
}),
],
};
}
if (method === "sessions.steer") {
return { status: "started", runId: "run-3", messageSeq: 1 };
}
throw new Error(`unexpected method: ${method}`);
});
const result = await executeSlashCommand(
{ request } as unknown as GatewayBrowserClient,
"agent:main:main",
"redirect",
"researcher start over completely",
);
expect(result.content).toBe("Redirected `researcher`.");
expect(result.trackRunId).toBeUndefined();
expect(request).toHaveBeenCalledWith("sessions.steer", {
key: "agent:main:subagent:researcher",
message: "start over completely",
});
});
it("returns usage when no message is provided", async () => {
const request = vi.fn();
const result = await executeSlashCommand(
{ request } as unknown as GatewayBrowserClient,
"agent:main:main",
"redirect",
"",
);
expect(result.content).toBe("Usage: `/redirect [id] <message>`");
expect(request).not.toHaveBeenCalled();
});
it("returns error message on RPC failure", async () => {
const request = vi.fn(async (method: string, _payload?: unknown) => {
if (method === "sessions.list") {
return { sessions: [row("agent:main:main")] };
}
throw new Error("connection lost");
});
const result = await executeSlashCommand(
{ request } as unknown as GatewayBrowserClient,
"agent:main:main",
"redirect",
"try again",
);
expect(result.content).toBe("Failed to redirect: Error: connection lost");
});
});