mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix(gateway): dispatch late-registered plugin RPC methods
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
/**
|
||||
* Regression tests for plugin-registered gateway RPC dispatch (#94127).
|
||||
*/
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { createEmptyPluginRegistry } from "../plugins/registry-empty.js";
|
||||
import { resetPluginRuntimeStateForTest, setActivePluginRegistry } from "../plugins/runtime.js";
|
||||
import {
|
||||
createGatewayMethodRegistry,
|
||||
createPluginGatewayMethodDescriptor,
|
||||
} from "./methods/registry.js";
|
||||
import { WRITE_SCOPE } from "./operator-scopes.js";
|
||||
import { handleGatewayRequest } from "./server-methods.js";
|
||||
import type { GatewayRequestHandler } from "./server-methods/types.js";
|
||||
|
||||
describe("handleGatewayRequest plugin gateway dispatch", () => {
|
||||
afterEach(() => {
|
||||
resetPluginRuntimeStateForTest();
|
||||
});
|
||||
|
||||
it("dispatches plugin methods registered after the startup method registry snapshot", async () => {
|
||||
const handler = vi.fn<GatewayRequestHandler>(({ respond }) => {
|
||||
respond(true, { ok: true, ts: 42 });
|
||||
});
|
||||
const activeRegistry = createEmptyPluginRegistry();
|
||||
activeRegistry.gatewayHandlers["demo.ping"] = handler;
|
||||
activeRegistry.gatewayMethodDescriptors.push(
|
||||
createPluginGatewayMethodDescriptor({
|
||||
pluginId: "demo",
|
||||
name: "demo.ping",
|
||||
handler,
|
||||
scope: WRITE_SCOPE,
|
||||
}),
|
||||
);
|
||||
setActivePluginRegistry(activeRegistry);
|
||||
|
||||
const staleStartupRegistry = createGatewayMethodRegistry([]);
|
||||
const respond = vi.fn();
|
||||
await handleGatewayRequest({
|
||||
req: {
|
||||
type: "req",
|
||||
id: "proof-94127",
|
||||
method: "demo.ping",
|
||||
params: { hello: "world" },
|
||||
},
|
||||
respond,
|
||||
client: {
|
||||
connId: "conn-proof",
|
||||
connect: {
|
||||
role: "operator",
|
||||
scopes: [WRITE_SCOPE],
|
||||
client: {
|
||||
id: "cli",
|
||||
version: "test",
|
||||
platform: "linux",
|
||||
mode: "cli",
|
||||
},
|
||||
minProtocol: 1,
|
||||
maxProtocol: 1,
|
||||
},
|
||||
},
|
||||
isWebchatConnect: () => false,
|
||||
context: {
|
||||
logGateway: { warn: vi.fn() },
|
||||
} as unknown as Parameters<typeof handleGatewayRequest>[0]["context"],
|
||||
methodRegistry: staleStartupRegistry,
|
||||
});
|
||||
|
||||
expect(handler).toHaveBeenCalledTimes(1);
|
||||
expect(respond).toHaveBeenCalledWith(true, { ok: true, ts: 42 });
|
||||
});
|
||||
|
||||
it("dispatches a method owned by the caller-attached registry even when global state lacks it (#94343)", async () => {
|
||||
const handler = vi.fn<GatewayRequestHandler>(({ respond }) => {
|
||||
respond(true, { ok: true, source: "attached" });
|
||||
});
|
||||
// Active plugin registry does NOT carry the method; only the caller-attached
|
||||
// snapshot owns it, so dispatch must prefer the attached registry.
|
||||
setActivePluginRegistry(createEmptyPluginRegistry());
|
||||
const attachedRegistry = createGatewayMethodRegistry([
|
||||
createPluginGatewayMethodDescriptor({
|
||||
pluginId: "demo",
|
||||
name: "demo.attached",
|
||||
handler,
|
||||
scope: WRITE_SCOPE,
|
||||
}),
|
||||
]);
|
||||
const respond = vi.fn();
|
||||
await handleGatewayRequest({
|
||||
req: { type: "req", id: "proof-94343", method: "demo.attached", params: {} },
|
||||
respond,
|
||||
client: {
|
||||
connId: "conn-proof",
|
||||
connect: {
|
||||
role: "operator",
|
||||
scopes: [WRITE_SCOPE],
|
||||
client: { id: "cli", version: "test", platform: "linux", mode: "cli" },
|
||||
minProtocol: 1,
|
||||
maxProtocol: 1,
|
||||
},
|
||||
},
|
||||
isWebchatConnect: () => false,
|
||||
context: {
|
||||
logGateway: { warn: vi.fn() },
|
||||
} as unknown as Parameters<typeof handleGatewayRequest>[0]["context"],
|
||||
methodRegistry: attachedRegistry,
|
||||
});
|
||||
|
||||
expect(handler).toHaveBeenCalledTimes(1);
|
||||
expect(respond).toHaveBeenCalledWith(true, { ok: true, source: "attached" });
|
||||
});
|
||||
|
||||
it("fails closed when neither the attached snapshot nor the live registry owns the method", async () => {
|
||||
const handler = vi.fn<GatewayRequestHandler>();
|
||||
setActivePluginRegistry(createEmptyPluginRegistry());
|
||||
const respond = vi.fn();
|
||||
await handleGatewayRequest({
|
||||
req: { type: "req", id: "proof-unknown", method: "demo.does-not-exist", params: {} },
|
||||
respond,
|
||||
client: {
|
||||
connId: "conn-proof",
|
||||
connect: {
|
||||
role: "operator",
|
||||
scopes: [WRITE_SCOPE],
|
||||
client: { id: "cli", version: "test", platform: "linux", mode: "cli" },
|
||||
minProtocol: 1,
|
||||
maxProtocol: 1,
|
||||
},
|
||||
},
|
||||
isWebchatConnect: () => false,
|
||||
context: {
|
||||
logGateway: { warn: vi.fn() },
|
||||
} as unknown as Parameters<typeof handleGatewayRequest>[0]["context"],
|
||||
methodRegistry: createGatewayMethodRegistry([]),
|
||||
});
|
||||
|
||||
expect(handler).not.toHaveBeenCalled();
|
||||
const [ok] = respond.mock.calls.at(-1) ?? [];
|
||||
expect(ok).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -639,8 +639,14 @@ export async function handleGatewayRequest(
|
||||
opts: GatewayRequestOptions & { extraHandlers?: GatewayRequestHandlers },
|
||||
): Promise<void> {
|
||||
const { req, respond, client, isWebchatConnect, context } = opts;
|
||||
// Prefer the caller-attached registry when it owns the requested method so plugin dispatch
|
||||
// metadata newer than global runtime state still authorizes and dispatches correctly. When the
|
||||
// attached snapshot does not own the method, rebuild from the live plugin registry so plugin RPC
|
||||
// methods registered after the startup snapshot stay reachable (#94127).
|
||||
const methodRegistry =
|
||||
opts.methodRegistry ?? createRequestGatewayMethodRegistry(opts.extraHandlers);
|
||||
opts.methodRegistry?.getHandler(req.method) !== undefined
|
||||
? opts.methodRegistry
|
||||
: createRequestGatewayMethodRegistry(opts.extraHandlers);
|
||||
const authError = authorizeGatewayMethod(req.method, client, req.params, methodRegistry);
|
||||
if (authError) {
|
||||
respond(false, undefined, authError);
|
||||
|
||||
Reference in New Issue
Block a user