@botcord/daemon 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. package/dist/activity-tracker.d.ts +43 -0
  2. package/dist/activity-tracker.js +110 -0
  3. package/dist/adapters/runtimes.d.ts +14 -0
  4. package/dist/adapters/runtimes.js +18 -0
  5. package/dist/agent-discovery.d.ts +81 -0
  6. package/dist/agent-discovery.js +181 -0
  7. package/dist/agent-workspace.d.ts +31 -0
  8. package/dist/agent-workspace.js +221 -0
  9. package/dist/config.d.ts +116 -0
  10. package/dist/config.js +180 -0
  11. package/dist/control-channel.d.ts +99 -0
  12. package/dist/control-channel.js +388 -0
  13. package/dist/cross-room.d.ts +23 -0
  14. package/dist/cross-room.js +55 -0
  15. package/dist/daemon-config-map.d.ts +61 -0
  16. package/dist/daemon-config-map.js +153 -0
  17. package/dist/daemon.d.ts +123 -0
  18. package/dist/daemon.js +349 -0
  19. package/dist/doctor.d.ts +89 -0
  20. package/dist/doctor.js +191 -0
  21. package/dist/gateway/channel-manager.d.ts +54 -0
  22. package/dist/gateway/channel-manager.js +292 -0
  23. package/dist/gateway/channels/botcord.d.ts +93 -0
  24. package/dist/gateway/channels/botcord.js +510 -0
  25. package/dist/gateway/channels/index.d.ts +2 -0
  26. package/dist/gateway/channels/index.js +1 -0
  27. package/dist/gateway/channels/sanitize.d.ts +20 -0
  28. package/dist/gateway/channels/sanitize.js +56 -0
  29. package/dist/gateway/dispatcher.d.ts +73 -0
  30. package/dist/gateway/dispatcher.js +431 -0
  31. package/dist/gateway/gateway.d.ts +87 -0
  32. package/dist/gateway/gateway.js +158 -0
  33. package/dist/gateway/index.d.ts +15 -0
  34. package/dist/gateway/index.js +15 -0
  35. package/dist/gateway/log.d.ts +9 -0
  36. package/dist/gateway/log.js +20 -0
  37. package/dist/gateway/router.d.ts +10 -0
  38. package/dist/gateway/router.js +48 -0
  39. package/dist/gateway/runtimes/claude-code.d.ts +30 -0
  40. package/dist/gateway/runtimes/claude-code.js +162 -0
  41. package/dist/gateway/runtimes/codex.d.ts +83 -0
  42. package/dist/gateway/runtimes/codex.js +272 -0
  43. package/dist/gateway/runtimes/gemini.d.ts +15 -0
  44. package/dist/gateway/runtimes/gemini.js +29 -0
  45. package/dist/gateway/runtimes/ndjson-stream.d.ts +43 -0
  46. package/dist/gateway/runtimes/ndjson-stream.js +169 -0
  47. package/dist/gateway/runtimes/probe.d.ts +17 -0
  48. package/dist/gateway/runtimes/probe.js +54 -0
  49. package/dist/gateway/runtimes/registry.d.ts +59 -0
  50. package/dist/gateway/runtimes/registry.js +94 -0
  51. package/dist/gateway/session-store.d.ts +39 -0
  52. package/dist/gateway/session-store.js +133 -0
  53. package/dist/gateway/types.d.ts +265 -0
  54. package/dist/gateway/types.js +1 -0
  55. package/dist/index.d.ts +2 -0
  56. package/dist/index.js +854 -0
  57. package/dist/log.d.ts +7 -0
  58. package/dist/log.js +44 -0
  59. package/dist/provision.d.ts +88 -0
  60. package/dist/provision.js +749 -0
  61. package/dist/room-context-fetcher.d.ts +18 -0
  62. package/dist/room-context-fetcher.js +101 -0
  63. package/dist/room-context.d.ts +53 -0
  64. package/dist/room-context.js +112 -0
  65. package/dist/sender-classify.d.ts +30 -0
  66. package/dist/sender-classify.js +32 -0
  67. package/dist/snapshot-writer.d.ts +37 -0
  68. package/dist/snapshot-writer.js +84 -0
  69. package/dist/status-render.d.ts +28 -0
  70. package/dist/status-render.js +97 -0
  71. package/dist/system-context.d.ts +57 -0
  72. package/dist/system-context.js +91 -0
  73. package/dist/turn-text.d.ts +36 -0
  74. package/dist/turn-text.js +57 -0
  75. package/dist/user-auth.d.ts +75 -0
  76. package/dist/user-auth.js +245 -0
  77. package/dist/working-memory.d.ts +46 -0
  78. package/dist/working-memory.js +274 -0
  79. package/package.json +39 -0
  80. package/src/__tests__/activity-tracker.test.ts +130 -0
  81. package/src/__tests__/agent-discovery.test.ts +191 -0
  82. package/src/__tests__/agent-workspace.test.ts +147 -0
  83. package/src/__tests__/control-channel.test.ts +327 -0
  84. package/src/__tests__/cross-room.test.ts +116 -0
  85. package/src/__tests__/daemon-config-map.test.ts +416 -0
  86. package/src/__tests__/daemon.test.ts +300 -0
  87. package/src/__tests__/device-code.test.ts +152 -0
  88. package/src/__tests__/doctor.test.ts +218 -0
  89. package/src/__tests__/protocol-core-reexport.test.ts +24 -0
  90. package/src/__tests__/provision.test.ts +922 -0
  91. package/src/__tests__/room-context.test.ts +233 -0
  92. package/src/__tests__/runtime-discovery.test.ts +173 -0
  93. package/src/__tests__/snapshot-writer.test.ts +141 -0
  94. package/src/__tests__/status-render.test.ts +137 -0
  95. package/src/__tests__/system-context.test.ts +315 -0
  96. package/src/__tests__/turn-text.test.ts +116 -0
  97. package/src/__tests__/user-auth.test.ts +125 -0
  98. package/src/__tests__/working-memory.test.ts +240 -0
  99. package/src/activity-tracker.ts +140 -0
  100. package/src/adapters/runtimes.ts +30 -0
  101. package/src/agent-discovery.ts +262 -0
  102. package/src/agent-workspace.ts +247 -0
  103. package/src/config.ts +290 -0
  104. package/src/control-channel.ts +455 -0
  105. package/src/cross-room.ts +89 -0
  106. package/src/daemon-config-map.ts +200 -0
  107. package/src/daemon.ts +478 -0
  108. package/src/doctor.ts +282 -0
  109. package/src/gateway/__tests__/.gitkeep +0 -0
  110. package/src/gateway/__tests__/botcord-channel.test.ts +480 -0
  111. package/src/gateway/__tests__/channel-manager.test.ts +475 -0
  112. package/src/gateway/__tests__/claude-code-adapter.test.ts +318 -0
  113. package/src/gateway/__tests__/codex-adapter.test.ts +350 -0
  114. package/src/gateway/__tests__/dispatcher.test.ts +1159 -0
  115. package/src/gateway/__tests__/gateway-add-channel.test.ts +180 -0
  116. package/src/gateway/__tests__/gateway-managed-routes.test.ts +181 -0
  117. package/src/gateway/__tests__/gateway.test.ts +222 -0
  118. package/src/gateway/__tests__/router.test.ts +247 -0
  119. package/src/gateway/__tests__/sanitize.test.ts +193 -0
  120. package/src/gateway/__tests__/session-store.test.ts +235 -0
  121. package/src/gateway/channel-manager.ts +349 -0
  122. package/src/gateway/channels/botcord.ts +605 -0
  123. package/src/gateway/channels/index.ts +6 -0
  124. package/src/gateway/channels/sanitize.ts +68 -0
  125. package/src/gateway/dispatcher.ts +554 -0
  126. package/src/gateway/gateway.ts +211 -0
  127. package/src/gateway/index.ts +29 -0
  128. package/src/gateway/log.ts +30 -0
  129. package/src/gateway/router.ts +60 -0
  130. package/src/gateway/runtimes/claude-code.ts +180 -0
  131. package/src/gateway/runtimes/codex.ts +312 -0
  132. package/src/gateway/runtimes/gemini.ts +43 -0
  133. package/src/gateway/runtimes/ndjson-stream.ts +225 -0
  134. package/src/gateway/runtimes/probe.ts +73 -0
  135. package/src/gateway/runtimes/registry.ts +143 -0
  136. package/src/gateway/session-store.ts +157 -0
  137. package/src/gateway/types.ts +325 -0
  138. package/src/index.ts +961 -0
  139. package/src/log.ts +47 -0
  140. package/src/provision.ts +879 -0
  141. package/src/room-context-fetcher.ts +124 -0
  142. package/src/room-context.ts +167 -0
  143. package/src/sender-classify.ts +46 -0
  144. package/src/snapshot-writer.ts +103 -0
  145. package/src/status-render.ts +132 -0
  146. package/src/system-context.ts +162 -0
  147. package/src/turn-text.ts +93 -0
  148. package/src/user-auth.ts +295 -0
  149. package/src/working-memory.ts +352 -0
