refactor(runtime): split runtime builders and stabilize cron tool seam

This commit is contained in:
Peter Steinberger
2026-02-19 16:09:50 +01:00
parent e1e91bdb4a
commit cc9be84b9c
4 changed files with 274 additions and 207 deletions

View File

@@ -1,11 +1,7 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
const { callGatewayMock } = vi.hoisted(() => ({
callGatewayMock: vi.fn(),
}));
vi.mock("../../gateway/call.js", () => ({
callGateway: (opts: unknown) => callGatewayMock(opts),
const { callGatewayToolMock } = vi.hoisted(() => ({
callGatewayToolMock: vi.fn(),
}));
vi.mock("../agent-scope.js", () => ({
@@ -16,12 +12,15 @@ import { createCronTool } from "./cron-tool.js";
describe("cron tool flat-params", () => {
beforeEach(() => {
callGatewayMock.mockReset();
callGatewayMock.mockResolvedValue({ ok: true });
callGatewayToolMock.mockReset();
callGatewayToolMock.mockResolvedValue({ ok: true });
});
it("preserves explicit top-level sessionKey during flat-params recovery", async () => {
const tool = createCronTool({ agentSessionKey: "agent:main:discord:channel:ops" });
const tool = createCronTool(
{ agentSessionKey: "agent:main:discord:channel:ops" },
{ callGatewayTool: callGatewayToolMock },
);
await tool.execute("call-flat-session-key", {
action: "add",
sessionKey: "agent:main:telegram:group:-100123:topic:99",
@@ -29,11 +28,12 @@ describe("cron tool flat-params", () => {
message: "do stuff",
});
const call = callGatewayMock.mock.calls[0]?.[0] as {
method?: string;
params?: { sessionKey?: string };
};
expect(call.method).toBe("cron.add");
expect(call.params?.sessionKey).toBe("agent:main:telegram:group:-100123:topic:99");
const [method, _gatewayOpts, params] = callGatewayToolMock.mock.calls[0] as [
string,
unknown,
{ sessionKey?: string },
];
expect(method).toBe("cron.add");
expect(params.sessionKey).toBe("agent:main:telegram:group:-100123:topic:99");
});
});

View File

@@ -1,7 +1,7 @@
import { Type } from "@sinclair/typebox";
import type { CronDelivery, CronMessageChannel } from "../../cron/types.js";
import { loadConfig } from "../../config/config.js";
import { normalizeCronJobCreate, normalizeCronJobPatch } from "../../cron/normalize.js";
import type { CronDelivery, CronMessageChannel } from "../../cron/types.js";
import { normalizeHttpWebhookUrl } from "../../cron/webhook-url.js";
import { parseAgentSessionKey } from "../../sessions/session-key-utils.js";
import { extractTextFromChatContent } from "../../shared/chat-content.js";
@@ -50,6 +50,12 @@ type CronToolOptions = {
agentSessionKey?: string;
};
type GatewayToolCaller = typeof callGatewayTool;
type CronToolDeps = {
callGatewayTool?: GatewayToolCaller;
};
type ChatMessage = {
role?: unknown;
content?: unknown;
@@ -84,6 +90,7 @@ async function buildReminderContextLines(params: {
agentSessionKey?: string;
gatewayOpts: GatewayCallOptions;
contextMessages: number;
callGatewayTool: GatewayToolCaller;
}) {
const maxMessages = Math.min(
REMINDER_CONTEXT_MESSAGES_MAX,
@@ -100,7 +107,7 @@ async function buildReminderContextLines(params: {
const { mainKey, alias } = resolveMainSessionAlias(cfg);
const resolvedKey = resolveInternalSessionKey({ key: sessionKey, alias, mainKey });
try {
const res = await callGatewayTool<{ messages: Array<unknown> }>(
const res = await params.callGatewayTool<{ messages: Array<unknown> }>(
"chat.history",
params.gatewayOpts,
{
@@ -197,7 +204,8 @@ function inferDeliveryFromSessionKey(agentSessionKey?: string): CronDelivery | n
return delivery;
}
export function createCronTool(opts?: CronToolOptions): AnyAgentTool {
export function createCronTool(opts?: CronToolOptions, deps?: CronToolDeps): AnyAgentTool {
const callGateway = deps?.callGatewayTool ?? callGatewayTool;
return {
label: "Cron",
name: "cron",
@@ -272,10 +280,10 @@ Use jobId as the canonical identifier; id is accepted for compatibility. Use con
switch (action) {
case "status":
return jsonResult(await callGatewayTool("cron.status", gatewayOpts, {}));
return jsonResult(await callGateway("cron.status", gatewayOpts, {}));
case "list":
return jsonResult(
await callGatewayTool("cron.list", gatewayOpts, {
await callGateway("cron.list", gatewayOpts, {
includeDisabled: Boolean(params.includeDisabled),
}),
);
@@ -412,6 +420,7 @@ Use jobId as the canonical identifier; id is accepted for compatibility. Use con
agentSessionKey: opts?.agentSessionKey,
gatewayOpts,
contextMessages,
callGatewayTool: callGateway,
});
if (contextLines.length > 0) {
const baseText = stripExistingContext(payload.text);
@@ -419,7 +428,7 @@ Use jobId as the canonical identifier; id is accepted for compatibility. Use con
}
}
}
return jsonResult(await callGatewayTool("cron.add", gatewayOpts, job));
return jsonResult(await callGateway("cron.add", gatewayOpts, job));
}
case "update": {
const id = readStringParam(params, "jobId") ?? readStringParam(params, "id");
@@ -431,7 +440,7 @@ Use jobId as the canonical identifier; id is accepted for compatibility. Use con
}
const patch = normalizeCronJobPatch(params.patch) ?? params.patch;
return jsonResult(
await callGatewayTool("cron.update", gatewayOpts, {
await callGateway("cron.update", gatewayOpts, {
id,
patch,
}),
@@ -442,7 +451,7 @@ Use jobId as the canonical identifier; id is accepted for compatibility. Use con
if (!id) {
throw new Error("jobId required (id accepted for backward compatibility)");
}
return jsonResult(await callGatewayTool("cron.remove", gatewayOpts, { id }));
return jsonResult(await callGateway("cron.remove", gatewayOpts, { id }));
}
case "run": {
const id = readStringParam(params, "jobId") ?? readStringParam(params, "id");
@@ -451,14 +460,14 @@ Use jobId as the canonical identifier; id is accepted for compatibility. Use con
}
const runMode =
params.runMode === "due" || params.runMode === "force" ? params.runMode : "force";
return jsonResult(await callGatewayTool("cron.run", gatewayOpts, { id, mode: runMode }));
return jsonResult(await callGateway("cron.run", gatewayOpts, { id, mode: runMode }));
}
case "runs": {
const id = readStringParam(params, "jobId") ?? readStringParam(params, "id");
if (!id) {
throw new Error("jobId required (id accepted for backward compatibility)");
}
return jsonResult(await callGatewayTool("cron.runs", gatewayOpts, { id }));
return jsonResult(await callGateway("cron.runs", gatewayOpts, { id }));
}
case "wake": {
const text = readStringParam(params, "text", { required: true });
@@ -467,7 +476,7 @@ Use jobId as the canonical identifier; id is accepted for compatibility. Use con
? params.mode
: "next-heartbeat";
return jsonResult(
await callGatewayTool("wake", gatewayOpts, { mode, text }, { expectFinal: false }),
await callGateway("wake", gatewayOpts, { mode, text }, { expectFinal: false }),
);
}
default: