alvin-bot 4.18.0 → 4.18.1

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 (71) hide show
  1. package/CHANGELOG.md +22 -2
  2. package/bin/cli.js +1 -1
  3. package/package.json +3 -1
  4. package/test/allowed-users-gate.test.ts +0 -98
  5. package/test/alvin-dispatch.test.ts +0 -220
  6. package/test/async-agent-chunk-flow.test.ts +0 -244
  7. package/test/async-agent-parser-staleness.test.ts +0 -412
  8. package/test/async-agent-parser-streamjson.test.ts +0 -273
  9. package/test/async-agent-parser.test.ts +0 -322
  10. package/test/async-agent-watcher.test.ts +0 -229
  11. package/test/background-bypass-integration.test.ts +0 -443
  12. package/test/background-bypass-stress.test.ts +0 -417
  13. package/test/background-bypass.test.ts +0 -127
  14. package/test/browser-webfetch.test.ts +0 -121
  15. package/test/claude-sdk-provider.test.ts +0 -115
  16. package/test/claude-sdk-tool-use-id.test.ts +0 -180
  17. package/test/console-timestamps.test.ts +0 -98
  18. package/test/cron-progress-ticker.test.ts +0 -76
  19. package/test/cron-restart-resilience.test.ts +0 -191
  20. package/test/cron-run-resolver.test.ts +0 -133
  21. package/test/cron-runjobnow-throw.test.ts +0 -100
  22. package/test/debounce.test.ts +0 -60
  23. package/test/delivery-registry.test.ts +0 -71
  24. package/test/exec-guard-metachars.test.ts +0 -110
  25. package/test/file-permissions.test.ts +0 -130
  26. package/test/i18n.test.ts +0 -108
  27. package/test/list-subagents-merged.test.ts +0 -172
  28. package/test/memory-extractor.test.ts +0 -151
  29. package/test/memory-layers.test.ts +0 -169
  30. package/test/memory-sdk-injection.test.ts +0 -146
  31. package/test/memory-stress-restart.test.ts +0 -337
  32. package/test/multi-session-stress.test.ts +0 -255
  33. package/test/platform-session-key.test.ts +0 -69
  34. package/test/process-manager.test.ts +0 -186
  35. package/test/registry.test.ts +0 -201
  36. package/test/session-pending-background.test.ts +0 -59
  37. package/test/session-persistence.test.ts +0 -195
  38. package/test/slack-progress-ticker.test.ts +0 -123
  39. package/test/slack-slash-command.test.ts +0 -61
  40. package/test/slack-test-connection.test.ts +0 -176
  41. package/test/stress-scenarios.test.ts +0 -356
  42. package/test/stuck-timer.test.ts +0 -116
  43. package/test/subagent-delivery-markdown-fallback.test.ts +0 -147
  44. package/test/subagent-delivery-platform-routing.test.ts +0 -232
  45. package/test/subagent-delivery.test.ts +0 -273
  46. package/test/subagent-final-text.test.ts +0 -132
  47. package/test/subagent-stats.test.ts +0 -119
  48. package/test/subagent-toolset-allowlist.test.ts +0 -146
  49. package/test/subagents-commands.test.ts +0 -64
  50. package/test/subagents-config.test.ts +0 -114
  51. package/test/subagents-depth.test.ts +0 -58
  52. package/test/subagents-inheritance.test.ts +0 -67
  53. package/test/subagents-name-resolver.test.ts +0 -122
  54. package/test/subagents-priority-reject.test.ts +0 -88
  55. package/test/subagents-queue.test.ts +0 -127
  56. package/test/subagents-shutdown.test.ts +0 -126
  57. package/test/subagents-toolset.test.ts +0 -71
  58. package/test/sync-task-timeout.test.ts +0 -153
  59. package/test/system-prompt-background-hint.test.ts +0 -65
  60. package/test/telegram-error-filter.test.ts +0 -85
  61. package/test/telegram-workspace-command.test.ts +0 -78
  62. package/test/timing-safe-bearer.test.ts +0 -65
  63. package/test/watchdog-brake.test.ts +0 -157
  64. package/test/watcher-pending-count.test.ts +0 -228
  65. package/test/watcher-zombie-fix.test.ts +0 -252
  66. package/test/web-server-integration.test.ts +0 -189
  67. package/test/web-server-resilience.test.ts +0 -118
  68. package/test/web-server-shutdown.test.ts +0 -117
  69. package/test/whatsapp-auth-resilience.test.ts +0 -96
  70. package/test/workspaces.test.ts +0 -196
  71. package/vitest.config.ts +0 -17
