@nebutra/agent-runtime 0.2.1 → 2.0.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 (114) hide show
  1. package/LICENSE +21 -676
  2. package/README.md +33 -10
  3. package/dist/adapters/index.d.ts +24 -5
  4. package/dist/adapters/index.js +27 -0
  5. package/dist/adapters/index.js.map +1 -1
  6. package/dist/{chunk-KCNN4QUQ.js → chunk-5UGIUYJR.js} +4 -4
  7. package/dist/chunk-7ELA4SWE.js +73 -0
  8. package/dist/chunk-7ELA4SWE.js.map +1 -0
  9. package/dist/chunk-Q62VKHIT.js +178 -0
  10. package/dist/chunk-Q62VKHIT.js.map +1 -0
  11. package/dist/chunk-VXILZAXK.js +541 -0
  12. package/dist/chunk-VXILZAXK.js.map +1 -0
  13. package/dist/command-exec.d.ts +45 -0
  14. package/dist/command-exec.js +15 -0
  15. package/dist/command-exec.js.map +1 -0
  16. package/dist/index.d.ts +3 -2
  17. package/dist/index.js +81 -28
  18. package/dist/index.js.map +1 -1
  19. package/dist/orchestration.d.ts +42 -1
  20. package/dist/orchestration.js +7 -3
  21. package/dist/pulsar.js +2 -2
  22. package/dist/sandbox-DfcRttXt.d.ts +219 -0
  23. package/dist/sandbox.d.ts +2 -64
  24. package/dist/sandbox.js +25 -3
  25. package/package.json +81 -30
  26. package/.turbo/turbo-build.log +0 -130
  27. package/.turbo/turbo-test.log +0 -47
  28. package/.turbo/turbo-typecheck.log +0 -4
  29. package/CHANGELOG.md +0 -250
  30. package/dist/chunk-MUF7ZZTO.js +0 -57
  31. package/dist/chunk-MUF7ZZTO.js.map +0 -1
  32. package/dist/chunk-MX2WL43P.js +0 -90
  33. package/dist/chunk-MX2WL43P.js.map +0 -1
  34. package/examples/pulsar-quickstart.ts +0 -35
  35. package/examples/resume-branch.ts +0 -45
  36. package/examples/subagent-fanout.ts +0 -20
  37. package/src/adapters/dispatcher-sse.test.ts +0 -218
  38. package/src/adapters/dispatcher-sse.ts +0 -222
  39. package/src/adapters/index.ts +0 -18
  40. package/src/adapters/mcp-catalog.test.ts +0 -213
  41. package/src/adapters/mcp-catalog.ts +0 -188
  42. package/src/adapters/prisma-rollout.test.ts +0 -153
  43. package/src/adapters/prisma-rollout.ts +0 -104
  44. package/src/agent-runtime.test.ts +0 -176
  45. package/src/artifact-stream.test.ts +0 -330
  46. package/src/artifact-stream.ts +0 -453
  47. package/src/channel-gateway.test.ts +0 -432
  48. package/src/channel-gateway.ts +0 -357
  49. package/src/cli.ts +0 -49
  50. package/src/code-review.test.ts +0 -501
  51. package/src/code-review.ts +0 -495
  52. package/src/command-suggestions.test.ts +0 -251
  53. package/src/command-suggestions.ts +0 -338
  54. package/src/commands.test.ts +0 -184
  55. package/src/commands.ts +0 -140
  56. package/src/commit-message.test.ts +0 -249
  57. package/src/commit-message.ts +0 -180
  58. package/src/context-compaction.test.ts +0 -522
  59. package/src/context-compaction.ts +0 -438
  60. package/src/definitions.test.ts +0 -78
  61. package/src/definitions.ts +0 -190
  62. package/src/deployment-status.test.ts +0 -215
  63. package/src/deployment-status.ts +0 -227
  64. package/src/design-context.test.ts +0 -195
  65. package/src/design-context.ts +0 -198
  66. package/src/dispatcher.test.ts +0 -234
  67. package/src/dispatcher.ts +0 -189
  68. package/src/durable-turn.test.ts +0 -209
  69. package/src/durable-turn.ts +0 -135
  70. package/src/edit-planner.test.ts +0 -204
  71. package/src/edit-planner.ts +0 -325
  72. package/src/fuzzy-match.test.ts +0 -311
  73. package/src/fuzzy-match.ts +0 -444
  74. package/src/hook-pipeline.test.ts +0 -279
  75. package/src/hook-pipeline.ts +0 -373
  76. package/src/inbound-admission.test.ts +0 -394
  77. package/src/inbound-admission.ts +0 -246
  78. package/src/index.ts +0 -49
  79. package/src/loop.test.ts +0 -161
  80. package/src/loop.ts +0 -211
  81. package/src/mcp-bridge.test.ts +0 -165
  82. package/src/mcp-bridge.ts +0 -81
  83. package/src/memory-provider.test.ts +0 -232
  84. package/src/memory-provider.ts +0 -257
  85. package/src/model.ts +0 -168
  86. package/src/orchestration.test.ts +0 -53
  87. package/src/orchestration.ts +0 -146
  88. package/src/permission-ruleset.test.ts +0 -301
  89. package/src/permission-ruleset.ts +0 -1
  90. package/src/policy.ts +0 -151
  91. package/src/project-repo.test.ts +0 -232
  92. package/src/project-repo.ts +0 -311
  93. package/src/protocol.ts +0 -159
  94. package/src/pulsar.test.ts +0 -156
  95. package/src/pulsar.ts +0 -322
  96. package/src/rollout-store-persistent.test.ts +0 -217
  97. package/src/rollout-store-persistent.ts +0 -166
  98. package/src/rollout.ts +0 -150
  99. package/src/sandbox.ts +0 -113
  100. package/src/session-share.test.ts +0 -360
  101. package/src/session-share.ts +0 -310
  102. package/src/skill-distillation.test.ts +0 -177
  103. package/src/skill-distillation.ts +0 -369
  104. package/src/skills.test.ts +0 -277
  105. package/src/skills.ts +0 -277
  106. package/src/subagents.test.ts +0 -290
  107. package/src/subagents.ts +0 -332
  108. package/src/tools.test.ts +0 -12
  109. package/src/tools.ts +0 -129
  110. package/src/workbench.test.ts +0 -0
  111. package/src/workbench.ts +0 -0
  112. package/tsconfig.json +0 -12
  113. package/tsup.config.ts +0 -36
  114. /package/dist/{chunk-KCNN4QUQ.js.map → chunk-5UGIUYJR.js.map} +0 -0
