From 2d15dd757d011b96fb18fad6d0a6a9597e166597 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A4=A7=E7=8C=AB=E5=AD=90?= Date: Fri, 6 Feb 2026 08:11:19 +0800 Subject: [PATCH] fix(cron): handle undefined sessionTarget in list output (#9649) (#9752) * fix(cron): handle undefined sessionTarget in list output (#9649) When sessionTarget is undefined, pad() would crash with 'Cannot read properties of undefined (reading trim)'. Use '-' as fallback value. * test(cron): add regression test for undefined sessionTarget (#9649) Verifies that printCronList handles jobs with undefined sessionTarget without crashing. Test fails on main branch, passes with the fix. * fix: use correct CronSchedule format in tests (#9752) (thanks @lailoo) Tests were using { kind: 'at', atMs: number } but the CronSchedule type requires { kind: 'at', at: string } where 'at' is an ISO date string. --------- Co-authored-by: damaozi <1811866786@qq.com> Co-authored-by: Tyler Yust --- src/cli/cron-cli/shared.test.ts | 63 +++++++++++++++++++++++++++++++++ src/cli/cron-cli/shared.ts | 2 +- 2 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 src/cli/cron-cli/shared.test.ts 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 = (() => {