ralph-cli-sandboxed 0.6.5 → 0.7.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 (38) hide show
  1. package/LICENSE +191 -0
  2. package/README.md +1 -1
  3. package/dist/commands/action.js +6 -8
  4. package/dist/commands/ask.d.ts +6 -0
  5. package/dist/commands/ask.js +140 -0
  6. package/dist/commands/branch.js +8 -4
  7. package/dist/commands/chat.js +11 -8
  8. package/dist/commands/docker.js +19 -4
  9. package/dist/commands/fix-config.js +0 -41
  10. package/dist/commands/help.js +10 -0
  11. package/dist/commands/run.js +9 -9
  12. package/dist/config/languages.json +5 -3
  13. package/dist/index.js +2 -0
  14. package/dist/providers/telegram.js +1 -1
  15. package/dist/responders/claude-code-responder.js +1 -0
  16. package/dist/responders/cli-responder.js +1 -0
  17. package/dist/responders/llm-responder.js +1 -1
  18. package/dist/templates/macos-scripts.js +18 -18
  19. package/dist/tui/components/JsonSnippetEditor.js +7 -7
  20. package/dist/tui/components/KeyValueEditor.js +5 -1
  21. package/dist/tui/components/LLMProvidersEditor.js +7 -9
  22. package/dist/tui/components/Preview.js +1 -1
  23. package/dist/tui/components/SectionNav.js +18 -2
  24. package/dist/utils/chat-client.js +1 -0
  25. package/dist/utils/config.d.ts +1 -0
  26. package/dist/utils/config.js +3 -1
  27. package/dist/utils/config.test.d.ts +1 -0
  28. package/dist/utils/config.test.js +424 -0
  29. package/dist/utils/notification.js +1 -1
  30. package/dist/utils/prd-validator.js +16 -4
  31. package/dist/utils/prd-validator.test.d.ts +1 -0
  32. package/dist/utils/prd-validator.test.js +1095 -0
  33. package/dist/utils/responder.js +4 -1
  34. package/dist/utils/stream-json.test.d.ts +1 -0
  35. package/dist/utils/stream-json.test.js +1007 -0
  36. package/docs/DOCKER.md +14 -0
  37. package/docs/PRD-GENERATOR.md +15 -0
  38. package/package.json +16 -13
