mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-03 21:24:05 +00:00
refactor: share node invoke approval test helpers
This commit is contained in:
@@ -51,6 +51,38 @@ async function expectNoForwardedInvoke(hasInvoke: () => boolean): Promise<void>
|
||||
expect(hasInvoke()).toBe(false);
|
||||
}
|
||||
|
||||
function parseInvokeParamsJSON(payload: unknown): Record<string, unknown> | null {
|
||||
const obj = payload as { paramsJSON?: unknown };
|
||||
const raw = typeof obj?.paramsJSON === "string" ? obj.paramsJSON : "";
|
||||
return raw ? (JSON.parse(raw) as Record<string, unknown>) : null;
|
||||
}
|
||||
|
||||
function createInvokeParamCapture() {
|
||||
let invokeCount = 0;
|
||||
let lastInvokeParams: Record<string, unknown> | null = null;
|
||||
return {
|
||||
count: () => invokeCount,
|
||||
onInvoke: (payload: unknown) => {
|
||||
invokeCount += 1;
|
||||
lastInvokeParams = parseInvokeParamsJSON(payload);
|
||||
},
|
||||
waitForParams: async () => {
|
||||
await vi.waitFor(
|
||||
() => {
|
||||
if (!lastInvokeParams) {
|
||||
throw new Error("expected forwarded invoke params");
|
||||
}
|
||||
},
|
||||
{
|
||||
timeout: 5_000,
|
||||
interval: 50,
|
||||
},
|
||||
);
|
||||
return requireRecord(lastInvokeParams, "forwarded invoke params");
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function requireNonEmptyString(value: string | null | undefined, label: string): string {
|
||||
if (!value) {
|
||||
throw new Error(`expected ${label}`);
|
||||
@@ -126,6 +158,38 @@ async function requestAllowOnceApproval(
|
||||
return approvalId;
|
||||
}
|
||||
|
||||
function approvedSystemRunParams(
|
||||
command: string[],
|
||||
rawCommand: string,
|
||||
runId: string,
|
||||
extra: Record<string, unknown> = {},
|
||||
): Record<string, unknown> {
|
||||
return {
|
||||
command,
|
||||
rawCommand,
|
||||
runId,
|
||||
approved: true,
|
||||
approvalDecision: "allow-once",
|
||||
...extra,
|
||||
};
|
||||
}
|
||||
|
||||
function approvedChatSystemRunParams(
|
||||
context: ChatApprovalContext,
|
||||
runId: string,
|
||||
extra: Record<string, unknown> = {},
|
||||
): Record<string, unknown> {
|
||||
return approvedSystemRunParams(["echo", "chat"], "echo chat", runId, {
|
||||
agentId: context.agentId,
|
||||
sessionKey: context.sessionKey,
|
||||
turnSourceChannel: context.turnSourceChannel,
|
||||
turnSourceTo: context.turnSourceTo,
|
||||
turnSourceAccountId: context.turnSourceAccountId,
|
||||
turnSourceThreadId: context.turnSourceThreadId,
|
||||
...extra,
|
||||
});
|
||||
}
|
||||
|
||||
type ChatApprovalContext = {
|
||||
agentId: string;
|
||||
sessionKey: string;
|
||||
@@ -509,18 +573,8 @@ describe("node.invoke approval bypass", () => {
|
||||
});
|
||||
|
||||
test("binds approvals to decision/device and blocks cross-device replay", async () => {
|
||||
let invokeCount = 0;
|
||||
let lastInvokeParams: Record<string, unknown> | null = null;
|
||||
const node = await connectLinuxNode((payload) => {
|
||||
invokeCount += 1;
|
||||
const obj = payload as { paramsJSON?: unknown };
|
||||
const raw = typeof obj?.paramsJSON === "string" ? obj.paramsJSON : "";
|
||||
if (!raw) {
|
||||
lastInvokeParams = null;
|
||||
return;
|
||||
}
|
||||
lastInvokeParams = JSON.parse(raw) as Record<string, unknown>;
|
||||
});
|
||||
const invokeCapture = createInvokeParamCapture();
|
||||
const node = await connectLinuxNode(invokeCapture.onInvoke);
|
||||
|
||||
const wsApprover = await connectOperator(["operator.write", "operator.approvals"]);
|
||||
const wsCaller = await connectOperator(["operator.write"]);
|
||||
@@ -534,50 +588,29 @@ describe("node.invoke approval bypass", () => {
|
||||
const invoke = await rpcReq(wsCaller, "node.invoke", {
|
||||
nodeId,
|
||||
command: "system.run",
|
||||
params: {
|
||||
command: ["echo", "hi"],
|
||||
rawCommand: "echo hi",
|
||||
runId: approvalId,
|
||||
approved: true,
|
||||
params: approvedSystemRunParams(["echo", "hi"], "echo hi", approvalId, {
|
||||
approvalDecision: "allow-always",
|
||||
injected: "nope",
|
||||
},
|
||||
}),
|
||||
idempotencyKey: crypto.randomUUID(),
|
||||
});
|
||||
expect(invoke.ok).toBe(true);
|
||||
await vi.waitFor(
|
||||
() => {
|
||||
if (!lastInvokeParams) {
|
||||
throw new Error("expected forwarded invoke params");
|
||||
}
|
||||
},
|
||||
{
|
||||
timeout: 5_000,
|
||||
interval: 50,
|
||||
},
|
||||
);
|
||||
const forwardedParams = requireRecord(lastInvokeParams, "forwarded invoke params");
|
||||
const forwardedParams = await invokeCapture.waitForParams();
|
||||
expect(forwardedParams["approved"]).toBe(true);
|
||||
expect(forwardedParams["approvalDecision"]).toBe("allow-once");
|
||||
expect(forwardedParams["injected"]).toBeUndefined();
|
||||
|
||||
const replayApprovalId = await requestAllowOnceApproval(wsApprover, "echo hi", nodeId);
|
||||
const invokeCountBeforeReplay = invokeCount;
|
||||
const invokeCountBeforeReplay = invokeCapture.count();
|
||||
const replay = await rpcReq(wsOtherDevice, "node.invoke", {
|
||||
nodeId,
|
||||
command: "system.run",
|
||||
params: {
|
||||
command: ["echo", "hi"],
|
||||
rawCommand: "echo hi",
|
||||
runId: replayApprovalId,
|
||||
approved: true,
|
||||
approvalDecision: "allow-once",
|
||||
},
|
||||
params: approvedSystemRunParams(["echo", "hi"], "echo hi", replayApprovalId),
|
||||
idempotencyKey: crypto.randomUUID(),
|
||||
});
|
||||
expect(replay.ok).toBe(false);
|
||||
expect(replay.error?.message ?? "").toContain("not valid for this device");
|
||||
await expectNoForwardedInvoke(() => invokeCount > invokeCountBeforeReplay);
|
||||
await expectNoForwardedInvoke(() => invokeCapture.count() > invokeCountBeforeReplay);
|
||||
} finally {
|
||||
wsApprover.close();
|
||||
wsCaller.close();
|
||||
@@ -587,14 +620,8 @@ describe("node.invoke approval bypass", () => {
|
||||
});
|
||||
|
||||
test("bridges no-device chat approvals across backend reconnects only for the same turn source", async () => {
|
||||
let invokeCount = 0;
|
||||
let lastInvokeParams: Record<string, unknown> | null = null;
|
||||
const node = await connectLinuxNode((payload) => {
|
||||
invokeCount += 1;
|
||||
const obj = payload as { paramsJSON?: unknown };
|
||||
const raw = typeof obj?.paramsJSON === "string" ? obj.paramsJSON : "";
|
||||
lastInvokeParams = raw ? (JSON.parse(raw) as Record<string, unknown>) : null;
|
||||
});
|
||||
const invokeCapture = createInvokeParamCapture();
|
||||
const node = await connectLinuxNode(invokeCapture.onInvoke);
|
||||
|
||||
const wsRequest = await connectTrustedBackend(["operator.write", "operator.approvals"]);
|
||||
const wsReplay = await connectTrustedBackend(["operator.write", "operator.approvals"]);
|
||||
@@ -619,34 +646,11 @@ describe("node.invoke approval bypass", () => {
|
||||
const invoke = await rpcReq(wsReplay, "node.invoke", {
|
||||
nodeId,
|
||||
command: "system.run",
|
||||
params: {
|
||||
command: ["echo", "chat"],
|
||||
rawCommand: "echo chat",
|
||||
agentId: context.agentId,
|
||||
sessionKey: context.sessionKey,
|
||||
turnSourceChannel: context.turnSourceChannel,
|
||||
turnSourceTo: context.turnSourceTo,
|
||||
turnSourceAccountId: context.turnSourceAccountId,
|
||||
turnSourceThreadId: context.turnSourceThreadId,
|
||||
runId: approvalId,
|
||||
approved: true,
|
||||
approvalDecision: "allow-once",
|
||||
},
|
||||
params: approvedChatSystemRunParams(context, approvalId),
|
||||
idempotencyKey: crypto.randomUUID(),
|
||||
});
|
||||
expect(invoke.ok).toBe(true);
|
||||
await vi.waitFor(
|
||||
() => {
|
||||
if (!lastInvokeParams) {
|
||||
throw new Error("expected forwarded invoke params");
|
||||
}
|
||||
},
|
||||
{
|
||||
timeout: 5_000,
|
||||
interval: 50,
|
||||
},
|
||||
);
|
||||
const forwardedParams = requireRecord(lastInvokeParams, "forwarded invoke params");
|
||||
const forwardedParams = await invokeCapture.waitForParams();
|
||||
expect(forwardedParams["approved"]).toBe(true);
|
||||
expect(forwardedParams["approvalDecision"]).toBe("allow-once");
|
||||
expect(forwardedParams["turnSourceTo"]).toBeUndefined();
|
||||
@@ -657,28 +661,18 @@ describe("node.invoke approval bypass", () => {
|
||||
nodeId,
|
||||
context,
|
||||
});
|
||||
const invokeCountBeforeMismatch = invokeCount;
|
||||
const invokeCountBeforeMismatch = invokeCapture.count();
|
||||
const mismatch = await rpcReq(wsReplay, "node.invoke", {
|
||||
nodeId,
|
||||
command: "system.run",
|
||||
params: {
|
||||
command: ["echo", "chat"],
|
||||
rawCommand: "echo chat",
|
||||
agentId: context.agentId,
|
||||
sessionKey: context.sessionKey,
|
||||
turnSourceChannel: context.turnSourceChannel,
|
||||
params: approvedChatSystemRunParams(context, mismatchApprovalId, {
|
||||
turnSourceTo: "telegram:67890",
|
||||
turnSourceAccountId: context.turnSourceAccountId,
|
||||
turnSourceThreadId: context.turnSourceThreadId,
|
||||
runId: mismatchApprovalId,
|
||||
approved: true,
|
||||
approvalDecision: "allow-once",
|
||||
},
|
||||
}),
|
||||
idempotencyKey: crypto.randomUUID(),
|
||||
});
|
||||
expect(mismatch.ok).toBe(false);
|
||||
expect(mismatch.error?.message ?? "").toContain("not valid for this client");
|
||||
await expectNoForwardedInvoke(() => invokeCount > invokeCountBeforeMismatch);
|
||||
await expectNoForwardedInvoke(() => invokeCapture.count() > invokeCountBeforeMismatch);
|
||||
} finally {
|
||||
wsRequest.close();
|
||||
wsReplay.close();
|
||||
|
||||
Reference in New Issue
Block a user