
























@@ -67,6 +67,7 @@ type CronUpdatePatch = {
6767mode?: string;
6868channel?: string;
6969to?: string;
70+threadId?: number;
7071accountId?: string;
7172bestEffort?: boolean;
7273};
@@ -81,7 +82,13 @@ type CronAddParams = {
8182lightContext?: boolean;
8283toolsAllow?: string[];
8384};
84-delivery?: { mode?: string; accountId?: string };
85+delivery?: {
86+mode?: string;
87+channel?: string;
88+to?: string;
89+threadId?: number;
90+accountId?: string;
91+};
8592deleteAfterRun?: boolean;
8693agentId?: string;
8794sessionTarget?: string;
@@ -379,6 +386,32 @@ describe("cron cli", () => {
379386expect(params?.delivery?.accountId).toBe("coordinator");
380387});
381388389+it("includes --thread-id on Telegram cron add delivery", async () => {
390+const params = await runCronAddAndGetParams([
391+"--name",
392+"telegram topic add",
393+"--cron",
394+"* * * * *",
395+"--session",
396+"SESSION:agent:ops:telegram:group:-100123:topic:42",
397+"--message",
398+"hello",
399+"--deliver",
400+"--channel",
401+"telegram",
402+"--to",
403+"-100123",
404+"--thread-id",
405+" 42 ",
406+]);
407+408+expect(params?.sessionTarget).toBe("session:agent:ops:telegram:group:-100123:topic:42");
409+expect(params?.delivery?.mode).toBe("announce");
410+expect(params?.delivery?.channel).toBe("telegram");
411+expect(params?.delivery?.to).toBe("-100123");
412+expect(params?.delivery?.threadId).toBe(42);
413+});
414+382415it("rejects --account on non-isolated/systemEvent cron add", async () => {
383416await expectCronCommandExit([
384417"cron",
@@ -396,6 +429,40 @@ describe("cron cli", () => {
396429]);
397430});
398431432+it("rejects invalid --thread-id on cron add", async () => {
433+await expectCronCommandExit([
434+"cron",
435+"add",
436+"--name",
437+"invalid topic add",
438+"--cron",
439+"* * * * *",
440+"--session",
441+"isolated",
442+"--message",
443+"hello",
444+"--thread-id",
445+"topic-42",
446+]);
447+});
448+449+it("rejects negative --thread-id on cron add", async () => {
450+await expectCronCommandExit([
451+"cron",
452+"add",
453+"--name",
454+"invalid negative topic add",
455+"--cron",
456+"* * * * *",
457+"--session",
458+"isolated",
459+"--message",
460+"hello",
461+"--thread-id",
462+"-5",
463+]);
464+});
465+399466it.each([
400467{ command: "enable" as const, expectedEnabled: true },
401468{ command: "disable" as const, expectedEnabled: false },
@@ -593,6 +660,47 @@ describe("cron cli", () => {
593660expect(patch?.patch?.payload?.message).toBeUndefined();
594661});
595662663+it("updates Telegram thread id without requiring --message on cron edit", async () => {
664+const patch = await runCronEditAndGetPatch([
665+"--deliver",
666+"--channel",
667+"telegram",
668+"--to",
669+"-100123",
670+"--thread-id",
671+"42",
672+]);
673+674+expect(patch?.patch?.payload?.kind).toBe("agentTurn");
675+expect(patch?.patch?.delivery?.mode).toBe("announce");
676+expect(patch?.patch?.delivery?.channel).toBe("telegram");
677+expect(patch?.patch?.delivery?.to).toBe("-100123");
678+expect(patch?.patch?.delivery?.threadId).toBe(42);
679+});
680+681+it("preserves existing delivery mode on thread-only cron edit patches", async () => {
682+const patch = await runCronEditAndGetPatch(["--thread-id", "42"]);
683+684+expect(patch?.patch?.payload?.kind).toBe("agentTurn");
685+expect(patch?.patch?.delivery?.mode).toBeUndefined();
686+expect(patch?.patch?.delivery?.threadId).toBe(42);
687+});
688+689+it("normalizes case-insensitive custom session targets on cron edit", async () => {
690+await runCronCommand(["cron", "edit", "job-1", "--session", "SESSION:Project-Alpha"]);
691+692+const patch = getGatewayCallParams<{ patch?: { sessionTarget?: string } }>("cron.update");
693+expect(patch?.patch?.sessionTarget).toBe("session:Project-Alpha");
694+});
695+696+it("rejects invalid --thread-id on cron edit", async () => {
697+await expectCronCommandExit(["cron", "edit", "job-1", "--thread-id", "topic-42"]);
698+});
699+700+it("rejects negative --thread-id on cron edit", async () => {
701+await expectCronCommandExit(["cron", "edit", "job-1", "--thread-id", "-5"]);
702+});
703+596704it("supports --no-deliver on cron edit", async () => {
597705await runCronCommand(["cron", "edit", "job-1", "--no-deliver"]);
598706@@ -857,6 +965,120 @@ describe("cron cli", () => {
857965});
858966});
859967968+it("paginates cron edit existing-job schedule lookups", async () => {
969+resetGatewayMock();
970+callGatewayFromCli.mockImplementation(
971+async (method: string, _opts: unknown, params?: unknown) => {
972+if (method === "cron.status") {
973+return { enabled: true };
974+}
975+if (method === "cron.list") {
976+const offset = (params as { offset?: number }).offset ?? 0;
977+if (offset === 0) {
978+return {
979+jobs: [
980+{
981+ ...createCronJob("first-page", "First Page"),
982+schedule: { kind: "cron", expr: "0 * * * *" },
983+},
984+],
985+hasMore: true,
986+nextOffset: 200,
987+};
988+}
989+return {
990+jobs: [
991+{
992+ ...createCronJob("job-1", "Target Job"),
993+schedule: { kind: "cron", expr: "0 */2 * * *", staggerMs: 300_000 },
994+},
995+],
996+hasMore: false,
997+nextOffset: null,
998+};
999+}
1000+return { ok: true, params };
1001+},
1002+);
1003+1004+const program = buildProgram();
1005+await program.parseAsync(["cron", "edit", "job-1", "--exact"], { from: "user" });
1006+1007+const listParams = callGatewayFromCli.mock.calls
1008+.filter((call) => call[0] === "cron.list")
1009+.map((call) => call[2]);
1010+expect(listParams).toEqual([
1011+{ includeDisabled: true, limit: 200, offset: 0 },
1012+{ includeDisabled: true, limit: 200, offset: 200 },
1013+]);
1014+1015+const patch = getGatewayCallParams<CronUpdatePatch>("cron.update");
1016+expect(patch?.patch?.schedule).toEqual({
1017+kind: "cron",
1018+expr: "0 */2 * * *",
1019+staggerMs: 0,
1020+});
1021+});
1022+1023+it("rejects non-advancing cron edit lookup pagination", async () => {
1024+resetGatewayMock();
1025+callGatewayFromCli.mockImplementation(
1026+async (method: string, _opts: unknown, params?: unknown) => {
1027+if (method === "cron.status") {
1028+return { enabled: true };
1029+}
1030+if (method === "cron.list") {
1031+return {
1032+jobs: [],
1033+hasMore: true,
1034+nextOffset: (params as { offset?: number }).offset ?? 0,
1035+};
1036+}
1037+return { ok: true, params };
1038+},
1039+);
1040+1041+const program = buildProgram();
1042+await expect(
1043+program.parseAsync(["cron", "edit", "job-1", "--exact"], { from: "user" }),
1044+).rejects.toThrow("__exit__:1");
1045+1046+expect(defaultRuntime.error).toHaveBeenCalledWith(
1047+expect.stringContaining("cron.list pagination did not advance"),
1048+);
1049+});
1050+1051+it("rejects excessive cron edit lookup pagination", async () => {
1052+resetGatewayMock();
1053+callGatewayFromCli.mockImplementation(
1054+async (method: string, _opts: unknown, params?: unknown) => {
1055+if (method === "cron.status") {
1056+return { enabled: true };
1057+}
1058+if (method === "cron.list") {
1059+const offset = (params as { offset?: number }).offset ?? 0;
1060+return {
1061+jobs: [],
1062+hasMore: true,
1063+nextOffset: offset + 200,
1064+};
1065+}
1066+return { ok: true, params };
1067+},
1068+);
1069+1070+const program = buildProgram();
1071+await expect(
1072+program.parseAsync(["cron", "edit", "job-1", "--exact"], { from: "user" }),
1073+).rejects.toThrow("__exit__:1");
1074+1075+const listCalls = callGatewayFromCli.mock.calls.filter((call) => call[0] === "cron.list");
1076+expect(listCalls).toHaveLength(50);
1077+expect(defaultRuntime.error).toHaveBeenCalledWith(
1078+expect.stringContaining("cron.list pagination exceeded maximum pages"),
1079+);
1080+});
1081+8601082it("rejects --exact on edit when existing job is not cron", async () => {
8611083await expectCronEditWithScheduleLookupExit({ kind: "every", everyMs: 60_000 }, ["--exact"]);
8621084});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。