@herdctl/core 4.1.0 → 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.
Files changed (76) hide show
  1. package/dist/config/loader.d.ts.map +1 -1
  2. package/dist/config/loader.js +3 -1
  3. package/dist/config/loader.js.map +1 -1
  4. package/dist/fleet-manager/__tests__/coverage.test.js +8 -0
  5. package/dist/fleet-manager/__tests__/coverage.test.js.map +1 -1
  6. package/dist/fleet-manager/__tests__/discord-manager.test.js +2 -2
  7. package/dist/fleet-manager/__tests__/discord-manager.test.js.map +1 -1
  8. package/dist/fleet-manager/__tests__/slack-manager.test.d.ts +2 -2
  9. package/dist/fleet-manager/__tests__/slack-manager.test.js +125 -164
  10. package/dist/fleet-manager/__tests__/slack-manager.test.js.map +1 -1
  11. package/dist/fleet-manager/discord-manager.js +7 -7
  12. package/dist/fleet-manager/discord-manager.js.map +1 -1
  13. package/dist/fleet-manager/event-types.d.ts +10 -4
  14. package/dist/fleet-manager/event-types.d.ts.map +1 -1
  15. package/dist/fleet-manager/fleet-manager.d.ts.map +1 -1
  16. package/dist/fleet-manager/fleet-manager.js +5 -9
  17. package/dist/fleet-manager/fleet-manager.js.map +1 -1
  18. package/dist/fleet-manager/job-control.js +1 -1
  19. package/dist/fleet-manager/job-control.js.map +1 -1
  20. package/dist/fleet-manager/job-queue.d.ts.map +1 -1
  21. package/dist/fleet-manager/job-queue.js +2 -6
  22. package/dist/fleet-manager/job-queue.js.map +1 -1
  23. package/dist/fleet-manager/slack-manager.d.ts +72 -29
  24. package/dist/fleet-manager/slack-manager.d.ts.map +1 -1
  25. package/dist/fleet-manager/slack-manager.js +153 -147
  26. package/dist/fleet-manager/slack-manager.js.map +1 -1
  27. package/dist/fleet-manager/status-queries.js +1 -1
  28. package/dist/fleet-manager/status-queries.js.map +1 -1
  29. package/dist/index.d.ts +1 -0
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +2 -0
  32. package/dist/index.js.map +1 -1
  33. package/dist/runner/job-executor.d.ts +1 -0
  34. package/dist/runner/job-executor.d.ts.map +1 -1
  35. package/dist/runner/job-executor.js +4 -7
  36. package/dist/runner/job-executor.js.map +1 -1
  37. package/dist/runner/runtime/cli-runtime.d.ts.map +1 -1
  38. package/dist/runner/runtime/cli-runtime.js +29 -27
  39. package/dist/runner/runtime/cli-runtime.js.map +1 -1
  40. package/dist/runner/runtime/cli-session-watcher.d.ts.map +1 -1
  41. package/dist/runner/runtime/cli-session-watcher.js +20 -18
  42. package/dist/runner/runtime/cli-session-watcher.js.map +1 -1
  43. package/dist/runner/runtime/container-manager.d.ts.map +1 -1
  44. package/dist/runner/runtime/container-manager.js +7 -5
  45. package/dist/runner/runtime/container-manager.js.map +1 -1
  46. package/dist/runner/runtime/container-runner.d.ts.map +1 -1
  47. package/dist/runner/runtime/container-runner.js +12 -10
  48. package/dist/runner/runtime/container-runner.js.map +1 -1
  49. package/dist/scheduler/__tests__/scheduler.test.js +2 -2
  50. package/dist/scheduler/__tests__/scheduler.test.js.map +1 -1
  51. package/dist/scheduler/schedule-runner.d.ts.map +1 -1
  52. package/dist/scheduler/schedule-runner.js +2 -6
  53. package/dist/scheduler/schedule-runner.js.map +1 -1
  54. package/dist/scheduler/schedule-state.d.ts.map +1 -1
  55. package/dist/scheduler/schedule-state.js +2 -3
  56. package/dist/scheduler/schedule-state.js.map +1 -1
  57. package/dist/scheduler/scheduler.d.ts.map +1 -1
  58. package/dist/scheduler/scheduler.js +3 -7
  59. package/dist/scheduler/scheduler.js.map +1 -1
  60. package/dist/state/fleet-state.d.ts.map +1 -1
  61. package/dist/state/fleet-state.js +2 -3
  62. package/dist/state/fleet-state.js.map +1 -1
  63. package/dist/state/schemas/job-output.d.ts +2 -2
  64. package/dist/utils/__tests__/logger.test.d.ts +2 -0
  65. package/dist/utils/__tests__/logger.test.d.ts.map +1 -0
  66. package/dist/utils/__tests__/logger.test.js +360 -0
  67. package/dist/utils/__tests__/logger.test.js.map +1 -0
  68. package/dist/utils/index.d.ts +5 -0
  69. package/dist/utils/index.d.ts.map +1 -0
  70. package/dist/utils/index.js +5 -0
  71. package/dist/utils/index.js.map +1 -0
  72. package/dist/utils/logger.d.ts +72 -0
  73. package/dist/utils/logger.d.ts.map +1 -0
  74. package/dist/utils/logger.js +117 -0
  75. package/dist/utils/logger.js.map +1 -0
  76. package/package.json +1 -1
