alvin-bot 4.18.0 → 4.18.2

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 (74) hide show
  1. package/AEC-PLUGINS-SOURCES.md +53 -0
  2. package/CHANGELOG.md +37 -2
  3. package/DESIGN-SKILLS-SOURCES.md +81 -0
  4. package/bin/cli.js +1 -1
  5. package/dist/providers/claude-sdk-provider.js +24 -0
  6. package/package.json +3 -1
  7. package/test/allowed-users-gate.test.ts +0 -98
  8. package/test/alvin-dispatch.test.ts +0 -220
  9. package/test/async-agent-chunk-flow.test.ts +0 -244
  10. package/test/async-agent-parser-staleness.test.ts +0 -412
  11. package/test/async-agent-parser-streamjson.test.ts +0 -273
  12. package/test/async-agent-parser.test.ts +0 -322
  13. package/test/async-agent-watcher.test.ts +0 -229
  14. package/test/background-bypass-integration.test.ts +0 -443
  15. package/test/background-bypass-stress.test.ts +0 -417
  16. package/test/background-bypass.test.ts +0 -127
  17. package/test/browser-webfetch.test.ts +0 -121
  18. package/test/claude-sdk-provider.test.ts +0 -115
  19. package/test/claude-sdk-tool-use-id.test.ts +0 -180
  20. package/test/console-timestamps.test.ts +0 -98
  21. package/test/cron-progress-ticker.test.ts +0 -76
  22. package/test/cron-restart-resilience.test.ts +0 -191
  23. package/test/cron-run-resolver.test.ts +0 -133
  24. package/test/cron-runjobnow-throw.test.ts +0 -100
  25. package/test/debounce.test.ts +0 -60
  26. package/test/delivery-registry.test.ts +0 -71
  27. package/test/exec-guard-metachars.test.ts +0 -110
  28. package/test/file-permissions.test.ts +0 -130
  29. package/test/i18n.test.ts +0 -108
  30. package/test/list-subagents-merged.test.ts +0 -172
  31. package/test/memory-extractor.test.ts +0 -151
  32. package/test/memory-layers.test.ts +0 -169
  33. package/test/memory-sdk-injection.test.ts +0 -146
  34. package/test/memory-stress-restart.test.ts +0 -337
  35. package/test/multi-session-stress.test.ts +0 -255
  36. package/test/platform-session-key.test.ts +0 -69
  37. package/test/process-manager.test.ts +0 -186
  38. package/test/registry.test.ts +0 -201
  39. package/test/session-pending-background.test.ts +0 -59
  40. package/test/session-persistence.test.ts +0 -195
  41. package/test/slack-progress-ticker.test.ts +0 -123
  42. package/test/slack-slash-command.test.ts +0 -61
  43. package/test/slack-test-connection.test.ts +0 -176
  44. package/test/stress-scenarios.test.ts +0 -356
  45. package/test/stuck-timer.test.ts +0 -116
  46. package/test/subagent-delivery-markdown-fallback.test.ts +0 -147
  47. package/test/subagent-delivery-platform-routing.test.ts +0 -232
  48. package/test/subagent-delivery.test.ts +0 -273
  49. package/test/subagent-final-text.test.ts +0 -132
  50. package/test/subagent-stats.test.ts +0 -119
  51. package/test/subagent-toolset-allowlist.test.ts +0 -146
  52. package/test/subagents-commands.test.ts +0 -64
  53. package/test/subagents-config.test.ts +0 -114
  54. package/test/subagents-depth.test.ts +0 -58
  55. package/test/subagents-inheritance.test.ts +0 -67
  56. package/test/subagents-name-resolver.test.ts +0 -122
  57. package/test/subagents-priority-reject.test.ts +0 -88
  58. package/test/subagents-queue.test.ts +0 -127
  59. package/test/subagents-shutdown.test.ts +0 -126
  60. package/test/subagents-toolset.test.ts +0 -71
  61. package/test/sync-task-timeout.test.ts +0 -153
  62. package/test/system-prompt-background-hint.test.ts +0 -65
  63. package/test/telegram-error-filter.test.ts +0 -85
  64. package/test/telegram-workspace-command.test.ts +0 -78
  65. package/test/timing-safe-bearer.test.ts +0 -65
  66. package/test/watchdog-brake.test.ts +0 -157
  67. package/test/watcher-pending-count.test.ts +0 -228
  68. package/test/watcher-zombie-fix.test.ts +0 -252
  69. package/test/web-server-integration.test.ts +0 -189
  70. package/test/web-server-resilience.test.ts +0 -118
  71. package/test/web-server-shutdown.test.ts +0 -117
  72. package/test/whatsapp-auth-resilience.test.ts +0 -96
  73. package/test/workspaces.test.ts +0 -196
  74. package/vitest.config.ts +0 -17
