mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-15 14:00:46 +00:00
308 lines
10 KiB
TypeScript
308 lines
10 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { resolveSessionAgentId } from "../../agents/agent-scope.js";
|
|
import type { OpenClawConfig } from "../../config/config.js";
|
|
import type { MsgContext } from "../templating.js";
|
|
import { handleContextCommand } from "./commands-context-command.js";
|
|
import { handleExportTrajectoryCommand, handleStatusCommand } from "./commands-info.js";
|
|
import { buildStatusReply } from "./commands-status.js";
|
|
import type { HandleCommandsParams } from "./commands-types.js";
|
|
import { handleWhoamiCommand } from "./commands-whoami.js";
|
|
|
|
const buildContextReplyMock = vi.hoisted(() => vi.fn());
|
|
const buildExportTrajectoryReplyMock = vi.hoisted(() => vi.fn(async () => ({ text: "exported" })));
|
|
const listSkillCommandsForAgentsMock = vi.hoisted(() => vi.fn(() => []));
|
|
const buildCommandsMessagePaginatedMock = vi.hoisted(() =>
|
|
vi.fn(() => ({ text: "/commands", currentPage: 1, totalPages: 1 })),
|
|
);
|
|
|
|
vi.mock("./commands-context-report.js", () => ({
|
|
buildContextReply: buildContextReplyMock,
|
|
}));
|
|
|
|
vi.mock("./commands-export-trajectory.js", () => ({
|
|
buildExportTrajectoryReply: buildExportTrajectoryReplyMock,
|
|
}));
|
|
|
|
vi.mock("./commands-status.js", () => ({
|
|
buildStatusReply: vi.fn(async () => ({ text: "status reply" })),
|
|
}));
|
|
|
|
vi.mock("../../agents/agent-scope.js", async () => {
|
|
const actual = await vi.importActual<typeof import("../../agents/agent-scope.js")>(
|
|
"../../agents/agent-scope.js",
|
|
);
|
|
return {
|
|
...actual,
|
|
resolveSessionAgentId: vi.fn(actual.resolveSessionAgentId),
|
|
};
|
|
});
|
|
|
|
vi.mock("../skill-commands.js", () => ({
|
|
listSkillCommandsForAgents: listSkillCommandsForAgentsMock,
|
|
}));
|
|
|
|
vi.mock("../status.js", async () => {
|
|
const actual = await vi.importActual<typeof import("../status.js")>("../status.js");
|
|
return {
|
|
...actual,
|
|
buildCommandsMessagePaginated: buildCommandsMessagePaginatedMock,
|
|
};
|
|
});
|
|
|
|
function buildInfoParams(
|
|
commandBodyNormalized: string,
|
|
cfg: OpenClawConfig,
|
|
ctxOverrides?: Partial<MsgContext>,
|
|
): HandleCommandsParams {
|
|
return {
|
|
cfg,
|
|
ctx: {
|
|
Provider: "whatsapp",
|
|
Surface: "whatsapp",
|
|
CommandSource: "text",
|
|
...ctxOverrides,
|
|
},
|
|
command: {
|
|
commandBodyNormalized,
|
|
isAuthorizedSender: true,
|
|
senderIsOwner: true,
|
|
senderId: "12345",
|
|
channel: "whatsapp",
|
|
channelId: "whatsapp",
|
|
surface: "whatsapp",
|
|
ownerList: [],
|
|
from: "12345",
|
|
to: "bot",
|
|
},
|
|
sessionKey: "agent:main:whatsapp:direct:12345",
|
|
workspaceDir: "/tmp",
|
|
provider: "whatsapp",
|
|
model: "test-model",
|
|
contextTokens: 0,
|
|
defaultGroupActivation: () => "mention",
|
|
resolvedVerboseLevel: "off",
|
|
resolvedReasoningLevel: "off",
|
|
resolveDefaultThinkingLevel: async () => undefined,
|
|
isGroup: false,
|
|
directives: {},
|
|
elevated: { enabled: true, allowed: true, failures: [] },
|
|
} as unknown as HandleCommandsParams;
|
|
}
|
|
|
|
describe("info command handlers", () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
buildExportTrajectoryReplyMock.mockResolvedValue({ text: "exported" });
|
|
buildContextReplyMock.mockImplementation(async (params: HandleCommandsParams) => {
|
|
const normalized = params.command.commandBodyNormalized;
|
|
if (normalized === "/context list") {
|
|
return { text: "Injected workspace files:\n- AGENTS.md" };
|
|
}
|
|
if (normalized === "/context detail") {
|
|
return { text: "Context breakdown (detailed)\nTop tools (schema size):" };
|
|
}
|
|
return { text: "/context\n- /context list\nInline shortcut" };
|
|
});
|
|
buildCommandsMessagePaginatedMock.mockReturnValue({
|
|
text: "/commands",
|
|
currentPage: 1,
|
|
totalPages: 1,
|
|
});
|
|
});
|
|
|
|
it("only lets owners export trajectory bundles", async () => {
|
|
const params = buildInfoParams("/export-trajectory", {
|
|
commands: { text: true },
|
|
} as OpenClawConfig);
|
|
params.command.senderIsOwner = false;
|
|
|
|
const result = await handleExportTrajectoryCommand(params, true);
|
|
|
|
expect(result).toEqual({ shouldContinue: false });
|
|
expect(buildExportTrajectoryReplyMock).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("returns sender details for /whoami", async () => {
|
|
const result = await handleWhoamiCommand(
|
|
buildInfoParams(
|
|
"/whoami",
|
|
{
|
|
commands: { text: true },
|
|
channels: { whatsapp: { allowFrom: ["*"] } },
|
|
} as OpenClawConfig,
|
|
{
|
|
SenderId: "12345",
|
|
SenderUsername: "TestUser",
|
|
ChatType: "direct",
|
|
},
|
|
),
|
|
true,
|
|
);
|
|
expect(result?.shouldContinue).toBe(false);
|
|
expect(result?.reply?.text).toContain("Channel: whatsapp");
|
|
expect(result?.reply?.text).toContain("User id: 12345");
|
|
expect(result?.reply?.text).toContain("Username: @TestUser");
|
|
expect(result?.reply?.text).toContain("AllowFrom: 12345");
|
|
});
|
|
|
|
it("uses the canonical command sender identity for /whoami AllowFrom", async () => {
|
|
const params = buildInfoParams(
|
|
"/whoami",
|
|
{
|
|
commands: { text: true },
|
|
channels: { whatsapp: { allowFrom: ["*"] } },
|
|
} as OpenClawConfig,
|
|
{
|
|
SenderId: "123@lid",
|
|
SenderUsername: "TestUser",
|
|
SenderE164: "+15551234567",
|
|
ChatType: "direct",
|
|
},
|
|
);
|
|
params.command.senderId = "+15551234567";
|
|
|
|
const result = await handleWhoamiCommand(params, true);
|
|
|
|
expect(result?.shouldContinue).toBe(false);
|
|
expect(result?.reply?.text).toContain("User id: 123@lid");
|
|
expect(result?.reply?.text).toContain("AllowFrom: +15551234567");
|
|
});
|
|
|
|
it("returns expected details for /context commands", async () => {
|
|
const cfg = {
|
|
commands: { text: true },
|
|
channels: { whatsapp: { allowFrom: ["*"] } },
|
|
} as OpenClawConfig;
|
|
const cases = [
|
|
{ commandBody: "/context", expectedText: ["/context list", "Inline shortcut"] },
|
|
{ commandBody: "/context list", expectedText: ["Injected workspace files:", "AGENTS.md"] },
|
|
{
|
|
commandBody: "/context detail",
|
|
expectedText: ["Context breakdown (detailed)", "Top tools (schema size):"],
|
|
},
|
|
] as const;
|
|
|
|
for (const testCase of cases) {
|
|
const result = await handleContextCommand(buildInfoParams(testCase.commandBody, cfg), true);
|
|
expect(result?.shouldContinue).toBe(false);
|
|
for (const expectedText of testCase.expectedText) {
|
|
expect(result?.reply?.text).toContain(expectedText);
|
|
}
|
|
}
|
|
});
|
|
|
|
it("prefers the persisted session parent when routing /status context", async () => {
|
|
const params = buildInfoParams(
|
|
"/status",
|
|
{
|
|
commands: { text: true },
|
|
channels: { whatsapp: { allowFrom: ["*"] } },
|
|
} as OpenClawConfig,
|
|
{
|
|
ParentSessionKey: undefined,
|
|
},
|
|
);
|
|
params.sessionEntry = {
|
|
sessionId: "session-1",
|
|
updatedAt: Date.now(),
|
|
parentSessionKey: "discord:group:parent-room",
|
|
} as HandleCommandsParams["sessionEntry"];
|
|
|
|
const statusResult = await handleStatusCommand(params, true);
|
|
|
|
expect(statusResult?.shouldContinue).toBe(false);
|
|
|
|
expect(vi.mocked(buildStatusReply)).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
parentSessionKey: "discord:group:parent-room",
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("preserves the shared session store path when routing /status", async () => {
|
|
const params = buildInfoParams("/status", {
|
|
commands: { text: true },
|
|
channels: { whatsapp: { allowFrom: ["*"] } },
|
|
} as OpenClawConfig);
|
|
params.storePath = "/tmp/target-session-store.json";
|
|
|
|
const statusResult = await handleStatusCommand(params, true);
|
|
|
|
expect(statusResult?.shouldContinue).toBe(false);
|
|
expect(vi.mocked(buildStatusReply)).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
storePath: "/tmp/target-session-store.json",
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("prefers the target session entry when routing /status", async () => {
|
|
const params = buildInfoParams("/status", {
|
|
commands: { text: true },
|
|
channels: { whatsapp: { allowFrom: ["*"] } },
|
|
} as OpenClawConfig);
|
|
params.sessionEntry = {
|
|
sessionId: "wrapper-session",
|
|
updatedAt: Date.now(),
|
|
parentSessionKey: "wrapper-parent",
|
|
} as HandleCommandsParams["sessionEntry"];
|
|
params.sessionStore = {
|
|
"agent:main:whatsapp:direct:12345": {
|
|
sessionId: "target-session",
|
|
updatedAt: Date.now(),
|
|
parentSessionKey: "target-parent",
|
|
},
|
|
};
|
|
|
|
const statusResult = await handleStatusCommand(params, true);
|
|
|
|
expect(statusResult?.shouldContinue).toBe(false);
|
|
expect(vi.mocked(buildStatusReply)).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
sessionEntry: expect.objectContaining({
|
|
sessionId: "target-session",
|
|
parentSessionKey: "target-parent",
|
|
}),
|
|
parentSessionKey: "target-parent",
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("forwards resolved fast mode to /status", async () => {
|
|
const params = buildInfoParams("/status", {
|
|
commands: { text: true },
|
|
channels: { whatsapp: { allowFrom: ["*"] } },
|
|
} as OpenClawConfig);
|
|
params.resolvedFastMode = true;
|
|
|
|
const statusResult = await handleStatusCommand(params, true);
|
|
|
|
expect(statusResult?.shouldContinue).toBe(false);
|
|
expect(vi.mocked(buildStatusReply)).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
resolvedFastMode: true,
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("uses the canonical target session agent when listing /commands", async () => {
|
|
const { handleCommandsListCommand } = await import("./commands-info.js");
|
|
const params = buildInfoParams("/commands", {
|
|
commands: { text: true },
|
|
channels: { whatsapp: { allowFrom: ["*"] } },
|
|
} as OpenClawConfig);
|
|
params.agentId = "main";
|
|
params.sessionKey = "agent:target:whatsapp:direct:12345";
|
|
vi.mocked(resolveSessionAgentId).mockReturnValue("target");
|
|
|
|
const result = await handleCommandsListCommand(params, true);
|
|
|
|
expect(result?.shouldContinue).toBe(false);
|
|
expect(listSkillCommandsForAgentsMock).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
agentIds: ["target"],
|
|
}),
|
|
);
|
|
});
|
|
});
|