mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-15 03:01:02 +00:00
259 lines
8.0 KiB
TypeScript
259 lines
8.0 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
import type { OpenClawConfig } from "../../config/config.js";
|
|
import type { MsgContext } from "../templating.js";
|
|
import { maybeHandleResetCommand } from "./commands-reset.js";
|
|
import type { HandleCommandsParams } from "./commands-types.js";
|
|
import { parseInlineDirectives } from "./directive-handling.parse.js";
|
|
|
|
const triggerInternalHookMock = vi.hoisted(() => vi.fn().mockResolvedValue(undefined));
|
|
const routeReplyMock = vi.hoisted(() =>
|
|
vi.fn<(params: unknown) => Promise<{ ok: boolean }>>(async () => ({ ok: true })),
|
|
);
|
|
const resetMocks = vi.hoisted(() => ({
|
|
resetConfiguredBindingTargetInPlace: vi.fn().mockResolvedValue({ ok: true as const }),
|
|
resolveBoundAcpThreadSessionKey: vi.fn(() => undefined as string | undefined),
|
|
}));
|
|
|
|
vi.mock("../../hooks/internal-hooks.js", () => ({
|
|
createInternalHookEvent: (
|
|
type: string,
|
|
action: string,
|
|
sessionKey: string,
|
|
context: Record<string, unknown>,
|
|
) => ({
|
|
type,
|
|
action,
|
|
sessionKey,
|
|
context,
|
|
timestamp: new Date(0),
|
|
messages: [],
|
|
}),
|
|
triggerInternalHook: triggerInternalHookMock,
|
|
}));
|
|
|
|
vi.mock("../../plugins/hook-runner-global.js", () => ({
|
|
getGlobalHookRunner: vi.fn(() => null),
|
|
}));
|
|
|
|
vi.mock("../commands-registry.js", () => ({
|
|
normalizeCommandBody: (raw: string) => raw.trim(),
|
|
shouldHandleTextCommands: () => true,
|
|
}));
|
|
|
|
vi.mock("../../channels/plugins/binding-targets.js", () => ({
|
|
resetConfiguredBindingTargetInPlace: resetMocks.resetConfiguredBindingTargetInPlace,
|
|
}));
|
|
|
|
vi.mock("./commands-acp/targets.js", () => ({
|
|
resolveBoundAcpThreadSessionKey: resetMocks.resolveBoundAcpThreadSessionKey,
|
|
}));
|
|
|
|
vi.mock("./commands-handlers.runtime.js", () => ({
|
|
loadCommandHandlers: () => [],
|
|
}));
|
|
|
|
vi.mock("./route-reply.runtime.js", () => ({
|
|
routeReply: (params: unknown) => routeReplyMock(params),
|
|
}));
|
|
|
|
function buildResetParams(
|
|
commandBody: string,
|
|
cfg: OpenClawConfig,
|
|
ctxOverrides?: Partial<MsgContext>,
|
|
): HandleCommandsParams {
|
|
const ctx = {
|
|
Body: commandBody,
|
|
CommandBody: commandBody,
|
|
CommandSource: "text",
|
|
CommandAuthorized: true,
|
|
Provider: "whatsapp",
|
|
Surface: "whatsapp",
|
|
SessionKey: "agent:main:main",
|
|
...ctxOverrides,
|
|
} as MsgContext;
|
|
|
|
return {
|
|
ctx,
|
|
cfg,
|
|
command: {
|
|
rawBodyNormalized: commandBody.trim(),
|
|
commandBodyNormalized: commandBody.trim(),
|
|
isAuthorizedSender: true,
|
|
senderIsOwner: true,
|
|
senderId: ctx.SenderId ?? "123",
|
|
channel: ctx.Surface ?? "whatsapp",
|
|
channelId: ctx.Surface ?? "whatsapp",
|
|
surface: ctx.Surface ?? "whatsapp",
|
|
ownerList: [],
|
|
from: ctx.From ?? "sender",
|
|
to: ctx.To ?? "bot",
|
|
resetHookTriggered: false,
|
|
},
|
|
directives: parseInlineDirectives(""),
|
|
elevated: { enabled: true, allowed: true, failures: [] },
|
|
sessionKey: "agent:main:main",
|
|
workspaceDir: "/tmp/openclaw-commands",
|
|
defaultGroupActivation: () => "mention",
|
|
resolvedVerboseLevel: "off",
|
|
resolvedReasoningLevel: "off",
|
|
resolveDefaultThinkingLevel: async () => undefined,
|
|
provider: "whatsapp",
|
|
model: "test-model",
|
|
contextTokens: 0,
|
|
isGroup: false,
|
|
};
|
|
}
|
|
|
|
describe("handleCommands reset hooks", () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
resetMocks.resetConfiguredBindingTargetInPlace.mockResolvedValue({ ok: true });
|
|
resetMocks.resolveBoundAcpThreadSessionKey.mockReturnValue(undefined);
|
|
triggerInternalHookMock.mockResolvedValue(undefined);
|
|
});
|
|
|
|
it("triggers hooks for /new commands", async () => {
|
|
const cases = [
|
|
{
|
|
name: "text command with arguments",
|
|
params: buildResetParams("/new take notes", {
|
|
commands: { text: true },
|
|
channels: { whatsapp: { allowFrom: ["*"] } },
|
|
} as OpenClawConfig),
|
|
expectedCall: expect.objectContaining({ type: "command", action: "new" }),
|
|
},
|
|
{
|
|
name: "native command routed to target session",
|
|
params: (() => {
|
|
const params = buildResetParams(
|
|
"/new",
|
|
{
|
|
commands: { text: true },
|
|
channels: { telegram: { allowFrom: ["*"] } },
|
|
} as OpenClawConfig,
|
|
{
|
|
Provider: "telegram",
|
|
Surface: "telegram",
|
|
CommandSource: "native",
|
|
CommandTargetSessionKey: "agent:main:telegram:direct:123",
|
|
SessionKey: "telegram:slash:123",
|
|
SenderId: "123",
|
|
From: "telegram:123",
|
|
To: "slash:123",
|
|
CommandAuthorized: true,
|
|
},
|
|
);
|
|
params.sessionKey = "agent:main:telegram:direct:123";
|
|
return params;
|
|
})(),
|
|
expectedCall: expect.objectContaining({
|
|
type: "command",
|
|
action: "new",
|
|
sessionKey: "agent:main:telegram:direct:123",
|
|
context: expect.objectContaining({
|
|
workspaceDir: "/tmp/openclaw-commands",
|
|
}),
|
|
}),
|
|
},
|
|
] as const;
|
|
|
|
for (const testCase of cases) {
|
|
await maybeHandleResetCommand(testCase.params);
|
|
expect(triggerInternalHookMock, testCase.name).toHaveBeenCalledWith(testCase.expectedCall);
|
|
triggerInternalHookMock.mockClear();
|
|
}
|
|
});
|
|
|
|
it("uses gateway session reset for bound ACP sessions", async () => {
|
|
resetMocks.resolveBoundAcpThreadSessionKey.mockReturnValue(
|
|
"agent:claude:acp:binding:discord:default:9373ab192b2317f4",
|
|
);
|
|
const params = buildResetParams(
|
|
"/reset",
|
|
{
|
|
commands: { text: true },
|
|
channels: { discord: { allowFrom: ["*"] } },
|
|
} as OpenClawConfig,
|
|
{
|
|
Provider: "discord",
|
|
Surface: "discord",
|
|
CommandSource: "native",
|
|
},
|
|
);
|
|
|
|
const result = await maybeHandleResetCommand(params);
|
|
|
|
expect(resetMocks.resetConfiguredBindingTargetInPlace).toHaveBeenCalledWith({
|
|
cfg: expect.any(Object),
|
|
sessionKey: "agent:claude:acp:binding:discord:default:9373ab192b2317f4",
|
|
reason: "reset",
|
|
commandSource: "discord:native",
|
|
});
|
|
expect(result).toEqual({
|
|
shouldContinue: false,
|
|
reply: { text: "✅ ACP session reset in place." },
|
|
});
|
|
expect(triggerInternalHookMock).not.toHaveBeenCalled();
|
|
expect(params.command.resetHookTriggered).toBe(true);
|
|
});
|
|
|
|
it("keeps tail dispatch after a bound ACP reset", async () => {
|
|
resetMocks.resolveBoundAcpThreadSessionKey.mockReturnValue(
|
|
"agent:claude:acp:binding:discord:default:9373ab192b2317f4",
|
|
);
|
|
const params = buildResetParams(
|
|
"/new who are you",
|
|
{
|
|
commands: { text: true },
|
|
channels: { discord: { allowFrom: ["*"] } },
|
|
} as OpenClawConfig,
|
|
{
|
|
Provider: "discord",
|
|
Surface: "discord",
|
|
CommandSource: "native",
|
|
},
|
|
);
|
|
|
|
const result = await maybeHandleResetCommand(params);
|
|
|
|
expect(result).toEqual({ shouldContinue: false });
|
|
expect(params.ctx.Body).toBe("who are you");
|
|
expect(params.ctx.CommandBody).toBe("who are you");
|
|
expect(params.ctx.AcpDispatchTailAfterReset).toBe(true);
|
|
});
|
|
|
|
it("forwards non-id sender fields when reset hooks emit routed replies", async () => {
|
|
triggerInternalHookMock.mockImplementationOnce(async (event: { messages: string[] }) => {
|
|
event.messages.push("Reset hook says hi");
|
|
});
|
|
const params = buildResetParams(
|
|
"/new",
|
|
{
|
|
commands: { text: true },
|
|
channels: { whatsapp: { allowFrom: ["*"] } },
|
|
} as OpenClawConfig,
|
|
{
|
|
SenderId: "id:whatsapp:123",
|
|
SenderName: "Alice",
|
|
SenderUsername: "alice_u",
|
|
SenderE164: "+15551234567",
|
|
OriginatingChannel: "whatsapp",
|
|
OriginatingTo: "group:ops",
|
|
MessageThreadId: "thread-1",
|
|
},
|
|
);
|
|
|
|
await maybeHandleResetCommand(params);
|
|
|
|
expect(routeReplyMock).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
requesterSenderId: "id:whatsapp:123",
|
|
requesterSenderName: "Alice",
|
|
requesterSenderUsername: "alice_u",
|
|
requesterSenderE164: "+15551234567",
|
|
threadId: "thread-1",
|
|
}),
|
|
);
|
|
});
|
|
});
|