@@ -0,0 +1,475 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
+ import { ChannelManager } from "../channel-manager.js";
3
+ import type {
4
+ ChannelAdapter,
5
+ ChannelStartContext,
6
+ ChannelStopContext,
7
+ GatewayConfig,
8
+ GatewayInboundEnvelope,
9
+ GatewayInboundMessage,
10
+ } from "../types.js";
11
+ import type { GatewayLogger } from "../log.js";
12
+
13
+ interface StartCall {
14
+ ctx: ChannelStartContext;
15
+ resolve: () => void;
16
+ reject: (err: unknown) => void;
17
+ promise: Promise<void>;
18
+ }
19
+
20
+ class FakeChannel implements ChannelAdapter {
21
+ readonly id: string;
22
+ readonly type = "fake";
23
+ readonly starts: StartCall[] = [];
24
+ readonly stopCalls: ChannelStopContext[] = [];
25
+ aborted = false;
26
+
27
+ constructor(id: string) {
28
+ this.id = id;
29
+ }
30
+
31
+ async start(ctx: ChannelStartContext): Promise<void> {
32
+ let resolveFn!: () => void;
33
+ let rejectFn!: (err: unknown) => void;
34
+ const promise = new Promise<void>((res, rej) => {
35
+ resolveFn = res;
36
+ rejectFn = rej;
37
+ });
38
+ const call: StartCall = { ctx, resolve: resolveFn, reject: rejectFn, promise };
39
+ this.starts.push(call);
40
+ ctx.abortSignal.addEventListener("abort", () => {
41
+ this.aborted = true;
42
+ });
43
+ await promise;
44
+ }
45
+
46
+ async stop(ctx: ChannelStopContext): Promise<void> {
47
+ this.stopCalls.push(ctx);
48
+ }
49
+
50
+ async send(): Promise<{ providerMessageId?: string | null }> {
51
+ return {};
52
+ }
53
+
54
+ latest(): StartCall {
55
+ const c = this.starts[this.starts.length - 1];
56
+ if (!c) throw new Error("start not called");
57
+ return c;
58
+ }
59
+
60
+ async emitNow(env: GatewayInboundEnvelope): Promise<void> {
61
+ await this.latest().ctx.emit(env);
62
+ }
63
+ }
64
+
65
+ function makeLogger(): GatewayLogger & { warns: unknown[][]; errors: unknown[][]; infos: unknown[][] } {
66
+ const warns: unknown[][] = [];
67
+ const errors: unknown[][] = [];
68
+ const infos: unknown[][] = [];
69
+ return {
70
+ info: (msg, meta) => {
71
+ infos.push([msg, meta]);
72
+ },
73
+ warn: (msg, meta) => {
74
+ warns.push([msg, meta]);
75
+ },
76
+ error: (msg, meta) => {
77
+ errors.push([msg, meta]);
78
+ },
79
+ debug: () => {},
80
+ warns,
81
+ errors,
82
+ infos,
83
+ };
84
+ }
85
+
86
+ function makeConfig(channelIds: string[]): GatewayConfig {
87
+ return {
88
+ channels: channelIds.map((id) => ({ id, type: "fake", accountId: `acc_${id}` })),
89
+ defaultRoute: { runtime: "claude-code", cwd: "/tmp" },
90
+ };
91
+ }
92
+
93
+ function makeMessage(id = "m_1", channel = "c1"): GatewayInboundMessage {
94
+ return {
95
+ id,
96
+ channel,
97
+ accountId: "acc_c1",
98
+ conversation: { id: "rm_1", kind: "direct" },
99
+ sender: { id: "ag_x", kind: "user" },
100
+ text: "hi",
101
+ raw: {},
102
+ receivedAt: 0,
103
+ };
104
+ }
105
+
106
+ async function flush(): Promise<void> {
107
+ // Let queued microtasks drain (state transitions from starting → running).
108
+ await Promise.resolve();
109
+ await Promise.resolve();
110
+ }
111
+
112
+ describe("ChannelManager", () => {
113
+ beforeEach(() => {
114
+ vi.useFakeTimers();
115
+ });
116
+ afterEach(() => {
117
+ vi.useRealTimers();
118
+ });
119
+
120
+ it("startAll calls channel.start on each channel", async () => {
121
+ const c1 = new FakeChannel("c1");
122
+ const c2 = new FakeChannel("c2");
123
+ const log = makeLogger();
124
+ const mgr = new ChannelManager({
125
+ config: makeConfig(["c1", "c2"]),
126
+ channels: [c1, c2],
127
+ log,
128
+ emit: async () => {},
129
+ });
130
+ await mgr.startAll();
131
+ expect(c1.starts).toHaveLength(1);
132
+ expect(c2.starts).toHaveLength(1);
133
+ });
134
+
135
+ it("status reports running true with lastStartAt after start", async () => {
136
+ const c1 = new FakeChannel("c1");
137
+ const mgr = new ChannelManager({
138
+ config: makeConfig(["c1"]),
139
+ channels: [c1],
140
+ log: makeLogger(),
141
+ emit: async () => {},
142
+ });
143
+ const before = Date.now();
144
+ await mgr.startAll();
145
+ await flush();
146
+ const snap = mgr.status()["c1"];
147
+ expect(snap.running).toBe(true);
148
+ expect(snap.lastStartAt).toBeGreaterThanOrEqual(before);
149
+ expect(snap.accountId).toBe("acc_c1");
150
+ });
151
+
152
+ it("stopAll aborts and returns once start resolves", async () => {
153
+ const c1 = new FakeChannel("c1");
154
+ const mgr = new ChannelManager({
155
+ config: makeConfig(["c1"]),
156
+ channels: [c1],
157
+ log: makeLogger(),
158
+ emit: async () => {},
159
+ });
160
+ await mgr.startAll();
161
+ await flush();
162
+ const stopP = mgr.stopAll("test");
163
+ // stopAll aborts, and the fake channel resolves on abort by our prompt.
164
+ c1.latest().resolve();
165
+ await stopP;
166
+ expect(c1.aborted).toBe(true);
167
+ expect(c1.stopCalls).toHaveLength(1);
168
+ expect(c1.stopCalls[0]?.reason).toBe("test");
169
+ expect(mgr.status()["c1"].running).toBe(false);
170
+ });
171
+
172
+ it("stopAll is idempotent", async () => {
173
+ const c1 = new FakeChannel("c1");
174
+ const mgr = new ChannelManager({
175
+ config: makeConfig(["c1"]),
176
+ channels: [c1],
177
+ log: makeLogger(),
178
+ emit: async () => {},
179
+ });
180
+ await mgr.startAll();
181
+ await flush();
182
+ const stopP = mgr.stopAll();
183
+ c1.latest().resolve();
184
+ await stopP;
185
+ // Second call: no pending promises, returns quickly.
186
+ await mgr.stopAll();
187
+ expect(mgr.status()["c1"].running).toBe(false);
188
+ });
189
+
190
+ it("startAll is a no-op for already running channels", async () => {
191
+ const c1 = new FakeChannel("c1");
192
+ const mgr = new ChannelManager({
193
+ config: makeConfig(["c1"]),
194
+ channels: [c1],
195
+ log: makeLogger(),
196
+ emit: async () => {},
197
+ });
198
+ await mgr.startAll();
199
+ await flush();
200
+ await mgr.startAll();
201
+ await flush();
202
+ expect(c1.starts).toHaveLength(1);
203
+ });
204
+
205
+ it("emit passthrough: ctx.emit reaches opts.emit", async () => {
206
+ const c1 = new FakeChannel("c1");
207
+ const received: GatewayInboundEnvelope[] = [];
208
+ const mgr = new ChannelManager({
209
+ config: makeConfig(["c1"]),
210
+ channels: [c1],
211
+ log: makeLogger(),
212
+ emit: async (env) => {
213
+ received.push(env);
214
+ },
215
+ });
216
+ await mgr.startAll();
217
+ await flush();
218
+ const env: GatewayInboundEnvelope = { message: makeMessage("m_1", "c1") };
219
+ await c1.emitNow(env);
220
+ expect(received).toHaveLength(1);
221
+ expect(received[0]).toBe(env);
222
+ });
223
+
224
+ it("drops malformed envelope (missing id) without throwing", async () => {
225
+ const c1 = new FakeChannel("c1");
226
+ const log = makeLogger();
227
+ const received: GatewayInboundEnvelope[] = [];
228
+ const mgr = new ChannelManager({
229
+ config: makeConfig(["c1"]),
230
+ channels: [c1],
231
+ log,
232
+ emit: async (env) => {
233
+ received.push(env);
234
+ },
235
+ });
236
+ await mgr.startAll();
237
+ await flush();
238
+ const bad = {
239
+ message: { ...makeMessage(), id: "" },
240
+ } as GatewayInboundEnvelope;
241
+ await expect(c1.emitNow(bad)).resolves.toBeUndefined();
242
+ expect(received).toHaveLength(0);
243
+ expect(log.warns.some((w) => String(w[0]).includes("malformed"))).toBe(true);
244
+ });
245
+
246
+ it("restarts after backoff when channel rejects; reconnectAttempts increments", async () => {
247
+ const c1 = new FakeChannel("c1");
248
+ const mgr = new ChannelManager({
249
+ config: makeConfig(["c1"]),
250
+ channels: [c1],
251
+ log: makeLogger(),
252
+ emit: async () => {},
253
+ backoffMs: { initial: 1000, max: 60_000, factor: 2 },
254
+ });
255
+ await mgr.startAll();
256
+ await flush();
257
+ // Crash the channel.
258
+ c1.latest().reject(new Error("boom"));
259
+ await flush();
260
+ expect(mgr.status()["c1"].restartPending).toBe(true);
261
+ expect(mgr.status()["c1"].lastError).toBe("boom");
262
+
263
+ await vi.advanceTimersByTimeAsync(1000);
264
+ await flush();
265
+ expect(c1.starts).toHaveLength(2);
266
+ expect(mgr.status()["c1"].reconnectAttempts).toBe(1);
267
+ expect(mgr.status()["c1"].restartPending).toBe(false);
268
+ // Clean up.
269
+ c1.latest().resolve();
270
+ await mgr.stopAll();
271
+ });
272
+
273
+ it("restarts when channel resolves (graceful) without stopAll", async () => {
274
+ const c1 = new FakeChannel("c1");
275
+ const mgr = new ChannelManager({
276
+ config: makeConfig(["c1"]),
277
+ channels: [c1],
278
+ log: makeLogger(),
279
+ emit: async () => {},
280
+ backoffMs: { initial: 1000, max: 60_000, factor: 2 },
281
+ });
282
+ await mgr.startAll();
283
+ await flush();
284
+ c1.latest().resolve();
285
+ await flush();
286
+ expect(mgr.status()["c1"].restartPending).toBe(true);
287
+ await vi.advanceTimersByTimeAsync(1000);
288
+ await flush();
289
+ expect(c1.starts).toHaveLength(2);
290
+ c1.latest().resolve();
291
+ await mgr.stopAll();
292
+ });
293
+
294
+ it("stopAll cancels a pending restart timer", async () => {
295
+ const c1 = new FakeChannel("c1");
296
+ const mgr = new ChannelManager({
297
+ config: makeConfig(["c1"]),
298
+ channels: [c1],
299
+ log: makeLogger(),
300
+ emit: async () => {},
301
+ backoffMs: { initial: 1000 },
302
+ });
303
+ await mgr.startAll();
304
+ await flush();
305
+ c1.latest().reject(new Error("crash"));
306
+ await flush();
307
+ expect(mgr.status()["c1"].restartPending).toBe(true);
308
+ await mgr.stopAll();
309
+ expect(mgr.status()["c1"].restartPending).toBe(false);
310
+ // Advance time well past backoff — no new start should occur.
311
+ await vi.advanceTimersByTimeAsync(5000);
312
+ await flush();
313
+ expect(c1.starts).toHaveLength(1);
314
+ });
315
+
316
+ it("backoff grows exponentially and caps at max", async () => {
317
+ const c1 = new FakeChannel("c1");
318
+ const mgr = new ChannelManager({
319
+ config: makeConfig(["c1"]),
320
+ channels: [c1],
321
+ log: makeLogger(),
322
+ emit: async () => {},
323
+ backoffMs: { initial: 1000, max: 4000, factor: 2 },
324
+ });
325
+ await mgr.startAll();
326
+ await flush();
327
+
328
+ // Crash #1 — schedule at 1000ms.
329
+ c1.latest().reject(new Error("e1"));
330
+ await flush();
331
+ await vi.advanceTimersByTimeAsync(999);
332
+ await flush();
333
+ expect(c1.starts).toHaveLength(1);
334
+ await vi.advanceTimersByTimeAsync(1);
335
+ await flush();
336
+ expect(c1.starts).toHaveLength(2);
337
+
338
+ // Crash #2 — schedule at 2000ms.
339
+ c1.latest().reject(new Error("e2"));
340
+ await flush();
341
+ await vi.advanceTimersByTimeAsync(1999);
342
+ await flush();
343
+ expect(c1.starts).toHaveLength(2);
344
+ await vi.advanceTimersByTimeAsync(1);
345
+ await flush();
346
+ expect(c1.starts).toHaveLength(3);
347
+
348
+ // Crash #3 — schedule at 4000ms (capped from 4000).
349
+ c1.latest().reject(new Error("e3"));
350
+ await flush();
351
+ await vi.advanceTimersByTimeAsync(3999);
352
+ await flush();
353
+ expect(c1.starts).toHaveLength(3);
354
+ await vi.advanceTimersByTimeAsync(1);
355
+ await flush();
356
+ expect(c1.starts).toHaveLength(4);
357
+
358
+ // Crash #4 — still capped at 4000ms.
359
+ c1.latest().reject(new Error("e4"));
360
+ await flush();
361
+ await vi.advanceTimersByTimeAsync(3999);
362
+ await flush();
363
+ expect(c1.starts).toHaveLength(4);
364
+ await vi.advanceTimersByTimeAsync(1);
365
+ await flush();
366
+ expect(c1.starts).toHaveLength(5);
367
+
368
+ c1.latest().resolve();
369
+ await mgr.stopAll();
370
+ });
371
+
372
+ it("long successful run resets backoff to initial", async () => {
373
+ const c1 = new FakeChannel("c1");
374
+ const mgr = new ChannelManager({
375
+ config: makeConfig(["c1"]),
376
+ channels: [c1],
377
+ log: makeLogger(),
378
+ emit: async () => {},
379
+ backoffMs: { initial: 1000, max: 60_000, factor: 2 },
380
+ });
381
+ await mgr.startAll();
382
+ await flush();
383
+
384
+ // Crash twice so backoff grows to 2000.
385
+ c1.latest().reject(new Error("e1"));
386
+ await flush();
387
+ await vi.advanceTimersByTimeAsync(1000);
388
+ await flush();
389
+ c1.latest().reject(new Error("e2"));
390
+ await flush();
391
+ await vi.advanceTimersByTimeAsync(2000);
392
+ await flush();
393
+ expect(c1.starts).toHaveLength(3);
394
+
395
+ // Now run for > 30s before crashing; backoff should reset to initial.
396
+ await vi.advanceTimersByTimeAsync(31_000);
397
+ c1.latest().reject(new Error("e3"));
398
+ await flush();
399
+ // Next restart should be at initial (1000), not 4000.
400
+ await vi.advanceTimersByTimeAsync(999);
401
+ await flush();
402
+ expect(c1.starts).toHaveLength(3);
403
+ await vi.advanceTimersByTimeAsync(1);
404
+ await flush();
405
+ expect(c1.starts).toHaveLength(4);
406
+
407
+ c1.latest().resolve();
408
+ await mgr.stopAll();
409
+ });
410
+
411
+ it("getChannel returns the adapter instance", () => {
412
+ const c1 = new FakeChannel("c1");
413
+ const mgr = new ChannelManager({
414
+ config: makeConfig(["c1"]),
415
+ channels: [c1],
416
+ log: makeLogger(),
417
+ emit: async () => {},
418
+ });
419
+ expect(mgr.getChannel("c1")).toBe(c1);
420
+ expect(mgr.getChannel("nope")).toBeUndefined();
421
+ });
422
+
423
+ it("startAll can be re-entered after stopAll", async () => {
424
+ const c1 = new FakeChannel("c1");
425
+ const mgr = new ChannelManager({
426
+ config: makeConfig(["c1"]),
427
+ channels: [c1],
428
+ log: makeLogger(),
429
+ emit: async () => {},
430
+ });
431
+ await mgr.startAll();
432
+ await flush();
433
+ const stopP = mgr.stopAll();
434
+ c1.latest().resolve();
435
+ await stopP;
436
+
437
+ await mgr.startAll();
438
+ await flush();
439
+ expect(c1.starts).toHaveLength(2);
440
+ c1.latest().resolve();
441
+ await mgr.stopAll();
442
+ });
443
+
444
+ it("passes accountId from config into ChannelStartContext", async () => {
445
+ const c1 = new FakeChannel("c1");
446
+ const mgr = new ChannelManager({
447
+ config: makeConfig(["c1"]),
448
+ channels: [c1],
449
+ log: makeLogger(),
450
+ emit: async () => {},
451
+ });
452
+ await mgr.startAll();
453
+ await flush();
454
+ expect(c1.latest().ctx.accountId).toBe("acc_c1");
455
+ c1.latest().resolve();
456
+ await mgr.stopAll();
457
+ });
458
+
459
+ it("setStatus merges patches into the channel snapshot", async () => {
460
+ const c1 = new FakeChannel("c1");
461
+ const mgr = new ChannelManager({
462
+ config: makeConfig(["c1"]),
463
+ channels: [c1],
464
+ log: makeLogger(),
465
+ emit: async () => {},
466
+ });
467
+ await mgr.startAll();
468
+ await flush();
469
+ c1.latest().ctx.setStatus({ connected: true });
470
+ expect(mgr.status()["c1"].connected).toBe(true);
471
+ expect(mgr.status()["c1"].running).toBe(true);
472
+ c1.latest().resolve();
473
+ await mgr.stopAll();
474
+ });
475
+ });