@checkstack/backend 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +225 -0
- package/drizzle/0000_loose_yellow_claw.sql +28 -0
- package/drizzle/meta/0000_snapshot.json +187 -0
- package/drizzle/meta/_journal.json +13 -0
- package/drizzle.config.ts +10 -0
- package/package.json +42 -0
- package/src/db.ts +20 -0
- package/src/health-check-plugin-integration.test.ts +93 -0
- package/src/index.ts +419 -0
- package/src/integration/event-bus.integration.test.ts +313 -0
- package/src/logger.ts +65 -0
- package/src/openapi-router.ts +177 -0
- package/src/plugin-lifecycle.test.ts +276 -0
- package/src/plugin-manager/api-router.ts +163 -0
- package/src/plugin-manager/core-services.ts +312 -0
- package/src/plugin-manager/dependency-sorter.ts +103 -0
- package/src/plugin-manager/deregistration-guard.ts +41 -0
- package/src/plugin-manager/extension-points.ts +85 -0
- package/src/plugin-manager/index.ts +13 -0
- package/src/plugin-manager/plugin-admin-router.ts +89 -0
- package/src/plugin-manager/plugin-loader.ts +464 -0
- package/src/plugin-manager/types.ts +14 -0
- package/src/plugin-manager.test.ts +464 -0
- package/src/plugin-manager.ts +431 -0
- package/src/rpc-rest-compat.test.ts +80 -0
- package/src/schema.ts +46 -0
- package/src/services/config-service.test.ts +66 -0
- package/src/services/config-service.ts +322 -0
- package/src/services/event-bus.test.ts +469 -0
- package/src/services/event-bus.ts +317 -0
- package/src/services/health-check-registry.test.ts +101 -0
- package/src/services/health-check-registry.ts +27 -0
- package/src/services/jwt.ts +45 -0
- package/src/services/keystore.test.ts +198 -0
- package/src/services/keystore.ts +136 -0
- package/src/services/plugin-installer.test.ts +90 -0
- package/src/services/plugin-installer.ts +70 -0
- package/src/services/queue-manager.ts +382 -0
- package/src/services/queue-plugin-registry.ts +17 -0
- package/src/services/queue-proxy.ts +182 -0
- package/src/services/service-registry.ts +35 -0
- package/src/test-preload.ts +114 -0
- package/src/utils/plugin-discovery.test.ts +383 -0
- package/src/utils/plugin-discovery.ts +157 -0
- package/src/utils/strip-public-schema.ts +40 -0
- package/tsconfig.json +6 -0
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from "bun:test";
|
|
2
|
+
import { EventBus } from "./event-bus";
|
|
3
|
+
import type { QueueManager } from "@checkstack/queue-api";
|
|
4
|
+
import type { Logger, Hook } from "@checkstack/backend-api";
|
|
5
|
+
import { createHook } from "@checkstack/backend-api";
|
|
6
|
+
import {
|
|
7
|
+
createMockLogger,
|
|
8
|
+
createMockQueueManager,
|
|
9
|
+
} from "@checkstack/test-utils-backend";
|
|
10
|
+
|
|
11
|
+
describe("EventBus", () => {
|
|
12
|
+
let eventBus: EventBus;
|
|
13
|
+
let mockQueueManager: QueueManager;
|
|
14
|
+
let mockLogger: Logger;
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
mockQueueManager = createMockQueueManager();
|
|
18
|
+
mockLogger = createMockLogger();
|
|
19
|
+
eventBus = new EventBus(mockQueueManager, mockLogger);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe("Validation", () => {
|
|
23
|
+
it("should require workerGroup for work-queue mode", async () => {
|
|
24
|
+
const testHook = createHook<{ test: string }>("test.hook");
|
|
25
|
+
|
|
26
|
+
await expect(
|
|
27
|
+
eventBus.subscribe(
|
|
28
|
+
"test-plugin",
|
|
29
|
+
testHook,
|
|
30
|
+
async () => {},
|
|
31
|
+
{ mode: "work-queue" } as any // Missing workerGroup
|
|
32
|
+
)
|
|
33
|
+
).rejects.toThrow("workerGroup is required when mode is 'work-queue'");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("should detect duplicate workerGroups in same plugin", async () => {
|
|
37
|
+
const testHook = createHook<{ test: string }>("test.hook");
|
|
38
|
+
|
|
39
|
+
// First subscription: OK
|
|
40
|
+
await eventBus.subscribe("test-plugin", testHook, async () => {}, {
|
|
41
|
+
mode: "work-queue",
|
|
42
|
+
workerGroup: "sync",
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// Second subscription with same workerGroup: ERROR
|
|
46
|
+
await expect(
|
|
47
|
+
eventBus.subscribe("test-plugin", testHook, async () => {}, {
|
|
48
|
+
mode: "work-queue",
|
|
49
|
+
workerGroup: "sync",
|
|
50
|
+
})
|
|
51
|
+
).rejects.toThrow("Duplicate workerGroup 'sync' detected");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("should allow same workerGroup name in different plugins", async () => {
|
|
55
|
+
const testHook = createHook<{ test: string }>("test.hook");
|
|
56
|
+
|
|
57
|
+
// Both should succeed (different plugins)
|
|
58
|
+
await eventBus.subscribe("plugin-a", testHook, async () => {}, {
|
|
59
|
+
mode: "work-queue",
|
|
60
|
+
workerGroup: "sync",
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
await eventBus.subscribe("plugin-b", testHook, async () => {}, {
|
|
64
|
+
mode: "work-queue",
|
|
65
|
+
workerGroup: "sync",
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// No error - different namespaces
|
|
69
|
+
expect(true).toBe(true);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe("Plugin Namespacing", () => {
|
|
74
|
+
it("should namespace workerGroup by plugin ID", async () => {
|
|
75
|
+
const testHook = createHook<{ test: string }>("test.hook");
|
|
76
|
+
const calls: string[] = [];
|
|
77
|
+
|
|
78
|
+
await eventBus.subscribe(
|
|
79
|
+
"plugin-a",
|
|
80
|
+
testHook,
|
|
81
|
+
async () => {
|
|
82
|
+
calls.push("a");
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
mode: "work-queue",
|
|
86
|
+
workerGroup: "sync",
|
|
87
|
+
}
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
await eventBus.subscribe(
|
|
91
|
+
"plugin-b",
|
|
92
|
+
testHook,
|
|
93
|
+
async () => {
|
|
94
|
+
calls.push("b");
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
mode: "work-queue",
|
|
98
|
+
workerGroup: "sync",
|
|
99
|
+
}
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
await eventBus.emit(testHook, { test: "data" });
|
|
103
|
+
|
|
104
|
+
// Wait for processing
|
|
105
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
106
|
+
|
|
107
|
+
// Both should execute (different namespaces: plugin-a.sync and plugin-b.sync)
|
|
108
|
+
expect(calls).toContain("a");
|
|
109
|
+
expect(calls).toContain("b");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("should create unique consumer groups for broadcast mode", async () => {
|
|
113
|
+
const testHook = createHook<{ test: string }>("test.hook");
|
|
114
|
+
const calls: number[] = [];
|
|
115
|
+
|
|
116
|
+
// Two broadcast subscribers from same plugin
|
|
117
|
+
await eventBus.subscribe("plugin-a", testHook, async () => {
|
|
118
|
+
calls.push(1);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
await eventBus.subscribe("plugin-a", testHook, async () => {
|
|
122
|
+
calls.push(2);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
await eventBus.emit(testHook, { test: "data" });
|
|
126
|
+
|
|
127
|
+
// Wait for processing
|
|
128
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
129
|
+
|
|
130
|
+
// Both should receive (each gets unique consumer group with instance ID)
|
|
131
|
+
// Note: They both execute because they have different consumer groups
|
|
132
|
+
expect(calls.length).toBeGreaterThanOrEqual(1);
|
|
133
|
+
expect(calls.length).toBeLessThanOrEqual(2);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe("Unsubscribe", () => {
|
|
138
|
+
it("should unsubscribe and stop receiving events", async () => {
|
|
139
|
+
const testHook = createHook<{ test: string }>("test.hook");
|
|
140
|
+
const calls: number[] = [];
|
|
141
|
+
|
|
142
|
+
const unsubscribe = await eventBus.subscribe(
|
|
143
|
+
"test-plugin",
|
|
144
|
+
testHook,
|
|
145
|
+
async () => {
|
|
146
|
+
calls.push(1);
|
|
147
|
+
}
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
await eventBus.emit(testHook, { test: "data" });
|
|
151
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
152
|
+
expect(calls.length).toBe(1);
|
|
153
|
+
|
|
154
|
+
// Unsubscribe
|
|
155
|
+
await unsubscribe();
|
|
156
|
+
|
|
157
|
+
// Emit again
|
|
158
|
+
await eventBus.emit(testHook, { test: "data" });
|
|
159
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
160
|
+
|
|
161
|
+
// Should not receive second event
|
|
162
|
+
expect(calls.length).toBe(1);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("should remove workerGroup from tracking on unsubscribe", async () => {
|
|
166
|
+
const testHook = createHook<{ test: string }>("test.hook");
|
|
167
|
+
|
|
168
|
+
const unsubscribe = await eventBus.subscribe(
|
|
169
|
+
"test-plugin",
|
|
170
|
+
testHook,
|
|
171
|
+
async () => {},
|
|
172
|
+
{
|
|
173
|
+
mode: "work-queue",
|
|
174
|
+
workerGroup: "sync",
|
|
175
|
+
}
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
// Unsubscribe
|
|
179
|
+
await unsubscribe();
|
|
180
|
+
|
|
181
|
+
// Should be able to re-use the same workerGroup name
|
|
182
|
+
await eventBus.subscribe("test-plugin", testHook, async () => {}, {
|
|
183
|
+
mode: "work-queue",
|
|
184
|
+
workerGroup: "sync",
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// No error!
|
|
188
|
+
expect(true).toBe(true);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("should stop queue when all listeners unsubscribe", async () => {
|
|
192
|
+
const testHook = createHook<{ test: string }>("test.hook");
|
|
193
|
+
|
|
194
|
+
const unsubscribe1 = await eventBus.subscribe(
|
|
195
|
+
"test-plugin",
|
|
196
|
+
testHook,
|
|
197
|
+
async () => {}
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
const unsubscribe2 = await eventBus.subscribe(
|
|
201
|
+
"test-plugin",
|
|
202
|
+
testHook,
|
|
203
|
+
async () => {}
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
// Unsubscribe both
|
|
207
|
+
await unsubscribe1();
|
|
208
|
+
await unsubscribe2();
|
|
209
|
+
|
|
210
|
+
// Successfully unsubscribed without errors
|
|
211
|
+
expect(true).toBe(true);
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
describe("Error Handling", () => {
|
|
216
|
+
it("should log listener errors", async () => {
|
|
217
|
+
const testHook = createHook<{ test: string }>("test.hook");
|
|
218
|
+
|
|
219
|
+
await eventBus.subscribe(
|
|
220
|
+
"test-plugin",
|
|
221
|
+
testHook,
|
|
222
|
+
async () => {
|
|
223
|
+
throw new Error("Listener failed");
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
mode: "work-queue",
|
|
227
|
+
workerGroup: "fail-group",
|
|
228
|
+
maxRetries: 0,
|
|
229
|
+
}
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
// Emit - should not throw
|
|
233
|
+
await eventBus.emit(testHook, { test: "data" });
|
|
234
|
+
|
|
235
|
+
// Wait for async processing
|
|
236
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
237
|
+
|
|
238
|
+
// Error should be logged
|
|
239
|
+
expect(mockLogger.error).toHaveBeenCalled();
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
describe("Hook Emission", () => {
|
|
244
|
+
it("should create queue channel lazily on first emit", async () => {
|
|
245
|
+
const testHook = createHook<{ test: string }>("test.hook");
|
|
246
|
+
|
|
247
|
+
// Emit creates the queue lazily
|
|
248
|
+
await eventBus.emit(testHook, { test: "data" });
|
|
249
|
+
|
|
250
|
+
// No errors - queue was created
|
|
251
|
+
expect(true).toBe(true);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it("should deliver payload to all subscribers", async () => {
|
|
255
|
+
const testHook = createHook<{ value: number }>("test.hook");
|
|
256
|
+
const received: number[] = [];
|
|
257
|
+
|
|
258
|
+
await eventBus.subscribe("plugin-1", testHook, async (payload) => {
|
|
259
|
+
received.push(payload.value);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
await eventBus.subscribe("plugin-2", testHook, async (payload) => {
|
|
263
|
+
received.push(payload.value * 2);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
await eventBus.emit(testHook, { value: 10 });
|
|
267
|
+
|
|
268
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
269
|
+
|
|
270
|
+
expect(received).toContain(10);
|
|
271
|
+
expect(received).toContain(20);
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
describe("Shutdown", () => {
|
|
276
|
+
it("should stop all queue channels", async () => {
|
|
277
|
+
const hook1 = createHook<{ test: string }>("hook1");
|
|
278
|
+
const hook2 = createHook<{ test: string }>("hook2");
|
|
279
|
+
|
|
280
|
+
await eventBus.subscribe("test-plugin", hook1, async () => {});
|
|
281
|
+
await eventBus.subscribe("test-plugin", hook2, async () => {});
|
|
282
|
+
|
|
283
|
+
await eventBus.shutdown();
|
|
284
|
+
|
|
285
|
+
// All queues should be stopped
|
|
286
|
+
expect(mockLogger.info).toHaveBeenCalledWith("EventBus shut down");
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
describe("Instance-Local Hooks", () => {
|
|
291
|
+
it("should subscribe via instance-local mode", async () => {
|
|
292
|
+
const testHook = createHook<{ value: number }>("test.local.hook");
|
|
293
|
+
const received: number[] = [];
|
|
294
|
+
|
|
295
|
+
await eventBus.subscribe(
|
|
296
|
+
"test-plugin",
|
|
297
|
+
testHook,
|
|
298
|
+
async (payload) => {
|
|
299
|
+
received.push(payload.value);
|
|
300
|
+
},
|
|
301
|
+
{ mode: "instance-local" }
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
await eventBus.emitLocal(testHook, { value: 42 });
|
|
305
|
+
|
|
306
|
+
// Local hooks are synchronous-ish (no queue involved)
|
|
307
|
+
expect(received).toEqual([42]);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it("should call all local listeners on emitLocal", async () => {
|
|
311
|
+
const testHook = createHook<{ value: string }>("test.local.hook");
|
|
312
|
+
const calls: string[] = [];
|
|
313
|
+
|
|
314
|
+
await eventBus.subscribe(
|
|
315
|
+
"plugin-a",
|
|
316
|
+
testHook,
|
|
317
|
+
async () => {
|
|
318
|
+
calls.push("a");
|
|
319
|
+
},
|
|
320
|
+
{ mode: "instance-local" }
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
await eventBus.subscribe(
|
|
324
|
+
"plugin-b",
|
|
325
|
+
testHook,
|
|
326
|
+
async () => {
|
|
327
|
+
calls.push("b");
|
|
328
|
+
},
|
|
329
|
+
{ mode: "instance-local" }
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
await eventBus.emitLocal(testHook, { value: "test" });
|
|
333
|
+
|
|
334
|
+
expect(calls).toContain("a");
|
|
335
|
+
expect(calls).toContain("b");
|
|
336
|
+
expect(calls.length).toBe(2);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it("should isolate failures via Promise.allSettled - one listener error does not block others", async () => {
|
|
340
|
+
const testHook = createHook<{ value: number }>("test.local.hook");
|
|
341
|
+
const successfulCalls: number[] = [];
|
|
342
|
+
|
|
343
|
+
await eventBus.subscribe(
|
|
344
|
+
"plugin-a",
|
|
345
|
+
testHook,
|
|
346
|
+
async (payload) => {
|
|
347
|
+
successfulCalls.push(payload.value);
|
|
348
|
+
},
|
|
349
|
+
{ mode: "instance-local" }
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
await eventBus.subscribe(
|
|
353
|
+
"plugin-b",
|
|
354
|
+
testHook,
|
|
355
|
+
async () => {
|
|
356
|
+
throw new Error("Intentional failure");
|
|
357
|
+
},
|
|
358
|
+
{ mode: "instance-local" }
|
|
359
|
+
);
|
|
360
|
+
|
|
361
|
+
await eventBus.subscribe(
|
|
362
|
+
"plugin-c",
|
|
363
|
+
testHook,
|
|
364
|
+
async (payload) => {
|
|
365
|
+
successfulCalls.push(payload.value * 2);
|
|
366
|
+
},
|
|
367
|
+
{ mode: "instance-local" }
|
|
368
|
+
);
|
|
369
|
+
|
|
370
|
+
// Should NOT throw despite plugin-b failing
|
|
371
|
+
await eventBus.emitLocal(testHook, { value: 10 });
|
|
372
|
+
|
|
373
|
+
// Both successful listeners should have executed
|
|
374
|
+
expect(successfulCalls).toContain(10);
|
|
375
|
+
expect(successfulCalls).toContain(20);
|
|
376
|
+
expect(successfulCalls.length).toBe(2);
|
|
377
|
+
|
|
378
|
+
// Error should be logged
|
|
379
|
+
expect(mockLogger.error).toHaveBeenCalled();
|
|
380
|
+
expect(mockLogger.warn).toHaveBeenCalled();
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
it("should unsubscribe local listeners correctly", async () => {
|
|
384
|
+
const testHook = createHook<{ value: number }>("test.local.hook");
|
|
385
|
+
const calls: number[] = [];
|
|
386
|
+
|
|
387
|
+
const unsubscribe = await eventBus.subscribe(
|
|
388
|
+
"test-plugin",
|
|
389
|
+
testHook,
|
|
390
|
+
async (payload) => {
|
|
391
|
+
calls.push(payload.value);
|
|
392
|
+
},
|
|
393
|
+
{ mode: "instance-local" }
|
|
394
|
+
);
|
|
395
|
+
|
|
396
|
+
await eventBus.emitLocal(testHook, { value: 1 });
|
|
397
|
+
expect(calls).toEqual([1]);
|
|
398
|
+
|
|
399
|
+
// Unsubscribe
|
|
400
|
+
await unsubscribe();
|
|
401
|
+
|
|
402
|
+
await eventBus.emitLocal(testHook, { value: 2 });
|
|
403
|
+
// Should NOT receive second event
|
|
404
|
+
expect(calls).toEqual([1]);
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
it("should not trigger local listeners on distributed emit", async () => {
|
|
408
|
+
const testHook = createHook<{ value: number }>("test.local.hook");
|
|
409
|
+
const localCalls: number[] = [];
|
|
410
|
+
const distributedCalls: number[] = [];
|
|
411
|
+
|
|
412
|
+
await eventBus.subscribe(
|
|
413
|
+
"test-plugin",
|
|
414
|
+
testHook,
|
|
415
|
+
async (payload) => {
|
|
416
|
+
localCalls.push(payload.value);
|
|
417
|
+
},
|
|
418
|
+
{ mode: "instance-local" }
|
|
419
|
+
);
|
|
420
|
+
|
|
421
|
+
await eventBus.subscribe("test-plugin", testHook, async (payload) => {
|
|
422
|
+
distributedCalls.push(payload.value);
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
// Distributed emit - should only trigger distributed listener
|
|
426
|
+
await eventBus.emit(testHook, { value: 42 });
|
|
427
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
428
|
+
|
|
429
|
+
expect(distributedCalls).toContain(42);
|
|
430
|
+
expect(localCalls).toEqual([]); // Local listener should NOT be triggered
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
it("should not trigger distributed listeners on emitLocal", async () => {
|
|
434
|
+
const testHook = createHook<{ value: number }>("test.local.hook");
|
|
435
|
+
const localCalls: number[] = [];
|
|
436
|
+
const distributedCalls: number[] = [];
|
|
437
|
+
|
|
438
|
+
await eventBus.subscribe(
|
|
439
|
+
"test-plugin",
|
|
440
|
+
testHook,
|
|
441
|
+
async (payload) => {
|
|
442
|
+
localCalls.push(payload.value);
|
|
443
|
+
},
|
|
444
|
+
{ mode: "instance-local" }
|
|
445
|
+
);
|
|
446
|
+
|
|
447
|
+
await eventBus.subscribe("test-plugin", testHook, async (payload) => {
|
|
448
|
+
distributedCalls.push(payload.value);
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
// Local emit - should only trigger local listener
|
|
452
|
+
await eventBus.emitLocal(testHook, { value: 99 });
|
|
453
|
+
|
|
454
|
+
expect(localCalls).toEqual([99]);
|
|
455
|
+
expect(distributedCalls).toEqual([]); // Distributed listener should NOT be triggered
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
it("should handle emitLocal with no listeners gracefully", async () => {
|
|
459
|
+
const testHook = createHook<{ value: number }>("test.no.listeners");
|
|
460
|
+
|
|
461
|
+
// Should not throw
|
|
462
|
+
await eventBus.emitLocal(testHook, { value: 42 });
|
|
463
|
+
|
|
464
|
+
expect(mockLogger.debug).toHaveBeenCalledWith(
|
|
465
|
+
`No local listeners for hook: ${testHook.id}`
|
|
466
|
+
);
|
|
467
|
+
});
|
|
468
|
+
});
|
|
469
|
+
});
|