@@ -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) {
@@ -110,6 +113,11 @@ function createMockSessionManager(agentName) {
110
113
  describe("SlackManager (no @herdctl/slack)", () => {
111
114
  beforeEach(() => {
112
115
  vi.clearAllMocks();
116
+ vi.resetModules();
117
+ // Mock @herdctl/slack to simulate it not being installed
118
+ vi.doMock("@herdctl/slack", () => {
119
+ throw new Error("Cannot find package '@herdctl/slack'");
120
+ });
113
121
  });
114
122
  afterEach(() => {
115
123
  vi.restoreAllMocks();
@@ -141,7 +149,7 @@ describe("SlackManager (no @herdctl/slack)", () => {
141
149
  const ctx = createMockContext(config);
142
150
  const manager = new SlackManager(ctx);
143
151
  await manager.initialize();
144
- expect(mockLogger.debug).toHaveBeenCalledWith("@herdctl/slack not installed, skipping Slack connector");
152
+ expect(mockLogger.debug).toHaveBeenCalledWith("@herdctl/slack not installed, skipping Slack connectors");
145
153
  });
146
154
  it("skips when @herdctl/slack is not installed (with slack agents)", async () => {
147
155
  const SlackManager = await getSlackManager();
@@ -149,7 +157,7 @@ describe("SlackManager (no @herdctl/slack)", () => {
149
157
  const ctx = createMockContext(config);
150
158
  const manager = new SlackManager(ctx);
151
159
  await manager.initialize();
152
- expect(mockLogger.debug).toHaveBeenCalledWith("@herdctl/slack not installed, skipping Slack connector");
160
+ expect(mockLogger.debug).toHaveBeenCalledWith("@herdctl/slack not installed, skipping Slack connectors");
153
161
  });
154
162
  it("allows retry when no config (initialized not set)", async () => {
155
163
  const SlackManager = await getSlackManager();
@@ -168,7 +176,7 @@ describe("SlackManager (no @herdctl/slack)", () => {
168
176
  const manager = new SlackManager(ctx);
169
177
  await manager.initialize();
170
178
  await manager.start();
171
- expect(mockLogger.debug).toHaveBeenCalledWith("No Slack connector to start");
179
+ expect(mockLogger.debug).toHaveBeenCalledWith("No Slack connectors to start");
172
180
  });
173
181
  });
174
182
  describe("stop", () => {
@@ -178,7 +186,7 @@ describe("SlackManager (no @herdctl/slack)", () => {
178
186
  const manager = new SlackManager(ctx);
179
187
  await manager.initialize();
180
188
  await manager.stop();
181
- expect(mockLogger.debug).toHaveBeenCalledWith("No Slack connector to stop");
189
+ expect(mockLogger.debug).toHaveBeenCalledWith("No Slack connectors to stop");
182
190
  });
183
191
  });
184
192
  describe("hasAgent", () => {
@@ -190,35 +198,35 @@ describe("SlackManager (no @herdctl/slack)", () => {
190
198
  });
191
199
  });
192
200
  describe("getState", () => {
193
- it("returns null when no connector", async () => {
201
+ it("returns null when no connector for agent", async () => {
194
202
  const SlackManager = await getSlackManager();
195
203
  const ctx = createMockContext(null);
196
204
  const manager = new SlackManager(ctx);
197
- expect(manager.getState()).toBeNull();
205
+ expect(manager.getState("test-agent")).toBeNull();
198
206
  });
199
207
  });
200
- describe("isConnected", () => {
201
- it("returns false when no connector", async () => {
208
+ describe("getConnectedCount", () => {
209
+ it("returns 0 when no connectors", async () => {
202
210
  const SlackManager = await getSlackManager();
203
211
  const ctx = createMockContext(null);
204
212
  const manager = new SlackManager(ctx);
205
- expect(manager.isConnected()).toBe(false);
213
+ expect(manager.getConnectedCount()).toBe(0);
206
214
  });
207
215
  });
208
216
  describe("getConnector", () => {
209
- it("returns null when no connector", async () => {
217
+ it("returns undefined when no connector for agent", async () => {
210
218
  const SlackManager = await getSlackManager();
211
219
  const ctx = createMockContext(null);
212
220
  const manager = new SlackManager(ctx);
213
- expect(manager.getConnector()).toBeNull();
221
+ expect(manager.getConnector("test-agent")).toBeUndefined();
214
222
  });
215
223
  });
216
- describe("getChannelAgentMap", () => {
217
- it("returns empty map when not initialized", async () => {
224
+ describe("getConnectorNames", () => {
225
+ it("returns empty array when not initialized", async () => {
218
226
  const SlackManager = await getSlackManager();
219
227
  const ctx = createMockContext(null);
220
228
  const manager = new SlackManager(ctx);
221
- expect(manager.getChannelAgentMap().size).toBe(0);
229
+ expect(manager.getConnectorNames()).toEqual([]);
222
230
  });
223
231
  });
224
232
  describe("splitResponse", () => {
@@ -293,7 +301,8 @@ describe("SlackManager (no @herdctl/slack)", () => {
293
301
  // Tests – With Mocked @herdctl/slack (full initialization paths)
294
302
  // ---------------------------------------------------------------------------
295
303
  describe("SlackManager (mocked @herdctl/slack)", () => {
296
- let mockConnector;
304
+ let mockConnectors;
305
+ let mockSessionManagers;
297
306
  let MockSlackConnector;
298
307
  let MockSessionManager;
299
308
  let originalEnv;
@@ -304,14 +313,19 @@ describe("SlackManager (mocked @herdctl/slack)", () => {
304
313
  // Set required env vars
305
314
  process.env.SLACK_BOT_TOKEN = "xoxb-test-bot-token";
306
315
  process.env.SLACK_APP_TOKEN = "xapp-test-app-token";
307
- // Create mock implementations
308
- mockConnector = createMockConnector();
316
+ // Create mock implementations - per-agent connectors
317
+ mockConnectors = new Map();
318
+ mockSessionManagers = new Map();
309
319
  // Must use function expressions (not arrows) so they work with `new`
310
- MockSlackConnector = vi.fn().mockImplementation(function () {
311
- return mockConnector;
312
- });
313
320
  MockSessionManager = vi.fn().mockImplementation(function (opts) {
314
- 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;
315
329
  });
316
330
  });
317
331
  afterEach(() => {
@@ -342,14 +356,15 @@ describe("SlackManager (mocked @herdctl/slack)", () => {
342
356
  sessionExpiryHours: 24,
343
357
  }));
344
358
  expect(MockSlackConnector).toHaveBeenCalledWith(expect.objectContaining({
359
+ agentName: "agent1",
345
360
  botToken: "xoxb-test-bot-token",
346
361
  appToken: "xapp-test-app-token",
347
- stateDir: "/tmp/test-state",
362
+ channels: [{ id: "C0123456789", mode: "mention" }],
348
363
  }));
349
364
  expect(manager.hasAgent("agent1")).toBe(true);
350
- expect(manager.getConnector()).toBe(mockConnector);
365
+ expect(manager.getConnector("agent1")).toBe(mockConnectors.get("agent1"));
351
366
  });
352
- it("builds channel→agent routing map", async () => {
367
+ it("creates separate connectors for multiple agents", async () => {
353
368
  const SlackManager = await getSlackManagerWithMock();
354
369
  const config = createConfigWithAgents(createSlackAgent("agent1", {
355
370
  ...defaultSlackConfig,
@@ -361,27 +376,10 @@ describe("SlackManager (mocked @herdctl/slack)", () => {
361
376
  const ctx = createMockContext(config);
362
377
  const manager = new SlackManager(ctx);
363
378
  await manager.initialize();
364
- const channelMap = manager.getChannelAgentMap();
365
- expect(channelMap.get("C001")).toBe("agent1");
366
- expect(channelMap.get("C002")).toBe("agent1");
367
- expect(channelMap.get("C003")).toBe("agent2");
368
- expect(channelMap.size).toBe(3);
369
- });
370
- it("warns about overlapping channel mappings", async () => {
371
- const SlackManager = await getSlackManagerWithMock();
372
- const config = createConfigWithAgents(createSlackAgent("agent1", {
373
- ...defaultSlackConfig,
374
- channels: [{ id: "C001", mode: "mention", context_messages: 10 }],
375
- }), createSlackAgent("agent2", {
376
- ...defaultSlackConfig,
377
- channels: [{ id: "C001", mode: "mention", context_messages: 10 }], // Same channel as agent1
378
- }));
379
- const ctx = createMockContext(config);
380
- const manager = new SlackManager(ctx);
381
- await manager.initialize();
382
- expect(mockLogger.warn).toHaveBeenCalledWith(expect.stringContaining("Channel C001 is already mapped"));
383
- // Second agent wins
384
- 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();
385
383
  });
386
384
  it("skips when no agents have Slack configured", async () => {
387
385
  const SlackManager = await getSlackManagerWithMock();
@@ -392,7 +390,7 @@ describe("SlackManager (mocked @herdctl/slack)", () => {
392
390
  expect(mockLogger.debug).toHaveBeenCalledWith("No agents with Slack configured");
393
391
  expect(MockSlackConnector).not.toHaveBeenCalled();
394
392
  });
395
- 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 () => {
396
394
  delete process.env.SLACK_BOT_TOKEN;
397
395
  const SlackManager = await getSlackManagerWithMock();
398
396
  const config = createConfigWithAgents(createSlackAgent("agent1", defaultSlackConfig));
@@ -402,7 +400,7 @@ describe("SlackManager (mocked @herdctl/slack)", () => {
402
400
  expect(mockLogger.warn).toHaveBeenCalledWith(expect.stringContaining("Slack bot token not found"));
403
401
  expect(MockSlackConnector).not.toHaveBeenCalled();
404
402
  });
405
- 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 () => {
406
404
  delete process.env.SLACK_APP_TOKEN;
407
405
  const SlackManager = await getSlackManagerWithMock();
408
406
  const config = createConfigWithAgents(createSlackAgent("agent1", defaultSlackConfig));
@@ -438,10 +436,10 @@ describe("SlackManager (mocked @herdctl/slack)", () => {
438
436
  const ctx = createMockContext(config);
439
437
  const manager = new SlackManager(ctx);
440
438
  await manager.initialize();
441
- expect(mockLogger.info).toHaveBeenCalledWith(expect.stringContaining("Slack manager initialized with 1 agent(s)"));
439
+ expect(mockLogger.debug).toHaveBeenCalledWith(expect.stringContaining("Slack manager initialized with 1 connector(s)"));
442
440
  });
443
- it("handles connector creation failure", async () => {
444
- MockSlackConnector.mockImplementation(() => {
441
+ it("handles connector creation failure for one agent", async () => {
442
+ MockSlackConnector.mockImplementationOnce(() => {
445
443
  throw new Error("Failed to create Bolt app");
446
444
  });
447
445
  const SlackManager = await getSlackManagerWithMock();
@@ -449,10 +447,10 @@ describe("SlackManager (mocked @herdctl/slack)", () => {
449
447
  const ctx = createMockContext(config);
450
448
  const manager = new SlackManager(ctx);
451
449
  await manager.initialize();
452
- expect(mockLogger.error).toHaveBeenCalledWith(expect.stringContaining("Failed to create Slack connector"));
453
- 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();
454
452
  });
455
- it("creates multiple session managers for multiple agents", async () => {
453
+ it("creates connectors for multiple agents", async () => {
456
454
  const SlackManager = await getSlackManagerWithMock();
457
455
  const config = createConfigWithAgents(createSlackAgent("agent1", {
458
456
  ...defaultSlackConfig,
@@ -465,6 +463,7 @@ describe("SlackManager (mocked @herdctl/slack)", () => {
465
463
  const manager = new SlackManager(ctx);
466
464
  await manager.initialize();
467
465
  expect(MockSessionManager).toHaveBeenCalledTimes(2);
466
+ expect(MockSlackConnector).toHaveBeenCalledTimes(2);
468
467
  expect(manager.hasAgent("agent1")).toBe(true);
469
468
  expect(manager.hasAgent("agent2")).toBe(true);
470
469
  expect(manager.hasAgent("agent3")).toBe(false);
@@ -478,90 +477,92 @@ describe("SlackManager (mocked @herdctl/slack)", () => {
478
477
  const manager = new SlackManager(ctx);
479
478
  await manager.initialize();
480
479
  await manager.start();
481
- expect(mockConnector.connect).toHaveBeenCalledTimes(1);
482
- 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"));
483
483
  });
484
- it("handles connection failure", async () => {
485
- mockConnector.connect.mockRejectedValue(new Error("Connection refused"));
484
+ it("handles connection failure for one agent", async () => {
486
485
  const SlackManager = await getSlackManagerWithMock();
487
486
  const config = createConfigWithAgents(createSlackAgent("agent1", defaultSlackConfig));
488
487
  const ctx = createMockContext(config);
489
488
  const manager = new SlackManager(ctx);
490
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"));
491
493
  await manager.start();
492
- expect(mockLogger.error).toHaveBeenCalledWith(expect.stringContaining("Failed to connect Slack"));
494
+ expect(mockLogger.error).toHaveBeenCalledWith(expect.stringContaining("Failed to connect Slack for agent 'agent1'"));
493
495
  });
494
- it("logs debug message when no connector to start", async () => {
496
+ it("logs debug message when no connectors to start", async () => {
495
497
  const SlackManager = await getSlackManagerWithMock();
496
498
  const ctx = createMockContext(null);
497
499
  const manager = new SlackManager(ctx);
498
500
  await manager.start();
499
- expect(mockLogger.debug).toHaveBeenCalledWith("No Slack connector to start");
501
+ expect(mockLogger.debug).toHaveBeenCalledWith("No Slack connectors to start");
500
502
  });
501
503
  });
502
504
  describe("stop", () => {
503
- it("disconnects the connector", async () => {
505
+ it("disconnects all connectors", async () => {
504
506
  const SlackManager = await getSlackManagerWithMock();
505
507
  const config = createConfigWithAgents(createSlackAgent("agent1", defaultSlackConfig));
506
508
  const ctx = createMockContext(config);
507
509
  const manager = new SlackManager(ctx);
508
510
  await manager.initialize();
509
511
  await manager.stop();
510
- expect(mockConnector.disconnect).toHaveBeenCalledTimes(1);
511
- expect(mockLogger.info).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");
512
515
  });
513
- it("handles disconnect failure", async () => {
514
- mockConnector.disconnect.mockRejectedValue(new Error("Disconnect timeout"));
516
+ it("handles disconnect failure for one agent", async () => {
515
517
  const SlackManager = await getSlackManagerWithMock();
516
518
  const config = createConfigWithAgents(createSlackAgent("agent1", defaultSlackConfig));
517
519
  const ctx = createMockContext(config);
518
520
  const manager = new SlackManager(ctx);
519
521
  await manager.initialize();
522
+ const connector = mockConnectors.get("agent1");
523
+ connector?.disconnect.mockRejectedValue(new Error("Disconnect timeout"));
520
524
  await manager.stop();
521
- expect(mockLogger.error).toHaveBeenCalledWith(expect.stringContaining("Error disconnecting Slack"));
525
+ expect(mockLogger.error).toHaveBeenCalledWith(expect.stringContaining("Error disconnecting Slack for agent 'agent1'"));
522
526
  });
523
527
  it("logs active session counts before stopping", async () => {
524
- const mockSessionMgr = createMockSessionManager("agent1");
525
- mockSessionMgr.getActiveSessionCount.mockResolvedValue(3);
526
- MockSessionManager.mockImplementation(function () {
527
- return mockSessionMgr;
528
- });
529
528
  const SlackManager = await getSlackManagerWithMock();
530
529
  const config = createConfigWithAgents(createSlackAgent("agent1", defaultSlackConfig));
531
530
  const ctx = createMockContext(config);
532
531
  const manager = new SlackManager(ctx);
533
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);
534
536
  await manager.stop();
535
- expect(mockLogger.info).toHaveBeenCalledWith(expect.stringContaining("Preserving 3 active Slack session(s)"));
537
+ expect(mockLogger.debug).toHaveBeenCalledWith(expect.stringContaining("Preserving 3 active Slack session(s)"));
536
538
  });
537
539
  it("handles session count query failure gracefully", async () => {
538
- const mockSessionMgr = createMockSessionManager("agent1");
539
- mockSessionMgr.getActiveSessionCount.mockRejectedValue(new Error("File read error"));
540
- MockSessionManager.mockImplementation(function () {
541
- return mockSessionMgr;
542
- });
543
540
  const SlackManager = await getSlackManagerWithMock();
544
541
  const config = createConfigWithAgents(createSlackAgent("agent1", defaultSlackConfig));
545
542
  const ctx = createMockContext(config);
546
543
  const manager = new SlackManager(ctx);
547
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"));
548
548
  await manager.stop();
549
549
  expect(mockLogger.warn).toHaveBeenCalledWith(expect.stringContaining("Failed to get Slack session count"));
550
550
  });
551
551
  });
552
- describe("isConnected", () => {
553
- it("delegates to connector.isConnected()", async () => {
554
- mockConnector.isConnected.mockReturnValue(true);
552
+ describe("getConnectedCount", () => {
553
+ it("returns count of connected connectors", async () => {
555
554
  const SlackManager = await getSlackManagerWithMock();
556
555
  const config = createConfigWithAgents(createSlackAgent("agent1", defaultSlackConfig));
557
556
  const ctx = createMockContext(config);
558
557
  const manager = new SlackManager(ctx);
559
558
  await manager.initialize();
560
- expect(manager.isConnected()).toBe(true);
559
+ const connector = mockConnectors.get("agent1");
560
+ connector?.isConnected.mockReturnValue(true);
561
+ expect(manager.getConnectedCount()).toBe(1);
561
562
  });
562
563
  });
563
564
  describe("getState", () => {
564
- it("delegates to connector.getState()", async () => {
565
+ it("delegates to connector.getState() for specific agent", async () => {
565
566
  const state = {
566
567
  status: "connected",
567
568
  connectedAt: "2026-01-01T00:00:00Z",
@@ -571,13 +572,14 @@ describe("SlackManager (mocked @herdctl/slack)", () => {
571
572
  botUser: { id: "U123", username: "testbot" },
572
573
  messageStats: { received: 5, sent: 3, ignored: 1 },
573
574
  };
574
- mockConnector.getState.mockReturnValue(state);
575
575
  const SlackManager = await getSlackManagerWithMock();
576
576
  const config = createConfigWithAgents(createSlackAgent("agent1", defaultSlackConfig));
577
577
  const ctx = createMockContext(config);
578
578
  const manager = new SlackManager(ctx);
579
579
  await manager.initialize();
580
- expect(manager.getState()).toBe(state);
580
+ const connector = mockConnectors.get("agent1");
581
+ connector?.getState.mockReturnValue(state);
582
+ expect(manager.getState("agent1")).toBe(state);
581
583
  });
582
584
  });
583
585
  describe("message handling (via connector events)", () => {
@@ -589,43 +591,18 @@ describe("SlackManager (mocked @herdctl/slack)", () => {
589
591
  const manager = new SlackManager(ctx);
590
592
  await manager.initialize();
591
593
  await manager.start();
592
- // Simulate error from connector (no agentName — connector is shared)
593
- mockConnector.emit("error", {
594
+ // Simulate error from connector (with agentName)
595
+ const connector = mockConnectors.get("agent1");
596
+ connector?.emit("error", {
597
+ agentName: "agent1",
594
598
  error: new Error("Socket closed"),
595
599
  });
596
- 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'"));
597
601
  expect(emitter.emit).toHaveBeenCalledWith("slack:error", expect.objectContaining({
598
- agentName: "slack",
602
+ agentName: "agent1",
599
603
  error: "Socket closed",
600
604
  }));
601
605
  });
602
- it("handles message for unknown agent", async () => {
603
- const SlackManager = await getSlackManagerWithMock();
604
- const emitter = createMockEmitter();
605
- const config = createConfigWithAgents(createSlackAgent("agent1", defaultSlackConfig));
606
- const ctx = createMockContext(config, emitter);
607
- const manager = new SlackManager(ctx);
608
- await manager.initialize();
609
- await manager.start();
610
- const replyFn = vi.fn().mockResolvedValue(undefined);
611
- // Simulate message for an agent not in config
612
- mockConnector.emit("message", {
613
- agentName: "unknown-agent",
614
- prompt: "Hello there",
615
- metadata: {
616
- channelId: "C0123456789",
617
- messageTs: "1707930001.000000",
618
- userId: "U0123456789",
619
- wasMentioned: true,
620
- },
621
- reply: replyFn,
622
- startProcessingIndicator: () => () => { },
623
- });
624
- // Give time for the async handler to run
625
- await new Promise((r) => setTimeout(r, 50));
626
- expect(mockLogger.error).toHaveBeenCalledWith("Agent 'unknown-agent' not found in configuration");
627
- expect(replyFn).toHaveBeenCalledWith(expect.stringContaining("not properly configured"));
628
- });
629
606
  it("handles message with successful trigger", async () => {
630
607
  const SlackManager = await getSlackManagerWithMock();
631
608
  const emitter = createMockEmitter();
@@ -643,7 +620,8 @@ describe("SlackManager (mocked @herdctl/slack)", () => {
643
620
  await manager.start();
644
621
  const replyFn = vi.fn().mockResolvedValue(undefined);
645
622
  const stopIndicator = vi.fn();
646
- mockConnector.emit("message", {
623
+ const connector = mockConnectors.get("agent1");
624
+ connector?.emit("message", {
647
625
  agentName: "agent1",
648
626
  prompt: "Help me with coding",
649
627
  metadata: {
@@ -680,7 +658,8 @@ describe("SlackManager (mocked @herdctl/slack)", () => {
680
658
  await manager.initialize();
681
659
  await manager.start();
682
660
  const replyFn = vi.fn().mockResolvedValue(undefined);
683
- mockConnector.emit("message", {
661
+ const connector = mockConnectors.get("agent1");
662
+ connector?.emit("message", {
684
663
  agentName: "agent1",
685
664
  prompt: "Do something",
686
665
  metadata: {
@@ -712,7 +691,8 @@ describe("SlackManager (mocked @herdctl/slack)", () => {
712
691
  await manager.initialize();
713
692
  await manager.start();
714
693
  const replyFn = vi.fn().mockResolvedValue(undefined);
715
- mockConnector.emit("message", {
694
+ const connector = mockConnectors.get("agent1");
695
+ connector?.emit("message", {
716
696
  agentName: "agent1",
717
697
  prompt: "Do something",
718
698
  metadata: {
@@ -738,7 +718,8 @@ describe("SlackManager (mocked @herdctl/slack)", () => {
738
718
  await manager.initialize();
739
719
  await manager.start();
740
720
  const replyFn = vi.fn().mockResolvedValue(undefined);
741
- mockConnector.emit("message", {
721
+ const connector = mockConnectors.get("agent1");
722
+ connector?.emit("message", {
742
723
  agentName: "agent1",
743
724
  prompt: "Do something",
744
725
  metadata: {
@@ -759,14 +740,6 @@ describe("SlackManager (mocked @herdctl/slack)", () => {
759
740
  }));
760
741
  });
761
742
  it("resumes existing session when one exists", async () => {
762
- const mockSessionMgr = createMockSessionManager("agent1");
763
- mockSessionMgr.getSession.mockResolvedValue({
764
- sessionId: "existing-session-456",
765
- lastMessageAt: "2026-02-15T10:00:00Z",
766
- });
767
- MockSessionManager.mockImplementation(function () {
768
- return mockSessionMgr;
769
- });
770
743
  const SlackManager = await getSlackManagerWithMock();
771
744
  const emitter = createMockEmitter();
772
745
  const config = createConfigWithAgents(createSlackAgent("agent1", defaultSlackConfig));
@@ -779,9 +752,15 @@ describe("SlackManager (mocked @herdctl/slack)", () => {
779
752
  const ctx = createMockContext(config, emitter);
780
753
  const manager = new SlackManager(ctx);
781
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
+ });
782
761
  await manager.start();
783
762
  const replyFn = vi.fn().mockResolvedValue(undefined);
784
- mockConnector.emit("message", {
763
+ connector?.emit("message", {
785
764
  agentName: "agent1",
786
765
  prompt: "Continue our conversation",
787
766
  metadata: {
@@ -799,7 +778,7 @@ describe("SlackManager (mocked @herdctl/slack)", () => {
799
778
  resume: "existing-session-456",
800
779
  }));
801
780
  // Should store the new session
802
- expect(mockSessionMgr.setSession).toHaveBeenCalledWith("C0123456789", "new-session-789");
781
+ expect(connector?.sessionManager.setSession).toHaveBeenCalledWith("C0123456789", "new-session-789");
803
782
  });
804
783
  it("streams assistant messages via onMessage callback", async () => {
805
784
  const SlackManager = await getSlackManagerWithMock();
@@ -820,7 +799,8 @@ describe("SlackManager (mocked @herdctl/slack)", () => {
820
799
  await manager.initialize();
821
800
  await manager.start();
822
801
  const replyFn = vi.fn().mockResolvedValue(undefined);
823
- mockConnector.emit("message", {
802
+ const connector = mockConnectors.get("agent1");
803
+ connector?.emit("message", {
824
804
  agentName: "agent1",
825
805
  prompt: "Say hello",
826
806
  metadata: {
@@ -845,10 +825,12 @@ describe("SlackManager (mocked @herdctl/slack)", () => {
845
825
  await manager.initialize();
846
826
  await manager.start();
847
827
  // Simulate non-Error (string) error from connector
848
- mockConnector.emit("error", {
828
+ const connector = mockConnectors.get("agent1");
829
+ connector?.emit("error", {
830
+ agentName: "agent1",
849
831
  error: "string error",
850
832
  });
851
- 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"));
852
834
  });
853
835
  it("handles reply failure during error handling gracefully", async () => {
854
836
  const SlackManager = await getSlackManagerWithMock();
@@ -861,7 +843,8 @@ describe("SlackManager (mocked @herdctl/slack)", () => {
861
843
  await manager.initialize();
862
844
  await manager.start();
863
845
  const replyFn = vi.fn().mockRejectedValue(new Error("Reply failed too"));
864
- mockConnector.emit("message", {
846
+ const connector = mockConnectors.get("agent1");
847
+ connector?.emit("message", {
865
848
  agentName: "agent1",
866
849
  prompt: "Do something",
867
850
  metadata: {
@@ -878,31 +861,6 @@ describe("SlackManager (mocked @herdctl/slack)", () => {
878
861
  expect(mockLogger.error).toHaveBeenCalledWith(expect.stringContaining("Slack message handling failed"));
879
862
  expect(mockLogger.error).toHaveBeenCalledWith(expect.stringContaining("Failed to send error reply"));
880
863
  });
881
- it("handles error from reply during agent-not-found", async () => {
882
- const SlackManager = await getSlackManagerWithMock();
883
- const emitter = createMockEmitter();
884
- const config = createConfigWithAgents(createSlackAgent("agent1", defaultSlackConfig));
885
- const ctx = createMockContext(config, emitter);
886
- const manager = new SlackManager(ctx);
887
- await manager.initialize();
888
- await manager.start();
889
- const replyFn = vi.fn().mockRejectedValue(new Error("Reply error"));
890
- mockConnector.emit("message", {
891
- agentName: "nonexistent",
892
- prompt: "Hello",
893
- metadata: {
894
- channelId: "C0123456789",
895
- messageTs: "1707930001.000000",
896
- userId: "U0123456789",
897
- wasMentioned: true,
898
- },
899
- reply: replyFn,
900
- startProcessingIndicator: () => () => { },
901
- });
902
- await new Promise((r) => setTimeout(r, 50));
903
- expect(mockLogger.error).toHaveBeenCalledWith(expect.stringContaining("Agent 'nonexistent' not found"));
904
- expect(mockLogger.error).toHaveBeenCalledWith(expect.stringContaining("Failed to send error reply"));
905
- });
906
864
  it("extracts text from message.message.content array", async () => {
907
865
  const SlackManager = await getSlackManagerWithMock();
908
866
  const emitter = createMockEmitter();
@@ -930,7 +888,8 @@ describe("SlackManager (mocked @herdctl/slack)", () => {
930
888
  await manager.initialize();
931
889
  await manager.start();
932
890
  const replyFn = vi.fn().mockResolvedValue(undefined);
933
- mockConnector.emit("message", {
891
+ const connector = mockConnectors.get("agent1");
892
+ connector?.emit("message", {
934
893
  agentName: "agent1",
935
894
  prompt: "Test",
936
895
  metadata: {
@@ -966,7 +925,8 @@ describe("SlackManager (mocked @herdctl/slack)", () => {
966
925
  await manager.initialize();
967
926
  await manager.start();
968
927
  const replyFn = vi.fn().mockResolvedValue(undefined);
969
- mockConnector.emit("message", {
928
+ const connector = mockConnectors.get("agent1");
929
+ connector?.emit("message", {
970
930
  agentName: "agent1",
971
931
  prompt: "Test",
972
932
  metadata: {
@@ -1000,7 +960,8 @@ describe("SlackManager (mocked @herdctl/slack)", () => {
1000
960
  await manager.initialize();
1001
961
  await manager.start();
1002
962
  const replyFn = vi.fn().mockResolvedValue(undefined);
1003
- mockConnector.emit("message", {
963
+ const connector = mockConnectors.get("agent1");
964
+ connector?.emit("message", {
1004
965
  agentName: "agent1",
1005
966
  prompt: "Test",
1006
967
  metadata: {