mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-13 18:21:27 +00:00
* refactor: consume acpx runtime library * refactor: remove duplicated acpx runtime files * fix: update acpx runtime dependency * fix: preserve acp runtime error codes * fix: migrate legacy acpx session files * fix: update acpx runtime dependency * fix: import Dirent from node fs * ACPX: repin shared runtime engine * ACPX: repin runtime semantics fixes * ACPX: repin runtime contract cleanup * Extensions: repin ACPX after layout refactor * ACPX: drop legacy session migration * ACPX: drop direct ACP SDK dependency * Discord ACP: stop duplicate direct fallback replies * ACP: rename delivered text visibility hook * ACPX: pin extension to 0.5.0 * Deps: drop stale ACPX build-script allowlist * ACPX: add local development guidance * ACPX: document temporary pnpm exception flow * SDK: preserve legacy ACP visibility hook * ACP: keep reset commands on local path * ACP: make in-place reset start fresh session * ACP: recover broken bindings on fresh reset * ACP: defer fresh reset marker until close succeeds * ACP: reset bound sessions fresh again * Discord: ensure ACP bindings before /new * ACP: recover missing persistent sessions
145 lines
3.9 KiB
TypeScript
145 lines
3.9 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { buildTestCtx } from "../auto-reply/reply/test-ctx.js";
|
|
|
|
const { bypassMock, dispatchMock } = vi.hoisted(() => ({
|
|
bypassMock: vi.fn(),
|
|
dispatchMock: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("../auto-reply/reply/dispatch-acp.runtime.js", () => ({
|
|
shouldBypassAcpDispatchForCommand: bypassMock,
|
|
tryDispatchAcpReply: dispatchMock,
|
|
}));
|
|
|
|
import { tryDispatchAcpReplyHook } from "./acp-runtime.js";
|
|
|
|
const event = {
|
|
ctx: buildTestCtx({
|
|
SessionKey: "agent:test:session",
|
|
CommandBody: "/acp cancel",
|
|
BodyForCommands: "/acp cancel",
|
|
BodyForAgent: "/acp cancel",
|
|
}),
|
|
runId: "run-1",
|
|
sessionKey: "agent:test:session",
|
|
inboundAudio: false,
|
|
sessionTtsAuto: "off" as const,
|
|
ttsChannel: undefined,
|
|
suppressUserDelivery: false,
|
|
shouldRouteToOriginating: false,
|
|
originatingChannel: undefined,
|
|
originatingTo: undefined,
|
|
shouldSendToolSummaries: true,
|
|
sendPolicy: "allow" as const,
|
|
};
|
|
|
|
const ctx = {
|
|
cfg: {},
|
|
dispatcher: {
|
|
sendToolResult: () => false,
|
|
sendBlockReply: () => false,
|
|
sendFinalReply: () => false,
|
|
waitForIdle: async () => {},
|
|
getQueuedCounts: () => ({ tool: 0, block: 0, final: 0 }),
|
|
getFailedCounts: () => ({ tool: 0, block: 0, final: 0 }),
|
|
markComplete: () => {},
|
|
},
|
|
abortSignal: undefined,
|
|
onReplyStart: undefined,
|
|
recordProcessed: vi.fn(),
|
|
markIdle: vi.fn(),
|
|
};
|
|
|
|
describe("tryDispatchAcpReplyHook", () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
it("skips ACP runtime lookup for plain-text deny turns", async () => {
|
|
const result = await tryDispatchAcpReplyHook(
|
|
{
|
|
...event,
|
|
sendPolicy: "deny",
|
|
ctx: buildTestCtx({
|
|
SessionKey: "agent:test:session",
|
|
BodyForCommands: "write a test",
|
|
BodyForAgent: "write a test",
|
|
}),
|
|
},
|
|
ctx,
|
|
);
|
|
|
|
expect(result).toBeUndefined();
|
|
expect(bypassMock).not.toHaveBeenCalled();
|
|
expect(dispatchMock).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("skips ACP dispatch when send policy denies delivery and no bypass applies", async () => {
|
|
bypassMock.mockResolvedValue(false);
|
|
|
|
const result = await tryDispatchAcpReplyHook({ ...event, sendPolicy: "deny" }, ctx);
|
|
|
|
expect(result).toBeUndefined();
|
|
expect(dispatchMock).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("dispatches through ACP when command bypass applies", async () => {
|
|
bypassMock.mockResolvedValue(true);
|
|
dispatchMock.mockResolvedValue({
|
|
queuedFinal: true,
|
|
counts: { tool: 1, block: 2, final: 3 },
|
|
});
|
|
|
|
const result = await tryDispatchAcpReplyHook({ ...event, sendPolicy: "deny" }, ctx);
|
|
|
|
expect(result).toEqual({
|
|
handled: true,
|
|
queuedFinal: true,
|
|
counts: { tool: 1, block: 2, final: 3 },
|
|
});
|
|
expect(dispatchMock).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
ctx: event.ctx,
|
|
cfg: ctx.cfg,
|
|
dispatcher: ctx.dispatcher,
|
|
bypassForCommand: true,
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("returns unhandled when ACP dispatcher declines the turn", async () => {
|
|
bypassMock.mockResolvedValue(false);
|
|
dispatchMock.mockResolvedValue(undefined);
|
|
|
|
const result = await tryDispatchAcpReplyHook(event, ctx);
|
|
|
|
expect(result).toBeUndefined();
|
|
expect(dispatchMock).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it("does not let ACP claim reset commands before local command handling", async () => {
|
|
bypassMock.mockResolvedValue(true);
|
|
dispatchMock.mockResolvedValue(undefined);
|
|
|
|
const result = await tryDispatchAcpReplyHook(
|
|
{
|
|
...event,
|
|
ctx: buildTestCtx({
|
|
SessionKey: "agent:test:session",
|
|
CommandBody: "/new",
|
|
BodyForCommands: "/new",
|
|
BodyForAgent: "/new",
|
|
}),
|
|
},
|
|
ctx,
|
|
);
|
|
|
|
expect(result).toBeUndefined();
|
|
expect(dispatchMock).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
bypassForCommand: true,
|
|
}),
|
|
);
|
|
});
|
|
});
|