@herdctl/core 4.1.1 → 4.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.
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Tests for SlackManager
3
3
  *
4
- * Tests the SlackManager class which manages a single Slack connector
5
- * shared across agents with chat.slack configured.
4
+ * Tests the SlackManager class which manages Slack connectors for agents
5
+ * with chat.slack configured (one connector per agent).
6
6
  *
7
7
  * Since @herdctl/slack is not a dependency of @herdctl/core, we mock the
8
8
  * dynamic import to test the full initialization and lifecycle paths.
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Tests for SlackManager
3
3
  *
4
- * Tests the SlackManager class which manages a single Slack connector
5
- * shared across agents with chat.slack configured.
4
+ * Tests the SlackManager class which manages Slack connectors for agents
5
+ * with chat.slack configured (one connector per agent).
6
6
  *
7
7
  * Since @herdctl/slack is not a dependency of @herdctl/core, we mock the
8
8
  * dynamic import to test the full initialization and lifecycle paths.
@@ -76,8 +76,10 @@ function createConfigWithAgents(...agents) {
76
76
  // ---------------------------------------------------------------------------
77
77
  // Mock SlackConnector and SessionManager
78
78
  // ---------------------------------------------------------------------------
79
- function createMockConnector() {
79
+ function createMockConnector(agentName, sessionManager) {
80
80
  const connector = new EventEmitter();
81
+ connector.agentName = agentName;
82
+ connector.sessionManager = sessionManager;
81
83
  connector.connect = vi.fn().mockResolvedValue(undefined);
82
84
  connector.disconnect = vi.fn().mockResolvedValue(undefined);
83
85
  connector.isConnected = vi.fn().mockReturnValue(false);
@@ -90,6 +92,7 @@ function createMockConnector() {
90
92
  botUser: null,
91
93
  messageStats: { received: 0, sent: 0, ignored: 0 },
92
94
  });
95
+ connector.uploadFile = vi.fn().mockResolvedValue({ fileId: "file-123" });
93
96
  return connector;
94
97
  }
95
98
  function createMockSessionManager(agentName) {
@@ -146,7 +149,7 @@ describe("SlackManager (no @herdctl/slack)", () => {
146
149
  const ctx = createMockContext(config);
147
150
  const manager = new SlackManager(ctx);
148
151
  await manager.initialize();
149
- expect(mockLogger.debug).toHaveBeenCalledWith("@herdctl/slack not installed, skipping Slack connector");
152
+ expect(mockLogger.debug).toHaveBeenCalledWith("@herdctl/slack not installed, skipping Slack connectors");
150
153
  });
151
154
  it("skips when @herdctl/slack is not installed (with slack agents)", async () => {
152
155
  const SlackManager = await getSlackManager();
@@ -154,7 +157,7 @@ describe("SlackManager (no @herdctl/slack)", () => {
154
157
  const ctx = createMockContext(config);
155
158
  const manager = new SlackManager(ctx);
156
159
  await manager.initialize();
157
- expect(mockLogger.debug).toHaveBeenCalledWith("@herdctl/slack not installed, skipping Slack connector");
160
+ expect(mockLogger.debug).toHaveBeenCalledWith("@herdctl/slack not installed, skipping Slack connectors");
158
161
  });
159
162
  it("allows retry when no config (initialized not set)", async () => {
160
163
  const SlackManager = await getSlackManager();
@@ -173,7 +176,7 @@ describe("SlackManager (no @herdctl/slack)", () => {
173
176
  const manager = new SlackManager(ctx);
174
177
  await manager.initialize();
175
178
  await manager.start();
176
- expect(mockLogger.debug).toHaveBeenCalledWith("No Slack connector to start");
179
+ expect(mockLogger.debug).toHaveBeenCalledWith("No Slack connectors to start");
177
180
  });
178
181
  });
179
182
  describe("stop", () => {
@@ -183,7 +186,7 @@ describe("SlackManager (no @herdctl/slack)", () => {
183
186
  const manager = new SlackManager(ctx);
184
187
  await manager.initialize();
185
188
  await manager.stop();
186
- expect(mockLogger.debug).toHaveBeenCalledWith("No Slack connector to stop");
189
+ expect(mockLogger.debug).toHaveBeenCalledWith("No Slack connectors to stop");
187
190
  });
188
191
  });
189
192
  describe("hasAgent", () => {
@@ -195,35 +198,35 @@ describe("SlackManager (no @herdctl/slack)", () => {
195
198
  });
196
199
  });
197
200
  describe("getState", () => {
198
- it("returns null when no connector", async () => {
201
+ it("returns null when no connector for agent", async () => {
199
202
  const SlackManager = await getSlackManager();
200
203
  const ctx = createMockContext(null);
201
204
  const manager = new SlackManager(ctx);
202
- expect(manager.getState()).toBeNull();
205
+ expect(manager.getState("test-agent")).toBeNull();
203
206
  });
204
207
  });
205
- describe("isConnected", () => {
206
- it("returns false when no connector", async () => {
208
+ describe("getConnectedCount", () => {
209
+ it("returns 0 when no connectors", async () => {
207
210
  const SlackManager = await getSlackManager();
208
211
  const ctx = createMockContext(null);
209
212
  const manager = new SlackManager(ctx);
210
- expect(manager.isConnected()).toBe(false);
213
+ expect(manager.getConnectedCount()).toBe(0);
211
214
  });
212
215
  });
213
216
  describe("getConnector", () => {
214
- it("returns null when no connector", async () => {
217
+ it("returns undefined when no connector for agent", async () => {
215
218
  const SlackManager = await getSlackManager();
216
219
  const ctx = createMockContext(null);
217
220
  const manager = new SlackManager(ctx);
218
- expect(manager.getConnector()).toBeNull();
221
+ expect(manager.getConnector("test-agent")).toBeUndefined();
219
222
  });
220
223
  });
221
- describe("getChannelAgentMap", () => {
222
- it("returns empty map when not initialized", async () => {
224
+ describe("getConnectorNames", () => {
225
+ it("returns empty array when not initialized", async () => {
223
226
  const SlackManager = await getSlackManager();
224
227
  const ctx = createMockContext(null);
225
228
  const manager = new SlackManager(ctx);
226
- expect(manager.getChannelAgentMap().size).toBe(0);
229
+ expect(manager.getConnectorNames()).toEqual([]);
227
230
  });
228
231
  });
229
232
  describe("splitResponse", () => {
@@ -298,7 +301,8 @@ describe("SlackManager (no @herdctl/slack)", () => {
298
301
  // Tests – With Mocked @herdctl/slack (full initialization paths)
299
302
  // ---------------------------------------------------------------------------
300
303
  describe("SlackManager (mocked @herdctl/slack)", () => {
301
- let mockConnector;
304
+ let mockConnectors;
305
+ let mockSessionManagers;
302
306
  let MockSlackConnector;
303
307
  let MockSessionManager;
304
308
  let originalEnv;
@@ -309,14 +313,19 @@ describe("SlackManager (mocked @herdctl/slack)", () => {
309
313
  // Set required env vars
310
314
  process.env.SLACK_BOT_TOKEN = "xoxb-test-bot-token";
311
315
  process.env.SLACK_APP_TOKEN = "xapp-test-app-token";
312
- // Create mock implementations
313
- mockConnector = createMockConnector();
316
+ // Create mock implementations - per-agent connectors
317
+ mockConnectors = new Map();
318
+ mockSessionManagers = new Map();
314
319
  // Must use function expressions (not arrows) so they work with `new`
315
- MockSlackConnector = vi.fn().mockImplementation(function () {
316
- return mockConnector;
317
- });
318
320
  MockSessionManager = vi.fn().mockImplementation(function (opts) {
319
- return createMockSessionManager(opts.agentName);
321
+ const sessionMgr = createMockSessionManager(opts.agentName);
322
+ mockSessionManagers.set(opts.agentName, sessionMgr);
323
+ return sessionMgr;
324
+ });
325
+ MockSlackConnector = vi.fn().mockImplementation(function (opts) {
326
+ const connector = createMockConnector(opts.agentName, opts.sessionManager);
327
+ mockConnectors.set(opts.agentName, connector);
328
+ return connector;
320
329
  });
321
330
  });
322
331
  afterEach(() => {
@@ -347,14 +356,15 @@ describe("SlackManager (mocked @herdctl/slack)", () => {
347
356
  sessionExpiryHours: 24,
348
357
  }));
349
358
  expect(MockSlackConnector).toHaveBeenCalledWith(expect.objectContaining({
359
+ agentName: "agent1",
350
360
  botToken: "xoxb-test-bot-token",
351
361
  appToken: "xapp-test-app-token",
352
- stateDir: "/tmp/test-state",
362
+ channels: [{ id: "C0123456789", mode: "mention" }],
353
363
  }));
354
364
  expect(manager.hasAgent("agent1")).toBe(true);
355
- expect(manager.getConnector()).toBe(mockConnector);
365
+ expect(manager.getConnector("agent1")).toBe(mockConnectors.get("agent1"));
356
366
  });
357
- it("builds channel→agent routing map", async () => {
367
+ it("creates separate connectors for multiple agents", async () => {
358
368
  const SlackManager = await getSlackManagerWithMock();
359
369
  const config = createConfigWithAgents(createSlackAgent("agent1", {
360
370
  ...defaultSlackConfig,
@@ -366,27 +376,10 @@ describe("SlackManager (mocked @herdctl/slack)", () => {
366
376
  const ctx = createMockContext(config);
367
377
  const manager = new SlackManager(ctx);
368
378
  await manager.initialize();
369
- const channelMap = manager.getChannelAgentMap();
370
- expect(channelMap.get("C001")).toBe("agent1");
371
- expect(channelMap.get("C002")).toBe("agent1");
372
- expect(channelMap.get("C003")).toBe("agent2");
373
- expect(channelMap.size).toBe(3);
374
- });
375
- it("warns about overlapping channel mappings", async () => {
376
- const SlackManager = await getSlackManagerWithMock();
377
- const config = createConfigWithAgents(createSlackAgent("agent1", {
378
- ...defaultSlackConfig,
379
- channels: [{ id: "C001", mode: "mention", context_messages: 10 }],
380
- }), createSlackAgent("agent2", {
381
- ...defaultSlackConfig,
382
- channels: [{ id: "C001", mode: "mention", context_messages: 10 }], // Same channel as agent1
383
- }));
384
- const ctx = createMockContext(config);
385
- const manager = new SlackManager(ctx);
386
- await manager.initialize();
387
- expect(mockLogger.warn).toHaveBeenCalledWith(expect.stringContaining("Channel C001 is already mapped"));
388
- // Second agent wins
389
- expect(manager.getChannelAgentMap().get("C001")).toBe("agent2");
379
+ expect(manager.getConnectorNames()).toEqual(["agent1", "agent2"]);
380
+ expect(MockSlackConnector).toHaveBeenCalledTimes(2);
381
+ expect(manager.getConnector("agent1")).toBeDefined();
382
+ expect(manager.getConnector("agent2")).toBeDefined();
390
383
  });
391
384
  it("skips when no agents have Slack configured", async () => {
392
385
  const SlackManager = await getSlackManagerWithMock();
@@ -397,7 +390,7 @@ describe("SlackManager (mocked @herdctl/slack)", () => {
397
390
  expect(mockLogger.debug).toHaveBeenCalledWith("No agents with Slack configured");
398
391
  expect(MockSlackConnector).not.toHaveBeenCalled();
399
392
  });
400
- it("warns and skips when bot token env var is missing", async () => {
393
+ it("warns and skips agent when bot token env var is missing", async () => {
401
394
  delete process.env.SLACK_BOT_TOKEN;
402
395
  const SlackManager = await getSlackManagerWithMock();
403
396
  const config = createConfigWithAgents(createSlackAgent("agent1", defaultSlackConfig));
@@ -407,7 +400,7 @@ describe("SlackManager (mocked @herdctl/slack)", () => {
407
400
  expect(mockLogger.warn).toHaveBeenCalledWith(expect.stringContaining("Slack bot token not found"));
408
401
  expect(MockSlackConnector).not.toHaveBeenCalled();
409
402
  });
410
- it("warns and skips when app token env var is missing", async () => {
403
+ it("warns and skips agent when app token env var is missing", async () => {
411
404
  delete process.env.SLACK_APP_TOKEN;
412
405
  const SlackManager = await getSlackManagerWithMock();
413
406
  const config = createConfigWithAgents(createSlackAgent("agent1", defaultSlackConfig));
@@ -443,10 +436,10 @@ describe("SlackManager (mocked @herdctl/slack)", () => {
443
436
  const ctx = createMockContext(config);
444
437
  const manager = new SlackManager(ctx);
445
438
  await manager.initialize();
446
- expect(mockLogger.debug).toHaveBeenCalledWith(expect.stringContaining("Slack manager initialized with 1 agent(s)"));
439
+ expect(mockLogger.debug).toHaveBeenCalledWith(expect.stringContaining("Slack manager initialized with 1 connector(s)"));
447
440
  });
448
- it("handles connector creation failure", async () => {
449
- MockSlackConnector.mockImplementation(() => {
441
+ it("handles connector creation failure for one agent", async () => {
442
+ MockSlackConnector.mockImplementationOnce(() => {
450
443
  throw new Error("Failed to create Bolt app");
451
444
  });
452
445
  const SlackManager = await getSlackManagerWithMock();
@@ -454,10 +447,10 @@ describe("SlackManager (mocked @herdctl/slack)", () => {
454
447
  const ctx = createMockContext(config);
455
448
  const manager = new SlackManager(ctx);
456
449
  await manager.initialize();
457
- expect(mockLogger.error).toHaveBeenCalledWith(expect.stringContaining("Failed to create Slack connector"));
458
- expect(manager.getConnector()).toBeNull();
450
+ expect(mockLogger.error).toHaveBeenCalledWith(expect.stringContaining("Failed to create Slack connector for agent 'agent1'"));
451
+ expect(manager.getConnector("agent1")).toBeUndefined();
459
452
  });
460
- it("creates multiple session managers for multiple agents", async () => {
453
+ it("creates connectors for multiple agents", async () => {
461
454
  const SlackManager = await getSlackManagerWithMock();
462
455
  const config = createConfigWithAgents(createSlackAgent("agent1", {
463
456
  ...defaultSlackConfig,
@@ -470,6 +463,7 @@ describe("SlackManager (mocked @herdctl/slack)", () => {
470
463
  const manager = new SlackManager(ctx);
471
464
  await manager.initialize();
472
465
  expect(MockSessionManager).toHaveBeenCalledTimes(2);
466
+ expect(MockSlackConnector).toHaveBeenCalledTimes(2);
473
467
  expect(manager.hasAgent("agent1")).toBe(true);
474
468
  expect(manager.hasAgent("agent2")).toBe(true);
475
469
  expect(manager.hasAgent("agent3")).toBe(false);
@@ -483,90 +477,92 @@ describe("SlackManager (mocked @herdctl/slack)", () => {
483
477
  const manager = new SlackManager(ctx);
484
478
  await manager.initialize();
485
479
  await manager.start();
486
- expect(mockConnector.connect).toHaveBeenCalledTimes(1);
487
- expect(mockLogger.info).toHaveBeenCalledWith("Slack connector started");
480
+ const connector = mockConnectors.get("agent1");
481
+ expect(connector?.connect).toHaveBeenCalledTimes(1);
482
+ expect(mockLogger.info).toHaveBeenCalledWith(expect.stringContaining("Slack connectors started"));
488
483
  });
489
- it("handles connection failure", async () => {
490
- mockConnector.connect.mockRejectedValue(new Error("Connection refused"));
484
+ it("handles connection failure for one agent", async () => {
491
485
  const SlackManager = await getSlackManagerWithMock();
492
486
  const config = createConfigWithAgents(createSlackAgent("agent1", defaultSlackConfig));
493
487
  const ctx = createMockContext(config);
494
488
  const manager = new SlackManager(ctx);
495
489
  await manager.initialize();
490
+ // Make the connector fail to connect
491
+ const connector = mockConnectors.get("agent1");
492
+ connector?.connect.mockRejectedValue(new Error("Connection refused"));
496
493
  await manager.start();
497
- expect(mockLogger.error).toHaveBeenCalledWith(expect.stringContaining("Failed to connect Slack"));
494
+ expect(mockLogger.error).toHaveBeenCalledWith(expect.stringContaining("Failed to connect Slack for agent 'agent1'"));
498
495
  });
499
- it("logs debug message when no connector to start", async () => {
496
+ it("logs debug message when no connectors to start", async () => {
500
497
  const SlackManager = await getSlackManagerWithMock();
501
498
  const ctx = createMockContext(null);
502
499
  const manager = new SlackManager(ctx);
503
500
  await manager.start();
504
- expect(mockLogger.debug).toHaveBeenCalledWith("No Slack connector to start");
501
+ expect(mockLogger.debug).toHaveBeenCalledWith("No Slack connectors to start");
505
502
  });
506
503
  });
507
504
  describe("stop", () => {
508
- it("disconnects the connector", async () => {
505
+ it("disconnects all connectors", async () => {
509
506
  const SlackManager = await getSlackManagerWithMock();
510
507
  const config = createConfigWithAgents(createSlackAgent("agent1", defaultSlackConfig));
511
508
  const ctx = createMockContext(config);
512
509
  const manager = new SlackManager(ctx);
513
510
  await manager.initialize();
514
511
  await manager.stop();
515
- expect(mockConnector.disconnect).toHaveBeenCalledTimes(1);
516
- expect(mockLogger.debug).toHaveBeenCalledWith("Slack connector stopped");
512
+ const connector = mockConnectors.get("agent1");
513
+ expect(connector?.disconnect).toHaveBeenCalledTimes(1);
514
+ expect(mockLogger.debug).toHaveBeenCalledWith("All Slack connectors stopped");
517
515
  });
518
- it("handles disconnect failure", async () => {
519
- mockConnector.disconnect.mockRejectedValue(new Error("Disconnect timeout"));
516
+ it("handles disconnect failure for one agent", async () => {
520
517
  const SlackManager = await getSlackManagerWithMock();
521
518
  const config = createConfigWithAgents(createSlackAgent("agent1", defaultSlackConfig));
522
519
  const ctx = createMockContext(config);
523
520
  const manager = new SlackManager(ctx);
524
521
  await manager.initialize();
522
+ const connector = mockConnectors.get("agent1");
523
+ connector?.disconnect.mockRejectedValue(new Error("Disconnect timeout"));
525
524
  await manager.stop();
526
- expect(mockLogger.error).toHaveBeenCalledWith(expect.stringContaining("Error disconnecting Slack"));
525
+ expect(mockLogger.error).toHaveBeenCalledWith(expect.stringContaining("Error disconnecting Slack for agent 'agent1'"));
527
526
  });
528
527
  it("logs active session counts before stopping", async () => {
529
- const mockSessionMgr = createMockSessionManager("agent1");
530
- mockSessionMgr.getActiveSessionCount.mockResolvedValue(3);
531
- MockSessionManager.mockImplementation(function () {
532
- return mockSessionMgr;
533
- });
534
528
  const SlackManager = await getSlackManagerWithMock();
535
529
  const config = createConfigWithAgents(createSlackAgent("agent1", defaultSlackConfig));
536
530
  const ctx = createMockContext(config);
537
531
  const manager = new SlackManager(ctx);
538
532
  await manager.initialize();
533
+ // Update the session manager to return 3 active sessions
534
+ const connector = mockConnectors.get("agent1");
535
+ connector?.sessionManager.getActiveSessionCount.mockResolvedValue(3);
539
536
  await manager.stop();
540
537
  expect(mockLogger.debug).toHaveBeenCalledWith(expect.stringContaining("Preserving 3 active Slack session(s)"));
541
538
  });
542
539
  it("handles session count query failure gracefully", async () => {
543
- const mockSessionMgr = createMockSessionManager("agent1");
544
- mockSessionMgr.getActiveSessionCount.mockRejectedValue(new Error("File read error"));
545
- MockSessionManager.mockImplementation(function () {
546
- return mockSessionMgr;
547
- });
548
540
  const SlackManager = await getSlackManagerWithMock();
549
541
  const config = createConfigWithAgents(createSlackAgent("agent1", defaultSlackConfig));
550
542
  const ctx = createMockContext(config);
551
543
  const manager = new SlackManager(ctx);
552
544
  await manager.initialize();
545
+ // Make the session manager fail
546
+ const connector = mockConnectors.get("agent1");
547
+ connector?.sessionManager.getActiveSessionCount.mockRejectedValue(new Error("File read error"));
553
548
  await manager.stop();
554
549
  expect(mockLogger.warn).toHaveBeenCalledWith(expect.stringContaining("Failed to get Slack session count"));
555
550
  });
556
551
  });
557
- describe("isConnected", () => {
558
- it("delegates to connector.isConnected()", async () => {
559
- mockConnector.isConnected.mockReturnValue(true);
552
+ describe("getConnectedCount", () => {
553
+ it("returns count of connected connectors", async () => {
560
554
  const SlackManager = await getSlackManagerWithMock();
561
555
  const config = createConfigWithAgents(createSlackAgent("agent1", defaultSlackConfig));
562
556
  const ctx = createMockContext(config);
563
557
  const manager = new SlackManager(ctx);
564
558
  await manager.initialize();
565
- expect(manager.isConnected()).toBe(true);
559
+ const connector = mockConnectors.get("agent1");
560
+ connector?.isConnected.mockReturnValue(true);
561
+ expect(manager.getConnectedCount()).toBe(1);
566
562
  });
567
563
  });
568
564
  describe("getState", () => {
569
- it("delegates to connector.getState()", async () => {
565
+ it("delegates to connector.getState() for specific agent", async () => {
570
566
  const state = {
571
567
  status: "connected",
572
568
  connectedAt: "2026-01-01T00:00:00Z",
@@ -576,13 +572,14 @@ describe("SlackManager (mocked @herdctl/slack)", () => {
576
572
  botUser: { id: "U123", username: "testbot" },
577
573
  messageStats: { received: 5, sent: 3, ignored: 1 },
578
574
  };
579
- mockConnector.getState.mockReturnValue(state);
580
575
  const SlackManager = await getSlackManagerWithMock();
581
576
  const config = createConfigWithAgents(createSlackAgent("agent1", defaultSlackConfig));
582
577
  const ctx = createMockContext(config);
583
578
  const manager = new SlackManager(ctx);
584
579
  await manager.initialize();
585
- expect(manager.getState()).toBe(state);
580
+ const connector = mockConnectors.get("agent1");
581
+ connector?.getState.mockReturnValue(state);
582
+ expect(manager.getState("agent1")).toBe(state);
586
583
  });
587
584
  });
588
585
  describe("message handling (via connector events)", () => {
@@ -594,43 +591,18 @@ describe("SlackManager (mocked @herdctl/slack)", () => {
594
591
  const manager = new SlackManager(ctx);
595
592
  await manager.initialize();
596
593
  await manager.start();
597
- // Simulate error from connector (no agentName — connector is shared)
598
- mockConnector.emit("error", {
594
+ // Simulate error from connector (with agentName)
595
+ const connector = mockConnectors.get("agent1");
596
+ connector?.emit("error", {
597
+ agentName: "agent1",
599
598
  error: new Error("Socket closed"),
600
599
  });
601
- expect(mockLogger.error).toHaveBeenCalledWith(expect.stringContaining("Slack connector error for agent 'slack'"));
600
+ expect(mockLogger.error).toHaveBeenCalledWith(expect.stringContaining("Slack connector error for agent 'agent1'"));
602
601
  expect(emitter.emit).toHaveBeenCalledWith("slack:error", expect.objectContaining({
603
- agentName: "slack",
602
+ agentName: "agent1",
604
603
  error: "Socket closed",
605
604
  }));
606
605
  });
607
- it("handles message for unknown agent", async () => {
608
- const SlackManager = await getSlackManagerWithMock();
609
- const emitter = createMockEmitter();
610
- const config = createConfigWithAgents(createSlackAgent("agent1", defaultSlackConfig));
611
- const ctx = createMockContext(config, emitter);
612
- const manager = new SlackManager(ctx);
613
- await manager.initialize();
614
- await manager.start();
615
- const replyFn = vi.fn().mockResolvedValue(undefined);
616
- // Simulate message for an agent not in config
617
- mockConnector.emit("message", {
618
- agentName: "unknown-agent",
619
- prompt: "Hello there",
620
- metadata: {
621
- channelId: "C0123456789",
622
- messageTs: "1707930001.000000",
623
- userId: "U0123456789",
624
- wasMentioned: true,
625
- },
626
- reply: replyFn,
627
- startProcessingIndicator: () => () => { },
628
- });
629
- // Give time for the async handler to run
630
- await new Promise((r) => setTimeout(r, 50));
631
- expect(mockLogger.error).toHaveBeenCalledWith("Agent 'unknown-agent' not found in configuration");
632
- expect(replyFn).toHaveBeenCalledWith(expect.stringContaining("not properly configured"));
633
- });
634
606
  it("handles message with successful trigger", async () => {
635
607
  const SlackManager = await getSlackManagerWithMock();
636
608
  const emitter = createMockEmitter();
@@ -648,7 +620,8 @@ describe("SlackManager (mocked @herdctl/slack)", () => {
648
620
  await manager.start();
649
621
  const replyFn = vi.fn().mockResolvedValue(undefined);
650
622
  const stopIndicator = vi.fn();
651
- mockConnector.emit("message", {
623
+ const connector = mockConnectors.get("agent1");
624
+ connector?.emit("message", {
652
625
  agentName: "agent1",
653
626
  prompt: "Help me with coding",
654
627
  metadata: {
@@ -685,7 +658,8 @@ describe("SlackManager (mocked @herdctl/slack)", () => {
685
658
  await manager.initialize();
686
659
  await manager.start();
687
660
  const replyFn = vi.fn().mockResolvedValue(undefined);
688
- mockConnector.emit("message", {
661
+ const connector = mockConnectors.get("agent1");
662
+ connector?.emit("message", {
689
663
  agentName: "agent1",
690
664
  prompt: "Do something",
691
665
  metadata: {
@@ -717,7 +691,8 @@ describe("SlackManager (mocked @herdctl/slack)", () => {
717
691
  await manager.initialize();
718
692
  await manager.start();
719
693
  const replyFn = vi.fn().mockResolvedValue(undefined);
720
- mockConnector.emit("message", {
694
+ const connector = mockConnectors.get("agent1");
695
+ connector?.emit("message", {
721
696
  agentName: "agent1",
722
697
  prompt: "Do something",
723
698
  metadata: {
@@ -743,7 +718,8 @@ describe("SlackManager (mocked @herdctl/slack)", () => {
743
718
  await manager.initialize();
744
719
  await manager.start();
745
720
  const replyFn = vi.fn().mockResolvedValue(undefined);
746
- mockConnector.emit("message", {
721
+ const connector = mockConnectors.get("agent1");
722
+ connector?.emit("message", {
747
723
  agentName: "agent1",
748
724
  prompt: "Do something",
749
725
  metadata: {
@@ -764,14 +740,6 @@ describe("SlackManager (mocked @herdctl/slack)", () => {
764
740
  }));
765
741
  });
766
742
  it("resumes existing session when one exists", async () => {
767
- const mockSessionMgr = createMockSessionManager("agent1");
768
- mockSessionMgr.getSession.mockResolvedValue({
769
- sessionId: "existing-session-456",
770
- lastMessageAt: "2026-02-15T10:00:00Z",
771
- });
772
- MockSessionManager.mockImplementation(function () {
773
- return mockSessionMgr;
774
- });
775
743
  const SlackManager = await getSlackManagerWithMock();
776
744
  const emitter = createMockEmitter();
777
745
  const config = createConfigWithAgents(createSlackAgent("agent1", defaultSlackConfig));
@@ -784,9 +752,15 @@ describe("SlackManager (mocked @herdctl/slack)", () => {
784
752
  const ctx = createMockContext(config, emitter);
785
753
  const manager = new SlackManager(ctx);
786
754
  await manager.initialize();
755
+ // Set up the session manager to return an existing session
756
+ const connector = mockConnectors.get("agent1");
757
+ connector?.sessionManager.getSession.mockResolvedValue({
758
+ sessionId: "existing-session-456",
759
+ lastMessageAt: "2026-02-15T10:00:00Z",
760
+ });
787
761
  await manager.start();
788
762
  const replyFn = vi.fn().mockResolvedValue(undefined);
789
- mockConnector.emit("message", {
763
+ connector?.emit("message", {
790
764
  agentName: "agent1",
791
765
  prompt: "Continue our conversation",
792
766
  metadata: {
@@ -804,7 +778,7 @@ describe("SlackManager (mocked @herdctl/slack)", () => {
804
778
  resume: "existing-session-456",
805
779
  }));
806
780
  // Should store the new session
807
- expect(mockSessionMgr.setSession).toHaveBeenCalledWith("C0123456789", "new-session-789");
781
+ expect(connector?.sessionManager.setSession).toHaveBeenCalledWith("C0123456789", "new-session-789");
808
782
  });
809
783
  it("streams assistant messages via onMessage callback", async () => {
810
784
  const SlackManager = await getSlackManagerWithMock();
@@ -825,7 +799,8 @@ describe("SlackManager (mocked @herdctl/slack)", () => {
825
799
  await manager.initialize();
826
800
  await manager.start();
827
801
  const replyFn = vi.fn().mockResolvedValue(undefined);
828
- mockConnector.emit("message", {
802
+ const connector = mockConnectors.get("agent1");
803
+ connector?.emit("message", {
829
804
  agentName: "agent1",
830
805
  prompt: "Say hello",
831
806
  metadata: {
@@ -850,10 +825,12 @@ describe("SlackManager (mocked @herdctl/slack)", () => {
850
825
  await manager.initialize();
851
826
  await manager.start();
852
827
  // Simulate non-Error (string) error from connector
853
- mockConnector.emit("error", {
828
+ const connector = mockConnectors.get("agent1");
829
+ connector?.emit("error", {
830
+ agentName: "agent1",
854
831
  error: "string error",
855
832
  });
856
- expect(mockLogger.error).toHaveBeenCalledWith(expect.stringContaining("Slack connector error for agent 'slack': string error"));
833
+ expect(mockLogger.error).toHaveBeenCalledWith(expect.stringContaining("Slack connector error for agent 'agent1': string error"));
857
834
  });
858
835
  it("handles reply failure during error handling gracefully", async () => {
859
836
  const SlackManager = await getSlackManagerWithMock();
@@ -866,7 +843,8 @@ describe("SlackManager (mocked @herdctl/slack)", () => {
866
843
  await manager.initialize();
867
844
  await manager.start();
868
845
  const replyFn = vi.fn().mockRejectedValue(new Error("Reply failed too"));
869
- mockConnector.emit("message", {
846
+ const connector = mockConnectors.get("agent1");
847
+ connector?.emit("message", {
870
848
  agentName: "agent1",
871
849
  prompt: "Do something",
872
850
  metadata: {
@@ -883,31 +861,6 @@ describe("SlackManager (mocked @herdctl/slack)", () => {
883
861
  expect(mockLogger.error).toHaveBeenCalledWith(expect.stringContaining("Slack message handling failed"));
884
862
  expect(mockLogger.error).toHaveBeenCalledWith(expect.stringContaining("Failed to send error reply"));
885
863
  });
886
- it("handles error from reply during agent-not-found", async () => {
887
- const SlackManager = await getSlackManagerWithMock();
888
- const emitter = createMockEmitter();
889
- const config = createConfigWithAgents(createSlackAgent("agent1", defaultSlackConfig));
890
- const ctx = createMockContext(config, emitter);
891
- const manager = new SlackManager(ctx);
892
- await manager.initialize();
893
- await manager.start();
894
- const replyFn = vi.fn().mockRejectedValue(new Error("Reply error"));
895
- mockConnector.emit("message", {
896
- agentName: "nonexistent",
897
- prompt: "Hello",
898
- metadata: {
899
- channelId: "C0123456789",
900
- messageTs: "1707930001.000000",
901
- userId: "U0123456789",
902
- wasMentioned: true,
903
- },
904
- reply: replyFn,
905
- startProcessingIndicator: () => () => { },
906
- });
907
- await new Promise((r) => setTimeout(r, 50));
908
- expect(mockLogger.error).toHaveBeenCalledWith(expect.stringContaining("Agent 'nonexistent' not found"));
909
- expect(mockLogger.error).toHaveBeenCalledWith(expect.stringContaining("Failed to send error reply"));
910
- });
911
864
  it("extracts text from message.message.content array", async () => {
912
865
  const SlackManager = await getSlackManagerWithMock();
913
866
  const emitter = createMockEmitter();
@@ -935,7 +888,8 @@ describe("SlackManager (mocked @herdctl/slack)", () => {
935
888
  await manager.initialize();
936
889
  await manager.start();
937
890
  const replyFn = vi.fn().mockResolvedValue(undefined);
938
- mockConnector.emit("message", {
891
+ const connector = mockConnectors.get("agent1");
892
+ connector?.emit("message", {
939
893
  agentName: "agent1",
940
894
  prompt: "Test",
941
895
  metadata: {
@@ -971,7 +925,8 @@ describe("SlackManager (mocked @herdctl/slack)", () => {
971
925
  await manager.initialize();
972
926
  await manager.start();
973
927
  const replyFn = vi.fn().mockResolvedValue(undefined);
974
- mockConnector.emit("message", {
928
+ const connector = mockConnectors.get("agent1");
929
+ connector?.emit("message", {
975
930
  agentName: "agent1",
976
931
  prompt: "Test",
977
932
  metadata: {
@@ -1005,7 +960,8 @@ describe("SlackManager (mocked @herdctl/slack)", () => {
1005
960
  await manager.initialize();
1006
961
  await manager.start();
1007
962
  const replyFn = vi.fn().mockResolvedValue(undefined);
1008
- mockConnector.emit("message", {
963
+ const connector = mockConnectors.get("agent1");
964
+ connector?.emit("message", {
1009
965
  agentName: "agent1",
1010
966
  prompt: "Test",
1011
967
  metadata: {