@aaroncql/pim-agent 0.0.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/README.md +94 -66
  2. package/bin/pim.ts +55 -3
  3. package/package.json +20 -5
  4. package/src/extensions/_init/index.ts +3 -2
  5. package/src/extensions/apply-patch/coordinator.ts +49 -0
  6. package/src/extensions/apply-patch/executor.ts +566 -0
  7. package/src/extensions/apply-patch/index.ts +74 -0
  8. package/src/extensions/apply-patch/matcher.ts +66 -0
  9. package/src/extensions/apply-patch/model.ts +34 -0
  10. package/src/extensions/apply-patch/parser.ts +381 -0
  11. package/src/extensions/apply-patch/render.ts +261 -0
  12. package/src/extensions/apply-patch/schema.ts +43 -0
  13. package/src/extensions/apply-patch/types.ts +30 -0
  14. package/src/extensions/bash/index.ts +3 -3
  15. package/src/extensions/edit/index.ts +2 -1
  16. package/src/extensions/glob/index.ts +3 -1
  17. package/src/extensions/glob/schema.ts +2 -1
  18. package/src/extensions/grep/index.ts +3 -1
  19. package/src/extensions/grep/render.ts +18 -4
  20. package/src/extensions/grep/schema.ts +1 -1
  21. package/src/extensions/read/index.ts +36 -9
  22. package/src/extensions/read/render.ts +31 -3
  23. package/src/extensions/subagent/index.ts +4 -1
  24. package/src/extensions/todo/index.ts +4 -3
  25. package/src/extensions/web-search/index.ts +2 -1
  26. package/src/extensions/write/index.ts +2 -1
  27. package/src/shared/PatchSummary.ts +82 -0
  28. package/src/telegram/Renderer.ts +190 -4
  29. package/src/extensions/bash/capture.test.ts +0 -126
  30. package/src/extensions/bash/format.test.ts +0 -240
  31. package/src/extensions/bash/run.test.ts +0 -262
  32. package/src/extensions/command-picker/ranker.test.ts +0 -46
  33. package/src/extensions/edit/edit.test.ts +0 -285
  34. package/src/extensions/file-picker/catalog.test.ts +0 -263
  35. package/src/extensions/file-picker/index.test.ts +0 -168
  36. package/src/extensions/file-picker/ranker.test.ts +0 -94
  37. package/src/extensions/footer/git.test.ts +0 -76
  38. package/src/extensions/footer/index.test.ts +0 -161
  39. package/src/extensions/footer/segments.test.ts +0 -164
  40. package/src/extensions/glob/glob.test.ts +0 -171
  41. package/src/extensions/glob/index.test.ts +0 -68
  42. package/src/extensions/glob/render.test.ts +0 -126
  43. package/src/extensions/grep/grep.test.ts +0 -387
  44. package/src/extensions/grep/index.test.ts +0 -68
  45. package/src/extensions/grep/render.test.ts +0 -269
  46. package/src/extensions/read/read.test.ts +0 -177
  47. package/src/extensions/read/render.test.ts +0 -61
  48. package/src/extensions/subagent/index.test.ts +0 -44
  49. package/src/extensions/subagent/render.test.ts +0 -292
  50. package/src/extensions/subagent/subagent.test.ts +0 -315
  51. package/src/extensions/system-prompt/prompt.test.ts +0 -64
  52. package/src/extensions/todo/index.test.ts +0 -244
  53. package/src/extensions/todo/render.test.ts +0 -180
  54. package/src/extensions/todo/todo.test.ts +0 -222
  55. package/src/extensions/tps/index.test.ts +0 -254
  56. package/src/extensions/web-fetch/WebViewMarkdownSnapshot.test.ts +0 -119
  57. package/src/extensions/web-fetch/fetch.test.ts +0 -244
  58. package/src/extensions/web-fetch/render.test.ts +0 -56
  59. package/src/extensions/web-search/ExaMcpClient.test.ts +0 -143
  60. package/src/extensions/web-search/render.test.ts +0 -21
  61. package/src/extensions/web-search/search.test.ts +0 -53
  62. package/src/extensions/working-indicator/index.test.ts +0 -21
  63. package/src/extensions/write/render.test.ts +0 -64
  64. package/src/extensions/write/write.test.ts +0 -108
  65. package/src/shared/DiffLines.test.ts +0 -193
  66. package/src/shared/DiffRenderer.test.ts +0 -206
  67. package/src/shared/EditMatcher.test.ts +0 -123
  68. package/src/shared/FileScanner.test.ts +0 -158
  69. package/src/shared/FuzzyMatcher.test.ts +0 -114
  70. package/src/shared/GitignoreFilter.test.ts +0 -64
  71. package/src/shared/Lines.test.ts +0 -25
  72. package/src/shared/McpClient.test.ts +0 -235
  73. package/src/shared/OutputBudget.test.ts +0 -99
  74. package/src/shared/Paths.test.ts +0 -51
  75. package/src/shared/PimSettings.test.ts +0 -90
  76. package/src/shared/Renderer.test.ts +0 -190
  77. package/src/shared/SpillCache.test.ts +0 -94
  78. package/src/shared/Tools.test.ts +0 -392
  79. package/src/telegram/Config.test.ts +0 -275
  80. package/src/telegram/Markdown.test.ts +0 -143
  81. package/src/telegram/Renderer.test.ts +0 -216
  82. package/src/telegram/SessionRegistry.test.ts +0 -89
  83. package/src/telegram/TaskScheduler.test.ts +0 -278
  84. package/src/telegram/TaskTool.test.ts +0 -179
