@gethmy/mcp 2.5.0 → 2.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -19
- package/dist/cli.js +1216 -32028
- package/dist/index.js +923 -28746
- package/package.json +3 -2
- package/dist/http.js +0 -1959
- package/dist/remote.js +0 -32328
- package/dist/server.js +0 -31967
- package/src/__tests__/auto-session.test.ts +0 -912
- package/src/__tests__/graph-expansion.test.ts +0 -285
- package/src/__tests__/integration-memory-crud.test.ts +0 -948
- package/src/__tests__/integration-memory-system.test.ts +0 -321
- package/src/__tests__/mcp-integration.test.ts +0 -141
- package/src/__tests__/memory-floor.test.ts +0 -126
- package/src/__tests__/memory-park.test.ts +0 -213
- package/src/__tests__/memory-session.test.ts +0 -77
- package/src/__tests__/prompt-builder.test.ts +0 -739
- package/src/__tests__/remote-routing.test.ts +0 -285
- package/src/__tests__/skills.test.ts +0 -111
- package/src/__tests__/tool-dispatch.test.ts +0 -260
|
@@ -1,912 +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
|
-
|
|
7
|
-
import { afterEach, describe, expect, mock, test } from "bun:test";
|
|
8
|
-
import {
|
|
9
|
-
AUTO_START_TRIGGERS,
|
|
10
|
-
checkInactivity,
|
|
11
|
-
destroyAutoSession,
|
|
12
|
-
getActiveSessions,
|
|
13
|
-
INACTIVITY_TIMEOUT_MS,
|
|
14
|
-
initAutoSession,
|
|
15
|
-
markExplicit,
|
|
16
|
-
shutdownAllSessions,
|
|
17
|
-
trackActivity,
|
|
18
|
-
untrack,
|
|
19
|
-
} from "../auto-session.js";
|
|
20
|
-
|
|
21
|
-
function makeMockClient() {
|
|
22
|
-
return {
|
|
23
|
-
startAgentSession: mock(async () => ({ session: { id: "sess-1" } })),
|
|
24
|
-
endAgentSession: mock(async () => ({ session: { id: "sess-1" } })),
|
|
25
|
-
getCard: mock(async () => ({
|
|
26
|
-
card: { id: "card-1", title: "Test", labels: [], subtasks: [] },
|
|
27
|
-
})),
|
|
28
|
-
getAgentSession: mock(async () => ({ session: null })),
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const CARD_A = "aaaaaaaa-1111-1111-1111-111111111111";
|
|
33
|
-
const CARD_B = "bbbbbbbb-2222-2222-2222-222222222222";
|
|
34
|
-
const CARD_C = "cccccccc-3333-3333-3333-333333333333";
|
|
35
|
-
|
|
36
|
-
afterEach(() => {
|
|
37
|
-
destroyAutoSession();
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
// ─── Basic auto-start ──────────────────────────────────────────────
|
|
41
|
-
|
|
42
|
-
describe("auto-start", () => {
|
|
43
|
-
test("triggers session on autoStart=true with cardId", async () => {
|
|
44
|
-
const client = makeMockClient();
|
|
45
|
-
initAutoSession(
|
|
46
|
-
mock(async () => {}),
|
|
47
|
-
() => client as any,
|
|
48
|
-
);
|
|
49
|
-
|
|
50
|
-
await trackActivity(CARD_A, { autoStart: true, client: client as any });
|
|
51
|
-
|
|
52
|
-
expect(client.startAgentSession).toHaveBeenCalledTimes(1);
|
|
53
|
-
expect(client.startAgentSession).toHaveBeenCalledWith(CARD_A, {
|
|
54
|
-
agentIdentifier: "unknown",
|
|
55
|
-
agentName: "Unknown Agent",
|
|
56
|
-
status: "working",
|
|
57
|
-
});
|
|
58
|
-
expect(getActiveSessions().size).toBe(1);
|
|
59
|
-
const session = getActiveSessions().get(CARD_A);
|
|
60
|
-
expect(session).toBeDefined();
|
|
61
|
-
expect(session!.isExplicit).toBe(false);
|
|
62
|
-
expect(session!.agentIdentifier).toBe("unknown");
|
|
63
|
-
expect(session!.agentName).toBe("Unknown Agent");
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
test("does NOT trigger on autoStart=false", async () => {
|
|
67
|
-
const client = makeMockClient();
|
|
68
|
-
initAutoSession(
|
|
69
|
-
mock(async () => {}),
|
|
70
|
-
() => client as any,
|
|
71
|
-
);
|
|
72
|
-
|
|
73
|
-
await trackActivity(CARD_A, { autoStart: false, client: client as any });
|
|
74
|
-
|
|
75
|
-
expect(client.startAgentSession).not.toHaveBeenCalled();
|
|
76
|
-
expect(getActiveSessions().size).toBe(0);
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
test("does NOT trigger when options is undefined", async () => {
|
|
80
|
-
const client = makeMockClient();
|
|
81
|
-
initAutoSession(
|
|
82
|
-
mock(async () => {}),
|
|
83
|
-
() => client as any,
|
|
84
|
-
);
|
|
85
|
-
|
|
86
|
-
await trackActivity(CARD_A);
|
|
87
|
-
|
|
88
|
-
expect(client.startAgentSession).not.toHaveBeenCalled();
|
|
89
|
-
expect(getActiveSessions().size).toBe(0);
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
test("does NOT trigger when no client is available", async () => {
|
|
93
|
-
// initAutoSession not called — no clientGetter
|
|
94
|
-
await trackActivity(CARD_A, { autoStart: true });
|
|
95
|
-
expect(getActiveSessions().size).toBe(0);
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
test("still tracks locally when startAgentSession API throws", async () => {
|
|
99
|
-
const client = makeMockClient();
|
|
100
|
-
client.startAgentSession = mock(async () => {
|
|
101
|
-
throw new Error("Session already exists");
|
|
102
|
-
});
|
|
103
|
-
initAutoSession(
|
|
104
|
-
mock(async () => {}),
|
|
105
|
-
() => client as any,
|
|
106
|
-
);
|
|
107
|
-
|
|
108
|
-
await trackActivity(CARD_A, { autoStart: true, client: client as any });
|
|
109
|
-
|
|
110
|
-
// Should still be tracked despite API error
|
|
111
|
-
expect(getActiveSessions().size).toBe(1);
|
|
112
|
-
expect(getActiveSessions().get(CARD_A)!.agentIdentifier).toBe("unknown");
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
test("uses clientGetter when no client in options", async () => {
|
|
116
|
-
const client = makeMockClient();
|
|
117
|
-
initAutoSession(
|
|
118
|
-
mock(async () => {}),
|
|
119
|
-
() => client as any,
|
|
120
|
-
);
|
|
121
|
-
|
|
122
|
-
await trackActivity(CARD_A, { autoStart: true });
|
|
123
|
-
|
|
124
|
-
expect(client.startAgentSession).toHaveBeenCalledTimes(1);
|
|
125
|
-
expect(getActiveSessions().size).toBe(1);
|
|
126
|
-
});
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
// ─── Activity tracking ─────────────────────────────────────────────
|
|
130
|
-
|
|
131
|
-
describe("activity tracking", () => {
|
|
132
|
-
test("repeated activity updates lastActivityAt without re-starting", async () => {
|
|
133
|
-
const client = makeMockClient();
|
|
134
|
-
initAutoSession(
|
|
135
|
-
mock(async () => {}),
|
|
136
|
-
() => client as any,
|
|
137
|
-
);
|
|
138
|
-
|
|
139
|
-
await trackActivity(CARD_A, { autoStart: true, client: client as any });
|
|
140
|
-
const firstActivity = getActiveSessions().get(CARD_A)!.lastActivityAt;
|
|
141
|
-
|
|
142
|
-
await new Promise((r) => setTimeout(r, 10));
|
|
143
|
-
|
|
144
|
-
await trackActivity(CARD_A, { autoStart: true, client: client as any });
|
|
145
|
-
|
|
146
|
-
expect(client.startAgentSession).toHaveBeenCalledTimes(1);
|
|
147
|
-
expect(getActiveSessions().get(CARD_A)!.lastActivityAt).toBeGreaterThan(
|
|
148
|
-
firstActivity,
|
|
149
|
-
);
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
test("activity on existing session works even without autoStart flag", async () => {
|
|
153
|
-
const client = makeMockClient();
|
|
154
|
-
initAutoSession(
|
|
155
|
-
mock(async () => {}),
|
|
156
|
-
() => client as any,
|
|
157
|
-
);
|
|
158
|
-
|
|
159
|
-
await trackActivity(CARD_A, { autoStart: true, client: client as any });
|
|
160
|
-
const firstActivity = getActiveSessions().get(CARD_A)!.lastActivityAt;
|
|
161
|
-
|
|
162
|
-
await new Promise((r) => setTimeout(r, 10));
|
|
163
|
-
|
|
164
|
-
// Second call with autoStart=false — should still update timestamp
|
|
165
|
-
await trackActivity(CARD_A, { autoStart: false });
|
|
166
|
-
|
|
167
|
-
expect(getActiveSessions().get(CARD_A)!.lastActivityAt).toBeGreaterThan(
|
|
168
|
-
firstActivity,
|
|
169
|
-
);
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
test("activity on existing explicit session updates timestamp", async () => {
|
|
173
|
-
const client = makeMockClient();
|
|
174
|
-
initAutoSession(
|
|
175
|
-
mock(async () => {}),
|
|
176
|
-
() => client as any,
|
|
177
|
-
);
|
|
178
|
-
|
|
179
|
-
markExplicit(CARD_A);
|
|
180
|
-
const firstActivity = getActiveSessions().get(CARD_A)!.lastActivityAt;
|
|
181
|
-
|
|
182
|
-
await new Promise((r) => setTimeout(r, 10));
|
|
183
|
-
|
|
184
|
-
await trackActivity(CARD_A, { autoStart: true, client: client as any });
|
|
185
|
-
|
|
186
|
-
expect(getActiveSessions().get(CARD_A)!.lastActivityAt).toBeGreaterThan(
|
|
187
|
-
firstActivity,
|
|
188
|
-
);
|
|
189
|
-
// Should NOT have called startAgentSession (session already exists)
|
|
190
|
-
expect(client.startAgentSession).not.toHaveBeenCalled();
|
|
191
|
-
});
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
// ─── Card switching ─────────────────────────────────────────────────
|
|
195
|
-
|
|
196
|
-
describe("card switching", () => {
|
|
197
|
-
test("switching cards auto-ends previous auto-session", async () => {
|
|
198
|
-
const client = makeMockClient();
|
|
199
|
-
const endCb = mock(async () => {});
|
|
200
|
-
initAutoSession(endCb, () => client as any);
|
|
201
|
-
|
|
202
|
-
await trackActivity(CARD_A, { autoStart: true, client: client as any });
|
|
203
|
-
expect(getActiveSessions().size).toBe(1);
|
|
204
|
-
|
|
205
|
-
await trackActivity(CARD_B, { autoStart: true, client: client as any });
|
|
206
|
-
|
|
207
|
-
expect(client.endAgentSession).toHaveBeenCalledWith(CARD_A, {
|
|
208
|
-
status: "completed",
|
|
209
|
-
});
|
|
210
|
-
expect(endCb).toHaveBeenCalledWith(client, CARD_A, "completed");
|
|
211
|
-
expect(getActiveSessions().has(CARD_A)).toBe(false);
|
|
212
|
-
expect(getActiveSessions().has(CARD_B)).toBe(true);
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
test("explicit sessions are NOT auto-ended by card switching", async () => {
|
|
216
|
-
const client = makeMockClient();
|
|
217
|
-
const endCb = mock(async () => {});
|
|
218
|
-
initAutoSession(endCb, () => client as any);
|
|
219
|
-
|
|
220
|
-
await trackActivity(CARD_A, { autoStart: true, client: client as any });
|
|
221
|
-
markExplicit(CARD_A);
|
|
222
|
-
|
|
223
|
-
await trackActivity(CARD_B, { autoStart: true, client: client as any });
|
|
224
|
-
|
|
225
|
-
// Card A should still be tracked (explicit)
|
|
226
|
-
expect(getActiveSessions().has(CARD_A)).toBe(true);
|
|
227
|
-
expect(getActiveSessions().has(CARD_B)).toBe(true);
|
|
228
|
-
expect(client.endAgentSession).not.toHaveBeenCalled();
|
|
229
|
-
expect(endCb).not.toHaveBeenCalled();
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
test("switching from card A to B to C ends A then B correctly", async () => {
|
|
233
|
-
const client = makeMockClient();
|
|
234
|
-
const endCb = mock(async () => {});
|
|
235
|
-
initAutoSession(endCb, () => client as any);
|
|
236
|
-
|
|
237
|
-
await trackActivity(CARD_A, { autoStart: true, client: client as any });
|
|
238
|
-
await trackActivity(CARD_B, { autoStart: true, client: client as any });
|
|
239
|
-
|
|
240
|
-
// A should be ended, B should be active
|
|
241
|
-
expect(client.endAgentSession).toHaveBeenCalledWith(CARD_A, {
|
|
242
|
-
status: "completed",
|
|
243
|
-
});
|
|
244
|
-
expect(getActiveSessions().has(CARD_A)).toBe(false);
|
|
245
|
-
expect(getActiveSessions().has(CARD_B)).toBe(true);
|
|
246
|
-
|
|
247
|
-
client.endAgentSession.mockClear();
|
|
248
|
-
endCb.mockClear();
|
|
249
|
-
|
|
250
|
-
await trackActivity(CARD_C, { autoStart: true, client: client as any });
|
|
251
|
-
|
|
252
|
-
// B should be ended, C should be active
|
|
253
|
-
expect(client.endAgentSession).toHaveBeenCalledWith(CARD_B, {
|
|
254
|
-
status: "completed",
|
|
255
|
-
});
|
|
256
|
-
expect(getActiveSessions().has(CARD_B)).toBe(false);
|
|
257
|
-
expect(getActiveSessions().has(CARD_C)).toBe(true);
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
test("card switch with endAgentSession API error still cleans up tracking", async () => {
|
|
261
|
-
const client = makeMockClient();
|
|
262
|
-
client.endAgentSession = mock(async () => {
|
|
263
|
-
throw new Error("API unavailable");
|
|
264
|
-
});
|
|
265
|
-
const endCb = mock(async () => {});
|
|
266
|
-
initAutoSession(endCb, () => client as any);
|
|
267
|
-
|
|
268
|
-
await trackActivity(CARD_A, { autoStart: true, client: client as any });
|
|
269
|
-
await trackActivity(CARD_B, { autoStart: true, client: client as any });
|
|
270
|
-
|
|
271
|
-
// Card A should still be removed from tracking despite API error
|
|
272
|
-
expect(getActiveSessions().has(CARD_A)).toBe(false);
|
|
273
|
-
expect(getActiveSessions().has(CARD_B)).toBe(true);
|
|
274
|
-
// Callback should still run even though endAgentSession threw
|
|
275
|
-
expect(endCb).toHaveBeenCalledWith(client, CARD_A, "completed");
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
test("card switch with endCallback error still cleans up tracking", async () => {
|
|
279
|
-
const client = makeMockClient();
|
|
280
|
-
const endCb = mock(async () => {
|
|
281
|
-
throw new Error("Pipeline failed");
|
|
282
|
-
});
|
|
283
|
-
initAutoSession(endCb, () => client as any);
|
|
284
|
-
|
|
285
|
-
await trackActivity(CARD_A, { autoStart: true, client: client as any });
|
|
286
|
-
await trackActivity(CARD_B, { autoStart: true, client: client as any });
|
|
287
|
-
|
|
288
|
-
// Card A should be cleaned up despite callback error
|
|
289
|
-
expect(getActiveSessions().has(CARD_A)).toBe(false);
|
|
290
|
-
expect(getActiveSessions().has(CARD_B)).toBe(true);
|
|
291
|
-
expect(client.endAgentSession).toHaveBeenCalledWith(CARD_A, {
|
|
292
|
-
status: "completed",
|
|
293
|
-
});
|
|
294
|
-
});
|
|
295
|
-
|
|
296
|
-
test("card switch with explicit session on A only ends non-explicit sessions", async () => {
|
|
297
|
-
const client = makeMockClient();
|
|
298
|
-
const endCb = mock(async () => {});
|
|
299
|
-
initAutoSession(endCb, () => client as any);
|
|
300
|
-
|
|
301
|
-
// A is explicit, B is auto
|
|
302
|
-
markExplicit(CARD_A);
|
|
303
|
-
await trackActivity(CARD_B, { autoStart: true, client: client as any });
|
|
304
|
-
|
|
305
|
-
// Now switch to C — only B should be ended, A stays
|
|
306
|
-
await trackActivity(CARD_C, { autoStart: true, client: client as any });
|
|
307
|
-
|
|
308
|
-
expect(getActiveSessions().has(CARD_A)).toBe(true); // explicit, untouched
|
|
309
|
-
expect(getActiveSessions().has(CARD_B)).toBe(false); // auto-ended
|
|
310
|
-
expect(getActiveSessions().has(CARD_C)).toBe(true); // new auto
|
|
311
|
-
expect(client.endAgentSession).toHaveBeenCalledWith(CARD_B, {
|
|
312
|
-
status: "completed",
|
|
313
|
-
});
|
|
314
|
-
});
|
|
315
|
-
});
|
|
316
|
-
|
|
317
|
-
// ─── Inactivity timeout ─────────────────────────────────────────────
|
|
318
|
-
|
|
319
|
-
describe("inactivity timeout", () => {
|
|
320
|
-
test("checkInactivity ends sessions past the timeout", async () => {
|
|
321
|
-
const client = makeMockClient();
|
|
322
|
-
const endCb = mock(async () => {});
|
|
323
|
-
initAutoSession(endCb, () => client as any);
|
|
324
|
-
|
|
325
|
-
// Manually add a session with old lastActivityAt
|
|
326
|
-
getActiveSessions().set(CARD_A, {
|
|
327
|
-
cardId: CARD_A,
|
|
328
|
-
startedAt: Date.now() - INACTIVITY_TIMEOUT_MS - 1000,
|
|
329
|
-
lastActivityAt: Date.now() - INACTIVITY_TIMEOUT_MS - 1000,
|
|
330
|
-
isExplicit: false,
|
|
331
|
-
agentIdentifier: "unknown",
|
|
332
|
-
agentName: "Unknown Agent",
|
|
333
|
-
});
|
|
334
|
-
|
|
335
|
-
checkInactivity();
|
|
336
|
-
|
|
337
|
-
// autoEndSession is fire-and-forget in checkInactivity, wait for it
|
|
338
|
-
await new Promise((r) => setTimeout(r, 50));
|
|
339
|
-
|
|
340
|
-
expect(client.endAgentSession).toHaveBeenCalledWith(CARD_A, {
|
|
341
|
-
status: "completed",
|
|
342
|
-
});
|
|
343
|
-
expect(getActiveSessions().has(CARD_A)).toBe(false);
|
|
344
|
-
});
|
|
345
|
-
|
|
346
|
-
test("checkInactivity does NOT end sessions within timeout", () => {
|
|
347
|
-
const client = makeMockClient();
|
|
348
|
-
initAutoSession(
|
|
349
|
-
mock(async () => {}),
|
|
350
|
-
() => client as any,
|
|
351
|
-
);
|
|
352
|
-
|
|
353
|
-
getActiveSessions().set(CARD_A, {
|
|
354
|
-
cardId: CARD_A,
|
|
355
|
-
startedAt: Date.now(),
|
|
356
|
-
lastActivityAt: Date.now(), // just now — well within timeout
|
|
357
|
-
isExplicit: false,
|
|
358
|
-
agentIdentifier: "unknown",
|
|
359
|
-
agentName: "Unknown Agent",
|
|
360
|
-
});
|
|
361
|
-
|
|
362
|
-
checkInactivity();
|
|
363
|
-
|
|
364
|
-
expect(client.endAgentSession).not.toHaveBeenCalled();
|
|
365
|
-
expect(getActiveSessions().has(CARD_A)).toBe(true);
|
|
366
|
-
});
|
|
367
|
-
|
|
368
|
-
test("checkInactivity skips explicit sessions even if timed out", () => {
|
|
369
|
-
const client = makeMockClient();
|
|
370
|
-
initAutoSession(
|
|
371
|
-
mock(async () => {}),
|
|
372
|
-
() => client as any,
|
|
373
|
-
);
|
|
374
|
-
|
|
375
|
-
getActiveSessions().set(CARD_A, {
|
|
376
|
-
cardId: CARD_A,
|
|
377
|
-
startedAt: Date.now() - INACTIVITY_TIMEOUT_MS - 60000,
|
|
378
|
-
lastActivityAt: Date.now() - INACTIVITY_TIMEOUT_MS - 60000,
|
|
379
|
-
isExplicit: true,
|
|
380
|
-
agentIdentifier: "explicit",
|
|
381
|
-
agentName: "Explicit Agent",
|
|
382
|
-
});
|
|
383
|
-
|
|
384
|
-
checkInactivity();
|
|
385
|
-
|
|
386
|
-
expect(client.endAgentSession).not.toHaveBeenCalled();
|
|
387
|
-
expect(getActiveSessions().has(CARD_A)).toBe(true);
|
|
388
|
-
});
|
|
389
|
-
|
|
390
|
-
test("checkInactivity handles mix of timed-out and active sessions", async () => {
|
|
391
|
-
const client = makeMockClient();
|
|
392
|
-
const endCb = mock(async () => {});
|
|
393
|
-
initAutoSession(endCb, () => client as any);
|
|
394
|
-
|
|
395
|
-
// CARD_A — timed out
|
|
396
|
-
getActiveSessions().set(CARD_A, {
|
|
397
|
-
cardId: CARD_A,
|
|
398
|
-
startedAt: Date.now() - INACTIVITY_TIMEOUT_MS - 5000,
|
|
399
|
-
lastActivityAt: Date.now() - INACTIVITY_TIMEOUT_MS - 5000,
|
|
400
|
-
isExplicit: false,
|
|
401
|
-
agentIdentifier: "unknown",
|
|
402
|
-
agentName: "Unknown Agent",
|
|
403
|
-
});
|
|
404
|
-
|
|
405
|
-
// CARD_B — still active
|
|
406
|
-
getActiveSessions().set(CARD_B, {
|
|
407
|
-
cardId: CARD_B,
|
|
408
|
-
startedAt: Date.now() - 1000,
|
|
409
|
-
lastActivityAt: Date.now() - 1000,
|
|
410
|
-
isExplicit: false,
|
|
411
|
-
agentIdentifier: "unknown",
|
|
412
|
-
agentName: "Unknown Agent",
|
|
413
|
-
});
|
|
414
|
-
|
|
415
|
-
// CARD_C — timed out but explicit
|
|
416
|
-
getActiveSessions().set(CARD_C, {
|
|
417
|
-
cardId: CARD_C,
|
|
418
|
-
startedAt: Date.now() - INACTIVITY_TIMEOUT_MS - 5000,
|
|
419
|
-
lastActivityAt: Date.now() - INACTIVITY_TIMEOUT_MS - 5000,
|
|
420
|
-
isExplicit: true,
|
|
421
|
-
agentIdentifier: "explicit",
|
|
422
|
-
agentName: "Explicit Agent",
|
|
423
|
-
});
|
|
424
|
-
|
|
425
|
-
checkInactivity();
|
|
426
|
-
await new Promise((r) => setTimeout(r, 50));
|
|
427
|
-
|
|
428
|
-
// Only CARD_A should be ended
|
|
429
|
-
expect(getActiveSessions().has(CARD_A)).toBe(false);
|
|
430
|
-
expect(getActiveSessions().has(CARD_B)).toBe(true);
|
|
431
|
-
expect(getActiveSessions().has(CARD_C)).toBe(true);
|
|
432
|
-
expect(client.endAgentSession).toHaveBeenCalledTimes(1);
|
|
433
|
-
expect(client.endAgentSession).toHaveBeenCalledWith(CARD_A, {
|
|
434
|
-
status: "completed",
|
|
435
|
-
});
|
|
436
|
-
});
|
|
437
|
-
|
|
438
|
-
test("checkInactivity is a no-op when no client is available", () => {
|
|
439
|
-
// No initAutoSession — clientGetter is null
|
|
440
|
-
getActiveSessions().set(CARD_A, {
|
|
441
|
-
cardId: CARD_A,
|
|
442
|
-
startedAt: 0,
|
|
443
|
-
lastActivityAt: 0,
|
|
444
|
-
isExplicit: false,
|
|
445
|
-
agentIdentifier: "unknown",
|
|
446
|
-
agentName: "Unknown Agent",
|
|
447
|
-
});
|
|
448
|
-
|
|
449
|
-
// Should not throw
|
|
450
|
-
checkInactivity();
|
|
451
|
-
|
|
452
|
-
// Session should still be there (can't end without client)
|
|
453
|
-
expect(getActiveSessions().has(CARD_A)).toBe(true);
|
|
454
|
-
});
|
|
455
|
-
|
|
456
|
-
test("activity resets the inactivity timer", async () => {
|
|
457
|
-
const client = makeMockClient();
|
|
458
|
-
const endCb = mock(async () => {});
|
|
459
|
-
initAutoSession(endCb, () => client as any);
|
|
460
|
-
|
|
461
|
-
// Start with old lastActivityAt
|
|
462
|
-
getActiveSessions().set(CARD_A, {
|
|
463
|
-
cardId: CARD_A,
|
|
464
|
-
startedAt: Date.now() - INACTIVITY_TIMEOUT_MS - 5000,
|
|
465
|
-
lastActivityAt: Date.now() - INACTIVITY_TIMEOUT_MS - 5000,
|
|
466
|
-
isExplicit: false,
|
|
467
|
-
agentIdentifier: "unknown",
|
|
468
|
-
agentName: "Unknown Agent",
|
|
469
|
-
});
|
|
470
|
-
|
|
471
|
-
// Refresh activity
|
|
472
|
-
await trackActivity(CARD_A, { autoStart: false });
|
|
473
|
-
|
|
474
|
-
// Now check — should NOT be timed out
|
|
475
|
-
checkInactivity();
|
|
476
|
-
await new Promise((r) => setTimeout(r, 50));
|
|
477
|
-
|
|
478
|
-
expect(getActiveSessions().has(CARD_A)).toBe(true);
|
|
479
|
-
expect(client.endAgentSession).not.toHaveBeenCalled();
|
|
480
|
-
});
|
|
481
|
-
});
|
|
482
|
-
|
|
483
|
-
// ─── markExplicit ───────────────────────────────────────────────────
|
|
484
|
-
|
|
485
|
-
describe("markExplicit", () => {
|
|
486
|
-
test("marks existing auto-session as explicit", async () => {
|
|
487
|
-
const client = makeMockClient();
|
|
488
|
-
initAutoSession(
|
|
489
|
-
mock(async () => {}),
|
|
490
|
-
() => client as any,
|
|
491
|
-
);
|
|
492
|
-
|
|
493
|
-
await trackActivity(CARD_A, { autoStart: true, client: client as any });
|
|
494
|
-
expect(getActiveSessions().get(CARD_A)!.isExplicit).toBe(false);
|
|
495
|
-
|
|
496
|
-
markExplicit(CARD_A);
|
|
497
|
-
expect(getActiveSessions().get(CARD_A)!.isExplicit).toBe(true);
|
|
498
|
-
// agentIdentifier should remain "unknown" since no clientInfo was provided
|
|
499
|
-
expect(getActiveSessions().get(CARD_A)!.agentIdentifier).toBe("unknown");
|
|
500
|
-
});
|
|
501
|
-
|
|
502
|
-
test("creates new tracking entry for unknown card", () => {
|
|
503
|
-
initAutoSession(
|
|
504
|
-
mock(async () => {}),
|
|
505
|
-
() => makeMockClient() as any,
|
|
506
|
-
);
|
|
507
|
-
|
|
508
|
-
markExplicit(CARD_A);
|
|
509
|
-
|
|
510
|
-
expect(getActiveSessions().has(CARD_A)).toBe(true);
|
|
511
|
-
expect(getActiveSessions().get(CARD_A)!.isExplicit).toBe(true);
|
|
512
|
-
expect(getActiveSessions().get(CARD_A)!.agentIdentifier).toBe("explicit");
|
|
513
|
-
});
|
|
514
|
-
|
|
515
|
-
test("double markExplicit is idempotent", async () => {
|
|
516
|
-
const client = makeMockClient();
|
|
517
|
-
initAutoSession(
|
|
518
|
-
mock(async () => {}),
|
|
519
|
-
() => client as any,
|
|
520
|
-
);
|
|
521
|
-
|
|
522
|
-
await trackActivity(CARD_A, { autoStart: true, client: client as any });
|
|
523
|
-
markExplicit(CARD_A);
|
|
524
|
-
markExplicit(CARD_A);
|
|
525
|
-
|
|
526
|
-
expect(getActiveSessions().get(CARD_A)!.isExplicit).toBe(true);
|
|
527
|
-
expect(getActiveSessions().size).toBe(1);
|
|
528
|
-
});
|
|
529
|
-
});
|
|
530
|
-
|
|
531
|
-
// ─── untrack ────────────────────────────────────────────────────────
|
|
532
|
-
|
|
533
|
-
describe("untrack", () => {
|
|
534
|
-
test("removes session from tracking", async () => {
|
|
535
|
-
const client = makeMockClient();
|
|
536
|
-
initAutoSession(
|
|
537
|
-
mock(async () => {}),
|
|
538
|
-
() => client as any,
|
|
539
|
-
);
|
|
540
|
-
|
|
541
|
-
await trackActivity(CARD_A, { autoStart: true, client: client as any });
|
|
542
|
-
expect(getActiveSessions().has(CARD_A)).toBe(true);
|
|
543
|
-
|
|
544
|
-
untrack(CARD_A);
|
|
545
|
-
expect(getActiveSessions().has(CARD_A)).toBe(false);
|
|
546
|
-
});
|
|
547
|
-
|
|
548
|
-
test("is a no-op for non-existent cardId", () => {
|
|
549
|
-
initAutoSession(
|
|
550
|
-
mock(async () => {}),
|
|
551
|
-
() => makeMockClient() as any,
|
|
552
|
-
);
|
|
553
|
-
|
|
554
|
-
// Should not throw
|
|
555
|
-
untrack(CARD_A);
|
|
556
|
-
expect(getActiveSessions().size).toBe(0);
|
|
557
|
-
});
|
|
558
|
-
|
|
559
|
-
test("removes explicit sessions too", () => {
|
|
560
|
-
initAutoSession(
|
|
561
|
-
mock(async () => {}),
|
|
562
|
-
() => makeMockClient() as any,
|
|
563
|
-
);
|
|
564
|
-
|
|
565
|
-
markExplicit(CARD_A);
|
|
566
|
-
expect(getActiveSessions().has(CARD_A)).toBe(true);
|
|
567
|
-
|
|
568
|
-
untrack(CARD_A);
|
|
569
|
-
expect(getActiveSessions().has(CARD_A)).toBe(false);
|
|
570
|
-
});
|
|
571
|
-
});
|
|
572
|
-
|
|
573
|
-
// ─── shutdown ───────────────────────────────────────────────────────
|
|
574
|
-
|
|
575
|
-
describe("shutdown", () => {
|
|
576
|
-
test("ends all active sessions with paused status", async () => {
|
|
577
|
-
const client = makeMockClient();
|
|
578
|
-
const endCb = mock(async () => {});
|
|
579
|
-
initAutoSession(endCb, () => client as any);
|
|
580
|
-
|
|
581
|
-
getActiveSessions().set(CARD_A, {
|
|
582
|
-
cardId: CARD_A,
|
|
583
|
-
startedAt: Date.now(),
|
|
584
|
-
lastActivityAt: Date.now(),
|
|
585
|
-
isExplicit: false,
|
|
586
|
-
agentIdentifier: "unknown",
|
|
587
|
-
agentName: "Unknown Agent",
|
|
588
|
-
});
|
|
589
|
-
|
|
590
|
-
await shutdownAllSessions();
|
|
591
|
-
|
|
592
|
-
expect(client.endAgentSession).toHaveBeenCalledWith(CARD_A, {
|
|
593
|
-
status: "paused",
|
|
594
|
-
});
|
|
595
|
-
expect(endCb).toHaveBeenCalledWith(client, CARD_A, "paused");
|
|
596
|
-
expect(getActiveSessions().size).toBe(0);
|
|
597
|
-
});
|
|
598
|
-
|
|
599
|
-
test("ends multiple sessions including explicit ones", async () => {
|
|
600
|
-
const client = makeMockClient();
|
|
601
|
-
const endCb = mock(async () => {});
|
|
602
|
-
initAutoSession(endCb, () => client as any);
|
|
603
|
-
|
|
604
|
-
getActiveSessions().set(CARD_A, {
|
|
605
|
-
cardId: CARD_A,
|
|
606
|
-
startedAt: Date.now(),
|
|
607
|
-
lastActivityAt: Date.now(),
|
|
608
|
-
isExplicit: false,
|
|
609
|
-
agentIdentifier: "unknown",
|
|
610
|
-
agentName: "Unknown Agent",
|
|
611
|
-
});
|
|
612
|
-
getActiveSessions().set(CARD_B, {
|
|
613
|
-
cardId: CARD_B,
|
|
614
|
-
startedAt: Date.now(),
|
|
615
|
-
lastActivityAt: Date.now(),
|
|
616
|
-
isExplicit: true,
|
|
617
|
-
agentIdentifier: "explicit",
|
|
618
|
-
agentName: "Explicit Agent",
|
|
619
|
-
});
|
|
620
|
-
|
|
621
|
-
await shutdownAllSessions();
|
|
622
|
-
|
|
623
|
-
// Both should be ended on shutdown (even explicit)
|
|
624
|
-
expect(client.endAgentSession).toHaveBeenCalledTimes(2);
|
|
625
|
-
expect(endCb).toHaveBeenCalledTimes(2);
|
|
626
|
-
expect(getActiveSessions().size).toBe(0);
|
|
627
|
-
});
|
|
628
|
-
|
|
629
|
-
test("is a no-op when no client is available", async () => {
|
|
630
|
-
// No initAutoSession called
|
|
631
|
-
getActiveSessions().set(CARD_A, {
|
|
632
|
-
cardId: CARD_A,
|
|
633
|
-
startedAt: Date.now(),
|
|
634
|
-
lastActivityAt: Date.now(),
|
|
635
|
-
isExplicit: false,
|
|
636
|
-
agentIdentifier: "unknown",
|
|
637
|
-
agentName: "Unknown Agent",
|
|
638
|
-
});
|
|
639
|
-
|
|
640
|
-
// Should not throw
|
|
641
|
-
await shutdownAllSessions();
|
|
642
|
-
|
|
643
|
-
// Session still tracked (couldn't end without client)
|
|
644
|
-
expect(getActiveSessions().has(CARD_A)).toBe(true);
|
|
645
|
-
});
|
|
646
|
-
|
|
647
|
-
test("is resilient when endAgentSession throws for some sessions", async () => {
|
|
648
|
-
let callCount = 0;
|
|
649
|
-
const client = makeMockClient();
|
|
650
|
-
client.endAgentSession = mock(async () => {
|
|
651
|
-
callCount++;
|
|
652
|
-
if (callCount === 1) throw new Error("Network error");
|
|
653
|
-
return { session: { id: "sess-1" } };
|
|
654
|
-
});
|
|
655
|
-
const endCb = mock(async () => {});
|
|
656
|
-
initAutoSession(endCb, () => client as any);
|
|
657
|
-
|
|
658
|
-
getActiveSessions().set(CARD_A, {
|
|
659
|
-
cardId: CARD_A,
|
|
660
|
-
startedAt: Date.now(),
|
|
661
|
-
lastActivityAt: Date.now(),
|
|
662
|
-
isExplicit: false,
|
|
663
|
-
agentIdentifier: "unknown",
|
|
664
|
-
agentName: "Unknown Agent",
|
|
665
|
-
});
|
|
666
|
-
getActiveSessions().set(CARD_B, {
|
|
667
|
-
cardId: CARD_B,
|
|
668
|
-
startedAt: Date.now(),
|
|
669
|
-
lastActivityAt: Date.now(),
|
|
670
|
-
isExplicit: false,
|
|
671
|
-
agentIdentifier: "unknown",
|
|
672
|
-
agentName: "Unknown Agent",
|
|
673
|
-
});
|
|
674
|
-
|
|
675
|
-
await shutdownAllSessions();
|
|
676
|
-
|
|
677
|
-
// Both should be removed from tracking despite first API error
|
|
678
|
-
expect(getActiveSessions().size).toBe(0);
|
|
679
|
-
// Callback should still be called for both
|
|
680
|
-
expect(endCb).toHaveBeenCalledTimes(2);
|
|
681
|
-
});
|
|
682
|
-
|
|
683
|
-
test("empty sessions map is a no-op", async () => {
|
|
684
|
-
const client = makeMockClient();
|
|
685
|
-
initAutoSession(
|
|
686
|
-
mock(async () => {}),
|
|
687
|
-
() => client as any,
|
|
688
|
-
);
|
|
689
|
-
|
|
690
|
-
await shutdownAllSessions();
|
|
691
|
-
|
|
692
|
-
expect(client.endAgentSession).not.toHaveBeenCalled();
|
|
693
|
-
});
|
|
694
|
-
});
|
|
695
|
-
|
|
696
|
-
// ─── destroyAutoSession ─────────────────────────────────────────────
|
|
697
|
-
|
|
698
|
-
describe("destroyAutoSession", () => {
|
|
699
|
-
test("clears all state", async () => {
|
|
700
|
-
const client = makeMockClient();
|
|
701
|
-
initAutoSession(
|
|
702
|
-
mock(async () => {}),
|
|
703
|
-
() => client as any,
|
|
704
|
-
);
|
|
705
|
-
|
|
706
|
-
await trackActivity(CARD_A, { autoStart: true, client: client as any });
|
|
707
|
-
expect(getActiveSessions().size).toBe(1);
|
|
708
|
-
|
|
709
|
-
destroyAutoSession();
|
|
710
|
-
|
|
711
|
-
expect(getActiveSessions().size).toBe(0);
|
|
712
|
-
});
|
|
713
|
-
|
|
714
|
-
test("calling destroyAutoSession twice is safe", () => {
|
|
715
|
-
initAutoSession(
|
|
716
|
-
mock(async () => {}),
|
|
717
|
-
() => makeMockClient() as any,
|
|
718
|
-
);
|
|
719
|
-
destroyAutoSession();
|
|
720
|
-
destroyAutoSession();
|
|
721
|
-
expect(getActiveSessions().size).toBe(0);
|
|
722
|
-
});
|
|
723
|
-
|
|
724
|
-
test("re-initialization after destroy works", async () => {
|
|
725
|
-
const client = makeMockClient();
|
|
726
|
-
initAutoSession(
|
|
727
|
-
mock(async () => {}),
|
|
728
|
-
() => client as any,
|
|
729
|
-
);
|
|
730
|
-
await trackActivity(CARD_A, { autoStart: true, client: client as any });
|
|
731
|
-
destroyAutoSession();
|
|
732
|
-
|
|
733
|
-
// Re-init
|
|
734
|
-
const client2 = makeMockClient();
|
|
735
|
-
initAutoSession(
|
|
736
|
-
mock(async () => {}),
|
|
737
|
-
() => client2 as any,
|
|
738
|
-
);
|
|
739
|
-
await trackActivity(CARD_B, { autoStart: true, client: client2 as any });
|
|
740
|
-
|
|
741
|
-
expect(getActiveSessions().size).toBe(1);
|
|
742
|
-
expect(getActiveSessions().has(CARD_B)).toBe(true);
|
|
743
|
-
expect(client2.startAgentSession).toHaveBeenCalledTimes(1);
|
|
744
|
-
});
|
|
745
|
-
});
|
|
746
|
-
|
|
747
|
-
// ─── AUTO_START_TRIGGERS ────────────────────────────────────────────
|
|
748
|
-
|
|
749
|
-
describe("AUTO_START_TRIGGERS", () => {
|
|
750
|
-
test("contains all card-mutating tools", () => {
|
|
751
|
-
const expected = [
|
|
752
|
-
"harmony_generate_prompt",
|
|
753
|
-
"harmony_update_card",
|
|
754
|
-
"harmony_move_card",
|
|
755
|
-
"harmony_create_subtask",
|
|
756
|
-
"harmony_toggle_subtask",
|
|
757
|
-
"harmony_add_label_to_card",
|
|
758
|
-
"harmony_remove_label_from_card",
|
|
759
|
-
];
|
|
760
|
-
for (const tool of expected) {
|
|
761
|
-
expect(AUTO_START_TRIGGERS.has(tool)).toBe(true);
|
|
762
|
-
}
|
|
763
|
-
expect(AUTO_START_TRIGGERS.size).toBe(expected.length);
|
|
764
|
-
});
|
|
765
|
-
|
|
766
|
-
test("does NOT contain read-only tools", () => {
|
|
767
|
-
const readOnly = [
|
|
768
|
-
"harmony_get_card",
|
|
769
|
-
"harmony_get_card_by_short_id",
|
|
770
|
-
"harmony_get_board",
|
|
771
|
-
"harmony_search_cards",
|
|
772
|
-
"harmony_get_agent_session",
|
|
773
|
-
"harmony_get_card_links",
|
|
774
|
-
"harmony_list_workspaces",
|
|
775
|
-
"harmony_list_projects",
|
|
776
|
-
"harmony_get_context",
|
|
777
|
-
];
|
|
778
|
-
for (const tool of readOnly) {
|
|
779
|
-
expect(AUTO_START_TRIGGERS.has(tool)).toBe(false);
|
|
780
|
-
}
|
|
781
|
-
});
|
|
782
|
-
|
|
783
|
-
test("does NOT contain session management tools", () => {
|
|
784
|
-
expect(AUTO_START_TRIGGERS.has("harmony_start_agent_session")).toBe(false);
|
|
785
|
-
expect(AUTO_START_TRIGGERS.has("harmony_end_agent_session")).toBe(false);
|
|
786
|
-
expect(AUTO_START_TRIGGERS.has("harmony_update_agent_progress")).toBe(
|
|
787
|
-
false,
|
|
788
|
-
);
|
|
789
|
-
});
|
|
790
|
-
});
|
|
791
|
-
|
|
792
|
-
// ─── INACTIVITY_TIMEOUT_MS ──────────────────────────────────────────
|
|
793
|
-
|
|
794
|
-
describe("INACTIVITY_TIMEOUT_MS", () => {
|
|
795
|
-
test("is 10 minutes", () => {
|
|
796
|
-
expect(INACTIVITY_TIMEOUT_MS).toBe(10 * 60 * 1000);
|
|
797
|
-
});
|
|
798
|
-
});
|
|
799
|
-
|
|
800
|
-
// ─── Edge cases / race conditions ───────────────────────────────────
|
|
801
|
-
|
|
802
|
-
describe("edge cases", () => {
|
|
803
|
-
test("trackActivity before initAutoSession uses client from options", async () => {
|
|
804
|
-
// No initAutoSession called
|
|
805
|
-
const client = makeMockClient();
|
|
806
|
-
|
|
807
|
-
await trackActivity(CARD_A, { autoStart: true, client: client as any });
|
|
808
|
-
|
|
809
|
-
expect(client.startAgentSession).toHaveBeenCalledTimes(1);
|
|
810
|
-
expect(getActiveSessions().size).toBe(1);
|
|
811
|
-
});
|
|
812
|
-
|
|
813
|
-
test("re-initialization clears old interval timer", async () => {
|
|
814
|
-
const client1 = makeMockClient();
|
|
815
|
-
const endCb1 = mock(async () => {});
|
|
816
|
-
initAutoSession(endCb1, () => client1 as any);
|
|
817
|
-
|
|
818
|
-
// Re-initialize
|
|
819
|
-
const client2 = makeMockClient();
|
|
820
|
-
const endCb2 = mock(async () => {});
|
|
821
|
-
initAutoSession(endCb2, () => client2 as any);
|
|
822
|
-
|
|
823
|
-
// Add stale session and trigger check
|
|
824
|
-
getActiveSessions().set(CARD_A, {
|
|
825
|
-
cardId: CARD_A,
|
|
826
|
-
startedAt: 0,
|
|
827
|
-
lastActivityAt: 0,
|
|
828
|
-
isExplicit: false,
|
|
829
|
-
agentIdentifier: "unknown",
|
|
830
|
-
agentName: "Unknown Agent",
|
|
831
|
-
});
|
|
832
|
-
|
|
833
|
-
checkInactivity();
|
|
834
|
-
await new Promise((r) => setTimeout(r, 50));
|
|
835
|
-
|
|
836
|
-
// Should use client2, not client1
|
|
837
|
-
expect(client2.endAgentSession).toHaveBeenCalledTimes(1);
|
|
838
|
-
expect(client1.endAgentSession).not.toHaveBeenCalled();
|
|
839
|
-
});
|
|
840
|
-
|
|
841
|
-
test("endCallback throwing does not prevent session removal", async () => {
|
|
842
|
-
const client = makeMockClient();
|
|
843
|
-
const endCb = mock(async () => {
|
|
844
|
-
throw new Error("Callback crash");
|
|
845
|
-
});
|
|
846
|
-
initAutoSession(endCb, () => client as any);
|
|
847
|
-
|
|
848
|
-
getActiveSessions().set(CARD_A, {
|
|
849
|
-
cardId: CARD_A,
|
|
850
|
-
startedAt: 0,
|
|
851
|
-
lastActivityAt: 0,
|
|
852
|
-
isExplicit: false,
|
|
853
|
-
agentIdentifier: "unknown",
|
|
854
|
-
agentName: "Unknown Agent",
|
|
855
|
-
});
|
|
856
|
-
|
|
857
|
-
checkInactivity();
|
|
858
|
-
await new Promise((r) => setTimeout(r, 50));
|
|
859
|
-
|
|
860
|
-
// Session should be removed despite callback error
|
|
861
|
-
expect(getActiveSessions().has(CARD_A)).toBe(false);
|
|
862
|
-
});
|
|
863
|
-
|
|
864
|
-
test("both endAgentSession and endCallback throwing still cleans up", async () => {
|
|
865
|
-
const client = makeMockClient();
|
|
866
|
-
client.endAgentSession = mock(async () => {
|
|
867
|
-
throw new Error("API down");
|
|
868
|
-
});
|
|
869
|
-
const endCb = mock(async () => {
|
|
870
|
-
throw new Error("Pipeline crash");
|
|
871
|
-
});
|
|
872
|
-
initAutoSession(endCb, () => client as any);
|
|
873
|
-
|
|
874
|
-
await trackActivity(CARD_A, { autoStart: true, client: client as any });
|
|
875
|
-
await trackActivity(CARD_B, { autoStart: true, client: client as any });
|
|
876
|
-
|
|
877
|
-
// Despite both throwing, tracking should be clean
|
|
878
|
-
expect(getActiveSessions().has(CARD_A)).toBe(false);
|
|
879
|
-
expect(getActiveSessions().has(CARD_B)).toBe(true);
|
|
880
|
-
});
|
|
881
|
-
|
|
882
|
-
test("concurrent auto-start on same card only starts once", async () => {
|
|
883
|
-
const client = makeMockClient();
|
|
884
|
-
// Slow startAgentSession to create a window for concurrent calls
|
|
885
|
-
client.startAgentSession = mock(
|
|
886
|
-
async () =>
|
|
887
|
-
new Promise((resolve) =>
|
|
888
|
-
setTimeout(() => resolve({ session: { id: "sess-1" } }), 20),
|
|
889
|
-
),
|
|
890
|
-
);
|
|
891
|
-
initAutoSession(
|
|
892
|
-
mock(async () => {}),
|
|
893
|
-
() => client as any,
|
|
894
|
-
);
|
|
895
|
-
|
|
896
|
-
// Fire two concurrent auto-starts for the same card
|
|
897
|
-
const p1 = trackActivity(CARD_A, {
|
|
898
|
-
autoStart: true,
|
|
899
|
-
client: client as any,
|
|
900
|
-
});
|
|
901
|
-
const p2 = trackActivity(CARD_A, {
|
|
902
|
-
autoStart: true,
|
|
903
|
-
client: client as any,
|
|
904
|
-
});
|
|
905
|
-
await Promise.all([p1, p2]);
|
|
906
|
-
|
|
907
|
-
// First call creates session, second call hits early return (existing)
|
|
908
|
-
// but due to async, both might race. At minimum, session should exist once.
|
|
909
|
-
expect(getActiveSessions().size).toBe(1);
|
|
910
|
-
expect(getActiveSessions().has(CARD_A)).toBe(true);
|
|
911
|
-
});
|
|
912
|
-
});
|