macroclaw 0.25.0 → 0.27.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/package.json +1 -1
- package/src/app.test.ts +27 -65
- package/src/app.ts +13 -13
- package/src/orchestrator.test.ts +246 -331
- package/src/orchestrator.ts +141 -153
- package/src/prompts.test.ts +1 -1
- package/src/prompts.ts +10 -16
- package/src/sessions.test.ts +6 -3
- package/src/system-service.test.ts +3 -0
package/src/orchestrator.test.ts
CHANGED
|
@@ -7,13 +7,14 @@ import { saveSessions } from "./sessions";
|
|
|
7
7
|
const tmpSettingsDir = "/tmp/macroclaw-test-orchestrator-settings";
|
|
8
8
|
const TEST_WORKSPACE = "/tmp/macroclaw-test-workspace";
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
});
|
|
10
|
+
function cleanup() {
|
|
11
|
+
try {
|
|
12
|
+
if (existsSync(tmpSettingsDir)) rmSync(tmpSettingsDir, { recursive: true });
|
|
13
|
+
} catch { /* async completion handlers may race with cleanup */ }
|
|
14
|
+
}
|
|
13
15
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
});
|
|
16
|
+
beforeEach(cleanup);
|
|
17
|
+
afterEach(cleanup);
|
|
17
18
|
|
|
18
19
|
interface CallInfo {
|
|
19
20
|
method: "newSession" | "resumeSession" | "forkSession";
|
|
@@ -38,6 +39,17 @@ function resolvedQuery<T>(value: T, sessionId = "test-session-id"): RunningQuery
|
|
|
38
39
|
};
|
|
39
40
|
}
|
|
40
41
|
|
|
42
|
+
function pendingQuery(sessionId = "pending-sid"): { query: RunningQuery<unknown>; resolve: (v: QueryResult<unknown>) => void; reject: (e: Error) => void } {
|
|
43
|
+
let resolve!: (v: QueryResult<unknown>) => void;
|
|
44
|
+
let reject!: (e: Error) => void;
|
|
45
|
+
const result = new Promise<QueryResult<unknown>>((res, rej) => { resolve = res; reject = rej; });
|
|
46
|
+
return {
|
|
47
|
+
query: { sessionId, startedAt: new Date(), result, kill: mock(async () => {}) },
|
|
48
|
+
resolve,
|
|
49
|
+
reject,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
41
53
|
function mockClaude(handler: MockHandler | unknown) {
|
|
42
54
|
const calls: CallInfo[] = [];
|
|
43
55
|
const handlerFn: MockHandler = typeof handler === "function"
|
|
@@ -114,36 +126,6 @@ describe("Orchestrator", () => {
|
|
|
114
126
|
expect(claude.calls[0].prompt).toBe("[File: /tmp/photo.jpg]");
|
|
115
127
|
});
|
|
116
128
|
|
|
117
|
-
it("builds cron prompt with prefix", async () => {
|
|
118
|
-
const claude = mockClaude({ action: "send", message: "ok", actionReason: "ok" });
|
|
119
|
-
const { orch } = makeOrchestrator(claude);
|
|
120
|
-
|
|
121
|
-
orch.handleCron("daily", "check updates");
|
|
122
|
-
await waitForProcessing();
|
|
123
|
-
|
|
124
|
-
expect(claude.calls[0].prompt).toBe("[Context: cron/daily] check updates");
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
it("uses cron model override", async () => {
|
|
128
|
-
const claude = mockClaude({ action: "send", message: "ok", actionReason: "ok" });
|
|
129
|
-
const { orch } = makeOrchestrator(claude, { model: "sonnet" });
|
|
130
|
-
|
|
131
|
-
orch.handleCron("smart", "think", "opus");
|
|
132
|
-
await waitForProcessing();
|
|
133
|
-
|
|
134
|
-
expect(claude.calls[0].model).toBe("opus");
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
it("falls back to config model when cron has no model", async () => {
|
|
138
|
-
const claude = mockClaude({ action: "send", message: "ok", actionReason: "ok" });
|
|
139
|
-
const { orch } = makeOrchestrator(claude, { model: "sonnet" });
|
|
140
|
-
|
|
141
|
-
orch.handleCron("basic", "check");
|
|
142
|
-
await waitForProcessing();
|
|
143
|
-
|
|
144
|
-
expect(claude.calls[0].model).toBe("sonnet");
|
|
145
|
-
});
|
|
146
|
-
|
|
147
129
|
it("builds button click prompt", async () => {
|
|
148
130
|
const claude = mockClaude({ action: "send", message: "ok", actionReason: "ok" });
|
|
149
131
|
const { orch } = makeOrchestrator(claude);
|
|
@@ -226,6 +208,24 @@ describe("Orchestrator", () => {
|
|
|
226
208
|
|
|
227
209
|
expect(responses[0].message).toContain("[JSON Error]");
|
|
228
210
|
});
|
|
211
|
+
|
|
212
|
+
it("reports error when resume fails", async () => {
|
|
213
|
+
saveSessions({ mainSessionId: "old-session" }, tmpSettingsDir);
|
|
214
|
+
const claude = mockClaude((): RunningQuery<unknown> => ({
|
|
215
|
+
sessionId: "old-session",
|
|
216
|
+
startedAt: new Date(),
|
|
217
|
+
result: Promise.reject(new QueryProcessError(1, "session not found")),
|
|
218
|
+
kill: mock(async () => {}),
|
|
219
|
+
}));
|
|
220
|
+
const { orch, responses } = makeOrchestrator(claude);
|
|
221
|
+
|
|
222
|
+
orch.handleMessage("hello");
|
|
223
|
+
await waitForProcessing();
|
|
224
|
+
|
|
225
|
+
expect(claude.calls[0].method).toBe("resumeSession");
|
|
226
|
+
expect(responses[0].message).toContain("[Error]");
|
|
227
|
+
expect(responses[0].message).toContain("session not found");
|
|
228
|
+
});
|
|
229
229
|
});
|
|
230
230
|
|
|
231
231
|
describe("session management", () => {
|
|
@@ -251,31 +251,6 @@ describe("Orchestrator", () => {
|
|
|
251
251
|
expect(claude.calls[0].method).toBe("newSession");
|
|
252
252
|
});
|
|
253
253
|
|
|
254
|
-
it("creates new session when resume fails", async () => {
|
|
255
|
-
saveSessions({ mainSessionId: "old-session" }, tmpSettingsDir);
|
|
256
|
-
let callCount = 0;
|
|
257
|
-
const claude = mockClaude((_info: CallInfo): RunningQuery<unknown> => {
|
|
258
|
-
callCount++;
|
|
259
|
-
if (callCount === 1) {
|
|
260
|
-
return {
|
|
261
|
-
sessionId: "old-session",
|
|
262
|
-
startedAt: new Date(),
|
|
263
|
-
result: Promise.reject(new QueryProcessError(1, "session not found")),
|
|
264
|
-
kill: mock(async () => {}),
|
|
265
|
-
};
|
|
266
|
-
}
|
|
267
|
-
return resolvedQuery({ action: "send", message: "ok", actionReason: "ok" });
|
|
268
|
-
});
|
|
269
|
-
const { orch } = makeOrchestrator(claude);
|
|
270
|
-
|
|
271
|
-
orch.handleMessage("hello");
|
|
272
|
-
await waitForProcessing();
|
|
273
|
-
|
|
274
|
-
expect(callCount).toBe(2);
|
|
275
|
-
expect(claude.calls[0].method).toBe("resumeSession");
|
|
276
|
-
expect(claude.calls[1].method).toBe("newSession");
|
|
277
|
-
});
|
|
278
|
-
|
|
279
254
|
it("switches to resumeSession after first success", async () => {
|
|
280
255
|
const claude = mockClaude({ action: "send", message: "ok", actionReason: "ok" });
|
|
281
256
|
const { orch } = makeOrchestrator(claude);
|
|
@@ -289,18 +264,6 @@ describe("Orchestrator", () => {
|
|
|
289
264
|
expect(claude.calls[1].method).toBe("resumeSession");
|
|
290
265
|
});
|
|
291
266
|
|
|
292
|
-
it("handleSessionCommand sends session via onResponse", async () => {
|
|
293
|
-
saveSessions({ mainSessionId: "test-id" }, tmpSettingsDir);
|
|
294
|
-
const claude = mockClaude({ action: "send", message: "", actionReason: "" });
|
|
295
|
-
const { orch, responses } = makeOrchestrator(claude);
|
|
296
|
-
|
|
297
|
-
orch.handleSessionCommand();
|
|
298
|
-
await waitForProcessing();
|
|
299
|
-
|
|
300
|
-
expect(responses).toHaveLength(1);
|
|
301
|
-
expect(responses[0].message).toBe("Session: <code>test-id</code>");
|
|
302
|
-
});
|
|
303
|
-
|
|
304
267
|
it("background-agent forks from main session", async () => {
|
|
305
268
|
saveSessions({ mainSessionId: "main-session" }, tmpSettingsDir);
|
|
306
269
|
const claude = mockClaude({ action: "send", message: "ok", actionReason: "ok" });
|
|
@@ -312,7 +275,6 @@ describe("Orchestrator", () => {
|
|
|
312
275
|
orch.handleBackgroundCommand("do work");
|
|
313
276
|
await waitForProcessing();
|
|
314
277
|
|
|
315
|
-
// bg agent uses forkSession
|
|
316
278
|
expect(claude.calls[1].method).toBe("forkSession");
|
|
317
279
|
});
|
|
318
280
|
|
|
@@ -324,7 +286,6 @@ describe("Orchestrator", () => {
|
|
|
324
286
|
orch.handleMessage("hello");
|
|
325
287
|
await waitForProcessing();
|
|
326
288
|
|
|
327
|
-
// Next call should use the new session ID
|
|
328
289
|
orch.handleMessage("follow up");
|
|
329
290
|
await waitForProcessing();
|
|
330
291
|
|
|
@@ -332,75 +293,166 @@ describe("Orchestrator", () => {
|
|
|
332
293
|
});
|
|
333
294
|
});
|
|
334
295
|
|
|
335
|
-
describe("
|
|
336
|
-
it("
|
|
337
|
-
const
|
|
296
|
+
describe("non-blocking handler", () => {
|
|
297
|
+
it("handler returns immediately, result delivered by completion handler", async () => {
|
|
298
|
+
const { query, resolve } = pendingQuery("main-sid");
|
|
299
|
+
const claude = mockClaude(() => query);
|
|
338
300
|
const { orch, responses } = makeOrchestrator(claude);
|
|
339
301
|
|
|
340
|
-
orch.handleMessage("
|
|
302
|
+
orch.handleMessage("hello");
|
|
341
303
|
await waitForProcessing();
|
|
342
304
|
|
|
343
|
-
|
|
344
|
-
expect(responses
|
|
305
|
+
// Handler returned but no response yet (query still pending)
|
|
306
|
+
expect(responses).toHaveLength(0);
|
|
307
|
+
|
|
308
|
+
// Query completes — completion handler delivers
|
|
309
|
+
resolve(queryResult({ action: "send", message: "done!", actionReason: "ok" }));
|
|
310
|
+
await waitForProcessing();
|
|
311
|
+
|
|
312
|
+
expect(responses[0].message).toBe("done!");
|
|
345
313
|
});
|
|
346
314
|
|
|
347
|
-
it("
|
|
348
|
-
const
|
|
349
|
-
const {
|
|
315
|
+
it("second message processes while first is still running", async () => {
|
|
316
|
+
const { query: q1, resolve: resolve1 } = pendingQuery("q1-sid");
|
|
317
|
+
const { query: q2, resolve: resolve2 } = pendingQuery("q2-sid");
|
|
350
318
|
|
|
351
|
-
|
|
319
|
+
let callCount = 0;
|
|
320
|
+
const claude = mockClaude((): RunningQuery<unknown> => {
|
|
321
|
+
callCount++;
|
|
322
|
+
if (callCount === 1) return q1;
|
|
323
|
+
return q2;
|
|
324
|
+
});
|
|
325
|
+
// waitThreshold=0 so second message demotes immediately
|
|
326
|
+
const { orch, responses } = makeOrchestrator(claude, { waitThreshold: 0 });
|
|
327
|
+
|
|
328
|
+
orch.handleMessage("first");
|
|
352
329
|
await waitForProcessing();
|
|
353
330
|
|
|
354
|
-
|
|
355
|
-
expect(
|
|
331
|
+
// First query is running, handler returned
|
|
332
|
+
expect(callCount).toBe(1);
|
|
333
|
+
|
|
334
|
+
orch.handleMessage("second");
|
|
335
|
+
await waitForProcessing();
|
|
336
|
+
|
|
337
|
+
// Second message caused a fork+demote, second query started
|
|
338
|
+
expect(callCount).toBe(2);
|
|
339
|
+
const secondCall = claude.calls[1];
|
|
340
|
+
expect(secondCall.method).toBe("forkSession");
|
|
341
|
+
expect(secondCall.prompt).toContain("[Context: previous task");
|
|
342
|
+
expect(secondCall.prompt).toContain("moved to background]");
|
|
343
|
+
expect(secondCall.prompt).toContain("second");
|
|
344
|
+
|
|
345
|
+
// Resolve both
|
|
346
|
+
resolve2(queryResult({ action: "send", message: "second done", actionReason: "ok" }, "q2-sid"));
|
|
347
|
+
resolve1(queryResult({ action: "send", message: "first done", actionReason: "ok" }, "q1-sid"));
|
|
348
|
+
await waitForProcessing(100);
|
|
349
|
+
|
|
350
|
+
const messages = responses.map((r) => r.message);
|
|
351
|
+
expect(messages).toContain("second done");
|
|
352
|
+
// First result goes through Claude as background context (not direct)
|
|
356
353
|
});
|
|
357
354
|
|
|
358
|
-
it("
|
|
359
|
-
const
|
|
355
|
+
it("waits for main to finish when within threshold, then processes next message", async () => {
|
|
356
|
+
const { query: q1, resolve: resolve1 } = pendingQuery("main-sid");
|
|
357
|
+
|
|
358
|
+
let callCount = 0;
|
|
359
|
+
const claude = mockClaude((): RunningQuery<unknown> => {
|
|
360
|
+
callCount++;
|
|
361
|
+
if (callCount === 1) return q1;
|
|
362
|
+
return resolvedQuery({ action: "send", message: "follow-up result", actionReason: "ok" });
|
|
363
|
+
});
|
|
360
364
|
const { orch, responses } = makeOrchestrator(claude);
|
|
361
365
|
|
|
362
|
-
|
|
366
|
+
// First message — handler returns immediately
|
|
367
|
+
orch.handleMessage("slow task");
|
|
363
368
|
await waitForProcessing();
|
|
369
|
+
expect(callCount).toBe(1);
|
|
364
370
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
371
|
+
// Second message — main is running, within threshold, handler blocks
|
|
372
|
+
orch.handleMessage("follow up");
|
|
373
|
+
await waitForProcessing(10);
|
|
374
|
+
|
|
375
|
+
// Still blocked, only 1 call
|
|
376
|
+
expect(callCount).toBe(1);
|
|
377
|
+
|
|
378
|
+
// First query finishes — completion handler delivers, then handler unblocks
|
|
379
|
+
resolve1(queryResult({ action: "send", message: "slow done", actionReason: "ok" }));
|
|
380
|
+
await waitForProcessing(100);
|
|
381
|
+
|
|
382
|
+
expect(callCount).toBe(2);
|
|
383
|
+
const messages = responses.map((r) => r.message);
|
|
384
|
+
expect(messages).toContain("slow done");
|
|
385
|
+
expect(messages).toContain("follow-up result");
|
|
368
386
|
});
|
|
369
387
|
|
|
370
|
-
it("
|
|
371
|
-
const
|
|
372
|
-
let firstResolve: () => void;
|
|
373
|
-
const firstCallDone = new Promise<void>((r) => { firstResolve = r; });
|
|
388
|
+
it("demotes after wait timeout when main does not finish in time", async () => {
|
|
389
|
+
const { query: q1 } = pendingQuery("main-sid");
|
|
374
390
|
|
|
375
|
-
let
|
|
391
|
+
let callCount = 0;
|
|
376
392
|
const claude = mockClaude((): RunningQuery<unknown> => {
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
return {
|
|
381
|
-
sessionId: "sid",
|
|
382
|
-
startedAt: new Date(),
|
|
383
|
-
result: firstCallDone.then(() => {
|
|
384
|
-
callOrder.push(n);
|
|
385
|
-
return queryResult({ action: "send", message: `call ${n}`, actionReason: "ok" });
|
|
386
|
-
}),
|
|
387
|
-
kill: mock(async () => {}),
|
|
388
|
-
};
|
|
389
|
-
}
|
|
390
|
-
callOrder.push(n);
|
|
391
|
-
return resolvedQuery({ action: "send", message: `call ${n}`, actionReason: "ok" });
|
|
393
|
+
callCount++;
|
|
394
|
+
if (callCount === 1) return q1;
|
|
395
|
+
return resolvedQuery({ action: "send", message: "forked result", actionReason: "ok" }, "new-main");
|
|
392
396
|
});
|
|
393
|
-
|
|
397
|
+
// Short threshold so we don't wait long
|
|
398
|
+
const { orch, responses } = makeOrchestrator(claude, { waitThreshold: 10 });
|
|
394
399
|
|
|
395
|
-
orch.handleMessage("
|
|
396
|
-
|
|
400
|
+
orch.handleMessage("slow task");
|
|
401
|
+
await waitForProcessing();
|
|
402
|
+
|
|
403
|
+
orch.handleMessage("follow up");
|
|
404
|
+
await waitForProcessing(200);
|
|
405
|
+
|
|
406
|
+
expect(callCount).toBe(2);
|
|
407
|
+
const userCall = claude.calls[1];
|
|
408
|
+
expect(userCall.prompt).toContain("[Context: previous task");
|
|
409
|
+
expect(userCall.prompt).toContain("follow up");
|
|
410
|
+
expect(responses.map((r) => r.message)).toContain("forked result");
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
it("delivers result with error when session not in runningSessions", async () => {
|
|
414
|
+
saveSessions({ mainSessionId: "main-session" }, tmpSettingsDir);
|
|
415
|
+
const { query, resolve } = pendingQuery("main-session");
|
|
416
|
+
const claude = mockClaude(() => query);
|
|
417
|
+
const { orch, responses } = makeOrchestrator(claude);
|
|
418
|
+
|
|
419
|
+
orch.handleMessage("hello");
|
|
420
|
+
await waitForProcessing();
|
|
421
|
+
|
|
422
|
+
// Kill the session (removes from runningSessions)
|
|
423
|
+
await orch.handleKill("main-session");
|
|
424
|
+
await waitForProcessing();
|
|
425
|
+
|
|
426
|
+
// Query completes after kill — should still deliver (with error log)
|
|
427
|
+
resolve(queryResult({ action: "send", message: "late result", actionReason: "ok" }));
|
|
428
|
+
await waitForProcessing();
|
|
429
|
+
|
|
430
|
+
const messages = responses.map((r) => r.message);
|
|
431
|
+
expect(messages).toContain("late result");
|
|
432
|
+
});
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
describe("queue-based processing", () => {
|
|
436
|
+
it("handleMessage queues a user request and calls onResponse", async () => {
|
|
437
|
+
const claude = mockClaude({ action: "send", message: "result", actionReason: "ok" });
|
|
438
|
+
const { orch, responses } = makeOrchestrator(claude);
|
|
439
|
+
|
|
440
|
+
orch.handleMessage("test message");
|
|
441
|
+
await waitForProcessing();
|
|
442
|
+
|
|
443
|
+
expect(claude.calls).toHaveLength(1);
|
|
444
|
+
expect(responses[0].message).toBe("result");
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
it("handleButton queues a button request", async () => {
|
|
448
|
+
const claude = mockClaude({ action: "send", message: "button response", actionReason: "ok" });
|
|
449
|
+
const { orch, responses } = makeOrchestrator(claude);
|
|
397
450
|
|
|
398
|
-
|
|
399
|
-
firstResolve!();
|
|
451
|
+
orch.handleButton("Yes");
|
|
400
452
|
await waitForProcessing();
|
|
401
453
|
|
|
402
|
-
expect(claude.calls).
|
|
403
|
-
expect(
|
|
454
|
+
expect(claude.calls[0].prompt).toBe('[Context: button-click] User tapped "Yes"');
|
|
455
|
+
expect(responses[0].message).toBe("button response");
|
|
404
456
|
});
|
|
405
457
|
|
|
406
458
|
it("silent response: onResponse not called when action=silent", async () => {
|
|
@@ -436,130 +488,65 @@ describe("Orchestrator", () => {
|
|
|
436
488
|
expect(messages).toContain("Starting research");
|
|
437
489
|
expect(messages).toContain('Background agent "research" started.');
|
|
438
490
|
|
|
439
|
-
// Background agent result should be fed back
|
|
440
491
|
await waitForProcessing(100);
|
|
441
492
|
expect(callCount).toBe(3); // 1 main + 1 bg agent + 1 bg result fed back
|
|
442
493
|
});
|
|
494
|
+
});
|
|
443
495
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
const
|
|
448
|
-
|
|
449
|
-
// Mock setTimeout to fire immediately only for the timeout race, not for waitForProcessing
|
|
450
|
-
const origSetTimeout = globalThis.setTimeout;
|
|
451
|
-
const claude = mockClaude((): RunningQuery<unknown> => {
|
|
452
|
-
// Mock setTimeout right before the race happens (synchronously after this returns)
|
|
453
|
-
globalThis.setTimeout = ((fn: Function) => { fn(); return 0 as any; }) as any;
|
|
454
|
-
return {
|
|
455
|
-
sessionId: "test-session",
|
|
456
|
-
startedAt: new Date(),
|
|
457
|
-
result: completion,
|
|
458
|
-
kill: mock(async () => {}),
|
|
459
|
-
};
|
|
460
|
-
});
|
|
461
|
-
const { orch, responses } = makeOrchestrator(claude);
|
|
496
|
+
describe("cron routing", () => {
|
|
497
|
+
it("cron always forks as background, never goes through queue", async () => {
|
|
498
|
+
saveSessions({ mainSessionId: "main-session" }, tmpSettingsDir);
|
|
499
|
+
const claude = mockClaude({ action: "send", message: "ok", actionReason: "ok" });
|
|
500
|
+
const { orch } = makeOrchestrator(claude);
|
|
462
501
|
|
|
463
|
-
orch.
|
|
464
|
-
// Restore immediately so waitForProcessing works
|
|
465
|
-
await new Promise((r) => origSetTimeout(r, 10));
|
|
466
|
-
globalThis.setTimeout = origSetTimeout;
|
|
502
|
+
orch.handleCron("daily-check", "Check for updates", "haiku");
|
|
467
503
|
await waitForProcessing();
|
|
468
504
|
|
|
469
|
-
|
|
470
|
-
expect(
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
await waitForProcessing(100);
|
|
474
|
-
|
|
475
|
-
const allMessages = responses.map((r) => r.message);
|
|
476
|
-
expect(allMessages).toContain("done!");
|
|
505
|
+
expect(claude.calls[0].method).toBe("forkSession");
|
|
506
|
+
expect(claude.calls[0].prompt).toContain("[Context: background-agent/cron-daily-check]");
|
|
507
|
+
expect(claude.calls[0].prompt).toContain("[Context: cron/daily-check] Check for updates");
|
|
508
|
+
expect(claude.calls[0].model).toBe("haiku");
|
|
477
509
|
});
|
|
478
510
|
|
|
479
|
-
it("
|
|
480
|
-
saveSessions({ mainSessionId: "
|
|
481
|
-
|
|
482
|
-
const
|
|
483
|
-
|
|
484
|
-
const origSetTimeout = globalThis.setTimeout;
|
|
485
|
-
|
|
486
|
-
let callCount = 0;
|
|
487
|
-
const claude = mockClaude((): RunningQuery<unknown> => {
|
|
488
|
-
callCount++;
|
|
489
|
-
if (callCount === 1) {
|
|
490
|
-
globalThis.setTimeout = ((fn: Function) => { fn(); return 0 as any; }) as any;
|
|
491
|
-
return {
|
|
492
|
-
sessionId: "test-session",
|
|
493
|
-
startedAt: new Date(),
|
|
494
|
-
result: completion,
|
|
495
|
-
kill: mock(async () => {}),
|
|
496
|
-
};
|
|
497
|
-
}
|
|
498
|
-
return resolvedQuery({ action: "send", message: "forked response", actionReason: "ok" });
|
|
499
|
-
});
|
|
500
|
-
const { orch } = makeOrchestrator(claude);
|
|
501
|
-
|
|
502
|
-
// First message gets deferred (backgrounded on test-session)
|
|
503
|
-
orch.handleMessage("slow task");
|
|
504
|
-
await new Promise((r) => origSetTimeout(r, 10));
|
|
505
|
-
globalThis.setTimeout = origSetTimeout;
|
|
506
|
-
await waitForProcessing();
|
|
511
|
+
it("cron uses config model when none specified", async () => {
|
|
512
|
+
saveSessions({ mainSessionId: "main-session" }, tmpSettingsDir);
|
|
513
|
+
const claude = mockClaude({ action: "send", message: "ok", actionReason: "ok" });
|
|
514
|
+
const { orch } = makeOrchestrator(claude, { model: "sonnet" });
|
|
507
515
|
|
|
508
|
-
|
|
509
|
-
orch.handleMessage("follow up");
|
|
516
|
+
orch.handleCron("basic", "check");
|
|
510
517
|
await waitForProcessing();
|
|
511
518
|
|
|
512
|
-
expect(claude.calls[
|
|
513
|
-
|
|
514
|
-
resolveCompletion!(queryResult({ action: "send", message: "bg done", actionReason: "ok" }));
|
|
515
|
-
await waitForProcessing(50);
|
|
519
|
+
expect(claude.calls[0].model).toBe("sonnet");
|
|
516
520
|
});
|
|
517
521
|
|
|
518
|
-
it("
|
|
519
|
-
saveSessions({ mainSessionId: "
|
|
520
|
-
let
|
|
521
|
-
const completion = new Promise<QueryResult<unknown>>((r) => { resolveCompletion = r; });
|
|
522
|
-
|
|
523
|
-
const origSetTimeout = globalThis.setTimeout;
|
|
524
|
-
|
|
522
|
+
it("cron result feeds back into main session", async () => {
|
|
523
|
+
saveSessions({ mainSessionId: "main-session" }, tmpSettingsDir);
|
|
524
|
+
let callCount = 0;
|
|
525
525
|
const claude = mockClaude((): RunningQuery<unknown> => {
|
|
526
|
-
|
|
527
|
-
return {
|
|
528
|
-
sessionId: "test-session",
|
|
529
|
-
startedAt: new Date(),
|
|
530
|
-
result: completion,
|
|
531
|
-
kill: mock(async () => {}),
|
|
532
|
-
};
|
|
526
|
+
callCount++;
|
|
527
|
+
return resolvedQuery({ action: "send", message: `call ${callCount}`, actionReason: "ok" });
|
|
533
528
|
});
|
|
534
|
-
const { orch
|
|
535
|
-
|
|
536
|
-
orch.handleMessage("slow");
|
|
537
|
-
await new Promise((r) => origSetTimeout(r, 10));
|
|
538
|
-
globalThis.setTimeout = origSetTimeout;
|
|
539
|
-
await waitForProcessing();
|
|
529
|
+
const { orch } = makeOrchestrator(claude);
|
|
540
530
|
|
|
541
|
-
|
|
542
|
-
await waitForProcessing(
|
|
531
|
+
orch.handleCron("check", "any updates?");
|
|
532
|
+
await waitForProcessing(150);
|
|
543
533
|
|
|
544
|
-
|
|
545
|
-
expect(messages).toContain("direct result");
|
|
546
|
-
// Only called once (for the initial slow request, not for the result)
|
|
547
|
-
expect(claude.calls).toHaveLength(1);
|
|
534
|
+
expect(callCount).toBe(2); // 1 cron bg + 1 bg result fed back
|
|
548
535
|
});
|
|
549
536
|
});
|
|
550
537
|
|
|
551
|
-
describe("
|
|
552
|
-
it("sends 'no
|
|
538
|
+
describe("handleSessions", () => {
|
|
539
|
+
it("sends 'no sessions' message when none running", async () => {
|
|
553
540
|
const claude = mockClaude({ action: "send", message: "ok", actionReason: "ok" });
|
|
554
541
|
const { orch, responses } = makeOrchestrator(claude);
|
|
555
542
|
|
|
556
|
-
orch.
|
|
543
|
+
orch.handleSessions();
|
|
557
544
|
await waitForProcessing();
|
|
558
545
|
|
|
559
|
-
expect(responses[0].message).toBe("No
|
|
546
|
+
expect(responses[0].message).toBe("No running sessions.");
|
|
560
547
|
});
|
|
561
548
|
|
|
562
|
-
it("includes detail buttons and dismiss when
|
|
549
|
+
it("includes detail buttons and dismiss when sessions are running", async () => {
|
|
563
550
|
saveSessions({ mainSessionId: "test-session" }, tmpSettingsDir);
|
|
564
551
|
const claude = mockClaude((): RunningQuery<unknown> => ({
|
|
565
552
|
sessionId: `bg-${Date.now()}`,
|
|
@@ -572,19 +559,35 @@ describe("Orchestrator", () => {
|
|
|
572
559
|
orch.handleBackgroundCommand("long-task");
|
|
573
560
|
await waitForProcessing();
|
|
574
561
|
|
|
575
|
-
orch.
|
|
562
|
+
orch.handleSessions();
|
|
576
563
|
await waitForProcessing();
|
|
577
564
|
|
|
578
565
|
const listResponse = responses[responses.length - 1];
|
|
579
566
|
expect(listResponse.message).toContain("long-task");
|
|
580
567
|
expect(listResponse.buttons).toBeDefined();
|
|
581
|
-
expect(listResponse.buttons!.length).toBe(2);
|
|
568
|
+
expect(listResponse.buttons!.length).toBe(2);
|
|
582
569
|
const detailBtn = listResponse.buttons![0];
|
|
583
570
|
expect(typeof detailBtn).toBe("object");
|
|
584
571
|
expect((detailBtn as any).data).toMatch(/^detail:/);
|
|
585
572
|
expect((detailBtn as any).text).toContain("long-task");
|
|
586
573
|
expect(listResponse.buttons![1]).toEqual({ text: "Dismiss", data: "_dismiss" });
|
|
587
574
|
});
|
|
575
|
+
|
|
576
|
+
it("marks main session in listing", async () => {
|
|
577
|
+
const { query } = pendingQuery("main-sid");
|
|
578
|
+
const claude = mockClaude(() => query);
|
|
579
|
+
const { orch, responses } = makeOrchestrator(claude);
|
|
580
|
+
|
|
581
|
+
// Start a main query (non-blocking, stays in runningSessions)
|
|
582
|
+
orch.handleMessage("task");
|
|
583
|
+
await waitForProcessing();
|
|
584
|
+
|
|
585
|
+
orch.handleSessions();
|
|
586
|
+
await waitForProcessing();
|
|
587
|
+
|
|
588
|
+
const listResponse = responses[responses.length - 1];
|
|
589
|
+
expect(listResponse.message).toContain("[main]");
|
|
590
|
+
});
|
|
588
591
|
});
|
|
589
592
|
|
|
590
593
|
describe("handlePeek", () => {
|
|
@@ -595,7 +598,7 @@ describe("Orchestrator", () => {
|
|
|
595
598
|
await orch.handlePeek("nonexistent-session");
|
|
596
599
|
await waitForProcessing();
|
|
597
600
|
|
|
598
|
-
expect(responses[0].message).toBe("
|
|
601
|
+
expect(responses[0].message).toBe("Session not found or already finished.");
|
|
599
602
|
});
|
|
600
603
|
|
|
601
604
|
it("peeks at running agent and returns status", async () => {
|
|
@@ -604,7 +607,6 @@ describe("Orchestrator", () => {
|
|
|
604
607
|
const claude = mockClaude((): RunningQuery<unknown> => {
|
|
605
608
|
callCount++;
|
|
606
609
|
if (callCount === 1) {
|
|
607
|
-
// bg agent — never finishes
|
|
608
610
|
return {
|
|
609
611
|
sessionId: "bg-sid",
|
|
610
612
|
startedAt: new Date(),
|
|
@@ -612,7 +614,6 @@ describe("Orchestrator", () => {
|
|
|
612
614
|
kill: mock(async () => {}),
|
|
613
615
|
};
|
|
614
616
|
}
|
|
615
|
-
// peek fork call
|
|
616
617
|
return resolvedQuery("Working on it, 50% done.", "peek-session");
|
|
617
618
|
});
|
|
618
619
|
const { orch, responses } = makeOrchestrator(claude);
|
|
@@ -620,12 +621,11 @@ describe("Orchestrator", () => {
|
|
|
620
621
|
orch.handleBackgroundCommand("research");
|
|
621
622
|
await waitForProcessing();
|
|
622
623
|
|
|
623
|
-
|
|
624
|
-
orch.handleBackgroundList();
|
|
624
|
+
orch.handleSessions();
|
|
625
625
|
await waitForProcessing();
|
|
626
626
|
const listResponse = responses[responses.length - 1];
|
|
627
627
|
const detailBtn = listResponse.buttons![0] as { text: string; data: string };
|
|
628
|
-
const sessionId = detailBtn.data.slice(7);
|
|
628
|
+
const sessionId = detailBtn.data.slice(7);
|
|
629
629
|
|
|
630
630
|
await orch.handlePeek(sessionId);
|
|
631
631
|
await waitForProcessing();
|
|
@@ -660,11 +660,11 @@ describe("Orchestrator", () => {
|
|
|
660
660
|
orch.handleBackgroundCommand("failing-peek");
|
|
661
661
|
await waitForProcessing();
|
|
662
662
|
|
|
663
|
-
orch.
|
|
663
|
+
orch.handleSessions();
|
|
664
664
|
await waitForProcessing();
|
|
665
665
|
const listResponse = responses[responses.length - 1];
|
|
666
666
|
const detailBtn = listResponse.buttons![0] as { text: string; data: string };
|
|
667
|
-
const sessionId = detailBtn.data.slice(7);
|
|
667
|
+
const sessionId = detailBtn.data.slice(7);
|
|
668
668
|
|
|
669
669
|
await orch.handlePeek(sessionId);
|
|
670
670
|
await waitForProcessing();
|
|
@@ -682,10 +682,10 @@ describe("Orchestrator", () => {
|
|
|
682
682
|
orch.handleDetail("nonexistent-session");
|
|
683
683
|
await waitForProcessing();
|
|
684
684
|
|
|
685
|
-
expect(responses[0].message).toBe("
|
|
685
|
+
expect(responses[0].message).toBe("Session not found or already finished.");
|
|
686
686
|
});
|
|
687
687
|
|
|
688
|
-
it("shows
|
|
688
|
+
it("shows session details with peek/kill/dismiss buttons", async () => {
|
|
689
689
|
saveSessions({ mainSessionId: "test-session" }, tmpSettingsDir);
|
|
690
690
|
const claude = mockClaude((): RunningQuery<unknown> => ({
|
|
691
691
|
sessionId: "bg-sid",
|
|
@@ -698,7 +698,7 @@ describe("Orchestrator", () => {
|
|
|
698
698
|
orch.handleBackgroundCommand("research pricing");
|
|
699
699
|
await waitForProcessing();
|
|
700
700
|
|
|
701
|
-
orch.
|
|
701
|
+
orch.handleSessions();
|
|
702
702
|
await waitForProcessing();
|
|
703
703
|
const listResponse = responses[responses.length - 1];
|
|
704
704
|
const detailBtn = listResponse.buttons![0] as { text: string; data: string };
|
|
@@ -732,7 +732,7 @@ describe("Orchestrator", () => {
|
|
|
732
732
|
orch.handleBackgroundCommand(longPrompt);
|
|
733
733
|
await waitForProcessing();
|
|
734
734
|
|
|
735
|
-
orch.
|
|
735
|
+
orch.handleSessions();
|
|
736
736
|
await waitForProcessing();
|
|
737
737
|
const listResponse = responses[responses.length - 1];
|
|
738
738
|
const detailBtn = listResponse.buttons![0] as { text: string; data: string };
|
|
@@ -742,7 +742,6 @@ describe("Orchestrator", () => {
|
|
|
742
742
|
await waitForProcessing();
|
|
743
743
|
|
|
744
744
|
const detailResponse = responses[responses.length - 1];
|
|
745
|
-
// 300 chars + ellipsis
|
|
746
745
|
expect(detailResponse.message).toContain("a".repeat(300));
|
|
747
746
|
expect(detailResponse.message).toContain("…");
|
|
748
747
|
expect(detailResponse.message).not.toContain("a".repeat(301));
|
|
@@ -761,7 +760,7 @@ describe("Orchestrator", () => {
|
|
|
761
760
|
orch.handleBackgroundCommand("research");
|
|
762
761
|
await waitForProcessing();
|
|
763
762
|
|
|
764
|
-
orch.
|
|
763
|
+
orch.handleSessions();
|
|
765
764
|
await waitForProcessing();
|
|
766
765
|
const listResponse = responses[responses.length - 1];
|
|
767
766
|
const detailBtn = listResponse.buttons![0] as { text: string; data: string };
|
|
@@ -783,10 +782,10 @@ describe("Orchestrator", () => {
|
|
|
783
782
|
await orch.handleKill("nonexistent-session");
|
|
784
783
|
await waitForProcessing();
|
|
785
784
|
|
|
786
|
-
expect(responses[0].message).toBe("
|
|
785
|
+
expect(responses[0].message).toBe("Session not found or already finished.");
|
|
787
786
|
});
|
|
788
787
|
|
|
789
|
-
it("kills running
|
|
788
|
+
it("kills running session and sends confirmation", async () => {
|
|
790
789
|
saveSessions({ mainSessionId: "test-session" }, tmpSettingsDir);
|
|
791
790
|
const killMock = mock(async () => {});
|
|
792
791
|
const claude = mockClaude((): RunningQuery<unknown> => ({
|
|
@@ -800,7 +799,7 @@ describe("Orchestrator", () => {
|
|
|
800
799
|
orch.handleBackgroundCommand("research pricing");
|
|
801
800
|
await waitForProcessing();
|
|
802
801
|
|
|
803
|
-
orch.
|
|
802
|
+
orch.handleSessions();
|
|
804
803
|
await waitForProcessing();
|
|
805
804
|
const listResponse = responses[responses.length - 1];
|
|
806
805
|
const detailBtn = listResponse.buttons![0] as { text: string; data: string };
|
|
@@ -814,10 +813,9 @@ describe("Orchestrator", () => {
|
|
|
814
813
|
expect(killResponse.message).toContain("Killed");
|
|
815
814
|
expect(killResponse.message).toContain("research-pricing");
|
|
816
815
|
|
|
817
|
-
|
|
818
|
-
orch.handleBackgroundList();
|
|
816
|
+
orch.handleSessions();
|
|
819
817
|
await waitForProcessing();
|
|
820
|
-
expect(responses[responses.length - 1].message).toBe("No
|
|
818
|
+
expect(responses[responses.length - 1].message).toBe("No running sessions.");
|
|
821
819
|
});
|
|
822
820
|
|
|
823
821
|
it("does not feed error back to queue after kill", async () => {
|
|
@@ -835,7 +833,7 @@ describe("Orchestrator", () => {
|
|
|
835
833
|
orch.handleBackgroundCommand("task");
|
|
836
834
|
await waitForProcessing();
|
|
837
835
|
|
|
838
|
-
orch.
|
|
836
|
+
orch.handleSessions();
|
|
839
837
|
await waitForProcessing();
|
|
840
838
|
const listResponse = responses[responses.length - 1];
|
|
841
839
|
const detailBtn = listResponse.buttons![0] as { text: string; data: string };
|
|
@@ -845,12 +843,9 @@ describe("Orchestrator", () => {
|
|
|
845
843
|
await waitForProcessing();
|
|
846
844
|
const countAfterKill = responses.length;
|
|
847
845
|
|
|
848
|
-
// Simulate the bg process rejecting after kill
|
|
849
846
|
rejectBg!(new Error("process killed"));
|
|
850
847
|
await waitForProcessing(100);
|
|
851
848
|
|
|
852
|
-
// No additional error responses should have been added
|
|
853
|
-
// Only the "Killed" message, no "[Error]" from the bg handler
|
|
854
849
|
const newResponses = responses.slice(countAfterKill);
|
|
855
850
|
expect(newResponses.every((r) => !r.message.includes("[Error]"))).toBe(true);
|
|
856
851
|
});
|
|
@@ -875,23 +870,15 @@ describe("Orchestrator", () => {
|
|
|
875
870
|
});
|
|
876
871
|
});
|
|
877
872
|
|
|
878
|
-
describe("background management
|
|
873
|
+
describe("background management", () => {
|
|
879
874
|
it("spawns background agent and feeds result back to queue", async () => {
|
|
880
875
|
saveSessions({ mainSessionId: "test-session" }, tmpSettingsDir);
|
|
881
|
-
|
|
882
|
-
const bgResult = new Promise<QueryResult<unknown>>((r) => { resolvePromise = r; });
|
|
876
|
+
const { query: bgQuery, resolve: resolveBg } = pendingQuery("bg-sid");
|
|
883
877
|
|
|
884
878
|
let callCount = 0;
|
|
885
879
|
const claude = mockClaude((): RunningQuery<unknown> => {
|
|
886
880
|
callCount++;
|
|
887
|
-
if (callCount === 1)
|
|
888
|
-
return {
|
|
889
|
-
sessionId: "bg-sid",
|
|
890
|
-
startedAt: new Date(),
|
|
891
|
-
result: bgResult,
|
|
892
|
-
kill: mock(async () => {}),
|
|
893
|
-
};
|
|
894
|
-
}
|
|
881
|
+
if (callCount === 1) return bgQuery;
|
|
895
882
|
return resolvedQuery({ action: "send", message: "bg result processed", actionReason: "ok" });
|
|
896
883
|
});
|
|
897
884
|
const { orch, responses } = makeOrchestrator(claude);
|
|
@@ -901,29 +888,20 @@ describe("Orchestrator", () => {
|
|
|
901
888
|
|
|
902
889
|
expect(responses[0].message).toContain('started.');
|
|
903
890
|
|
|
904
|
-
|
|
891
|
+
resolveBg(queryResult({ action: "send", message: "done!", actionReason: "completed" }));
|
|
905
892
|
await waitForProcessing(100);
|
|
906
893
|
|
|
907
|
-
|
|
908
|
-
expect(callCount).toBe(2); // 1 bg agent + 1 bg result fed back
|
|
894
|
+
expect(callCount).toBe(2);
|
|
909
895
|
});
|
|
910
896
|
|
|
911
897
|
it("feeds error back to queue on spawn failure", async () => {
|
|
912
898
|
saveSessions({ mainSessionId: "test-session" }, tmpSettingsDir);
|
|
913
|
-
|
|
914
|
-
const bgResult = new Promise<QueryResult<unknown>>((_, r) => { rejectPromise = r; });
|
|
899
|
+
const { query: bgQuery, reject: rejectBg } = pendingQuery("bg-sid");
|
|
915
900
|
|
|
916
901
|
let callCount = 0;
|
|
917
902
|
const claude = mockClaude((): RunningQuery<unknown> => {
|
|
918
903
|
callCount++;
|
|
919
|
-
if (callCount === 1)
|
|
920
|
-
return {
|
|
921
|
-
sessionId: "bg-sid",
|
|
922
|
-
startedAt: new Date(),
|
|
923
|
-
result: bgResult,
|
|
924
|
-
kill: mock(async () => {}),
|
|
925
|
-
};
|
|
926
|
-
}
|
|
904
|
+
if (callCount === 1) return bgQuery;
|
|
927
905
|
return resolvedQuery({ action: "send", message: "error processed", actionReason: "ok" });
|
|
928
906
|
});
|
|
929
907
|
const { orch, responses } = makeOrchestrator(claude);
|
|
@@ -931,75 +909,12 @@ describe("Orchestrator", () => {
|
|
|
931
909
|
orch.handleBackgroundCommand("failing task");
|
|
932
910
|
await waitForProcessing();
|
|
933
911
|
|
|
934
|
-
|
|
912
|
+
rejectBg(new Error("spawn failed"));
|
|
935
913
|
await waitForProcessing(100);
|
|
936
914
|
|
|
937
|
-
// Error should be fed back and processed
|
|
938
915
|
expect(callCount).toBe(2);
|
|
939
916
|
expect(responses[responses.length - 1].message).toBe("error processed");
|
|
940
917
|
});
|
|
941
|
-
|
|
942
|
-
it("adopt feeds error back when deferred rejects", async () => {
|
|
943
|
-
saveSessions({ mainSessionId: "adopted-session" }, tmpSettingsDir);
|
|
944
|
-
let rejectCompletion: (err: Error) => void;
|
|
945
|
-
const completion = new Promise<QueryResult<unknown>>((_, r) => { rejectCompletion = r; });
|
|
946
|
-
|
|
947
|
-
const origSetTimeout = globalThis.setTimeout;
|
|
948
|
-
|
|
949
|
-
const claude = mockClaude((): RunningQuery<unknown> => {
|
|
950
|
-
globalThis.setTimeout = ((fn: Function) => { fn(); return 0 as any; }) as any;
|
|
951
|
-
return {
|
|
952
|
-
sessionId: "adopted-session",
|
|
953
|
-
startedAt: new Date(),
|
|
954
|
-
result: completion,
|
|
955
|
-
kill: mock(async () => {}),
|
|
956
|
-
};
|
|
957
|
-
});
|
|
958
|
-
const { orch, responses } = makeOrchestrator(claude);
|
|
959
|
-
|
|
960
|
-
orch.handleMessage("slow");
|
|
961
|
-
await new Promise((r) => origSetTimeout(r, 10));
|
|
962
|
-
globalThis.setTimeout = origSetTimeout;
|
|
963
|
-
await waitForProcessing();
|
|
964
|
-
|
|
965
|
-
rejectCompletion!(new Error("process crashed"));
|
|
966
|
-
await waitForProcessing(100);
|
|
967
|
-
|
|
968
|
-
const messages = responses.map((r) => r.message);
|
|
969
|
-
expect(messages.some((m) => m.includes("[Error]"))).toBe(true);
|
|
970
|
-
});
|
|
971
|
-
|
|
972
|
-
it("adopt feeds result back when deferred resolves", async () => {
|
|
973
|
-
saveSessions({ mainSessionId: "adopted-session" }, tmpSettingsDir);
|
|
974
|
-
let resolveCompletion: (r: QueryResult<unknown>) => void;
|
|
975
|
-
const completion = new Promise<QueryResult<unknown>>((r) => { resolveCompletion = r; });
|
|
976
|
-
|
|
977
|
-
const origSetTimeout = globalThis.setTimeout;
|
|
978
|
-
|
|
979
|
-
const claude = mockClaude((): RunningQuery<unknown> => {
|
|
980
|
-
globalThis.setTimeout = ((fn: Function) => { fn(); return 0 as any; }) as any;
|
|
981
|
-
return {
|
|
982
|
-
sessionId: "adopted-session",
|
|
983
|
-
startedAt: new Date(),
|
|
984
|
-
result: completion,
|
|
985
|
-
kill: mock(async () => {}),
|
|
986
|
-
};
|
|
987
|
-
});
|
|
988
|
-
const { orch, responses } = makeOrchestrator(claude);
|
|
989
|
-
|
|
990
|
-
orch.handleMessage("slow");
|
|
991
|
-
await new Promise((r) => origSetTimeout(r, 10));
|
|
992
|
-
globalThis.setTimeout = origSetTimeout;
|
|
993
|
-
await waitForProcessing();
|
|
994
|
-
|
|
995
|
-
expect(responses[0].message).toContain("taking longer");
|
|
996
|
-
|
|
997
|
-
resolveCompletion!(queryResult({ action: "send", message: "completed!", actionReason: "ok" }));
|
|
998
|
-
await waitForProcessing(100);
|
|
999
|
-
|
|
1000
|
-
const messages = responses.map((r) => r.message);
|
|
1001
|
-
expect(messages).toContain("completed!");
|
|
1002
|
-
});
|
|
1003
918
|
});
|
|
1004
919
|
|
|
1005
920
|
describe("onResponse error handling", () => {
|
|
@@ -1013,7 +928,7 @@ describe("Orchestrator", () => {
|
|
|
1013
928
|
claude,
|
|
1014
929
|
});
|
|
1015
930
|
|
|
1016
|
-
orch.
|
|
931
|
+
orch.handleSessions();
|
|
1017
932
|
await waitForProcessing();
|
|
1018
933
|
|
|
1019
934
|
expect(failingOnResponse).toHaveBeenCalled();
|