@@ -1,89 +0,0 @@
1
- import type { Api } from "grammy";
2
- import { mkdtemp, rm } from "node:fs/promises";
3
- import { tmpdir } from "node:os";
4
- import { join } from "node:path";
5
- import { afterEach, beforeEach, describe, expect, test } from "bun:test";
6
-
7
- import { Fs } from "../shared/Fs";
8
- import { type TelegramConfig } from "./Config";
9
- import { SessionRegistry } from "./SessionRegistry";
10
- import type { SessionSettings } from "./Session";
11
- import { TaskScheduler } from "./TaskScheduler";
12
-
13
- let tmp: string;
14
- let config: TelegramConfig;
15
- const stubApi = {} as Api;
16
- const stubScheduler = new TaskScheduler({
17
- configDir: "/tmp",
18
- runTask: async () => {},
19
- });
20
-
21
- beforeEach(async () => {
22
- tmp = await mkdtemp(join(tmpdir(), "pim-session-registry-test-"));
23
- config = {
24
- token: "token",
25
- allow: [],
26
- cwd: tmp,
27
- configDir: tmp,
28
- };
29
- });
30
-
31
- afterEach(async () => {
32
- await rm(tmp, { recursive: true, force: true });
33
- });
34
-
35
- async function readState(): Promise<Record<string, SessionSettings>> {
36
- return Fs.readJsonOrEmpty<Record<string, SessionSettings>>(
37
- join(tmp, "state.json"),
38
- {}
39
- );
40
- }
41
-
42
- async function writeState(
43
- state: Record<string, SessionSettings>
44
- ): Promise<void> {
45
- await Fs.writeAtomic(join(tmp, "state.json"), JSON.stringify(state, null, 2));
46
- }
47
-
48
- describe("SessionRegistry state", () => {
49
- test("loads persisted state and preserves it when mutating another session", async () => {
50
- await writeState({
51
- "1-main": {
52
- cwd: "/repo",
53
- cumulativeCost: 12.5,
54
- sessionPath: "/sessions/one.jsonl",
55
- },
56
- });
57
-
58
- const registry = new SessionRegistry(config, stubApi, stubScheduler);
59
- await registry.init();
60
- const session = registry.get({ chatId: 2, threadId: undefined });
61
- await session.setThinkingLevel("off");
62
-
63
- const loaded = await readState();
64
- expect(loaded["1-main"]).toEqual({
65
- cwd: "/repo",
66
- cumulativeCost: 12.5,
67
- sessionPath: "/sessions/one.jsonl",
68
- });
69
- expect(loaded["2-main"]?.thinkingLevel).toBe("off");
70
- });
71
-
72
- test("does not flush state when disposed before init", async () => {
73
- await writeState({
74
- "1-main": {
75
- cwd: "/repo",
76
- cumulativeCost: 12.5,
77
- },
78
- });
79
-
80
- const registry = new SessionRegistry(config, stubApi, stubScheduler);
81
- await registry.disposeAll();
82
-
83
- const loaded = await readState();
84
- expect(loaded["1-main"]).toEqual({
85
- cwd: "/repo",
86
- cumulativeCost: 12.5,
87
- });
88
- });
89
- });
@@ -1,278 +0,0 @@
1
- import { mkdtemp, readdir, rm } from "node:fs/promises";
2
- import { tmpdir } from "node:os";
3
- import { join } from "node:path";
4
- import { afterEach, beforeEach, describe, expect, test } from "bun:test";
5
-
6
- import type { SessionId } from "./Session";
7
- import { TaskScheduler } from "./TaskScheduler";
8
- import type { ScheduledTask } from "./TaskSchema";
9
- import { TaskStore } from "./TaskStore";
10
-
11
- let tmp: string;
12
- const sessionId: SessionId = { chatId: 42, threadId: undefined };
13
-
14
- beforeEach(async () => {
15
- tmp = await mkdtemp(join(tmpdir(), "pim-scheduler-test-"));
16
- });
17
-
18
- afterEach(async () => {
19
- await rm(tmp, { recursive: true, force: true });
20
- });
21
-
22
- function makeScheduler(opts: {
23
- readonly now: () => number;
24
- readonly onFire?: (task: ScheduledTask) => Promise<void>;
25
- }): { readonly scheduler: TaskScheduler; readonly fired: ScheduledTask[] } {
26
- const fired: ScheduledTask[] = [];
27
- const scheduler = new TaskScheduler({
28
- configDir: tmp,
29
- runTask: async (task) => {
30
- fired.push(task);
31
- await opts.onFire?.(task);
32
- },
33
- now: opts.now,
34
- });
35
- return { scheduler, fired };
36
- }
37
-
38
- async function listTaskFiles(): Promise<string[]> {
39
- try {
40
- return await readdir(join(tmp, "tasks"));
41
- } catch {
42
- return [];
43
- }
44
- }
45
-
46
- describe("TaskScheduler.parseDuration", () => {
47
- test("parses simple units", () => {
48
- expect(TaskScheduler.parseDuration("30s")).toBe(30_000);
49
- expect(TaskScheduler.parseDuration("5m")).toBe(300_000);
50
- expect(TaskScheduler.parseDuration("2h")).toBe(7_200_000);
51
- expect(TaskScheduler.parseDuration("1d")).toBe(86_400_000);
52
- });
53
-
54
- test("parses compound durations", () => {
55
- expect(TaskScheduler.parseDuration("1h30m")).toBe(5_400_000);
56
- expect(TaskScheduler.parseDuration("2h15m30s")).toBe(8_130_000);
57
- });
58
-
59
- test("rejects garbage", () => {
60
- expect(() => TaskScheduler.parseDuration("")).toThrow();
61
- expect(() => TaskScheduler.parseDuration("forever")).toThrow();
62
- expect(() => TaskScheduler.parseDuration("10")).toThrow();
63
- });
64
- });
65
-
66
- describe("TaskScheduler.tick", () => {
67
- test("fires due active task and reschedules interval", async () => {
68
- const t0 = Date.parse("2026-05-14T12:00:00Z");
69
- const { scheduler, fired } = makeScheduler({ now: () => t0 });
70
- const task = await scheduler.create(sessionId, {
71
- prompt: "test",
72
- schedule: { type: "interval", every: "30m" },
73
- });
74
- expect(task.nextRun).toBe(new Date(t0 + 30 * 60_000).toISOString());
75
-
76
- // Not yet due
77
- await scheduler.tick();
78
- expect(fired).toHaveLength(0);
79
-
80
- // 30 min later, due
81
- const t1 = t0 + 30 * 60_000 + 5_000;
82
- const later = makeScheduler({ now: () => t1 });
83
- await later.scheduler.tick();
84
- expect(later.fired).toHaveLength(1);
85
- expect(later.fired[0]!.id).toBe(task.id);
86
-
87
- const reloaded = (await TaskStore.loadAll(tmp))[0]!;
88
- expect(reloaded.nextRun).toBe(new Date(t1 + 30 * 60_000).toISOString());
89
- });
90
-
91
- test("skips paused tasks", async () => {
92
- const t0 = Date.parse("2026-05-14T12:00:00Z");
93
- const { scheduler } = makeScheduler({ now: () => t0 });
94
- const task = await scheduler.create(sessionId, {
95
- prompt: "test",
96
- schedule: { type: "interval", every: "5m" },
97
- });
98
- await scheduler.setStatus(sessionId, task.id, "paused");
99
-
100
- const t1 = t0 + 6 * 60_000;
101
- const { scheduler: s2, fired } = makeScheduler({ now: () => t1 });
102
- await s2.tick();
103
- expect(fired).toHaveLength(0);
104
-
105
- // File still exists
106
- expect(await listTaskFiles()).toHaveLength(1);
107
- });
108
-
109
- test("once task is deleted after firing", async () => {
110
- const t0 = Date.parse("2026-05-14T12:00:00Z");
111
- const { scheduler } = makeScheduler({ now: () => t0 });
112
- const at = new Date(t0 + 60_000).toISOString();
113
- await scheduler.create(sessionId, {
114
- prompt: "test",
115
- schedule: { type: "once", at },
116
- });
117
-
118
- const t1 = t0 + 90_000;
119
- const { scheduler: s2, fired } = makeScheduler({ now: () => t1 });
120
- await s2.tick();
121
- expect(fired).toHaveLength(1);
122
- expect(await listTaskFiles()).toHaveLength(0);
123
- });
124
-
125
- test("missed >24h is advanced silently without firing", async () => {
126
- const t0 = Date.parse("2026-05-14T12:00:00Z");
127
- // Manually write a task whose nextRun is 26h in the past
128
- const stale: ScheduledTask = {
129
- id: "stale-task",
130
- prompt: "test",
131
- chatId: sessionId.chatId,
132
- threadId: sessionId.threadId,
133
- schedule: { type: "interval", every: "1h" },
134
- status: "active",
135
- nextRun: new Date(t0 - 26 * 3600_000).toISOString(),
136
- expires: null,
137
- isolatedSession: false,
138
- createdAt: new Date(t0 - 30 * 3600_000).toISOString(),
139
- };
140
- await TaskStore.save(tmp, stale);
141
-
142
- const { scheduler, fired } = makeScheduler({ now: () => t0 });
143
- await scheduler.tick();
144
- expect(fired).toHaveLength(0);
145
-
146
- const reloaded = (await TaskStore.loadAll(tmp))[0]!;
147
- expect(Date.parse(reloaded.nextRun)).toBeGreaterThan(t0);
148
- });
149
-
150
- test("missed <24h fires once", async () => {
151
- const t0 = Date.parse("2026-05-14T12:00:00Z");
152
- const stale: ScheduledTask = {
153
- id: "recent-miss",
154
- prompt: "test",
155
- chatId: sessionId.chatId,
156
- threadId: sessionId.threadId,
157
- schedule: { type: "interval", every: "1h" },
158
- status: "active",
159
- nextRun: new Date(t0 - 2 * 3600_000).toISOString(),
160
- expires: null,
161
- isolatedSession: false,
162
- createdAt: new Date(t0 - 5 * 3600_000).toISOString(),
163
- };
164
- await TaskStore.save(tmp, stale);
165
-
166
- const { scheduler, fired } = makeScheduler({ now: () => t0 });
167
- await scheduler.tick();
168
- expect(fired).toHaveLength(1);
169
- });
170
-
171
- test("expired task is deleted without firing", async () => {
172
- const t0 = Date.parse("2026-05-14T12:00:00Z");
173
- const expired: ScheduledTask = {
174
- id: "expired",
175
- prompt: "test",
176
- chatId: sessionId.chatId,
177
- threadId: sessionId.threadId,
178
- schedule: { type: "interval", every: "1h" },
179
- status: "active",
180
- nextRun: new Date(t0 - 60_000).toISOString(),
181
- expires: new Date(t0 - 30_000).toISOString(),
182
- isolatedSession: false,
183
- createdAt: new Date(t0 - 2 * 3600_000).toISOString(),
184
- };
185
- await TaskStore.save(tmp, expired);
186
-
187
- const { scheduler, fired } = makeScheduler({ now: () => t0 });
188
- await scheduler.tick();
189
- expect(fired).toHaveLength(0);
190
- expect(await listTaskFiles()).toHaveLength(0);
191
- });
192
-
193
- test("cron task next-run advances via Bun.cron.parse", async () => {
194
- const t0 = Date.parse("2026-05-14T12:00:00Z");
195
- const { scheduler } = makeScheduler({ now: () => t0 });
196
- const task = await scheduler.create(sessionId, {
197
- prompt: "test",
198
- schedule: { type: "cron", expr: "0 * * * *" },
199
- });
200
- expect(task.nextRun).toBe(new Date(t0 + 3600_000).toISOString());
201
-
202
- // Fire it
203
- const fireTime = t0 + 3600_000 + 30_000;
204
- const { scheduler: s2, fired } = makeScheduler({ now: () => fireTime });
205
- await s2.tick();
206
- expect(fired).toHaveLength(1);
207
-
208
- const reloaded = (await TaskStore.loadAll(tmp))[0]!;
209
- // Should advance to next top-of-hour after firing
210
- expect(Date.parse(reloaded.nextRun)).toBeGreaterThan(fireTime);
211
- });
212
- });
213
-
214
- describe("TaskScheduler.create validation", () => {
215
- test("rejects 'once' with past timestamp", async () => {
216
- const t0 = Date.parse("2026-05-14T12:00:00Z");
217
- const { scheduler } = makeScheduler({ now: () => t0 });
218
- await expect(
219
- scheduler.create(sessionId, {
220
- prompt: "test",
221
- schedule: { type: "once", at: new Date(t0 - 1000).toISOString() },
222
- })
223
- ).rejects.toThrow();
224
- });
225
-
226
- test("rejects sub-minute interval", async () => {
227
- const t0 = Date.parse("2026-05-14T12:00:00Z");
228
- const { scheduler } = makeScheduler({ now: () => t0 });
229
- await expect(
230
- scheduler.create(sessionId, {
231
- prompt: "test",
232
- schedule: { type: "interval", every: "30s" },
233
- })
234
- ).rejects.toThrow();
235
- });
236
-
237
- test("rejects expires before first nextRun", async () => {
238
- const t0 = Date.parse("2026-05-14T12:00:00Z");
239
- const { scheduler } = makeScheduler({ now: () => t0 });
240
- await expect(
241
- scheduler.create(sessionId, {
242
- prompt: "test",
243
- schedule: { type: "interval", every: "1h" },
244
- expires: new Date(t0 + 5 * 60_000).toISOString(),
245
- })
246
- ).rejects.toThrow();
247
- });
248
- });
249
-
250
- describe("TaskScheduler.list", () => {
251
- test("filters by chat/thread", async () => {
252
- const t0 = Date.parse("2026-05-14T12:00:00Z");
253
- const { scheduler } = makeScheduler({ now: () => t0 });
254
- await scheduler.create(
255
- { chatId: 1, threadId: undefined },
256
- { prompt: "a", schedule: { type: "interval", every: "1h" } }
257
- );
258
- await scheduler.create(
259
- { chatId: 1, threadId: 99 },
260
- { prompt: "b", schedule: { type: "interval", every: "1h" } }
261
- );
262
- await scheduler.create(
263
- { chatId: 2, threadId: undefined },
264
- { prompt: "c", schedule: { type: "interval", every: "1h" } }
265
- );
266
-
267
- const main = await scheduler.list({
268
- chatId: 1,
269
- threadId: undefined,
270
- });
271
- expect(main).toHaveLength(1);
272
- expect(main[0]!.prompt).toBe("a");
273
-
274
- const threaded = await scheduler.list({ chatId: 1, threadId: 99 });
275
- expect(threaded).toHaveLength(1);
276
- expect(threaded[0]!.prompt).toBe("b");
277
- });
278
- });
@@ -1,179 +0,0 @@
1
- import { mkdtemp, rm } from "node:fs/promises";
2
- import { tmpdir } from "node:os";
3
- import { join } from "node:path";
4
- import { afterEach, beforeEach, describe, expect, test } from "bun:test";
5
-
6
- import type { SessionId } from "./Session";
7
- import { TaskScheduler } from "./TaskScheduler";
8
- import { TaskTool } from "./TaskTool";
9
- import type { ScheduledTask, TaskToolInput } from "./TaskSchema";
10
-
11
- let tmp: string;
12
- const sessionId: SessionId = { chatId: 7, threadId: undefined };
13
- const otherSessionId: SessionId = { chatId: 7, threadId: 99 };
14
-
15
- beforeEach(async () => {
16
- tmp = await mkdtemp(join(tmpdir(), "pim-task-tool-test-"));
17
- });
18
-
19
- afterEach(async () => {
20
- await rm(tmp, { recursive: true, force: true });
21
- });
22
-
23
- function makeTool(opts?: { readonly now?: () => number }): {
24
- readonly run: (
25
- input: TaskToolInput
26
- ) => Promise<{ readonly text: string; readonly details: unknown }>;
27
- readonly scheduler: TaskScheduler;
28
- } {
29
- const scheduler = new TaskScheduler({
30
- configDir: tmp,
31
- runTask: async () => {},
32
- now: opts?.now ?? ((): number => Date.now()),
33
- });
34
- const tool = TaskTool.build({ scheduler, sessionId });
35
- const run = async (
36
- input: TaskToolInput
37
- ): Promise<{ readonly text: string; readonly details: unknown }> => {
38
- const result = await tool.execute(
39
- "test-call-id",
40
- input,
41
- new AbortController().signal,
42
- undefined,
43
- // ExtensionContext stub — tool doesn't use it
44
- {} as never
45
- );
46
- return {
47
- text: result.content
48
- .filter((c): c is { type: "text"; text: string } => c.type === "text")
49
- .map((c) => c.text)
50
- .join("\n"),
51
- details: result.details,
52
- };
53
- };
54
- return { run, scheduler };
55
- }
56
-
57
- describe("task tool: create", () => {
58
- test("creates an interval task and returns id + nextRun", async () => {
59
- const t0 = Date.parse("2026-05-14T12:00:00Z");
60
- const { run } = makeTool({ now: () => t0 });
61
- const result = await run({
62
- action: "create",
63
- prompt: "drink water",
64
- schedule: { type: "interval", every: "1h" },
65
- });
66
- expect(result.text).toMatch(/^Created task /);
67
- const details = result.details as ScheduledTask;
68
- expect(details.prompt).toBe("drink water");
69
- expect(details.nextRun).toBe(new Date(t0 + 3600_000).toISOString());
70
- });
71
-
72
- test("rejects sub-minute interval", async () => {
73
- const { run } = makeTool();
74
- await expect(
75
- run({
76
- action: "create",
77
- prompt: "spam",
78
- schedule: { type: "interval", every: "10s" },
79
- })
80
- ).rejects.toThrow(/at least 1 minute/);
81
- });
82
-
83
- test("rejects invalid cron", async () => {
84
- const { run } = makeTool();
85
- await expect(
86
- run({
87
- action: "create",
88
- prompt: "test",
89
- schedule: { type: "cron", expr: "not a cron" },
90
- })
91
- ).rejects.toThrow();
92
- });
93
-
94
- test("requires prompt and schedule", async () => {
95
- const { run } = makeTool();
96
- await expect(run({ action: "create" })).rejects.toThrow(/prompt/);
97
- await expect(run({ action: "create", prompt: "hi" })).rejects.toThrow(
98
- /schedule/
99
- );
100
- });
101
- });
102
-
103
- describe("task tool: list/delete/pause/resume", () => {
104
- test("list filters to current sessionId", async () => {
105
- const t0 = Date.parse("2026-05-14T12:00:00Z");
106
- const { run, scheduler } = makeTool({ now: () => t0 });
107
- await run({
108
- action: "create",
109
- prompt: "mine",
110
- schedule: { type: "interval", every: "1h" },
111
- });
112
- await scheduler.create(otherSessionId, {
113
- prompt: "not mine",
114
- schedule: { type: "interval", every: "1h" },
115
- });
116
- const result = await run({ action: "list" });
117
- const details = result.details as { readonly tasks: ScheduledTask[] };
118
- expect(details.tasks).toHaveLength(1);
119
- expect(details.tasks[0]!.prompt).toBe("mine");
120
- });
121
-
122
- test("delete removes by id", async () => {
123
- const t0 = Date.parse("2026-05-14T12:00:00Z");
124
- const { run } = makeTool({ now: () => t0 });
125
- const created = await run({
126
- action: "create",
127
- prompt: "soon-gone",
128
- schedule: { type: "interval", every: "1h" },
129
- });
130
- const id = (created.details as ScheduledTask).id;
131
- await run({ action: "delete", id });
132
- const list = await run({ action: "list" });
133
- expect((list.details as { tasks: ScheduledTask[] }).tasks).toHaveLength(0);
134
- });
135
-
136
- test("delete refuses cross-session id", async () => {
137
- const t0 = Date.parse("2026-05-14T12:00:00Z");
138
- const { run, scheduler } = makeTool({ now: () => t0 });
139
- const foreign = await scheduler.create(otherSessionId, {
140
- prompt: "not yours",
141
- schedule: { type: "interval", every: "1h" },
142
- });
143
- await expect(run({ action: "delete", id: foreign.id })).rejects.toThrow(
144
- /no task/
145
- );
146
- });
147
-
148
- test("pause flips status; resume undoes it", async () => {
149
- const t0 = Date.parse("2026-05-14T12:00:00Z");
150
- const { run } = makeTool({ now: () => t0 });
151
- const created = await run({
152
- action: "create",
153
- prompt: "p",
154
- schedule: { type: "interval", every: "1h" },
155
- });
156
- const id = (created.details as ScheduledTask).id;
157
-
158
- const paused = await run({ action: "pause", id });
159
- expect((paused.details as ScheduledTask).status).toBe("paused");
160
-
161
- const resumed = await run({ action: "resume", id });
162
- expect((resumed.details as ScheduledTask).status).toBe("active");
163
- });
164
- });
165
-
166
- describe("task tool: update_prompt", () => {
167
- test("changes prompt text", async () => {
168
- const t0 = Date.parse("2026-05-14T12:00:00Z");
169
- const { run } = makeTool({ now: () => t0 });
170
- const created = await run({
171
- action: "create",
172
- prompt: "old",
173
- schedule: { type: "interval", every: "1h" },
174
- });
175
- const id = (created.details as ScheduledTask).id;
176
- const updated = await run({ action: "update_prompt", id, prompt: "new" });
177
- expect((updated.details as ScheduledTask).prompt).toBe("new");
178
- });
179
- });