diff --git a/src/cli/cron-cli/shared.test.ts b/src/cli/cron-cli/shared.test.ts new file mode 100644 index 000000000..ffd67c1f2 --- /dev/null +++ b/src/cli/cron-cli/shared.test.ts @@ -0,0 +1,63 @@ +import { describe, expect, it } from "vitest"; +import type { CronJob } from "../../cron/types.js"; +import type { RuntimeEnv } from "../../runtime.js"; +import { printCronList } from "./shared.js"; + +describe("printCronList", () => { + it("handles job with undefined sessionTarget (#9649)", () => { + const logs: string[] = []; + const mockRuntime = { + log: (msg: string) => logs.push(msg), + error: () => {}, + exit: () => {}, + } as RuntimeEnv; + + // Simulate a job without sessionTarget (as reported in #9649) + const jobWithUndefinedTarget = { + id: "test-job-id", + agentId: "main", + name: "Test Job", + enabled: true, + createdAtMs: Date.now(), + updatedAtMs: Date.now(), + schedule: { kind: "at", at: new Date(Date.now() + 3600000).toISOString() }, + // sessionTarget is intentionally omitted to simulate the bug + wakeMode: "next-heartbeat", + payload: { kind: "systemEvent", text: "test" }, + state: { nextRunAtMs: Date.now() + 3600000 }, + } as CronJob; + + // This should not throw "Cannot read properties of undefined (reading 'trim')" + expect(() => printCronList([jobWithUndefinedTarget], mockRuntime)).not.toThrow(); + + // Verify output contains the job + expect(logs.length).toBeGreaterThan(1); + expect(logs.some((line) => line.includes("test-job-id"))).toBe(true); + }); + + it("handles job with defined sessionTarget", () => { + const logs: string[] = []; + const mockRuntime = { + log: (msg: string) => logs.push(msg), + error: () => {}, + exit: () => {}, + } as RuntimeEnv; + + const jobWithTarget: CronJob = { + id: "test-job-id-2", + agentId: "main", + name: "Test Job 2", + enabled: true, + createdAtMs: Date.now(), + updatedAtMs: Date.now(), + schedule: { kind: "at", at: new Date(Date.now() + 3600000).toISOString() }, + sessionTarget: "isolated", + wakeMode: "next-heartbeat", + payload: { kind: "systemEvent", text: "test" }, + state: { nextRunAtMs: Date.now() + 3600000 }, + }; + + expect(() => printCronList([jobWithTarget], mockRuntime)).not.toThrow(); + expect(logs.some((line) => line.includes("isolated"))).toBe(true); + }); +}); diff --git a/src/cli/cron-cli/shared.ts b/src/cli/cron-cli/shared.ts index 0a04fb0c1..bd7f473c6 100644 --- a/src/cli/cron-cli/shared.ts +++ b/src/cli/cron-cli/shared.ts @@ -197,7 +197,7 @@ export function printCronList(jobs: CronJob[], runtime = defaultRuntime) { const lastLabel = pad(formatRelative(job.state.lastRunAtMs, now), CRON_LAST_PAD); const statusRaw = formatStatus(job); const statusLabel = pad(statusRaw, CRON_STATUS_PAD); - const targetLabel = pad(job.sessionTarget, CRON_TARGET_PAD); + const targetLabel = pad(job.sessionTarget ?? "-", CRON_TARGET_PAD); const agentLabel = pad(truncate(job.agentId ?? "default", CRON_AGENT_PAD), CRON_AGENT_PAD); const coloredStatus = (() => {