mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 13:10:43 +00:00
refactor: reuse operator approval gateway lifecycle
This commit is contained in:
@@ -106,4 +106,19 @@ describe("withOperatorApprovalsGatewayClient", () => {
|
||||
),
|
||||
).rejects.toThrow("gateway closed (1008): pairing required");
|
||||
});
|
||||
|
||||
it("falls back to stop when stopAndWait rejects", async () => {
|
||||
clientState.stopAndWaitSpy.mockRejectedValueOnce(new Error("close failed"));
|
||||
|
||||
await withOperatorApprovalsGatewayClient(
|
||||
{
|
||||
config: {} as never,
|
||||
clientDisplayName: "Matrix approval (@owner:example.org)",
|
||||
},
|
||||
async () => undefined,
|
||||
);
|
||||
|
||||
expect(clientState.stopAndWaitSpy).toHaveBeenCalledTimes(1);
|
||||
expect(clientState.stopSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,51 +2,19 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { resolveApprovalOverGateway } from "./approval-gateway-resolver.js";
|
||||
|
||||
const hoisted = vi.hoisted(() => ({
|
||||
createOperatorApprovalsGatewayClient: vi.fn(),
|
||||
clientStart: vi.fn(),
|
||||
clientStop: vi.fn(),
|
||||
clientStopAndWait: vi.fn(),
|
||||
withOperatorApprovalsGatewayClient: vi.fn(),
|
||||
clientRequest: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../gateway/operator-approvals-client.js", () => ({
|
||||
createOperatorApprovalsGatewayClient: hoisted.createOperatorApprovalsGatewayClient,
|
||||
withOperatorApprovalsGatewayClient: hoisted.withOperatorApprovalsGatewayClient,
|
||||
}));
|
||||
|
||||
function createGatewayClient(params: {
|
||||
stopAndWaitRejects?: boolean;
|
||||
requestImpl?: typeof hoisted.clientRequest;
|
||||
}) {
|
||||
const request = params.requestImpl ?? hoisted.clientRequest;
|
||||
return {
|
||||
start: () => {
|
||||
hoisted.clientStart();
|
||||
},
|
||||
stop: hoisted.clientStop,
|
||||
stopAndWait: params.stopAndWaitRejects
|
||||
? vi.fn(async () => {
|
||||
hoisted.clientStopAndWait();
|
||||
throw new Error("close failed");
|
||||
})
|
||||
: vi.fn(async () => {
|
||||
hoisted.clientStopAndWait();
|
||||
}),
|
||||
request,
|
||||
};
|
||||
}
|
||||
|
||||
describe("resolveApprovalOverGateway", () => {
|
||||
beforeEach(() => {
|
||||
hoisted.clientStart.mockReset();
|
||||
hoisted.clientStop.mockReset();
|
||||
hoisted.clientStopAndWait.mockReset();
|
||||
hoisted.clientRequest.mockReset().mockResolvedValue({ ok: true });
|
||||
hoisted.createOperatorApprovalsGatewayClient.mockReset().mockImplementation(async (params) => {
|
||||
const client = createGatewayClient({});
|
||||
queueMicrotask(() => {
|
||||
params.onHelloOk?.({} as never);
|
||||
});
|
||||
return client;
|
||||
hoisted.withOperatorApprovalsGatewayClient.mockReset().mockImplementation(async (_, run) => {
|
||||
await run({ request: hoisted.clientRequest });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -59,19 +27,18 @@ describe("resolveApprovalOverGateway", () => {
|
||||
clientDisplayName: "Discord approval (default)",
|
||||
});
|
||||
|
||||
expect(hoisted.createOperatorApprovalsGatewayClient).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
expect(hoisted.withOperatorApprovalsGatewayClient).toHaveBeenCalledWith(
|
||||
{
|
||||
config: { gateway: { auth: { token: "cfg-token" } } },
|
||||
gatewayUrl: "ws://gateway.example.test",
|
||||
clientDisplayName: "Discord approval (default)",
|
||||
}),
|
||||
},
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(hoisted.clientStart).toHaveBeenCalledTimes(1);
|
||||
expect(hoisted.clientRequest).toHaveBeenCalledWith("exec.approval.resolve", {
|
||||
id: "approval-1",
|
||||
decision: "allow-once",
|
||||
});
|
||||
expect(hoisted.clientStopAndWait).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("routes plugin approvals through plugin.approval.resolve", async () => {
|
||||
@@ -121,23 +88,4 @@ describe("resolveApprovalOverGateway", () => {
|
||||
|
||||
expect(hoisted.clientRequest).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("falls back to stop when stopAndWait rejects", async () => {
|
||||
hoisted.createOperatorApprovalsGatewayClient.mockReset().mockImplementation(async (params) => {
|
||||
const client = createGatewayClient({ stopAndWaitRejects: true });
|
||||
queueMicrotask(() => {
|
||||
params.onHelloOk?.({} as never);
|
||||
});
|
||||
return client;
|
||||
});
|
||||
|
||||
await resolveApprovalOverGateway({
|
||||
cfg: {} as never,
|
||||
approvalId: "approval-1",
|
||||
decision: "allow-once",
|
||||
});
|
||||
|
||||
expect(hoisted.clientStopAndWait).toHaveBeenCalledTimes(1);
|
||||
expect(hoisted.clientStop).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { createOperatorApprovalsGatewayClient } from "../gateway/operator-approvals-client.js";
|
||||
import { withOperatorApprovalsGatewayClient } from "../gateway/operator-approvals-client.js";
|
||||
import { isApprovalNotFoundError } from "./approval-errors.js";
|
||||
import type { ExecApprovalDecision } from "./exec-approvals.js";
|
||||
|
||||
@@ -16,64 +16,34 @@ export type ResolveApprovalOverGatewayParams = {
|
||||
export async function resolveApprovalOverGateway(
|
||||
params: ResolveApprovalOverGatewayParams,
|
||||
): Promise<void> {
|
||||
let readySettled = false;
|
||||
let resolveReady!: () => void;
|
||||
let rejectReady!: (err: unknown) => void;
|
||||
const ready = new Promise<void>((resolve, reject) => {
|
||||
resolveReady = resolve;
|
||||
rejectReady = reject;
|
||||
});
|
||||
const markReady = () => {
|
||||
if (readySettled) {
|
||||
return;
|
||||
}
|
||||
readySettled = true;
|
||||
resolveReady();
|
||||
};
|
||||
const failReady = (err: unknown) => {
|
||||
if (readySettled) {
|
||||
return;
|
||||
}
|
||||
readySettled = true;
|
||||
rejectReady(err);
|
||||
};
|
||||
|
||||
const gatewayClient = await createOperatorApprovalsGatewayClient({
|
||||
config: params.cfg,
|
||||
gatewayUrl: params.gatewayUrl,
|
||||
clientDisplayName:
|
||||
params.clientDisplayName ?? `Approval (${params.senderId?.trim() || "unknown"})`,
|
||||
onHelloOk: markReady,
|
||||
onConnectError: failReady,
|
||||
onClose: (code, reason) => {
|
||||
failReady(new Error(`gateway closed (${code}): ${reason}`));
|
||||
await withOperatorApprovalsGatewayClient(
|
||||
{
|
||||
config: params.cfg,
|
||||
gatewayUrl: params.gatewayUrl,
|
||||
clientDisplayName:
|
||||
params.clientDisplayName ?? `Approval (${params.senderId?.trim() || "unknown"})`,
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
gatewayClient.start();
|
||||
await ready;
|
||||
const requestResolve = async (method: "exec.approval.resolve" | "plugin.approval.resolve") => {
|
||||
await gatewayClient.request(method, {
|
||||
id: params.approvalId,
|
||||
decision: params.decision,
|
||||
});
|
||||
};
|
||||
if (params.approvalId.startsWith("plugin:")) {
|
||||
await requestResolve("plugin.approval.resolve");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await requestResolve("exec.approval.resolve");
|
||||
} catch (err) {
|
||||
if (!params.allowPluginFallback || !isApprovalNotFoundError(err)) {
|
||||
throw err;
|
||||
async (gatewayClient) => {
|
||||
const requestResolve = async (
|
||||
method: "exec.approval.resolve" | "plugin.approval.resolve",
|
||||
) => {
|
||||
await gatewayClient.request(method, {
|
||||
id: params.approvalId,
|
||||
decision: params.decision,
|
||||
});
|
||||
};
|
||||
if (params.approvalId.startsWith("plugin:")) {
|
||||
await requestResolve("plugin.approval.resolve");
|
||||
return;
|
||||
}
|
||||
await requestResolve("plugin.approval.resolve");
|
||||
}
|
||||
} finally {
|
||||
await gatewayClient.stopAndWait().catch(() => {
|
||||
gatewayClient.stop();
|
||||
});
|
||||
}
|
||||
try {
|
||||
await requestResolve("exec.approval.resolve");
|
||||
} catch (err) {
|
||||
if (!params.allowPluginFallback || !isApprovalNotFoundError(err)) {
|
||||
throw err;
|
||||
}
|
||||
await requestResolve("plugin.approval.resolve");
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user