@@ -0,0 +1,1007 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { ClaudeStreamParser, GeminiStreamParser, OpenCodeStreamParser, CodexStreamParser, GooseStreamParser, AiderStreamParser, DefaultStreamParser, getStreamJsonParser, } from "./stream-json.js";
3
+ // ─── ClaudeStreamParser ─────────────────────────────────────────────
4
+ describe("ClaudeStreamParser", () => {
5
+ const parser = new ClaudeStreamParser();
6
+ it("parses text_delta events", () => {
7
+ const line = JSON.stringify({
8
+ type: "content_block_delta",
9
+ delta: { type: "text_delta", text: "Hello" },
10
+ });
11
+ expect(parser.parseStreamJsonLine(line)).toBe("Hello");
12
+ });
13
+ it("ignores input_json_delta events", () => {
14
+ const line = JSON.stringify({
15
+ type: "content_block_delta",
16
+ delta: { type: "input_json_delta", partial_json: '{"key":' },
17
+ });
18
+ expect(parser.parseStreamJsonLine(line)).toBe("");
19
+ });
20
+ it("parses text events", () => {
21
+ const line = JSON.stringify({ type: "text", text: "World" });
22
+ expect(parser.parseStreamJsonLine(line)).toBe("World");
23
+ });
24
+ it("parses tool_use content_block_start", () => {
25
+ const line = JSON.stringify({
26
+ type: "content_block_start",
27
+ content_block: { type: "tool_use", name: "read_file" },
28
+ });
29
+ expect(parser.parseStreamJsonLine(line)).toContain("read_file");
30
+ });
31
+ it("parses text content_block_start", () => {
32
+ const line = JSON.stringify({
33
+ type: "content_block_start",
34
+ content_block: { type: "text", text: "Starting" },
35
+ });
36
+ expect(parser.parseStreamJsonLine(line)).toBe("Starting");
37
+ });
38
+ it("parses tool_result events", () => {
39
+ const line = JSON.stringify({ type: "tool_result", content: "file contents here" });
40
+ expect(parser.parseStreamJsonLine(line)).toContain("file contents here");
41
+ });
42
+ it("parses assistant messages with text blocks", () => {
43
+ const line = JSON.stringify({
44
+ type: "assistant",
45
+ content: [{ type: "text", text: "I will help" }],
46
+ });
47
+ expect(parser.parseStreamJsonLine(line)).toBe("I will help");
48
+ });
49
+ it("parses assistant messages with tool_use blocks", () => {
50
+ const line = JSON.stringify({
51
+ type: "assistant",
52
+ content: [{ type: "tool_use", name: "bash", input: { command: "ls" } }],
53
+ });
54
+ const result = parser.parseStreamJsonLine(line);
55
+ expect(result).toContain("bash");
56
+ });
57
+ it("parses file operations", () => {
58
+ expect(parser.parseStreamJsonLine(JSON.stringify({ type: "file_edit", path: "src/main.ts" }))).toContain("src/main.ts");
59
+ expect(parser.parseStreamJsonLine(JSON.stringify({ type: "file_read", path: "README.md" }))).toContain("README.md");
60
+ });
61
+ it("parses bash/command events", () => {
62
+ const line = JSON.stringify({ type: "bash", command: "npm test" });
63
+ expect(parser.parseStreamJsonLine(line)).toContain("npm test");
64
+ });
65
+ it("parses error events", () => {
66
+ const line = JSON.stringify({ type: "error", error: { message: "Rate limited" } });
67
+ expect(parser.parseStreamJsonLine(line)).toContain("Rate limited");
68
+ });
69
+ it("returns empty string for invalid JSON", () => {
70
+ expect(parser.parseStreamJsonLine("not json")).toBe("");
71
+ });
72
+ it("handles message lifecycle events", () => {
73
+ expect(parser.parseStreamJsonLine(JSON.stringify({ type: "message_start" }))).toBe("\n");
74
+ expect(parser.parseStreamJsonLine(JSON.stringify({ type: "message_stop" }))).toBe("\n");
75
+ expect(parser.parseStreamJsonLine(JSON.stringify({ type: "message_delta", delta: { stop_reason: "end_turn" } }))).toContain("end_turn");
76
+ });
77
+ it("parses system messages", () => {
78
+ const line = JSON.stringify({ type: "system", message: "Initializing" });
79
+ expect(parser.parseStreamJsonLine(line)).toContain("Initializing");
80
+ });
81
+ it("falls back to text/content/message fields", () => {
82
+ expect(parser.parseStreamJsonLine(JSON.stringify({ type: "unknown", text: "fallback" }))).toBe("fallback");
83
+ expect(parser.parseStreamJsonLine(JSON.stringify({ type: "unknown", content: "fallback2" }))).toBe("fallback2");
84
+ });
85
+ // --- new edge case tests ---
86
+ it("returns empty for content_block_stop", () => {
87
+ const line = JSON.stringify({ type: "content_block_stop" });
88
+ expect(parser.parseStreamJsonLine(line)).toBe("");
89
+ });
90
+ it("returns empty for user events", () => {
91
+ const line = JSON.stringify({ type: "user", content: "user message" });
92
+ expect(parser.parseStreamJsonLine(line)).toBe("");
93
+ });
94
+ it("handles content_block_delta with missing delta text", () => {
95
+ const line = JSON.stringify({
96
+ type: "content_block_delta",
97
+ delta: { type: "text_delta" },
98
+ });
99
+ expect(parser.parseStreamJsonLine(line)).toBe("");
100
+ });
101
+ it("handles content_block_delta with unknown delta type", () => {
102
+ const line = JSON.stringify({
103
+ type: "content_block_delta",
104
+ delta: { type: "custom_delta", text: "hi" },
105
+ });
106
+ // Falls through to delta.text fallback
107
+ expect(parser.parseStreamJsonLine(line)).toBe("hi");
108
+ });
109
+ it("handles text event with empty text", () => {
110
+ const line = JSON.stringify({ type: "text", text: "" });
111
+ expect(parser.parseStreamJsonLine(line)).toBe("");
112
+ });
113
+ it("handles content_block_start with unknown block type", () => {
114
+ const line = JSON.stringify({
115
+ type: "content_block_start",
116
+ content_block: { type: "image", url: "http://example.com" },
117
+ });
118
+ expect(parser.parseStreamJsonLine(line)).toBe("");
119
+ });
120
+ it("handles tool_result with output field instead of content", () => {
121
+ const line = JSON.stringify({ type: "tool_result", output: "tool output here" });
122
+ expect(parser.parseStreamJsonLine(line)).toContain("tool output here");
123
+ });
124
+ it("handles tool_use content_block_start without name", () => {
125
+ const line = JSON.stringify({
126
+ type: "content_block_start",
127
+ content_block: { type: "tool_use" },
128
+ });
129
+ expect(parser.parseStreamJsonLine(line)).toContain("unknown");
130
+ });
131
+ it("handles assistant messages from message.content path", () => {
132
+ const line = JSON.stringify({
133
+ type: "assistant",
134
+ message: { content: [{ type: "text", text: "From message" }] },
135
+ });
136
+ expect(parser.parseStreamJsonLine(line)).toBe("From message");
137
+ });
138
+ it("handles assistant messages with multiple content blocks", () => {
139
+ const line = JSON.stringify({
140
+ type: "assistant",
141
+ content: [
142
+ { type: "text", text: "First " },
143
+ { type: "text", text: "Second" },
144
+ ],
145
+ });
146
+ expect(parser.parseStreamJsonLine(line)).toBe("First Second");
147
+ });
148
+ it("handles assistant tool_use without input", () => {
149
+ const line = JSON.stringify({
150
+ type: "assistant",
151
+ content: [{ type: "tool_use", name: "read_file" }],
152
+ });
153
+ const result = parser.parseStreamJsonLine(line);
154
+ expect(result).toContain("read_file");
155
+ });
156
+ it("handles file_write event", () => {
157
+ const line = JSON.stringify({ type: "file_write", path: "output.txt" });
158
+ expect(parser.parseStreamJsonLine(line)).toContain("output.txt");
159
+ });
160
+ it("handles file_edit with file field instead of path", () => {
161
+ const line = JSON.stringify({ type: "file_edit", file: "alt.ts" });
162
+ expect(parser.parseStreamJsonLine(line)).toContain("alt.ts");
163
+ });
164
+ it("handles file_read with file field instead of path", () => {
165
+ const line = JSON.stringify({ type: "file_read", file: "data.json" });
166
+ expect(parser.parseStreamJsonLine(line)).toContain("data.json");
167
+ });
168
+ it("handles command event", () => {
169
+ const line = JSON.stringify({ type: "command", command: "git status" });
170
+ expect(parser.parseStreamJsonLine(line)).toContain("git status");
171
+ });
172
+ it("handles bash event with content field instead of command", () => {
173
+ const line = JSON.stringify({ type: "bash", content: "echo hello" });
174
+ expect(parser.parseStreamJsonLine(line)).toContain("echo hello");
175
+ });
176
+ it("handles bash_output event", () => {
177
+ const line = JSON.stringify({ type: "bash_output", output: "hello world" });
178
+ expect(parser.parseStreamJsonLine(line)).toContain("hello world");
179
+ });
180
+ it("handles command_output event", () => {
181
+ const line = JSON.stringify({ type: "command_output", content: "output data" });
182
+ expect(parser.parseStreamJsonLine(line)).toContain("output data");
183
+ });
184
+ it("handles result event", () => {
185
+ const line = JSON.stringify({ type: "result", result: { success: true } });
186
+ expect(parser.parseStreamJsonLine(line)).toContain("success");
187
+ });
188
+ it("handles result event with undefined result", () => {
189
+ const line = JSON.stringify({ type: "result" });
190
+ expect(parser.parseStreamJsonLine(line)).toBe("");
191
+ });
192
+ it("handles error event without message (raw error object)", () => {
193
+ const line = JSON.stringify({ type: "error", error: { code: 500, detail: "server" } });
194
+ const result = parser.parseStreamJsonLine(line);
195
+ expect(result).toContain("Error");
196
+ expect(result).toContain("500");
197
+ });
198
+ it("handles message_delta without stop_reason", () => {
199
+ const line = JSON.stringify({ type: "message_delta", delta: {} });
200
+ expect(parser.parseStreamJsonLine(line)).toBe("");
201
+ });
202
+ it("handles system event without message", () => {
203
+ const line = JSON.stringify({ type: "system" });
204
+ expect(parser.parseStreamJsonLine(line)).toBe("");
205
+ });
206
+ it("falls back to output field for unknown types", () => {
207
+ const line = JSON.stringify({ type: "custom", output: "custom output" });
208
+ expect(parser.parseStreamJsonLine(line)).toBe("custom output");
209
+ });
210
+ it("returns empty for unknown type with no fallback fields", () => {
211
+ const line = JSON.stringify({ type: "unknown", data: 42 });
212
+ expect(parser.parseStreamJsonLine(line)).toBe("");
213
+ });
214
+ it("handles empty JSON object", () => {
215
+ expect(parser.parseStreamJsonLine("{}")).toBe("");
216
+ });
217
+ it("handles JSON array (not an object)", () => {
218
+ expect(parser.parseStreamJsonLine("[]")).toBe("");
219
+ });
220
+ it("handles truncation of long tool results", () => {
221
+ const longOutput = "x".repeat(1000);
222
+ const line = JSON.stringify({ type: "tool_result", content: longOutput });
223
+ const result = parser.parseStreamJsonLine(line);
224
+ expect(result).toContain("truncated");
225
+ expect(result.length).toBeLessThan(1000);
226
+ });
227
+ });
228
+ // ─── GeminiStreamParser ─────────────────────────────────────────────
229
+ describe("GeminiStreamParser", () => {
230
+ const parser = new GeminiStreamParser();
231
+ it("parses initialization events", () => {
232
+ const line = JSON.stringify({ type: "initialization", model: "gemini-pro" });
233
+ expect(parser.parseStreamJsonLine(line)).toContain("gemini-pro");
234
+ });
235
+ it("parses assistant messages", () => {
236
+ const line = JSON.stringify({
237
+ type: "messages",
238
+ messages: [{ role: "assistant", content: "Hello from Gemini" }],
239
+ });
240
+ expect(parser.parseStreamJsonLine(line)).toBe("Hello from Gemini");
241
+ });
242
+ it("parses model role messages", () => {
243
+ const line = JSON.stringify({
244
+ type: "messages",
245
+ messages: [{ role: "model", content: [{ type: "text", text: "Model says" }] }],
246
+ });
247
+ expect(parser.parseStreamJsonLine(line)).toBe("Model says");
248
+ });
249
+ it("parses tool events", () => {
250
+ const line = JSON.stringify({
251
+ type: "tools",
252
+ tools: [{ name: "search", output: "Results here" }],
253
+ });
254
+ const result = parser.parseStreamJsonLine(line);
255
+ expect(result).toContain("search");
256
+ expect(result).toContain("Results here");
257
+ });
258
+ it("parses response events", () => {
259
+ const line = JSON.stringify({ type: "response", text: "Final answer" });
260
+ expect(parser.parseStreamJsonLine(line)).toBe("Final answer");
261
+ });
262
+ it("returns empty for invalid JSON", () => {
263
+ expect(parser.parseStreamJsonLine("bad")).toBe("");
264
+ });
265
+ // --- new edge case tests ---
266
+ it("handles initialization without model", () => {
267
+ const line = JSON.stringify({ type: "initialization" });
268
+ expect(parser.parseStreamJsonLine(line)).toBe("");
269
+ });
270
+ it("ignores user role messages", () => {
271
+ const line = JSON.stringify({
272
+ type: "messages",
273
+ messages: [{ role: "user", content: "User input" }],
274
+ });
275
+ expect(parser.parseStreamJsonLine(line)).toBe("");
276
+ });
277
+ it("handles messages event without messages array", () => {
278
+ const line = JSON.stringify({ type: "messages" });
279
+ expect(parser.parseStreamJsonLine(line)).toBe("");
280
+ });
281
+ it("handles messages with empty messages array", () => {
282
+ const line = JSON.stringify({ type: "messages", messages: [] });
283
+ expect(parser.parseStreamJsonLine(line)).toBe("");
284
+ });
285
+ it("handles multiple assistant messages", () => {
286
+ const line = JSON.stringify({
287
+ type: "messages",
288
+ messages: [
289
+ { role: "assistant", content: "First " },
290
+ { role: "assistant", content: "Second" },
291
+ ],
292
+ });
293
+ expect(parser.parseStreamJsonLine(line)).toBe("First Second");
294
+ });
295
+ it("handles tools with input", () => {
296
+ const line = JSON.stringify({
297
+ type: "tools",
298
+ tools: [{ name: "calc", input: { expression: "2+2" } }],
299
+ });
300
+ const result = parser.parseStreamJsonLine(line);
301
+ expect(result).toContain("calc");
302
+ expect(result).toContain("2+2");
303
+ });
304
+ it("handles tools with result field instead of output", () => {
305
+ const line = JSON.stringify({
306
+ type: "tools",
307
+ tools: [{ name: "search", result: "Found it" }],
308
+ });
309
+ expect(parser.parseStreamJsonLine(line)).toContain("Found it");
310
+ });
311
+ it("handles tools event without tools array", () => {
312
+ const line = JSON.stringify({ type: "tools" });
313
+ expect(parser.parseStreamJsonLine(line)).toBe("");
314
+ });
315
+ it("handles empty tools array", () => {
316
+ const line = JSON.stringify({ type: "tools", tools: [] });
317
+ expect(parser.parseStreamJsonLine(line)).toBe("");
318
+ });
319
+ it("handles tool without name", () => {
320
+ const line = JSON.stringify({
321
+ type: "tools",
322
+ tools: [{ output: "anonymous tool result" }],
323
+ });
324
+ const result = parser.parseStreamJsonLine(line);
325
+ expect(result).toContain("anonymous tool result");
326
+ });
327
+ it("handles turn_complete event", () => {
328
+ const line = JSON.stringify({ type: "turn_complete" });
329
+ expect(parser.parseStreamJsonLine(line)).toBe("\n");
330
+ });
331
+ it("handles response with content field instead of text", () => {
332
+ const line = JSON.stringify({ type: "response", content: "Content response" });
333
+ expect(parser.parseStreamJsonLine(line)).toBe("Content response");
334
+ });
335
+ it("handles response with neither text nor content", () => {
336
+ const line = JSON.stringify({ type: "response" });
337
+ expect(parser.parseStreamJsonLine(line)).toBe("");
338
+ });
339
+ it("falls back to text field for unknown types", () => {
340
+ const line = JSON.stringify({ type: "custom_type", text: "custom text" });
341
+ expect(parser.parseStreamJsonLine(line)).toBe("custom text");
342
+ });
343
+ it("falls back to content field for unknown types", () => {
344
+ const line = JSON.stringify({ type: "custom_type", content: "custom content" });
345
+ expect(parser.parseStreamJsonLine(line)).toBe("custom content");
346
+ });
347
+ it("returns empty for unknown type with no fallback fields", () => {
348
+ const line = JSON.stringify({ type: "custom_type", data: 42 });
349
+ expect(parser.parseStreamJsonLine(line)).toBe("");
350
+ });
351
+ it("handles model content with mixed block types", () => {
352
+ const line = JSON.stringify({
353
+ type: "messages",
354
+ messages: [
355
+ {
356
+ role: "model",
357
+ content: [
358
+ { type: "text", text: "Text block" },
359
+ { type: "image", url: "http://example.com" },
360
+ { type: "text", text: " more text" },
361
+ ],
362
+ },
363
+ ],
364
+ });
365
+ expect(parser.parseStreamJsonLine(line)).toBe("Text block more text");
366
+ });
367
+ });
368
+ // ─── OpenCodeStreamParser ───────────────────────────────────────────
369
+ describe("OpenCodeStreamParser", () => {
370
+ const parser = new OpenCodeStreamParser();
371
+ it("parses step_start events", () => {
372
+ const line = JSON.stringify({ type: "step_start", step: "Planning" });
373
+ expect(parser.parseStreamJsonLine(line)).toContain("Planning");
374
+ });
375
+ it("parses tool_use events with part structure", () => {
376
+ const line = JSON.stringify({
377
+ type: "tool_use",
378
+ part: { type: "tool", tool: "file_read", title: "main.ts" },
379
+ });
380
+ const result = parser.parseStreamJsonLine(line);
381
+ expect(result).toContain("file_read");
382
+ expect(result).toContain("main.ts");
383
+ });
384
+ it("parses text events with part structure", () => {
385
+ const line = JSON.stringify({
386
+ type: "text",
387
+ part: { text: "Analyzing code" },
388
+ });
389
+ expect(parser.parseStreamJsonLine(line)).toBe("Analyzing code");
390
+ });
391
+ it("parses assistant_message events", () => {
392
+ const line = JSON.stringify({ type: "assistant_message", content: "Here is my analysis" });
393
+ expect(parser.parseStreamJsonLine(line)).toBe("Here is my analysis");
394
+ });
395
+ it("parses thinking events", () => {
396
+ const line = JSON.stringify({ type: "thinking", content: "Let me think" });
397
+ expect(parser.parseStreamJsonLine(line)).toContain("Let me think");
398
+ });
399
+ it("returns empty for invalid JSON", () => {
400
+ expect(parser.parseStreamJsonLine("{broken")).toBe("");
401
+ });
402
+ // --- new edge case tests ---
403
+ it("handles step_start with name field instead of step", () => {
404
+ const line = JSON.stringify({ type: "step_start", name: "Analysis" });
405
+ expect(parser.parseStreamJsonLine(line)).toContain("Analysis");
406
+ });
407
+ it("handles step_start without step or name", () => {
408
+ const line = JSON.stringify({ type: "step_start" });
409
+ expect(parser.parseStreamJsonLine(line)).toBe("\n");
410
+ });
411
+ it("returns empty for step_end", () => {
412
+ const line = JSON.stringify({ type: "step_end" });
413
+ expect(parser.parseStreamJsonLine(line)).toBe("");
414
+ });
415
+ it("returns empty for step_finish", () => {
416
+ const line = JSON.stringify({ type: "step_finish" });
417
+ expect(parser.parseStreamJsonLine(line)).toBe("");
418
+ });
419
+ it("handles tool_use without part", () => {
420
+ const line = JSON.stringify({ type: "tool_use" });
421
+ expect(parser.parseStreamJsonLine(line)).toBe("");
422
+ });
423
+ it("handles tool_use with non-tool part type", () => {
424
+ const line = JSON.stringify({
425
+ type: "tool_use",
426
+ part: { type: "text", text: "not a tool" },
427
+ });
428
+ expect(parser.parseStreamJsonLine(line)).toBe("");
429
+ });
430
+ it("handles tool_use with completed state and output", () => {
431
+ const line = JSON.stringify({
432
+ type: "tool_use",
433
+ part: {
434
+ type: "tool",
435
+ tool: "file_read",
436
+ state: { status: "completed", output: "file contents" },
437
+ },
438
+ });
439
+ const result = parser.parseStreamJsonLine(line);
440
+ expect(result).toContain("file_read");
441
+ expect(result).toContain("file contents");
442
+ });
443
+ it("handles tool event (direct tool invocation)", () => {
444
+ const line = JSON.stringify({ type: "tool", name: "search", input: { query: "test" } });
445
+ const result = parser.parseStreamJsonLine(line);
446
+ expect(result).toContain("search");
447
+ expect(result).toContain("test");
448
+ });
449
+ it("handles tool_call event", () => {
450
+ const line = JSON.stringify({ type: "tool_call", tool: "execute", args: "npm test" });
451
+ const result = parser.parseStreamJsonLine(line);
452
+ expect(result).toContain("execute");
453
+ });
454
+ it("handles tool_call with string input", () => {
455
+ const line = JSON.stringify({ type: "tool_call", name: "bash", input: "ls -la" });
456
+ const result = parser.parseStreamJsonLine(line);
457
+ expect(result).toContain("bash");
458
+ expect(result).toContain("ls -la");
459
+ });
460
+ it("handles tool_response event", () => {
461
+ const line = JSON.stringify({ type: "tool_response", output: "command output" });
462
+ expect(parser.parseStreamJsonLine(line)).toContain("command output");
463
+ });
464
+ it("handles model_response event", () => {
465
+ const line = JSON.stringify({ type: "model_response", content: "model says" });
466
+ expect(parser.parseStreamJsonLine(line)).toBe("model says");
467
+ });
468
+ it("handles assistant_message with text field", () => {
469
+ const line = JSON.stringify({ type: "assistant_message", text: "text field" });
470
+ expect(parser.parseStreamJsonLine(line)).toBe("text field");
471
+ });
472
+ it("handles assistant_message with content array", () => {
473
+ const line = JSON.stringify({
474
+ type: "assistant_message",
475
+ content: [{ type: "text", text: "part 1 " }, "part 2"],
476
+ });
477
+ expect(parser.parseStreamJsonLine(line)).toBe("part 1 part 2");
478
+ });
479
+ it("handles text event with direct text field", () => {
480
+ const line = JSON.stringify({ type: "text", text: "Direct text" });
481
+ expect(parser.parseStreamJsonLine(line)).toBe("Direct text");
482
+ });
483
+ it("handles text event without part or text", () => {
484
+ const line = JSON.stringify({ type: "text" });
485
+ expect(parser.parseStreamJsonLine(line)).toBe("");
486
+ });
487
+ it("handles reasoning event", () => {
488
+ const line = JSON.stringify({ type: "reasoning", text: "reasoning text" });
489
+ expect(parser.parseStreamJsonLine(line)).toContain("reasoning text");
490
+ });
491
+ it("handles thinking event without content or text", () => {
492
+ const line = JSON.stringify({ type: "thinking" });
493
+ expect(parser.parseStreamJsonLine(line)).toBe("");
494
+ });
495
+ it("handles done event", () => {
496
+ const line = JSON.stringify({ type: "done" });
497
+ expect(parser.parseStreamJsonLine(line)).toBe("\n");
498
+ });
499
+ it("handles complete event", () => {
500
+ const line = JSON.stringify({ type: "complete" });
501
+ expect(parser.parseStreamJsonLine(line)).toBe("\n");
502
+ });
503
+ it("falls back to text field for unknown type", () => {
504
+ const line = JSON.stringify({ type: "custom", text: "fallback" });
505
+ expect(parser.parseStreamJsonLine(line)).toBe("fallback");
506
+ });
507
+ it("returns empty for unknown type with no fallback", () => {
508
+ const line = JSON.stringify({ type: "custom", data: 123 });
509
+ expect(parser.parseStreamJsonLine(line)).toBe("");
510
+ });
511
+ });
512
+ // ─── CodexStreamParser ──────────────────────────────────────────────
513
+ describe("CodexStreamParser", () => {
514
+ const parser = new CodexStreamParser();
515
+ it("parses thread.started events", () => {
516
+ const line = JSON.stringify({ type: "thread.started", thread_id: "abc123" });
517
+ expect(parser.parseStreamJsonLine(line)).toContain("abc123");
518
+ });
519
+ it("parses item.started command_execution", () => {
520
+ const line = JSON.stringify({
521
+ type: "item.started",
522
+ item: { type: "command_execution", command: "ls -la" },
523
+ });
524
+ expect(parser.parseStreamJsonLine(line)).toContain("ls -la");
525
+ });
526
+ it("parses item.started file_change", () => {
527
+ const line = JSON.stringify({
528
+ type: "item.started",
529
+ item: { type: "file_change", path: "src/index.ts" },
530
+ });
531
+ expect(parser.parseStreamJsonLine(line)).toContain("src/index.ts");
532
+ });
533
+ it("parses item.completed agent_message", () => {
534
+ const line = JSON.stringify({
535
+ type: "item.completed",
536
+ item: { type: "agent_message", text: "Done editing" },
537
+ });
538
+ expect(parser.parseStreamJsonLine(line)).toBe("Done editing");
539
+ });
540
+ it("parses turn.completed with usage", () => {
541
+ const line = JSON.stringify({
542
+ type: "turn.completed",
543
+ usage: { input_tokens: 100, output_tokens: 50 },
544
+ });
545
+ const result = parser.parseStreamJsonLine(line);
546
+ expect(result).toContain("100");
547
+ expect(result).toContain("50");
548
+ });
549
+ it("parses turn.failed events", () => {
550
+ const line = JSON.stringify({ type: "turn.failed", error: "Timeout" });
551
+ expect(parser.parseStreamJsonLine(line)).toContain("Timeout");
552
+ });
553
+ it("parses item.failed events", () => {
554
+ const line = JSON.stringify({
555
+ type: "item.failed",
556
+ item: { type: "command_execution", error: "Permission denied" },
557
+ });
558
+ expect(parser.parseStreamJsonLine(line)).toContain("Permission denied");
559
+ });
560
+ // --- new edge case tests ---
561
+ it("handles thread.started without thread_id", () => {
562
+ const line = JSON.stringify({ type: "thread.started" });
563
+ expect(parser.parseStreamJsonLine(line)).toBe("");
564
+ });
565
+ it("handles turn.started event", () => {
566
+ const line = JSON.stringify({ type: "turn.started" });
567
+ expect(parser.parseStreamJsonLine(line)).toBe("\n");
568
+ });
569
+ it("handles turn.completed without usage", () => {
570
+ const line = JSON.stringify({ type: "turn.completed" });
571
+ expect(parser.parseStreamJsonLine(line)).toBe("\n");
572
+ });
573
+ it("handles turn.failed with message field", () => {
574
+ const line = JSON.stringify({ type: "turn.failed", message: "Rate limited" });
575
+ expect(parser.parseStreamJsonLine(line)).toContain("Rate limited");
576
+ });
577
+ it("handles turn.failed without error or message", () => {
578
+ const line = JSON.stringify({ type: "turn.failed" });
579
+ expect(parser.parseStreamJsonLine(line)).toContain("Turn failed");
580
+ });
581
+ it("handles item.started file_edit type", () => {
582
+ const line = JSON.stringify({
583
+ type: "item.started",
584
+ item: { type: "file_edit", path: "src/utils.ts" },
585
+ });
586
+ expect(parser.parseStreamJsonLine(line)).toContain("src/utils.ts");
587
+ });
588
+ it("handles item.started file_read type", () => {
589
+ const line = JSON.stringify({
590
+ type: "item.started",
591
+ item: { type: "file_read", path: "config.json" },
592
+ });
593
+ expect(parser.parseStreamJsonLine(line)).toContain("config.json");
594
+ });
595
+ it("handles item.started file_read with file field", () => {
596
+ const line = JSON.stringify({
597
+ type: "item.started",
598
+ item: { type: "file_read", file: "alt-path.ts" },
599
+ });
600
+ expect(parser.parseStreamJsonLine(line)).toContain("alt-path.ts");
601
+ });
602
+ it("handles item.started mcp_tool_call", () => {
603
+ const line = JSON.stringify({
604
+ type: "item.started",
605
+ item: { type: "mcp_tool_call", name: "custom_tool" },
606
+ });
607
+ expect(parser.parseStreamJsonLine(line)).toContain("custom_tool");
608
+ });
609
+ it("handles item.started tool_call", () => {
610
+ const line = JSON.stringify({
611
+ type: "item.started",
612
+ item: { type: "tool_call", tool: "search" },
613
+ });
614
+ expect(parser.parseStreamJsonLine(line)).toContain("search");
615
+ });
616
+ it("handles item.started web_search", () => {
617
+ const line = JSON.stringify({
618
+ type: "item.started",
619
+ item: { type: "web_search", query: "how to parse JSON" },
620
+ });
621
+ expect(parser.parseStreamJsonLine(line)).toContain("how to parse JSON");
622
+ });
623
+ it("handles item.started web_search without query", () => {
624
+ const line = JSON.stringify({
625
+ type: "item.started",
626
+ item: { type: "web_search" },
627
+ });
628
+ expect(parser.parseStreamJsonLine(line)).toContain("Web search");
629
+ });
630
+ it("handles item.started plan_update", () => {
631
+ const line = JSON.stringify({
632
+ type: "item.started",
633
+ item: { type: "plan_update" },
634
+ });
635
+ expect(parser.parseStreamJsonLine(line)).toContain("Plan update");
636
+ });
637
+ it("handles item.started with unknown item type", () => {
638
+ const line = JSON.stringify({
639
+ type: "item.started",
640
+ item: { type: "custom_action" },
641
+ });
642
+ expect(parser.parseStreamJsonLine(line)).toBe("");
643
+ });
644
+ it("handles item.started without item", () => {
645
+ const line = JSON.stringify({ type: "item.started" });
646
+ expect(parser.parseStreamJsonLine(line)).toBe("");
647
+ });
648
+ it("handles item.completed command_execution with output", () => {
649
+ const line = JSON.stringify({
650
+ type: "item.completed",
651
+ item: { type: "command_execution", output: "test passed" },
652
+ });
653
+ expect(parser.parseStreamJsonLine(line)).toContain("test passed");
654
+ });
655
+ it("handles item.completed reasoning with text", () => {
656
+ const line = JSON.stringify({
657
+ type: "item.completed",
658
+ item: { type: "reasoning", text: "I think we should..." },
659
+ });
660
+ const result = parser.parseStreamJsonLine(line);
661
+ expect(result).toContain("Thinking");
662
+ expect(result).toContain("I think we should...");
663
+ });
664
+ it("handles item.completed with generic text", () => {
665
+ const line = JSON.stringify({
666
+ type: "item.completed",
667
+ item: { type: "unknown", text: "some text" },
668
+ });
669
+ expect(parser.parseStreamJsonLine(line)).toBe("some text");
670
+ });
671
+ it("handles item.completed without item", () => {
672
+ const line = JSON.stringify({ type: "item.completed" });
673
+ expect(parser.parseStreamJsonLine(line)).toBe("");
674
+ });
675
+ it("handles item.completed with empty item", () => {
676
+ const line = JSON.stringify({ type: "item.completed", item: {} });
677
+ expect(parser.parseStreamJsonLine(line)).toBe("");
678
+ });
679
+ it("handles item.failed without item", () => {
680
+ const line = JSON.stringify({ type: "item.failed" });
681
+ expect(parser.parseStreamJsonLine(line)).toBe("");
682
+ });
683
+ it("handles item.failed with message instead of error", () => {
684
+ const line = JSON.stringify({
685
+ type: "item.failed",
686
+ item: { type: "bash", message: "Command not found" },
687
+ });
688
+ expect(parser.parseStreamJsonLine(line)).toContain("Command not found");
689
+ });
690
+ it("handles item.failed without error or message", () => {
691
+ const line = JSON.stringify({
692
+ type: "item.failed",
693
+ item: { type: "bash" },
694
+ });
695
+ expect(parser.parseStreamJsonLine(line)).toContain("Unknown error");
696
+ });
697
+ it("truncates long command_execution output", () => {
698
+ const longOutput = "x".repeat(1000);
699
+ const line = JSON.stringify({
700
+ type: "item.completed",
701
+ item: { type: "command_execution", output: longOutput },
702
+ });
703
+ const result = parser.parseStreamJsonLine(line);
704
+ expect(result).toContain("truncated");
705
+ });
706
+ it("handles turn.completed with zero tokens", () => {
707
+ const line = JSON.stringify({
708
+ type: "turn.completed",
709
+ usage: { input_tokens: 0, output_tokens: 0 },
710
+ });
711
+ const result = parser.parseStreamJsonLine(line);
712
+ expect(result).toContain("0");
713
+ });
714
+ it("falls back to text field for unknown type", () => {
715
+ const line = JSON.stringify({ type: "custom.event", text: "fallback" });
716
+ expect(parser.parseStreamJsonLine(line)).toBe("fallback");
717
+ });
718
+ it("returns empty for unknown type with no fallback", () => {
719
+ const line = JSON.stringify({ type: "custom.event", data: [1, 2] });
720
+ expect(parser.parseStreamJsonLine(line)).toBe("");
721
+ });
722
+ it("returns empty for invalid JSON", () => {
723
+ expect(parser.parseStreamJsonLine("not json at all")).toBe("");
724
+ });
725
+ });
726
+ // ─── GooseStreamParser ──────────────────────────────────────────────
727
+ describe("GooseStreamParser", () => {
728
+ it("delegates to ClaudeStreamParser for text events", () => {
729
+ const parser = new GooseStreamParser();
730
+ const line = JSON.stringify({ type: "text", text: "Hello from Goose" });
731
+ expect(parser.parseStreamJsonLine(line)).toBe("Hello from Goose");
732
+ });
733
+ it("delegates to ClaudeStreamParser for content_block_delta", () => {
734
+ const parser = new GooseStreamParser();
735
+ const line = JSON.stringify({
736
+ type: "content_block_delta",
737
+ delta: { type: "text_delta", text: "streaming" },
738
+ });
739
+ expect(parser.parseStreamJsonLine(line)).toBe("streaming");
740
+ });
741
+ it("delegates to ClaudeStreamParser for tool events", () => {
742
+ const parser = new GooseStreamParser();
743
+ const line = JSON.stringify({
744
+ type: "content_block_start",
745
+ content_block: { type: "tool_use", name: "file_read" },
746
+ });
747
+ expect(parser.parseStreamJsonLine(line)).toContain("file_read");
748
+ });
749
+ it("handles error events via Claude parser", () => {
750
+ const parser = new GooseStreamParser();
751
+ const line = JSON.stringify({ type: "error", error: { message: "Oops" } });
752
+ expect(parser.parseStreamJsonLine(line)).toContain("Oops");
753
+ });
754
+ it("returns empty for invalid JSON", () => {
755
+ const parser = new GooseStreamParser();
756
+ expect(parser.parseStreamJsonLine("not json")).toBe("");
757
+ });
758
+ it("handles file operations via Claude parser", () => {
759
+ const parser = new GooseStreamParser();
760
+ const line = JSON.stringify({ type: "file_edit", path: "goose.ts" });
761
+ expect(parser.parseStreamJsonLine(line)).toContain("goose.ts");
762
+ });
763
+ });
764
+ // ─── AiderStreamParser ──────────────────────────────────────────────
765
+ describe("AiderStreamParser", () => {
766
+ const parser = new AiderStreamParser();
767
+ it("parses text events", () => {
768
+ const line = JSON.stringify({ type: "text", text: "Hello" });
769
+ expect(parser.parseStreamJsonLine(line)).toBe("Hello");
770
+ });
771
+ it("parses tool_call events", () => {
772
+ const line = JSON.stringify({ type: "tool_call", name: "edit", arguments: { file: "a.ts" } });
773
+ const result = parser.parseStreamJsonLine(line);
774
+ expect(result).toContain("edit");
775
+ });
776
+ it("parses file_edit events", () => {
777
+ const line = JSON.stringify({ type: "file_edit", path: "src/main.ts" });
778
+ expect(parser.parseStreamJsonLine(line)).toContain("src/main.ts");
779
+ });
780
+ it("parses error events", () => {
781
+ const line = JSON.stringify({ type: "error", message: "Something broke" });
782
+ expect(parser.parseStreamJsonLine(line)).toContain("Something broke");
783
+ });
784
+ it("returns raw line for non-JSON input", () => {
785
+ expect(parser.parseStreamJsonLine("plain text output")).toBe("plain text output");
786
+ });
787
+ // --- new edge case tests ---
788
+ it("parses content events (alternative to text)", () => {
789
+ const line = JSON.stringify({ type: "content", content: "Content text" });
790
+ expect(parser.parseStreamJsonLine(line)).toBe("Content text");
791
+ });
792
+ it("parses function_call events (alternative to tool_call)", () => {
793
+ const line = JSON.stringify({ type: "function_call", function: "write_file", args: "test.ts" });
794
+ const result = parser.parseStreamJsonLine(line);
795
+ expect(result).toContain("write_file");
796
+ });
797
+ it("parses tool_call with string arguments", () => {
798
+ const line = JSON.stringify({ type: "tool_call", name: "bash", arguments: "npm test" });
799
+ const result = parser.parseStreamJsonLine(line);
800
+ expect(result).toContain("bash");
801
+ expect(result).toContain("npm test");
802
+ });
803
+ it("parses tool_call with args field", () => {
804
+ const line = JSON.stringify({ type: "tool_call", name: "search", args: { query: "test" } });
805
+ const result = parser.parseStreamJsonLine(line);
806
+ expect(result).toContain("search");
807
+ });
808
+ it("handles tool_call without name or function", () => {
809
+ const line = JSON.stringify({ type: "tool_call" });
810
+ expect(parser.parseStreamJsonLine(line)).toBe("");
811
+ });
812
+ it("parses tool_result events", () => {
813
+ const line = JSON.stringify({ type: "tool_result", result: "success" });
814
+ expect(parser.parseStreamJsonLine(line)).toContain("success");
815
+ });
816
+ it("parses function_result events", () => {
817
+ const line = JSON.stringify({ type: "function_result", output: "function output" });
818
+ expect(parser.parseStreamJsonLine(line)).toContain("function output");
819
+ });
820
+ it("parses edit events (alternative to file_edit)", () => {
821
+ const line = JSON.stringify({ type: "edit", file: "utils.ts" });
822
+ expect(parser.parseStreamJsonLine(line)).toContain("utils.ts");
823
+ });
824
+ it("handles file_edit with file field instead of path", () => {
825
+ const line = JSON.stringify({ type: "file_edit", file: "alt.ts" });
826
+ expect(parser.parseStreamJsonLine(line)).toContain("alt.ts");
827
+ });
828
+ it("handles error with error field instead of message", () => {
829
+ const line = JSON.stringify({ type: "error", error: "Error string" });
830
+ expect(parser.parseStreamJsonLine(line)).toContain("Error string");
831
+ });
832
+ it("handles error without message or error field", () => {
833
+ const line = JSON.stringify({ type: "error" });
834
+ const result = parser.parseStreamJsonLine(line);
835
+ expect(result).toContain("Error");
836
+ });
837
+ it("handles done event", () => {
838
+ const line = JSON.stringify({ type: "done" });
839
+ expect(parser.parseStreamJsonLine(line)).toBe("\n");
840
+ });
841
+ it("handles complete event", () => {
842
+ const line = JSON.stringify({ type: "complete" });
843
+ expect(parser.parseStreamJsonLine(line)).toBe("\n");
844
+ });
845
+ it("falls back to text field for unknown type", () => {
846
+ const line = JSON.stringify({ type: "custom", text: "fallback text" });
847
+ expect(parser.parseStreamJsonLine(line)).toBe("fallback text");
848
+ });
849
+ it("falls back to content field for unknown type", () => {
850
+ const line = JSON.stringify({ type: "custom", content: "fallback content" });
851
+ expect(parser.parseStreamJsonLine(line)).toBe("fallback content");
852
+ });
853
+ it("falls back to message field for unknown type", () => {
854
+ const line = JSON.stringify({ type: "custom", message: "fallback msg" });
855
+ expect(parser.parseStreamJsonLine(line)).toBe("fallback msg");
856
+ });
857
+ it("returns empty for unknown type with no fallback", () => {
858
+ const line = JSON.stringify({ type: "custom", data: 123 });
859
+ expect(parser.parseStreamJsonLine(line)).toBe("");
860
+ });
861
+ it("handles multiline plain text output", () => {
862
+ expect(parser.parseStreamJsonLine("line 1\nline 2")).toBe("line 1\nline 2");
863
+ });
864
+ it("handles empty string input", () => {
865
+ // Empty string is not valid JSON, returns as raw line
866
+ expect(parser.parseStreamJsonLine("")).toBe("");
867
+ });
868
+ it("truncates long tool results", () => {
869
+ const longResult = "x".repeat(1000);
870
+ const line = JSON.stringify({ type: "tool_result", result: longResult });
871
+ const result = parser.parseStreamJsonLine(line);
872
+ expect(result).toContain("truncated");
873
+ });
874
+ });
875
+ // ─── DefaultStreamParser ────────────────────────────────────────────
876
+ describe("DefaultStreamParser", () => {
877
+ const parser = new DefaultStreamParser();
878
+ it("extracts text field", () => {
879
+ const line = JSON.stringify({ type: "anything", text: "Hello" });
880
+ expect(parser.parseStreamJsonLine(line)).toBe("Hello");
881
+ });
882
+ it("extracts content field", () => {
883
+ const line = JSON.stringify({ type: "anything", content: "World" });
884
+ expect(parser.parseStreamJsonLine(line)).toBe("World");
885
+ });
886
+ it("extracts message field", () => {
887
+ const line = JSON.stringify({ type: "anything", message: "Info" });
888
+ expect(parser.parseStreamJsonLine(line)).toBe("Info");
889
+ });
890
+ it("returns empty for unrecognized events", () => {
891
+ const line = JSON.stringify({ type: "unknown", data: [1, 2, 3] });
892
+ expect(parser.parseStreamJsonLine(line)).toBe("");
893
+ });
894
+ it("returns empty for invalid JSON", () => {
895
+ expect(parser.parseStreamJsonLine("not json")).toBe("");
896
+ });
897
+ // --- new edge case tests ---
898
+ it("extracts output field", () => {
899
+ const line = JSON.stringify({ type: "anything", output: "output text" });
900
+ expect(parser.parseStreamJsonLine(line)).toBe("output text");
901
+ });
902
+ it("prefers text over content, message, and output", () => {
903
+ const line = JSON.stringify({
904
+ type: "anything",
905
+ text: "text wins",
906
+ content: "not this",
907
+ message: "not this",
908
+ output: "not this",
909
+ });
910
+ expect(parser.parseStreamJsonLine(line)).toBe("text wins");
911
+ });
912
+ it("prefers content over message and output when text missing", () => {
913
+ const line = JSON.stringify({
914
+ type: "anything",
915
+ content: "content wins",
916
+ message: "not this",
917
+ output: "not this",
918
+ });
919
+ expect(parser.parseStreamJsonLine(line)).toBe("content wins");
920
+ });
921
+ it("prefers message over output when text and content missing", () => {
922
+ const line = JSON.stringify({
923
+ type: "anything",
924
+ message: "message wins",
925
+ output: "not this",
926
+ });
927
+ expect(parser.parseStreamJsonLine(line)).toBe("message wins");
928
+ });
929
+ it("returns empty for empty JSON object", () => {
930
+ expect(parser.parseStreamJsonLine("{}")).toBe("");
931
+ });
932
+ it("returns empty for JSON with only non-string content", () => {
933
+ const line = JSON.stringify({ content: [1, 2, 3] });
934
+ expect(parser.parseStreamJsonLine(line)).toBe("");
935
+ });
936
+ it("handles JSON with only numeric fields", () => {
937
+ const line = JSON.stringify({ type: "metrics", tokens: 100, cost: 0.5 });
938
+ expect(parser.parseStreamJsonLine(line)).toBe("");
939
+ });
940
+ it("returns empty for JSON null", () => {
941
+ expect(parser.parseStreamJsonLine("null")).toBe("");
942
+ });
943
+ it("returns empty for JSON number", () => {
944
+ expect(parser.parseStreamJsonLine("42")).toBe("");
945
+ });
946
+ it("returns empty for JSON string", () => {
947
+ expect(parser.parseStreamJsonLine('"hello"')).toBe("");
948
+ });
949
+ });
950
+ // ─── getStreamJsonParser ────────────────────────────────────────────
951
+ describe("getStreamJsonParser", () => {
952
+ it("returns ClaudeStreamParser for 'claude'", () => {
953
+ expect(getStreamJsonParser("claude")).toBeInstanceOf(ClaudeStreamParser);
954
+ });
955
+ it("returns GeminiStreamParser for 'gemini'", () => {
956
+ expect(getStreamJsonParser("gemini")).toBeInstanceOf(GeminiStreamParser);
957
+ });
958
+ it("returns OpenCodeStreamParser for 'opencode'", () => {
959
+ expect(getStreamJsonParser("opencode")).toBeInstanceOf(OpenCodeStreamParser);
960
+ });
961
+ it("returns CodexStreamParser for 'codex'", () => {
962
+ expect(getStreamJsonParser("codex")).toBeInstanceOf(CodexStreamParser);
963
+ });
964
+ it("returns AiderStreamParser for 'aider'", () => {
965
+ expect(getStreamJsonParser("aider")).toBeInstanceOf(AiderStreamParser);
966
+ });
967
+ it("returns DefaultStreamParser for unknown providers", () => {
968
+ expect(getStreamJsonParser("unknown")).toBeInstanceOf(DefaultStreamParser);
969
+ expect(getStreamJsonParser(undefined)).toBeInstanceOf(DefaultStreamParser);
970
+ });
971
+ // --- new edge case tests ---
972
+ it("returns GooseStreamParser for 'goose'", () => {
973
+ expect(getStreamJsonParser("goose")).toBeInstanceOf(GooseStreamParser);
974
+ });
975
+ it("returns DefaultStreamParser for empty string", () => {
976
+ expect(getStreamJsonParser("")).toBeInstanceOf(DefaultStreamParser);
977
+ });
978
+ it("returns DefaultStreamParser for null-like values", () => {
979
+ expect(getStreamJsonParser(undefined)).toBeInstanceOf(DefaultStreamParser);
980
+ });
981
+ it("is case-sensitive (uppercase does not match)", () => {
982
+ expect(getStreamJsonParser("Claude")).toBeInstanceOf(DefaultStreamParser);
983
+ expect(getStreamJsonParser("AIDER")).toBeInstanceOf(DefaultStreamParser);
984
+ expect(getStreamJsonParser("Gemini")).toBeInstanceOf(DefaultStreamParser);
985
+ });
986
+ it("returns unique instances on each call", () => {
987
+ const parser1 = getStreamJsonParser("claude");
988
+ const parser2 = getStreamJsonParser("claude");
989
+ expect(parser1).not.toBe(parser2);
990
+ });
991
+ it("all parsers implement parseStreamJsonLine method", () => {
992
+ const providers = [
993
+ "claude",
994
+ "gemini",
995
+ "opencode",
996
+ "codex",
997
+ "goose",
998
+ "aider",
999
+ "unknown",
1000
+ undefined,
1001
+ ];
1002
+ for (const provider of providers) {
1003
+ const parser = getStreamJsonParser(provider);
1004
+ expect(typeof parser.parseStreamJsonLine).toBe("function");
1005
+ }
1006
+ });
1007
+ });