@herdctl/discord 1.1.0 → 1.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.
- package/README.md +14 -1
- package/dist/__tests__/attachments.test.d.ts +8 -0
- package/dist/__tests__/attachments.test.d.ts.map +1 -0
- package/dist/__tests__/attachments.test.js +439 -0
- package/dist/__tests__/attachments.test.js.map +1 -0
- package/dist/__tests__/discord-connector.test.js +4 -1
- package/dist/__tests__/discord-connector.test.js.map +1 -1
- package/dist/__tests__/embeds.test.d.ts +2 -0
- package/dist/__tests__/embeds.test.d.ts.map +1 -0
- package/dist/__tests__/embeds.test.js +47 -0
- package/dist/__tests__/embeds.test.js.map +1 -0
- package/dist/__tests__/logger.test.js +4 -1
- package/dist/__tests__/logger.test.js.map +1 -1
- package/dist/__tests__/manager.test.js +1193 -28
- package/dist/__tests__/manager.test.js.map +1 -1
- package/dist/__tests__/message-normalizer.test.d.ts +2 -0
- package/dist/__tests__/message-normalizer.test.d.ts.map +1 -0
- package/dist/__tests__/message-normalizer.test.js +83 -0
- package/dist/__tests__/message-normalizer.test.js.map +1 -0
- package/dist/__tests__/runtime-parity.test.d.ts +2 -0
- package/dist/__tests__/runtime-parity.test.d.ts.map +1 -0
- package/dist/__tests__/runtime-parity.test.js +157 -0
- package/dist/__tests__/runtime-parity.test.js.map +1 -0
- package/dist/auto-mode-handler.d.ts.map +1 -1
- package/dist/auto-mode-handler.js +9 -0
- package/dist/auto-mode-handler.js.map +1 -1
- package/dist/commands/__tests__/command-manager.test.js +63 -3
- package/dist/commands/__tests__/command-manager.test.js.map +1 -1
- package/dist/commands/__tests__/extended-commands.test.d.ts +2 -0
- package/dist/commands/__tests__/extended-commands.test.d.ts.map +1 -0
- package/dist/commands/__tests__/extended-commands.test.js +159 -0
- package/dist/commands/__tests__/extended-commands.test.js.map +1 -0
- package/dist/commands/__tests__/help.test.js +5 -6
- package/dist/commands/__tests__/help.test.js.map +1 -1
- package/dist/commands/__tests__/reset.test.js +14 -6
- package/dist/commands/__tests__/reset.test.js.map +1 -1
- package/dist/commands/__tests__/status.test.js +27 -25
- package/dist/commands/__tests__/status.test.js.map +1 -1
- package/dist/commands/cancel.d.ts +3 -0
- package/dist/commands/cancel.d.ts.map +1 -0
- package/dist/commands/cancel.js +7 -0
- package/dist/commands/cancel.js.map +1 -0
- package/dist/commands/command-manager.d.ts +4 -1
- package/dist/commands/command-manager.d.ts.map +1 -1
- package/dist/commands/command-manager.js +65 -3
- package/dist/commands/command-manager.js.map +1 -1
- package/dist/commands/config.d.ts +3 -0
- package/dist/commands/config.d.ts.map +1 -0
- package/dist/commands/config.js +33 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/help.d.ts +1 -1
- package/dist/commands/help.d.ts.map +1 -1
- package/dist/commands/help.js +26 -12
- package/dist/commands/help.js.map +1 -1
- package/dist/commands/index.d.ts +12 -1
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/commands/index.js +12 -1
- package/dist/commands/index.js.map +1 -1
- package/dist/commands/new.d.ts +3 -0
- package/dist/commands/new.d.ts.map +1 -0
- package/dist/commands/new.js +22 -0
- package/dist/commands/new.js.map +1 -0
- package/dist/commands/ping.d.ts +3 -0
- package/dist/commands/ping.d.ts.map +1 -0
- package/dist/commands/ping.js +22 -0
- package/dist/commands/ping.js.map +1 -0
- package/dist/commands/reset.d.ts +1 -1
- package/dist/commands/reset.d.ts.map +1 -1
- package/dist/commands/reset.js +13 -13
- package/dist/commands/reset.js.map +1 -1
- package/dist/commands/retry.d.ts +3 -0
- package/dist/commands/retry.d.ts.map +1 -0
- package/dist/commands/retry.js +25 -0
- package/dist/commands/retry.js.map +1 -0
- package/dist/commands/session.d.ts +3 -0
- package/dist/commands/session.d.ts.map +1 -0
- package/dist/commands/session.js +47 -0
- package/dist/commands/session.js.map +1 -0
- package/dist/commands/skill.d.ts +3 -0
- package/dist/commands/skill.d.ts.map +1 -0
- package/dist/commands/skill.js +44 -0
- package/dist/commands/skill.js.map +1 -0
- package/dist/commands/skills.d.ts +3 -0
- package/dist/commands/skills.d.ts.map +1 -0
- package/dist/commands/skills.js +30 -0
- package/dist/commands/skills.js.map +1 -0
- package/dist/commands/status.d.ts +1 -1
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +25 -18
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/stop.d.ts +3 -0
- package/dist/commands/stop.d.ts.map +1 -0
- package/dist/commands/stop.js +25 -0
- package/dist/commands/stop.js.map +1 -0
- package/dist/commands/tools.d.ts +3 -0
- package/dist/commands/tools.d.ts.map +1 -0
- package/dist/commands/tools.js +30 -0
- package/dist/commands/tools.js.map +1 -0
- package/dist/commands/types.d.ts +71 -1
- package/dist/commands/types.d.ts.map +1 -1
- package/dist/commands/usage.d.ts +3 -0
- package/dist/commands/usage.d.ts.map +1 -0
- package/dist/commands/usage.js +58 -0
- package/dist/commands/usage.js.map +1 -0
- package/dist/discord-connector.d.ts +10 -1
- package/dist/discord-connector.d.ts.map +1 -1
- package/dist/discord-connector.js +153 -8
- package/dist/discord-connector.js.map +1 -1
- package/dist/embeds.d.ts +47 -0
- package/dist/embeds.d.ts.map +1 -0
- package/dist/embeds.js +121 -0
- package/dist/embeds.js.map +1 -0
- package/dist/index.d.ts +6 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/manager.d.ts +53 -24
- package/dist/manager.d.ts.map +1 -1
- package/dist/manager.js +1031 -217
- package/dist/manager.js.map +1 -1
- package/dist/mention-handler.d.ts.map +1 -1
- package/dist/mention-handler.js +27 -0
- package/dist/mention-handler.js.map +1 -1
- package/dist/message-normalizer.d.ts +40 -0
- package/dist/message-normalizer.d.ts.map +1 -0
- package/dist/message-normalizer.js +99 -0
- package/dist/message-normalizer.js.map +1 -0
- package/dist/types.d.ts +80 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/voice-transcriber.d.ts +31 -0
- package/dist/voice-transcriber.d.ts.map +1 -0
- package/dist/voice-transcriber.js +44 -0
- package/dist/voice-transcriber.js.map +1 -0
- package/package.json +3 -3
|
@@ -133,9 +133,12 @@ describe("DiscordManager", () => {
|
|
|
133
133
|
tool_results: true,
|
|
134
134
|
tool_result_max_length: 900,
|
|
135
135
|
system_status: true,
|
|
136
|
-
result_summary:
|
|
136
|
+
result_summary: true,
|
|
137
137
|
typing_indicator: true,
|
|
138
138
|
errors: true,
|
|
139
|
+
acknowledge_emoji: "eyes",
|
|
140
|
+
assistant_messages: "answers",
|
|
141
|
+
progress_indicator: true,
|
|
139
142
|
},
|
|
140
143
|
guilds: [],
|
|
141
144
|
};
|
|
@@ -183,6 +186,140 @@ describe("DiscordManager", () => {
|
|
|
183
186
|
expect(mockLogger.debug).toHaveBeenCalledWith("No Discord connectors to stop");
|
|
184
187
|
});
|
|
185
188
|
});
|
|
189
|
+
describe("retry channel run controls", () => {
|
|
190
|
+
it("routes retry through the normal handleMessage pipeline", async () => {
|
|
191
|
+
const ctx = createMockContext(null);
|
|
192
|
+
const manager = new DiscordManager(ctx);
|
|
193
|
+
const managerAny = manager;
|
|
194
|
+
const mockSend = vi.fn().mockResolvedValue({
|
|
195
|
+
edit: vi.fn().mockResolvedValue(undefined),
|
|
196
|
+
delete: vi.fn().mockResolvedValue(undefined),
|
|
197
|
+
});
|
|
198
|
+
const mockChannel = {
|
|
199
|
+
isTextBased: () => true,
|
|
200
|
+
isDMBased: () => false,
|
|
201
|
+
guildId: "guild-1",
|
|
202
|
+
send: mockSend,
|
|
203
|
+
};
|
|
204
|
+
managerAny.connectors = new Map([
|
|
205
|
+
[
|
|
206
|
+
"agent-1",
|
|
207
|
+
{
|
|
208
|
+
client: {
|
|
209
|
+
isReady: () => true,
|
|
210
|
+
channels: { fetch: vi.fn().mockResolvedValue(mockChannel) },
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
],
|
|
214
|
+
]);
|
|
215
|
+
managerAny.lastPromptByChannel.set("agent-1:channel-1", "retry prompt");
|
|
216
|
+
const handleMessageSpy = vi
|
|
217
|
+
.spyOn(managerAny, "handleMessage")
|
|
218
|
+
.mockResolvedValue(undefined);
|
|
219
|
+
const result = await managerAny.retryChannelRun("agent-1", "channel-1");
|
|
220
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
221
|
+
expect(result.success).toBe(true);
|
|
222
|
+
expect(handleMessageSpy).toHaveBeenCalledTimes(1);
|
|
223
|
+
const [, event] = handleMessageSpy.mock.calls[0];
|
|
224
|
+
expect(event.prompt).toBe("retry prompt");
|
|
225
|
+
expect(event.metadata.channelId).toBe("channel-1");
|
|
226
|
+
});
|
|
227
|
+
it("catches background retry failures and posts an error message", async () => {
|
|
228
|
+
const ctx = createMockContext(null);
|
|
229
|
+
const manager = new DiscordManager(ctx);
|
|
230
|
+
const managerAny = manager;
|
|
231
|
+
const mockSend = vi.fn().mockResolvedValue({
|
|
232
|
+
edit: vi.fn().mockResolvedValue(undefined),
|
|
233
|
+
delete: vi.fn().mockResolvedValue(undefined),
|
|
234
|
+
});
|
|
235
|
+
const mockChannel = {
|
|
236
|
+
isTextBased: () => true,
|
|
237
|
+
isDMBased: () => false,
|
|
238
|
+
guildId: "guild-1",
|
|
239
|
+
send: mockSend,
|
|
240
|
+
};
|
|
241
|
+
managerAny.connectors = new Map([
|
|
242
|
+
[
|
|
243
|
+
"agent-1",
|
|
244
|
+
{
|
|
245
|
+
client: {
|
|
246
|
+
isReady: () => true,
|
|
247
|
+
channels: { fetch: vi.fn().mockResolvedValue(mockChannel) },
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
],
|
|
251
|
+
]);
|
|
252
|
+
managerAny.lastPromptByChannel.set("agent-1:channel-1", "retry prompt");
|
|
253
|
+
vi.spyOn(managerAny, "handleMessage").mockRejectedValue(new Error("retry boom"));
|
|
254
|
+
const result = await managerAny.retryChannelRun("agent-1", "channel-1");
|
|
255
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
256
|
+
expect(result.success).toBe(true);
|
|
257
|
+
expect(mockSend).toHaveBeenCalled();
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
describe("skill discovery and validation", () => {
|
|
261
|
+
it("uses explicit chat.discord.skills when working_directory is not set", async () => {
|
|
262
|
+
const discordConfig = {
|
|
263
|
+
bot_token_env: "TEST_TOKEN",
|
|
264
|
+
session_expiry_hours: 24,
|
|
265
|
+
log_level: "standard",
|
|
266
|
+
output: {
|
|
267
|
+
tool_results: true,
|
|
268
|
+
tool_result_max_length: 900,
|
|
269
|
+
system_status: true,
|
|
270
|
+
result_summary: true,
|
|
271
|
+
typing_indicator: true,
|
|
272
|
+
errors: true,
|
|
273
|
+
acknowledge_emoji: "eyes",
|
|
274
|
+
assistant_messages: "answers",
|
|
275
|
+
progress_indicator: true,
|
|
276
|
+
},
|
|
277
|
+
guilds: [{ id: "g1" }],
|
|
278
|
+
skills: [{ name: "pdf", description: "Work with PDFs" }],
|
|
279
|
+
};
|
|
280
|
+
const config = {
|
|
281
|
+
fleet: { name: "test-fleet" },
|
|
282
|
+
agents: [createDiscordAgent("agent1", discordConfig)],
|
|
283
|
+
configPath: "/test/herdctl.yaml",
|
|
284
|
+
configDir: "/test",
|
|
285
|
+
};
|
|
286
|
+
const manager = new DiscordManager(createMockContext(config));
|
|
287
|
+
const managerAny = manager;
|
|
288
|
+
const skills = await managerAny.discoverAgentSkills(config.agents[0]);
|
|
289
|
+
expect(skills.map((s) => s.name)).toContain("pdf");
|
|
290
|
+
});
|
|
291
|
+
it("rejects unknown /skill before attempting execution", async () => {
|
|
292
|
+
const discordConfig = {
|
|
293
|
+
bot_token_env: "TEST_TOKEN",
|
|
294
|
+
session_expiry_hours: 24,
|
|
295
|
+
log_level: "standard",
|
|
296
|
+
output: {
|
|
297
|
+
tool_results: true,
|
|
298
|
+
tool_result_max_length: 900,
|
|
299
|
+
system_status: true,
|
|
300
|
+
result_summary: true,
|
|
301
|
+
typing_indicator: true,
|
|
302
|
+
errors: true,
|
|
303
|
+
acknowledge_emoji: "eyes",
|
|
304
|
+
assistant_messages: "answers",
|
|
305
|
+
progress_indicator: true,
|
|
306
|
+
},
|
|
307
|
+
guilds: [{ id: "g1" }],
|
|
308
|
+
skills: [{ name: "pdf" }],
|
|
309
|
+
};
|
|
310
|
+
const config = {
|
|
311
|
+
fleet: { name: "test-fleet" },
|
|
312
|
+
agents: [createDiscordAgent("agent1", discordConfig)],
|
|
313
|
+
configPath: "/test/herdctl.yaml",
|
|
314
|
+
configDir: "/test",
|
|
315
|
+
};
|
|
316
|
+
const manager = new DiscordManager(createMockContext(config));
|
|
317
|
+
const managerAny = manager;
|
|
318
|
+
const result = await managerAny.runChannelSkill("agent1", "channel-1", "nonexistent");
|
|
319
|
+
expect(result.success).toBe(false);
|
|
320
|
+
expect(result.message).toContain("Unknown skill");
|
|
321
|
+
});
|
|
322
|
+
});
|
|
186
323
|
describe("getConnector", () => {
|
|
187
324
|
it("returns undefined for non-existent agent", () => {
|
|
188
325
|
const ctx = createMockContext(null);
|
|
@@ -330,10 +467,14 @@ describe("DiscordMessageEvent type", () => {
|
|
|
330
467
|
wasMentioned: true,
|
|
331
468
|
mode: "mention",
|
|
332
469
|
},
|
|
333
|
-
reply:
|
|
334
|
-
console.log("Reply:", content);
|
|
335
|
-
},
|
|
470
|
+
reply: vi.fn().mockResolvedValue(undefined),
|
|
336
471
|
startTyping: () => () => { },
|
|
472
|
+
addReaction: vi.fn().mockResolvedValue(undefined),
|
|
473
|
+
removeReaction: vi.fn().mockResolvedValue(undefined),
|
|
474
|
+
replyWithRef: vi.fn().mockResolvedValue({
|
|
475
|
+
edit: vi.fn().mockResolvedValue(undefined),
|
|
476
|
+
delete: vi.fn().mockResolvedValue(undefined),
|
|
477
|
+
}),
|
|
337
478
|
};
|
|
338
479
|
expect(event.agentName).toBe("test-agent");
|
|
339
480
|
expect(event.prompt).toBe("Hello, how are you?");
|
|
@@ -360,6 +501,12 @@ describe("DiscordMessageEvent type", () => {
|
|
|
360
501
|
},
|
|
361
502
|
reply: async () => { },
|
|
362
503
|
startTyping: () => () => { },
|
|
504
|
+
addReaction: vi.fn().mockResolvedValue(undefined),
|
|
505
|
+
removeReaction: vi.fn().mockResolvedValue(undefined),
|
|
506
|
+
replyWithRef: vi.fn().mockResolvedValue({
|
|
507
|
+
edit: vi.fn().mockResolvedValue(undefined),
|
|
508
|
+
delete: vi.fn().mockResolvedValue(undefined),
|
|
509
|
+
}),
|
|
363
510
|
};
|
|
364
511
|
expect(event.metadata.guildId).toBeNull();
|
|
365
512
|
expect(event.metadata.mode).toBe("auto");
|
|
@@ -404,6 +551,9 @@ describe("DiscordManager typing_indicator config", () => {
|
|
|
404
551
|
result_summary: false,
|
|
405
552
|
typing_indicator: false,
|
|
406
553
|
errors: true,
|
|
554
|
+
acknowledge_emoji: "👀",
|
|
555
|
+
assistant_messages: "answers",
|
|
556
|
+
progress_indicator: false,
|
|
407
557
|
},
|
|
408
558
|
guilds: [],
|
|
409
559
|
}),
|
|
@@ -483,6 +633,9 @@ describe("DiscordManager typing_indicator config", () => {
|
|
|
483
633
|
},
|
|
484
634
|
reply: replyMock,
|
|
485
635
|
startTyping: startTypingMock,
|
|
636
|
+
addReaction: vi.fn().mockResolvedValue(undefined),
|
|
637
|
+
removeReaction: vi.fn().mockResolvedValue(undefined),
|
|
638
|
+
replyWithRef: vi.fn().mockResolvedValue({ messageId: "ref-msg" }),
|
|
486
639
|
};
|
|
487
640
|
mockConnector.emit("message", messageEvent);
|
|
488
641
|
await new Promise((resolve) => setTimeout(resolve, 2500));
|
|
@@ -659,35 +812,743 @@ describe.skip("DiscordManager response splitting", () => {
|
|
|
659
812
|
expect(result).toContain("/reset");
|
|
660
813
|
expect(result).toContain("Please try again");
|
|
661
814
|
});
|
|
662
|
-
it("handles errors with special characters", () => {
|
|
663
|
-
const error = new Error("Error with `code` and *markdown*");
|
|
664
|
-
const result = manager.formatErrorMessage(error);
|
|
665
|
-
expect(result).toContain("Error with `code` and *markdown*");
|
|
815
|
+
it("handles errors with special characters", () => {
|
|
816
|
+
const error = new Error("Error with `code` and *markdown*");
|
|
817
|
+
const result = manager.formatErrorMessage(error);
|
|
818
|
+
expect(result).toContain("Error with `code` and *markdown*");
|
|
819
|
+
});
|
|
820
|
+
});
|
|
821
|
+
describe("sendResponse", () => {
|
|
822
|
+
it("sends single message for short content", async () => {
|
|
823
|
+
const replyMock = vi.fn().mockResolvedValue(undefined);
|
|
824
|
+
await manager.sendResponse(replyMock, "Short message");
|
|
825
|
+
expect(replyMock).toHaveBeenCalledTimes(1);
|
|
826
|
+
expect(replyMock).toHaveBeenCalledWith("Short message");
|
|
827
|
+
});
|
|
828
|
+
it("sends multiple messages for long content", async () => {
|
|
829
|
+
const replyMock = vi.fn().mockResolvedValue(undefined);
|
|
830
|
+
const longText = "word ".repeat(500); // About 2500 chars
|
|
831
|
+
await manager.sendResponse(replyMock, longText);
|
|
832
|
+
expect(replyMock).toHaveBeenCalledTimes(2);
|
|
833
|
+
});
|
|
834
|
+
it("sends messages in order", async () => {
|
|
835
|
+
const calls = [];
|
|
836
|
+
const replyMock = vi.fn().mockImplementation(async (content) => {
|
|
837
|
+
calls.push(content);
|
|
838
|
+
});
|
|
839
|
+
const text = `First part.\n${"x".repeat(2000)}\nLast part.`;
|
|
840
|
+
await manager.sendResponse(replyMock, text);
|
|
841
|
+
// Verify order by checking first call starts with "First"
|
|
842
|
+
expect(calls[0]).toMatch(/^First/);
|
|
843
|
+
});
|
|
844
|
+
});
|
|
845
|
+
});
|
|
846
|
+
// =============================================================================
|
|
847
|
+
// handleMessage pipeline tests (active)
|
|
848
|
+
// =============================================================================
|
|
849
|
+
/**
|
|
850
|
+
* These tests exercise the handleMessage() pipeline end-to-end by:
|
|
851
|
+
* 1. Creating a DiscordManager with a mock FleetManagerContext
|
|
852
|
+
* 2. Injecting a mock connector (EventEmitter with session manager)
|
|
853
|
+
* 3. Emitting a "message" event on the connector → triggers handleMessage()
|
|
854
|
+
* 4. The mock ctx.trigger() captures and invokes the onMessage callback
|
|
855
|
+
* 5. Assertions verify reply calls, embeds, fallback behavior
|
|
856
|
+
*/
|
|
857
|
+
describe("DiscordManager handleMessage pipeline", () => {
|
|
858
|
+
/**
|
|
859
|
+
* Helper: build a DiscordManager wired to a custom ctx.trigger mock.
|
|
860
|
+
* Returns { manager, connector, triggerMock } ready for emitting events.
|
|
861
|
+
*/
|
|
862
|
+
function buildManagerWithTrigger(triggerImpl, agentOverrides) {
|
|
863
|
+
const discordConfig = {
|
|
864
|
+
bot_token_env: "TEST_BOT_TOKEN",
|
|
865
|
+
session_expiry_hours: 24,
|
|
866
|
+
log_level: "standard",
|
|
867
|
+
output: {
|
|
868
|
+
tool_results: true,
|
|
869
|
+
tool_result_max_length: 900,
|
|
870
|
+
system_status: true,
|
|
871
|
+
result_summary: false,
|
|
872
|
+
typing_indicator: true,
|
|
873
|
+
errors: true,
|
|
874
|
+
acknowledge_emoji: "",
|
|
875
|
+
assistant_messages: "answers",
|
|
876
|
+
progress_indicator: false, // disable for cleaner assertions
|
|
877
|
+
},
|
|
878
|
+
guilds: [],
|
|
879
|
+
};
|
|
880
|
+
const agent = {
|
|
881
|
+
...createDiscordAgent("test-agent", discordConfig),
|
|
882
|
+
...agentOverrides,
|
|
883
|
+
};
|
|
884
|
+
const emitter = new EventEmitter();
|
|
885
|
+
const ctx = {
|
|
886
|
+
getConfig: () => ({
|
|
887
|
+
fleet: { name: "test-fleet" },
|
|
888
|
+
agents: [agent],
|
|
889
|
+
configPath: "/test/herdctl.yaml",
|
|
890
|
+
configDir: "/test",
|
|
891
|
+
}),
|
|
892
|
+
getStateDir: () => "/tmp/test-state",
|
|
893
|
+
getStateDirInfo: () => null,
|
|
894
|
+
getLogger: () => mockLogger,
|
|
895
|
+
getScheduler: () => null,
|
|
896
|
+
getStatus: () => "running",
|
|
897
|
+
getInitializedAt: () => "2024-01-01T00:00:00.000Z",
|
|
898
|
+
getStartedAt: () => "2024-01-01T00:00:01.000Z",
|
|
899
|
+
getStoppedAt: () => null,
|
|
900
|
+
getLastError: () => null,
|
|
901
|
+
getCheckInterval: () => 1000,
|
|
902
|
+
emit: (event, ...args) => emitter.emit(event, ...args),
|
|
903
|
+
getEmitter: () => emitter,
|
|
904
|
+
trigger: triggerImpl,
|
|
905
|
+
};
|
|
906
|
+
const manager = new DiscordManager(ctx);
|
|
907
|
+
// Build mock connector
|
|
908
|
+
const connector = new EventEmitter();
|
|
909
|
+
connector.connect = vi.fn().mockResolvedValue(undefined);
|
|
910
|
+
connector.disconnect = vi.fn().mockResolvedValue(undefined);
|
|
911
|
+
connector.isConnected = vi.fn().mockReturnValue(true);
|
|
912
|
+
connector.getState = vi.fn().mockReturnValue({
|
|
913
|
+
status: "connected",
|
|
914
|
+
connectedAt: "2024-01-01T00:00:00.000Z",
|
|
915
|
+
disconnectedAt: null,
|
|
916
|
+
reconnectAttempts: 0,
|
|
917
|
+
lastError: null,
|
|
918
|
+
botUser: { id: "bot1", username: "TestBot", discriminator: "0000" },
|
|
919
|
+
rateLimits: {
|
|
920
|
+
totalCount: 0,
|
|
921
|
+
lastRateLimitAt: null,
|
|
922
|
+
isRateLimited: false,
|
|
923
|
+
currentResetTime: 0,
|
|
924
|
+
},
|
|
925
|
+
messageStats: { received: 0, sent: 0, ignored: 0 },
|
|
926
|
+
});
|
|
927
|
+
connector.uploadFile = vi.fn().mockResolvedValue({ fileId: "f1" });
|
|
928
|
+
connector.agentName = "test-agent";
|
|
929
|
+
connector.sessionManager = {
|
|
930
|
+
getOrCreateSession: vi.fn().mockResolvedValue({ sessionId: "s1", isNew: false }),
|
|
931
|
+
getSession: vi.fn().mockResolvedValue(null),
|
|
932
|
+
setSession: vi.fn().mockResolvedValue(undefined),
|
|
933
|
+
touchSession: vi.fn().mockResolvedValue(undefined),
|
|
934
|
+
getActiveSessionCount: vi.fn().mockResolvedValue(0),
|
|
935
|
+
};
|
|
936
|
+
// Inject connector
|
|
937
|
+
// @ts-expect-error - accessing private property for testing
|
|
938
|
+
manager.connectors.set("test-agent", connector);
|
|
939
|
+
// @ts-expect-error - accessing private property for testing
|
|
940
|
+
manager.initialized = true;
|
|
941
|
+
return { manager, connector, ctx };
|
|
942
|
+
}
|
|
943
|
+
/** Create a standard message event with configurable reply mock */
|
|
944
|
+
function createMessageEvent(replyMock) {
|
|
945
|
+
const replyFn = replyMock ?? vi.fn().mockResolvedValue(undefined);
|
|
946
|
+
const replyWithRefFn = vi.fn().mockResolvedValue({
|
|
947
|
+
edit: vi.fn().mockResolvedValue(undefined),
|
|
948
|
+
delete: vi.fn().mockResolvedValue(undefined),
|
|
949
|
+
});
|
|
950
|
+
const event = {
|
|
951
|
+
agentName: "test-agent",
|
|
952
|
+
prompt: "Hello bot!",
|
|
953
|
+
context: { messages: [], wasMentioned: true, prompt: "Hello bot!" },
|
|
954
|
+
metadata: {
|
|
955
|
+
guildId: "guild1",
|
|
956
|
+
channelId: "channel1",
|
|
957
|
+
messageId: "msg1",
|
|
958
|
+
userId: "user1",
|
|
959
|
+
username: "TestUser",
|
|
960
|
+
wasMentioned: true,
|
|
961
|
+
mode: "mention",
|
|
962
|
+
},
|
|
963
|
+
reply: replyFn,
|
|
964
|
+
startTyping: () => () => { },
|
|
965
|
+
addReaction: vi.fn().mockResolvedValue(undefined),
|
|
966
|
+
removeReaction: vi.fn().mockResolvedValue(undefined),
|
|
967
|
+
replyWithRef: replyWithRefFn,
|
|
968
|
+
};
|
|
969
|
+
return { event, reply: replyFn, replyWithRef: replyWithRefFn };
|
|
970
|
+
}
|
|
971
|
+
// ---- answers mode: suppresses reasoning turns, sends answer turns ----
|
|
972
|
+
it("suppresses reasoning turns (text + tool_use) in 'answers' mode", async () => {
|
|
973
|
+
const { manager, connector } = buildManagerWithTrigger(async (...args) => {
|
|
974
|
+
const options = args[2];
|
|
975
|
+
if (options?.onMessage) {
|
|
976
|
+
// Turn 1: reasoning (text + tool_use) — should be suppressed
|
|
977
|
+
await options.onMessage({
|
|
978
|
+
type: "assistant",
|
|
979
|
+
message: {
|
|
980
|
+
id: "msg_1",
|
|
981
|
+
stop_reason: "tool_use",
|
|
982
|
+
content: [
|
|
983
|
+
{ type: "text", text: "Let me check..." },
|
|
984
|
+
{ type: "tool_use", name: "Read", id: "t1", input: { file_path: "/x.txt" } },
|
|
985
|
+
],
|
|
986
|
+
},
|
|
987
|
+
});
|
|
988
|
+
// Turn 2: answer (text only) — should be sent
|
|
989
|
+
await options.onMessage({
|
|
990
|
+
type: "assistant",
|
|
991
|
+
message: {
|
|
992
|
+
id: "msg_2",
|
|
993
|
+
stop_reason: "end_turn",
|
|
994
|
+
content: [{ type: "text", text: "Here is the answer." }],
|
|
995
|
+
},
|
|
996
|
+
});
|
|
997
|
+
await options.onMessage({ type: "result", result: "Here is the answer." });
|
|
998
|
+
}
|
|
999
|
+
return {
|
|
1000
|
+
jobId: "j1",
|
|
1001
|
+
agentName: "test-agent",
|
|
1002
|
+
scheduleName: null,
|
|
1003
|
+
startedAt: new Date().toISOString(),
|
|
1004
|
+
success: true,
|
|
1005
|
+
sessionId: "sid1",
|
|
1006
|
+
};
|
|
1007
|
+
});
|
|
1008
|
+
await manager.start();
|
|
1009
|
+
const { event, reply } = createMessageEvent();
|
|
1010
|
+
connector.emit("message", event);
|
|
1011
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
1012
|
+
// Only the answer turn (no tool_use) should be delivered
|
|
1013
|
+
const textCalls = reply.mock.calls.filter((call) => typeof call[0] === "string");
|
|
1014
|
+
expect(textCalls).toHaveLength(1);
|
|
1015
|
+
expect(textCalls[0][0]).toBe("Here is the answer.");
|
|
1016
|
+
});
|
|
1017
|
+
it("sends all answer turns immediately in 'answers' mode", async () => {
|
|
1018
|
+
const { manager, connector } = buildManagerWithTrigger(async (...args) => {
|
|
1019
|
+
const options = args[2];
|
|
1020
|
+
if (options?.onMessage) {
|
|
1021
|
+
// Two answer turns (text only, no tool_use) — both should be sent
|
|
1022
|
+
await options.onMessage({
|
|
1023
|
+
type: "assistant",
|
|
1024
|
+
message: {
|
|
1025
|
+
id: "msg_1",
|
|
1026
|
+
stop_reason: "end_turn",
|
|
1027
|
+
content: [{ type: "text", text: "First answer." }],
|
|
1028
|
+
},
|
|
1029
|
+
});
|
|
1030
|
+
await options.onMessage({
|
|
1031
|
+
type: "assistant",
|
|
1032
|
+
message: {
|
|
1033
|
+
id: "msg_2",
|
|
1034
|
+
stop_reason: "end_turn",
|
|
1035
|
+
content: [{ type: "text", text: "Second answer." }],
|
|
1036
|
+
},
|
|
1037
|
+
});
|
|
1038
|
+
await options.onMessage({ type: "result", result: "Second answer." });
|
|
1039
|
+
}
|
|
1040
|
+
return {
|
|
1041
|
+
jobId: "j1b",
|
|
1042
|
+
agentName: "test-agent",
|
|
1043
|
+
scheduleName: null,
|
|
1044
|
+
startedAt: new Date().toISOString(),
|
|
1045
|
+
success: true,
|
|
1046
|
+
sessionId: "sid1b",
|
|
1047
|
+
};
|
|
1048
|
+
});
|
|
1049
|
+
await manager.start();
|
|
1050
|
+
const { event, reply } = createMessageEvent();
|
|
1051
|
+
connector.emit("message", event);
|
|
1052
|
+
await new Promise((resolve) => setTimeout(resolve, 2500)); // wait for rate limiting
|
|
1053
|
+
// Both answer turns should be delivered
|
|
1054
|
+
const textCalls = reply.mock.calls.filter((call) => typeof call[0] === "string");
|
|
1055
|
+
expect(textCalls).toHaveLength(2);
|
|
1056
|
+
expect(textCalls[0][0]).toBe("First answer.");
|
|
1057
|
+
expect(textCalls[1][0]).toBe("Second answer.");
|
|
1058
|
+
});
|
|
1059
|
+
// ---- resultText fallback when all turns are reasoning (tool-only) ----
|
|
1060
|
+
it("uses SDK result text as fallback when all turns are reasoning", async () => {
|
|
1061
|
+
const { manager, connector } = buildManagerWithTrigger(async (...args) => {
|
|
1062
|
+
const options = args[2];
|
|
1063
|
+
if (options?.onMessage) {
|
|
1064
|
+
// Assistant message with ONLY tool_use (no text blocks)
|
|
1065
|
+
await options.onMessage({
|
|
1066
|
+
type: "assistant",
|
|
1067
|
+
message: {
|
|
1068
|
+
id: "msg_1",
|
|
1069
|
+
stop_reason: "tool_use",
|
|
1070
|
+
content: [{ type: "tool_use", name: "Bash", id: "t1", input: { command: "ls" } }],
|
|
1071
|
+
},
|
|
1072
|
+
});
|
|
1073
|
+
// Tool result
|
|
1074
|
+
await options.onMessage({
|
|
1075
|
+
type: "user",
|
|
1076
|
+
message: {
|
|
1077
|
+
content: [{ type: "tool_result", tool_use_id: "t1", content: "file1 file2" }],
|
|
1078
|
+
},
|
|
1079
|
+
});
|
|
1080
|
+
// Result message has the final answer text
|
|
1081
|
+
await options.onMessage({
|
|
1082
|
+
type: "result",
|
|
1083
|
+
result: "The directory contains file1 and file2.",
|
|
1084
|
+
});
|
|
1085
|
+
}
|
|
1086
|
+
return {
|
|
1087
|
+
jobId: "j2",
|
|
1088
|
+
agentName: "test-agent",
|
|
1089
|
+
scheduleName: null,
|
|
1090
|
+
startedAt: new Date().toISOString(),
|
|
1091
|
+
success: true,
|
|
1092
|
+
sessionId: "sid2",
|
|
1093
|
+
};
|
|
1094
|
+
});
|
|
1095
|
+
await manager.start();
|
|
1096
|
+
const { event, reply } = createMessageEvent();
|
|
1097
|
+
connector.emit("message", event);
|
|
1098
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
1099
|
+
// The result text should be sent as fallback
|
|
1100
|
+
const textCalls = reply.mock.calls.filter((call) => typeof call[0] === "string");
|
|
1101
|
+
expect(textCalls).toHaveLength(1);
|
|
1102
|
+
expect(textCalls[0][0]).toBe("The directory contains file1 and file2.");
|
|
1103
|
+
});
|
|
1104
|
+
// ---- Dedup: skip intermediates (stop_reason: null) ----
|
|
1105
|
+
it("skips intermediate assistant snapshots (stop_reason: null)", async () => {
|
|
1106
|
+
const { manager, connector } = buildManagerWithTrigger(async (...args) => {
|
|
1107
|
+
const options = args[2];
|
|
1108
|
+
if (options?.onMessage) {
|
|
1109
|
+
// Intermediate snapshot (stop_reason: null) — should be skipped
|
|
1110
|
+
await options.onMessage({
|
|
1111
|
+
type: "assistant",
|
|
1112
|
+
message: {
|
|
1113
|
+
id: "msg_1",
|
|
1114
|
+
stop_reason: null,
|
|
1115
|
+
content: [{ type: "text", text: "Partial..." }],
|
|
1116
|
+
},
|
|
1117
|
+
});
|
|
1118
|
+
// Final snapshot (stop_reason: "end_turn") — should be delivered
|
|
1119
|
+
await options.onMessage({
|
|
1120
|
+
type: "assistant",
|
|
1121
|
+
message: {
|
|
1122
|
+
id: "msg_1",
|
|
1123
|
+
stop_reason: "end_turn",
|
|
1124
|
+
content: [{ type: "text", text: "Complete answer." }],
|
|
1125
|
+
},
|
|
1126
|
+
});
|
|
1127
|
+
await options.onMessage({ type: "result", result: "Complete answer." });
|
|
1128
|
+
}
|
|
1129
|
+
return {
|
|
1130
|
+
jobId: "j3",
|
|
1131
|
+
agentName: "test-agent",
|
|
1132
|
+
scheduleName: null,
|
|
1133
|
+
startedAt: new Date().toISOString(),
|
|
1134
|
+
success: true,
|
|
1135
|
+
sessionId: "sid3",
|
|
1136
|
+
};
|
|
1137
|
+
});
|
|
1138
|
+
await manager.start();
|
|
1139
|
+
const { event, reply } = createMessageEvent();
|
|
1140
|
+
connector.emit("message", event);
|
|
1141
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
1142
|
+
const textCalls = reply.mock.calls.filter((call) => typeof call[0] === "string");
|
|
1143
|
+
expect(textCalls).toHaveLength(1);
|
|
1144
|
+
expect(textCalls[0][0]).toBe("Complete answer.");
|
|
1145
|
+
});
|
|
1146
|
+
// ---- Dedup: deduplicate same message.id ----
|
|
1147
|
+
it("deduplicates assistant messages by message.id", async () => {
|
|
1148
|
+
const { manager, connector } = buildManagerWithTrigger(async (...args) => {
|
|
1149
|
+
const options = args[2];
|
|
1150
|
+
if (options?.onMessage) {
|
|
1151
|
+
// Same message.id delivered twice (stop_reason not null) — second should be deduped
|
|
1152
|
+
await options.onMessage({
|
|
1153
|
+
type: "assistant",
|
|
1154
|
+
message: {
|
|
1155
|
+
id: "msg_1",
|
|
1156
|
+
stop_reason: "end_turn",
|
|
1157
|
+
content: [{ type: "text", text: "First delivery." }],
|
|
1158
|
+
},
|
|
1159
|
+
});
|
|
1160
|
+
await options.onMessage({
|
|
1161
|
+
type: "assistant",
|
|
1162
|
+
message: {
|
|
1163
|
+
id: "msg_1",
|
|
1164
|
+
stop_reason: "end_turn",
|
|
1165
|
+
content: [{ type: "text", text: "Duplicate delivery." }],
|
|
1166
|
+
},
|
|
1167
|
+
});
|
|
1168
|
+
await options.onMessage({ type: "result", result: "First delivery." });
|
|
1169
|
+
}
|
|
1170
|
+
return {
|
|
1171
|
+
jobId: "j4",
|
|
1172
|
+
agentName: "test-agent",
|
|
1173
|
+
scheduleName: null,
|
|
1174
|
+
startedAt: new Date().toISOString(),
|
|
1175
|
+
success: true,
|
|
1176
|
+
sessionId: "sid4",
|
|
1177
|
+
};
|
|
1178
|
+
});
|
|
1179
|
+
await manager.start();
|
|
1180
|
+
const { event, reply } = createMessageEvent();
|
|
1181
|
+
connector.emit("message", event);
|
|
1182
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
1183
|
+
const textCalls = reply.mock.calls.filter((call) => typeof call[0] === "string");
|
|
1184
|
+
expect(textCalls).toHaveLength(1);
|
|
1185
|
+
// Only the first delivery should be buffered (not the duplicate)
|
|
1186
|
+
expect(textCalls[0][0]).toBe("First delivery.");
|
|
1187
|
+
});
|
|
1188
|
+
// ---- Fallback when no output at all ----
|
|
1189
|
+
it("shows fallback embed when no messages are sent", async () => {
|
|
1190
|
+
const { manager, connector } = buildManagerWithTrigger(async (...args) => {
|
|
1191
|
+
const options = args[2];
|
|
1192
|
+
if (options?.onMessage) {
|
|
1193
|
+
// Only a result message with no text
|
|
1194
|
+
await options.onMessage({ type: "result", is_error: false });
|
|
1195
|
+
}
|
|
1196
|
+
return {
|
|
1197
|
+
jobId: "j5",
|
|
1198
|
+
agentName: "test-agent",
|
|
1199
|
+
scheduleName: null,
|
|
1200
|
+
startedAt: new Date().toISOString(),
|
|
1201
|
+
success: true,
|
|
1202
|
+
sessionId: "sid5",
|
|
1203
|
+
};
|
|
1204
|
+
});
|
|
1205
|
+
await manager.start();
|
|
1206
|
+
const { event, reply } = createMessageEvent();
|
|
1207
|
+
connector.emit("message", event);
|
|
1208
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
1209
|
+
// Should show the "Task completed" fallback embed
|
|
1210
|
+
const embedCalls = reply.mock.calls.filter((call) => typeof call[0] === "object" && call[0].embeds);
|
|
1211
|
+
expect(embedCalls.length).toBeGreaterThanOrEqual(1);
|
|
1212
|
+
const lastEmbed = embedCalls[embedCalls.length - 1][0];
|
|
1213
|
+
expect(lastEmbed.embeds[0].description).toContain("Task completed");
|
|
1214
|
+
});
|
|
1215
|
+
// ---- Error fallback ----
|
|
1216
|
+
it("shows error fallback when job fails with no output", async () => {
|
|
1217
|
+
const { manager, connector } = buildManagerWithTrigger(async () => {
|
|
1218
|
+
return {
|
|
1219
|
+
jobId: "j6",
|
|
1220
|
+
agentName: "test-agent",
|
|
1221
|
+
scheduleName: null,
|
|
1222
|
+
startedAt: new Date().toISOString(),
|
|
1223
|
+
success: false,
|
|
1224
|
+
error: { message: "Agent crashed" },
|
|
1225
|
+
errorDetails: { message: "Agent crashed" },
|
|
1226
|
+
};
|
|
1227
|
+
});
|
|
1228
|
+
await manager.start();
|
|
1229
|
+
const { event, reply } = createMessageEvent();
|
|
1230
|
+
connector.emit("message", event);
|
|
1231
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
1232
|
+
const embedCalls = reply.mock.calls.filter((call) => typeof call[0] === "object" && call[0].embeds);
|
|
1233
|
+
expect(embedCalls.length).toBeGreaterThanOrEqual(1);
|
|
1234
|
+
const lastEmbed = embedCalls[embedCalls.length - 1][0];
|
|
1235
|
+
expect(lastEmbed.embeds[0].description).toContain("Agent crashed");
|
|
1236
|
+
});
|
|
1237
|
+
// ---- Tool result embeds ----
|
|
1238
|
+
it("sends final answer without per-tool embed burst when tool results are enabled", async () => {
|
|
1239
|
+
const { manager, connector } = buildManagerWithTrigger(async (...args) => {
|
|
1240
|
+
const options = args[2];
|
|
1241
|
+
if (options?.onMessage) {
|
|
1242
|
+
// Assistant with tool_use
|
|
1243
|
+
await options.onMessage({
|
|
1244
|
+
type: "assistant",
|
|
1245
|
+
message: {
|
|
1246
|
+
id: "msg_1",
|
|
1247
|
+
stop_reason: "tool_use",
|
|
1248
|
+
content: [
|
|
1249
|
+
{ type: "text", text: "Let me check." },
|
|
1250
|
+
{ type: "tool_use", name: "Bash", id: "t1", input: { command: "ls" } },
|
|
1251
|
+
],
|
|
1252
|
+
},
|
|
1253
|
+
});
|
|
1254
|
+
// Tool result
|
|
1255
|
+
await options.onMessage({
|
|
1256
|
+
type: "user",
|
|
1257
|
+
message: {
|
|
1258
|
+
content: [{ type: "tool_result", tool_use_id: "t1", content: "file1.txt\nfile2.txt" }],
|
|
1259
|
+
},
|
|
1260
|
+
});
|
|
1261
|
+
// Final answer
|
|
1262
|
+
await options.onMessage({
|
|
1263
|
+
type: "assistant",
|
|
1264
|
+
message: {
|
|
1265
|
+
id: "msg_2",
|
|
1266
|
+
stop_reason: "end_turn",
|
|
1267
|
+
content: [{ type: "text", text: "Found 2 files." }],
|
|
1268
|
+
},
|
|
1269
|
+
});
|
|
1270
|
+
await options.onMessage({ type: "result", result: "Found 2 files." });
|
|
1271
|
+
}
|
|
1272
|
+
return {
|
|
1273
|
+
jobId: "j7",
|
|
1274
|
+
agentName: "test-agent",
|
|
1275
|
+
scheduleName: null,
|
|
1276
|
+
startedAt: new Date().toISOString(),
|
|
1277
|
+
success: true,
|
|
1278
|
+
sessionId: "sid7",
|
|
1279
|
+
};
|
|
1280
|
+
});
|
|
1281
|
+
await manager.start();
|
|
1282
|
+
const { event, reply } = createMessageEvent();
|
|
1283
|
+
connector.emit("message", event);
|
|
1284
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
1285
|
+
// Should have sent final text without a per-tool embed burst.
|
|
1286
|
+
const embedCalls = reply.mock.calls.filter((call) => typeof call[0] === "object" && call[0].embeds);
|
|
1287
|
+
const textCalls = reply.mock.calls.filter((call) => typeof call[0] === "string");
|
|
1288
|
+
expect(embedCalls).toHaveLength(0);
|
|
1289
|
+
expect(textCalls).toHaveLength(1);
|
|
1290
|
+
expect(textCalls[0][0]).toBe("Found 2 files.");
|
|
1291
|
+
});
|
|
1292
|
+
// ---- No system prompt injection (concise_mode removed) ----
|
|
1293
|
+
it("does not inject a systemPromptAppend", async () => {
|
|
1294
|
+
let capturedSystemPrompt;
|
|
1295
|
+
const { manager, connector } = buildManagerWithTrigger(async (...args) => {
|
|
1296
|
+
const options = args[2];
|
|
1297
|
+
capturedSystemPrompt = options?.systemPromptAppend;
|
|
1298
|
+
if (options?.onMessage) {
|
|
1299
|
+
await options.onMessage({
|
|
1300
|
+
type: "assistant",
|
|
1301
|
+
message: {
|
|
1302
|
+
id: "msg_1",
|
|
1303
|
+
stop_reason: "end_turn",
|
|
1304
|
+
content: [{ type: "text", text: "Done." }],
|
|
1305
|
+
},
|
|
1306
|
+
});
|
|
1307
|
+
await options.onMessage({ type: "result", result: "Done." });
|
|
1308
|
+
}
|
|
1309
|
+
return {
|
|
1310
|
+
jobId: "j8",
|
|
1311
|
+
agentName: "test-agent",
|
|
1312
|
+
scheduleName: null,
|
|
1313
|
+
startedAt: new Date().toISOString(),
|
|
1314
|
+
success: true,
|
|
1315
|
+
sessionId: "sid8",
|
|
1316
|
+
};
|
|
1317
|
+
});
|
|
1318
|
+
await manager.start();
|
|
1319
|
+
const { event } = createMessageEvent();
|
|
1320
|
+
connector.emit("message", event);
|
|
1321
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
1322
|
+
expect(capturedSystemPrompt).toBeUndefined();
|
|
1323
|
+
});
|
|
1324
|
+
// ---- Session persistence ----
|
|
1325
|
+
it("stores session ID after successful job", async () => {
|
|
1326
|
+
const { manager, connector } = buildManagerWithTrigger(async (...args) => {
|
|
1327
|
+
const options = args[2];
|
|
1328
|
+
if (options?.onMessage) {
|
|
1329
|
+
await options.onMessage({
|
|
1330
|
+
type: "assistant",
|
|
1331
|
+
message: {
|
|
1332
|
+
id: "msg_1",
|
|
1333
|
+
stop_reason: "end_turn",
|
|
1334
|
+
content: [{ type: "text", text: "OK." }],
|
|
1335
|
+
},
|
|
1336
|
+
});
|
|
1337
|
+
await options.onMessage({ type: "result", result: "OK." });
|
|
1338
|
+
}
|
|
1339
|
+
return {
|
|
1340
|
+
jobId: "j9",
|
|
1341
|
+
agentName: "test-agent",
|
|
1342
|
+
scheduleName: null,
|
|
1343
|
+
startedAt: new Date().toISOString(),
|
|
1344
|
+
success: true,
|
|
1345
|
+
sessionId: "sdk-session-42",
|
|
1346
|
+
};
|
|
666
1347
|
});
|
|
1348
|
+
await manager.start();
|
|
1349
|
+
const { event } = createMessageEvent();
|
|
1350
|
+
connector.emit("message", event);
|
|
1351
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
1352
|
+
expect(connector.sessionManager.setSession).toHaveBeenCalledWith("channel1", "sdk-session-42");
|
|
667
1353
|
});
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
1354
|
+
// ---- Mixed: tool-only turns + final text ----
|
|
1355
|
+
it("handles multi-turn tool usage with final text answer", async () => {
|
|
1356
|
+
const { manager, connector } = buildManagerWithTrigger(async (...args) => {
|
|
1357
|
+
const options = args[2];
|
|
1358
|
+
if (options?.onMessage) {
|
|
1359
|
+
// Turn 1: text + tool
|
|
1360
|
+
await options.onMessage({
|
|
1361
|
+
type: "assistant",
|
|
1362
|
+
message: {
|
|
1363
|
+
id: "msg_1",
|
|
1364
|
+
stop_reason: "tool_use",
|
|
1365
|
+
content: [
|
|
1366
|
+
{ type: "text", text: "Let me look." },
|
|
1367
|
+
{ type: "tool_use", name: "Read", id: "t1", input: { file_path: "/x.txt" } },
|
|
1368
|
+
],
|
|
1369
|
+
},
|
|
1370
|
+
});
|
|
1371
|
+
await options.onMessage({
|
|
1372
|
+
type: "user",
|
|
1373
|
+
message: {
|
|
1374
|
+
content: [{ type: "tool_result", tool_use_id: "t1", content: "contents" }],
|
|
1375
|
+
},
|
|
1376
|
+
});
|
|
1377
|
+
// Turn 2: tool-only (no text)
|
|
1378
|
+
await options.onMessage({
|
|
1379
|
+
type: "assistant",
|
|
1380
|
+
message: {
|
|
1381
|
+
id: "msg_2",
|
|
1382
|
+
stop_reason: "tool_use",
|
|
1383
|
+
content: [{ type: "tool_use", name: "Bash", id: "t2", input: { command: "wc -l" } }],
|
|
1384
|
+
},
|
|
1385
|
+
});
|
|
1386
|
+
await options.onMessage({
|
|
1387
|
+
type: "user",
|
|
1388
|
+
message: {
|
|
1389
|
+
content: [{ type: "tool_result", tool_use_id: "t2", content: "42" }],
|
|
1390
|
+
},
|
|
1391
|
+
});
|
|
1392
|
+
// Turn 3: final text
|
|
1393
|
+
await options.onMessage({
|
|
1394
|
+
type: "assistant",
|
|
1395
|
+
message: {
|
|
1396
|
+
id: "msg_3",
|
|
1397
|
+
stop_reason: "end_turn",
|
|
1398
|
+
content: [{ type: "text", text: "The file has 42 lines." }],
|
|
1399
|
+
},
|
|
1400
|
+
});
|
|
1401
|
+
await options.onMessage({ type: "result", result: "The file has 42 lines." });
|
|
1402
|
+
}
|
|
1403
|
+
return {
|
|
1404
|
+
jobId: "j10",
|
|
1405
|
+
agentName: "test-agent",
|
|
1406
|
+
scheduleName: null,
|
|
1407
|
+
startedAt: new Date().toISOString(),
|
|
1408
|
+
success: true,
|
|
1409
|
+
sessionId: "sid10",
|
|
1410
|
+
};
|
|
674
1411
|
});
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
1412
|
+
await manager.start();
|
|
1413
|
+
const { event, reply } = createMessageEvent();
|
|
1414
|
+
connector.emit("message", event);
|
|
1415
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
1416
|
+
// Only the answer turn (no tool_use) should be delivered
|
|
1417
|
+
const textCalls = reply.mock.calls.filter((call) => typeof call[0] === "string");
|
|
1418
|
+
expect(textCalls).toHaveLength(1);
|
|
1419
|
+
expect(textCalls[0][0]).toBe("The file has 42 lines.");
|
|
1420
|
+
});
|
|
1421
|
+
// ---- "all" mode: sends every assistant turn with text ----
|
|
1422
|
+
it("sends every assistant message immediately in 'all' mode", async () => {
|
|
1423
|
+
const { manager, connector } = buildManagerWithTrigger(async (...args) => {
|
|
1424
|
+
const options = args[2];
|
|
1425
|
+
if (options?.onMessage) {
|
|
1426
|
+
await options.onMessage({
|
|
1427
|
+
type: "assistant",
|
|
1428
|
+
message: {
|
|
1429
|
+
id: "msg_1",
|
|
1430
|
+
stop_reason: "end_turn",
|
|
1431
|
+
content: [{ type: "text", text: "First turn." }],
|
|
1432
|
+
},
|
|
1433
|
+
});
|
|
1434
|
+
await options.onMessage({
|
|
1435
|
+
type: "assistant",
|
|
1436
|
+
message: {
|
|
1437
|
+
id: "msg_2",
|
|
1438
|
+
stop_reason: "end_turn",
|
|
1439
|
+
content: [{ type: "text", text: "Second turn." }],
|
|
1440
|
+
},
|
|
1441
|
+
});
|
|
1442
|
+
await options.onMessage({ type: "result", result: "Second turn." });
|
|
1443
|
+
}
|
|
1444
|
+
return {
|
|
1445
|
+
jobId: "j11",
|
|
1446
|
+
agentName: "test-agent",
|
|
1447
|
+
scheduleName: null,
|
|
1448
|
+
startedAt: new Date().toISOString(),
|
|
1449
|
+
success: true,
|
|
1450
|
+
sessionId: "sid11",
|
|
1451
|
+
};
|
|
1452
|
+
},
|
|
1453
|
+
// Override: use "all" mode to send every turn
|
|
1454
|
+
{
|
|
1455
|
+
chat: {
|
|
1456
|
+
discord: {
|
|
1457
|
+
bot_token_env: "TEST_BOT_TOKEN",
|
|
1458
|
+
session_expiry_hours: 24,
|
|
1459
|
+
log_level: "standard",
|
|
1460
|
+
output: {
|
|
1461
|
+
tool_results: true,
|
|
1462
|
+
tool_result_max_length: 900,
|
|
1463
|
+
system_status: true,
|
|
1464
|
+
result_summary: false,
|
|
1465
|
+
typing_indicator: true,
|
|
1466
|
+
errors: true,
|
|
1467
|
+
acknowledge_emoji: "",
|
|
1468
|
+
assistant_messages: "all",
|
|
1469
|
+
progress_indicator: false,
|
|
1470
|
+
},
|
|
1471
|
+
guilds: [],
|
|
1472
|
+
},
|
|
1473
|
+
},
|
|
680
1474
|
});
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
1475
|
+
await manager.start();
|
|
1476
|
+
const { event, reply } = createMessageEvent();
|
|
1477
|
+
connector.emit("message", event);
|
|
1478
|
+
await new Promise((resolve) => setTimeout(resolve, 2500)); // wait for rate limiting
|
|
1479
|
+
// Both messages should be sent
|
|
1480
|
+
const textCalls = reply.mock.calls.filter((call) => typeof call[0] === "string");
|
|
1481
|
+
expect(textCalls).toHaveLength(2);
|
|
1482
|
+
expect(textCalls[0][0]).toBe("First turn.");
|
|
1483
|
+
expect(textCalls[1][0]).toBe("Second turn.");
|
|
1484
|
+
});
|
|
1485
|
+
it("sends reasoning turns (text + tool_use) in 'all' mode", async () => {
|
|
1486
|
+
const { manager, connector } = buildManagerWithTrigger(async (...args) => {
|
|
1487
|
+
const options = args[2];
|
|
1488
|
+
if (options?.onMessage) {
|
|
1489
|
+
// Reasoning turn: text + tool_use
|
|
1490
|
+
await options.onMessage({
|
|
1491
|
+
type: "assistant",
|
|
1492
|
+
message: {
|
|
1493
|
+
id: "msg_1",
|
|
1494
|
+
stop_reason: "tool_use",
|
|
1495
|
+
content: [
|
|
1496
|
+
{ type: "text", text: "Let me check the file." },
|
|
1497
|
+
{ type: "tool_use", name: "Read", id: "t1", input: { file_path: "/x.txt" } },
|
|
1498
|
+
],
|
|
1499
|
+
},
|
|
1500
|
+
});
|
|
1501
|
+
// Answer turn: text only
|
|
1502
|
+
await options.onMessage({
|
|
1503
|
+
type: "assistant",
|
|
1504
|
+
message: {
|
|
1505
|
+
id: "msg_2",
|
|
1506
|
+
stop_reason: "end_turn",
|
|
1507
|
+
content: [{ type: "text", text: "The file has 42 lines." }],
|
|
1508
|
+
},
|
|
1509
|
+
});
|
|
1510
|
+
await options.onMessage({ type: "result", result: "The file has 42 lines." });
|
|
1511
|
+
}
|
|
1512
|
+
return {
|
|
1513
|
+
jobId: "j12",
|
|
1514
|
+
agentName: "test-agent",
|
|
1515
|
+
scheduleName: null,
|
|
1516
|
+
startedAt: new Date().toISOString(),
|
|
1517
|
+
success: true,
|
|
1518
|
+
sessionId: "sid12",
|
|
1519
|
+
};
|
|
1520
|
+
},
|
|
1521
|
+
// Override: use "all" mode to send every turn including reasoning
|
|
1522
|
+
{
|
|
1523
|
+
chat: {
|
|
1524
|
+
discord: {
|
|
1525
|
+
bot_token_env: "TEST_BOT_TOKEN",
|
|
1526
|
+
session_expiry_hours: 24,
|
|
1527
|
+
log_level: "standard",
|
|
1528
|
+
output: {
|
|
1529
|
+
tool_results: true,
|
|
1530
|
+
tool_result_max_length: 900,
|
|
1531
|
+
system_status: true,
|
|
1532
|
+
result_summary: false,
|
|
1533
|
+
typing_indicator: true,
|
|
1534
|
+
errors: true,
|
|
1535
|
+
acknowledge_emoji: "",
|
|
1536
|
+
assistant_messages: "all",
|
|
1537
|
+
progress_indicator: false,
|
|
1538
|
+
},
|
|
1539
|
+
guilds: [],
|
|
1540
|
+
},
|
|
1541
|
+
},
|
|
690
1542
|
});
|
|
1543
|
+
await manager.start();
|
|
1544
|
+
const { event, reply } = createMessageEvent();
|
|
1545
|
+
connector.emit("message", event);
|
|
1546
|
+
await new Promise((resolve) => setTimeout(resolve, 2500)); // wait for rate limiting
|
|
1547
|
+
// Both reasoning and answer turns should be sent in "all" mode
|
|
1548
|
+
const textCalls = reply.mock.calls.filter((call) => typeof call[0] === "string");
|
|
1549
|
+
expect(textCalls).toHaveLength(2);
|
|
1550
|
+
expect(textCalls[0][0]).toBe("Let me check the file.");
|
|
1551
|
+
expect(textCalls[1][0]).toBe("The file has 42 lines.");
|
|
691
1552
|
});
|
|
692
1553
|
});
|
|
693
1554
|
// Message handling tests are skipped pending refactor to work with the new architecture
|
|
@@ -719,6 +1580,9 @@ describe.skip("DiscordManager message handling", () => {
|
|
|
719
1580
|
result_summary: false,
|
|
720
1581
|
typing_indicator: true,
|
|
721
1582
|
errors: true,
|
|
1583
|
+
acknowledge_emoji: "eyes",
|
|
1584
|
+
assistant_messages: "answers",
|
|
1585
|
+
progress_indicator: true,
|
|
722
1586
|
},
|
|
723
1587
|
guilds: [],
|
|
724
1588
|
}),
|
|
@@ -832,6 +1696,12 @@ describe.skip("DiscordManager message handling", () => {
|
|
|
832
1696
|
},
|
|
833
1697
|
reply: replyMock,
|
|
834
1698
|
startTyping: () => () => { },
|
|
1699
|
+
addReaction: vi.fn().mockResolvedValue(undefined),
|
|
1700
|
+
removeReaction: vi.fn().mockResolvedValue(undefined),
|
|
1701
|
+
replyWithRef: vi.fn().mockResolvedValue({
|
|
1702
|
+
edit: vi.fn().mockResolvedValue(undefined),
|
|
1703
|
+
delete: vi.fn().mockResolvedValue(undefined),
|
|
1704
|
+
}),
|
|
835
1705
|
};
|
|
836
1706
|
// Emit the message event
|
|
837
1707
|
mockConnector.emit("message", messageEvent);
|
|
@@ -873,6 +1743,9 @@ describe.skip("DiscordManager message handling", () => {
|
|
|
873
1743
|
result_summary: false,
|
|
874
1744
|
typing_indicator: true,
|
|
875
1745
|
errors: true,
|
|
1746
|
+
acknowledge_emoji: "eyes",
|
|
1747
|
+
assistant_messages: "answers",
|
|
1748
|
+
progress_indicator: true,
|
|
876
1749
|
},
|
|
877
1750
|
guilds: [],
|
|
878
1751
|
}),
|
|
@@ -958,6 +1831,12 @@ describe.skip("DiscordManager message handling", () => {
|
|
|
958
1831
|
},
|
|
959
1832
|
reply: replyMock,
|
|
960
1833
|
startTyping: () => () => { },
|
|
1834
|
+
addReaction: vi.fn().mockResolvedValue(undefined),
|
|
1835
|
+
removeReaction: vi.fn().mockResolvedValue(undefined),
|
|
1836
|
+
replyWithRef: vi.fn().mockResolvedValue({
|
|
1837
|
+
edit: vi.fn().mockResolvedValue(undefined),
|
|
1838
|
+
delete: vi.fn().mockResolvedValue(undefined),
|
|
1839
|
+
}),
|
|
961
1840
|
};
|
|
962
1841
|
// Emit the message event
|
|
963
1842
|
mockConnector.emit("message", messageEvent);
|
|
@@ -996,6 +1875,9 @@ describe.skip("DiscordManager message handling", () => {
|
|
|
996
1875
|
result_summary: false,
|
|
997
1876
|
typing_indicator: true,
|
|
998
1877
|
errors: true,
|
|
1878
|
+
acknowledge_emoji: "eyes",
|
|
1879
|
+
assistant_messages: "answers",
|
|
1880
|
+
progress_indicator: true,
|
|
999
1881
|
},
|
|
1000
1882
|
guilds: [],
|
|
1001
1883
|
}),
|
|
@@ -1081,6 +1963,12 @@ describe.skip("DiscordManager message handling", () => {
|
|
|
1081
1963
|
},
|
|
1082
1964
|
reply: replyMock,
|
|
1083
1965
|
startTyping: () => () => { },
|
|
1966
|
+
addReaction: vi.fn().mockResolvedValue(undefined),
|
|
1967
|
+
removeReaction: vi.fn().mockResolvedValue(undefined),
|
|
1968
|
+
replyWithRef: vi.fn().mockResolvedValue({
|
|
1969
|
+
edit: vi.fn().mockResolvedValue(undefined),
|
|
1970
|
+
delete: vi.fn().mockResolvedValue(undefined),
|
|
1971
|
+
}),
|
|
1084
1972
|
};
|
|
1085
1973
|
// Emit the message event
|
|
1086
1974
|
mockConnector.emit("message", messageEvent);
|
|
@@ -1150,6 +2038,9 @@ describe.skip("DiscordManager message handling", () => {
|
|
|
1150
2038
|
result_summary: false,
|
|
1151
2039
|
typing_indicator: true,
|
|
1152
2040
|
errors: true,
|
|
2041
|
+
acknowledge_emoji: "eyes",
|
|
2042
|
+
assistant_messages: "answers",
|
|
2043
|
+
progress_indicator: true,
|
|
1153
2044
|
},
|
|
1154
2045
|
guilds: [],
|
|
1155
2046
|
}),
|
|
@@ -1232,6 +2123,12 @@ describe.skip("DiscordManager message handling", () => {
|
|
|
1232
2123
|
},
|
|
1233
2124
|
reply: replyMock,
|
|
1234
2125
|
startTyping: () => () => { },
|
|
2126
|
+
addReaction: vi.fn().mockResolvedValue(undefined),
|
|
2127
|
+
removeReaction: vi.fn().mockResolvedValue(undefined),
|
|
2128
|
+
replyWithRef: vi.fn().mockResolvedValue({
|
|
2129
|
+
edit: vi.fn().mockResolvedValue(undefined),
|
|
2130
|
+
delete: vi.fn().mockResolvedValue(undefined),
|
|
2131
|
+
}),
|
|
1235
2132
|
};
|
|
1236
2133
|
mockConnector.emit("message", messageEvent);
|
|
1237
2134
|
// Wait for async processing (includes rate limiting delays)
|
|
@@ -1287,6 +2184,9 @@ describe.skip("DiscordManager message handling", () => {
|
|
|
1287
2184
|
result_summary: false,
|
|
1288
2185
|
typing_indicator: true,
|
|
1289
2186
|
errors: true,
|
|
2187
|
+
acknowledge_emoji: "eyes",
|
|
2188
|
+
assistant_messages: "answers",
|
|
2189
|
+
progress_indicator: true,
|
|
1290
2190
|
},
|
|
1291
2191
|
guilds: [],
|
|
1292
2192
|
}),
|
|
@@ -1369,6 +2269,12 @@ describe.skip("DiscordManager message handling", () => {
|
|
|
1369
2269
|
},
|
|
1370
2270
|
reply: replyMock,
|
|
1371
2271
|
startTyping: () => () => { },
|
|
2272
|
+
addReaction: vi.fn().mockResolvedValue(undefined),
|
|
2273
|
+
removeReaction: vi.fn().mockResolvedValue(undefined),
|
|
2274
|
+
replyWithRef: vi.fn().mockResolvedValue({
|
|
2275
|
+
edit: vi.fn().mockResolvedValue(undefined),
|
|
2276
|
+
delete: vi.fn().mockResolvedValue(undefined),
|
|
2277
|
+
}),
|
|
1372
2278
|
};
|
|
1373
2279
|
mockConnector.emit("message", messageEvent);
|
|
1374
2280
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
@@ -1469,6 +2375,12 @@ describe.skip("DiscordManager message handling", () => {
|
|
|
1469
2375
|
},
|
|
1470
2376
|
reply: replyMock,
|
|
1471
2377
|
startTyping: () => () => { },
|
|
2378
|
+
addReaction: vi.fn().mockResolvedValue(undefined),
|
|
2379
|
+
removeReaction: vi.fn().mockResolvedValue(undefined),
|
|
2380
|
+
replyWithRef: vi.fn().mockResolvedValue({
|
|
2381
|
+
edit: vi.fn().mockResolvedValue(undefined),
|
|
2382
|
+
delete: vi.fn().mockResolvedValue(undefined),
|
|
2383
|
+
}),
|
|
1472
2384
|
};
|
|
1473
2385
|
// Emit the message event - this will trigger handleMessage which will fail
|
|
1474
2386
|
// because agent is not in config, and then try to reply, and that also fails
|
|
@@ -1567,6 +2479,12 @@ describe.skip("DiscordManager message handling", () => {
|
|
|
1567
2479
|
},
|
|
1568
2480
|
reply: replyMock,
|
|
1569
2481
|
startTyping: () => () => { },
|
|
2482
|
+
addReaction: vi.fn().mockResolvedValue(undefined),
|
|
2483
|
+
removeReaction: vi.fn().mockResolvedValue(undefined),
|
|
2484
|
+
replyWithRef: vi.fn().mockResolvedValue({
|
|
2485
|
+
edit: vi.fn().mockResolvedValue(undefined),
|
|
2486
|
+
delete: vi.fn().mockResolvedValue(undefined),
|
|
2487
|
+
}),
|
|
1570
2488
|
};
|
|
1571
2489
|
// Emit the message event
|
|
1572
2490
|
mockConnector.emit("message", messageEvent);
|
|
@@ -1627,6 +2545,12 @@ describe.skip("DiscordManager message handling", () => {
|
|
|
1627
2545
|
},
|
|
1628
2546
|
reply: replyMock,
|
|
1629
2547
|
startTyping: () => () => { },
|
|
2548
|
+
addReaction: vi.fn().mockResolvedValue(undefined),
|
|
2549
|
+
removeReaction: vi.fn().mockResolvedValue(undefined),
|
|
2550
|
+
replyWithRef: vi.fn().mockResolvedValue({
|
|
2551
|
+
edit: vi.fn().mockResolvedValue(undefined),
|
|
2552
|
+
delete: vi.fn().mockResolvedValue(undefined),
|
|
2553
|
+
}),
|
|
1630
2554
|
};
|
|
1631
2555
|
// Emit the message event
|
|
1632
2556
|
mockConnector.emit("message", messageEvent);
|
|
@@ -1684,6 +2608,12 @@ describe.skip("DiscordManager message handling", () => {
|
|
|
1684
2608
|
},
|
|
1685
2609
|
reply: replyMock,
|
|
1686
2610
|
startTyping: () => () => { },
|
|
2611
|
+
addReaction: vi.fn().mockResolvedValue(undefined),
|
|
2612
|
+
removeReaction: vi.fn().mockResolvedValue(undefined),
|
|
2613
|
+
replyWithRef: vi.fn().mockResolvedValue({
|
|
2614
|
+
edit: vi.fn().mockResolvedValue(undefined),
|
|
2615
|
+
delete: vi.fn().mockResolvedValue(undefined),
|
|
2616
|
+
}),
|
|
1687
2617
|
};
|
|
1688
2618
|
// Emit the message event
|
|
1689
2619
|
mockConnector.emit("message", messageEvent);
|
|
@@ -2179,6 +3109,9 @@ describe.skip("DiscordManager session integration", () => {
|
|
|
2179
3109
|
result_summary: false,
|
|
2180
3110
|
typing_indicator: true,
|
|
2181
3111
|
errors: true,
|
|
3112
|
+
acknowledge_emoji: "eyes",
|
|
3113
|
+
assistant_messages: "answers",
|
|
3114
|
+
progress_indicator: true,
|
|
2182
3115
|
},
|
|
2183
3116
|
guilds: [],
|
|
2184
3117
|
}),
|
|
@@ -2259,6 +3192,12 @@ describe.skip("DiscordManager session integration", () => {
|
|
|
2259
3192
|
},
|
|
2260
3193
|
reply: replyMock,
|
|
2261
3194
|
startTyping: () => () => { },
|
|
3195
|
+
addReaction: vi.fn().mockResolvedValue(undefined),
|
|
3196
|
+
removeReaction: vi.fn().mockResolvedValue(undefined),
|
|
3197
|
+
replyWithRef: vi.fn().mockResolvedValue({
|
|
3198
|
+
edit: vi.fn().mockResolvedValue(undefined),
|
|
3199
|
+
delete: vi.fn().mockResolvedValue(undefined),
|
|
3200
|
+
}),
|
|
2262
3201
|
};
|
|
2263
3202
|
// Emit the message event
|
|
2264
3203
|
mockConnector.emit("message", messageEvent);
|
|
@@ -2316,6 +3255,12 @@ describe.skip("DiscordManager session integration", () => {
|
|
|
2316
3255
|
},
|
|
2317
3256
|
reply: replyMock,
|
|
2318
3257
|
startTyping: () => () => { },
|
|
3258
|
+
addReaction: vi.fn().mockResolvedValue(undefined),
|
|
3259
|
+
removeReaction: vi.fn().mockResolvedValue(undefined),
|
|
3260
|
+
replyWithRef: vi.fn().mockResolvedValue({
|
|
3261
|
+
edit: vi.fn().mockResolvedValue(undefined),
|
|
3262
|
+
delete: vi.fn().mockResolvedValue(undefined),
|
|
3263
|
+
}),
|
|
2319
3264
|
};
|
|
2320
3265
|
// Emit the message event
|
|
2321
3266
|
mockConnector.emit("message", messageEvent);
|
|
@@ -2374,6 +3319,12 @@ describe.skip("DiscordManager session integration", () => {
|
|
|
2374
3319
|
},
|
|
2375
3320
|
reply: replyMock,
|
|
2376
3321
|
startTyping: () => () => { },
|
|
3322
|
+
addReaction: vi.fn().mockResolvedValue(undefined),
|
|
3323
|
+
removeReaction: vi.fn().mockResolvedValue(undefined),
|
|
3324
|
+
replyWithRef: vi.fn().mockResolvedValue({
|
|
3325
|
+
edit: vi.fn().mockResolvedValue(undefined),
|
|
3326
|
+
delete: vi.fn().mockResolvedValue(undefined),
|
|
3327
|
+
}),
|
|
2377
3328
|
};
|
|
2378
3329
|
// Emit the message event
|
|
2379
3330
|
mockConnector.emit("message", messageEvent);
|
|
@@ -2434,6 +3385,12 @@ describe.skip("DiscordManager session integration", () => {
|
|
|
2434
3385
|
},
|
|
2435
3386
|
reply: replyMock,
|
|
2436
3387
|
startTyping: () => () => { },
|
|
3388
|
+
addReaction: vi.fn().mockResolvedValue(undefined),
|
|
3389
|
+
removeReaction: vi.fn().mockResolvedValue(undefined),
|
|
3390
|
+
replyWithRef: vi.fn().mockResolvedValue({
|
|
3391
|
+
edit: vi.fn().mockResolvedValue(undefined),
|
|
3392
|
+
delete: vi.fn().mockResolvedValue(undefined),
|
|
3393
|
+
}),
|
|
2437
3394
|
};
|
|
2438
3395
|
// Emit the message event
|
|
2439
3396
|
mockConnector.emit("message", messageEvent);
|
|
@@ -2705,6 +3662,9 @@ describe.skip("DiscordManager lifecycle", () => {
|
|
|
2705
3662
|
result_summary: false,
|
|
2706
3663
|
typing_indicator: true,
|
|
2707
3664
|
errors: true,
|
|
3665
|
+
acknowledge_emoji: "eyes",
|
|
3666
|
+
assistant_messages: "answers",
|
|
3667
|
+
progress_indicator: true,
|
|
2708
3668
|
},
|
|
2709
3669
|
guilds: [],
|
|
2710
3670
|
}),
|
|
@@ -2786,6 +3746,12 @@ describe.skip("DiscordManager lifecycle", () => {
|
|
|
2786
3746
|
},
|
|
2787
3747
|
reply: replyMock,
|
|
2788
3748
|
startTyping: () => () => { },
|
|
3749
|
+
addReaction: vi.fn().mockResolvedValue(undefined),
|
|
3750
|
+
removeReaction: vi.fn().mockResolvedValue(undefined),
|
|
3751
|
+
replyWithRef: vi.fn().mockResolvedValue({
|
|
3752
|
+
edit: vi.fn().mockResolvedValue(undefined),
|
|
3753
|
+
delete: vi.fn().mockResolvedValue(undefined),
|
|
3754
|
+
}),
|
|
2789
3755
|
};
|
|
2790
3756
|
// Emit the message event
|
|
2791
3757
|
mockConnector.emit("message", messageEvent);
|
|
@@ -2826,6 +3792,9 @@ describe.skip("DiscordManager lifecycle", () => {
|
|
|
2826
3792
|
result_summary: false,
|
|
2827
3793
|
typing_indicator: true,
|
|
2828
3794
|
errors: true,
|
|
3795
|
+
acknowledge_emoji: "eyes",
|
|
3796
|
+
assistant_messages: "answers",
|
|
3797
|
+
progress_indicator: true,
|
|
2829
3798
|
},
|
|
2830
3799
|
guilds: [],
|
|
2831
3800
|
}),
|
|
@@ -2907,6 +3876,12 @@ describe.skip("DiscordManager lifecycle", () => {
|
|
|
2907
3876
|
},
|
|
2908
3877
|
reply: replyMock,
|
|
2909
3878
|
startTyping: () => () => { },
|
|
3879
|
+
addReaction: vi.fn().mockResolvedValue(undefined),
|
|
3880
|
+
removeReaction: vi.fn().mockResolvedValue(undefined),
|
|
3881
|
+
replyWithRef: vi.fn().mockResolvedValue({
|
|
3882
|
+
edit: vi.fn().mockResolvedValue(undefined),
|
|
3883
|
+
delete: vi.fn().mockResolvedValue(undefined),
|
|
3884
|
+
}),
|
|
2910
3885
|
};
|
|
2911
3886
|
// Emit the message event
|
|
2912
3887
|
mockConnector.emit("message", messageEvent);
|
|
@@ -3075,6 +4050,12 @@ describe.skip("DiscordManager lifecycle", () => {
|
|
|
3075
4050
|
},
|
|
3076
4051
|
reply: replyMock,
|
|
3077
4052
|
startTyping: () => () => { },
|
|
4053
|
+
addReaction: vi.fn().mockResolvedValue(undefined),
|
|
4054
|
+
removeReaction: vi.fn().mockResolvedValue(undefined),
|
|
4055
|
+
replyWithRef: vi.fn().mockResolvedValue({
|
|
4056
|
+
edit: vi.fn().mockResolvedValue(undefined),
|
|
4057
|
+
delete: vi.fn().mockResolvedValue(undefined),
|
|
4058
|
+
}),
|
|
3078
4059
|
};
|
|
3079
4060
|
// Emit the message event
|
|
3080
4061
|
mockConnector.emit("message", messageEvent);
|
|
@@ -3113,6 +4094,9 @@ describe.skip("DiscordManager lifecycle", () => {
|
|
|
3113
4094
|
result_summary: false,
|
|
3114
4095
|
typing_indicator: true,
|
|
3115
4096
|
errors: true,
|
|
4097
|
+
acknowledge_emoji: "eyes",
|
|
4098
|
+
assistant_messages: "answers",
|
|
4099
|
+
progress_indicator: true,
|
|
3116
4100
|
},
|
|
3117
4101
|
guilds: [],
|
|
3118
4102
|
}),
|
|
@@ -3194,6 +4178,12 @@ describe.skip("DiscordManager lifecycle", () => {
|
|
|
3194
4178
|
},
|
|
3195
4179
|
reply: replyMock,
|
|
3196
4180
|
startTyping: () => () => { },
|
|
4181
|
+
addReaction: vi.fn().mockResolvedValue(undefined),
|
|
4182
|
+
removeReaction: vi.fn().mockResolvedValue(undefined),
|
|
4183
|
+
replyWithRef: vi.fn().mockResolvedValue({
|
|
4184
|
+
edit: vi.fn().mockResolvedValue(undefined),
|
|
4185
|
+
delete: vi.fn().mockResolvedValue(undefined),
|
|
4186
|
+
}),
|
|
3197
4187
|
};
|
|
3198
4188
|
// Emit the message event
|
|
3199
4189
|
mockConnector.emit("message", messageEvent);
|
|
@@ -3270,6 +4260,9 @@ describe.skip("DiscordManager output configuration", () => {
|
|
|
3270
4260
|
result_summary: false,
|
|
3271
4261
|
typing_indicator: true,
|
|
3272
4262
|
errors: true,
|
|
4263
|
+
acknowledge_emoji: "eyes",
|
|
4264
|
+
assistant_messages: "answers",
|
|
4265
|
+
progress_indicator: true,
|
|
3273
4266
|
},
|
|
3274
4267
|
guilds: [],
|
|
3275
4268
|
},
|
|
@@ -3353,6 +4346,12 @@ describe.skip("DiscordManager output configuration", () => {
|
|
|
3353
4346
|
},
|
|
3354
4347
|
reply: replyMock,
|
|
3355
4348
|
startTyping: () => () => { },
|
|
4349
|
+
addReaction: vi.fn().mockResolvedValue(undefined),
|
|
4350
|
+
removeReaction: vi.fn().mockResolvedValue(undefined),
|
|
4351
|
+
replyWithRef: vi.fn().mockResolvedValue({
|
|
4352
|
+
edit: vi.fn().mockResolvedValue(undefined),
|
|
4353
|
+
delete: vi.fn().mockResolvedValue(undefined),
|
|
4354
|
+
}),
|
|
3356
4355
|
};
|
|
3357
4356
|
mockConnector.emit("message", messageEvent);
|
|
3358
4357
|
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
@@ -3403,6 +4402,9 @@ describe.skip("DiscordManager output configuration", () => {
|
|
|
3403
4402
|
result_summary: false,
|
|
3404
4403
|
typing_indicator: true,
|
|
3405
4404
|
errors: true,
|
|
4405
|
+
acknowledge_emoji: "eyes",
|
|
4406
|
+
assistant_messages: "answers",
|
|
4407
|
+
progress_indicator: true,
|
|
3406
4408
|
},
|
|
3407
4409
|
guilds: [],
|
|
3408
4410
|
},
|
|
@@ -3486,6 +4488,12 @@ describe.skip("DiscordManager output configuration", () => {
|
|
|
3486
4488
|
},
|
|
3487
4489
|
reply: replyMock,
|
|
3488
4490
|
startTyping: () => () => { },
|
|
4491
|
+
addReaction: vi.fn().mockResolvedValue(undefined),
|
|
4492
|
+
removeReaction: vi.fn().mockResolvedValue(undefined),
|
|
4493
|
+
replyWithRef: vi.fn().mockResolvedValue({
|
|
4494
|
+
edit: vi.fn().mockResolvedValue(undefined),
|
|
4495
|
+
delete: vi.fn().mockResolvedValue(undefined),
|
|
4496
|
+
}),
|
|
3489
4497
|
};
|
|
3490
4498
|
mockConnector.emit("message", messageEvent);
|
|
3491
4499
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
@@ -3535,6 +4543,9 @@ describe.skip("DiscordManager output configuration", () => {
|
|
|
3535
4543
|
result_summary: false,
|
|
3536
4544
|
typing_indicator: true,
|
|
3537
4545
|
errors: true,
|
|
4546
|
+
acknowledge_emoji: "eyes",
|
|
4547
|
+
assistant_messages: "answers",
|
|
4548
|
+
progress_indicator: true,
|
|
3538
4549
|
},
|
|
3539
4550
|
guilds: [],
|
|
3540
4551
|
},
|
|
@@ -3618,6 +4629,12 @@ describe.skip("DiscordManager output configuration", () => {
|
|
|
3618
4629
|
},
|
|
3619
4630
|
reply: replyMock,
|
|
3620
4631
|
startTyping: () => () => { },
|
|
4632
|
+
addReaction: vi.fn().mockResolvedValue(undefined),
|
|
4633
|
+
removeReaction: vi.fn().mockResolvedValue(undefined),
|
|
4634
|
+
replyWithRef: vi.fn().mockResolvedValue({
|
|
4635
|
+
edit: vi.fn().mockResolvedValue(undefined),
|
|
4636
|
+
delete: vi.fn().mockResolvedValue(undefined),
|
|
4637
|
+
}),
|
|
3621
4638
|
};
|
|
3622
4639
|
mockConnector.emit("message", messageEvent);
|
|
3623
4640
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
@@ -3668,6 +4685,9 @@ describe.skip("DiscordManager output configuration", () => {
|
|
|
3668
4685
|
result_summary: true,
|
|
3669
4686
|
typing_indicator: true,
|
|
3670
4687
|
errors: true,
|
|
4688
|
+
acknowledge_emoji: "eyes",
|
|
4689
|
+
assistant_messages: "answers",
|
|
4690
|
+
progress_indicator: true,
|
|
3671
4691
|
},
|
|
3672
4692
|
guilds: [],
|
|
3673
4693
|
},
|
|
@@ -3751,6 +4771,12 @@ describe.skip("DiscordManager output configuration", () => {
|
|
|
3751
4771
|
},
|
|
3752
4772
|
reply: replyMock,
|
|
3753
4773
|
startTyping: () => () => { },
|
|
4774
|
+
addReaction: vi.fn().mockResolvedValue(undefined),
|
|
4775
|
+
removeReaction: vi.fn().mockResolvedValue(undefined),
|
|
4776
|
+
replyWithRef: vi.fn().mockResolvedValue({
|
|
4777
|
+
edit: vi.fn().mockResolvedValue(undefined),
|
|
4778
|
+
delete: vi.fn().mockResolvedValue(undefined),
|
|
4779
|
+
}),
|
|
3754
4780
|
};
|
|
3755
4781
|
mockConnector.emit("message", messageEvent);
|
|
3756
4782
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
@@ -3804,6 +4830,9 @@ describe.skip("DiscordManager output configuration", () => {
|
|
|
3804
4830
|
result_summary: false,
|
|
3805
4831
|
typing_indicator: true,
|
|
3806
4832
|
errors: true,
|
|
4833
|
+
acknowledge_emoji: "eyes",
|
|
4834
|
+
assistant_messages: "answers",
|
|
4835
|
+
progress_indicator: true,
|
|
3807
4836
|
},
|
|
3808
4837
|
guilds: [],
|
|
3809
4838
|
},
|
|
@@ -3887,6 +4916,12 @@ describe.skip("DiscordManager output configuration", () => {
|
|
|
3887
4916
|
},
|
|
3888
4917
|
reply: replyMock,
|
|
3889
4918
|
startTyping: () => () => { },
|
|
4919
|
+
addReaction: vi.fn().mockResolvedValue(undefined),
|
|
4920
|
+
removeReaction: vi.fn().mockResolvedValue(undefined),
|
|
4921
|
+
replyWithRef: vi.fn().mockResolvedValue({
|
|
4922
|
+
edit: vi.fn().mockResolvedValue(undefined),
|
|
4923
|
+
delete: vi.fn().mockResolvedValue(undefined),
|
|
4924
|
+
}),
|
|
3890
4925
|
};
|
|
3891
4926
|
mockConnector.emit("message", messageEvent);
|
|
3892
4927
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
@@ -3935,6 +4970,9 @@ describe.skip("DiscordManager output configuration", () => {
|
|
|
3935
4970
|
result_summary: false,
|
|
3936
4971
|
typing_indicator: true,
|
|
3937
4972
|
errors: false,
|
|
4973
|
+
acknowledge_emoji: "eyes",
|
|
4974
|
+
assistant_messages: "answers",
|
|
4975
|
+
progress_indicator: true,
|
|
3938
4976
|
},
|
|
3939
4977
|
guilds: [],
|
|
3940
4978
|
},
|
|
@@ -4018,6 +5056,12 @@ describe.skip("DiscordManager output configuration", () => {
|
|
|
4018
5056
|
},
|
|
4019
5057
|
reply: replyMock,
|
|
4020
5058
|
startTyping: () => () => { },
|
|
5059
|
+
addReaction: vi.fn().mockResolvedValue(undefined),
|
|
5060
|
+
removeReaction: vi.fn().mockResolvedValue(undefined),
|
|
5061
|
+
replyWithRef: vi.fn().mockResolvedValue({
|
|
5062
|
+
edit: vi.fn().mockResolvedValue(undefined),
|
|
5063
|
+
delete: vi.fn().mockResolvedValue(undefined),
|
|
5064
|
+
}),
|
|
4021
5065
|
};
|
|
4022
5066
|
mockConnector.emit("message", messageEvent);
|
|
4023
5067
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
@@ -4028,6 +5072,127 @@ describe.skip("DiscordManager output configuration", () => {
|
|
|
4028
5072
|
});
|
|
4029
5073
|
expect(embedCalls.length).toBe(0);
|
|
4030
5074
|
}, 10000);
|
|
5075
|
+
describe("file sender MCP injection", () => {
|
|
5076
|
+
it("passes injectedMcpServers to trigger when agent has working_directory", async () => {
|
|
5077
|
+
const triggerMock = vi.fn().mockResolvedValue({
|
|
5078
|
+
jobId: "job-file",
|
|
5079
|
+
agentName: "test",
|
|
5080
|
+
scheduleName: null,
|
|
5081
|
+
startedAt: new Date().toISOString(),
|
|
5082
|
+
success: true,
|
|
5083
|
+
});
|
|
5084
|
+
const agentWithWorkDir = {
|
|
5085
|
+
...createDiscordAgent("file-agent", {
|
|
5086
|
+
bot_token_env: "TEST_BOT_TOKEN",
|
|
5087
|
+
session_expiry_hours: 24,
|
|
5088
|
+
log_level: "standard",
|
|
5089
|
+
output: {
|
|
5090
|
+
tool_results: true,
|
|
5091
|
+
tool_result_max_length: 900,
|
|
5092
|
+
system_status: true,
|
|
5093
|
+
result_summary: false,
|
|
5094
|
+
typing_indicator: true,
|
|
5095
|
+
errors: true,
|
|
5096
|
+
acknowledge_emoji: "",
|
|
5097
|
+
assistant_messages: "answers",
|
|
5098
|
+
progress_indicator: true,
|
|
5099
|
+
},
|
|
5100
|
+
guilds: [],
|
|
5101
|
+
}),
|
|
5102
|
+
working_directory: "/tmp/test-workspace",
|
|
5103
|
+
};
|
|
5104
|
+
const config = {
|
|
5105
|
+
fleet: { name: "test-fleet" },
|
|
5106
|
+
agents: [agentWithWorkDir],
|
|
5107
|
+
configPath: "/test/herdctl.yaml",
|
|
5108
|
+
configDir: "/test",
|
|
5109
|
+
};
|
|
5110
|
+
const mockContext = {
|
|
5111
|
+
getConfig: () => config,
|
|
5112
|
+
getStateDir: () => "/tmp/test-state",
|
|
5113
|
+
getStateDirInfo: () => null,
|
|
5114
|
+
getLogger: () => mockLogger,
|
|
5115
|
+
getScheduler: () => null,
|
|
5116
|
+
getStatus: () => "running",
|
|
5117
|
+
getInitializedAt: () => "2024-01-01T00:00:00.000Z",
|
|
5118
|
+
getStartedAt: () => "2024-01-01T00:00:01.000Z",
|
|
5119
|
+
getStoppedAt: () => null,
|
|
5120
|
+
getLastError: () => null,
|
|
5121
|
+
getCheckInterval: () => 1000,
|
|
5122
|
+
emit: vi.fn(),
|
|
5123
|
+
getEmitter: () => new EventEmitter(),
|
|
5124
|
+
trigger: triggerMock,
|
|
5125
|
+
};
|
|
5126
|
+
const manager = new DiscordManager(mockContext);
|
|
5127
|
+
const mockConnector = new EventEmitter();
|
|
5128
|
+
mockConnector.connect = vi.fn().mockResolvedValue(undefined);
|
|
5129
|
+
mockConnector.disconnect = vi.fn().mockResolvedValue(undefined);
|
|
5130
|
+
mockConnector.isConnected = vi.fn().mockReturnValue(true);
|
|
5131
|
+
mockConnector.getState = vi.fn().mockReturnValue({
|
|
5132
|
+
status: "connected",
|
|
5133
|
+
connectedAt: "2024-01-01T00:00:00.000Z",
|
|
5134
|
+
disconnectedAt: null,
|
|
5135
|
+
reconnectAttempts: 0,
|
|
5136
|
+
lastError: null,
|
|
5137
|
+
botUser: { id: "bot1", username: "TestBot", discriminator: "0000" },
|
|
5138
|
+
rateLimits: {
|
|
5139
|
+
totalCount: 0,
|
|
5140
|
+
lastRateLimitAt: null,
|
|
5141
|
+
isRateLimited: false,
|
|
5142
|
+
currentResetTime: 0,
|
|
5143
|
+
},
|
|
5144
|
+
messageStats: { received: 0, sent: 0, ignored: 0 },
|
|
5145
|
+
});
|
|
5146
|
+
mockConnector.uploadFile = vi.fn().mockResolvedValue({ fileId: "file-123" });
|
|
5147
|
+
mockConnector.agentName = "file-agent";
|
|
5148
|
+
mockConnector.sessionManager = {
|
|
5149
|
+
getSession: vi.fn().mockResolvedValue(null),
|
|
5150
|
+
setSession: vi.fn().mockResolvedValue(undefined),
|
|
5151
|
+
touchSession: vi.fn().mockResolvedValue(undefined),
|
|
5152
|
+
getActiveSessionCount: vi.fn().mockResolvedValue(0),
|
|
5153
|
+
};
|
|
5154
|
+
// @ts-expect-error - accessing private property for testing
|
|
5155
|
+
manager.connectors.set("file-agent", mockConnector);
|
|
5156
|
+
// @ts-expect-error - accessing private property for testing
|
|
5157
|
+
manager.initialized = true;
|
|
5158
|
+
await manager.start();
|
|
5159
|
+
const messageEvent = {
|
|
5160
|
+
agentName: "file-agent",
|
|
5161
|
+
prompt: "Send me the file",
|
|
5162
|
+
context: { messages: [], wasMentioned: true, prompt: "Send me the file" },
|
|
5163
|
+
metadata: {
|
|
5164
|
+
guildId: "guild1",
|
|
5165
|
+
channelId: "channel1",
|
|
5166
|
+
messageId: "msg1",
|
|
5167
|
+
userId: "user1",
|
|
5168
|
+
username: "TestUser",
|
|
5169
|
+
wasMentioned: true,
|
|
5170
|
+
mode: "mention",
|
|
5171
|
+
},
|
|
5172
|
+
reply: vi.fn().mockResolvedValue(undefined),
|
|
5173
|
+
startTyping: () => () => { },
|
|
5174
|
+
addReaction: vi.fn().mockResolvedValue(undefined),
|
|
5175
|
+
removeReaction: vi.fn().mockResolvedValue(undefined),
|
|
5176
|
+
replyWithRef: vi.fn().mockResolvedValue({
|
|
5177
|
+
edit: vi.fn().mockResolvedValue(undefined),
|
|
5178
|
+
delete: vi.fn().mockResolvedValue(undefined),
|
|
5179
|
+
}),
|
|
5180
|
+
};
|
|
5181
|
+
mockConnector.emit("message", messageEvent);
|
|
5182
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
5183
|
+
// Verify trigger was called with injectedMcpServers
|
|
5184
|
+
expect(triggerMock).toHaveBeenCalledWith("file-agent", undefined, expect.objectContaining({
|
|
5185
|
+
injectedMcpServers: expect.objectContaining({
|
|
5186
|
+
"herdctl-file-sender": expect.objectContaining({
|
|
5187
|
+
name: "herdctl-file-sender",
|
|
5188
|
+
tools: expect.arrayContaining([
|
|
5189
|
+
expect.objectContaining({ name: "herdctl_send_file" }),
|
|
5190
|
+
]),
|
|
5191
|
+
}),
|
|
5192
|
+
}),
|
|
5193
|
+
}));
|
|
5194
|
+
}, 10000);
|
|
5195
|
+
});
|
|
4031
5196
|
describe("buildToolEmbed with custom maxOutputChars", () => {
|
|
4032
5197
|
it("respects custom maxOutputChars parameter", () => {
|
|
4033
5198
|
const ctx = createMockContext(null);
|