@@ -1,322 +0,0 @@
1
- /**
2
- * Fix #17 (Stage 2) — async-agent-parser unit tests.
3
- *
4
- * Two pure helpers:
5
- * parseAsyncLaunchedToolResult(text) → { agentId, outputFile } | null
6
- * parseOutputFileStatus(path) → { state: "running"|"completed"|"failed"|"missing" }
7
- *
8
- * Format details captured from the live SDK probe in
9
- * docs/superpowers/specs/sdk-async-agent-outputfile-format.md
10
- */
11
- import { describe, it, expect, beforeEach, afterEach } from "vitest";
12
- import fs from "fs";
13
- import os from "os";
14
- import { resolve } from "path";
15
- import {
16
- parseAsyncLaunchedToolResult,
17
- parseOutputFileStatus,
18
- } from "../src/services/async-agent-parser.js";
19
-
20
- describe("parseAsyncLaunchedToolResult — plain text format (Stage 2)", () => {
21
- it("extracts agentId and output_file from the real SDK tool-result text", () => {
22
- const text = `Async agent launched successfully.
23
- agentId: a9e9c5913b2faec71 (internal ID - do not mention to user. Use SendMessage with to: 'a9e9c5913b2faec71' to continue this agent.)
24
- The agent is working in the background. You will be notified automatically when it completes.
25
- Do not duplicate this agent's work — avoid working with the same files or topics it is using.
26
- output_file: /private/tmp/claude-502/-Users-alvin-de-Projects-alvin-bot/abc/tasks/a9e9c5913b2faec71.output
27
- If asked, you can check progress before completion by using Read or Bash tail on the output file.`;
28
-
29
- const info = parseAsyncLaunchedToolResult(text);
30
- expect(info).not.toBeNull();
31
- expect(info?.agentId).toBe("a9e9c5913b2faec71");
32
- expect(info?.outputFile).toBe(
33
- "/private/tmp/claude-502/-Users-alvin-de-Projects-alvin-bot/abc/tasks/a9e9c5913b2faec71.output",
34
- );
35
- });
36
-
37
- it("returns null for ordinary tool result text (e.g. Read output)", () => {
38
- expect(parseAsyncLaunchedToolResult("file contents here")).toBeNull();
39
- });
40
-
41
- it("returns null for an empty string", () => {
42
- expect(parseAsyncLaunchedToolResult("")).toBeNull();
43
- });
44
-
45
- it("returns null when the marker line is missing", () => {
46
- expect(
47
- parseAsyncLaunchedToolResult("agentId: x\noutput_file: /tmp/a"),
48
- ).toBeNull();
49
- });
50
-
51
- it("returns null when output_file line is missing", () => {
52
- const text =
53
- "Async agent launched successfully.\nagentId: abc123\nMore prose";
54
- expect(parseAsyncLaunchedToolResult(text)).toBeNull();
55
- });
56
-
57
- it("returns null when agentId line is missing", () => {
58
- const text =
59
- "Async agent launched successfully.\noutput_file: /tmp/a\nMore prose";
60
- expect(parseAsyncLaunchedToolResult(text)).toBeNull();
61
- });
62
-
63
- it("trims whitespace around extracted values", () => {
64
- const text = `Async agent launched successfully.
65
- agentId: abc-with-spaces (something)
66
- output_file: /tmp/path with spaces.output `;
67
- const info = parseAsyncLaunchedToolResult(text);
68
- expect(info?.agentId).toBe("abc-with-spaces");
69
- // Path can contain spaces — we just trim leading/trailing
70
- expect(info?.outputFile).toBe("/tmp/path with spaces.output");
71
- });
72
-
73
- it("handles input that is an array of content blocks (Anthropic SDK shape)", () => {
74
- const blocks = [
75
- { type: "text", text: "Async agent launched successfully.\nagentId: id1\noutput_file: /tmp/o1\n" },
76
- ];
77
- const info = parseAsyncLaunchedToolResult(blocks);
78
- expect(info?.agentId).toBe("id1");
79
- expect(info?.outputFile).toBe("/tmp/o1");
80
- });
81
-
82
- it("handles non-string input gracefully", () => {
83
- expect(parseAsyncLaunchedToolResult(null)).toBeNull();
84
- expect(parseAsyncLaunchedToolResult(undefined)).toBeNull();
85
- expect(parseAsyncLaunchedToolResult(42 as unknown as string)).toBeNull();
86
- });
87
- });
88
-
89
- const TMP_BASE = resolve(os.tmpdir(), `alvin-parser-${process.pid}`);
90
-
91
- beforeEach(() => {
92
- fs.mkdirSync(TMP_BASE, { recursive: true });
93
- });
94
-
95
- afterEach(() => {
96
- try {
97
- fs.rmSync(TMP_BASE, { recursive: true, force: true });
98
- } catch { /* ignore */ }
99
- });
100
-
101
- async function writeJsonl(name: string, lines: object[]): Promise<string> {
102
- const path = resolve(TMP_BASE, name);
103
- fs.writeFileSync(
104
- path,
105
- lines.map((l) => JSON.stringify(l)).join("\n") + "\n",
106
- "utf-8",
107
- );
108
- return path;
109
- }
110
-
111
- describe("parseOutputFileStatus — JSONL completion detection (Stage 2)", () => {
112
- it("returns 'missing' when the file doesn't exist", async () => {
113
- const status = await parseOutputFileStatus(`${TMP_BASE}/nonexistent.jsonl`);
114
- expect(status.state).toBe("missing");
115
- });
116
-
117
- it("returns 'missing' for an empty file", async () => {
118
- const path = resolve(TMP_BASE, "empty.jsonl");
119
- fs.writeFileSync(path, "", "utf-8");
120
- const status = await parseOutputFileStatus(path);
121
- expect(status.state).toBe("missing");
122
- });
123
-
124
- it("returns 'running' when the file has events but no end_turn", async () => {
125
- const path = await writeJsonl("running.jsonl", [
126
- {
127
- type: "user",
128
- isSidechain: true,
129
- agentId: "x",
130
- message: { role: "user", content: "do the thing" },
131
- },
132
- {
133
- type: "assistant",
134
- isSidechain: true,
135
- agentId: "x",
136
- message: {
137
- role: "assistant",
138
- content: [{ type: "tool_use", name: "Bash", input: { command: "ls" } }],
139
- stop_reason: "tool_use",
140
- },
141
- },
142
- ]);
143
- const status = await parseOutputFileStatus(path);
144
- expect(status.state).toBe("running");
145
- });
146
-
147
- it("returns 'completed' with the final text when stop_reason is end_turn", async () => {
148
- const path = await writeJsonl("completed.jsonl", [
149
- {
150
- type: "user",
151
- isSidechain: true,
152
- agentId: "x",
153
- message: { role: "user", content: "p" },
154
- },
155
- {
156
- type: "assistant",
157
- isSidechain: true,
158
- agentId: "x",
159
- message: {
160
- role: "assistant",
161
- content: [{ type: "text", text: "Final report: it works!" }],
162
- stop_reason: "end_turn",
163
- usage: { input_tokens: 100, output_tokens: 50 },
164
- },
165
- },
166
- ]);
167
- const status = await parseOutputFileStatus(path);
168
- expect(status.state).toBe("completed");
169
- if (status.state === "completed") {
170
- expect(status.output).toContain("Final report: it works!");
171
- expect(status.tokensUsed).toEqual({ input: 100, output: 50 });
172
- }
173
- });
174
-
175
- it("concatenates multiple text blocks in the final assistant message", async () => {
176
- const path = await writeJsonl("multi-block.jsonl", [
177
- {
178
- type: "assistant",
179
- isSidechain: true,
180
- agentId: "x",
181
- message: {
182
- role: "assistant",
183
- content: [
184
- { type: "thinking", text: "let me think" },
185
- { type: "text", text: "Part one." },
186
- { type: "text", text: "Part two." },
187
- ],
188
- stop_reason: "end_turn",
189
- },
190
- },
191
- ]);
192
- const status = await parseOutputFileStatus(path);
193
- expect(status.state).toBe("completed");
194
- if (status.state === "completed") {
195
- expect(status.output).toBe("Part one.\n\nPart two.");
196
- // thinking blocks are NOT included
197
- expect(status.output).not.toContain("let me think");
198
- }
199
- });
200
-
201
- it("ignores assistant messages with stop_reason !== end_turn (still running)", async () => {
202
- const path = await writeJsonl("intermediate.jsonl", [
203
- {
204
- type: "assistant",
205
- isSidechain: true,
206
- agentId: "x",
207
- message: {
208
- role: "assistant",
209
- content: [{ type: "text", text: "checking..." }],
210
- stop_reason: "tool_use",
211
- },
212
- },
213
- ]);
214
- const status = await parseOutputFileStatus(path);
215
- expect(status.state).toBe("running");
216
- });
217
-
218
- it("uses the LAST end_turn assistant message when there are multiple turns", async () => {
219
- const path = await writeJsonl("multi-turn.jsonl", [
220
- {
221
- type: "assistant",
222
- agentId: "x",
223
- message: {
224
- content: [{ type: "text", text: "first answer" }],
225
- stop_reason: "end_turn",
226
- },
227
- },
228
- {
229
- type: "user",
230
- agentId: "x",
231
- message: { content: [{ type: "tool_result", content: "..." }] },
232
- },
233
- {
234
- type: "assistant",
235
- agentId: "x",
236
- message: {
237
- content: [{ type: "text", text: "second and final answer" }],
238
- stop_reason: "end_turn",
239
- },
240
- },
241
- ]);
242
- const status = await parseOutputFileStatus(path);
243
- expect(status.state).toBe("completed");
244
- if (status.state === "completed") {
245
- expect(status.output).toBe("second and final answer");
246
- }
247
- });
248
-
249
- it("survives partial final lines (mid-write)", async () => {
250
- const path = resolve(TMP_BASE, "partial.jsonl");
251
- fs.writeFileSync(
252
- path,
253
- JSON.stringify({
254
- type: "assistant",
255
- agentId: "x",
256
- message: {
257
- content: [{ type: "text", text: "checking" }],
258
- stop_reason: "tool_use",
259
- },
260
- }) +
261
- "\n" +
262
- '{"type":"assistant","agentId":"x","mes',
263
- "utf-8",
264
- );
265
- const status = await parseOutputFileStatus(path);
266
- // Partial line is ignored; only the complete event counts
267
- expect(status.state).toBe("running");
268
- });
269
-
270
- it("survives unparseable lines (skip them, keep checking)", async () => {
271
- const path = resolve(TMP_BASE, "garbage.jsonl");
272
- fs.writeFileSync(
273
- path,
274
- "garbage line\n" +
275
- JSON.stringify({
276
- type: "assistant",
277
- agentId: "x",
278
- message: {
279
- content: [{ type: "text", text: "the answer" }],
280
- stop_reason: "end_turn",
281
- },
282
- }) +
283
- "\n",
284
- "utf-8",
285
- );
286
- const status = await parseOutputFileStatus(path);
287
- expect(status.state).toBe("completed");
288
- if (status.state === "completed") {
289
- expect(status.output).toBe("the answer");
290
- }
291
- });
292
-
293
- it("only tail-reads large files (does not load entire content into memory)", async () => {
294
- const path = resolve(TMP_BASE, "huge.jsonl");
295
- // Write a 200KB padding stream of 'running' events, then an end_turn
296
- const padding = JSON.stringify({
297
- type: "assistant",
298
- agentId: "x",
299
- message: { content: [{ type: "text", text: "x".repeat(500) }], stop_reason: "tool_use" },
300
- });
301
- let buf = "";
302
- for (let i = 0; i < 200; i++) buf += padding + "\n";
303
- buf +=
304
- JSON.stringify({
305
- type: "assistant",
306
- agentId: "x",
307
- message: {
308
- content: [{ type: "text", text: "FINAL" }],
309
- stop_reason: "end_turn",
310
- },
311
- }) + "\n";
312
- fs.writeFileSync(path, buf, "utf-8");
313
- expect(fs.statSync(path).size).toBeGreaterThan(100_000);
314
-
315
- const status = await parseOutputFileStatus(path, { maxTailBytes: 8192 });
316
- // Tail should still find the last end_turn
317
- expect(status.state).toBe("completed");
318
- if (status.state === "completed") {
319
- expect(status.output).toBe("FINAL");
320
- }
321
- });
322
- });
@@ -1,229 +0,0 @@
1
- /**
2
- * Fix #17 (Stage 2) — async-agent-watcher integration tests.
3
- *
4
- * The watcher polls outputFiles of pending agents, detects completion,
5
- * delivers via subagent-delivery.ts, and persists state to disk so the
6
- * pending list survives bot restarts.
7
- */
8
- import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
9
- import fs from "fs";
10
- import os from "os";
11
- import { resolve } from "path";
12
-
13
- const TEST_DATA_DIR = resolve(os.tmpdir(), `alvin-async-watcher-${process.pid}-${Date.now()}`);
14
-
15
- interface DeliveredCall {
16
- info: { name: string; source?: string; parentChatId?: number; status?: string };
17
- result: { status: string; output: string; duration: number; error?: string };
18
- }
19
-
20
- let delivered: DeliveredCall[] = [];
21
-
22
- beforeEach(async () => {
23
- if (fs.existsSync(TEST_DATA_DIR)) fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
24
- fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
25
- process.env.ALVIN_DATA_DIR = TEST_DATA_DIR;
26
- delivered = [];
27
- vi.resetModules();
28
- vi.doMock("../src/services/subagent-delivery.js", () => ({
29
- deliverSubAgentResult: async (info: unknown, result: unknown) => {
30
- delivered.push({ info: info as DeliveredCall["info"], result: result as DeliveredCall["result"] });
31
- },
32
- attachBotApi: () => {},
33
- __setBotApiForTest: () => {},
34
- }));
35
- });
36
-
37
- afterEach(async () => {
38
- try {
39
- const mod = await import("../src/services/async-agent-watcher.js");
40
- mod.stopWatcher();
41
- mod.__resetForTest();
42
- } catch { /* ignore */ }
43
- });
44
-
45
- function writeCompletedJsonl(path: string, finalText: string): void {
46
- const lines = [
47
- JSON.stringify({
48
- type: "user",
49
- isSidechain: true,
50
- agentId: "x",
51
- message: { role: "user", content: "do it" },
52
- }),
53
- JSON.stringify({
54
- type: "assistant",
55
- isSidechain: true,
56
- agentId: "x",
57
- message: {
58
- role: "assistant",
59
- content: [{ type: "text", text: finalText }],
60
- stop_reason: "end_turn",
61
- usage: { input_tokens: 100, output_tokens: 50 },
62
- },
63
- }),
64
- ].join("\n") + "\n";
65
- fs.mkdirSync(resolve(path, ".."), { recursive: true });
66
- fs.writeFileSync(path, lines, "utf-8");
67
- }
68
-
69
- describe("async-agent-watcher (Stage 2)", () => {
70
- it("registers a pending agent and persists it to disk", async () => {
71
- const mod = await import("../src/services/async-agent-watcher.js");
72
- mod.registerPendingAgent({
73
- agentId: "abc-1",
74
- outputFile: `${TEST_DATA_DIR}/out-abc-1.jsonl`,
75
- description: "Test SEO audit",
76
- prompt: "do a test",
77
- chatId: 42,
78
- userId: 42,
79
- toolUseId: "toolu_1",
80
- });
81
- const stateFile = `${TEST_DATA_DIR}/state/async-agents.json`;
82
- expect(fs.existsSync(stateFile)).toBe(true);
83
- const parsed = JSON.parse(fs.readFileSync(stateFile, "utf-8"));
84
- expect(parsed).toHaveLength(1);
85
- expect(parsed[0].agentId).toBe("abc-1");
86
- expect(parsed[0].description).toBe("Test SEO audit");
87
- });
88
-
89
- it("delivers a pending agent when its outputFile completes", async () => {
90
- const mod = await import("../src/services/async-agent-watcher.js");
91
- const outPath = `${TEST_DATA_DIR}/out-abc-2.jsonl`;
92
- mod.registerPendingAgent({
93
- agentId: "abc-2",
94
- outputFile: outPath,
95
- description: "quick task",
96
- prompt: "p",
97
- chatId: 42,
98
- userId: 42,
99
- toolUseId: null,
100
- });
101
- writeCompletedJsonl(outPath, "Here is the report");
102
-
103
- await mod.pollOnce();
104
-
105
- expect(delivered).toHaveLength(1);
106
- expect(delivered[0].info.name).toBe("quick task");
107
- expect(delivered[0].result.output).toContain("Here is the report");
108
- expect(delivered[0].result.status).toBe("completed");
109
- });
110
-
111
- it("removes a pending agent from persistence after delivery", async () => {
112
- const mod = await import("../src/services/async-agent-watcher.js");
113
- const outPath = `${TEST_DATA_DIR}/out-abc-3.jsonl`;
114
- mod.registerPendingAgent({
115
- agentId: "abc-3",
116
- outputFile: outPath,
117
- description: "cleanup test",
118
- prompt: "p",
119
- chatId: 42,
120
- userId: 42,
121
- toolUseId: null,
122
- });
123
- writeCompletedJsonl(outPath, "done");
124
- await mod.pollOnce();
125
-
126
- const stateFile = `${TEST_DATA_DIR}/state/async-agents.json`;
127
- const state = JSON.parse(fs.readFileSync(stateFile, "utf-8"));
128
- expect(state).toHaveLength(0);
129
- });
130
-
131
- it("loads pending agents from disk at startup (bot restart catchup)", async () => {
132
- fs.mkdirSync(`${TEST_DATA_DIR}/state`, { recursive: true });
133
- const outPath = `${TEST_DATA_DIR}/out-preexisting.jsonl`;
134
- fs.writeFileSync(
135
- `${TEST_DATA_DIR}/state/async-agents.json`,
136
- JSON.stringify([
137
- {
138
- agentId: "preexisting",
139
- outputFile: outPath,
140
- description: "Survived restart",
141
- prompt: "p",
142
- chatId: 42,
143
- userId: 42,
144
- startedAt: Date.now() - 5000,
145
- lastCheckedAt: Date.now() - 1000,
146
- giveUpAt: Date.now() + 86_400_000,
147
- toolUseId: null,
148
- },
149
- ]),
150
- );
151
- writeCompletedJsonl(outPath, "result from earlier session");
152
-
153
- const mod = await import("../src/services/async-agent-watcher.js");
154
- mod.startWatcher();
155
- await mod.pollOnce();
156
-
157
- expect(delivered).toHaveLength(1);
158
- expect(delivered[0].info.name).toBe("Survived restart");
159
- expect(delivered[0].result.output).toContain("result from earlier session");
160
- });
161
-
162
- it("gives up on agents older than giveUpAt and delivers a timeout banner", async () => {
163
- const mod = await import("../src/services/async-agent-watcher.js");
164
- const outPath = `${TEST_DATA_DIR}/out-timeout.jsonl`;
165
- mod.registerPendingAgent({
166
- agentId: "abc-4",
167
- outputFile: outPath,
168
- description: "forever task",
169
- prompt: "p",
170
- chatId: 42,
171
- userId: 42,
172
- toolUseId: null,
173
- giveUpAt: Date.now() - 1000,
174
- });
175
- // File never exists
176
- await mod.pollOnce();
177
-
178
- expect(delivered).toHaveLength(1);
179
- expect(delivered[0].result.status).toBe("timeout");
180
- expect(delivered[0].info.status).toBe("timeout");
181
- });
182
-
183
- it("multiple concurrent pending agents all get delivered as they complete", async () => {
184
- const mod = await import("../src/services/async-agent-watcher.js");
185
- const outA = `${TEST_DATA_DIR}/out-a.jsonl`;
186
- const outB = `${TEST_DATA_DIR}/out-b.jsonl`;
187
- const outC = `${TEST_DATA_DIR}/out-c.jsonl`;
188
- mod.registerPendingAgent({
189
- agentId: "a", outputFile: outA, description: "A",
190
- prompt: "p", chatId: 1, userId: 1, toolUseId: null,
191
- });
192
- mod.registerPendingAgent({
193
- agentId: "b", outputFile: outB, description: "B",
194
- prompt: "p", chatId: 2, userId: 2, toolUseId: null,
195
- });
196
- mod.registerPendingAgent({
197
- agentId: "c", outputFile: outC, description: "C",
198
- prompt: "p", chatId: 3, userId: 3, toolUseId: null,
199
- });
200
-
201
- writeCompletedJsonl(outA, "A done");
202
- writeCompletedJsonl(outB, "B done");
203
- // C still pending
204
-
205
- await mod.pollOnce();
206
- expect(delivered).toHaveLength(2);
207
- expect(delivered.map((d) => d.info.name).sort()).toEqual(["A", "B"]);
208
-
209
- writeCompletedJsonl(outC, "C done");
210
- await mod.pollOnce();
211
- expect(delivered).toHaveLength(3);
212
- });
213
-
214
- it("listPendingAgents reflects in-memory state", async () => {
215
- const mod = await import("../src/services/async-agent-watcher.js");
216
- expect(mod.listPendingAgents()).toEqual([]);
217
- mod.registerPendingAgent({
218
- agentId: "x",
219
- outputFile: `${TEST_DATA_DIR}/out-x.jsonl`,
220
- description: "test",
221
- prompt: "p",
222
- chatId: 1,
223
- userId: 1,
224
- toolUseId: null,
225
- });
226
- expect(mod.listPendingAgents()).toHaveLength(1);
227
- expect(mod.listPendingAgents()[0].agentId).toBe("x");
228
- });
229
- });