mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-06 06:41:08 +00:00
Merged via squash.
Prepared head SHA: d9f048e827
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
260 lines
7.2 KiB
TypeScript
260 lines
7.2 KiB
TypeScript
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
|
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import { MatrixExecApprovalHandler } from "./exec-approvals-handler.js";
|
|
|
|
const baseRequest = {
|
|
id: "9f1c7d5d-b1fb-46ef-ac45-662723b65bb7",
|
|
request: {
|
|
command: "npm view diver name version description",
|
|
agentId: "main",
|
|
sessionKey: "agent:main:matrix:channel:!ops:example.org",
|
|
turnSourceChannel: "matrix",
|
|
turnSourceTo: "room:!ops:example.org",
|
|
turnSourceThreadId: "$thread",
|
|
turnSourceAccountId: "default",
|
|
},
|
|
createdAtMs: 1000,
|
|
expiresAtMs: 61_000,
|
|
};
|
|
|
|
function createHandler(cfg: OpenClawConfig, accountId = "default") {
|
|
const client = {} as never;
|
|
const sendMessage = vi
|
|
.fn()
|
|
.mockResolvedValueOnce({ messageId: "$m1", roomId: "!ops:example.org" })
|
|
.mockResolvedValue({ messageId: "$m2", roomId: "!dm-owner:example.org" });
|
|
const editMessage = vi.fn().mockResolvedValue({ eventId: "$edit1" });
|
|
const deleteMessage = vi.fn().mockResolvedValue(undefined);
|
|
const repairDirectRooms = vi.fn().mockResolvedValue({
|
|
activeRoomId: "!dm-owner:example.org",
|
|
});
|
|
const handler = new MatrixExecApprovalHandler(
|
|
{
|
|
client,
|
|
accountId,
|
|
cfg,
|
|
},
|
|
{
|
|
nowMs: () => 1000,
|
|
sendMessage,
|
|
editMessage,
|
|
deleteMessage,
|
|
repairDirectRooms,
|
|
},
|
|
);
|
|
return { client, handler, sendMessage, editMessage, deleteMessage, repairDirectRooms };
|
|
}
|
|
|
|
afterEach(() => {
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
describe("MatrixExecApprovalHandler", () => {
|
|
it("sends approval prompts to the originating matrix room when target=channel", async () => {
|
|
const cfg = {
|
|
channels: {
|
|
matrix: {
|
|
homeserver: "https://matrix.example.org",
|
|
userId: "@bot:example.org",
|
|
accessToken: "tok",
|
|
execApprovals: {
|
|
enabled: true,
|
|
approvers: ["@owner:example.org"],
|
|
target: "channel",
|
|
},
|
|
},
|
|
},
|
|
} as OpenClawConfig;
|
|
const { handler, sendMessage } = createHandler(cfg);
|
|
|
|
await handler.handleRequested(baseRequest);
|
|
|
|
expect(sendMessage).toHaveBeenCalledWith(
|
|
"room:!ops:example.org",
|
|
expect.stringContaining("/approve 9f1c7d5d-b1fb-46ef-ac45-662723b65bb7 allow-once"),
|
|
expect.objectContaining({
|
|
accountId: "default",
|
|
threadId: "$thread",
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("falls back to approver dms when channel routing is unavailable", async () => {
|
|
const cfg = {
|
|
channels: {
|
|
matrix: {
|
|
homeserver: "https://matrix.example.org",
|
|
userId: "@bot:example.org",
|
|
accessToken: "tok",
|
|
execApprovals: {
|
|
enabled: true,
|
|
approvers: ["@owner:example.org"],
|
|
target: "channel",
|
|
},
|
|
},
|
|
},
|
|
} as OpenClawConfig;
|
|
const { client, handler, sendMessage, repairDirectRooms } = createHandler(cfg);
|
|
|
|
await handler.handleRequested({
|
|
...baseRequest,
|
|
request: {
|
|
...baseRequest.request,
|
|
turnSourceChannel: "slack",
|
|
turnSourceTo: "channel:C1",
|
|
turnSourceAccountId: null,
|
|
turnSourceThreadId: null,
|
|
},
|
|
});
|
|
|
|
expect(repairDirectRooms).toHaveBeenCalledWith({
|
|
client,
|
|
remoteUserId: "@owner:example.org",
|
|
encrypted: false,
|
|
});
|
|
expect(sendMessage).toHaveBeenCalledWith(
|
|
"room:!dm-owner:example.org",
|
|
expect.stringContaining("/approve 9f1c7d5d-b1fb-46ef-ac45-662723b65bb7 allow-once"),
|
|
expect.objectContaining({
|
|
accountId: "default",
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("does not double-send when the origin room is the approver dm", async () => {
|
|
const cfg = {
|
|
channels: {
|
|
matrix: {
|
|
homeserver: "https://matrix.example.org",
|
|
userId: "@bot:example.org",
|
|
accessToken: "tok",
|
|
execApprovals: {
|
|
enabled: true,
|
|
approvers: ["@owner:example.org"],
|
|
target: "dm",
|
|
},
|
|
},
|
|
},
|
|
} as OpenClawConfig;
|
|
const { handler, sendMessage } = createHandler(cfg);
|
|
|
|
await handler.handleRequested({
|
|
...baseRequest,
|
|
request: {
|
|
...baseRequest.request,
|
|
sessionKey: "agent:main:matrix:direct:!dm-owner:example.org",
|
|
turnSourceTo: "room:!dm-owner:example.org",
|
|
turnSourceThreadId: undefined,
|
|
},
|
|
});
|
|
|
|
expect(sendMessage).toHaveBeenCalledTimes(1);
|
|
expect(sendMessage).toHaveBeenCalledWith(
|
|
"room:!dm-owner:example.org",
|
|
expect.stringContaining("/approve 9f1c7d5d-b1fb-46ef-ac45-662723b65bb7 allow-once"),
|
|
expect.objectContaining({
|
|
accountId: "default",
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("edits tracked approval messages when resolved", async () => {
|
|
const cfg = {
|
|
channels: {
|
|
matrix: {
|
|
homeserver: "https://matrix.example.org",
|
|
userId: "@bot:example.org",
|
|
accessToken: "tok",
|
|
execApprovals: {
|
|
enabled: true,
|
|
approvers: ["@owner:example.org"],
|
|
target: "both",
|
|
},
|
|
},
|
|
},
|
|
} as OpenClawConfig;
|
|
const { handler, editMessage } = createHandler(cfg);
|
|
|
|
await handler.handleRequested(baseRequest);
|
|
await handler.handleResolved({
|
|
id: baseRequest.id,
|
|
decision: "allow-once",
|
|
resolvedBy: "matrix:@owner:example.org",
|
|
ts: 2000,
|
|
});
|
|
|
|
expect(editMessage).toHaveBeenCalledWith(
|
|
"!ops:example.org",
|
|
"$m1",
|
|
expect.stringContaining("Exec approval: Allowed once"),
|
|
expect.objectContaining({
|
|
accountId: "default",
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("deletes tracked approval messages when they expire", async () => {
|
|
vi.useFakeTimers();
|
|
const cfg = {
|
|
channels: {
|
|
matrix: {
|
|
homeserver: "https://matrix.example.org",
|
|
userId: "@bot:example.org",
|
|
accessToken: "tok",
|
|
execApprovals: {
|
|
enabled: true,
|
|
approvers: ["@owner:example.org"],
|
|
target: "both",
|
|
},
|
|
},
|
|
},
|
|
} as OpenClawConfig;
|
|
const { handler, deleteMessage } = createHandler(cfg);
|
|
|
|
await handler.handleRequested(baseRequest);
|
|
await vi.advanceTimersByTimeAsync(60_000);
|
|
|
|
expect(deleteMessage).toHaveBeenCalledWith(
|
|
"!ops:example.org",
|
|
"$m1",
|
|
expect.objectContaining({
|
|
accountId: "default",
|
|
reason: "approval expired",
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("honors request decision constraints in pending approval text", async () => {
|
|
const cfg = {
|
|
channels: {
|
|
matrix: {
|
|
homeserver: "https://matrix.example.org",
|
|
userId: "@bot:example.org",
|
|
accessToken: "tok",
|
|
execApprovals: {
|
|
enabled: true,
|
|
approvers: ["@owner:example.org"],
|
|
target: "channel",
|
|
},
|
|
},
|
|
},
|
|
} as OpenClawConfig;
|
|
const { handler, sendMessage } = createHandler(cfg);
|
|
|
|
await handler.handleRequested({
|
|
...baseRequest,
|
|
request: {
|
|
...baseRequest.request,
|
|
ask: "always",
|
|
allowedDecisions: ["allow-once", "deny"],
|
|
},
|
|
});
|
|
|
|
expect(sendMessage).toHaveBeenCalledWith(
|
|
"room:!ops:example.org",
|
|
expect.not.stringContaining("allow-always"),
|
|
expect.anything(),
|
|
);
|
|
});
|
|
});
|