refactor: dedupe agent and browser cli helpers

This commit is contained in:
Peter Steinberger
2026-03-03 00:14:48 +00:00
parent fe14be2352
commit fd3ca8a34c
46 changed files with 1051 additions and 1117 deletions

View File

@@ -28,6 +28,27 @@ describe("cron tool", () => {
return params?.payload?.text ?? "";
}
function expectSingleGatewayCallMethod(method: string) {
expect(callGatewayMock).toHaveBeenCalledTimes(1);
const call = readGatewayCall(0);
expect(call.method).toBe(method);
return call.params;
}
function buildReminderAgentTurnJob(overrides: Record<string, unknown> = {}): {
name: string;
schedule: { at: string };
payload: { kind: "agentTurn"; message: string };
delivery?: { mode: string; to?: string };
} {
return {
name: "reminder",
schedule: { at: new Date(123).toISOString() },
payload: { kind: "agentTurn", message: "hello" },
...overrides,
};
}
async function executeAddAndReadDelivery(params: {
callId: string;
agentSessionKey: string;
@@ -37,9 +58,7 @@ describe("cron tool", () => {
await tool.execute(params.callId, {
action: "add",
job: {
name: "reminder",
schedule: { at: new Date(123).toISOString() },
payload: { kind: "agentTurn", message: "hello" },
...buildReminderAgentTurnJob(),
...(params.delivery !== undefined ? { delivery: params.delivery } : {}),
},
});
@@ -114,13 +133,8 @@ describe("cron tool", () => {
const tool = createCronTool();
await tool.execute("call1", args);
expect(callGatewayMock).toHaveBeenCalledTimes(1);
const call = callGatewayMock.mock.calls[0]?.[0] as {
method?: string;
params?: unknown;
};
expect(call.method).toBe(`cron.${action}`);
expect(call.params).toEqual(expectedParams);
const params = expectSingleGatewayCallMethod(`cron.${action}`);
expect(params).toEqual(expectedParams);
});
it("prefers jobId over id when both are provided", async () => {
@@ -131,10 +145,7 @@ describe("cron tool", () => {
id: "job-legacy",
});
const call = callGatewayMock.mock.calls[0]?.[0] as {
params?: unknown;
};
expect(call?.params).toEqual({ id: "job-primary", mode: "force" });
expect(readGatewayCall().params).toEqual({ id: "job-primary", mode: "force" });
});
it("supports due-only run mode", async () => {
@@ -145,10 +156,7 @@ describe("cron tool", () => {
runMode: "due",
});
const call = callGatewayMock.mock.calls[0]?.[0] as {
params?: unknown;
};
expect(call?.params).toEqual({ id: "job-due", mode: "due" });
expect(readGatewayCall().params).toEqual({ id: "job-due", mode: "due" });
});
it("normalizes cron.add job payloads", async () => {
@@ -164,13 +172,8 @@ describe("cron tool", () => {
},
});
expect(callGatewayMock).toHaveBeenCalledTimes(1);
const call = callGatewayMock.mock.calls[0]?.[0] as {
method?: string;
params?: unknown;
};
expect(call.method).toBe("cron.add");
expect(call.params).toEqual({
const params = expectSingleGatewayCallMethod("cron.add");
expect(params).toEqual({
name: "wake-up",
enabled: true,
deleteAfterRun: true,
@@ -367,15 +370,12 @@ describe("cron tool", () => {
payload: { kind: "agentTurn", message: "do stuff" },
});
expect(callGatewayMock).toHaveBeenCalledTimes(1);
const call = callGatewayMock.mock.calls[0]?.[0] as {
method?: string;
params?: { name?: string; sessionTarget?: string; payload?: { kind?: string } };
};
expect(call.method).toBe("cron.add");
expect(call.params?.name).toBe("flat-job");
expect(call.params?.sessionTarget).toBe("isolated");
expect(call.params?.payload?.kind).toBe("agentTurn");
const params = expectSingleGatewayCallMethod("cron.add") as
| { name?: string; sessionTarget?: string; payload?: { kind?: string } }
| undefined;
expect(params?.name).toBe("flat-job");
expect(params?.sessionTarget).toBe("isolated");
expect(params?.payload?.kind).toBe("agentTurn");
});
it("recovers flat params when job is empty object", async () => {
@@ -391,15 +391,12 @@ describe("cron tool", () => {
payload: { kind: "systemEvent", text: "wake up" },
});
expect(callGatewayMock).toHaveBeenCalledTimes(1);
const call = callGatewayMock.mock.calls[0]?.[0] as {
method?: string;
params?: { name?: string; sessionTarget?: string; payload?: { text?: string } };
};
expect(call.method).toBe("cron.add");
expect(call.params?.name).toBe("empty-job");
expect(call.params?.sessionTarget).toBe("main");
expect(call.params?.payload?.text).toBe("wake up");
const params = expectSingleGatewayCallMethod("cron.add") as
| { name?: string; sessionTarget?: string; payload?: { text?: string } }
| undefined;
expect(params?.name).toBe("empty-job");
expect(params?.sessionTarget).toBe("main");
expect(params?.payload?.text).toBe("wake up");
});
it("recovers flat message shorthand as agentTurn payload", async () => {
@@ -412,16 +409,13 @@ describe("cron tool", () => {
message: "do stuff",
});
expect(callGatewayMock).toHaveBeenCalledTimes(1);
const call = callGatewayMock.mock.calls[0]?.[0] as {
method?: string;
params?: { payload?: { kind?: string; message?: string }; sessionTarget?: string };
};
expect(call.method).toBe("cron.add");
const params = expectSingleGatewayCallMethod("cron.add") as
| { payload?: { kind?: string; message?: string }; sessionTarget?: string }
| undefined;
// normalizeCronJobCreate infers agentTurn from message and isolated from agentTurn
expect(call.params?.payload?.kind).toBe("agentTurn");
expect(call.params?.payload?.message).toBe("do stuff");
expect(call.params?.sessionTarget).toBe("isolated");
expect(params?.payload?.kind).toBe("agentTurn");
expect(params?.payload?.message).toBe("do stuff");
expect(params?.sessionTarget).toBe("isolated");
});
it("does not recover flat params when no meaningful job field is present", async () => {
@@ -486,9 +480,7 @@ describe("cron tool", () => {
tool.execute("call-webhook-missing", {
action: "add",
job: {
name: "reminder",
schedule: { at: new Date(123).toISOString() },
payload: { kind: "agentTurn", message: "hello" },
...buildReminderAgentTurnJob(),
delivery: { mode: "webhook" },
},
}),
@@ -503,9 +495,7 @@ describe("cron tool", () => {
tool.execute("call-webhook-invalid", {
action: "add",
job: {
name: "reminder",
schedule: { at: new Date(123).toISOString() },
payload: { kind: "agentTurn", message: "hello" },
...buildReminderAgentTurnJob(),
delivery: { mode: "webhook", to: "ftp://example.invalid/cron-finished" },
},
}),
@@ -524,15 +514,12 @@ describe("cron tool", () => {
enabled: false,
});
expect(callGatewayMock).toHaveBeenCalledTimes(1);
const call = callGatewayMock.mock.calls[0]?.[0] as {
method?: string;
params?: { id?: string; patch?: { name?: string; enabled?: boolean } };
};
expect(call.method).toBe("cron.update");
expect(call.params?.id).toBe("job-1");
expect(call.params?.patch?.name).toBe("new-name");
expect(call.params?.patch?.enabled).toBe(false);
const params = expectSingleGatewayCallMethod("cron.update") as
| { id?: string; patch?: { name?: string; enabled?: boolean } }
| undefined;
expect(params?.id).toBe("job-1");
expect(params?.patch?.name).toBe("new-name");
expect(params?.patch?.enabled).toBe(false);
});
it("recovers additional flat patch params for update action", async () => {
@@ -546,16 +533,17 @@ describe("cron tool", () => {
failureAlert: { after: 3, cooldownMs: 60_000 },
});
const call = callGatewayMock.mock.calls[0]?.[0] as {
method?: string;
params?: {
id?: string;
patch?: { sessionTarget?: string; failureAlert?: { after?: number; cooldownMs?: number } };
};
};
expect(call.method).toBe("cron.update");
expect(call.params?.id).toBe("job-2");
expect(call.params?.patch?.sessionTarget).toBe("main");
expect(call.params?.patch?.failureAlert).toEqual({ after: 3, cooldownMs: 60_000 });
const params = expectSingleGatewayCallMethod("cron.update") as
| {
id?: string;
patch?: {
sessionTarget?: string;
failureAlert?: { after?: number; cooldownMs?: number };
};
}
| undefined;
expect(params?.id).toBe("job-2");
expect(params?.patch?.sessionTarget).toBe("main");
expect(params?.patch?.failureAlert).toEqual({ after: 3, cooldownMs: 60_000 });
});
});

View File

@@ -60,32 +60,38 @@ export function coercePdfAssistantText(params: {
provider: string;
model: string;
}): string {
const stop = params.message.stopReason;
const label = `${params.provider}/${params.model}`;
const errorMessage = params.message.errorMessage?.trim();
if (stop === "error" || stop === "aborted") {
const fail = (message?: string) => {
throw new Error(
errorMessage
? `PDF model failed (${params.provider}/${params.model}): ${errorMessage}`
: `PDF model failed (${params.provider}/${params.model})`,
message ? `PDF model failed (${label}): ${message}` : `PDF model failed (${label})`,
);
};
if (params.message.stopReason === "error" || params.message.stopReason === "aborted") {
fail(errorMessage);
}
if (errorMessage) {
throw new Error(`PDF model failed (${params.provider}/${params.model}): ${errorMessage}`);
fail(errorMessage);
}
const text = extractAssistantText(params.message);
if (text.trim()) {
return text.trim();
const trimmed = text.trim();
if (trimmed) {
return trimmed;
}
throw new Error(`PDF model returned no text (${params.provider}/${params.model}).`);
throw new Error(`PDF model returned no text (${label}).`);
}
export function coercePdfModelConfig(cfg?: OpenClawConfig): PdfModelConfig {
const primary = resolveAgentModelPrimaryValue(cfg?.agents?.defaults?.pdfModel);
const fallbacks = resolveAgentModelFallbackValues(cfg?.agents?.defaults?.pdfModel);
return {
...(primary?.trim() ? { primary: primary.trim() } : {}),
...(fallbacks.length > 0 ? { fallbacks } : {}),
};
const modelConfig: PdfModelConfig = {};
if (primary?.trim()) {
modelConfig.primary = primary.trim();
}
if (fallbacks.length > 0) {
modelConfig.fallbacks = fallbacks;
}
return modelConfig;
}
export function resolvePdfToolMaxTokens(

View File

@@ -89,9 +89,14 @@ export async function handleTelegramAction(
mediaLocalRoots?: readonly string[];
},
): Promise<AgentToolResult<unknown>> {
const action = readStringParam(params, "action", { required: true });
const accountId = readStringParam(params, "accountId");
const isActionEnabled = createTelegramActionGate({ cfg, accountId });
const { action, accountId } = {
action: readStringParam(params, "action", { required: true }),
accountId: readStringParam(params, "accountId"),
};
const isActionEnabled = createTelegramActionGate({
cfg,
accountId,
});
if (action === "react") {
// All react failures return soft results (jsonResult with ok:false) instead