mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-11 17:21:13 +00:00
fix: make same-chat approvals work across channels
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { hasApprovalTurnSourceRoute } from "../../infra/approval-turn-source.js";
|
||||
import { sanitizeExecApprovalDisplayText } from "../../infra/exec-approval-command-display.js";
|
||||
import type { ExecApprovalForwarder } from "../../infra/exec-approval-forwarder.js";
|
||||
import {
|
||||
@@ -188,6 +189,9 @@ export function createExecApprovalHandlers(
|
||||
{ dropIfSlow: true },
|
||||
);
|
||||
const hasExecApprovalClients = context.hasExecApprovalClients?.(client?.connId) ?? false;
|
||||
const hasTurnSourceRoute = hasApprovalTurnSourceRoute({
|
||||
turnSourceChannel: record.request.turnSourceChannel,
|
||||
});
|
||||
let forwarded = false;
|
||||
if (opts?.forwarder) {
|
||||
try {
|
||||
@@ -202,7 +206,7 @@ export function createExecApprovalHandlers(
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasExecApprovalClients && !forwarded) {
|
||||
if (!hasExecApprovalClients && !forwarded && !hasTurnSourceRoute) {
|
||||
manager.expire(record.id, "no-approval-route");
|
||||
respond(
|
||||
true,
|
||||
|
||||
@@ -163,6 +163,52 @@ describe("createPluginApprovalHandlers", () => {
|
||||
expect(hasExecApprovalClients).toHaveBeenCalledWith("backend-conn-42");
|
||||
});
|
||||
|
||||
it("keeps plugin approvals pending when the originating chat can handle /approve directly", async () => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
const handlers = createPluginApprovalHandlers(manager);
|
||||
const respond = vi.fn();
|
||||
const opts = createMockOptions(
|
||||
"plugin.approval.request",
|
||||
{
|
||||
title: "Sensitive action",
|
||||
description: "Desc",
|
||||
twoPhase: true,
|
||||
turnSourceChannel: "slack",
|
||||
turnSourceTo: "C123",
|
||||
},
|
||||
{
|
||||
respond,
|
||||
context: {
|
||||
broadcast: vi.fn(),
|
||||
logGateway: { error: vi.fn(), warn: vi.fn(), info: vi.fn(), debug: vi.fn() },
|
||||
hasExecApprovalClients: () => false,
|
||||
} as unknown as GatewayRequestHandlerOptions["context"],
|
||||
},
|
||||
);
|
||||
|
||||
const requestPromise = handlers["plugin.approval.request"](opts);
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(respond).toHaveBeenCalledWith(
|
||||
true,
|
||||
expect.objectContaining({ status: "accepted", id: expect.any(String) }),
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
const acceptedCall = respond.mock.calls.find(
|
||||
(call) => (call[1] as Record<string, unknown>)?.status === "accepted",
|
||||
);
|
||||
const approvalId = (acceptedCall?.[1] as Record<string, unknown>)?.id as string;
|
||||
manager.resolve(approvalId, "allow-once");
|
||||
|
||||
await requestPromise;
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects invalid severity value", async () => {
|
||||
const handlers = createPluginApprovalHandlers(manager);
|
||||
const opts = createMockOptions("plugin.approval.request", {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { hasApprovalTurnSourceRoute } from "../../infra/approval-turn-source.js";
|
||||
import type { ExecApprovalForwarder } from "../../infra/exec-approval-forwarder.js";
|
||||
import type { ExecApprovalDecision } from "../../infra/exec-approvals.js";
|
||||
import type { PluginApprovalRequestPayload } from "../../infra/plugin-approvals.js";
|
||||
@@ -121,7 +122,10 @@ export function createPluginApprovalHandlers(
|
||||
}
|
||||
|
||||
const hasApprovalClients = context.hasExecApprovalClients?.(client?.connId) ?? false;
|
||||
if (!hasApprovalClients && !forwarded) {
|
||||
const hasTurnSourceRoute = hasApprovalTurnSourceRoute({
|
||||
turnSourceChannel: record.request.turnSourceChannel,
|
||||
});
|
||||
if (!hasApprovalClients && !forwarded && !hasTurnSourceRoute) {
|
||||
manager.expire(record.id, "no-approval-route");
|
||||
respond(
|
||||
true,
|
||||
|
||||
@@ -987,6 +987,45 @@ describe("exec approval handlers", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps approvals pending when the originating chat can handle /approve directly", async () => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
const { manager, handlers, forwarder, respond, context } =
|
||||
createForwardingExecApprovalFixture();
|
||||
const expireSpy = vi.spyOn(manager, "expire");
|
||||
|
||||
const requestPromise = requestExecApproval({
|
||||
handlers,
|
||||
respond,
|
||||
context,
|
||||
params: {
|
||||
twoPhase: true,
|
||||
timeoutMs: 60_000,
|
||||
id: "approval-chat-route",
|
||||
host: "gateway",
|
||||
turnSourceChannel: "slack",
|
||||
turnSourceTo: "D123",
|
||||
},
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(respond).toHaveBeenCalledWith(
|
||||
true,
|
||||
expect.objectContaining({ status: "accepted", id: "approval-chat-route" }),
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
expect(forwarder.handleRequested).toHaveBeenCalledTimes(1);
|
||||
expect(expireSpy).not.toHaveBeenCalled();
|
||||
|
||||
manager.resolve("approval-chat-route", "allow-once");
|
||||
await requestPromise;
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps approvals pending when no approver clients but forwarding accepted the request", async () => {
|
||||
const { manager, handlers, forwarder, respond, context } =
|
||||
createForwardingExecApprovalFixture();
|
||||
|
||||
Reference in New Issue
Block a user