Skip to content

Commit 2bac970

Browse files
committed
refactor: share node invoke policy test setup
1 parent f8e9ba3 commit 2bac970

1 file changed

Lines changed: 112 additions & 151 deletions

File tree

src/gateway/node-invoke-plugin-policy.test.ts

Lines changed: 112 additions & 151 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ import { applyPluginNodeInvokePolicy } from "./node-invoke-plugin-policy.js";
1010
import type { NodeSession } from "./node-registry.js";
1111
import type { GatewayClient, GatewayRequestContext } from "./server-methods/types.js";
1212

13+
const DEMO_PLUGIN_ID = "demo";
14+
const DEMO_COMMAND = "demo.read";
15+
const DEMO_PARAMS = { path: "/tmp/x" };
16+
1317
const registryState = vi.hoisted(() => ({
1418
current: null as PluginRegistry | null,
1519
}));
@@ -94,35 +98,102 @@ function createOperatorClient(): GatewayClient {
9498
});
9599
}
96100

101+
type NodeInvokePolicyRegistration = NonNullable<PluginRegistry["nodeInvokePolicies"]>[number];
102+
type NodeInvokePolicyHandler = NodeInvokePolicyRegistration["policy"]["handle"];
103+
type PluginApprovalRecord = ReturnType<
104+
ExecApprovalManager<PluginApprovalRequestPayload>["listPendingRecords"]
105+
>[number];
106+
107+
function createDemoPolicy(handle: NodeInvokePolicyHandler): NodeInvokePolicyRegistration {
108+
return {
109+
pluginId: DEMO_PLUGIN_ID,
110+
policy: {
111+
commands: [DEMO_COMMAND],
112+
handle,
113+
},
114+
pluginConfig: { enabled: true },
115+
source: "test",
116+
};
117+
}
118+
119+
function createApprovalRequestPolicy(params?: {
120+
timeoutMs?: number;
121+
}): NodeInvokePolicyRegistration {
122+
return createDemoPolicy(async (ctx: OpenClawPluginNodeInvokePolicyContext) => {
123+
const approval = await ctx.approvals?.request({
124+
title: "Sensitive action",
125+
description: "Needs approval",
126+
...(params?.timeoutMs === undefined ? {} : { timeoutMs: params.timeoutMs }),
127+
});
128+
return { ok: true, payload: approval ?? null };
129+
});
130+
}
131+
132+
function setDangerousDemoCommandRegistry(policies: NodeInvokePolicyRegistration[] = []) {
133+
registryState.current = {
134+
nodeHostCommands: [
135+
{
136+
pluginId: DEMO_PLUGIN_ID,
137+
command: {
138+
command: DEMO_COMMAND,
139+
dangerous: true,
140+
handle: async () => "{}",
141+
},
142+
source: "test",
143+
},
144+
],
145+
nodeInvokePolicies: policies,
146+
} as unknown as PluginRegistry;
147+
}
148+
149+
async function invokeDemoPolicy(
150+
context: GatewayRequestContext,
151+
client: GatewayClient | null = null,
152+
) {
153+
return await applyPluginNodeInvokePolicy({
154+
context,
155+
client,
156+
nodeSession: createNodeSession(),
157+
command: DEMO_COMMAND,
158+
params: DEMO_PARAMS,
159+
});
160+
}
161+
162+
async function expectSinglePendingApproval(
163+
manager: ExecApprovalManager<PluginApprovalRequestPayload>,
164+
): Promise<PluginApprovalRecord> {
165+
await vi.waitFor(() => {
166+
expect(manager.listPendingRecords()).toHaveLength(1);
167+
});
168+
const [record] = manager.listPendingRecords();
169+
if (!record) {
170+
throw new Error("expected pending approval");
171+
}
172+
return record;
173+
}
174+
175+
async function expectApprovalResolution(
176+
resultPromise: ReturnType<typeof applyPluginNodeInvokePolicy>,
177+
manager: ExecApprovalManager<PluginApprovalRequestPayload>,
178+
record: PluginApprovalRecord,
179+
) {
180+
expect(manager.resolve(record.id, "allow-once")).toBe(true);
181+
await expect(resultPromise).resolves.toStrictEqual({
182+
ok: true,
183+
payload: { id: record.id, decision: "allow-once" },
184+
});
185+
}
186+
97187
describe("applyPluginNodeInvokePolicy", () => {
98188
beforeEach(() => {
99189
registryState.current = null;
100190
});
101191

102192
it("fails closed for dangerous plugin node commands without a policy", async () => {
103-
registryState.current = {
104-
nodeHostCommands: [
105-
{
106-
pluginId: "demo",
107-
command: {
108-
command: "demo.read",
109-
dangerous: true,
110-
handle: async () => "{}",
111-
},
112-
source: "test",
113-
},
114-
],
115-
nodeInvokePolicies: [],
116-
} as unknown as PluginRegistry;
193+
setDangerousDemoCommandRegistry();
117194
const { context, invoke } = createContext();
118195

119-
const result = await applyPluginNodeInvokePolicy({
120-
context,
121-
client: null,
122-
nodeSession: createNodeSession(),
123-
command: "demo.read",
124-
params: { path: "/tmp/x" },
125-
});
196+
const result = await invokeDemoPolicy(context);
126197

127198
if (result === null) {
128199
throw new Error("expected plugin policy failure");
@@ -136,45 +207,18 @@ describe("applyPluginNodeInvokePolicy", () => {
136207
});
137208

138209
it("uses a matching plugin policy when one is registered", async () => {
139-
registryState.current = {
140-
nodeHostCommands: [
141-
{
142-
pluginId: "demo",
143-
command: {
144-
command: "demo.read",
145-
dangerous: true,
146-
handle: async () => "{}",
147-
},
148-
source: "test",
149-
},
150-
],
151-
nodeInvokePolicies: [
152-
{
153-
pluginId: "demo",
154-
policy: {
155-
commands: ["demo.read"],
156-
handle: (ctx: OpenClawPluginNodeInvokePolicyContext) => ctx.invokeNode(),
157-
},
158-
pluginConfig: { enabled: true },
159-
source: "test",
160-
},
161-
],
162-
} as unknown as PluginRegistry;
210+
setDangerousDemoCommandRegistry([
211+
createDemoPolicy((ctx: OpenClawPluginNodeInvokePolicyContext) => ctx.invokeNode()),
212+
]);
163213
const { context, invoke } = createContext();
164214

165-
const result = await applyPluginNodeInvokePolicy({
166-
context,
167-
client: null,
168-
nodeSession: createNodeSession(),
169-
command: "demo.read",
170-
params: { path: "/tmp/x" },
171-
});
215+
const result = await invokeDemoPolicy(context);
172216

173217
expect(result).toStrictEqual({ ok: true, payload: { ok: true, value: 1 }, payloadJSON: null });
174218
expect(invoke).toHaveBeenCalledWith({
175219
nodeId: "node-1",
176-
command: "demo.read",
177-
params: { path: "/tmp/x" },
220+
command: DEMO_COMMAND,
221+
params: DEMO_PARAMS,
178222
timeoutMs: undefined,
179223
idempotencyKey: undefined,
180224
});
@@ -195,103 +239,33 @@ describe("applyPluginNodeInvokePolicy", () => {
195239
deviceId: "device-other",
196240
}),
197241
]);
198-
registryState.current = {
199-
nodeHostCommands: [
200-
{
201-
pluginId: "demo",
202-
command: {
203-
command: "demo.read",
204-
dangerous: true,
205-
handle: async () => "{}",
206-
},
207-
source: "test",
208-
},
209-
],
210-
nodeInvokePolicies: [
211-
{
212-
pluginId: "demo",
213-
policy: {
214-
commands: ["demo.read"],
215-
handle: async (ctx: OpenClawPluginNodeInvokePolicyContext) => {
216-
const approval = await ctx.approvals?.request({
217-
title: "Sensitive action",
218-
description: "Needs approval",
219-
});
220-
return { ok: true, payload: approval ?? null };
221-
},
222-
},
223-
pluginConfig: { enabled: true },
224-
source: "test",
225-
},
226-
],
227-
} as unknown as PluginRegistry;
242+
setDangerousDemoCommandRegistry([createApprovalRequestPolicy()]);
228243
const { context } = createContext({
229244
pluginApprovalManager: manager,
230245
getApprovalClientConnIds,
231246
});
232-
const resultPromise = applyPluginNodeInvokePolicy({
233-
context,
234-
client: createOperatorClient(),
235-
nodeSession: createNodeSession(),
236-
command: "demo.read",
237-
params: { path: "/tmp/x" },
238-
});
247+
const resultPromise = invokeDemoPolicy(context, createOperatorClient());
239248

240-
await vi.waitFor(() => {
241-
expect(manager.listPendingRecords()).toHaveLength(1);
242-
});
243-
const [record] = manager.listPendingRecords();
244-
expect(record?.requestedByConnId).toBe("conn-requester");
245-
expect(record?.requestedByDeviceId).toBe("device-owner");
246-
expect(record?.requestedByClientId).toBe("client-owner");
249+
const record = await expectSinglePendingApproval(manager);
250+
expect(record.requestedByConnId).toBe("conn-requester");
251+
expect(record.requestedByDeviceId).toBe("device-owner");
252+
expect(record.requestedByClientId).toBe("client-owner");
247253
expect(context.broadcast).not.toHaveBeenCalled();
248254
expect(context.broadcastToConnIds).toHaveBeenCalledWith(
249255
"plugin.approval.requested",
250-
expect.objectContaining({ id: record?.id }),
256+
expect.objectContaining({ id: record.id }),
251257
visibleConnIds,
252258
{ dropIfSlow: true },
253259
);
254260

255-
expect(manager.resolve(record.id, "allow-once")).toBe(true);
256-
await expect(resultPromise).resolves.toStrictEqual({
257-
ok: true,
258-
payload: { id: record?.id, decision: "allow-once" },
259-
});
261+
await expectApprovalResolution(resultPromise, manager, record);
260262
});
261263

262264
it("caps plugin policy approval timeouts through the shared approval policy", async () => {
263265
const manager = new ExecApprovalManager<PluginApprovalRequestPayload>();
264-
registryState.current = {
265-
nodeHostCommands: [
266-
{
267-
pluginId: "demo",
268-
command: {
269-
command: "demo.read",
270-
dangerous: true,
271-
handle: async () => "{}",
272-
},
273-
source: "test",
274-
},
275-
],
276-
nodeInvokePolicies: [
277-
{
278-
pluginId: "demo",
279-
policy: {
280-
commands: ["demo.read"],
281-
handle: async (ctx: OpenClawPluginNodeInvokePolicyContext) => {
282-
const approval = await ctx.approvals?.request({
283-
title: "Sensitive action",
284-
description: "Needs approval",
285-
timeoutMs: Number.MAX_SAFE_INTEGER,
286-
});
287-
return { ok: true, payload: approval ?? null };
288-
},
289-
},
290-
pluginConfig: { enabled: true },
291-
source: "test",
292-
},
293-
],
294-
} as unknown as PluginRegistry;
266+
setDangerousDemoCommandRegistry([
267+
createApprovalRequestPolicy({ timeoutMs: Number.MAX_SAFE_INTEGER }),
268+
]);
295269
const { context } = createContext({
296270
pluginApprovalManager: manager,
297271
getApprovalClientConnIds: createApprovalClientLookup([
@@ -302,25 +276,12 @@ describe("applyPluginNodeInvokePolicy", () => {
302276
}),
303277
]),
304278
});
305-
const resultPromise = applyPluginNodeInvokePolicy({
306-
context,
307-
client: createOperatorClient(),
308-
nodeSession: createNodeSession(),
309-
command: "demo.read",
310-
params: { path: "/tmp/x" },
311-
});
279+
const resultPromise = invokeDemoPolicy(context, createOperatorClient());
312280

313-
await vi.waitFor(() => {
314-
expect(manager.listPendingRecords()).toHaveLength(1);
315-
});
316-
const [record] = manager.listPendingRecords();
281+
const record = await expectSinglePendingApproval(manager);
317282
expect(record.expiresAtMs - record.createdAtMs).toBe(MAX_PLUGIN_APPROVAL_TIMEOUT_MS);
318283

319-
expect(manager.resolve(record.id, "allow-once")).toBe(true);
320-
await expect(resultPromise).resolves.toStrictEqual({
321-
ok: true,
322-
payload: { id: record.id, decision: "allow-once" },
323-
});
284+
await expectApprovalResolution(resultPromise, manager, record);
324285
});
325286

326287
it("leaves commands without a dangerous plugin registration to normal allowlist handling", async () => {

0 commit comments

Comments
 (0)