test(agents): dedupe agent and cron test scaffolds

This commit is contained in:
Peter Steinberger
2026-03-02 06:40:42 +00:00
parent 281494ae52
commit 7e29d604ba
38 changed files with 3114 additions and 4486 deletions

View File

@@ -15,6 +15,14 @@ import { createOpenClawTools } from "./openclaw-tools.js";
const NODE_ID = "mac-1";
const BASE_RUN_INPUT = { action: "run", node: NODE_ID, command: ["echo", "hi"] } as const;
const JPG_PAYLOAD = {
format: "jpg",
base64: "aGVsbG8=",
width: 1,
height: 1,
} as const;
type GatewayCall = { method: string; params?: unknown };
function unexpectedGatewayMethod(method: unknown): never {
throw new Error(`unexpected method: ${String(method)}`);
@@ -32,24 +40,99 @@ async function executeNodes(input: Record<string, unknown>) {
return getNodesTool().execute("call1", input as never);
}
type NodesToolResult = Awaited<ReturnType<typeof executeNodes>>;
type GatewayMockResult = Record<string, unknown> | null | undefined;
function mockNodeList(commands?: string[]) {
return {
nodes: [{ nodeId: NODE_ID, ...(commands ? { commands } : {}) }],
};
}
function expectSingleImage(result: NodesToolResult, params?: { mimeType?: string }) {
const images = (result.content ?? []).filter((block) => block.type === "image");
expect(images).toHaveLength(1);
if (params?.mimeType) {
expect(images[0]?.mimeType).toBe(params.mimeType);
}
}
function expectFirstTextContains(result: NodesToolResult, expectedText: string) {
expect(result.content?.[0]).toMatchObject({
type: "text",
text: expect.stringContaining(expectedText),
});
}
function setupNodeInvokeMock(params: {
commands?: string[];
onInvoke?: (invokeParams: unknown) => GatewayMockResult | Promise<GatewayMockResult>;
invokePayload?: unknown;
}) {
callGateway.mockImplementation(async ({ method, params: invokeParams }: GatewayCall) => {
if (method === "node.list") {
return mockNodeList(params.commands);
}
if (method === "node.invoke") {
if (params.onInvoke) {
return await params.onInvoke(invokeParams);
}
if (params.invokePayload !== undefined) {
return { payload: params.invokePayload };
}
return { payload: {} };
}
return unexpectedGatewayMethod(method);
});
}
function createSystemRunPreparePayload(cwd: string | null) {
return {
payload: {
cmdText: "echo hi",
plan: {
argv: ["echo", "hi"],
cwd,
rawCommand: "echo hi",
agentId: null,
sessionKey: null,
},
},
};
}
function setupSystemRunGateway(params: {
onRunInvoke: (invokeParams: unknown) => GatewayMockResult | Promise<GatewayMockResult>;
onApprovalRequest?: (approvalParams: unknown) => GatewayMockResult | Promise<GatewayMockResult>;
prepareCwd?: string | null;
}) {
callGateway.mockImplementation(async ({ method, params: gatewayParams }: GatewayCall) => {
if (method === "node.list") {
return mockNodeList(["system.run"]);
}
if (method === "node.invoke") {
const command = (gatewayParams as { command?: string } | undefined)?.command;
if (command === "system.run.prepare") {
return createSystemRunPreparePayload(params.prepareCwd ?? null);
}
return await params.onRunInvoke(gatewayParams);
}
if (method === "exec.approval.request" && params.onApprovalRequest) {
return await params.onApprovalRequest(gatewayParams);
}
return unexpectedGatewayMethod(method);
});
}
beforeEach(() => {
callGateway.mockClear();
});
describe("nodes camera_snap", () => {
it("uses front/high-quality defaults when params are omitted", async () => {
callGateway.mockImplementation(async ({ method, params }) => {
if (method === "node.list") {
return mockNodeList();
}
if (method === "node.invoke") {
expect(params).toMatchObject({
setupNodeInvokeMock({
onInvoke: (invokeParams) => {
expect(invokeParams).toMatchObject({
command: "camera.snap",
params: {
facing: "front",
@@ -57,16 +140,8 @@ describe("nodes camera_snap", () => {
quality: 0.95,
},
});
return {
payload: {
format: "jpg",
base64: "aGVsbG8=",
width: 1,
height: 1,
},
};
}
return unexpectedGatewayMethod(method);
return { payload: JPG_PAYLOAD };
},
});
const result = await executeNodes({
@@ -74,26 +149,12 @@ describe("nodes camera_snap", () => {
node: NODE_ID,
});
const images = (result.content ?? []).filter((block) => block.type === "image");
expect(images).toHaveLength(1);
expectSingleImage(result);
});
it("maps jpg payloads to image/jpeg", async () => {
callGateway.mockImplementation(async ({ method }) => {
if (method === "node.list") {
return mockNodeList();
}
if (method === "node.invoke") {
return {
payload: {
format: "jpg",
base64: "aGVsbG8=",
width: 1,
height: 1,
},
};
}
return unexpectedGatewayMethod(method);
setupNodeInvokeMock({
invokePayload: JPG_PAYLOAD,
});
const result = await executeNodes({
@@ -102,31 +163,18 @@ describe("nodes camera_snap", () => {
facing: "front",
});
const images = (result.content ?? []).filter((block) => block.type === "image");
expect(images).toHaveLength(1);
expect(images[0]?.mimeType).toBe("image/jpeg");
expectSingleImage(result, { mimeType: "image/jpeg" });
});
it("passes deviceId when provided", async () => {
callGateway.mockImplementation(async ({ method, params }) => {
if (method === "node.list") {
return mockNodeList();
}
if (method === "node.invoke") {
expect(params).toMatchObject({
setupNodeInvokeMock({
onInvoke: (invokeParams) => {
expect(invokeParams).toMatchObject({
command: "camera.snap",
params: { deviceId: "cam-123" },
});
return {
payload: {
format: "jpg",
base64: "aGVsbG8=",
width: 1,
height: 1,
},
};
}
return unexpectedGatewayMethod(method);
return { payload: JPG_PAYLOAD };
},
});
await executeNodes({
@@ -151,12 +199,10 @@ describe("nodes camera_snap", () => {
describe("nodes notifications_list", () => {
it("invokes notifications.list and returns payload", async () => {
callGateway.mockImplementation(async ({ method, params }) => {
if (method === "node.list") {
return mockNodeList(["notifications.list"]);
}
if (method === "node.invoke") {
expect(params).toMatchObject({
setupNodeInvokeMock({
commands: ["notifications.list"],
onInvoke: (invokeParams) => {
expect(invokeParams).toMatchObject({
nodeId: NODE_ID,
command: "notifications.list",
params: {},
@@ -169,8 +215,7 @@ describe("nodes notifications_list", () => {
notifications: [{ key: "n1", packageName: "com.example.app" }],
},
};
}
return unexpectedGatewayMethod(method);
},
});
const result = await executeNodes({
@@ -178,21 +223,16 @@ describe("nodes notifications_list", () => {
node: NODE_ID,
});
expect(result.content?.[0]).toMatchObject({
type: "text",
text: expect.stringContaining('"notifications"'),
});
expectFirstTextContains(result, '"notifications"');
});
});
describe("nodes notifications_action", () => {
it("invokes notifications.actions dismiss", async () => {
callGateway.mockImplementation(async ({ method, params }) => {
if (method === "node.list") {
return mockNodeList(["notifications.actions"]);
}
if (method === "node.invoke") {
expect(params).toMatchObject({
setupNodeInvokeMock({
commands: ["notifications.actions"],
onInvoke: (invokeParams) => {
expect(invokeParams).toMatchObject({
nodeId: NODE_ID,
command: "notifications.actions",
params: {
@@ -201,8 +241,7 @@ describe("nodes notifications_action", () => {
},
});
return { payload: { ok: true, key: "n1", action: "dismiss" } };
}
return unexpectedGatewayMethod(method);
},
});
const result = await executeNodes({
@@ -212,21 +251,16 @@ describe("nodes notifications_action", () => {
notificationAction: "dismiss",
});
expect(result.content?.[0]).toMatchObject({
type: "text",
text: expect.stringContaining('"dismiss"'),
});
expectFirstTextContains(result, '"dismiss"');
});
});
describe("nodes device_status and device_info", () => {
it("invokes device.status and returns payload", async () => {
callGateway.mockImplementation(async ({ method, params }) => {
if (method === "node.list") {
return mockNodeList(["device.status", "device.info"]);
}
if (method === "node.invoke") {
expect(params).toMatchObject({
setupNodeInvokeMock({
commands: ["device.status", "device.info"],
onInvoke: (invokeParams) => {
expect(invokeParams).toMatchObject({
nodeId: NODE_ID,
command: "device.status",
params: {},
@@ -236,8 +270,7 @@ describe("nodes device_status and device_info", () => {
battery: { state: "charging", lowPowerModeEnabled: false },
},
};
}
return unexpectedGatewayMethod(method);
},
});
const result = await executeNodes({
@@ -245,19 +278,14 @@ describe("nodes device_status and device_info", () => {
node: NODE_ID,
});
expect(result.content?.[0]).toMatchObject({
type: "text",
text: expect.stringContaining('"battery"'),
});
expectFirstTextContains(result, '"battery"');
});
it("invokes device.info and returns payload", async () => {
callGateway.mockImplementation(async ({ method, params }) => {
if (method === "node.list") {
return mockNodeList(["device.status", "device.info"]);
}
if (method === "node.invoke") {
expect(params).toMatchObject({
setupNodeInvokeMock({
commands: ["device.status", "device.info"],
onInvoke: (invokeParams) => {
expect(invokeParams).toMatchObject({
nodeId: NODE_ID,
command: "device.info",
params: {},
@@ -268,8 +296,7 @@ describe("nodes device_status and device_info", () => {
appVersion: "1.0.0",
},
};
}
return unexpectedGatewayMethod(method);
},
});
const result = await executeNodes({
@@ -277,19 +304,14 @@ describe("nodes device_status and device_info", () => {
node: NODE_ID,
});
expect(result.content?.[0]).toMatchObject({
type: "text",
text: expect.stringContaining('"systemName"'),
});
expectFirstTextContains(result, '"systemName"');
});
it("invokes device.permissions and returns payload", async () => {
callGateway.mockImplementation(async ({ method, params }) => {
if (method === "node.list") {
return mockNodeList(["device.permissions"]);
}
if (method === "node.invoke") {
expect(params).toMatchObject({
setupNodeInvokeMock({
commands: ["device.permissions"],
onInvoke: (invokeParams) => {
expect(invokeParams).toMatchObject({
nodeId: NODE_ID,
command: "device.permissions",
params: {},
@@ -301,8 +323,7 @@ describe("nodes device_status and device_info", () => {
},
},
};
}
return unexpectedGatewayMethod(method);
},
});
const result = await executeNodes({
@@ -310,19 +331,14 @@ describe("nodes device_status and device_info", () => {
node: NODE_ID,
});
expect(result.content?.[0]).toMatchObject({
type: "text",
text: expect.stringContaining('"permissions"'),
});
expectFirstTextContains(result, '"permissions"');
});
it("invokes device.health and returns payload", async () => {
callGateway.mockImplementation(async ({ method, params }) => {
if (method === "node.list") {
return mockNodeList(["device.health"]);
}
if (method === "node.invoke") {
expect(params).toMatchObject({
setupNodeInvokeMock({
commands: ["device.health"],
onInvoke: (invokeParams) => {
expect(invokeParams).toMatchObject({
nodeId: NODE_ID,
command: "device.health",
params: {},
@@ -333,8 +349,7 @@ describe("nodes device_status and device_info", () => {
battery: { chargingType: "usb" },
},
};
}
return unexpectedGatewayMethod(method);
},
});
const result = await executeNodes({
@@ -342,36 +357,16 @@ describe("nodes device_status and device_info", () => {
node: NODE_ID,
});
expect(result.content?.[0]).toMatchObject({
type: "text",
text: expect.stringContaining('"memory"'),
});
expectFirstTextContains(result, '"memory"');
});
});
describe("nodes run", () => {
it("passes invoke and command timeouts", async () => {
callGateway.mockImplementation(async ({ method, params }) => {
if (method === "node.list") {
return mockNodeList(["system.run"]);
}
if (method === "node.invoke") {
const command = (params as { command?: string } | undefined)?.command;
if (command === "system.run.prepare") {
return {
payload: {
cmdText: "echo hi",
plan: {
argv: ["echo", "hi"],
cwd: "/tmp",
rawCommand: "echo hi",
agentId: null,
sessionKey: null,
},
},
};
}
expect(params).toMatchObject({
setupSystemRunGateway({
prepareCwd: "/tmp",
onRunInvoke: (invokeParams) => {
expect(invokeParams).toMatchObject({
nodeId: NODE_ID,
command: "system.run",
timeoutMs: 45_000,
@@ -385,8 +380,7 @@ describe("nodes run", () => {
return {
payload: { stdout: "", stderr: "", exitCode: 0, success: true },
};
}
return unexpectedGatewayMethod(method);
},
});
await executeNodes({
@@ -401,31 +395,13 @@ describe("nodes run", () => {
it("requests approval and retries with allow-once decision", async () => {
let invokeCalls = 0;
let approvalId: string | null = null;
callGateway.mockImplementation(async ({ method, params }) => {
if (method === "node.list") {
return mockNodeList(["system.run"]);
}
if (method === "node.invoke") {
const command = (params as { command?: string } | undefined)?.command;
if (command === "system.run.prepare") {
return {
payload: {
cmdText: "echo hi",
plan: {
argv: ["echo", "hi"],
cwd: null,
rawCommand: "echo hi",
agentId: null,
sessionKey: null,
},
},
};
}
setupSystemRunGateway({
onRunInvoke: (invokeParams) => {
invokeCalls += 1;
if (invokeCalls === 1) {
throw new Error("SYSTEM_RUN_DENIED: approval required");
}
expect(params).toMatchObject({
expect(invokeParams).toMatchObject({
nodeId: NODE_ID,
command: "system.run",
params: {
@@ -436,9 +412,9 @@ describe("nodes run", () => {
},
});
return { payload: { stdout: "", stderr: "", exitCode: 0, success: true } };
}
if (method === "exec.approval.request") {
expect(params).toMatchObject({
},
onApprovalRequest: (approvalParams) => {
expect(approvalParams).toMatchObject({
id: expect.any(String),
command: "echo hi",
commandArgv: ["echo", "hi"],
@@ -450,12 +426,11 @@ describe("nodes run", () => {
timeoutMs: 120_000,
});
approvalId =
typeof (params as { id?: unknown } | undefined)?.id === "string"
? ((params as { id: string }).id ?? null)
typeof (approvalParams as { id?: unknown } | undefined)?.id === "string"
? ((approvalParams as { id: string }).id ?? null)
: null;
return { decision: "allow-once" };
}
return unexpectedGatewayMethod(method);
},
});
await executeNodes(BASE_RUN_INPUT);
@@ -463,93 +438,36 @@ describe("nodes run", () => {
});
it("fails with user denied when approval decision is deny", async () => {
callGateway.mockImplementation(async ({ method, params }) => {
if (method === "node.list") {
return mockNodeList(["system.run"]);
}
if (method === "node.invoke") {
const command = (params as { command?: string } | undefined)?.command;
if (command === "system.run.prepare") {
return {
payload: {
cmdText: "echo hi",
plan: {
argv: ["echo", "hi"],
cwd: null,
rawCommand: "echo hi",
agentId: null,
sessionKey: null,
},
},
};
}
setupSystemRunGateway({
onRunInvoke: () => {
throw new Error("SYSTEM_RUN_DENIED: approval required");
}
if (method === "exec.approval.request") {
},
onApprovalRequest: () => {
return { decision: "deny" };
}
return unexpectedGatewayMethod(method);
},
});
await expect(executeNodes(BASE_RUN_INPUT)).rejects.toThrow("exec denied: user denied");
});
it("fails closed for timeout and invalid approval decisions", async () => {
callGateway.mockImplementation(async ({ method, params }) => {
if (method === "node.list") {
return mockNodeList(["system.run"]);
}
if (method === "node.invoke") {
const command = (params as { command?: string } | undefined)?.command;
if (command === "system.run.prepare") {
return {
payload: {
cmdText: "echo hi",
plan: {
argv: ["echo", "hi"],
cwd: null,
rawCommand: "echo hi",
agentId: null,
sessionKey: null,
},
},
};
}
setupSystemRunGateway({
onRunInvoke: () => {
throw new Error("SYSTEM_RUN_DENIED: approval required");
}
if (method === "exec.approval.request") {
},
onApprovalRequest: () => {
return {};
}
return unexpectedGatewayMethod(method);
},
});
await expect(executeNodes(BASE_RUN_INPUT)).rejects.toThrow("exec denied: approval timed out");
callGateway.mockImplementation(async ({ method, params }) => {
if (method === "node.list") {
return mockNodeList(["system.run"]);
}
if (method === "node.invoke") {
const command = (params as { command?: string } | undefined)?.command;
if (command === "system.run.prepare") {
return {
payload: {
cmdText: "echo hi",
plan: {
argv: ["echo", "hi"],
cwd: null,
rawCommand: "echo hi",
agentId: null,
sessionKey: null,
},
},
};
}
setupSystemRunGateway({
onRunInvoke: () => {
throw new Error("SYSTEM_RUN_DENIED: approval required");
}
if (method === "exec.approval.request") {
},
onApprovalRequest: () => {
return { decision: "allow-never" };
}
return unexpectedGatewayMethod(method);
},
});
await expect(executeNodes(BASE_RUN_INPUT)).rejects.toThrow(
"exec denied: invalid approval decision",