@gethmy/mcp 2.3.0 → 2.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/dist/cli.js +80 -23
  2. package/dist/index.js +80 -23
  3. package/dist/lib/active-learning.js +939 -787
  4. package/dist/lib/api-client.js +2527 -638
  5. package/dist/lib/auto-session.js +177 -196
  6. package/dist/lib/cli.js +34954 -128
  7. package/dist/lib/config.js +235 -201
  8. package/dist/lib/consolidation.js +374 -289
  9. package/dist/lib/context-assembly.js +1265 -838
  10. package/dist/lib/graph-expansion.js +139 -155
  11. package/dist/lib/http.js +1917 -130
  12. package/dist/lib/index.js +29525 -5
  13. package/dist/lib/lifecycle-maintenance.js +663 -79
  14. package/dist/lib/memory-cleanup.js +1316 -381
  15. package/dist/lib/onboard.js +2588 -32
  16. package/dist/lib/prompt-builder.js +438 -445
  17. package/dist/lib/remote.js +31733 -143
  18. package/dist/lib/server.js +29389 -3216
  19. package/dist/lib/skills.js +315 -132
  20. package/dist/lib/tui/agents.js +128 -107
  21. package/dist/lib/tui/docs.js +1590 -687
  22. package/dist/lib/tui/setup.js +5698 -804
  23. package/dist/lib/tui/theme.js +183 -86
  24. package/dist/lib/tui/writer.js +1149 -176
  25. package/package.json +2 -2
  26. package/src/api-client.ts +37 -1
  27. package/src/memory-cleanup.ts +92 -52
  28. package/src/server.ts +16 -1
  29. package/dist/lib/__tests__/active-learning.test.js +0 -386
  30. package/dist/lib/__tests__/agent-performance-profiles.test.js +0 -325
  31. package/dist/lib/__tests__/auto-session.test.js +0 -661
  32. package/dist/lib/__tests__/context-assembly.test.js +0 -362
  33. package/dist/lib/__tests__/graph-expansion.test.js +0 -150
  34. package/dist/lib/__tests__/integration-memory-crud.test.js +0 -797
  35. package/dist/lib/__tests__/integration-memory-system.test.js +0 -281
  36. package/dist/lib/__tests__/lifecycle-maintenance.test.js +0 -207
  37. package/dist/lib/__tests__/pattern-detection.test.js +0 -295
  38. package/dist/lib/__tests__/prompt-builder.test.js +0 -418