@@ -1,432 +0,0 @@
1
- import { describe, expect, it, vi } from "vitest";
2
-
3
- import {
4
- asChannelId,
5
- type ChannelAdapter,
6
- ChannelGateway,
7
- type ChannelMeta,
8
- ChannelRegistry,
9
- DuplicateChannelError,
10
- type InboundMessage,
11
- type OutboundMessage,
12
- replyTo,
13
- TenantMismatchError,
14
- UnknownChannelError,
15
- } from "./channel-gateway.js";
16
-
17
- /* -------------------------------------------------------------------------- */
18
- /* Fixtures */
19
- /* -------------------------------------------------------------------------- */
20
-
21
- function meta(id: string, over?: Partial<ChannelMeta>): ChannelMeta {
22
- return {
23
- id: asChannelId(id),
24
- displayName: `Channel ${id}`,
25
- capabilities: { threads: true, reactions: false, streaming: true },
26
- ...over,
27
- };
28
- }
29
-
30
- /**
31
- * Fake adapter: parses a minimal "envelope" payload into an InboundMessage.
32
- * Returns null for non-message events (joins, typing, …) and never throws on
33
- * junk. `sendOutbound` records every dispatched message.
34
- */
35
- function fakeAdapter(
36
- id: string,
37
- metaOver?: Partial<ChannelMeta>,
38
- ): ChannelAdapter & {
39
- sent: OutboundMessage[];
40
- } {
41
- const sent: OutboundMessage[] = [];
42
- return {
43
- sent,
44
- meta: meta(id, metaOver),
45
- parseInbound(tenantId, raw) {
46
- if (typeof raw !== "object" || raw === null) return null;
47
- const r = raw as Record<string, unknown>;
48
- // Non-message events → null (never throw).
49
- if (r["type"] !== "message") return null;
50
- if (typeof r["chatId"] !== "string" || typeof r["text"] !== "string") return null;
51
- const inbound: InboundMessage = {
52
- tenantId,
53
- channelId: asChannelId(id),
54
- chatId: r["chatId"],
55
- senderId: typeof r["senderId"] === "string" ? r["senderId"] : "anon",
56
- text: r["text"],
57
- receivedAt: "2026-05-17T00:00:00.000Z",
58
- raw,
59
- ...(typeof r["threadId"] === "string" ? { threadId: r["threadId"] } : {}),
60
- ...(typeof r["senderLabel"] === "string" ? { senderLabel: r["senderLabel"] } : {}),
61
- };
62
- return inbound;
63
- },
64
- async sendOutbound(msg) {
65
- sent.push(msg);
66
- return { messageId: `${id}-${sent.length}` };
67
- },
68
- };
69
- }
70
-
71
- /** Adapter whose parseInbound returns a structurally-invalid object. */
72
- function malformedAdapter(id: string): ChannelAdapter {
73
- return {
74
- meta: meta(id),
75
- // @ts-expect-error — intentionally returns a non-conforming shape to
76
- // exercise the gateway's zod boundary validation.
77
- parseInbound() {
78
- return { tenantId: "", chatId: 123, text: null };
79
- },
80
- async sendOutbound() {
81
- return { messageId: "x" };
82
- },
83
- };
84
- }
85
-
86
- /* -------------------------------------------------------------------------- */
87
- /* asChannelId / branded type */
88
- /* -------------------------------------------------------------------------- */
89
-
90
- describe("asChannelId", () => {
91
- it("brands a non-empty string", () => {
92
- expect(asChannelId("slack")).toBe("slack");
93
- });
94
-
95
- it("rejects empty / blank ids", () => {
96
- expect(() => asChannelId("")).toThrow(/channel/i);
97
- expect(() => asChannelId(" ")).toThrow(/channel/i);
98
- });
99
- });
100
-
101
- /* -------------------------------------------------------------------------- */
102
- /* ChannelAdapter.parseInbound */
103
- /* -------------------------------------------------------------------------- */
104
-
105
- describe("ChannelAdapter.parseInbound", () => {
106
- it("returns null for junk without throwing", () => {
107
- const a = fakeAdapter("c1");
108
- expect(a.parseInbound("org_a", null)).toBeNull();
109
- expect(a.parseInbound("org_a", "garbage")).toBeNull();
110
- expect(a.parseInbound("org_a", { type: "typing" })).toBeNull();
111
- expect(a.parseInbound("org_a", { type: "join", user: "u1" })).toBeNull();
112
- });
113
-
114
- it("stamps the tenantId onto the normalized result", () => {
115
- const a = fakeAdapter("c1");
116
- const msg = a.parseInbound("org_a", { type: "message", chatId: "ch1", text: "hi" });
117
- expect(msg).not.toBeNull();
118
- expect(msg?.tenantId).toBe("org_a");
119
- expect(msg?.channelId).toBe("c1");
120
- });
121
- });
122
-
123
- /* -------------------------------------------------------------------------- */
124
- /* ChannelRegistry */
125
- /* -------------------------------------------------------------------------- */
126
-
127
- describe("ChannelRegistry", () => {
128
- it("registers and looks up an adapter by channelId for a tenant", () => {
129
- const reg = new ChannelRegistry();
130
- const a = fakeAdapter("c1");
131
- reg.register("org_a", a);
132
- expect(reg.lookup("org_a", asChannelId("c1"))).toBe(a);
133
- });
134
-
135
- it("throws DuplicateChannelError on duplicate registration", () => {
136
- const reg = new ChannelRegistry();
137
- reg.register("org_a", fakeAdapter("c1"));
138
- expect(() => reg.register("org_a", fakeAdapter("c1"))).toThrow(DuplicateChannelError);
139
- });
140
-
141
- it("throws UnknownChannelError on unknown lookup", () => {
142
- const reg = new ChannelRegistry();
143
- expect(() => reg.lookup("org_a", asChannelId("nope"))).toThrow(UnknownChannelError);
144
- });
145
-
146
- it("fails closed on empty tenantId for register and lookup", () => {
147
- const reg = new ChannelRegistry();
148
- expect(() => reg.register("", fakeAdapter("c1"))).toThrow(/tenant/i);
149
- expect(() => reg.register(" ", fakeAdapter("c1"))).toThrow(/tenant/i);
150
- expect(() => reg.lookup("", asChannelId("c1"))).toThrow(/tenant/i);
151
- });
152
-
153
- it("isolates tenants — A's adapter is invisible to B (foreign tenant fails closed)", () => {
154
- const reg = new ChannelRegistry();
155
- reg.register("org_a", fakeAdapter("c1"));
156
- expect(() => reg.lookup("org_b", asChannelId("c1"))).toThrow(UnknownChannelError);
157
- // Same channelId may be registered independently per tenant.
158
- expect(() => reg.register("org_b", fakeAdapter("c1"))).not.toThrow();
159
- });
160
-
161
- it("lists registered channel ids per tenant", () => {
162
- const reg = new ChannelRegistry();
163
- reg.register("org_a", fakeAdapter("c1"));
164
- reg.register("org_a", fakeAdapter("c2"));
165
- reg.register("org_b", fakeAdapter("c3"));
166
- expect([...reg.list("org_a")].sort()).toEqual(["c1", "c2"]);
167
- expect(reg.list("org_b")).toEqual(["c3"]);
168
- expect(reg.list("org_unknown")).toEqual([]);
169
- });
170
- });
171
-
172
- /* -------------------------------------------------------------------------- */
173
- /* ChannelGateway.ingest */
174
- /* -------------------------------------------------------------------------- */
175
-
176
- describe("ChannelGateway.ingest", () => {
177
- it("normalizes a valid inbound message tenant-stamped", () => {
178
- const reg = new ChannelRegistry();
179
- reg.register("org_a", fakeAdapter("c1"));
180
- const gw = new ChannelGateway(reg);
181
-
182
- const msg = gw.ingest("org_a", asChannelId("c1"), {
183
- type: "message",
184
- chatId: "ch1",
185
- text: "hello",
186
- threadId: "t1",
187
- });
188
- expect(msg).not.toBeNull();
189
- expect(msg?.tenantId).toBe("org_a");
190
- expect(msg?.channelId).toBe("c1");
191
- expect(msg?.chatId).toBe("ch1");
192
- expect(msg?.threadId).toBe("t1");
193
- });
194
-
195
- it("swallows non-message events as null (never throws to caller)", () => {
196
- const reg = new ChannelRegistry();
197
- reg.register("org_a", fakeAdapter("c1"));
198
- const gw = new ChannelGateway(reg);
199
-
200
- expect(gw.ingest("org_a", asChannelId("c1"), { type: "typing" })).toBeNull();
201
- expect(gw.ingest("org_a", asChannelId("c1"), "junk")).toBeNull();
202
- expect(gw.ingest("org_a", asChannelId("c1"), null)).toBeNull();
203
- });
204
-
205
- it("rejects a malformed adapter result via zod (typed, no crash)", () => {
206
- const reg = new ChannelRegistry();
207
- reg.register("org_a", malformedAdapter("c1"));
208
- const gw = new ChannelGateway(reg);
209
-
210
- expect(() => gw.ingest("org_a", asChannelId("c1"), { type: "message" })).toThrow();
211
- // Does not surface as a raw TypeError — it is a validation error.
212
- try {
213
- gw.ingest("org_a", asChannelId("c1"), { type: "message" });
214
- } catch (e) {
215
- expect(e).toBeInstanceOf(Error);
216
- expect((e as Error).message).toMatch(/inbound|valid/i);
217
- }
218
- });
219
-
220
- it("throws UnknownChannelError when the channel is not registered", () => {
221
- const gw = new ChannelGateway(new ChannelRegistry());
222
- expect(() => gw.ingest("org_a", asChannelId("ghost"), { type: "message" })).toThrow(
223
- UnknownChannelError,
224
- );
225
- });
226
-
227
- it("fails closed on empty / foreign tenant", () => {
228
- const reg = new ChannelRegistry();
229
- reg.register("org_a", fakeAdapter("c1"));
230
- const gw = new ChannelGateway(reg);
231
- expect(() => gw.ingest("", asChannelId("c1"), { type: "message" })).toThrow(/tenant/i);
232
- expect(() => gw.ingest("org_b", asChannelId("c1"), { type: "message" })).toThrow(
233
- UnknownChannelError,
234
- );
235
- });
236
- });
237
-
238
- /* -------------------------------------------------------------------------- */
239
- /* ChannelGateway.route */
240
- /* -------------------------------------------------------------------------- */
241
-
242
- describe("ChannelGateway.route", () => {
243
- it("sends a reply back through the originating channel", async () => {
244
- const reg = new ChannelRegistry();
245
- const a = fakeAdapter("c1");
246
- const b = fakeAdapter("c2");
247
- reg.register("org_a", a);
248
- reg.register("org_a", b);
249
- const gw = new ChannelGateway(reg);
250
-
251
- const res = await gw.route({
252
- tenantId: "org_a",
253
- channelId: asChannelId("c2"),
254
- chatId: "ch9",
255
- text: "pong",
256
- });
257
-
258
- expect(res.messageId).toBe("c2-1");
259
- expect(b.sent).toHaveLength(1);
260
- expect(a.sent).toHaveLength(0);
261
- expect(b.sent[0]).toMatchObject({ chatId: "ch9", text: "pong" });
262
- });
263
-
264
- it("throws UnknownChannelError when routing to an unknown channel", async () => {
265
- const gw = new ChannelGateway(new ChannelRegistry());
266
- await expect(
267
- gw.route({ tenantId: "org_a", channelId: asChannelId("ghost"), chatId: "c", text: "x" }),
268
- ).rejects.toThrow(UnknownChannelError);
269
- });
270
-
271
- it("fails closed on empty tenantId for route", async () => {
272
- const reg = new ChannelRegistry();
273
- reg.register("org_a", fakeAdapter("c1"));
274
- const gw = new ChannelGateway(reg);
275
- await expect(
276
- gw.route({ tenantId: "", channelId: asChannelId("c1"), chatId: "c", text: "x" }),
277
- ).rejects.toThrow(/tenant/i);
278
- });
279
-
280
- it("rejects a foreign tenant routing through another tenant's channel", async () => {
281
- const reg = new ChannelRegistry();
282
- reg.register("org_a", fakeAdapter("c1"));
283
- const gw = new ChannelGateway(reg);
284
- await expect(
285
- gw.route({ tenantId: "org_b", channelId: asChannelId("c1"), chatId: "c", text: "x" }),
286
- ).rejects.toThrow(UnknownChannelError);
287
- });
288
-
289
- it("rejects a tenant mismatch between reply and resolved adapter scope", async () => {
290
- const reg = new ChannelRegistry();
291
- reg.register("org_a", fakeAdapter("c1"));
292
- const gw = new ChannelGateway(reg);
293
- // Whitespace tenant — fail closed before any send.
294
- await expect(
295
- gw.route({ tenantId: " ", channelId: asChannelId("c1"), chatId: "c", text: "x" }),
296
- ).rejects.toThrow(/tenant/i);
297
- });
298
-
299
- it("rejects a structurally-invalid OutboundMessage via zod", async () => {
300
- const reg = new ChannelRegistry();
301
- const a = fakeAdapter("c1");
302
- reg.register("org_a", a);
303
- const gw = new ChannelGateway(reg);
304
- await expect(
305
- // @ts-expect-error — text missing on purpose to hit the zod boundary.
306
- gw.route({ tenantId: "org_a", channelId: asChannelId("c1"), chatId: "c" }),
307
- ).rejects.toThrow();
308
- expect(a.sent).toHaveLength(0);
309
- });
310
- });
311
-
312
- /* -------------------------------------------------------------------------- */
313
- /* replyTo (pure helper) */
314
- /* -------------------------------------------------------------------------- */
315
-
316
- describe("replyTo", () => {
317
- const inbound: InboundMessage = {
318
- tenantId: "org_a",
319
- channelId: asChannelId("c1"),
320
- chatId: "ch1",
321
- threadId: "t1",
322
- senderId: "u1",
323
- text: "ping",
324
- receivedAt: "2026-05-17T00:00:00.000Z",
325
- raw: { id: "m1" },
326
- };
327
-
328
- it("derives a correctly-addressed OutboundMessage on the same channel/chat/thread", () => {
329
- const out = replyTo(inbound, "pong");
330
- expect(out).toEqual({
331
- tenantId: "org_a",
332
- channelId: asChannelId("c1"),
333
- chatId: "ch1",
334
- threadId: "t1",
335
- text: "pong",
336
- });
337
- });
338
-
339
- it("wires replyToMessageId when provided", () => {
340
- const out = replyTo(inbound, "pong", { replyToMessageId: "m1" });
341
- expect(out.replyToMessageId).toBe("m1");
342
- });
343
-
344
- it("does not mutate the inbound message", () => {
345
- const snapshot = JSON.parse(JSON.stringify(inbound));
346
- replyTo(inbound, "pong", { replyToMessageId: "m1" });
347
- expect(inbound).toEqual(snapshot);
348
- });
349
-
350
- it("omits threadId when the inbound has none (exactOptional-safe)", () => {
351
- const noThread: InboundMessage = {
352
- tenantId: "org_a",
353
- channelId: asChannelId("c1"),
354
- chatId: "ch1",
355
- senderId: "u1",
356
- text: "ping",
357
- receivedAt: "2026-05-17T00:00:00.000Z",
358
- raw: {},
359
- };
360
- const out = replyTo(noThread, "pong");
361
- expect("threadId" in out).toBe(false);
362
- });
363
-
364
- it("fails closed when the inbound carries an empty tenantId", () => {
365
- const bad: InboundMessage = { ...inbound, tenantId: "" };
366
- expect(() => replyTo(bad, "pong")).toThrow(/tenant/i);
367
- });
368
-
369
- it("a replyTo result routes back through the same channel end-to-end", async () => {
370
- const reg = new ChannelRegistry();
371
- const a = fakeAdapter("c1");
372
- reg.register("org_a", a);
373
- const gw = new ChannelGateway(reg);
374
-
375
- const out = replyTo(inbound, "pong", { replyToMessageId: "m1" });
376
- const res = await gw.route(out);
377
- expect(res.messageId).toBe("c1-1");
378
- expect(a.sent[0]).toMatchObject({
379
- chatId: "ch1",
380
- threadId: "t1",
381
- text: "pong",
382
- replyToMessageId: "m1",
383
- });
384
- });
385
- });
386
-
387
- /* -------------------------------------------------------------------------- */
388
- /* Capabilities surfaced from meta */
389
- /* -------------------------------------------------------------------------- */
390
-
391
- describe("ChannelGateway capabilities", () => {
392
- it("surfaces channel capabilities from the adapter meta", () => {
393
- const reg = new ChannelRegistry();
394
- reg.register(
395
- "org_a",
396
- fakeAdapter("c1", { capabilities: { threads: false, reactions: true, streaming: false } }),
397
- );
398
- const gw = new ChannelGateway(reg);
399
- const caps = gw.capabilities("org_a", asChannelId("c1"));
400
- expect(caps).toEqual({ threads: false, reactions: true, streaming: false });
401
- });
402
-
403
- it("describe() returns the full meta for a registered channel", () => {
404
- const reg = new ChannelRegistry();
405
- reg.register("org_a", fakeAdapter("c1"));
406
- const gw = new ChannelGateway(reg);
407
- const m = gw.describe("org_a", asChannelId("c1"));
408
- expect(m.id).toBe("c1");
409
- expect(m.displayName).toBe("Channel c1");
410
- expect(m.capabilities).toEqual({ threads: true, reactions: false, streaming: true });
411
- });
412
-
413
- it("capabilities() throws UnknownChannelError for an unregistered channel", () => {
414
- const gw = new ChannelGateway(new ChannelRegistry());
415
- expect(() => gw.capabilities("org_a", asChannelId("ghost"))).toThrow(UnknownChannelError);
416
- });
417
-
418
- it("TenantMismatchError is exported and distinct from UnknownChannelError", () => {
419
- expect(new TenantMismatchError("x").name).toBe("TenantMismatchError");
420
- expect(new TenantMismatchError("x")).not.toBeInstanceOf(UnknownChannelError);
421
- });
422
-
423
- it("does not log to console during normal operation", () => {
424
- const spy = vi.spyOn(console, "log");
425
- const reg = new ChannelRegistry();
426
- reg.register("org_a", fakeAdapter("c1"));
427
- const gw = new ChannelGateway(reg);
428
- gw.ingest("org_a", asChannelId("c1"), { type: "message", chatId: "c", text: "hi" });
429
- expect(spy).not.toHaveBeenCalled();
430
- spy.mockRestore();
431
- });
432
- });