@runfusion/fusion 0.19.0 → 0.20.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 (58) hide show
  1. package/dist/bin.js +5019 -2164
  2. package/dist/client/assets/AgentDetailView-C6BG7O7i.js +18 -0
  3. package/dist/client/assets/AgentDetailView-CUtWvXBn.css +1 -0
  4. package/dist/client/assets/ChatView-DeXUYwSY.js +1 -0
  5. package/dist/client/assets/{DevServerView-DI71QIND.js → DevServerView-Dariyxt_.js} +1 -1
  6. package/dist/client/assets/{DirectoryPicker-6eBfMR3k.js → DirectoryPicker-SchiK-Aq.js} +1 -1
  7. package/dist/client/assets/{DocumentsView-D9pxwmaa.js → DocumentsView-C6v-tBhG.js} +1 -1
  8. package/dist/client/assets/InsightsView-AWo5o_81.css +1 -0
  9. package/dist/client/assets/InsightsView-Cqim12az.js +11 -0
  10. package/dist/client/assets/{MemoryView-DfjllRpZ.js → MemoryView-CakLoJtY.js} +2 -2
  11. package/dist/client/assets/NodesView-BxGm3poT.js +14 -0
  12. package/dist/client/assets/{NodesView-sJgPLTzz.css → NodesView-fXqDk9ur.css} +1 -1
  13. package/dist/client/assets/PiExtensionsManager-lJbmskyZ.js +6 -0
  14. package/dist/client/assets/PluginManager-BZjNNf9m.js +1 -0
  15. package/dist/client/assets/ResearchView-Bzsr9V0y.js +1 -0
  16. package/dist/client/assets/{RoadmapsView-ajwwf979.js → RoadmapsView-CeKks_OI.js} +2 -2
  17. package/dist/client/assets/SettingsModal-D-9CLguN.js +31 -0
  18. package/dist/client/assets/{SettingsModal-D732WMft.js → SettingsModal-YdeVPhRJ.js} +1 -1
  19. package/dist/client/assets/{SetupWizardModal-DRF5fOoR.css → SetupWizardModal-CGYGKurR.css} +1 -1
  20. package/dist/client/assets/SetupWizardModal-DAC04LlA.js +1 -0
  21. package/dist/client/assets/{SkillsView-CzVO7yTO.js → SkillsView-CClC_5RN.js} +1 -1
  22. package/dist/client/assets/index-CrHLf3pB.js +1222 -0
  23. package/dist/client/assets/index-Df1bHDY4.css +1 -0
  24. package/dist/client/assets/star-DxVRh9VT.js +6 -0
  25. package/dist/client/assets/{users-R3_m9pE5.js → users-3SD3oNMQ.js} +1 -1
  26. package/dist/client/index.html +2 -2
  27. package/dist/client/version.json +1 -1
  28. package/dist/droid-cli/index.ts +3 -5
  29. package/dist/droid-cli/package.json +1 -1
  30. package/dist/droid-cli/src/__tests__/event-bridge.test.ts +6 -1315
  31. package/dist/droid-cli/src/__tests__/provider.test.ts +6 -1927
  32. package/dist/droid-cli/src/control-handler.ts +1 -82
  33. package/dist/droid-cli/src/event-bridge.ts +1 -397
  34. package/dist/droid-cli/src/mcp-config.ts +1 -144
  35. package/dist/droid-cli/src/process-manager.ts +1 -358
  36. package/dist/droid-cli/src/prompt-builder.ts +1 -629
  37. package/dist/droid-cli/src/provider.ts +1 -447
  38. package/dist/droid-cli/src/stream-parser.ts +1 -37
  39. package/dist/droid-cli/src/thinking-config.ts +1 -83
  40. package/dist/droid-cli/src/tool-mapping.ts +1 -147
  41. package/dist/droid-cli/src/types.ts +1 -87
  42. package/dist/extension.js +4084 -1654
  43. package/dist/pi-claude-cli/package.json +1 -1
  44. package/dist/plugins/fusion-plugin-dependency-graph/package.json +1 -1
  45. package/package.json +3 -3
  46. package/skill/fusion/references/engine-tools.md +3 -1
  47. package/skill/fusion/references/extension-tools.md +3 -1
  48. package/dist/client/assets/ChatView-DEG93wpC.js +0 -1
  49. package/dist/client/assets/InsightsView-4KiUKzbz.css +0 -1
  50. package/dist/client/assets/InsightsView-D2_XwizY.js +0 -11
  51. package/dist/client/assets/NodesView-D7hWWUCW.js +0 -14
  52. package/dist/client/assets/PiExtensionsManager-d8cJKjcL.js +0 -11
  53. package/dist/client/assets/PluginManager-CNzhmPzJ.js +0 -1
  54. package/dist/client/assets/ResearchView-2xAa3pzZ.js +0 -1
  55. package/dist/client/assets/SettingsModal-Dk0zKdTy.js +0 -31
  56. package/dist/client/assets/SetupWizardModal-DohGTvQT.js +0 -1
  57. package/dist/client/assets/index-CVCt2pCH.css +0 -1
  58. package/dist/client/assets/index-hnO5QagU.js +0 -1239
@@ -1,1318 +1,9 @@
1
- import { describe, it, expect, vi, beforeEach } from "vitest";
1
+ import { describe, expect, it } from "vitest";
2
+ import { createEventBridge as shimCreateEventBridge } from "../event-bridge";
3
+ import { createEventBridge as pluginCreateEventBridge } from "../../../../plugins/fusion-plugin-droid-runtime/src/event-bridge.js";
2
4
 