@@ -1,661 +0,0 @@
1
- /**
2
- * Unit tests for the Auto-Session tracking system.
3
- *
4
- * Run with: bun test packages/mcp-server/src/__tests__/auto-session.test.ts
5
- */
6
- import { afterEach, describe, expect, mock, test } from "bun:test";
7
- import { AUTO_START_TRIGGERS, checkInactivity, destroyAutoSession, getActiveSessions, INACTIVITY_TIMEOUT_MS, initAutoSession, markExplicit, shutdownAllSessions, trackActivity, untrack, } from "../auto-session.js";
8
- function makeMockClient() {
9
- return {
10
- startAgentSession: mock(async () => ({ session: { id: "sess-1" } })),
11
- endAgentSession: mock(async () => ({ session: { id: "sess-1" } })),
12
- getCard: mock(async () => ({
13
- card: { id: "card-1", title: "Test", labels: [], subtasks: [] },
14
- })),
15
- getAgentSession: mock(async () => ({ session: null })),
16
- };
17
- }
18
- const CARD_A = "aaaaaaaa-1111-1111-1111-111111111111";
19
- const CARD_B = "bbbbbbbb-2222-2222-2222-222222222222";
20
- const CARD_C = "cccccccc-3333-3333-3333-333333333333";
21
- afterEach(() => {
22
- destroyAutoSession();
23
- });
24
- // ─── Basic auto-start ──────────────────────────────────────────────
25
- describe("auto-start", () => {
26
- test("triggers session on autoStart=true with cardId", async () => {
27
- const client = makeMockClient();
28
- initAutoSession(mock(async () => { }), () => client);
29
- await trackActivity(CARD_A, { autoStart: true, client: client });
30
- expect(client.startAgentSession).toHaveBeenCalledTimes(1);
31
- expect(client.startAgentSession).toHaveBeenCalledWith(CARD_A, {
32
- agentIdentifier: "unknown",
33
- agentName: "Unknown Agent",
34
- status: "working",
35
- });
36
- expect(getActiveSessions().size).toBe(1);
37
- const session = getActiveSessions().get(CARD_A);
38
- expect(session).toBeDefined();
39
- expect(session.isExplicit).toBe(false);
40
- expect(session.agentIdentifier).toBe("unknown");
41
- expect(session.agentName).toBe("Unknown Agent");
42
- });
43
- test("does NOT trigger on autoStart=false", async () => {
44
- const client = makeMockClient();
45
- initAutoSession(mock(async () => { }), () => client);
46
- await trackActivity(CARD_A, { autoStart: false, client: client });
47
- expect(client.startAgentSession).not.toHaveBeenCalled();
48
- expect(getActiveSessions().size).toBe(0);
49
- });
50
- test("does NOT trigger when options is undefined", async () => {
51
- const client = makeMockClient();
52
- initAutoSession(mock(async () => { }), () => client);
53
- await trackActivity(CARD_A);
54
- expect(client.startAgentSession).not.toHaveBeenCalled();
55
- expect(getActiveSessions().size).toBe(0);
56
- });
57
- test("does NOT trigger when no client is available", async () => {
58
- // initAutoSession not called — no clientGetter
59
- await trackActivity(CARD_A, { autoStart: true });
60
- expect(getActiveSessions().size).toBe(0);
61
- });
62
- test("still tracks locally when startAgentSession API throws", async () => {
63
- const client = makeMockClient();
64
- client.startAgentSession = mock(async () => {
65
- throw new Error("Session already exists");
66
- });
67
- initAutoSession(mock(async () => { }), () => client);
68
- await trackActivity(CARD_A, { autoStart: true, client: client });
69
- // Should still be tracked despite API error
70
- expect(getActiveSessions().size).toBe(1);
71
- expect(getActiveSessions().get(CARD_A).agentIdentifier).toBe("unknown");
72
- });
73
- test("uses clientGetter when no client in options", async () => {
74
- const client = makeMockClient();
75
- initAutoSession(mock(async () => { }), () => client);
76
- await trackActivity(CARD_A, { autoStart: true });
77
- expect(client.startAgentSession).toHaveBeenCalledTimes(1);
78
- expect(getActiveSessions().size).toBe(1);
79
- });
80
- });
81
- // ─── Activity tracking ─────────────────────────────────────────────
82
- describe("activity tracking", () => {
83
- test("repeated activity updates lastActivityAt without re-starting", async () => {
84
- const client = makeMockClient();
85
- initAutoSession(mock(async () => { }), () => client);
86
- await trackActivity(CARD_A, { autoStart: true, client: client });
87
- const firstActivity = getActiveSessions().get(CARD_A).lastActivityAt;
88
- await new Promise((r) => setTimeout(r, 10));
89
- await trackActivity(CARD_A, { autoStart: true, client: client });
90
- expect(client.startAgentSession).toHaveBeenCalledTimes(1);
91
- expect(getActiveSessions().get(CARD_A).lastActivityAt).toBeGreaterThan(firstActivity);
92
- });
93
- test("activity on existing session works even without autoStart flag", async () => {
94
- const client = makeMockClient();
95
- initAutoSession(mock(async () => { }), () => client);
96
- await trackActivity(CARD_A, { autoStart: true, client: client });
97
- const firstActivity = getActiveSessions().get(CARD_A).lastActivityAt;
98
- await new Promise((r) => setTimeout(r, 10));
99
- // Second call with autoStart=false — should still update timestamp
100
- await trackActivity(CARD_A, { autoStart: false });
101
- expect(getActiveSessions().get(CARD_A).lastActivityAt).toBeGreaterThan(firstActivity);
102
- });
103
- test("activity on existing explicit session updates timestamp", async () => {
104
- const client = makeMockClient();
105
- initAutoSession(mock(async () => { }), () => client);
106
- markExplicit(CARD_A);
107
- const firstActivity = getActiveSessions().get(CARD_A).lastActivityAt;
108
- await new Promise((r) => setTimeout(r, 10));
109
- await trackActivity(CARD_A, { autoStart: true, client: client });
110
- expect(getActiveSessions().get(CARD_A).lastActivityAt).toBeGreaterThan(firstActivity);
111
- // Should NOT have called startAgentSession (session already exists)
112
- expect(client.startAgentSession).not.toHaveBeenCalled();
113
- });
114
- });
115
- // ─── Card switching ─────────────────────────────────────────────────
116
- describe("card switching", () => {
117
- test("switching cards auto-ends previous auto-session", async () => {
118
- const client = makeMockClient();
119
- const endCb = mock(async () => { });
120
- initAutoSession(endCb, () => client);
121
- await trackActivity(CARD_A, { autoStart: true, client: client });
122
- expect(getActiveSessions().size).toBe(1);
123
- await trackActivity(CARD_B, { autoStart: true, client: client });
124
- expect(client.endAgentSession).toHaveBeenCalledWith(CARD_A, {
125
- status: "completed",
126
- });
127
- expect(endCb).toHaveBeenCalledWith(client, CARD_A, "completed");
128
- expect(getActiveSessions().has(CARD_A)).toBe(false);
129
- expect(getActiveSessions().has(CARD_B)).toBe(true);
130
- });
131
- test("explicit sessions are NOT auto-ended by card switching", async () => {
132
- const client = makeMockClient();
133
- const endCb = mock(async () => { });
134
- initAutoSession(endCb, () => client);
135
- await trackActivity(CARD_A, { autoStart: true, client: client });
136
- markExplicit(CARD_A);
137
- await trackActivity(CARD_B, { autoStart: true, client: client });
138
- // Card A should still be tracked (explicit)
139
- expect(getActiveSessions().has(CARD_A)).toBe(true);
140
- expect(getActiveSessions().has(CARD_B)).toBe(true);
141
- expect(client.endAgentSession).not.toHaveBeenCalled();
142
- expect(endCb).not.toHaveBeenCalled();
143
- });
144
- test("switching from card A to B to C ends A then B correctly", async () => {
145
- const client = makeMockClient();
146
- const endCb = mock(async () => { });
147
- initAutoSession(endCb, () => client);
148
- await trackActivity(CARD_A, { autoStart: true, client: client });
149
- await trackActivity(CARD_B, { autoStart: true, client: client });
150
- // A should be ended, B should be active
151
- expect(client.endAgentSession).toHaveBeenCalledWith(CARD_A, {
152
- status: "completed",
153
- });
154
- expect(getActiveSessions().has(CARD_A)).toBe(false);
155
- expect(getActiveSessions().has(CARD_B)).toBe(true);
156
- client.endAgentSession.mockClear();
157
- endCb.mockClear();
158
- await trackActivity(CARD_C, { autoStart: true, client: client });
159
- // B should be ended, C should be active
160
- expect(client.endAgentSession).toHaveBeenCalledWith(CARD_B, {
161
- status: "completed",
162
- });
163
- expect(getActiveSessions().has(CARD_B)).toBe(false);
164
- expect(getActiveSessions().has(CARD_C)).toBe(true);
165
- });
166
- test("card switch with endAgentSession API error still cleans up tracking", async () => {
167
- const client = makeMockClient();
168
- client.endAgentSession = mock(async () => {
169
- throw new Error("API unavailable");
170
- });
171
- const endCb = mock(async () => { });
172
- initAutoSession(endCb, () => client);
173
- await trackActivity(CARD_A, { autoStart: true, client: client });
174
- await trackActivity(CARD_B, { autoStart: true, client: client });
175
- // Card A should still be removed from tracking despite API error
176
- expect(getActiveSessions().has(CARD_A)).toBe(false);
177
- expect(getActiveSessions().has(CARD_B)).toBe(true);
178
- // Callback should still run even though endAgentSession threw
179
- expect(endCb).toHaveBeenCalledWith(client, CARD_A, "completed");
180
- });
181
- test("card switch with endCallback error still cleans up tracking", async () => {
182
- const client = makeMockClient();
183
- const endCb = mock(async () => {
184
- throw new Error("Pipeline failed");
185
- });
186
- initAutoSession(endCb, () => client);
187
- await trackActivity(CARD_A, { autoStart: true, client: client });
188
- await trackActivity(CARD_B, { autoStart: true, client: client });
189
- // Card A should be cleaned up despite callback error
190
- expect(getActiveSessions().has(CARD_A)).toBe(false);
191
- expect(getActiveSessions().has(CARD_B)).toBe(true);
192
- expect(client.endAgentSession).toHaveBeenCalledWith(CARD_A, {
193
- status: "completed",
194
- });
195
- });
196
- test("card switch with explicit session on A only ends non-explicit sessions", async () => {
197
- const client = makeMockClient();
198
- const endCb = mock(async () => { });
199
- initAutoSession(endCb, () => client);
200
- // A is explicit, B is auto
201
- markExplicit(CARD_A);
202
- await trackActivity(CARD_B, { autoStart: true, client: client });
203
- // Now switch to C — only B should be ended, A stays
204
- await trackActivity(CARD_C, { autoStart: true, client: client });
205
- expect(getActiveSessions().has(CARD_A)).toBe(true); // explicit, untouched
206
- expect(getActiveSessions().has(CARD_B)).toBe(false); // auto-ended
207
- expect(getActiveSessions().has(CARD_C)).toBe(true); // new auto
208
- expect(client.endAgentSession).toHaveBeenCalledWith(CARD_B, {
209
- status: "completed",
210
- });
211
- });
212
- });
213
- // ─── Inactivity timeout ─────────────────────────────────────────────
214
- describe("inactivity timeout", () => {
215
- test("checkInactivity ends sessions past the timeout", async () => {
216
- const client = makeMockClient();
217
- const endCb = mock(async () => { });
218
- initAutoSession(endCb, () => client);
219
- // Manually add a session with old lastActivityAt
220
- getActiveSessions().set(CARD_A, {
221
- cardId: CARD_A,
222
- startedAt: Date.now() - INACTIVITY_TIMEOUT_MS - 1000,
223
- lastActivityAt: Date.now() - INACTIVITY_TIMEOUT_MS - 1000,
224
- isExplicit: false,
225
- agentIdentifier: "unknown",
226
- agentName: "Unknown Agent",
227
- });
228
- checkInactivity();
229
- // autoEndSession is fire-and-forget in checkInactivity, wait for it
230
- await new Promise((r) => setTimeout(r, 50));
231
- expect(client.endAgentSession).toHaveBeenCalledWith(CARD_A, {
232
- status: "completed",
233
- });
234
- expect(getActiveSessions().has(CARD_A)).toBe(false);
235
- });
236
- test("checkInactivity does NOT end sessions within timeout", () => {
237
- const client = makeMockClient();
238
- initAutoSession(mock(async () => { }), () => client);
239
- getActiveSessions().set(CARD_A, {
240
- cardId: CARD_A,
241
- startedAt: Date.now(),
242
- lastActivityAt: Date.now(), // just now — well within timeout
243
- isExplicit: false,
244
- agentIdentifier: "unknown",
245
- agentName: "Unknown Agent",
246
- });
247
- checkInactivity();
248
- expect(client.endAgentSession).not.toHaveBeenCalled();
249
- expect(getActiveSessions().has(CARD_A)).toBe(true);
250
- });
251
- test("checkInactivity skips explicit sessions even if timed out", () => {
252
- const client = makeMockClient();
253
- initAutoSession(mock(async () => { }), () => client);
254
- getActiveSessions().set(CARD_A, {
255
- cardId: CARD_A,
256
- startedAt: Date.now() - INACTIVITY_TIMEOUT_MS - 60000,
257
- lastActivityAt: Date.now() - INACTIVITY_TIMEOUT_MS - 60000,
258
- isExplicit: true,
259
- agentIdentifier: "explicit",
260
- agentName: "Explicit Agent",
261
- });
262
- checkInactivity();
263
- expect(client.endAgentSession).not.toHaveBeenCalled();
264
- expect(getActiveSessions().has(CARD_A)).toBe(true);
265
- });
266
- test("checkInactivity handles mix of timed-out and active sessions", async () => {
267
- const client = makeMockClient();
268
- const endCb = mock(async () => { });
269
- initAutoSession(endCb, () => client);
270
- // CARD_A — timed out
271
- getActiveSessions().set(CARD_A, {
272
- cardId: CARD_A,
273
- startedAt: Date.now() - INACTIVITY_TIMEOUT_MS - 5000,
274
- lastActivityAt: Date.now() - INACTIVITY_TIMEOUT_MS - 5000,
275
- isExplicit: false,
276
- agentIdentifier: "unknown",
277
- agentName: "Unknown Agent",
278
- });
279
- // CARD_B — still active
280
- getActiveSessions().set(CARD_B, {
281
- cardId: CARD_B,
282
- startedAt: Date.now() - 1000,
283
- lastActivityAt: Date.now() - 1000,
284
- isExplicit: false,
285
- agentIdentifier: "unknown",
286
- agentName: "Unknown Agent",
287
- });
288
- // CARD_C — timed out but explicit
289
- getActiveSessions().set(CARD_C, {
290
- cardId: CARD_C,
291
- startedAt: Date.now() - INACTIVITY_TIMEOUT_MS - 5000,
292
- lastActivityAt: Date.now() - INACTIVITY_TIMEOUT_MS - 5000,
293
- isExplicit: true,
294
- agentIdentifier: "explicit",
295
- agentName: "Explicit Agent",
296
- });
297
- checkInactivity();
298
- await new Promise((r) => setTimeout(r, 50));
299
- // Only CARD_A should be ended
300
- expect(getActiveSessions().has(CARD_A)).toBe(false);
301
- expect(getActiveSessions().has(CARD_B)).toBe(true);
302
- expect(getActiveSessions().has(CARD_C)).toBe(true);
303
- expect(client.endAgentSession).toHaveBeenCalledTimes(1);
304
- expect(client.endAgentSession).toHaveBeenCalledWith(CARD_A, {
305
- status: "completed",
306
- });
307
- });
308
- test("checkInactivity is a no-op when no client is available", () => {
309
- // No initAutoSession — clientGetter is null
310
- getActiveSessions().set(CARD_A, {
311
- cardId: CARD_A,
312
- startedAt: 0,
313
- lastActivityAt: 0,
314
- isExplicit: false,
315
- agentIdentifier: "unknown",
316
- agentName: "Unknown Agent",
317
- });
318
- // Should not throw
319
- checkInactivity();
320
- // Session should still be there (can't end without client)
321
- expect(getActiveSessions().has(CARD_A)).toBe(true);
322
- });
323
- test("activity resets the inactivity timer", async () => {
324
- const client = makeMockClient();
325
- const endCb = mock(async () => { });
326
- initAutoSession(endCb, () => client);
327
- // Start with old lastActivityAt
328
- getActiveSessions().set(CARD_A, {
329
- cardId: CARD_A,
330
- startedAt: Date.now() - INACTIVITY_TIMEOUT_MS - 5000,
331
- lastActivityAt: Date.now() - INACTIVITY_TIMEOUT_MS - 5000,
332
- isExplicit: false,
333
- agentIdentifier: "unknown",
334
- agentName: "Unknown Agent",
335
- });
336
- // Refresh activity
337
- await trackActivity(CARD_A, { autoStart: false });
338
- // Now check — should NOT be timed out
339
- checkInactivity();
340
- await new Promise((r) => setTimeout(r, 50));
341
- expect(getActiveSessions().has(CARD_A)).toBe(true);
342
- expect(client.endAgentSession).not.toHaveBeenCalled();
343
- });
344
- });
345
- // ─── markExplicit ───────────────────────────────────────────────────
346
- describe("markExplicit", () => {
347
- test("marks existing auto-session as explicit", async () => {
348
- const client = makeMockClient();
349
- initAutoSession(mock(async () => { }), () => client);
350
- await trackActivity(CARD_A, { autoStart: true, client: client });
351
- expect(getActiveSessions().get(CARD_A).isExplicit).toBe(false);
352
- markExplicit(CARD_A);
353
- expect(getActiveSessions().get(CARD_A).isExplicit).toBe(true);
354
- // agentIdentifier should remain "unknown" since no clientInfo was provided
355
- expect(getActiveSessions().get(CARD_A).agentIdentifier).toBe("unknown");
356
- });
357
- test("creates new tracking entry for unknown card", () => {
358
- initAutoSession(mock(async () => { }), () => makeMockClient());
359
- markExplicit(CARD_A);
360
- expect(getActiveSessions().has(CARD_A)).toBe(true);
361
- expect(getActiveSessions().get(CARD_A).isExplicit).toBe(true);
362
- expect(getActiveSessions().get(CARD_A).agentIdentifier).toBe("explicit");
363
- });
364
- test("double markExplicit is idempotent", async () => {
365
- const client = makeMockClient();
366
- initAutoSession(mock(async () => { }), () => client);
367
- await trackActivity(CARD_A, { autoStart: true, client: client });
368
- markExplicit(CARD_A);
369
- markExplicit(CARD_A);
370
- expect(getActiveSessions().get(CARD_A).isExplicit).toBe(true);
371
- expect(getActiveSessions().size).toBe(1);
372
- });
373
- });
374
- // ─── untrack ────────────────────────────────────────────────────────
375
- describe("untrack", () => {
376
- test("removes session from tracking", async () => {
377
- const client = makeMockClient();
378
- initAutoSession(mock(async () => { }), () => client);
379
- await trackActivity(CARD_A, { autoStart: true, client: client });
380
- expect(getActiveSessions().has(CARD_A)).toBe(true);
381
- untrack(CARD_A);
382
- expect(getActiveSessions().has(CARD_A)).toBe(false);
383
- });
384
- test("is a no-op for non-existent cardId", () => {
385
- initAutoSession(mock(async () => { }), () => makeMockClient());
386
- // Should not throw
387
- untrack(CARD_A);
388
- expect(getActiveSessions().size).toBe(0);
389
- });
390
- test("removes explicit sessions too", () => {
391
- initAutoSession(mock(async () => { }), () => makeMockClient());
392
- markExplicit(CARD_A);
393
- expect(getActiveSessions().has(CARD_A)).toBe(true);
394
- untrack(CARD_A);
395
- expect(getActiveSessions().has(CARD_A)).toBe(false);
396
- });
397
- });
398
- // ─── shutdown ───────────────────────────────────────────────────────
399
- describe("shutdown", () => {
400
- test("ends all active sessions with paused status", async () => {
401
- const client = makeMockClient();
402
- const endCb = mock(async () => { });
403
- initAutoSession(endCb, () => client);
404
- getActiveSessions().set(CARD_A, {
405
- cardId: CARD_A,
406
- startedAt: Date.now(),
407
- lastActivityAt: Date.now(),
408
- isExplicit: false,
409
- agentIdentifier: "unknown",
410
- agentName: "Unknown Agent",
411
- });
412
- await shutdownAllSessions();
413
- expect(client.endAgentSession).toHaveBeenCalledWith(CARD_A, {
414
- status: "paused",
415
- });
416
- expect(endCb).toHaveBeenCalledWith(client, CARD_A, "paused");
417
- expect(getActiveSessions().size).toBe(0);
418
- });
419
- test("ends multiple sessions including explicit ones", async () => {
420
- const client = makeMockClient();
421
- const endCb = mock(async () => { });
422
- initAutoSession(endCb, () => client);
423
- getActiveSessions().set(CARD_A, {
424
- cardId: CARD_A,
425
- startedAt: Date.now(),
426
- lastActivityAt: Date.now(),
427
- isExplicit: false,
428
- agentIdentifier: "unknown",
429
- agentName: "Unknown Agent",
430
- });
431
- getActiveSessions().set(CARD_B, {
432
- cardId: CARD_B,
433
- startedAt: Date.now(),
434
- lastActivityAt: Date.now(),
435
- isExplicit: true,
436
- agentIdentifier: "explicit",
437
- agentName: "Explicit Agent",
438
- });
439
- await shutdownAllSessions();
440
- // Both should be ended on shutdown (even explicit)
441
- expect(client.endAgentSession).toHaveBeenCalledTimes(2);
442
- expect(endCb).toHaveBeenCalledTimes(2);
443
- expect(getActiveSessions().size).toBe(0);
444
- });
445
- test("is a no-op when no client is available", async () => {
446
- // No initAutoSession called
447
- getActiveSessions().set(CARD_A, {
448
- cardId: CARD_A,
449
- startedAt: Date.now(),
450
- lastActivityAt: Date.now(),
451
- isExplicit: false,
452
- agentIdentifier: "unknown",
453
- agentName: "Unknown Agent",
454
- });
455
- // Should not throw
456
- await shutdownAllSessions();
457
- // Session still tracked (couldn't end without client)
458
- expect(getActiveSessions().has(CARD_A)).toBe(true);
459
- });
460
- test("is resilient when endAgentSession throws for some sessions", async () => {
461
- let callCount = 0;
462
- const client = makeMockClient();
463
- client.endAgentSession = mock(async () => {
464
- callCount++;
465
- if (callCount === 1)
466
- throw new Error("Network error");
467
- return { session: { id: "sess-1" } };
468
- });
469
- const endCb = mock(async () => { });
470
- initAutoSession(endCb, () => client);
471
- getActiveSessions().set(CARD_A, {
472
- cardId: CARD_A,
473
- startedAt: Date.now(),
474
- lastActivityAt: Date.now(),
475
- isExplicit: false,
476
- agentIdentifier: "unknown",
477
- agentName: "Unknown Agent",
478
- });
479
- getActiveSessions().set(CARD_B, {
480
- cardId: CARD_B,
481
- startedAt: Date.now(),
482
- lastActivityAt: Date.now(),
483
- isExplicit: false,
484
- agentIdentifier: "unknown",
485
- agentName: "Unknown Agent",
486
- });
487
- await shutdownAllSessions();
488
- // Both should be removed from tracking despite first API error
489
- expect(getActiveSessions().size).toBe(0);
490
- // Callback should still be called for both
491
- expect(endCb).toHaveBeenCalledTimes(2);
492
- });
493
- test("empty sessions map is a no-op", async () => {
494
- const client = makeMockClient();
495
- initAutoSession(mock(async () => { }), () => client);
496
- await shutdownAllSessions();
497
- expect(client.endAgentSession).not.toHaveBeenCalled();
498
- });
499
- });
500
- // ─── destroyAutoSession ─────────────────────────────────────────────
501
- describe("destroyAutoSession", () => {
502
- test("clears all state", async () => {
503
- const client = makeMockClient();
504
- initAutoSession(mock(async () => { }), () => client);
505
- await trackActivity(CARD_A, { autoStart: true, client: client });
506
- expect(getActiveSessions().size).toBe(1);
507
- destroyAutoSession();
508
- expect(getActiveSessions().size).toBe(0);
509
- });
510
- test("calling destroyAutoSession twice is safe", () => {
511
- initAutoSession(mock(async () => { }), () => makeMockClient());
512
- destroyAutoSession();
513
- destroyAutoSession();
514
- expect(getActiveSessions().size).toBe(0);
515
- });
516
- test("re-initialization after destroy works", async () => {
517
- const client = makeMockClient();
518
- initAutoSession(mock(async () => { }), () => client);
519
- await trackActivity(CARD_A, { autoStart: true, client: client });
520
- destroyAutoSession();
521
- // Re-init
522
- const client2 = makeMockClient();
523
- initAutoSession(mock(async () => { }), () => client2);
524
- await trackActivity(CARD_B, { autoStart: true, client: client2 });
525
- expect(getActiveSessions().size).toBe(1);
526
- expect(getActiveSessions().has(CARD_B)).toBe(true);
527
- expect(client2.startAgentSession).toHaveBeenCalledTimes(1);
528
- });
529
- });
530
- // ─── AUTO_START_TRIGGERS ────────────────────────────────────────────
531
- describe("AUTO_START_TRIGGERS", () => {
532
- test("contains all card-mutating tools", () => {
533
- const expected = [
534
- "harmony_generate_prompt",
535
- "harmony_update_card",
536
- "harmony_move_card",
537
- "harmony_create_subtask",
538
- "harmony_toggle_subtask",
539
- "harmony_add_label_to_card",
540
- "harmony_remove_label_from_card",
541
- ];
542
- for (const tool of expected) {
543
- expect(AUTO_START_TRIGGERS.has(tool)).toBe(true);
544
- }
545
- expect(AUTO_START_TRIGGERS.size).toBe(expected.length);
546
- });
547
- test("does NOT contain read-only tools", () => {
548
- const readOnly = [
549
- "harmony_get_card",
550
- "harmony_get_card_by_short_id",
551
- "harmony_get_board",
552
- "harmony_search_cards",
553
- "harmony_get_agent_session",
554
- "harmony_get_card_links",
555
- "harmony_list_workspaces",
556
- "harmony_list_projects",
557
- "harmony_get_context",
558
- ];
559
- for (const tool of readOnly) {
560
- expect(AUTO_START_TRIGGERS.has(tool)).toBe(false);
561
- }
562
- });
563
- test("does NOT contain session management tools", () => {
564
- expect(AUTO_START_TRIGGERS.has("harmony_start_agent_session")).toBe(false);
565
- expect(AUTO_START_TRIGGERS.has("harmony_end_agent_session")).toBe(false);
566
- expect(AUTO_START_TRIGGERS.has("harmony_update_agent_progress")).toBe(false);
567
- });
568
- });
569
- // ─── INACTIVITY_TIMEOUT_MS ──────────────────────────────────────────
570
- describe("INACTIVITY_TIMEOUT_MS", () => {
571
- test("is 10 minutes", () => {
572
- expect(INACTIVITY_TIMEOUT_MS).toBe(10 * 60 * 1000);
573
- });
574
- });
575
- // ─── Edge cases / race conditions ───────────────────────────────────
576
- describe("edge cases", () => {
577
- test("trackActivity before initAutoSession uses client from options", async () => {
578
- // No initAutoSession called
579
- const client = makeMockClient();
580
- await trackActivity(CARD_A, { autoStart: true, client: client });
581
- expect(client.startAgentSession).toHaveBeenCalledTimes(1);
582
- expect(getActiveSessions().size).toBe(1);
583
- });
584
- test("re-initialization clears old interval timer", async () => {
585
- const client1 = makeMockClient();
586
- const endCb1 = mock(async () => { });
587
- initAutoSession(endCb1, () => client1);
588
- // Re-initialize
589
- const client2 = makeMockClient();
590
- const endCb2 = mock(async () => { });
591
- initAutoSession(endCb2, () => client2);
592
- // Add stale session and trigger check
593
- getActiveSessions().set(CARD_A, {
594
- cardId: CARD_A,
595
- startedAt: 0,
596
- lastActivityAt: 0,
597
- isExplicit: false,
598
- agentIdentifier: "unknown",
599
- agentName: "Unknown Agent",
600
- });
601
- checkInactivity();
602
- await new Promise((r) => setTimeout(r, 50));
603
- // Should use client2, not client1
604
- expect(client2.endAgentSession).toHaveBeenCalledTimes(1);
605
- expect(client1.endAgentSession).not.toHaveBeenCalled();
606
- });
607
- test("endCallback throwing does not prevent session removal", async () => {
608
- const client = makeMockClient();
609
- const endCb = mock(async () => {
610
- throw new Error("Callback crash");
611
- });
612
- initAutoSession(endCb, () => client);
613
- getActiveSessions().set(CARD_A, {
614
- cardId: CARD_A,
615
- startedAt: 0,
616
- lastActivityAt: 0,
617
- isExplicit: false,
618
- agentIdentifier: "unknown",
619
- agentName: "Unknown Agent",
620
- });
621
- checkInactivity();
622
- await new Promise((r) => setTimeout(r, 50));
623
- // Session should be removed despite callback error
624
- expect(getActiveSessions().has(CARD_A)).toBe(false);
625
- });
626
- test("both endAgentSession and endCallback throwing still cleans up", async () => {
627
- const client = makeMockClient();
628
- client.endAgentSession = mock(async () => {
629
- throw new Error("API down");
630
- });
631
- const endCb = mock(async () => {
632
- throw new Error("Pipeline crash");
633
- });
634
- initAutoSession(endCb, () => client);
635
- await trackActivity(CARD_A, { autoStart: true, client: client });
636
- await trackActivity(CARD_B, { autoStart: true, client: client });
637
- // Despite both throwing, tracking should be clean
638
- expect(getActiveSessions().has(CARD_A)).toBe(false);
639
- expect(getActiveSessions().has(CARD_B)).toBe(true);
640
- });
641
- test("concurrent auto-start on same card only starts once", async () => {
642
- const client = makeMockClient();
643
- // Slow startAgentSession to create a window for concurrent calls
644
- client.startAgentSession = mock(async () => new Promise((resolve) => setTimeout(() => resolve({ session: { id: "sess-1" } }), 20)));
645
- initAutoSession(mock(async () => { }), () => client);
646
- // Fire two concurrent auto-starts for the same card
647
- const p1 = trackActivity(CARD_A, {
648
- autoStart: true,
649
- client: client,
650
- });
651
- const p2 = trackActivity(CARD_A, {
652
- autoStart: true,
653
- client: client,
654
- });
655
- await Promise.all([p1, p2]);
656
- // First call creates session, second call hits early return (existing)
657
- // but due to async, both might race. At minimum, session should exist once.
658
- expect(getActiveSessions().size).toBe(1);
659
- expect(getActiveSessions().has(CARD_A)).toBe(true);
660
- });
661
- });