@@ -1,147 +0,0 @@
1
- /**
2
- * Fix #15 (A) — subagent-delivery must retry without parse_mode when
3
- * Telegram rejects the Markdown entities.
4
- *
5
- * Real regression: Daily Job Alert banners have been silently failing
6
- * with "Bad Request: can't parse entities: Can't find end of the entity"
7
- * every single day since the subagent-delivery module shipped. The
8
- * result text contains mixed `|`, `**`, `\|`, emoji, and asterisks that
9
- * Telegram's Markdown parser chokes on. The code currently logs the
10
- * error and drops the delivery, so the user never sees the banner.
11
- *
12
- * Contract: when `sendMessage(..., parse_mode: Markdown)` throws with
13
- * the "can't parse entities" pattern, retry the SAME text WITHOUT
14
- * `parse_mode`. Any other error still logs + bails.
15
- *
16
- * This file uses a minimal bot-api stub so we can drive both the happy
17
- * path and the parse-error path deterministically.
18
- */
19
- import { describe, it, expect, vi, beforeEach } from "vitest";
20
- import { deliverSubAgentResult, __setBotApiForTest } from "../src/services/subagent-delivery.js";
21
- import type { SubAgentInfo, SubAgentResult } from "../src/services/subagents.js";
22
-
23
- interface Sent {
24
- chatId: number;
25
- text: string;
26
- parseMode?: string;
27
- }
28
-
29
- function makeInfo(overrides: Partial<SubAgentInfo> = {}): SubAgentInfo {
30
- return {
31
- id: "id-1",
32
- name: "Daily Job Alert",
33
- status: "completed",
34
- startedAt: 0,
35
- depth: 0,
36
- source: "cron",
37
- parentChatId: 42,
38
- ...overrides,
39
- };
40
- }
41
-
42
- function makeResult(output: string): SubAgentResult {
43
- return {
44
- id: "id-1",
45
- name: "Daily Job Alert",
46
- status: "completed",
47
- output,
48
- tokensUsed: { input: 1000, output: 200 },
49
- duration: 60_000,
50
- };
51
- }
52
-
53
- beforeEach(() => {
54
- __setBotApiForTest(null);
55
- });
56
-
57
- describe("deliverSubAgentResult Markdown fallback (Fix #15)", () => {
58
- it("retries without parse_mode when Telegram rejects entity parsing", async () => {
59
- const sent: Sent[] = [];
60
- let callCount = 0;
61
-
62
- __setBotApiForTest({
63
- sendMessage: async (chatId: number, text: string, opts?: Record<string, unknown>) => {
64
- callCount++;
65
- const parseMode = opts?.parse_mode as string | undefined;
66
- // First call (Markdown) throws the real production error
67
- if (callCount === 1 && parseMode === "Markdown") {
68
- const err = Object.assign(
69
- new Error("Call to 'sendMessage' failed! (400: Bad Request: can't parse entities: Can't find end of the entity starting at byte offset 2636)"),
70
- {
71
- description: "Bad Request: can't parse entities: Can't find end of the entity starting at byte offset 2636",
72
- error_code: 400,
73
- },
74
- );
75
- throw err;
76
- }
77
- sent.push({ chatId, text, parseMode });
78
- return { message_id: 1 };
79
- },
80
- sendDocument: async () => ({}),
81
- });
82
-
83
- const info = makeInfo();
84
- const result = makeResult("This **has** | broken markdown \\| entities that fail Markdown parsing");
85
-
86
- await deliverSubAgentResult(info, result);
87
-
88
- // Must have retried at least once WITHOUT parse_mode
89
- const plainAttempt = sent.find((s) => s.parseMode === undefined);
90
- expect(plainAttempt).toBeDefined();
91
- expect(plainAttempt?.text).toContain("Daily Job Alert");
92
- expect(plainAttempt?.text).toContain("broken markdown");
93
- });
94
-
95
- it("does NOT retry for non-parse errors (e.g. chat not found)", async () => {
96
- let callCount = 0;
97
- __setBotApiForTest({
98
- sendMessage: async () => {
99
- callCount++;
100
- const err = Object.assign(new Error("Forbidden: bot was blocked by the user"), {
101
- description: "Forbidden: bot was blocked by the user",
102
- error_code: 403,
103
- });
104
- throw err;
105
- },
106
- sendDocument: async () => ({}),
107
- });
108
-
109
- await deliverSubAgentResult(makeInfo(), makeResult("some text"));
110
-
111
- // Should have tried once and given up — no retry
112
- expect(callCount).toBe(1);
113
- });
114
-
115
- it("chunked delivery also retries without parse_mode on parse errors", async () => {
116
- const sent: Sent[] = [];
117
- let callCount = 0;
118
-
119
- __setBotApiForTest({
120
- sendMessage: async (chatId: number, text: string, opts?: Record<string, unknown>) => {
121
- callCount++;
122
- const parseMode = opts?.parse_mode as string | undefined;
123
- // First banner attempt fails — should retry without parse_mode
124
- if (callCount === 1 && parseMode === "Markdown") {
125
- const err = Object.assign(
126
- new Error("400: Bad Request: can't parse entities"),
127
- { description: "can't parse entities", error_code: 400 },
128
- );
129
- throw err;
130
- }
131
- sent.push({ chatId, text, parseMode });
132
- return { message_id: callCount };
133
- },
134
- sendDocument: async () => ({}),
135
- });
136
-
137
- const info = makeInfo();
138
- // Large body forces the chunked path
139
- const result = makeResult("x".repeat(5000));
140
-
141
- await deliverSubAgentResult(info, result);
142
-
143
- // At least one plain-text delivery must have landed
144
- expect(sent.length).toBeGreaterThan(0);
145
- expect(sent.some((s) => s.parseMode === undefined)).toBe(true);
146
- });
147
- });
@@ -1,232 +0,0 @@
1
- /**
2
- * v4.14 — subagent-delivery platform routing tests.
3
- *
4
- * Covers the new v4.14 behavior: deliveries with `info.platform` other
5
- * than "telegram" go through the delivery-registry adapter instead of
6
- * the grammy bot API. Telegram path is unchanged and still uses the
7
- * injected grammy-compatible API.
8
- */
9
- import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
10
-
11
- interface CapturedMsg {
12
- chatId: string | number;
13
- text: string;
14
- }
15
-
16
- beforeEach(() => vi.resetModules());
17
-
18
- async function loadModules() {
19
- const delivery = await import("../src/services/subagent-delivery.js");
20
- const registry = await import("../src/services/delivery-registry.js");
21
- registry.__resetForTest();
22
- return { delivery, registry };
23
- }
24
-
25
- describe("subagent-delivery platform routing (v4.14)", () => {
26
- afterEach(async () => {
27
- const { delivery, registry } = await loadModules();
28
- delivery.__setBotApiForTest(null);
29
- registry.__resetForTest();
30
- });
31
-
32
- it("info.platform='slack' routes via delivery-registry (NOT grammy api)", async () => {
33
- const { delivery, registry } = await loadModules();
34
-
35
- // Register fake Slack adapter
36
- const sent: CapturedMsg[] = [];
37
- registry.registerDeliveryAdapter({
38
- platform: "slack",
39
- sendText: async (chatId, text) => {
40
- sent.push({ chatId, text });
41
- },
42
- });
43
-
44
- // Set a grammy api that SHOULD NOT be called
45
- const grammyCalls: CapturedMsg[] = [];
46
- delivery.__setBotApiForTest({
47
- sendMessage: async (chatId: number, text: string) => {
48
- grammyCalls.push({ chatId, text });
49
- return { message_id: 1 };
50
- },
51
- sendDocument: async () => ({ message_id: 1 }),
52
- });
53
-
54
- await delivery.deliverSubAgentResult(
55
- {
56
- id: "a1",
57
- name: "Research task",
58
- status: "completed",
59
- startedAt: Date.now() - 5000,
60
- source: "cron",
61
- depth: 0,
62
- parentChatId: "C012SLACKCH",
63
- platform: "slack",
64
- },
65
- {
66
- id: "a1",
67
- name: "Research task",
68
- status: "completed",
69
- output: "Result body",
70
- tokensUsed: { input: 100, output: 50 },
71
- duration: 5000,
72
- },
73
- );
74
-
75
- expect(sent).toHaveLength(1);
76
- expect(sent[0].chatId).toBe("C012SLACKCH");
77
- expect(sent[0].text).toContain("Research task");
78
- expect(sent[0].text).toContain("Result body");
79
- // grammy must NOT have been touched
80
- expect(grammyCalls).toHaveLength(0);
81
- });
82
-
83
- it("info.platform='telegram' (default) still uses grammy api — behavior unchanged", async () => {
84
- const { delivery, registry } = await loadModules();
85
-
86
- // Register Slack adapter that SHOULD NOT be called
87
- const slackCalls: CapturedMsg[] = [];
88
- registry.registerDeliveryAdapter({
89
- platform: "slack",
90
- sendText: async (chatId, text) => slackCalls.push({ chatId, text }),
91
- });
92
-
93
- const grammyCalls: CapturedMsg[] = [];
94
- delivery.__setBotApiForTest({
95
- sendMessage: async (chatId: number, text: string) => {
96
- grammyCalls.push({ chatId, text });
97
- return { message_id: 1 };
98
- },
99
- sendDocument: async () => ({ message_id: 1 }),
100
- });
101
-
102
- await delivery.deliverSubAgentResult(
103
- {
104
- id: "a2",
105
- name: "Telegram task",
106
- status: "completed",
107
- startedAt: Date.now() - 3000,
108
- source: "cron",
109
- depth: 0,
110
- parentChatId: 1234567890,
111
- // platform undefined → defaults to telegram
112
- },
113
- {
114
- id: "a2",
115
- name: "Telegram task",
116
- status: "completed",
117
- output: "Telegram body",
118
- tokensUsed: { input: 10, output: 5 },
119
- duration: 3000,
120
- },
121
- );
122
-
123
- expect(grammyCalls).toHaveLength(1);
124
- expect(grammyCalls[0].chatId).toBe(1234567890);
125
- expect(grammyCalls[0].text).toContain("Telegram body");
126
- // Slack adapter must NOT have been touched
127
- expect(slackCalls).toHaveLength(0);
128
- });
129
-
130
- it("info.platform='discord' routes to discord adapter", async () => {
131
- const { delivery, registry } = await loadModules();
132
-
133
- const discordCalls: CapturedMsg[] = [];
134
- registry.registerDeliveryAdapter({
135
- platform: "discord",
136
- sendText: async (chatId, text) =>
137
- discordCalls.push({ chatId, text }),
138
- });
139
-
140
- await delivery.deliverSubAgentResult(
141
- {
142
- id: "a3",
143
- name: "Discord task",
144
- status: "completed",
145
- startedAt: Date.now() - 1000,
146
- source: "cron",
147
- depth: 0,
148
- parentChatId: "1234567890123456",
149
- platform: "discord",
150
- },
151
- {
152
- id: "a3",
153
- name: "Discord task",
154
- status: "completed",
155
- output: "Discord body",
156
- tokensUsed: { input: 1, output: 1 },
157
- duration: 1000,
158
- },
159
- );
160
-
161
- expect(discordCalls).toHaveLength(1);
162
- expect(discordCalls[0].chatId).toBe("1234567890123456");
163
- });
164
-
165
- it("non-telegram platform with NO registered adapter skips delivery (no crash)", async () => {
166
- const { delivery } = await loadModules();
167
-
168
- await expect(
169
- delivery.deliverSubAgentResult(
170
- {
171
- id: "a4",
172
- name: "Orphan",
173
- status: "completed",
174
- startedAt: Date.now(),
175
- source: "cron",
176
- depth: 0,
177
- parentChatId: "C999",
178
- platform: "slack",
179
- },
180
- {
181
- id: "a4",
182
- name: "Orphan",
183
- status: "completed",
184
- output: "x",
185
- tokensUsed: { input: 1, output: 1 },
186
- duration: 100,
187
- },
188
- ),
189
- ).resolves.not.toThrow();
190
- });
191
-
192
- it("long output triggers chunking on non-Telegram adapter", async () => {
193
- const { delivery, registry } = await loadModules();
194
-
195
- const sent: string[] = [];
196
- registry.registerDeliveryAdapter({
197
- platform: "slack",
198
- sendText: async (_chatId, text) => {
199
- sent.push(text);
200
- },
201
- });
202
-
203
- // Build ~8000 chars of output (forces chunking at 3800)
204
- const longBody = "x".repeat(8000);
205
-
206
- await delivery.deliverSubAgentResult(
207
- {
208
- id: "a5",
209
- name: "Long task",
210
- status: "completed",
211
- startedAt: Date.now(),
212
- source: "cron",
213
- depth: 0,
214
- parentChatId: "C1",
215
- platform: "slack",
216
- },
217
- {
218
- id: "a5",
219
- name: "Long task",
220
- status: "completed",
221
- output: longBody,
222
- tokensUsed: { input: 1, output: 1 },
223
- duration: 100,
224
- },
225
- );
226
-
227
- // Expect: 1 banner + multiple body chunks
228
- expect(sent.length).toBeGreaterThan(1);
229
- const bodyBytes = sent.slice(1).join("").length;
230
- expect(bodyBytes).toBe(longBody.length);
231
- });
232
- });
@@ -1,273 +0,0 @@
1
- import { describe, it, expect, beforeEach, vi } from "vitest";
2
- import fs from "fs";
3
- import os from "os";
4
- import { resolve } from "path";
5
- import type { SubAgentInfo, SubAgentResult } from "../src/services/subagents.js";
6
-
7
- const TEST_DATA_DIR = resolve(os.tmpdir(), `alvin-bot-delivery-${process.pid}-${Date.now()}`);
8
-
9
- const sentMessages: Array<{ chatId: number; text: string }> = [];
10
- const sentDocuments: Array<{ chatId: number }> = [];
11
-
12
- beforeEach(() => {
13
- if (fs.existsSync(TEST_DATA_DIR)) fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
14
- fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
15
- process.env.ALVIN_DATA_DIR = TEST_DATA_DIR;
16
- sentMessages.length = 0;
17
- sentDocuments.length = 0;
18
- vi.resetModules();
19
- });
20
-
21
- async function wireFakeApi() {
22
- const mod = await import("../src/services/subagent-delivery.js");
23
- mod.__setBotApiForTest({
24
- sendMessage: async (chatId: number, text: string) => {
25
- sentMessages.push({ chatId, text });
26
- return {};
27
- },
28
- sendDocument: async (chatId: number) => {
29
- sentDocuments.push({ chatId });
30
- return {};
31
- },
32
- });
33
- return mod;
34
- }
35
-
36
- describe("subagent-delivery (I3)", () => {
37
- it("does nothing for source='implicit' (parent-stream handles it)", async () => {
38
- const mod = await wireFakeApi();
39
-
40
- const info: SubAgentInfo = {
41
- id: "x",
42
- name: "impl",
43
- status: "completed",
44
- startedAt: Date.now() - 1000,
45
- source: "implicit",
46
- depth: 0,
47
- parentChatId: 123,
48
- };
49
- const result: SubAgentResult = {
50
- id: "x",
51
- name: "impl",
52
- status: "completed",
53
- output: "anything",
54
- tokensUsed: { input: 10, output: 5 },
55
- duration: 1000,
56
- };
57
-
58
- await mod.deliverSubAgentResult(info, result);
59
- expect(sentMessages).toHaveLength(0);
60
- expect(sentDocuments).toHaveLength(0);
61
- });
62
-
63
- it("sends banner+final to parentChatId for source='user'", async () => {
64
- const mod = await wireFakeApi();
65
-
66
- const info: SubAgentInfo = {
67
- id: "u",
68
- name: "code-review",
69
- status: "completed",
70
- startedAt: Date.now() - 192000,
71
- source: "user",
72
- depth: 0,
73
- parentChatId: 555,
74
- };
75
- const result: SubAgentResult = {
76
- id: "u",
77
- name: "code-review",
78
- status: "completed",
79
- output: "Found 2 issues:\n1. bug\n2. nit",
80
- tokensUsed: { input: 4200, output: 2100 },
81
- duration: 192000,
82
- };
83
-
84
- await mod.deliverSubAgentResult(info, result);
85
- expect(sentMessages.length).toBeGreaterThanOrEqual(1);
86
- const all = sentMessages.map((m) => m.text).join("\n");
87
- expect(sentMessages[0].chatId).toBe(555);
88
- expect(all).toContain("code-review");
89
- expect(all).toContain("4.2k"); // token formatting
90
- expect(all).toContain("2.1k");
91
- expect(all).toContain("Found 2 issues");
92
- });
93
-
94
- it("splits long output into chunks (>3800 chars)", async () => {
95
- const mod = await wireFakeApi();
96
-
97
- const info: SubAgentInfo = {
98
- id: "c",
99
- name: "long",
100
- status: "completed",
101
- startedAt: Date.now() - 1000,
102
- source: "user",
103
- depth: 0,
104
- parentChatId: 1,
105
- };
106
- const result: SubAgentResult = {
107
- id: "c",
108
- name: "long",
109
- status: "completed",
110
- output: "x".repeat(9000),
111
- tokensUsed: { input: 0, output: 9000 },
112
- duration: 1000,
113
- };
114
-
115
- await mod.deliverSubAgentResult(info, result);
116
- // Expect: 1 banner + 3 content chunks (9000 / 3800 = 3 chunks)
117
- expect(sentMessages.length).toBeGreaterThanOrEqual(3);
118
- });
119
-
120
- it("silent visibility produces no delivery", async () => {
121
- const mod = await wireFakeApi();
122
-
123
- const info: SubAgentInfo = {
124
- id: "s",
125
- name: "silent-job",
126
- status: "completed",
127
- startedAt: Date.now() - 1000,
128
- source: "user",
129
- depth: 0,
130
- parentChatId: 1,
131
- };
132
- const result: SubAgentResult = {
133
- id: "s",
134
- name: "silent-job",
135
- status: "completed",
136
- output: "hello",
137
- tokensUsed: { input: 1, output: 1 },
138
- duration: 1000,
139
- };
140
-
141
- await mod.deliverSubAgentResult(info, result, { visibility: "silent" });
142
- expect(sentMessages).toHaveLength(0);
143
- });
144
-
145
- it("missing parentChatId logs but does not throw", async () => {
146
- const mod = await wireFakeApi();
147
-
148
- const info: SubAgentInfo = {
149
- id: "noparent",
150
- name: "orphan",
151
- status: "completed",
152
- startedAt: Date.now() - 1000,
153
- source: "user",
154
- depth: 0,
155
- // no parentChatId
156
- };
157
- const result: SubAgentResult = {
158
- id: "noparent",
159
- name: "orphan",
160
- status: "completed",
161
- output: "hi",
162
- tokensUsed: { input: 0, output: 0 },
163
- duration: 1000,
164
- };
165
-
166
- await expect(mod.deliverSubAgentResult(info, result)).resolves.toBeUndefined();
167
- expect(sentMessages).toHaveLength(0);
168
- });
169
- });
170
-
171
- describe("subagent-delivery LiveStream (A4)", () => {
172
- const edits: Array<{ chatId: number; messageId: number; text: string }> = [];
173
- let messageCounter = 100;
174
-
175
- beforeEach(() => {
176
- edits.length = 0;
177
- messageCounter = 100;
178
- });
179
-
180
- async function wireLiveApi() {
181
- const mod = await import("../src/services/subagent-delivery.js");
182
- mod.__setBotApiForTest({
183
- sendMessage: async (chatId: number, text: string) => {
184
- sentMessages.push({ chatId, text });
185
- return { message_id: messageCounter++ };
186
- },
187
- sendDocument: async (chatId: number) => {
188
- sentDocuments.push({ chatId });
189
- return {};
190
- },
191
- editMessageText: async (chatId: number, messageId: number, text: string) => {
192
- edits.push({ chatId, messageId, text });
193
- return {};
194
- },
195
- });
196
- return mod;
197
- }
198
-
199
- it("start posts an initial 'thinking…' message and records messageId", async () => {
200
- const mod = await wireLiveApi();
201
- const stream = mod.createLiveStream(555, "code-review");
202
- expect(stream).not.toBeNull();
203
- await stream!.start();
204
-
205
- expect(sentMessages).toHaveLength(1);
206
- expect(sentMessages[0].chatId).toBe(555);
207
- expect(sentMessages[0].text).toContain("thinking");
208
- expect(stream!.failed).toBe(false);
209
- });
210
-
211
- it("update coalesces multiple rapid calls into a single throttled edit", async () => {
212
- const mod = await wireLiveApi();
213
- const stream = mod.createLiveStream(1, "fast");
214
- await stream!.start();
215
-
216
- stream!.update("hello");
217
- stream!.update("hello world");
218
- stream!.update("hello world and more");
219
-
220
- // Wait for the throttle window to elapse
221
- await new Promise((r) => setTimeout(r, 900));
222
-
223
- // Should have produced exactly one edit with the LAST text
224
- expect(edits.length).toBe(1);
225
- expect(edits[0].text).toContain("hello world and more");
226
- });
227
-
228
- it("finalize posts a banner as a new message", async () => {
229
- const mod = await wireLiveApi();
230
- const stream = mod.createLiveStream(42, "done-agent");
231
- await stream!.start();
232
- stream!.update("final text");
233
- await new Promise((r) => setTimeout(r, 900)); // let flush run
234
-
235
- await stream!.finalize(
236
- {
237
- id: "x",
238
- name: "done-agent",
239
- status: "completed",
240
- startedAt: Date.now() - 5000,
241
- source: "user",
242
- depth: 0,
243
- parentChatId: 42,
244
- },
245
- {
246
- id: "x",
247
- name: "done-agent",
248
- status: "completed",
249
- output: "final text",
250
- tokensUsed: { input: 100, output: 50 },
251
- duration: 5000,
252
- },
253
- );
254
-
255
- // Two sends total: initial "thinking…" + final banner
256
- expect(sentMessages.length).toBe(2);
257
- const banner = sentMessages[sentMessages.length - 1].text;
258
- expect(banner).toContain("done-agent");
259
- expect(banner).toContain("completed");
260
- });
261
-
262
- it("createLiveStream returns null when bot api lacks editMessageText", async () => {
263
- const mod = await import("../src/services/subagent-delivery.js");
264
- // Set an api that intentionally has no editMessageText
265
- mod.__setBotApiForTest({
266
- sendMessage: async () => ({ message_id: 1 }),
267
- sendDocument: async () => ({}),
268
- // no editMessageText
269
- });
270
- const stream = mod.createLiveStream(1, "no-edit");
271
- expect(stream).toBeNull();
272
- });
273
- });