@pi-archimedes/subagent 2.10.3 → 2.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +1 -1
  2. package/package.json +10 -10
  3. package/src/agent-panel.mcp.test.ts +76 -0
  4. package/src/agent-panel.tools.test.ts +162 -0
  5. package/src/agent-panel.ts +37 -8
  6. package/src/agent-panel.width-audit.test.ts +258 -0
  7. package/src/agents-parse.test.ts +252 -0
  8. package/src/agents.ts +72 -5
  9. package/src/compact.test.ts +136 -57
  10. package/src/compact.ts +57 -37
  11. package/src/dispatch.test.ts +73 -1
  12. package/src/dispatch.ts +20 -2
  13. package/src/execute.test.ts +9 -1
  14. package/src/execute.ts +26 -29
  15. package/src/expanded.test.ts +61 -0
  16. package/src/expanded.ts +96 -80
  17. package/src/format.test.ts +71 -29
  18. package/src/format.ts +71 -28
  19. package/src/frontmatter-io.test.ts +62 -0
  20. package/src/frontmatter-io.ts +16 -2
  21. package/src/handlers.test.ts +21 -3
  22. package/src/handlers.ts +22 -3
  23. package/src/index.output-schema.test.ts +317 -0
  24. package/src/index.test.ts +486 -0
  25. package/src/index.ts +168 -8
  26. package/src/render.ts +46 -36
  27. package/src/row-block.test.ts +195 -0
  28. package/src/row-block.ts +186 -0
  29. package/src/run-outcome.test.ts +71 -0
  30. package/src/run-outcome.ts +84 -0
  31. package/src/spawn.test.ts +204 -19
  32. package/src/spawn.ts +49 -17
  33. package/src/stream.test.ts +215 -17
  34. package/src/stream.ts +47 -9
  35. package/src/thinking-validation.test.ts +96 -0
  36. package/src/thinking-validation.ts +78 -0
  37. package/src/tool-declarations.test.ts +58 -0
  38. package/src/tool-row.width-audit.test.ts +720 -0
  39. package/src/tool-schema.ts +85 -0
  40. package/src/tool-validation.test.ts +321 -0
  41. package/src/tool-validation.ts +262 -0
  42. package/src/types.ts +86 -0
  43. package/src/usage.test.ts +95 -0
  44. package/src/usage.ts +87 -0
  45. package/src/cost.test.ts +0 -62
  46. package/src/cost.ts +0 -8
