@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.
- package/dist/fleet-manager/__tests__/slack-manager.test.d.ts +2 -2
- package/dist/fleet-manager/__tests__/slack-manager.test.js +119 -163
- package/dist/fleet-manager/__tests__/slack-manager.test.js.map +1 -1
- package/dist/fleet-manager/event-types.d.ts +10 -4
- package/dist/fleet-manager/event-types.d.ts.map +1 -1
- package/dist/fleet-manager/slack-manager.d.ts +72 -29
- package/dist/fleet-manager/slack-manager.d.ts.map +1 -1
- package/dist/fleet-manager/slack-manager.js +152 -146
- package/dist/fleet-manager/slack-manager.js.map +1 -1
- package/dist/fleet-manager/status-queries.js +1 -1
- package/dist/fleet-manager/status-queries.js.map +1 -1
- 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
|
|
5
|
-
*
|
|
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
|
|
5
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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("
|
|
206
|
-
it("returns
|
|
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.
|
|
213
|
+
expect(manager.getConnectedCount()).toBe(0);
|
|
211
214
|
});
|
|
212
215
|
});
|
|
213
216
|
describe("getConnector", () => {
|
|
214
|
-
it("returns
|
|
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()).
|
|
221
|
+
expect(manager.getConnector("test-agent")).toBeUndefined();
|
|
219
222
|
});
|
|
220
223
|
});
|
|
221
|
-
describe("
|
|
222
|
-
it("returns empty
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
362
|
+
channels: [{ id: "C0123456789", mode: "mention" }],
|
|
353
363
|
}));
|
|
354
364
|
expect(manager.hasAgent("agent1")).toBe(true);
|
|
355
|
-
expect(manager.getConnector()).toBe(
|
|
365
|
+
expect(manager.getConnector("agent1")).toBe(mockConnectors.get("agent1"));
|
|
356
366
|
});
|
|
357
|
-
it("
|
|
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
|
-
|
|
370
|
-
expect(
|
|
371
|
-
expect(
|
|
372
|
-
expect(
|
|
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
|
|
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.
|
|
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()).
|
|
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
|
|
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
|
-
|
|
487
|
-
expect(
|
|
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
|
|
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
|
|
501
|
+
expect(mockLogger.debug).toHaveBeenCalledWith("No Slack connectors to start");
|
|
505
502
|
});
|
|
506
503
|
});
|
|
507
504
|
describe("stop", () => {
|
|
508
|
-
it("disconnects
|
|
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
|
-
|
|
516
|
-
expect(
|
|
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("
|
|
558
|
-
it("
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
598
|
-
|
|
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 '
|
|
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: "
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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 '
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
963
|
+
const connector = mockConnectors.get("agent1");
|
|
964
|
+
connector?.emit("message", {
|
|
1009
965
|
agentName: "agent1",
|
|
1010
966
|
prompt: "Test",
|
|
1011
967
|
metadata: {
|