test(matrix): cover approval metadata delivery

This commit is contained in:
Gustavo Madeira Santana
2026-04-27 22:32:48 -04:00
parent b1a36226b1
commit 795e58acf2

View File

@@ -1,9 +1,25 @@
import { describe, expect, it, vi } from "vitest";
import type {
ExecApprovalRequest,
PluginApprovalRequest,
} from "openclaw/plugin-sdk/approval-runtime";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { matrixApprovalNativeRuntime } from "./approval-handler.runtime.js";
import {
clearMatrixApprovalReactionTargetsForTest,
resolveMatrixApprovalReactionTarget,
} from "./approval-reactions.js";
type MatrixDeliverPendingParams = Parameters<
typeof matrixApprovalNativeRuntime.transport.deliverPending
>[0];
type MatrixPendingApprovalView = MatrixDeliverPendingParams["view"];
type MatrixPendingExecApprovalView = Extract<MatrixPendingApprovalView, { approvalKind: "exec" }>;
type MatrixPendingPluginApprovalView = Extract<
MatrixPendingApprovalView,
{ approvalKind: "plugin" }
>;
const MATRIX_APPROVAL_METADATA_KEY = "com.openclaw.approval";
function buildMatrixApprovalRoomTarget(
roomId: string,
@@ -17,7 +33,111 @@ function buildMatrixApprovalRoomTarget(
};
}
function buildExecApprovalView(
overrides: Partial<MatrixPendingExecApprovalView> = {},
): MatrixPendingExecApprovalView {
return {
approvalKind: "exec",
approvalId: "req-1",
phase: "pending",
title: "Exec Approval Required",
description: "A command needs your approval.",
metadata: [],
ask: "on-request",
agentId: "agent-1",
commandText: "echo hi",
commandPreview: "echo hi",
cwd: "/repo",
host: "gateway",
actions: [
{
decision: "allow-once",
label: "Allow Once",
style: "success",
command: "/approve req-1 allow-once",
},
{
decision: "deny",
label: "Deny",
style: "danger",
command: "/approve req-1 deny",
},
],
expiresAtMs: 1_000,
...overrides,
};
}
function buildPluginApprovalView(
overrides: Partial<MatrixPendingPluginApprovalView> = {},
): MatrixPendingPluginApprovalView {
return {
approvalKind: "plugin",
approvalId: "plugin:req-1",
phase: "pending",
title: "Plugin Approval Required",
description: "Approve the tool call.",
metadata: [],
agentId: "agent-1",
pluginId: "ops",
toolName: "deploy",
severity: "critical",
actions: [
{
decision: "allow-once",
label: "Allow Once",
style: "success",
command: "/approve plugin:req-1 allow-once",
},
],
expiresAtMs: 1_000,
...overrides,
};
}
async function buildPendingPayload(view: MatrixPendingApprovalView) {
const request =
view.approvalKind === "plugin"
? ({
id: view.approvalId,
request: {
title: view.title,
description: view.description ?? "",
severity: view.severity,
toolName: view.toolName ?? undefined,
pluginId: view.pluginId ?? undefined,
agentId: view.agentId ?? undefined,
},
createdAtMs: 0,
expiresAtMs: view.expiresAtMs,
} satisfies PluginApprovalRequest)
: ({
id: view.approvalId,
request: {
command: view.commandText,
cwd: view.cwd ?? undefined,
host: view.host ?? undefined,
agentId: view.agentId ?? undefined,
},
createdAtMs: 0,
expiresAtMs: view.expiresAtMs,
} satisfies ExecApprovalRequest);
return await matrixApprovalNativeRuntime.presentation.buildPendingPayload({
cfg: {} as never,
accountId: "default",
context: { client: {} as never },
request,
approvalKind: view.approvalKind,
nowMs: 100,
view,
});
}
describe("matrixApprovalNativeRuntime", () => {
beforeEach(() => {
clearMatrixApprovalReactionTargetsForTest();
});
it("sends versioned Matrix approval content with pending exec approvals", async () => {
const sendSingleTextMessage = vi.fn().mockResolvedValue({
messageId: "$approval",
@@ -26,54 +146,8 @@ describe("matrixApprovalNativeRuntime", () => {
roomId: "!room:example.org",
});
const reactMessage = vi.fn().mockResolvedValue(undefined);
const view = {
approvalKind: "exec",
approvalId: "req-1",
phase: "pending",
title: "Exec Approval Required",
description: "A command needs your approval.",
metadata: [],
ask: "on-request",
agentId: "agent-1",
commandText: "echo hi",
commandPreview: "echo hi",
cwd: "/repo",
host: "gateway",
actions: [
{
decision: "allow-once",
label: "Allow Once",
style: "success",
command: "/approve req-1 allow-once",
},
{
decision: "deny",
label: "Deny",
style: "danger",
command: "/approve req-1 deny",
},
],
expiresAtMs: 1_000,
} satisfies MatrixDeliverPendingParams["view"];
const pendingPayload = await matrixApprovalNativeRuntime.presentation.buildPendingPayload({
cfg: {} as never,
accountId: "default",
context: { client: {} as never },
request: {
id: "req-1",
request: {
command: "echo hi",
cwd: "/repo",
host: "gateway",
agentId: "agent-1",
},
createdAtMs: 0,
expiresAtMs: 1_000,
},
approvalKind: "exec",
nowMs: 100,
view,
});
const view = buildExecApprovalView();
const pendingPayload = await buildPendingPayload(view);
await matrixApprovalNativeRuntime.transport.deliverPending({
cfg: {} as never,
@@ -101,7 +175,7 @@ describe("matrixApprovalNativeRuntime", () => {
expect.stringContaining("echo hi"),
expect.objectContaining({
extraContent: {
"com.openclaw.approval": expect.objectContaining({
[MATRIX_APPROVAL_METADATA_KEY]: expect.objectContaining({
version: 1,
type: "approval.request",
state: "pending",
@@ -117,64 +191,79 @@ describe("matrixApprovalNativeRuntime", () => {
);
});
it("includes plugin approval fields in Matrix approval content", async () => {
const pendingPayload = await matrixApprovalNativeRuntime.presentation.buildPendingPayload({
it("delivers Matrix approval content with plugin approval fields", async () => {
const sendSingleTextMessage = vi.fn().mockResolvedValue({
messageId: "$plugin-approval",
primaryMessageId: "$plugin-approval",
messageIds: ["$plugin-approval"],
roomId: "!room:example.org",
});
const reactMessage = vi.fn().mockResolvedValue(undefined);
const view = buildPluginApprovalView();
const pendingPayload = await buildPendingPayload(view);
await matrixApprovalNativeRuntime.transport.deliverPending({
cfg: {} as never,
accountId: "default",
context: { client: {} as never },
request: {
id: "plugin:req-1",
request: {
title: "Plugin Approval Required",
description: "Approve the tool call.",
severity: "critical",
toolName: "deploy",
pluginId: "ops",
agentId: "agent-1",
context: {
client: {} as never,
deps: {
sendSingleTextMessage,
reactMessage,
},
createdAtMs: 0,
expiresAtMs: 1_000,
},
request: {} as never,
approvalKind: "plugin",
nowMs: 100,
view: {
approvalKind: "plugin",
approvalId: "plugin:req-1",
phase: "pending",
title: "Plugin Approval Required",
description: "Approve the tool call.",
metadata: [],
agentId: "agent-1",
pluginId: "ops",
toolName: "deploy",
severity: "critical",
actions: [
{
decision: "allow-once",
label: "Allow Once",
style: "success",
command: "/approve plugin:req-1 allow-once",
},
],
expiresAtMs: 1_000,
} as never,
plannedTarget: buildMatrixApprovalRoomTarget("!room:example.org"),
preparedTarget: {
to: "room:!room:example.org",
roomId: "!room:example.org",
},
view,
pendingPayload,
});
expect(pendingPayload).toMatchObject({
extraContent: {
"com.openclaw.approval": {
version: 1,
type: "approval.request",
state: "pending",
id: "plugin:req-1",
kind: "plugin",
pluginId: "ops",
toolName: "deploy",
agentId: "agent-1",
severity: "critical",
expect(sendSingleTextMessage).toHaveBeenCalledWith(
"room:!room:example.org",
expect.stringContaining("deploy"),
expect.objectContaining({
extraContent: {
[MATRIX_APPROVAL_METADATA_KEY]: {
version: 1,
type: "approval.request",
state: "pending",
phase: "pending",
id: "plugin:req-1",
kind: "plugin",
title: "Plugin Approval Required",
description: "Approve the tool call.",
expiresAtMs: 1_000,
metadata: [],
allowedDecisions: ["allow-once"],
actions: [
{
decision: "allow-once",
label: "Allow Once",
style: "success",
command: "/approve plugin:req-1 allow-once",
},
],
pluginId: "ops",
toolName: "deploy",
agentId: "agent-1",
severity: "critical",
},
},
},
});
}),
);
expect(reactMessage).toHaveBeenCalledWith(
"!room:example.org",
"$plugin-approval",
"✅",
expect.objectContaining({
accountId: "default",
}),
);
});
it("falls back to chunked Matrix delivery when approval content exceeds one event", async () => {
@@ -188,14 +277,7 @@ describe("matrixApprovalNativeRuntime", () => {
roomId: "!room:example.org",
});
const reactMessage = vi.fn().mockResolvedValue(undefined);
const view = {
approvalKind: "exec",
approvalId: "req-1",
phase: "pending",
title: "Exec Approval Required",
description: "A command needs your approval.",
metadata: [],
commandText: "echo hi",
const view = buildExecApprovalView({
actions: [
{
decision: "allow-once",
@@ -204,24 +286,8 @@ describe("matrixApprovalNativeRuntime", () => {
command: "/approve req-1 allow-once",
},
],
expiresAtMs: 1_000,
} satisfies MatrixDeliverPendingParams["view"];
const pendingPayload = await matrixApprovalNativeRuntime.presentation.buildPendingPayload({
cfg: {} as never,
accountId: "default",
context: { client: {} as never },
request: {
id: "req-1",
request: {
command: "echo hi",
},
createdAtMs: 0,
expiresAtMs: 1_000,
},
approvalKind: "exec",
nowMs: 100,
view,
});
const pendingPayload = await buildPendingPayload(view);
const entry = await matrixApprovalNativeRuntime.transport.deliverPending({
cfg: {} as never,
@@ -266,6 +332,44 @@ describe("matrixApprovalNativeRuntime", () => {
messageIds: ["$primary", "$last"],
reactionEventId: "$primary",
});
const bindPending = matrixApprovalNativeRuntime.interactions?.bindPending;
if (!bindPending) {
throw new Error("Matrix approval runtime must expose bindPending");
}
const binding = await bindPending({
cfg: {} as never,
accountId: "default",
context: {
client: {} as never,
},
request: {} as never,
approvalKind: "exec",
view,
pendingPayload,
entry: entry!,
});
expect(binding).toEqual({
roomId: "!room:example.org",
eventId: "$primary",
});
expect(
resolveMatrixApprovalReactionTarget({
roomId: "!room:example.org",
eventId: "$primary",
reactionKey: "✅",
}),
).toEqual({
approvalId: "req-1",
decision: "allow-once",
});
expect(
resolveMatrixApprovalReactionTarget({
roomId: "!room:example.org",
eventId: "$last",
reactionKey: "✅",
}),
).toBeNull();
});
it("uses a longer code fence when resolved commands contain triple backticks", async () => {