2026-02-19 15:41:24 +01:00
|
|
|
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),
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
vi.mock("../agent-scope.js", () => ({
|
|
|
|
|
resolveSessionAgentId: () => "agent-123",
|
|
|
|
|
}));
|
|
|
|
|
|
2026-01-04 14:57:26 +00:00
|
|
|
import { createCronTool } from "./cron-tool.js";
|
|
|
|
|
|
|
|
|
|
describe("cron tool", () => {
|
2026-02-22 17:11:17 +00:00
|
|
|
function readGatewayCall(index = 0): { method?: string; params?: Record<string, unknown> } {
|
|
|
|
|
return (
|
|
|
|
|
(callGatewayMock.mock.calls[index]?.[0] as
|
|
|
|
|
| { method?: string; params?: Record<string, unknown> }
|
|
|
|
|
| undefined) ?? { method: undefined, params: undefined }
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function readCronPayloadText(index = 0): string {
|
|
|
|
|
const params = readGatewayCall(index).params as { payload?: { text?: string } } | undefined;
|
|
|
|
|
return params?.payload?.text ?? "";
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-03 00:14:48 +00:00
|
|
|
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,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-16 14:52:09 +00:00
|
|
|
async function executeAddAndReadDelivery(params: {
|
|
|
|
|
callId: string;
|
|
|
|
|
agentSessionKey: string;
|
|
|
|
|
delivery?: { mode?: string; channel?: string; to?: string } | null;
|
|
|
|
|
}) {
|
|
|
|
|
const tool = createCronTool({ agentSessionKey: params.agentSessionKey });
|
|
|
|
|
await tool.execute(params.callId, {
|
|
|
|
|
action: "add",
|
|
|
|
|
job: {
|
2026-03-03 00:14:48 +00:00
|
|
|
...buildReminderAgentTurnJob(),
|
2026-02-16 14:52:09 +00:00
|
|
|
...(params.delivery !== undefined ? { delivery: params.delivery } : {}),
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const call = callGatewayMock.mock.calls[0]?.[0] as {
|
|
|
|
|
params?: { delivery?: { mode?: string; channel?: string; to?: string } };
|
|
|
|
|
};
|
|
|
|
|
return call?.params?.delivery;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-22 17:11:17 +00:00
|
|
|
async function executeAddAndReadSessionKey(params: {
|
|
|
|
|
callId: string;
|
|
|
|
|
agentSessionKey: string;
|
|
|
|
|
jobSessionKey?: string;
|
|
|
|
|
}): Promise<string | undefined> {
|
|
|
|
|
const tool = createCronTool({ agentSessionKey: params.agentSessionKey });
|
|
|
|
|
await tool.execute(params.callId, {
|
|
|
|
|
action: "add",
|
|
|
|
|
job: {
|
|
|
|
|
name: "wake-up",
|
|
|
|
|
schedule: { at: new Date(123).toISOString() },
|
|
|
|
|
...(params.jobSessionKey ? { sessionKey: params.jobSessionKey } : {}),
|
|
|
|
|
payload: { kind: "systemEvent", text: "hello" },
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
const call = readGatewayCall();
|
|
|
|
|
const payload = call.params as { sessionKey?: string } | undefined;
|
|
|
|
|
return payload?.sessionKey;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function executeAddWithContextMessages(callId: string, contextMessages: number) {
|
|
|
|
|
const tool = createCronTool({ agentSessionKey: "main" });
|
|
|
|
|
await tool.execute(callId, {
|
|
|
|
|
action: "add",
|
|
|
|
|
contextMessages,
|
|
|
|
|
job: {
|
|
|
|
|
name: "reminder",
|
|
|
|
|
schedule: { at: new Date(123).toISOString() },
|
|
|
|
|
payload: { kind: "systemEvent", text: "Reminder: the thing." },
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-04 14:57:26 +00:00
|
|
|
beforeEach(() => {
|
2026-02-22 08:35:32 +00:00
|
|
|
callGatewayMock.mockClear();
|
2026-02-18 13:32:38 +00:00
|
|
|
callGatewayMock.mockResolvedValue({ ok: true });
|
2026-01-04 14:57:26 +00:00
|
|
|
});
|
|
|
|
|
|
2026-02-19 15:27:45 +01:00
|
|
|
it("marks cron as owner-only", async () => {
|
|
|
|
|
const tool = createCronTool();
|
|
|
|
|
expect(tool.ownerOnly).toBe(true);
|
2026-02-19 14:37:56 +01:00
|
|
|
});
|
|
|
|
|
|
2026-01-04 14:57:26 +00:00
|
|
|
it.each([
|
|
|
|
|
[
|
|
|
|
|
"update",
|
2026-01-08 20:28:03 +01:00
|
|
|
{ action: "update", jobId: "job-1", patch: { foo: "bar" } },
|
2026-01-04 14:57:26 +00:00
|
|
|
{ id: "job-1", patch: { foo: "bar" } },
|
|
|
|
|
],
|
2026-01-08 20:46:58 +01:00
|
|
|
[
|
|
|
|
|
"update",
|
|
|
|
|
{ action: "update", id: "job-2", patch: { foo: "bar" } },
|
|
|
|
|
{ id: "job-2", patch: { foo: "bar" } },
|
|
|
|
|
],
|
2026-01-08 20:28:03 +01:00
|
|
|
["remove", { action: "remove", jobId: "job-1" }, { id: "job-1" }],
|
2026-01-08 20:46:58 +01:00
|
|
|
["remove", { action: "remove", id: "job-2" }, { id: "job-2" }],
|
fix: cron scheduler reliability, store hardening, and UX improvements (#10776)
* refactor: update cron job wake mode and run mode handling
- Changed default wake mode from 'next-heartbeat' to 'now' in CronJobEditor and related CLI commands.
- Updated cron-tool tests to reflect changes in run mode, introducing 'due' and 'force' options.
- Enhanced cron-tool logic to handle new run modes and ensure compatibility with existing job structures.
- Added new tests for delivery plan consistency and job execution behavior under various conditions.
- Improved normalization functions to handle wake mode and session target casing.
This refactor aims to streamline cron job configurations and enhance the overall user experience with clearer defaults and improved functionality.
* test: enhance cron job functionality and UI
- Added tests to ensure the isolated agent correctly announces the final payload text when delivering messages via Telegram.
- Implemented a new function to pick the last deliverable payload from a list of delivery payloads.
- Enhanced the cron service to maintain legacy "every" jobs while minute cron jobs recompute schedules.
- Updated the cron store migration tests to verify the addition of anchorMs to legacy every schedules.
- Improved the UI for displaying cron job details, including job state and delivery information, with new styles and layout adjustments.
These changes aim to improve the reliability and user experience of the cron job system.
* test: enhance sessions thinking level handling
- Added tests to verify that the correct thinking levels are applied during session spawning.
- Updated the sessions-spawn-tool to include a new parameter for overriding thinking levels.
- Enhanced the UI to support additional thinking levels, including "xhigh" and "full", and improved the handling of current options in dropdowns.
These changes aim to improve the flexibility and accuracy of thinking level configurations in session management.
* feat: enhance session management and cron job functionality
- Introduced passthrough arguments in the test-parallel script to allow for flexible command-line options.
- Updated session handling to hide cron run alias session keys from the sessions list, improving clarity.
- Enhanced the cron service to accurately record job start times and durations, ensuring better tracking of job execution.
- Added tests to verify the correct behavior of the cron service under various conditions, including zero-delay timers.
These changes aim to improve the usability and reliability of session and cron job management.
* feat: implement job running state checks in cron service
- Added functionality to prevent manual job runs if a job is already in progress, enhancing job management.
- Updated the `isJobDue` function to include checks for running jobs, ensuring accurate scheduling.
- Enhanced the `run` function to return a specific reason when a job is already running.
- Introduced a new test case to verify the behavior of forced manual runs during active job execution.
These changes aim to improve the reliability and clarity of cron job execution and management.
* feat: add session ID and key to CronRunLogEntry model
- Introduced `sessionid` and `sessionkey` properties to the `CronRunLogEntry` struct for enhanced tracking of session-related information.
- Updated the initializer and Codable conformance to accommodate the new properties, ensuring proper serialization and deserialization.
These changes aim to improve the granularity of logging and session management within the cron job system.
* fix: improve session display name resolution
- Updated the `resolveSessionDisplayName` function to ensure that both label and displayName are trimmed and default to an empty string if not present.
- Enhanced the logic to prevent returning the key if it matches the label or displayName, improving clarity in session naming.
These changes aim to enhance the accuracy and usability of session display names in the UI.
* perf: skip cron store persist when idle timer tick produces no changes
recomputeNextRuns now returns a boolean indicating whether any job
state was mutated. The idle path in onTimer only persists when the
return value is true, eliminating unnecessary file writes every 60s
for far-future or idle schedules.
* fix: prep for merge - explicit delivery mode migration, docs + changelog (#10776) (thanks @tyler6204)
2026-02-06 18:03:03 -08:00
|
|
|
["run", { action: "run", jobId: "job-1" }, { id: "job-1", mode: "force" }],
|
|
|
|
|
["run", { action: "run", id: "job-2" }, { id: "job-2", mode: "force" }],
|
2026-01-08 20:28:03 +01:00
|
|
|
["runs", { action: "runs", jobId: "job-1" }, { id: "job-1" }],
|
2026-01-08 20:46:58 +01:00
|
|
|
["runs", { action: "runs", id: "job-2" }, { id: "job-2" }],
|
2026-01-04 14:57:26 +00:00
|
|
|
])("%s sends id to gateway", async (action, args, expectedParams) => {
|
|
|
|
|
const tool = createCronTool();
|
|
|
|
|
await tool.execute("call1", args);
|
|
|
|
|
|
2026-03-03 00:14:48 +00:00
|
|
|
const params = expectSingleGatewayCallMethod(`cron.${action}`);
|
|
|
|
|
expect(params).toEqual(expectedParams);
|
2026-01-04 14:57:26 +00:00
|
|
|
});
|
2026-01-05 02:15:11 +01:00
|
|
|
|
2026-01-08 20:46:58 +01:00
|
|
|
it("prefers jobId over id when both are provided", async () => {
|
|
|
|
|
const tool = createCronTool();
|
|
|
|
|
await tool.execute("call1", {
|
|
|
|
|
action: "run",
|
|
|
|
|
jobId: "job-primary",
|
|
|
|
|
id: "job-legacy",
|
|
|
|
|
});
|
|
|
|
|
|
2026-03-03 00:14:48 +00:00
|
|
|
expect(readGatewayCall().params).toEqual({ id: "job-primary", mode: "force" });
|
fix: cron scheduler reliability, store hardening, and UX improvements (#10776)
* refactor: update cron job wake mode and run mode handling
- Changed default wake mode from 'next-heartbeat' to 'now' in CronJobEditor and related CLI commands.
- Updated cron-tool tests to reflect changes in run mode, introducing 'due' and 'force' options.
- Enhanced cron-tool logic to handle new run modes and ensure compatibility with existing job structures.
- Added new tests for delivery plan consistency and job execution behavior under various conditions.
- Improved normalization functions to handle wake mode and session target casing.
This refactor aims to streamline cron job configurations and enhance the overall user experience with clearer defaults and improved functionality.
* test: enhance cron job functionality and UI
- Added tests to ensure the isolated agent correctly announces the final payload text when delivering messages via Telegram.
- Implemented a new function to pick the last deliverable payload from a list of delivery payloads.
- Enhanced the cron service to maintain legacy "every" jobs while minute cron jobs recompute schedules.
- Updated the cron store migration tests to verify the addition of anchorMs to legacy every schedules.
- Improved the UI for displaying cron job details, including job state and delivery information, with new styles and layout adjustments.
These changes aim to improve the reliability and user experience of the cron job system.
* test: enhance sessions thinking level handling
- Added tests to verify that the correct thinking levels are applied during session spawning.
- Updated the sessions-spawn-tool to include a new parameter for overriding thinking levels.
- Enhanced the UI to support additional thinking levels, including "xhigh" and "full", and improved the handling of current options in dropdowns.
These changes aim to improve the flexibility and accuracy of thinking level configurations in session management.
* feat: enhance session management and cron job functionality
- Introduced passthrough arguments in the test-parallel script to allow for flexible command-line options.
- Updated session handling to hide cron run alias session keys from the sessions list, improving clarity.
- Enhanced the cron service to accurately record job start times and durations, ensuring better tracking of job execution.
- Added tests to verify the correct behavior of the cron service under various conditions, including zero-delay timers.
These changes aim to improve the usability and reliability of session and cron job management.
* feat: implement job running state checks in cron service
- Added functionality to prevent manual job runs if a job is already in progress, enhancing job management.
- Updated the `isJobDue` function to include checks for running jobs, ensuring accurate scheduling.
- Enhanced the `run` function to return a specific reason when a job is already running.
- Introduced a new test case to verify the behavior of forced manual runs during active job execution.
These changes aim to improve the reliability and clarity of cron job execution and management.
* feat: add session ID and key to CronRunLogEntry model
- Introduced `sessionid` and `sessionkey` properties to the `CronRunLogEntry` struct for enhanced tracking of session-related information.
- Updated the initializer and Codable conformance to accommodate the new properties, ensuring proper serialization and deserialization.
These changes aim to improve the granularity of logging and session management within the cron job system.
* fix: improve session display name resolution
- Updated the `resolveSessionDisplayName` function to ensure that both label and displayName are trimmed and default to an empty string if not present.
- Enhanced the logic to prevent returning the key if it matches the label or displayName, improving clarity in session naming.
These changes aim to enhance the accuracy and usability of session display names in the UI.
* perf: skip cron store persist when idle timer tick produces no changes
recomputeNextRuns now returns a boolean indicating whether any job
state was mutated. The idle path in onTimer only persists when the
return value is true, eliminating unnecessary file writes every 60s
for far-future or idle schedules.
* fix: prep for merge - explicit delivery mode migration, docs + changelog (#10776) (thanks @tyler6204)
2026-02-06 18:03:03 -08:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("supports due-only run mode", async () => {
|
|
|
|
|
const tool = createCronTool();
|
|
|
|
|
await tool.execute("call-due", {
|
|
|
|
|
action: "run",
|
|
|
|
|
jobId: "job-due",
|
|
|
|
|
runMode: "due",
|
|
|
|
|
});
|
|
|
|
|
|
2026-03-03 00:14:48 +00:00
|
|
|
expect(readGatewayCall().params).toEqual({ id: "job-due", mode: "due" });
|
2026-01-08 20:46:58 +01:00
|
|
|
});
|
|
|
|
|
|
2026-01-05 23:09:48 -03:00
|
|
|
it("normalizes cron.add job payloads", async () => {
|
2026-01-05 02:15:11 +01:00
|
|
|
const tool = createCronTool();
|
2026-01-05 23:09:48 -03:00
|
|
|
await tool.execute("call2", {
|
|
|
|
|
action: "add",
|
|
|
|
|
job: {
|
|
|
|
|
data: {
|
|
|
|
|
name: "wake-up",
|
|
|
|
|
schedule: { atMs: 123 },
|
2026-01-21 01:12:43 +00:00
|
|
|
payload: { kind: "systemEvent", text: "hello" },
|
2026-01-05 23:09:48 -03:00
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
2026-03-03 00:14:48 +00:00
|
|
|
const params = expectSingleGatewayCallMethod("cron.add");
|
|
|
|
|
expect(params).toEqual({
|
2026-01-05 23:09:48 -03:00
|
|
|
name: "wake-up",
|
2026-02-03 17:51:41 -08:00
|
|
|
enabled: true,
|
|
|
|
|
deleteAfterRun: true,
|
2026-02-03 16:53:46 -08:00
|
|
|
schedule: { kind: "at", at: new Date(123).toISOString() },
|
2026-01-05 23:09:48 -03:00
|
|
|
sessionTarget: "main",
|
fix: cron scheduler reliability, store hardening, and UX improvements (#10776)
* refactor: update cron job wake mode and run mode handling
- Changed default wake mode from 'next-heartbeat' to 'now' in CronJobEditor and related CLI commands.
- Updated cron-tool tests to reflect changes in run mode, introducing 'due' and 'force' options.
- Enhanced cron-tool logic to handle new run modes and ensure compatibility with existing job structures.
- Added new tests for delivery plan consistency and job execution behavior under various conditions.
- Improved normalization functions to handle wake mode and session target casing.
This refactor aims to streamline cron job configurations and enhance the overall user experience with clearer defaults and improved functionality.
* test: enhance cron job functionality and UI
- Added tests to ensure the isolated agent correctly announces the final payload text when delivering messages via Telegram.
- Implemented a new function to pick the last deliverable payload from a list of delivery payloads.
- Enhanced the cron service to maintain legacy "every" jobs while minute cron jobs recompute schedules.
- Updated the cron store migration tests to verify the addition of anchorMs to legacy every schedules.
- Improved the UI for displaying cron job details, including job state and delivery information, with new styles and layout adjustments.
These changes aim to improve the reliability and user experience of the cron job system.
* test: enhance sessions thinking level handling
- Added tests to verify that the correct thinking levels are applied during session spawning.
- Updated the sessions-spawn-tool to include a new parameter for overriding thinking levels.
- Enhanced the UI to support additional thinking levels, including "xhigh" and "full", and improved the handling of current options in dropdowns.
These changes aim to improve the flexibility and accuracy of thinking level configurations in session management.
* feat: enhance session management and cron job functionality
- Introduced passthrough arguments in the test-parallel script to allow for flexible command-line options.
- Updated session handling to hide cron run alias session keys from the sessions list, improving clarity.
- Enhanced the cron service to accurately record job start times and durations, ensuring better tracking of job execution.
- Added tests to verify the correct behavior of the cron service under various conditions, including zero-delay timers.
These changes aim to improve the usability and reliability of session and cron job management.
* feat: implement job running state checks in cron service
- Added functionality to prevent manual job runs if a job is already in progress, enhancing job management.
- Updated the `isJobDue` function to include checks for running jobs, ensuring accurate scheduling.
- Enhanced the `run` function to return a specific reason when a job is already running.
- Introduced a new test case to verify the behavior of forced manual runs during active job execution.
These changes aim to improve the reliability and clarity of cron job execution and management.
* feat: add session ID and key to CronRunLogEntry model
- Introduced `sessionid` and `sessionkey` properties to the `CronRunLogEntry` struct for enhanced tracking of session-related information.
- Updated the initializer and Codable conformance to accommodate the new properties, ensuring proper serialization and deserialization.
These changes aim to improve the granularity of logging and session management within the cron job system.
* fix: improve session display name resolution
- Updated the `resolveSessionDisplayName` function to ensure that both label and displayName are trimmed and default to an empty string if not present.
- Enhanced the logic to prevent returning the key if it matches the label or displayName, improving clarity in session naming.
These changes aim to enhance the accuracy and usability of session display names in the UI.
* perf: skip cron store persist when idle timer tick produces no changes
recomputeNextRuns now returns a boolean indicating whether any job
state was mutated. The idle path in onTimer only persists when the
return value is true, eliminating unnecessary file writes every 60s
for far-future or idle schedules.
* fix: prep for merge - explicit delivery mode migration, docs + changelog (#10776) (thanks @tyler6204)
2026-02-06 18:03:03 -08:00
|
|
|
wakeMode: "now",
|
2026-01-05 23:09:48 -03:00
|
|
|
payload: { kind: "systemEvent", text: "hello" },
|
|
|
|
|
});
|
2026-01-05 02:15:11 +01:00
|
|
|
});
|
2026-01-16 13:58:29 +09:00
|
|
|
|
2026-01-23 00:50:50 -08:00
|
|
|
it("does not default agentId when job.agentId is null", async () => {
|
|
|
|
|
const tool = createCronTool({ agentSessionKey: "main" });
|
|
|
|
|
await tool.execute("call-null", {
|
|
|
|
|
action: "add",
|
|
|
|
|
job: {
|
|
|
|
|
name: "wake-up",
|
2026-02-03 16:53:46 -08:00
|
|
|
schedule: { at: new Date(123).toISOString() },
|
2026-01-23 00:50:50 -08:00
|
|
|
agentId: null,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const call = callGatewayMock.mock.calls[0]?.[0] as {
|
|
|
|
|
params?: { agentId?: unknown };
|
|
|
|
|
};
|
|
|
|
|
expect(call?.params?.agentId).toBeNull();
|
|
|
|
|
});
|
|
|
|
|
|
2026-02-16 14:29:21 -08:00
|
|
|
it("stamps cron.add with caller sessionKey when missing", async () => {
|
|
|
|
|
callGatewayMock.mockResolvedValueOnce({ ok: true });
|
|
|
|
|
|
|
|
|
|
const callerSessionKey = "agent:main:discord:channel:ops";
|
2026-02-22 17:11:17 +00:00
|
|
|
const sessionKey = await executeAddAndReadSessionKey({
|
|
|
|
|
callId: "call-session-key",
|
|
|
|
|
agentSessionKey: callerSessionKey,
|
2026-02-16 14:29:21 -08:00
|
|
|
});
|
2026-02-22 17:11:17 +00:00
|
|
|
expect(sessionKey).toBe(callerSessionKey);
|
2026-02-16 14:29:21 -08:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("preserves explicit job.sessionKey on add", async () => {
|
|
|
|
|
callGatewayMock.mockResolvedValueOnce({ ok: true });
|
|
|
|
|
|
2026-02-22 17:11:17 +00:00
|
|
|
const sessionKey = await executeAddAndReadSessionKey({
|
|
|
|
|
callId: "call-explicit-session-key",
|
|
|
|
|
agentSessionKey: "agent:main:discord:channel:ops",
|
|
|
|
|
jobSessionKey: "agent:main:telegram:group:-100123:topic:99",
|
2026-02-16 14:29:21 -08:00
|
|
|
});
|
2026-02-22 17:11:17 +00:00
|
|
|
expect(sessionKey).toBe("agent:main:telegram:group:-100123:topic:99");
|
2026-02-16 14:29:21 -08:00
|
|
|
});
|
|
|
|
|
|
2026-01-17 09:06:54 -05:00
|
|
|
it("adds recent context for systemEvent reminders when contextMessages > 0", async () => {
|
2026-01-16 13:58:29 +09:00
|
|
|
callGatewayMock
|
|
|
|
|
.mockResolvedValueOnce({
|
|
|
|
|
messages: [
|
|
|
|
|
{ role: "user", content: [{ type: "text", text: "Discussed Q2 budget" }] },
|
|
|
|
|
{
|
|
|
|
|
role: "assistant",
|
|
|
|
|
content: [{ type: "text", text: "We agreed to review on Tuesday." }],
|
|
|
|
|
},
|
|
|
|
|
{ role: "user", content: [{ type: "text", text: "Remind me about the thing at 2pm" }] },
|
|
|
|
|
],
|
|
|
|
|
})
|
|
|
|
|
.mockResolvedValueOnce({ ok: true });
|
|
|
|
|
|
2026-02-22 17:11:17 +00:00
|
|
|
await executeAddWithContextMessages("call3", 3);
|
2026-01-16 13:58:29 +09:00
|
|
|
|
|
|
|
|
expect(callGatewayMock).toHaveBeenCalledTimes(2);
|
2026-02-22 17:11:17 +00:00
|
|
|
const historyCall = readGatewayCall(0);
|
2026-01-16 13:58:29 +09:00
|
|
|
expect(historyCall.method).toBe("chat.history");
|
|
|
|
|
|
2026-02-22 17:11:17 +00:00
|
|
|
const cronCall = readGatewayCall(1);
|
2026-01-16 13:58:29 +09:00
|
|
|
expect(cronCall.method).toBe("cron.add");
|
2026-02-22 17:11:17 +00:00
|
|
|
const text = readCronPayloadText(1);
|
2026-01-16 13:58:29 +09:00
|
|
|
expect(text).toContain("Recent context:");
|
|
|
|
|
expect(text).toContain("User: Discussed Q2 budget");
|
|
|
|
|
expect(text).toContain("Assistant: We agreed to review on Tuesday.");
|
|
|
|
|
expect(text).toContain("User: Remind me about the thing at 2pm");
|
|
|
|
|
});
|
2026-01-17 09:06:54 -05:00
|
|
|
|
2026-01-22 03:52:03 +00:00
|
|
|
it("caps contextMessages at 10", async () => {
|
|
|
|
|
const messages = Array.from({ length: 12 }, (_, idx) => ({
|
|
|
|
|
role: "user",
|
|
|
|
|
content: [{ type: "text", text: `Message ${idx + 1}` }],
|
|
|
|
|
}));
|
|
|
|
|
callGatewayMock.mockResolvedValueOnce({ messages }).mockResolvedValueOnce({ ok: true });
|
|
|
|
|
|
2026-02-22 17:11:17 +00:00
|
|
|
await executeAddWithContextMessages("call5", 20);
|
2026-01-22 03:52:03 +00:00
|
|
|
|
|
|
|
|
expect(callGatewayMock).toHaveBeenCalledTimes(2);
|
2026-02-22 17:11:17 +00:00
|
|
|
const historyCall = readGatewayCall(0);
|
2026-01-22 03:52:03 +00:00
|
|
|
expect(historyCall.method).toBe("chat.history");
|
2026-02-22 17:11:17 +00:00
|
|
|
const historyParams = historyCall.params as { limit?: number } | undefined;
|
|
|
|
|
expect(historyParams?.limit).toBe(10);
|
2026-01-22 03:52:03 +00:00
|
|
|
|
2026-02-22 17:11:17 +00:00
|
|
|
const text = readCronPayloadText(1);
|
2026-01-22 03:52:03 +00:00
|
|
|
expect(text).not.toMatch(/Message 1\\b/);
|
|
|
|
|
expect(text).not.toMatch(/Message 2\\b/);
|
|
|
|
|
expect(text).toContain("Message 3");
|
|
|
|
|
expect(text).toContain("Message 12");
|
|
|
|
|
});
|
|
|
|
|
|
2026-01-17 09:06:54 -05:00
|
|
|
it("does not add context when contextMessages is 0 (default)", async () => {
|
|
|
|
|
callGatewayMock.mockResolvedValueOnce({ ok: true });
|
|
|
|
|
|
|
|
|
|
const tool = createCronTool({ agentSessionKey: "main" });
|
|
|
|
|
await tool.execute("call4", {
|
|
|
|
|
action: "add",
|
|
|
|
|
job: {
|
|
|
|
|
name: "reminder",
|
2026-02-03 16:53:46 -08:00
|
|
|
schedule: { at: new Date(123).toISOString() },
|
2026-01-17 09:06:54 -05:00
|
|
|
payload: { text: "Reminder: the thing." },
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Should only call cron.add, not chat.history
|
|
|
|
|
expect(callGatewayMock).toHaveBeenCalledTimes(1);
|
2026-02-22 17:11:17 +00:00
|
|
|
const cronCall = readGatewayCall(0);
|
2026-01-17 09:06:54 -05:00
|
|
|
expect(cronCall.method).toBe("cron.add");
|
2026-02-22 17:11:17 +00:00
|
|
|
const text = readCronPayloadText(0);
|
2026-01-17 09:06:54 -05:00
|
|
|
expect(text).not.toContain("Recent context:");
|
|
|
|
|
});
|
2026-01-23 08:42:48 +00:00
|
|
|
|
|
|
|
|
it("preserves explicit agentId null on add", async () => {
|
|
|
|
|
callGatewayMock.mockResolvedValueOnce({ ok: true });
|
|
|
|
|
|
|
|
|
|
const tool = createCronTool({ agentSessionKey: "main" });
|
|
|
|
|
await tool.execute("call6", {
|
|
|
|
|
action: "add",
|
|
|
|
|
job: {
|
|
|
|
|
name: "reminder",
|
2026-02-03 16:53:46 -08:00
|
|
|
schedule: { at: new Date(123).toISOString() },
|
2026-01-23 08:42:48 +00:00
|
|
|
agentId: null,
|
|
|
|
|
payload: { kind: "systemEvent", text: "Reminder: the thing." },
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const call = callGatewayMock.mock.calls[0]?.[0] as {
|
|
|
|
|
method?: string;
|
|
|
|
|
params?: { agentId?: string | null };
|
|
|
|
|
};
|
|
|
|
|
expect(call.method).toBe("cron.add");
|
|
|
|
|
expect(call.params?.agentId).toBeNull();
|
|
|
|
|
});
|
2026-02-05 13:08:41 -08:00
|
|
|
|
|
|
|
|
it("infers delivery from threaded session keys", async () => {
|
2026-02-16 14:52:09 +00:00
|
|
|
expect(
|
|
|
|
|
await executeAddAndReadDelivery({
|
|
|
|
|
callId: "call-thread",
|
|
|
|
|
agentSessionKey: "agent:main:slack:channel:general:thread:1699999999.0001",
|
|
|
|
|
}),
|
|
|
|
|
).toEqual({
|
2026-02-05 13:08:41 -08:00
|
|
|
mode: "announce",
|
|
|
|
|
channel: "slack",
|
|
|
|
|
to: "general",
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("preserves telegram forum topics when inferring delivery", async () => {
|
2026-02-16 14:52:09 +00:00
|
|
|
expect(
|
|
|
|
|
await executeAddAndReadDelivery({
|
|
|
|
|
callId: "call-telegram-topic",
|
|
|
|
|
agentSessionKey: "agent:main:telegram:group:-1001234567890:topic:99",
|
|
|
|
|
}),
|
|
|
|
|
).toEqual({
|
2026-02-05 13:08:41 -08:00
|
|
|
mode: "announce",
|
|
|
|
|
channel: "telegram",
|
|
|
|
|
to: "-1001234567890:topic:99",
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("infers delivery when delivery is null", async () => {
|
2026-02-16 14:52:09 +00:00
|
|
|
expect(
|
|
|
|
|
await executeAddAndReadDelivery({
|
|
|
|
|
callId: "call-null-delivery",
|
|
|
|
|
agentSessionKey: "agent:main:dm:alice",
|
2026-02-05 13:08:41 -08:00
|
|
|
delivery: null,
|
2026-02-16 14:52:09 +00:00
|
|
|
}),
|
|
|
|
|
).toEqual({
|
2026-02-05 13:08:41 -08:00
|
|
|
mode: "announce",
|
|
|
|
|
to: "alice",
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
fix(cron): recover flat params when LLM omits job wrapper (#12124)
* fix(cron): recover flat params when LLM omits job wrapper (#11310)
Non-frontier models (e.g. Grok) flatten job properties to the top level
alongside `action` instead of nesting them inside the `job` parameter.
The opaque schema (`Type.Object({}, { additionalProperties: true })`)
gives these models no structural hint, so they put name, schedule,
payload, etc. as siblings of action.
Add a flat-params recovery step in the cron add handler: when
`params.job` is missing or an empty object, scan for recognised job
property names on params and construct a synthetic job object before
passing to `normalizeCronJobCreate`. Recovery requires at least one
meaningful signal field (schedule, payload, message, or text) to avoid
false positives.
Added tests:
- Flat params with no job wrapper → recovered
- Empty job object + flat params → recovered
- Message shorthand at top level → inferred as agentTurn
- No meaningful fields → still throws 'job required'
- Non-empty job takes precedence over flat params
* fix(cron): floor nowMs to second boundary before croner lookback
Cron expressions operate at second granularity. When nowMs falls
mid-second (e.g. 12:00:00.500) and the pattern targets that exact
second (like '0 0 12 * * *'), a 1ms lookback still lands inside the
matching second. Croner interprets this as 'already past' and skips
to the next occurrence (e.g. the following day).
Fix: floor nowMs to the start of the current second before applying
the 1ms lookback. This ensures the reference always falls in the
*previous* second, so croner correctly identifies the current match.
Also compare the result against the floored nowSecondMs (not raw nowMs)
so that a match at the start of the current second is not rejected by
the >= guard when nowMs has sub-second offset.
Adds regression tests for 6-field cron patterns with specific seconds.
* fix: add changelog entries for cron fixes (#12124) (thanks @tyler6204)
* test: stabilize warning filter emit assertion (#12124) (thanks @tyler6204)
2026-02-08 23:10:09 -08:00
|
|
|
// ── Flat-params recovery (issue #11310) ──────────────────────────────
|
|
|
|
|
|
|
|
|
|
it("recovers flat params when job is missing", async () => {
|
|
|
|
|
callGatewayMock.mockResolvedValueOnce({ ok: true });
|
|
|
|
|
|
|
|
|
|
const tool = createCronTool();
|
|
|
|
|
await tool.execute("call-flat", {
|
|
|
|
|
action: "add",
|
|
|
|
|
name: "flat-job",
|
|
|
|
|
schedule: { kind: "at", at: new Date(123).toISOString() },
|
|
|
|
|
sessionTarget: "isolated",
|
|
|
|
|
payload: { kind: "agentTurn", message: "do stuff" },
|
|
|
|
|
});
|
|
|
|
|
|
2026-03-03 00:14:48 +00:00
|
|
|
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");
|
fix(cron): recover flat params when LLM omits job wrapper (#12124)
* fix(cron): recover flat params when LLM omits job wrapper (#11310)
Non-frontier models (e.g. Grok) flatten job properties to the top level
alongside `action` instead of nesting them inside the `job` parameter.
The opaque schema (`Type.Object({}, { additionalProperties: true })`)
gives these models no structural hint, so they put name, schedule,
payload, etc. as siblings of action.
Add a flat-params recovery step in the cron add handler: when
`params.job` is missing or an empty object, scan for recognised job
property names on params and construct a synthetic job object before
passing to `normalizeCronJobCreate`. Recovery requires at least one
meaningful signal field (schedule, payload, message, or text) to avoid
false positives.
Added tests:
- Flat params with no job wrapper → recovered
- Empty job object + flat params → recovered
- Message shorthand at top level → inferred as agentTurn
- No meaningful fields → still throws 'job required'
- Non-empty job takes precedence over flat params
* fix(cron): floor nowMs to second boundary before croner lookback
Cron expressions operate at second granularity. When nowMs falls
mid-second (e.g. 12:00:00.500) and the pattern targets that exact
second (like '0 0 12 * * *'), a 1ms lookback still lands inside the
matching second. Croner interprets this as 'already past' and skips
to the next occurrence (e.g. the following day).
Fix: floor nowMs to the start of the current second before applying
the 1ms lookback. This ensures the reference always falls in the
*previous* second, so croner correctly identifies the current match.
Also compare the result against the floored nowSecondMs (not raw nowMs)
so that a match at the start of the current second is not rejected by
the >= guard when nowMs has sub-second offset.
Adds regression tests for 6-field cron patterns with specific seconds.
* fix: add changelog entries for cron fixes (#12124) (thanks @tyler6204)
* test: stabilize warning filter emit assertion (#12124) (thanks @tyler6204)
2026-02-08 23:10:09 -08:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("recovers flat params when job is empty object", async () => {
|
|
|
|
|
callGatewayMock.mockResolvedValueOnce({ ok: true });
|
|
|
|
|
|
|
|
|
|
const tool = createCronTool();
|
|
|
|
|
await tool.execute("call-empty-job", {
|
|
|
|
|
action: "add",
|
|
|
|
|
job: {},
|
|
|
|
|
name: "empty-job",
|
|
|
|
|
schedule: { kind: "cron", expr: "0 9 * * *" },
|
|
|
|
|
sessionTarget: "main",
|
|
|
|
|
payload: { kind: "systemEvent", text: "wake up" },
|
|
|
|
|
});
|
|
|
|
|
|
2026-03-03 00:14:48 +00:00
|
|
|
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");
|
fix(cron): recover flat params when LLM omits job wrapper (#12124)
* fix(cron): recover flat params when LLM omits job wrapper (#11310)
Non-frontier models (e.g. Grok) flatten job properties to the top level
alongside `action` instead of nesting them inside the `job` parameter.
The opaque schema (`Type.Object({}, { additionalProperties: true })`)
gives these models no structural hint, so they put name, schedule,
payload, etc. as siblings of action.
Add a flat-params recovery step in the cron add handler: when
`params.job` is missing or an empty object, scan for recognised job
property names on params and construct a synthetic job object before
passing to `normalizeCronJobCreate`. Recovery requires at least one
meaningful signal field (schedule, payload, message, or text) to avoid
false positives.
Added tests:
- Flat params with no job wrapper → recovered
- Empty job object + flat params → recovered
- Message shorthand at top level → inferred as agentTurn
- No meaningful fields → still throws 'job required'
- Non-empty job takes precedence over flat params
* fix(cron): floor nowMs to second boundary before croner lookback
Cron expressions operate at second granularity. When nowMs falls
mid-second (e.g. 12:00:00.500) and the pattern targets that exact
second (like '0 0 12 * * *'), a 1ms lookback still lands inside the
matching second. Croner interprets this as 'already past' and skips
to the next occurrence (e.g. the following day).
Fix: floor nowMs to the start of the current second before applying
the 1ms lookback. This ensures the reference always falls in the
*previous* second, so croner correctly identifies the current match.
Also compare the result against the floored nowSecondMs (not raw nowMs)
so that a match at the start of the current second is not rejected by
the >= guard when nowMs has sub-second offset.
Adds regression tests for 6-field cron patterns with specific seconds.
* fix: add changelog entries for cron fixes (#12124) (thanks @tyler6204)
* test: stabilize warning filter emit assertion (#12124) (thanks @tyler6204)
2026-02-08 23:10:09 -08:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("recovers flat message shorthand as agentTurn payload", async () => {
|
|
|
|
|
callGatewayMock.mockResolvedValueOnce({ ok: true });
|
|
|
|
|
|
|
|
|
|
const tool = createCronTool();
|
|
|
|
|
await tool.execute("call-msg-shorthand", {
|
|
|
|
|
action: "add",
|
|
|
|
|
schedule: { kind: "at", at: new Date(456).toISOString() },
|
|
|
|
|
message: "do stuff",
|
|
|
|
|
});
|
|
|
|
|
|
2026-03-03 00:14:48 +00:00
|
|
|
const params = expectSingleGatewayCallMethod("cron.add") as
|
|
|
|
|
| { payload?: { kind?: string; message?: string }; sessionTarget?: string }
|
|
|
|
|
| undefined;
|
fix(cron): recover flat params when LLM omits job wrapper (#12124)
* fix(cron): recover flat params when LLM omits job wrapper (#11310)
Non-frontier models (e.g. Grok) flatten job properties to the top level
alongside `action` instead of nesting them inside the `job` parameter.
The opaque schema (`Type.Object({}, { additionalProperties: true })`)
gives these models no structural hint, so they put name, schedule,
payload, etc. as siblings of action.
Add a flat-params recovery step in the cron add handler: when
`params.job` is missing or an empty object, scan for recognised job
property names on params and construct a synthetic job object before
passing to `normalizeCronJobCreate`. Recovery requires at least one
meaningful signal field (schedule, payload, message, or text) to avoid
false positives.
Added tests:
- Flat params with no job wrapper → recovered
- Empty job object + flat params → recovered
- Message shorthand at top level → inferred as agentTurn
- No meaningful fields → still throws 'job required'
- Non-empty job takes precedence over flat params
* fix(cron): floor nowMs to second boundary before croner lookback
Cron expressions operate at second granularity. When nowMs falls
mid-second (e.g. 12:00:00.500) and the pattern targets that exact
second (like '0 0 12 * * *'), a 1ms lookback still lands inside the
matching second. Croner interprets this as 'already past' and skips
to the next occurrence (e.g. the following day).
Fix: floor nowMs to the start of the current second before applying
the 1ms lookback. This ensures the reference always falls in the
*previous* second, so croner correctly identifies the current match.
Also compare the result against the floored nowSecondMs (not raw nowMs)
so that a match at the start of the current second is not rejected by
the >= guard when nowMs has sub-second offset.
Adds regression tests for 6-field cron patterns with specific seconds.
* fix: add changelog entries for cron fixes (#12124) (thanks @tyler6204)
* test: stabilize warning filter emit assertion (#12124) (thanks @tyler6204)
2026-02-08 23:10:09 -08:00
|
|
|
// normalizeCronJobCreate infers agentTurn from message and isolated from agentTurn
|
2026-03-03 00:14:48 +00:00
|
|
|
expect(params?.payload?.kind).toBe("agentTurn");
|
|
|
|
|
expect(params?.payload?.message).toBe("do stuff");
|
|
|
|
|
expect(params?.sessionTarget).toBe("isolated");
|
fix(cron): recover flat params when LLM omits job wrapper (#12124)
* fix(cron): recover flat params when LLM omits job wrapper (#11310)
Non-frontier models (e.g. Grok) flatten job properties to the top level
alongside `action` instead of nesting them inside the `job` parameter.
The opaque schema (`Type.Object({}, { additionalProperties: true })`)
gives these models no structural hint, so they put name, schedule,
payload, etc. as siblings of action.
Add a flat-params recovery step in the cron add handler: when
`params.job` is missing or an empty object, scan for recognised job
property names on params and construct a synthetic job object before
passing to `normalizeCronJobCreate`. Recovery requires at least one
meaningful signal field (schedule, payload, message, or text) to avoid
false positives.
Added tests:
- Flat params with no job wrapper → recovered
- Empty job object + flat params → recovered
- Message shorthand at top level → inferred as agentTurn
- No meaningful fields → still throws 'job required'
- Non-empty job takes precedence over flat params
* fix(cron): floor nowMs to second boundary before croner lookback
Cron expressions operate at second granularity. When nowMs falls
mid-second (e.g. 12:00:00.500) and the pattern targets that exact
second (like '0 0 12 * * *'), a 1ms lookback still lands inside the
matching second. Croner interprets this as 'already past' and skips
to the next occurrence (e.g. the following day).
Fix: floor nowMs to the start of the current second before applying
the 1ms lookback. This ensures the reference always falls in the
*previous* second, so croner correctly identifies the current match.
Also compare the result against the floored nowSecondMs (not raw nowMs)
so that a match at the start of the current second is not rejected by
the >= guard when nowMs has sub-second offset.
Adds regression tests for 6-field cron patterns with specific seconds.
* fix: add changelog entries for cron fixes (#12124) (thanks @tyler6204)
* test: stabilize warning filter emit assertion (#12124) (thanks @tyler6204)
2026-02-08 23:10:09 -08:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("does not recover flat params when no meaningful job field is present", async () => {
|
|
|
|
|
const tool = createCronTool();
|
|
|
|
|
await expect(
|
|
|
|
|
tool.execute("call-no-signal", {
|
|
|
|
|
action: "add",
|
|
|
|
|
name: "orphan-name",
|
|
|
|
|
enabled: true,
|
|
|
|
|
}),
|
|
|
|
|
).rejects.toThrow("job required");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("prefers existing non-empty job over flat params", async () => {
|
|
|
|
|
callGatewayMock.mockResolvedValueOnce({ ok: true });
|
|
|
|
|
|
|
|
|
|
const tool = createCronTool();
|
|
|
|
|
await tool.execute("call-nested-wins", {
|
|
|
|
|
action: "add",
|
|
|
|
|
job: {
|
|
|
|
|
name: "nested-job",
|
|
|
|
|
schedule: { kind: "at", at: new Date(123).toISOString() },
|
|
|
|
|
payload: { kind: "systemEvent", text: "from nested" },
|
|
|
|
|
},
|
|
|
|
|
name: "flat-name-should-be-ignored",
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const call = callGatewayMock.mock.calls[0]?.[0] as {
|
|
|
|
|
params?: { name?: string; payload?: { text?: string } };
|
|
|
|
|
};
|
|
|
|
|
expect(call?.params?.name).toBe("nested-job");
|
|
|
|
|
expect(call?.params?.payload?.text).toBe("from nested");
|
|
|
|
|
});
|
|
|
|
|
|
2026-02-05 13:08:41 -08:00
|
|
|
it("does not infer delivery when mode is none", async () => {
|
|
|
|
|
callGatewayMock.mockResolvedValueOnce({ ok: true });
|
2026-02-22 17:11:17 +00:00
|
|
|
const delivery = await executeAddAndReadDelivery({
|
|
|
|
|
callId: "call-none",
|
|
|
|
|
agentSessionKey: "agent:main:discord:dm:buddy",
|
|
|
|
|
delivery: { mode: "none" },
|
2026-02-05 13:08:41 -08:00
|
|
|
});
|
2026-02-22 17:11:17 +00:00
|
|
|
expect(delivery).toEqual({ mode: "none" });
|
2026-02-05 13:08:41 -08:00
|
|
|
});
|
2026-02-16 02:36:00 -08:00
|
|
|
|
|
|
|
|
it("does not infer announce delivery when mode is webhook", async () => {
|
|
|
|
|
callGatewayMock.mockResolvedValueOnce({ ok: true });
|
2026-02-22 17:11:17 +00:00
|
|
|
const delivery = await executeAddAndReadDelivery({
|
|
|
|
|
callId: "call-webhook-explicit",
|
|
|
|
|
agentSessionKey: "agent:main:discord:dm:buddy",
|
|
|
|
|
delivery: { mode: "webhook", to: "https://example.invalid/cron-finished" },
|
2026-02-16 02:36:00 -08:00
|
|
|
});
|
2026-02-22 17:11:17 +00:00
|
|
|
expect(delivery).toEqual({
|
2026-02-16 02:36:00 -08:00
|
|
|
mode: "webhook",
|
|
|
|
|
to: "https://example.invalid/cron-finished",
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("fails fast when webhook mode is missing delivery.to", async () => {
|
|
|
|
|
const tool = createCronTool({ agentSessionKey: "agent:main:discord:dm:buddy" });
|
|
|
|
|
|
|
|
|
|
await expect(
|
|
|
|
|
tool.execute("call-webhook-missing", {
|
|
|
|
|
action: "add",
|
|
|
|
|
job: {
|
2026-03-03 00:14:48 +00:00
|
|
|
...buildReminderAgentTurnJob(),
|
2026-02-16 02:36:00 -08:00
|
|
|
delivery: { mode: "webhook" },
|
|
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
).rejects.toThrow('delivery.mode="webhook" requires delivery.to to be a valid http(s) URL');
|
|
|
|
|
expect(callGatewayMock).toHaveBeenCalledTimes(0);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("fails fast when webhook mode uses a non-http URL", async () => {
|
|
|
|
|
const tool = createCronTool({ agentSessionKey: "agent:main:discord:dm:buddy" });
|
|
|
|
|
|
|
|
|
|
await expect(
|
|
|
|
|
tool.execute("call-webhook-invalid", {
|
|
|
|
|
action: "add",
|
|
|
|
|
job: {
|
2026-03-03 00:14:48 +00:00
|
|
|
...buildReminderAgentTurnJob(),
|
2026-02-16 02:36:00 -08:00
|
|
|
delivery: { mode: "webhook", to: "ftp://example.invalid/cron-finished" },
|
|
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
).rejects.toThrow('delivery.mode="webhook" requires delivery.to to be a valid http(s) URL');
|
|
|
|
|
expect(callGatewayMock).toHaveBeenCalledTimes(0);
|
|
|
|
|
});
|
2026-03-01 20:50:51 -05:00
|
|
|
|
|
|
|
|
it("recovers flat patch params for update action", async () => {
|
|
|
|
|
callGatewayMock.mockResolvedValueOnce({ ok: true });
|
|
|
|
|
|
|
|
|
|
const tool = createCronTool();
|
|
|
|
|
await tool.execute("call-update-flat", {
|
|
|
|
|
action: "update",
|
|
|
|
|
jobId: "job-1",
|
|
|
|
|
name: "new-name",
|
|
|
|
|
enabled: false,
|
|
|
|
|
});
|
|
|
|
|
|
2026-03-03 00:14:48 +00:00
|
|
|
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);
|
2026-03-01 20:50:51 -05:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("recovers additional flat patch params for update action", async () => {
|
|
|
|
|
callGatewayMock.mockResolvedValueOnce({ ok: true });
|
|
|
|
|
|
|
|
|
|
const tool = createCronTool();
|
|
|
|
|
await tool.execute("call-update-flat-extra", {
|
|
|
|
|
action: "update",
|
|
|
|
|
id: "job-2",
|
|
|
|
|
sessionTarget: "main",
|
|
|
|
|
failureAlert: { after: 3, cooldownMs: 60_000 },
|
|
|
|
|
});
|
|
|
|
|
|
2026-03-03 00:14:48 +00:00
|
|
|
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 });
|
2026-03-01 20:50:51 -05:00
|
|
|
});
|
2026-01-04 14:57:26 +00:00
|
|
|
});
|