@@ -0,0 +1,486 @@
1
+ /**
2
+ * Dispatch-time behaviour of the `subagent` tool: the two halves of tool-name
3
+ * validation as the calling model sees them.
4
+ *
5
+ * * A `tools:` list the config read rejected (an entry pi cannot read as a
6
+ * name) refuses the dispatch — the child must never be spawned with a list
7
+ * whose meaning is up to the host's parser.
8
+ * * A name that is well formed but not in `pi.getAllTools()` right now is a
9
+ * **warning** and the dispatch runs anyway. A plugin being toggled off, or an
10
+ * MCP server being disconnected, must not make `tools: web_search` unusable.
11
+ *
12
+ * `./agents.js` and `./execute.js` are mocked so the assertions are about the
13
+ * gate, not about the filesystem or a real child process.
14
+ */
15
+
16
+ import { describe, it, expect, vi, beforeEach } from "vitest";
17
+ import { Events } from "@pi-archimedes/core/events";
18
+
19
+ const discoverAgents = vi.hoisted(() => vi.fn());
20
+ const executeSubagent = vi.hoisted(() => vi.fn());
21
+ const executeParallel = vi.hoisted(() => vi.fn());
22
+
23
+ vi.mock("./agents.js", async (importOriginal) => ({
24
+ ...((await importOriginal()) as Record<string, unknown>),
25
+ discoverAgents,
26
+ }));
27
+ vi.mock("./execute.js", () => ({
28
+ executeSubagent,
29
+ executeParallel,
30
+ }));
31
+
32
+ const { registerSubagent } = await import("./index.js");
33
+
34
+ interface FakeTool {
35
+ name: string;
36
+ execute: (
37
+ id: string,
38
+ params: unknown,
39
+ signal: AbortSignal | undefined,
40
+ onUpdate: unknown,
41
+ ctx: unknown,
42
+ ) => Promise<{ content: Array<{ text: string }>; isError?: boolean }>;
43
+ }
44
+
45
+ function register(): FakeTool[] {
46
+ const tools: FakeTool[] = [];
47
+ const pi = {
48
+ registerTool: (def: FakeTool) => tools.push(def),
49
+ on: () => {},
50
+ getAllTools: () => [{ name: "read" }, { name: "bash" }],
51
+ // The handle the tool forwards to `executeSubagent`. Recorded so the
52
+ // COST_UPDATE assertions below can say "nothing was published here".
53
+ events: { emit: (channel: string, payload: unknown) => busEmits.push([channel, payload]), on: () => () => {} },
54
+ };
55
+ // `as never` at the seam only: the fake API carries the members the
56
+ // registration path actually touches.
57
+ registerSubagent(pi as never);
58
+ return tools;
59
+ }
60
+
61
+ /** Everything the tool emitted on its handle, in order. */
62
+ const busEmits: Array<[string, unknown]> = [];
63
+
64
+ beforeEach(() => {
65
+ busEmits.length = 0;
66
+ });
67
+
68
+ function makeCtx(overrides: Record<string, unknown> = {}) {
69
+ return {
70
+ cwd: "/tmp",
71
+ model: undefined,
72
+ modelRegistry: { getAll: () => [], getAvailable: () => [] },
73
+ hasUI: true,
74
+ ui: { notify: vi.fn() },
75
+ ...overrides,
76
+ };
77
+ }
78
+
79
+ const okResult = {
80
+ agent: "reviewer",
81
+ task: "review it",
82
+ exitCode: 0,
83
+ usage: { input: 10, output: 5, cacheRead: 0, cacheWrite: 0, cost: 0.01, turns: 1 },
84
+ model: "anthropic/claude-opus-4.5",
85
+ finalOutput: "all good",
86
+ error: undefined,
87
+ progress: undefined,
88
+ progressSummary: { toolCount: 1, tokens: 15, durationMs: 10 },
89
+ };
90
+
91
+ function agentConfig(overrides: Record<string, unknown> = {}) {
92
+ return {
93
+ name: "reviewer",
94
+ description: "d",
95
+ systemPrompt: "p",
96
+ source: "user",
97
+ filePath: "/tmp/agents/reviewer.md",
98
+ ...overrides,
99
+ };
100
+ }
101
+
102
+ function subagent(tools: FakeTool[]): FakeTool {
103
+ const tool = tools.find((t) => t.name === "subagent");
104
+ if (!tool) throw new Error("subagent tool was not registered");
105
+ return tool;
106
+ }
107
+
108
+ beforeEach(() => {
109
+ discoverAgents.mockReset();
110
+ executeSubagent.mockReset();
111
+ executeParallel.mockReset();
112
+ executeSubagent.mockResolvedValue(okResult);
113
+ executeParallel.mockResolvedValue([okResult]);
114
+ });
115
+
116
+ describe("dispatch: membership of tools: entries", () => {
117
+ it("a well-formed name pi does not have right now warns, naming the entry", async () => {
118
+ discoverAgents.mockReturnValue([agentConfig({ tools: ["read", "web_search"] })]);
119
+ const ctx = makeCtx();
120
+ const result = await subagent(register()).execute(
121
+ "call-1",
122
+ { agent: "reviewer", task: "review it" },
123
+ undefined,
124
+ undefined,
125
+ ctx,
126
+ );
127
+
128
+ const warnings = ctx.ui.notify.mock.calls.filter(([, kind]) => kind === "warning");
129
+ expect(warnings.length).toBe(1);
130
+ expect(String(warnings[0]![0])).toContain("web_search");
131
+ expect(result.isError).toBeFalsy();
132
+ });
133
+
134
+ it("and the dispatch still runs, returning the child's real result", async () => {
135
+ discoverAgents.mockReturnValue([agentConfig({ tools: ["read", "web_search"] })]);
136
+ const ctx = makeCtx();
137
+ const result = await subagent(register()).execute(
138
+ "call-1",
139
+ { agent: "reviewer", task: "review it" },
140
+ undefined,
141
+ undefined,
142
+ ctx,
143
+ );
144
+
145
+ // The success path, not just the message: the child ran and its output is
146
+ // what the model gets back.
147
+ expect(executeSubagent).toHaveBeenCalledTimes(1);
148
+ expect(result.content[0]!.text).toBe("all good");
149
+ expect(result.isError).toBeFalsy();
150
+ });
151
+
152
+ it("names every unavailable entry in one warning", async () => {
153
+ discoverAgents.mockReturnValue([agentConfig({ tools: ["read", "web_search", "fetch_content"] })]);
154
+ const ctx = makeCtx();
155
+ await subagent(register()).execute("c", { agent: "reviewer", task: "t" }, undefined, undefined, ctx);
156
+ const message = String(ctx.ui.notify.mock.calls.find(([, kind]) => kind === "warning")?.[0] ?? "");
157
+ expect(message).toContain("web_search");
158
+ expect(message).toContain("fetch_content");
159
+ });
160
+
161
+ it("says nothing when every entry is a tool pi has", async () => {
162
+ discoverAgents.mockReturnValue([agentConfig({ tools: ["read", "bash"] })]);
163
+ const ctx = makeCtx();
164
+ await subagent(register()).execute("c", { agent: "reviewer", task: "t" }, undefined, undefined, ctx);
165
+ expect(ctx.ui.notify).not.toHaveBeenCalled();
166
+ expect(executeSubagent).toHaveBeenCalledTimes(1);
167
+ });
168
+
169
+ it("says nothing when the agent has no allowlist (nothing to check)", async () => {
170
+ discoverAgents.mockReturnValue([agentConfig()]);
171
+ const ctx = makeCtx();
172
+ await subagent(register()).execute("c", { agent: "reviewer", task: "t" }, undefined, undefined, ctx);
173
+ expect(ctx.ui.notify).not.toHaveBeenCalled();
174
+ });
175
+
176
+ it("a * entry is a pattern, so it is judged against the tools pi has, not by name", async () => {
177
+ const tools: Array<{ name: string }> = [{ name: "read" }, { name: "bash" }, { name: "mcp__radius__query" }];
178
+ discoverAgents.mockReturnValue([agentConfig({ tools: ["read", "mcp__radius__*"] })]);
179
+ const ctx = makeCtx();
180
+ const registered: FakeTool[] = [];
181
+ registerSubagent({
182
+ registerTool: (def: FakeTool) => registered.push(def),
183
+ on: () => {},
184
+ getAllTools: () => tools,
185
+ } as never);
186
+ await subagent(registered).execute("c", { agent: "reviewer", task: "t" }, undefined, undefined, ctx);
187
+ expect(ctx.ui.notify).not.toHaveBeenCalled();
188
+ expect(executeSubagent).toHaveBeenCalledTimes(1);
189
+ });
190
+
191
+ it("a pattern that nothing currently satisfies is named in the warning", async () => {
192
+ discoverAgents.mockReturnValue([agentConfig({ tools: ["read", "mcp__*"] })]);
193
+ const ctx = makeCtx();
194
+ await subagent(register()).execute("c", { agent: "reviewer", task: "t" }, undefined, undefined, ctx);
195
+ const warning = ctx.ui.notify.mock.calls.find(([, kind]) => kind === "warning");
196
+ expect(warning).toBeTruthy();
197
+ expect(String(warning![0])).toContain("mcp__*");
198
+ // Still a warning: the run happens.
199
+ expect(executeSubagent).toHaveBeenCalledTimes(1);
200
+ });
201
+
202
+ it("warns per task in parallel mode and still dispatches the batch", async () => {
203
+ discoverAgents.mockReturnValue([agentConfig({ tools: ["read", "web_search"] })]);
204
+ const ctx = makeCtx();
205
+ const result = await subagent(register()).execute(
206
+ "c",
207
+ { tasks: [{ agent: "reviewer", task: "a" }, { agent: "reviewer", task: "b" }] },
208
+ undefined,
209
+ undefined,
210
+ ctx,
211
+ );
212
+ expect(executeParallel).toHaveBeenCalledTimes(1);
213
+ expect(result.isError).toBeFalsy();
214
+ const warnings = ctx.ui.notify.mock.calls.filter(([, kind]) => kind === "warning");
215
+ expect(warnings.length).toBeGreaterThan(0);
216
+ expect(String(warnings[0]![0])).toContain("web_search");
217
+ });
218
+
219
+ it("stays quiet when there is no UI to notify, and still runs", async () => {
220
+ discoverAgents.mockReturnValue([agentConfig({ tools: ["web_search"] })]);
221
+ const ctx = makeCtx({ hasUI: false });
222
+ await subagent(register()).execute("c", { agent: "reviewer", task: "t" }, undefined, undefined, ctx);
223
+ expect(ctx.ui.notify).not.toHaveBeenCalled();
224
+ expect(executeSubagent).toHaveBeenCalledTimes(1);
225
+ });
226
+ });
227
+
228
+ describe("dispatch: a tools list the config read rejected", () => {
229
+ it("refuses to spawn, naming the entry and the file", async () => {
230
+ discoverAgents.mockReturnValue([
231
+ agentConfig({
232
+ tools: undefined,
233
+ toolsError: 'Agent "reviewer" has a tools entry starting with "+" (in /tmp/agents/reviewer.md): "+web_search".',
234
+ }),
235
+ ]);
236
+ const ctx = makeCtx();
237
+ const result = await subagent(register()).execute(
238
+ "c",
239
+ { agent: "reviewer", task: "t" },
240
+ undefined,
241
+ undefined,
242
+ ctx,
243
+ );
244
+ expect(executeSubagent).not.toHaveBeenCalled();
245
+ expect(result.isError).toBe(true);
246
+ expect(result.content[0]!.text).toContain("+web_search");
247
+ expect(result.content[0]!.text).toContain("/tmp/agents/reviewer.md");
248
+ });
249
+
250
+ it("refuses the whole parallel batch rather than running the poisoned task", async () => {
251
+ discoverAgents.mockReturnValue([
252
+ agentConfig({ toolsError: 'Agent "reviewer" lists the tool "read" more than once (in /tmp/agents/reviewer.md).' }),
253
+ ]);
254
+ const ctx = makeCtx();
255
+ const result = await subagent(register()).execute(
256
+ "c",
257
+ { tasks: [{ agent: "reviewer", task: "a" }] },
258
+ undefined,
259
+ undefined,
260
+ ctx,
261
+ );
262
+ expect(executeParallel).not.toHaveBeenCalled();
263
+ expect(result.isError).toBe(true);
264
+ expect(result.content[0]!.text).toContain("read");
265
+ });
266
+ });
267
+
268
+ describe("dispatch: the result the model reads carries the run outcome", () => {
269
+ it("marks a truncated answer, and does not report it as an error", async () => {
270
+ discoverAgents.mockReturnValue([]);
271
+ executeSubagent.mockResolvedValue({
272
+ ...okResult,
273
+ outcome: "truncated",
274
+ exitCode: 1,
275
+ finalOutput: "here is most of it",
276
+ error: undefined,
277
+ });
278
+ const ctx = makeCtx();
279
+ const result = await subagent(register()).execute("c", { task: "t" }, undefined, undefined, ctx);
280
+ expect(result.isError).toBeFalsy();
281
+ expect(result.content[0]!.text).toContain("here is most of it");
282
+ expect(result.content[0]!.text).toMatch(/truncated/i);
283
+ });
284
+
285
+ it("marks a cancelled run", async () => {
286
+ discoverAgents.mockReturnValue([]);
287
+ executeSubagent.mockResolvedValue({
288
+ ...okResult,
289
+ outcome: "aborted",
290
+ exitCode: 143,
291
+ finalOutput: undefined,
292
+ });
293
+ const result = await subagent(register()).execute("c", { task: "t" }, undefined, undefined, makeCtx());
294
+ expect(result.isError).toBeFalsy();
295
+ expect(result.content[0]!.text).toMatch(/aborted/i);
296
+ });
297
+
298
+ it("keeps reporting a failed run as an error, with no outcome claim", async () => {
299
+ discoverAgents.mockReturnValue([]);
300
+ executeSubagent.mockResolvedValue({
301
+ ...okResult,
302
+ outcome: undefined,
303
+ exitCode: 1,
304
+ finalOutput: undefined,
305
+ error: "boom",
306
+ });
307
+ const result = await subagent(register()).execute("c", { task: "t" }, undefined, undefined, makeCtx());
308
+ expect(result.isError).toBe(true);
309
+ expect(result.content[0]!.text).toContain("boom");
310
+ });
311
+
312
+ it("reports a failed run that exited 0 as an error too", async () => {
313
+ // The exit code cannot be the trigger: `--mode json` — what the child runs —
314
+ // never maps `stopReason` onto it (that mapping is print mode's text branch),
315
+ // so a child whose last response errored exits 0. Keying `isError` off the
316
+ // code reported a provider failure to the model as a success.
317
+ discoverAgents.mockReturnValue([]);
318
+ executeSubagent.mockResolvedValue({
319
+ ...okResult,
320
+ outcome: undefined,
321
+ exitCode: 0,
322
+ finalOutput: undefined,
323
+ error: '401 unauthorized',
324
+ });
325
+ const result = await subagent(register()).execute("c", { task: "t" }, undefined, undefined, makeCtx());
326
+ expect(result.isError).toBe(true);
327
+ expect(result.content[0]!.text).toContain("401");
328
+ });
329
+
330
+ it("says nothing extra for a completed run", async () => {
331
+ discoverAgents.mockReturnValue([]);
332
+ const result = await subagent(register()).execute("c", { task: "t" }, undefined, undefined, makeCtx());
333
+ expect(result.content[0]!.text).toBe("all good");
334
+ expect(result.isError).toBeFalsy();
335
+ });
336
+ });
337
+
338
+ describe("dispatch: a thinking level pi would drop is said out loud", () => {
339
+ // pi's `--thinking` parser warns and drops an unknown level, and a json
340
+ // child's stderr is invisible on a successful run — so without this the user
341
+ // reads "thinking: ultra" in the config and gets the default. The run still
342
+ // goes ahead (that IS what pi would have done); only the silence changes.
343
+ it("warns, naming the level and what runs instead, and still dispatches", async () => {
344
+ discoverAgents.mockReturnValue([agentConfig({ thinking: "ultra" })]);
345
+ const ctx = makeCtx();
346
+ const result = await subagent(register()).execute(
347
+ "c",
348
+ { agent: "reviewer", task: "t" },
349
+ undefined,
350
+ undefined,
351
+ ctx,
352
+ );
353
+ const warnings = ctx.ui.notify.mock.calls.filter(([, kind]) => kind === "warning");
354
+ expect(warnings.length).toBe(1);
355
+ expect(String(warnings[0]![0])).toContain("ultra");
356
+ expect(String(warnings[0]![0])).toMatch(/default thinking level/i);
357
+ expect(result.isError).toBeFalsy();
358
+ expect(executeSubagent).toHaveBeenCalledTimes(1);
359
+ });
360
+
361
+ it("says nothing for a level pi takes", async () => {
362
+ discoverAgents.mockReturnValue([agentConfig({ thinking: "medium" })]);
363
+ const ctx = makeCtx();
364
+ await subagent(register()).execute("c", { agent: "reviewer", task: "t" }, undefined, undefined, ctx);
365
+ expect(ctx.ui.notify).not.toHaveBeenCalled();
366
+ });
367
+
368
+ it("says nothing when the config sets no thinking level", async () => {
369
+ discoverAgents.mockReturnValue([agentConfig()]);
370
+ const ctx = makeCtx();
371
+ await subagent(register()).execute("c", { agent: "reviewer", task: "t" }, undefined, undefined, ctx);
372
+ expect(ctx.ui.notify).not.toHaveBeenCalled();
373
+ });
374
+
375
+ it("reports it in parallel mode too, once per distinct level", async () => {
376
+ discoverAgents.mockReturnValue([agentConfig({ thinking: "turbo" })]);
377
+ const ctx = makeCtx();
378
+ await subagent(register()).execute(
379
+ "c",
380
+ { tasks: [{ agent: "reviewer", task: "a" }, { agent: "reviewer", task: "b" }] },
381
+ undefined,
382
+ undefined,
383
+ ctx,
384
+ );
385
+ const warnings = ctx.ui.notify.mock.calls.filter(([, kind]) => kind === "warning");
386
+ expect(warnings.length).toBe(1);
387
+ expect(String(warnings[0]![0])).toContain("turbo");
388
+ expect(executeParallel).toHaveBeenCalledTimes(1);
389
+ });
390
+ });
391
+
392
+ describe("dispatch: cost is reported natively, not on the bus", () => {
393
+ it("the tool result carries usage in pi's shape, so /session folds it in", async () => {
394
+ discoverAgents.mockReturnValue([]);
395
+ const result = (await subagent(register()).execute(
396
+ "c",
397
+ { task: "t" },
398
+ undefined,
399
+ undefined,
400
+ makeCtx(),
401
+ )) as unknown as {
402
+ usage?: {
403
+ input: number;
404
+ output: number;
405
+ cacheRead: number;
406
+ cacheWrite: number;
407
+ totalTokens: number;
408
+ cost: { total: number };
409
+ };
410
+ };
411
+ expect(result.usage?.input).toBe(10);
412
+ expect(result.usage?.output).toBe(5);
413
+ expect(result.usage?.totalTokens).toBe(15);
414
+ expect(result.usage?.cost.total).toBeCloseTo(0.01);
415
+ });
416
+
417
+ it("a parallel batch carries the summed usage of its runs", async () => {
418
+ discoverAgents.mockReturnValue([]);
419
+ executeParallel.mockResolvedValue([okResult, okResult]);
420
+ const result = (await subagent(register()).execute(
421
+ "c",
422
+ { tasks: [{ task: "a" }, { task: "b" }] },
423
+ undefined,
424
+ undefined,
425
+ makeCtx(),
426
+ )) as unknown as { usage?: { input: number; cost: { total: number } } };
427
+ expect(result.usage?.input).toBe(20);
428
+ expect(result.usage?.cost.total).toBeCloseTo(0.02);
429
+ });
430
+
431
+ it("does not republish the usage of a run the desktop performed (bridge mode)", async () => {
432
+ // A bridge-dispatched child is a session of the desktop's own and books its
433
+ // usage there. pi folds a tool result's `usage` into THIS session's totals,
434
+ // so copying it across would bill the parent for a run it did not perform —
435
+ // the same double-count that deleted the COST_UPDATE route. `executeSubagent`
436
+ // marks the result; the tool has to honour the mark.
437
+ discoverAgents.mockReturnValue([]);
438
+ executeSubagent.mockResolvedValue({ ...okResult, usageAccountedElsewhere: true });
439
+ const result = (await subagent(register()).execute(
440
+ "c",
441
+ { task: "t" },
442
+ undefined,
443
+ undefined,
444
+ makeCtx(),
445
+ )) as unknown as { usage?: unknown; content: Array<{ text: string }> };
446
+ expect(result.usage).toBeUndefined();
447
+ // Only the cost report is withheld — the run's output is untouched.
448
+ expect(result.content[0]!.text).toBe("all good");
449
+ });
450
+
451
+ it("reports the fork path's usage even when a sibling run was done elsewhere", async () => {
452
+ discoverAgents.mockReturnValue([]);
453
+ executeParallel.mockResolvedValue([
454
+ okResult,
455
+ { ...okResult, usageAccountedElsewhere: true },
456
+ ]);
457
+ const result = (await subagent(register()).execute(
458
+ "c",
459
+ { tasks: [{ task: "a" }, { task: "b" }] },
460
+ undefined,
461
+ undefined,
462
+ makeCtx(),
463
+ )) as unknown as { usage?: { input: number; cost: { total: number } } };
464
+ expect(result.usage?.input).toBe(10);
465
+ expect(result.usage?.cost.total).toBeCloseTo(0.01);
466
+ });
467
+
468
+ it("publishes nothing on COST_UPDATE while a run reports its cost", async () => {
469
+ discoverAgents.mockReturnValue([]);
470
+ await subagent(register()).execute("c", { task: "t" }, undefined, undefined, makeCtx());
471
+ expect(busEmits.filter(([channel]) => channel === Events.COST_UPDATE)).toEqual([]);
472
+ });
473
+
474
+ it("publishes nothing on COST_UPDATE for a parallel batch either", async () => {
475
+ executeParallel.mockResolvedValue([okResult, okResult]);
476
+ discoverAgents.mockReturnValue([]);
477
+ await subagent(register()).execute(
478
+ "c",
479
+ { tasks: [{ task: "a" }, { task: "b" }] },
480
+ undefined,
481
+ undefined,
482
+ makeCtx(),
483
+ );
484
+ expect(busEmits.filter(([channel]) => channel === Events.COST_UPDATE)).toEqual([]);
485
+ });
486
+ });