3
- // Mock @mariozechner/pi-ai before importing event-bridge
4
- vi.mock("@mariozechner/pi-ai", () => ({
5
- calculateCost: vi.fn(),
6
- }));
7
-
8
- import { createEventBridge } from "../event-bridge";
9
- import { calculateCost } from "@mariozechner/pi-ai";
10
-
11
- // Helper: create a mock stream that captures pushed events
12
- function createMockStream() {
13
- const events: unknown[] = [];
14
- return {
15
- push: vi.fn((event: unknown) => events.push(event)),
16
- end: vi.fn(),
17
- events,
18
- };
19
- }
20
-
21
- // Helper: create a minimal mock model
22
- function createMockModel() {
23
- return {
24
- id: "claude-sonnet-4-5-20250929",
25
- name: "Claude Sonnet 4.5",
26
- api: "droid-cli",
27
- provider: "anthropic",
28
- cost: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
29
- contextWindow: 200000,
30
- maxTokens: 8192,
31
- };
32
- }
33
-
34
- describe("createEventBridge", () => {
35
- let stream: ReturnType<typeof createMockStream>;
36
- let model: ReturnType<typeof createMockModel>;
37
-
38
- beforeEach(() => {
39
- stream = createMockStream();
40
- model = createMockModel();
41
- vi.clearAllMocks();
42
- });
43
-
44
- // Helper: create bridge and trigger the initial "start" event so tests
45
- // don't need to account for it in their push call counts.
46
- function createBridgeWithStart() {
47
- const bridge = createEventBridge(stream as any, model as any);
48
- // Trigger start event via message_start
49
- bridge.handleEvent({ type: "message_start", message: { usage: {} } });
50
- stream.push.mockClear();
51
- stream.events.length = 0;
52
- return bridge;
53
- }
54
-
55
- it("pushes start event on first handleEvent call", () => {
56
- const bridge = createEventBridge(stream as any, model as any);
57
- bridge.handleEvent({ type: "message_start", message: { usage: {} } });
58
-
59
- expect(stream.push).toHaveBeenCalledWith(
60
- expect.objectContaining({ type: "start" }),
61
- );
62
- });
63
-
64
- describe("text content block streaming", () => {
65
- it("pushes text_start on content_block_start with text type", () => {
66
- const bridge = createBridgeWithStart();
67
- bridge.handleEvent({
68
- type: "content_block_start",
69
- index: 0,
70
- content_block: { type: "text", text: "" },
71
- });
72
-
73
- expect(stream.push).toHaveBeenCalledTimes(1);
74
- const event = stream.events[0] as any;
75
- expect(event.type).toBe("text_start");
76
- expect(event.contentIndex).toBe(0);
77
- });
78
-
79
- it("pushes text_delta on content_block_delta with text_delta type", () => {
80
- const bridge = createBridgeWithStart();
81
- bridge.handleEvent({
82
- type: "content_block_start",
83
- index: 0,
84
- content_block: { type: "text", text: "" },
85
- });
86
- bridge.handleEvent({
87
- type: "content_block_delta",
88
- index: 0,
89
- delta: { type: "text_delta", text: "Hello" },
90
- });
91
-
92
- expect(stream.push).toHaveBeenCalledTimes(2);
93
- const event = stream.events[1] as any;
94
- expect(event.type).toBe("text_delta");
95
- expect(event.contentIndex).toBe(0);
96
- expect(event.delta).toBe("Hello");
97
- });
98
-
99
- it("pushes text_end on content_block_stop with accumulated text", () => {
100
- const bridge = createBridgeWithStart();
101
- bridge.handleEvent({
102
- type: "content_block_start",
103
- index: 0,
104
- content_block: { type: "text", text: "" },
105
- });
106
- bridge.handleEvent({
107
- type: "content_block_delta",
108
- index: 0,
109
- delta: { type: "text_delta", text: "Hello" },
110
- });
111
- bridge.handleEvent({
112
- type: "content_block_delta",
113
- index: 0,
114
- delta: { type: "text_delta", text: " world" },
115
- });
116
- bridge.handleEvent({
117
- type: "content_block_stop",
118
- index: 0,
119
- });
120
-
121
- expect(stream.push).toHaveBeenCalledTimes(4);
122
- const event = stream.events[3] as any;
123
- expect(event.type).toBe("text_end");
124
- expect(event.contentIndex).toBe(0);
125
- expect(event.content).toBe("Hello world");
126
- });
127
- });
128
-
129
- describe("multiple text blocks", () => {
130
- it("tracks multiple text blocks independently", () => {
131
- const bridge = createBridgeWithStart();
132
-
133
- // Block 0
134
- bridge.handleEvent({
135
- type: "content_block_start",
136
- index: 0,
137
- content_block: { type: "text", text: "" },
138
- });
139
- bridge.handleEvent({
140
- type: "content_block_delta",
141
- index: 0,
142
- delta: { type: "text_delta", text: "First" },
143
- });
144
- bridge.handleEvent({
145
- type: "content_block_stop",
146
- index: 0,
147
- });
148
-
149
- // Block 1
150
- bridge.handleEvent({
151
- type: "content_block_start",
152
- index: 1,
153
- content_block: { type: "text", text: "" },
154
- });
155
- bridge.handleEvent({
156
- type: "content_block_delta",
157
- index: 1,
158
- delta: { type: "text_delta", text: "Second" },
159
- });
160
- bridge.handleEvent({
161
- type: "content_block_stop",
162
- index: 1,
163
- });
164
-
165
- // Verify block 0
166
- const textEnd0 = stream.events[2] as any;
167
- expect(textEnd0.type).toBe("text_end");
168
- expect(textEnd0.contentIndex).toBe(0);
169
- expect(textEnd0.content).toBe("First");
170
-
171
- // Verify block 1
172
- const textStart1 = stream.events[3] as any;
173
- expect(textStart1.type).toBe("text_start");
174
- expect(textStart1.contentIndex).toBe(1);
175
-
176
- const textEnd1 = stream.events[5] as any;
177
- expect(textEnd1.type).toBe("text_end");
178
- expect(textEnd1.contentIndex).toBe(1);
179
- expect(textEnd1.content).toBe("Second");
180
- });
181
- });
182
-
183
- describe("message_start usage tracking", () => {
184
- it("captures initial usage from message_start", () => {
185
- const bridge = createEventBridge(stream as any, model as any);
186
- bridge.handleEvent({
187
- type: "message_start",
188
- message: {
189
- usage: {
190
- input_tokens: 100,
191
- output_tokens: 0,
192
- cache_read_input_tokens: 50,
193
- cache_creation_input_tokens: 25,
194
- },
195
- },
196
- });
197
-
198
- const output = bridge.getOutput();
199
- expect(output.usage.input).toBe(100);
200
- expect(output.usage.output).toBe(0);
201
- expect(output.usage.cacheRead).toBe(50);
202
- expect(output.usage.cacheWrite).toBe(25);
203
- expect(output.usage.totalTokens).toBe(175);
204
- expect(calculateCost).toHaveBeenCalled();
205
- });
206
-
207
- it("defaults missing usage fields to 0", () => {
208
- const bridge = createEventBridge(stream as any, model as any);
209
- bridge.handleEvent({
210
- type: "message_start",
211
- message: {
212
- usage: {
213
- input_tokens: 50,
214
- // output_tokens, cache_read_input_tokens, cache_creation_input_tokens all missing
215
- },
216
- },
217
- });
218
-
219
- const output = bridge.getOutput();
220
- expect(output.usage.input).toBe(50);
221
- expect(output.usage.output).toBe(0);
222
- expect(output.usage.cacheRead).toBe(0);
223
- expect(output.usage.cacheWrite).toBe(0);
224
- expect(output.usage.totalTokens).toBe(50);
225
- });
226
- });
227
-
228
- describe("message_delta handling", () => {
229
- it("captures stop_reason from message_delta", () => {
230
- const bridge = createEventBridge(stream as any, model as any);
231
- bridge.handleEvent({
232
- type: "message_delta",
233
- delta: { stop_reason: "end_turn" },
234
- usage: { output_tokens: 42 },
235
- });
236
-
237
- const output = bridge.getOutput();
238
- expect(output.stopReason).toBe("stop");
239
- });
240
-
241
- it("updates usage from message_delta", () => {
242
- const bridge = createEventBridge(stream as any, model as any);
243
- bridge.handleEvent({
244
- type: "message_start",
245
- message: {
246
- usage: {
247
- input_tokens: 100,
248
- output_tokens: 0,
249
- },
250
- },
251
- });
252
- bridge.handleEvent({
253
- type: "message_delta",
254
- delta: { stop_reason: "end_turn" },
255
- usage: { output_tokens: 42 },
256
- });
257
-
258
- const output = bridge.getOutput();
259
- expect(output.usage.output).toBe(42);
260
- expect(output.usage.totalTokens).toBe(142);
261
- });
262
- });
263
-
264
- describe("message_stop (no-op, done pushed by provider)", () => {
265
- it("does not push done (provider pushes it after readline closes)", () => {
266
- const bridge = createEventBridge(stream as any, model as any);
267
-
268
- bridge.handleEvent({
269
- type: "message_start",
270
- message: { usage: { input_tokens: 100, output_tokens: 0 } },
271
- });
272
- bridge.handleEvent({
273
- type: "content_block_start",
274
- index: 0,
275
- content_block: { type: "text", text: "" },
276
- });
277
- bridge.handleEvent({
278
- type: "content_block_delta",
279
- index: 0,
280
- delta: { type: "text_delta", text: "Hello world" },
281
- });
282
- bridge.handleEvent({
283
- type: "content_block_stop",
284
- index: 0,
285
- });
286
- bridge.handleEvent({
287
- type: "message_delta",
288
- delta: { stop_reason: "end_turn" },
289
- usage: { output_tokens: 5 },
290
- });
291
- bridge.handleEvent({
292
- type: "message_stop",
293
- });
294
-
295
- // No done event from event bridge (provider handles it after readline closes)
296
- const doneEvent = stream.events.find((e: any) => e.type === "done");
297
- expect(doneEvent).toBeUndefined();
298
- expect(stream.end).not.toHaveBeenCalled();
299
-
300
- // Output state is correct for provider to use
301
- const output = bridge.getOutput();
302
- expect(output.content).toHaveLength(1);
303
- expect((output.content[0] as any).text).toBe("Hello world");
304
- expect(output.stopReason).toBe("stop");
305
- });
306
- });
307
-
308
- describe("stop reason mapping", () => {
309
- it("maps end_turn to stop", () => {
310
- const bridge = createEventBridge(stream as any, model as any);
311
- bridge.handleEvent({
312
- type: "message_delta",
313
- delta: { stop_reason: "end_turn" },
314
- });
315
- expect(bridge.getOutput().stopReason).toBe("stop");
316
- });
317
-
318
- it("maps max_tokens to length", () => {
319
- const bridge = createEventBridge(stream as any, model as any);
320
- bridge.handleEvent({
321
- type: "message_delta",
322
- delta: { stop_reason: "max_tokens" },
323
- });
324
- expect(bridge.getOutput().stopReason).toBe("length");
325
- });
326
-
327
- it("maps tool_use to toolUse", () => {
328
- const bridge = createEventBridge(stream as any, model as any);
329
- bridge.handleEvent({
330
- type: "message_delta",
331
- delta: { stop_reason: "tool_use" },
332
- });
333
- expect(bridge.getOutput().stopReason).toBe("toolUse");
334
- });
335
-
336
- it("maps unknown stop reasons to stop", () => {
337
- const bridge = createEventBridge(stream as any, model as any);
338
- bridge.handleEvent({
339
- type: "message_delta",
340
- delta: { stop_reason: "something_else" },
341
- });
342
- expect(bridge.getOutput().stopReason).toBe("stop");
343
- });
344
- });
345
-
346
- describe("tool_use content block streaming", () => {
347
- it("pushes toolcall_start with mapped pi name on content_block_start", () => {
348
- const bridge = createBridgeWithStart();
349
- bridge.handleEvent({
350
- type: "content_block_start",
351
- index: 0,
352
- content_block: { type: "tool_use", id: "toolu_01ABC", name: "Read" },
353
- });
354
-
355
- expect(stream.push).toHaveBeenCalledTimes(1);
356
- const event = stream.events[0] as any;
357
- expect(event.type).toBe("toolcall_start");
358
- expect(event.contentIndex).toBe(0);
359
- // Tool name should be mapped from Claude "Read" to pi "read"
360
- expect(event.partial.content[0].name).toBe("read");
361
- });
362
-
363
- it("pushes toolcall_delta with raw JSON fragment on input_json_delta", () => {
364
- const bridge = createBridgeWithStart();
365
- bridge.handleEvent({
366
- type: "content_block_start",
367
- index: 0,
368
- content_block: { type: "tool_use", id: "toolu_01ABC", name: "Read" },
369
- });
370
- stream.push.mockClear();
371
- stream.events.length = 0;
372
-
373
- bridge.handleEvent({
374
- type: "content_block_delta",
375
- index: 0,
376
- delta: { type: "input_json_delta", partial_json: '{"file_' },
377
- });
378
-
379
- expect(stream.push).toHaveBeenCalledTimes(1);
380
- const event = stream.events[0] as any;
381
- expect(event.type).toBe("toolcall_delta");
382
- expect(event.delta).toBe('{"file_');
383
- expect(event.contentIndex).toBe(0);
384
- });
385
-
386
- it("accumulates partial JSON across multiple deltas", () => {
387
- const bridge = createBridgeWithStart();
388
- bridge.handleEvent({
389
- type: "content_block_start",
390
- index: 0,
391
- content_block: { type: "tool_use", id: "toolu_01ABC", name: "Read" },
392
- });
393
-
394
- bridge.handleEvent({
395
- type: "content_block_delta",
396
- index: 0,
397
- delta: { type: "input_json_delta", partial_json: '{"file_' },
398
- });
399
- bridge.handleEvent({
400
- type: "content_block_delta",
401
- index: 0,
402
- delta: { type: "input_json_delta", partial_json: 'path": "/foo.ts"}' },
403
- });
404
-
405
- // After full JSON is accumulated, the output content should have parsed args
406
- const output = bridge.getOutput();
407
- const toolCall = output.content[0] as any;
408
- expect(toolCall.type).toBe("toolCall");
409
- // Arguments should be updated as JSON becomes parseable
410
- expect(toolCall.arguments).toEqual({ file_path: "/foo.ts" });
411
- });
412
-
413
- it("pushes toolcall_end with fully parsed and argument-mapped ToolCall on block_stop", () => {
414
- const bridge = createBridgeWithStart();
415
- bridge.handleEvent({
416
- type: "content_block_start",
417
- index: 0,
418
- content_block: { type: "tool_use", id: "toolu_01ABC", name: "Read" },
419
- });
420
- bridge.handleEvent({
421
- type: "content_block_delta",
422
- index: 0,
423
- delta: {
424
- type: "input_json_delta",
425
- partial_json: '{"file_path": "/foo.ts"}',
426
- },
427
- });
428
- stream.push.mockClear();
429
- stream.events.length = 0;
430
-
431
- bridge.handleEvent({
432
- type: "content_block_stop",
433
- index: 0,
434
- });
435
-
436
- expect(stream.push).toHaveBeenCalledTimes(1);
437
- const event = stream.events[0] as any;
438
- expect(event.type).toBe("toolcall_end");
439
- expect(event.contentIndex).toBe(0);
440
- expect(event.toolCall.type).toBe("toolCall");
441
- expect(event.toolCall.id).toBe("toolu_01ABC");
442
- expect(event.toolCall.name).toBe("read");
443
- // Claude's "file_path" should be mapped to pi's "path"
444
- expect(event.toolCall.arguments).toEqual({ path: "/foo.ts" });
445
- });
446
-
447
- it("emits {} for parameterless MCP tool calls (no input_json_delta)", () => {
448
- // Parameterless MCP tools (e.g. fn_review_spec, schema
449
- // {type:"object", properties:{}}) emit ZERO input_json_delta events.
450
- // Without the empty-partialJson guard, finalArgs would fall through to
451
- // the raw-string fallback ("") and pi's TypeBox validator would reject
452
- // the call with "Validation failed for tool ...: root: must be object".
453
- const bridge = createBridgeWithStart();
454
- bridge.handleEvent({
455
- type: "content_block_start",
456
- index: 0,
457
- content_block: {
458
- type: "tool_use",
459
- id: "toolu_01XYZ",
460
- name: "mcp__custom-tools__fn_review_spec",
461
- },
462
- });
463
- // No content_block_delta with input_json_delta — Claude emits none for
464
- // parameterless tools.
465
- stream.push.mockClear();
466
- stream.events.length = 0;
467
-
468
- bridge.handleEvent({
469
- type: "content_block_stop",
470
- index: 0,
471
- });
472
-
473
- expect(stream.push).toHaveBeenCalledTimes(1);
474
- const event = stream.events[0] as any;
475
- expect(event.type).toBe("toolcall_end");
476
- expect(event.toolCall.arguments).toEqual({});
477
- // The MCP prefix should be stripped: pi sees the bare tool name.
478
- expect(event.toolCall.name).toBe("fn_review_spec");
479
- });
480
-
481
- it("tracks multiple tool_use blocks independently by Claude event.index", () => {
482
- const bridge = createBridgeWithStart();
483
-
484
- // Tool block at index 0
485
- bridge.handleEvent({
486
- type: "content_block_start",
487
- index: 0,
488
- content_block: { type: "tool_use", id: "toolu_01", name: "Read" },
489
- });
490
- bridge.handleEvent({
491
- type: "content_block_delta",
492
- index: 0,
493
- delta: {
494
- type: "input_json_delta",
495
- partial_json: '{"file_path": "/a.ts"}',
496
- },
497
- });
498
- bridge.handleEvent({
499
- type: "content_block_stop",
500
- index: 0,
501
- });
502
-
503
- // Tool block at index 1
504
- bridge.handleEvent({
505
- type: "content_block_start",
506
- index: 1,
507
- content_block: { type: "tool_use", id: "toolu_02", name: "Write" },
508
- });
509
- bridge.handleEvent({
510
- type: "content_block_delta",
511
- index: 1,
512
- delta: {
513
- type: "input_json_delta",
514
- partial_json: '{"file_path": "/b.ts", "content": "hello"}',
515
- },
516
- });
517
- bridge.handleEvent({
518
- type: "content_block_stop",
519
- index: 1,
520
- });
521
-
522
- // Find toolcall_end events
523
- const endEvents = stream.events.filter(
524
- (e: any) => e.type === "toolcall_end",
525
- ) as any[];
526
- expect(endEvents).toHaveLength(2);
527
- expect(endEvents[0].toolCall.id).toBe("toolu_01");
528
- expect(endEvents[0].toolCall.name).toBe("read");
529
- expect(endEvents[0].toolCall.arguments).toEqual({ path: "/a.ts" });
530
- expect(endEvents[1].toolCall.id).toBe("toolu_02");
531
- expect(endEvents[1].toolCall.name).toBe("write");
532
- expect(endEvents[1].toolCall.arguments).toEqual({
533
- path: "/b.ts",
534
- content: "hello",
535
- });
536
- });
537
-
538
- it("tracks tool_use block interleaved with text block correctly", () => {
539
- const bridge = createBridgeWithStart();
540
-
541
- // Text block at index 0
542
- bridge.handleEvent({
543
- type: "content_block_start",
544
- index: 0,
545
- content_block: { type: "text", text: "" },
546
- });
547
- bridge.handleEvent({
548
- type: "content_block_delta",
549
- index: 0,
550
- delta: { type: "text_delta", text: "Let me read that file." },
551
- });
552
- bridge.handleEvent({
553
- type: "content_block_stop",
554
- index: 0,
555
- });
556
-
557
- // Tool block at index 1
558
- bridge.handleEvent({
559
- type: "content_block_start",
560
- index: 1,
561
- content_block: { type: "tool_use", id: "toolu_01", name: "Read" },
562
- });
563
- bridge.handleEvent({
564
- type: "content_block_delta",
565
- index: 1,
566
- delta: {
567
- type: "input_json_delta",
568
- partial_json: '{"file_path": "/foo.ts"}',
569
- },
570
- });
571
- bridge.handleEvent({
572
- type: "content_block_stop",
573
- index: 1,
574
- });
575
-
576
- const output = bridge.getOutput();
577
- expect(output.content).toHaveLength(2);
578
- expect((output.content[0] as any).type).toBe("text");
579
- expect((output.content[0] as any).text).toBe("Let me read that file.");
580
- expect((output.content[1] as any).type).toBe("toolCall");
581
- expect((output.content[1] as any).name).toBe("read");
582
-
583
- // Verify contentIndex values
584
- const textStart = stream.events.find(
585
- (e: any) => e.type === "text_start",
586
- ) as any;
587
- expect(textStart.contentIndex).toBe(0);
588
- const toolStart = stream.events.find(
589
- (e: any) => e.type === "toolcall_start",
590
- ) as any;
591
- expect(toolStart.contentIndex).toBe(1);
592
- });
593
-
594
- it("handles partial JSON parse failure during delta gracefully (no crash)", () => {
595
- const bridge = createBridgeWithStart();
596
- bridge.handleEvent({
597
- type: "content_block_start",
598
- index: 0,
599
- content_block: { type: "tool_use", id: "toolu_01", name: "Bash" },
600
- });
601
-
602
- // Partial JSON that cannot be parsed yet
603
- bridge.handleEvent({
604
- type: "content_block_delta",
605
- index: 0,
606
- delta: { type: "input_json_delta", partial_json: '{"command": "ls' },
607
- });
608
-
609
- // Should not crash -- arguments should still be empty object (previous value)
610
- const output = bridge.getOutput();
611
- const toolCall = output.content[0] as any;
612
- expect(toolCall.arguments).toEqual({});
613
-
614
- // Complete the JSON
615
- bridge.handleEvent({
616
- type: "content_block_delta",
617
- index: 0,
618
- delta: { type: "input_json_delta", partial_json: ' -la"}' },
619
- });
620
-
621
- // Now should be parsed
622
- expect(toolCall.arguments).toEqual({ command: "ls -la" });
623
- });
624
-
625
- it("emits toolcall_end with raw string arguments when final JSON parse fails", () => {
626
- const bridge = createBridgeWithStart();
627
- bridge.handleEvent({
628
- type: "content_block_start",
629
- index: 0,
630
- content_block: { type: "tool_use", id: "toolu_01", name: "Read" },
631
- });
632
-
633
- // Send invalid JSON that will never parse
634
- bridge.handleEvent({
635
- type: "content_block_delta",
636
- index: 0,
637
- delta: {
638
- type: "input_json_delta",
639
- partial_json: "not valid json at all",
640
- },
641
- });
642
- stream.push.mockClear();
643
- stream.events.length = 0;
644
-
645
- bridge.handleEvent({
646
- type: "content_block_stop",
647
- index: 0,
648
- });
649
-
650
- const event = stream.events[0] as any;
651
- expect(event.type).toBe("toolcall_end");
652
- // Arguments should be the raw string since JSON parse failed
653
- expect(event.toolCall.arguments).toBe("not valid json at all");
654
- });
655
-
656
- it("getOutput().stopReason is toolUse when stop_reason is tool_use", () => {
657
- const bridge = createBridgeWithStart();
658
- bridge.handleEvent({
659
- type: "content_block_start",
660
- index: 0,
661
- content_block: { type: "tool_use", id: "toolu_01", name: "Read" },
662
- });
663
- bridge.handleEvent({
664
- type: "content_block_delta",
665
- index: 0,
666
- delta: {
667
- type: "input_json_delta",
668
- partial_json: '{"file_path": "/foo"}',
669
- },
670
- });
671
- bridge.handleEvent({ type: "content_block_stop", index: 0 });
672
-
673
- bridge.handleEvent({
674
- type: "message_delta",
675
- delta: { stop_reason: "tool_use" },
676
- usage: { output_tokens: 10 },
677
- });
678
- bridge.handleEvent({ type: "message_stop" });
679
-
680
- // Done is now pushed by provider, not event bridge. Check output state.
681
- expect(bridge.getOutput().stopReason).toBe("toolUse");
682
- });
683
-
684
- it("output.content includes ToolCall objects alongside TextContent", () => {
685
- const bridge = createBridgeWithStart();
686
-
687
- // Text block
688
- bridge.handleEvent({
689
- type: "content_block_start",
690
- index: 0,
691
- content_block: { type: "text", text: "" },
692
- });
693
- bridge.handleEvent({
694
- type: "content_block_delta",
695
- index: 0,
696
- delta: { type: "text_delta", text: "Reading file..." },
697
- });
698
- bridge.handleEvent({ type: "content_block_stop", index: 0 });
699
-
700
- // Tool block
701
- bridge.handleEvent({
702
- type: "content_block_start",
703
- index: 1,
704
- content_block: { type: "tool_use", id: "toolu_01", name: "Read" },
705
- });
706
- bridge.handleEvent({
707
- type: "content_block_delta",
708
- index: 1,
709
- delta: {
710
- type: "input_json_delta",
711
- partial_json: '{"file_path": "/test.ts"}',
712
- },
713
- });
714
- bridge.handleEvent({ type: "content_block_stop", index: 1 });
715
-
716
- const output = bridge.getOutput();
717
- expect(output.content).toHaveLength(2);
718
- expect(output.content[0]).toEqual({
719
- type: "text",
720
- text: "Reading file...",
721
- });
722
- expect(output.content[1]).toEqual({
723
- type: "toolCall",
724
- id: "toolu_01",
725
- name: "read",
726
- arguments: { path: "/test.ts" },
727
- });
728
- });
729
- });
730
-
731
- describe("thinking and other content block types", () => {
732
- it("emits thinking_start for thinking blocks", () => {
733
- const bridge = createBridgeWithStart();
734
-
735
- bridge.handleEvent({
736
- type: "content_block_start",
737
- index: 0,
738
- content_block: { type: "thinking" },
739
- });
740
-
741
- expect(stream.push).toHaveBeenCalledWith(
742
- expect.objectContaining({
743
- type: "thinking_start",
744
- contentIndex: 0,
745
- }),
746
- );
747
- });
748
-
749
- it("emits thinking_delta for thinking content", () => {
750
- const bridge = createBridgeWithStart();
751
-
752
- bridge.handleEvent({
753
- type: "content_block_start",
754
- index: 0,
755
- content_block: { type: "thinking" },
756
- });
757
- stream.push.mockClear();
758
-
759
- bridge.handleEvent({
760
- type: "content_block_delta",
761
- index: 0,
762
- delta: { type: "thinking_delta", thinking: "Let me think..." },
763
- });
764
-
765
- expect(stream.push).toHaveBeenCalledWith(
766
- expect.objectContaining({
767
- type: "thinking_delta",
768
- contentIndex: 0,
769
- delta: "Let me think...",
770
- }),
771
- );
772
- });
773
-
774
- it("accumulates thinking text across multiple deltas", () => {
775
- const bridge = createBridgeWithStart();
776
-
777
- bridge.handleEvent({
778
- type: "content_block_start",
779
- index: 0,
780
- content_block: { type: "thinking" },
781
- });
782
- bridge.handleEvent({
783
- type: "content_block_delta",
784
- index: 0,
785
- delta: { type: "thinking_delta", thinking: "First thought. " },
786
- });
787
- bridge.handleEvent({
788
- type: "content_block_delta",
789
- index: 0,
790
- delta: { type: "thinking_delta", thinking: "Second thought." },
791
- });
792
-
793
- const output = bridge.getOutput();
794
- const thinkingBlock = output.content[0] as any;
795
- expect(thinkingBlock.thinking).toBe("First thought. Second thought.");
796
- });
797
-
798
- it("emits thinking_end for thinking block stop", () => {
799
- const bridge = createBridgeWithStart();
800
-
801
- bridge.handleEvent({
802
- type: "content_block_start",
803
- index: 0,
804
- content_block: { type: "thinking" },
805
- });
806
- bridge.handleEvent({
807
- type: "content_block_delta",
808
- index: 0,
809
- delta: { type: "thinking_delta", thinking: "reasoning here" },
810
- });
811
- stream.push.mockClear();
812
-
813
- bridge.handleEvent({
814
- type: "content_block_stop",
815
- index: 0,
816
- });
817
-
818
- expect(stream.push).toHaveBeenCalledWith(
819
- expect.objectContaining({
820
- type: "thinking_end",
821
- contentIndex: 0,
822
- content: "reasoning here",
823
- }),
824
- );
825
- });
826
-
827
- it("accumulates signature_delta on thinking block thinkingSignature", () => {
828
- const bridge = createBridgeWithStart();
829
-
830
- bridge.handleEvent({
831
- type: "content_block_start",
832
- index: 0,
833
- content_block: { type: "thinking" },
834
- });
835
- bridge.handleEvent({
836
- type: "content_block_delta",
837
- index: 0,
838
- delta: { type: "thinking_delta", thinking: "some reasoning" },
839
- });
840
-
841
- // Signature arrives in multiple chunks
842
- bridge.handleEvent({
843
- type: "content_block_delta",
844
- index: 0,
845
- delta: { type: "signature_delta", signature: "sig_part1" },
846
- });
847
- bridge.handleEvent({
848
- type: "content_block_delta",
849
- index: 0,
850
- delta: { type: "signature_delta", signature: "sig_part2" },
851
- });
852
-
853
- const output = bridge.getOutput();
854
- const thinkingBlock = output.content[0] as any;
855
- expect(thinkingBlock.thinkingSignature).toBe("sig_part1sig_part2");
856
- });
857
-
858
- it("tracks thinking block interleaved with text block correctly", () => {
859
- const bridge = createBridgeWithStart();
860
-
861
- // Thinking block at index 0
862
- bridge.handleEvent({
863
- type: "content_block_start",
864
- index: 0,
865
- content_block: { type: "thinking" },
866
- });
867
- bridge.handleEvent({
868
- type: "content_block_delta",
869
- index: 0,
870
- delta: { type: "thinking_delta", thinking: "deep thought" },
871
- });
872
- bridge.handleEvent({
873
- type: "content_block_delta",
874
- index: 0,
875
- delta: { type: "signature_delta", signature: "sig123" },
876
- });
877
- bridge.handleEvent({
878
- type: "content_block_stop",
879
- index: 0,
880
- });
881
-
882
- // Text block at index 1
883
- bridge.handleEvent({
884
- type: "content_block_start",
885
- index: 1,
886
- content_block: { type: "text", text: "" },
887
- });
888
- bridge.handleEvent({
889
- type: "content_block_delta",
890
- index: 1,
891
- delta: { type: "text_delta", text: "The answer is 42." },
892
- });
893
- bridge.handleEvent({
894
- type: "content_block_stop",
895
- index: 1,
896
- });
897
-
898
- const output = bridge.getOutput();
899
- expect(output.content).toHaveLength(2);
900
-
901
- const thinking = output.content[0] as any;
902
- expect(thinking.type).toBe("thinking");
903
- expect(thinking.thinking).toBe("deep thought");
904
- expect(thinking.thinkingSignature).toBe("sig123");
905
-
906
- const text = output.content[1] as any;
907
- expect(text.type).toBe("text");
908
- expect(text.text).toBe("The answer is 42.");
909
-
910
- // Verify correct contentIndex values
911
- const thinkingStart = stream.events.find(
912
- (e: any) => e.type === "thinking_start",
913
- ) as any;
914
- expect(thinkingStart.contentIndex).toBe(0);
915
- const textStart = stream.events.find(
916
- (e: any) => e.type === "text_start",
917
- ) as any;
918
- expect(textStart.contentIndex).toBe(1);
919
- });
920
- });
921
-
922
- describe("MCP prefix stripping via mapDroidToolNameToPi", () => {
923
- it("strips mcp__custom-tools__ prefix from tool_use name in toolcall_start", () => {
924
- const bridge = createBridgeWithStart();
925
- bridge.handleEvent({
926
- type: "content_block_start",
927
- index: 0,
928
- content_block: {
929
- type: "tool_use",
930
- id: "toolu_mcp01",
931
- name: "mcp__custom-tools__foo",
932
- },
933
- });
934
-
935
- expect(stream.push).toHaveBeenCalledTimes(1);
936
- const event = stream.events[0] as any;
937
- expect(event.type).toBe("toolcall_start");
938
- // Tool name should be stripped from "mcp__custom-tools__foo" to "foo"
939
- expect(event.partial.content[0].name).toBe("foo");
940
- });
941
-
942
- it("strips mcp__custom-tools__ prefix in toolcall_end", () => {
943
- const bridge = createBridgeWithStart();
944
- bridge.handleEvent({
945
- type: "content_block_start",
946
- index: 0,
947
- content_block: {
948
- type: "tool_use",
949
- id: "toolu_mcp01",
950
- name: "mcp__custom-tools__foo",
951
- },
952
- });
953
- bridge.handleEvent({
954
- type: "content_block_delta",
955
- index: 0,
956
- delta: { type: "input_json_delta", partial_json: '{"target": "prod"}' },
957
- });
958
- stream.push.mockClear();
959
- stream.events.length = 0;
960
-
961
- bridge.handleEvent({
962
- type: "content_block_stop",
963
- index: 0,
964
- });
965
-
966
- expect(stream.push).toHaveBeenCalledTimes(1);
967
- const event = stream.events[0] as any;
968
- expect(event.type).toBe("toolcall_end");
969
- expect(event.toolCall.name).toBe("foo");
970
- // Custom tool args pass through unchanged (no argument renames for MCP tools)
971
- expect(event.toolCall.arguments).toEqual({ target: "prod" });
972
- });
973
-
974
- it("handles MCP-prefixed triage tool names", () => {
975
- const bridge = createBridgeWithStart();
976
- bridge.handleEvent({
977
- type: "content_block_start",
978
- index: 0,
979
- content_block: {
980
- type: "tool_use",
981
- id: "toolu_triage1",
982
- name: "mcp__custom-tools__fn_task_list",
983
- },
984
- });
985
- bridge.handleEvent({
986
- type: "content_block_stop",
987
- index: 0,
988
- });
989
-
990
- bridge.handleEvent({
991
- type: "content_block_start",
992
- index: 1,
993
- content_block: {
994
- type: "tool_use",
995
- id: "toolu_triage2",
996
- name: "mcp__custom-tools__fn_review_spec",
997
- },
998
- });
999
- bridge.handleEvent({
1000
- type: "content_block_stop",
1001
- index: 1,
1002
- });
1003
-
1004
- const output = bridge.getOutput();
1005
- const calls = output.content.filter((c: any) => c.type === "toolCall") as any[];
1006
- expect(calls.map((call) => call.name)).toEqual([
1007
- "fn_task_list",
1008
- "fn_review_spec",
1009
- ]);
1010
- expect(calls[0].arguments).toEqual({});
1011
- expect(calls[1].arguments).toEqual({});
1012
- });
1013
- });
1014
-
1015
- describe("internal Claude Code tools are filtered", () => {
1016
- it("skips ToolSearch tool_use blocks (not a pi-known tool)", () => {
1017
- const bridge = createBridgeWithStart();
1018
- bridge.handleEvent({
1019
- type: "content_block_start",
1020
- index: 0,
1021
- content_block: {
1022
- type: "tool_use",
1023
- id: "toolu_ts01",
1024
- name: "ToolSearch",
1025
- },
1026
- });
1027
-
1028
- // No toolcall_start should be emitted
1029
- expect(stream.push).not.toHaveBeenCalled();
1030
- // No content added to output
1031
- expect(bridge.getOutput().content).toHaveLength(0);
1032
- });
1033
-
1034
- it("skips Task, Agent, and other internal tools", () => {
1035
- const bridge = createBridgeWithStart();
1036
- for (const internalTool of [
1037
- "Task",
1038
- "TaskOutput",
1039
- "Agent",
1040
- "WebSearch",
1041
- "WebFetch",
1042
- "NotebookEdit",
1043
- ]) {
1044
- bridge.handleEvent({
1045
- type: "content_block_start",
1046
- index: 0,
1047
- content_block: {
1048
- type: "tool_use",
1049
- id: `toolu_${internalTool}`,
1050
- name: internalTool,
1051
- },
1052
- });
1053
- }
1054
- expect(stream.push).not.toHaveBeenCalled();
1055
- });
1056
-
1057
- it("allows built-in tools (Read, Write, etc.)", () => {
1058
- const bridge = createBridgeWithStart();
1059
- bridge.handleEvent({
1060
- type: "content_block_start",
1061
- index: 0,
1062
- content_block: { type: "tool_use", id: "toolu_read01", name: "Read" },
1063
- });
1064
- expect(stream.push).toHaveBeenCalledTimes(1);
1065
- expect((stream.events[0] as any).type).toBe("toolcall_start");
1066
- });
1067
-
1068
- it("silently drops deltas and stop events for filtered tools", () => {
1069
- const bridge = createBridgeWithStart();
1070
- // ToolSearch start (filtered)
1071
- bridge.handleEvent({
1072
- type: "content_block_start",
1073
- index: 0,
1074
- content_block: {
1075
- type: "tool_use",
1076
- id: "toolu_ts01",
1077
- name: "ToolSearch",
1078
- },
1079
- });
1080
- // Delta for filtered tool (should be ignored)
1081
- bridge.handleEvent({
1082
- type: "content_block_delta",
1083
- index: 0,
1084
- delta: { type: "input_json_delta", partial_json: '{"query":"test"}' },
1085
- });
1086
- // Stop for filtered tool (should be ignored)
1087
- bridge.handleEvent({
1088
- type: "content_block_stop",
1089
- index: 0,
1090
- });
1091
- // Nothing emitted
1092
- expect(stream.push).not.toHaveBeenCalled();
1093
- });
1094
- });
1095
-
1096
- describe("unknown event types", () => {
1097
- it("silently ignores unknown event types (after start)", () => {
1098
- const bridge = createBridgeWithStart();
1099
- bridge.handleEvent({
1100
- type: "some_unknown_event" as any,
1101
- });
1102
- expect(stream.push).not.toHaveBeenCalled();
1103
- });
1104
- });
1105
-
1106
- describe("complete text streaming sequence", () => {
1107
- it("produces correct pi event sequence for a full conversation turn", () => {
1108
- const bridge = createEventBridge(stream as any, model as any);
1109
-
1110
- // 1. message_start
1111
- bridge.handleEvent({
1112
- type: "message_start",
1113
- message: {
1114
- usage: {
1115
- input_tokens: 150,
1116
- output_tokens: 0,
1117
- cache_read_input_tokens: 30,
1118
- cache_creation_input_tokens: 10,
1119
- },
1120
- },
1121
- });
1122
-
1123
- // 2. content_block_start
1124
- bridge.handleEvent({
1125
- type: "content_block_start",
1126
- index: 0,
1127
- content_block: { type: "text", text: "" },
1128
- });
1129
-
1130
- // 3. content_block_delta x2
1131
- bridge.handleEvent({
1132
- type: "content_block_delta",
1133
- index: 0,
1134
- delta: { type: "text_delta", text: "Hello" },
1135
- });
1136
- bridge.handleEvent({
1137
- type: "content_block_delta",
1138
- index: 0,
1139
- delta: { type: "text_delta", text: " world" },
1140
- });
1141
-
1142
- // 4. content_block_stop
1143
- bridge.handleEvent({
1144
- type: "content_block_stop",
1145
- index: 0,
1146
- });
1147
-
1148
- // 5. message_delta
1149
- bridge.handleEvent({
1150
- type: "message_delta",
1151
- delta: { stop_reason: "end_turn" },
1152
- usage: { output_tokens: 5 },
1153
- });
1154
-
1155
- // 6. message_stop
1156
- bridge.handleEvent({
1157
- type: "message_stop",
1158
- });
1159
-
1160
- // Verify event sequence: start, text_start, text_delta x2, text_end (no done — provider pushes it)
1161
- expect(stream.events).toHaveLength(5);
1162
- expect((stream.events[0] as any).type).toBe("start");
1163
- expect((stream.events[1] as any).type).toBe("text_start");
1164
- expect((stream.events[2] as any).type).toBe("text_delta");
1165
- expect((stream.events[2] as any).delta).toBe("Hello");
1166
- expect((stream.events[3] as any).type).toBe("text_delta");
1167
- expect((stream.events[3] as any).delta).toBe(" world");
1168
- expect((stream.events[4] as any).content).toBe("Hello world");
1169
-
1170
- // Output fully populated for provider to read via getOutput()
1171
- const output = bridge.getOutput();
1172
- expect(output.role).toBe("assistant");
1173
- expect(output.content).toEqual([{ type: "text", text: "Hello world" }]);
1174
- expect(output.usage.input).toBe(150);
1175
- expect(output.usage.output).toBe(5);
1176
- expect(output.stopReason).toBe("stop");
1177
-
1178
- expect(stream.end).not.toHaveBeenCalled();
1179
-
1180
- expect(calculateCost).toHaveBeenCalled();
1181
- });
1182
- });
1183
-
1184
- describe("output initialization", () => {
1185
- it("initializes output with correct defaults", () => {
1186
- const bridge = createEventBridge(stream as any, model as any);
1187
- const output = bridge.getOutput();
1188
-
1189
- expect(output.role).toBe("assistant");
1190
- expect(output.content).toEqual([]);
1191
- expect(output.api).toBe("droid-cli");
1192
- expect(output.provider).toBe("anthropic");
1193
- expect(output.model).toBe("claude-sonnet-4-5-20250929");
1194
- expect(output.stopReason).toBe("stop");
1195
- expect(output.usage.input).toBe(0);
1196
- expect(output.usage.output).toBe(0);
1197
- expect(output.usage.cacheRead).toBe(0);
1198
- expect(output.usage.cacheWrite).toBe(0);
1199
- expect(output.usage.totalTokens).toBe(0);
1200
- expect(output.usage.cost.total).toBe(0);
1201
- expect(typeof output.timestamp).toBe("number");
1202
- });
1203
- });
1204
-
1205
- describe("stopReason mapping (provider reads via getOutput)", () => {
1206
- it("stopReason is toolUse for tool_use", () => {
1207
- const bridge = createEventBridge(stream as any, model as any);
1208
- bridge.handleEvent({
1209
- type: "message_delta",
1210
- delta: { stop_reason: "tool_use" },
1211
- });
1212
- bridge.handleEvent({ type: "message_stop" });
1213
-
1214
- expect(bridge.getOutput().stopReason).toBe("toolUse");
1215
- });
1216
-
1217
- it("stopReason is length for max_tokens", () => {
1218
- const bridge = createEventBridge(stream as any, model as any);
1219
- bridge.handleEvent({
1220
- type: "message_delta",
1221
- delta: { stop_reason: "max_tokens" },
1222
- });
1223
- bridge.handleEvent({ type: "message_stop" });
1224
-
1225
- expect(bridge.getOutput().stopReason).toBe("length");
1226
- });
1227
- });
1228
-
1229
- describe("delta without matching block", () => {
1230
- it("handles text_delta arriving with no matching block (index mismatch)", () => {
1231
- const bridge = createBridgeWithStart();
1232
- // Send a delta for index 5 without any block_start
1233
- expect(() => {
1234
- bridge.handleEvent({
1235
- type: "content_block_delta",
1236
- index: 5,
1237
- delta: { type: "text_delta", text: "orphan text" },
1238
- });
1239
- }).not.toThrow();
1240
- // No event should be pushed for the orphan delta
1241
- expect(stream.push).not.toHaveBeenCalled();
1242
- });
1243
-
1244
- it("handles input_json_delta arriving with no matching block", () => {
1245
- const bridge = createBridgeWithStart();
1246
- expect(() => {
1247
- bridge.handleEvent({
1248
- type: "content_block_delta",
1249
- index: 99,
1250
- delta: { type: "input_json_delta", partial_json: '{"key":"val"}' },
1251
- });
1252
- }).not.toThrow();
1253
- expect(stream.push).not.toHaveBeenCalled();
1254
- });
1255
-
1256
- it("handles thinking_delta arriving with no matching block", () => {
1257
- const bridge = createBridgeWithStart();
1258
- expect(() => {
1259
- bridge.handleEvent({
1260
- type: "content_block_delta",
1261
- index: 42,
1262
- delta: { type: "thinking_delta", thinking: "orphan thought" },
1263
- });
1264
- }).not.toThrow();
1265
- expect(stream.push).not.toHaveBeenCalled();
1266
- });
1267
-
1268
- it("handles signature_delta arriving with no matching block", () => {
1269
- const bridge = createBridgeWithStart();
1270
- expect(() => {
1271
- bridge.handleEvent({
1272
- type: "content_block_delta",
1273
- index: 7,
1274
- delta: { type: "signature_delta", signature: "sig_orphan" },
1275
- });
1276
- }).not.toThrow();
1277
- expect(stream.push).not.toHaveBeenCalled();
1278
- });
1279
-
1280
- it("handles content_block_stop arriving with no matching block", () => {
1281
- const bridge = createBridgeWithStart();
1282
- expect(() => {
1283
- bridge.handleEvent({
1284
- type: "content_block_stop",
1285
- index: 10,
1286
- });
1287
- }).not.toThrow();
1288
- expect(stream.push).not.toHaveBeenCalled();
1289
- });
1290
- });
1291
-
1292
- describe("unknown content block type in content_block_start", () => {
1293
- it("silently ignores unknown content block types", () => {
1294
- const bridge = createBridgeWithStart();
1295
- bridge.handleEvent({
1296
- type: "content_block_start",
1297
- index: 0,
1298
- content_block: { type: "some_new_type" as any },
1299
- });
1300
- // No event pushed and no crash
1301
- expect(stream.push).not.toHaveBeenCalled();
1302
- expect(bridge.getOutput().content).toHaveLength(0);
1303
- });
1304
- });
1305
-
1306
- describe("message_delta without usage", () => {
1307
- it("handles message_delta with stop_reason but no usage", () => {
1308
- const bridge = createBridgeWithStart();
1309
- bridge.handleEvent({
1310
- type: "message_delta",
1311
- delta: { stop_reason: "end_turn" },
1312
- });
1313
- expect(bridge.getOutput().stopReason).toBe("stop");
1314
- // Usage should remain at defaults
1315
- expect(bridge.getOutput().usage.output).toBe(0);
1316
- });
5
+ describe("droid-cli event-bridge shim", () => {
6
+ it("re-exports createEventBridge from the droid runtime plugin", () => {
7
+ expect(shimCreateEventBridge).toBe(pluginCreateEventBridge);
1317
8
  });
1318
9
  });