@nebutra/agent-runtime 0.2.0 → 0.2.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.
Files changed (137) hide show
  1. package/LICENSE +21 -676
  2. package/README.md +2 -0
  3. package/dist/adapters/dispatcher-sse.js +1 -0
  4. package/dist/adapters/index.d.ts +24 -5
  5. package/dist/adapters/index.js +28 -0
  6. package/dist/adapters/index.js.map +1 -1
  7. package/dist/adapters/mcp-catalog.js +1 -0
  8. package/dist/adapters/prisma-rollout.js +1 -0
  9. package/dist/chunk-424PT5DM.js +23 -0
  10. package/dist/chunk-424PT5DM.js.map +1 -0
  11. package/dist/{chunk-NN7DATXA.js → chunk-4Y25ZTKI.js} +3 -3
  12. package/dist/chunk-4Y25ZTKI.js.map +1 -0
  13. package/dist/{chunk-BJBBR3QA.js → chunk-D4YAPLOW.js} +4 -4
  14. package/dist/chunk-D4YAPLOW.js.map +1 -0
  15. package/dist/{chunk-ZMYX5VBU.js → chunk-GQZKYWFT.js} +23 -7
  16. package/dist/chunk-GQZKYWFT.js.map +1 -0
  17. package/dist/chunk-KCNN4QUQ.js +255 -0
  18. package/dist/chunk-KCNN4QUQ.js.map +1 -0
  19. package/dist/{chunk-PGGWSUTM.js → chunk-NI4EDT4T.js} +2 -2
  20. package/dist/chunk-NI4EDT4T.js.map +1 -0
  21. package/dist/chunk-Q62VKHIT.js +178 -0
  22. package/dist/chunk-Q62VKHIT.js.map +1 -0
  23. package/dist/{chunk-MUF7ZZTO.js → chunk-R5HOSQUW.js} +2 -1
  24. package/dist/{chunk-MUF7ZZTO.js.map → chunk-R5HOSQUW.js.map} +1 -1
  25. package/dist/{chunk-5N4644PB.js → chunk-SD2ZJ7XG.js} +4 -4
  26. package/dist/cli.d.ts +2 -0
  27. package/dist/cli.js +52 -0
  28. package/dist/cli.js.map +1 -0
  29. package/dist/commands.js +1 -0
  30. package/dist/definitions.js +1 -0
  31. package/dist/dispatcher.js +1 -0
  32. package/dist/durable-turn.js +3 -2
  33. package/dist/hook-pipeline.js +1 -0
  34. package/dist/index.d.ts +8 -85
  35. package/dist/index.js +249 -134
  36. package/dist/index.js.map +1 -1
  37. package/dist/loop.d.ts +2 -2
  38. package/dist/loop.js +3 -2
  39. package/dist/mcp-bridge.d.ts +3 -3
  40. package/dist/mcp-bridge.js +3 -2
  41. package/dist/model.js +1 -0
  42. package/dist/orchestration.d.ts +84 -0
  43. package/dist/orchestration.js +16 -0
  44. package/dist/orchestration.js.map +1 -0
  45. package/dist/policy.js +1 -0
  46. package/dist/protocol.js +1 -0
  47. package/dist/pulsar.d.ts +78 -0
  48. package/dist/pulsar.js +17 -0
  49. package/dist/pulsar.js.map +1 -0
  50. package/dist/rollout-store-persistent.js +1 -0
  51. package/dist/rollout.js +1 -0
  52. package/dist/sandbox.js +2 -1
  53. package/dist/skills.js +2 -1
  54. package/dist/subagents.js +1 -0
  55. package/dist/tools.d.ts +4 -3
  56. package/dist/tools.js +5 -3
  57. package/package.json +84 -27
  58. package/.turbo/turbo-build.log +0 -115
  59. package/.turbo/turbo-test.log +0 -44
  60. package/.turbo/turbo-typecheck.log +0 -4
  61. package/CHANGELOG.md +0 -253
  62. package/dist/chunk-BJBBR3QA.js.map +0 -1
  63. package/dist/chunk-NN7DATXA.js.map +0 -1
  64. package/dist/chunk-PGGWSUTM.js.map +0 -1
  65. package/dist/chunk-ZMYX5VBU.js.map +0 -1
  66. package/src/adapters/dispatcher-sse.test.ts +0 -218
  67. package/src/adapters/dispatcher-sse.ts +0 -222
  68. package/src/adapters/index.ts +0 -18
  69. package/src/adapters/mcp-catalog.test.ts +0 -213
  70. package/src/adapters/mcp-catalog.ts +0 -188
  71. package/src/adapters/prisma-rollout.test.ts +0 -153
  72. package/src/adapters/prisma-rollout.ts +0 -104
  73. package/src/agent-runtime.test.ts +0 -176
  74. package/src/artifact-stream.test.ts +0 -330
  75. package/src/artifact-stream.ts +0 -453
  76. package/src/channel-gateway.test.ts +0 -432
  77. package/src/channel-gateway.ts +0 -357
  78. package/src/code-review.test.ts +0 -501
  79. package/src/code-review.ts +0 -495
  80. package/src/command-suggestions.test.ts +0 -251
  81. package/src/command-suggestions.ts +0 -338
  82. package/src/commands.test.ts +0 -184
  83. package/src/commands.ts +0 -140
  84. package/src/commit-message.test.ts +0 -249
  85. package/src/commit-message.ts +0 -180
  86. package/src/context-compaction.test.ts +0 -522
  87. package/src/context-compaction.ts +0 -434
  88. package/src/definitions.test.ts +0 -78
  89. package/src/definitions.ts +0 -190
  90. package/src/deployment-status.test.ts +0 -215
  91. package/src/deployment-status.ts +0 -227
  92. package/src/design-context.test.ts +0 -195
  93. package/src/design-context.ts +0 -198
  94. package/src/dispatcher.test.ts +0 -234
  95. package/src/dispatcher.ts +0 -189
  96. package/src/durable-turn.test.ts +0 -209
  97. package/src/durable-turn.ts +0 -135
  98. package/src/edit-planner.test.ts +0 -204
  99. package/src/edit-planner.ts +0 -325
  100. package/src/fuzzy-match.test.ts +0 -311
  101. package/src/fuzzy-match.ts +0 -444
  102. package/src/hook-pipeline.test.ts +0 -279
  103. package/src/hook-pipeline.ts +0 -373
  104. package/src/inbound-admission.test.ts +0 -394
  105. package/src/inbound-admission.ts +0 -246
  106. package/src/index.ts +0 -47
  107. package/src/loop.test.ts +0 -161
  108. package/src/loop.ts +0 -211
  109. package/src/mcp-bridge.test.ts +0 -165
  110. package/src/mcp-bridge.ts +0 -76
  111. package/src/memory-provider.test.ts +0 -232
  112. package/src/memory-provider.ts +0 -257
  113. package/src/model.ts +0 -168
  114. package/src/permission-ruleset.test.ts +0 -301
  115. package/src/permission-ruleset.ts +0 -200
  116. package/src/policy.ts +0 -151
  117. package/src/project-repo.test.ts +0 -232
  118. package/src/project-repo.ts +0 -311
  119. package/src/protocol.ts +0 -159
  120. package/src/rollout-store-persistent.test.ts +0 -217
  121. package/src/rollout-store-persistent.ts +0 -166
  122. package/src/rollout.ts +0 -150
  123. package/src/sandbox.ts +0 -113
  124. package/src/session-share.test.ts +0 -360
  125. package/src/session-share.ts +0 -310
  126. package/src/skill-distillation.test.ts +0 -177
  127. package/src/skill-distillation.ts +0 -369
  128. package/src/skills.test.ts +0 -277
  129. package/src/skills.ts +0 -255
  130. package/src/subagents.test.ts +0 -290
  131. package/src/subagents.ts +0 -332
  132. package/src/tools.ts +0 -126
  133. package/src/workbench.test.ts +0 -0
  134. package/src/workbench.ts +0 -0
  135. package/tsconfig.json +0 -12
  136. package/tsup.config.ts +0 -33
  137. /package/dist/{chunk-5N4644PB.js.map → chunk-SD2ZJ7XG.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
- });