@vacbo/opencode-anthropic-fix 0.1.7 → 0.1.9

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 (107) hide show
  1. package/README.md +88 -88
  2. package/dist/opencode-anthropic-auth-cli.mjs +804 -507
  3. package/dist/opencode-anthropic-auth-plugin.js +4751 -4109
  4. package/package.json +67 -59
  5. package/src/__tests__/billing-edge-cases.test.ts +59 -59
  6. package/src/__tests__/bun-proxy.parallel.test.ts +388 -382
  7. package/src/__tests__/cc-comparison.test.ts +87 -87
  8. package/src/__tests__/cc-credentials.test.ts +254 -250
  9. package/src/__tests__/cch-drift-checker.test.ts +51 -51
  10. package/src/__tests__/cch-native-style.test.ts +56 -56
  11. package/src/__tests__/debug-gating.test.ts +42 -42
  12. package/src/__tests__/decomposition-smoke.test.ts +68 -68
  13. package/src/__tests__/fingerprint-regression.test.ts +575 -566
  14. package/src/__tests__/helpers/conversation-history.smoke.test.ts +271 -271
  15. package/src/__tests__/helpers/conversation-history.ts +119 -119
  16. package/src/__tests__/helpers/deferred.smoke.test.ts +103 -103
  17. package/src/__tests__/helpers/deferred.ts +69 -69
  18. package/src/__tests__/helpers/in-memory-storage.smoke.test.ts +155 -155
  19. package/src/__tests__/helpers/in-memory-storage.ts +88 -88
  20. package/src/__tests__/helpers/mock-bun-proxy.smoke.test.ts +68 -68
  21. package/src/__tests__/helpers/mock-bun-proxy.ts +189 -189
  22. package/src/__tests__/helpers/plugin-fetch-harness.smoke.test.ts +273 -273
  23. package/src/__tests__/helpers/plugin-fetch-harness.ts +288 -288
  24. package/src/__tests__/helpers/sse.smoke.test.ts +236 -236
  25. package/src/__tests__/helpers/sse.ts +209 -209
  26. package/src/__tests__/index.parallel.test.ts +605 -595
  27. package/src/__tests__/sanitization-regex.test.ts +112 -112
  28. package/src/__tests__/state-bounds.test.ts +90 -90
  29. package/src/account-identity.test.ts +197 -192
  30. package/src/account-identity.ts +69 -67
  31. package/src/account-state.test.ts +86 -86
  32. package/src/account-state.ts +25 -25
  33. package/src/accounts/matching.test.ts +335 -0
  34. package/src/accounts/matching.ts +167 -0
  35. package/src/accounts/persistence.test.ts +345 -0
  36. package/src/accounts/persistence.ts +432 -0
  37. package/src/accounts/repair.test.ts +276 -0
  38. package/src/accounts/repair.ts +407 -0
  39. package/src/accounts.dedup.test.ts +621 -621
  40. package/src/accounts.test.ts +933 -929
  41. package/src/accounts.ts +633 -989
  42. package/src/backoff.test.ts +345 -345
  43. package/src/backoff.ts +219 -219
  44. package/src/betas.ts +124 -124
  45. package/src/bun-fetch.test.ts +345 -342
  46. package/src/bun-fetch.ts +424 -424
  47. package/src/bun-proxy.test.ts +25 -25
  48. package/src/bun-proxy.ts +209 -209
  49. package/src/cc-credentials.ts +111 -111
  50. package/src/circuit-breaker.test.ts +184 -184
  51. package/src/circuit-breaker.ts +169 -169
  52. package/src/cli/commands/auth.ts +963 -0
  53. package/src/cli/commands/config.ts +547 -0
  54. package/src/cli/formatting.test.ts +406 -0
  55. package/src/cli/formatting.ts +219 -0
  56. package/src/cli.ts +255 -2022
  57. package/src/commands/handlers/betas.ts +100 -0
  58. package/src/commands/handlers/config.ts +99 -0
  59. package/src/commands/handlers/files.ts +375 -0
  60. package/src/commands/oauth-flow.ts +181 -166
  61. package/src/commands/prompts.ts +61 -61
  62. package/src/commands/router.test.ts +421 -0
  63. package/src/commands/router.ts +143 -635
  64. package/src/config.test.ts +482 -482
  65. package/src/config.ts +412 -404
  66. package/src/constants.ts +48 -48
  67. package/src/drift/cch-constants.ts +95 -95
  68. package/src/env.ts +111 -105
  69. package/src/headers/billing.ts +33 -33
  70. package/src/headers/builder.ts +130 -130
  71. package/src/headers/cch.ts +75 -75
  72. package/src/headers/stainless.ts +25 -25
  73. package/src/headers/user-agent.ts +23 -23
  74. package/src/index.ts +436 -828
  75. package/src/models.ts +27 -27
  76. package/src/oauth.test.ts +102 -102
  77. package/src/oauth.ts +178 -178
  78. package/src/parent-pid-watcher.test.ts +148 -148
  79. package/src/parent-pid-watcher.ts +69 -69
  80. package/src/plugin-helpers.ts +82 -82
  81. package/src/refresh-helpers.ts +145 -139
  82. package/src/refresh-lock.test.ts +94 -94
  83. package/src/refresh-lock.ts +93 -93
  84. package/src/request/body.history.test.ts +579 -571
  85. package/src/request/body.ts +255 -255
  86. package/src/request/metadata.ts +65 -65
  87. package/src/request/retry.test.ts +156 -156
  88. package/src/request/retry.ts +67 -67
  89. package/src/request/url.ts +21 -21
  90. package/src/request-orchestration-helpers.ts +648 -0
  91. package/src/response/index.ts +5 -5
  92. package/src/response/mcp.ts +58 -58
  93. package/src/response/streaming.test.ts +313 -311
  94. package/src/response/streaming.ts +412 -410
  95. package/src/rotation.test.ts +304 -301
  96. package/src/rotation.ts +205 -205
  97. package/src/storage.test.ts +547 -547
  98. package/src/storage.ts +315 -291
  99. package/src/system-prompt/builder.ts +38 -38
  100. package/src/system-prompt/index.ts +5 -5
  101. package/src/system-prompt/normalize.ts +60 -60
  102. package/src/system-prompt/sanitize.ts +30 -30
  103. package/src/thinking.ts +21 -20
  104. package/src/token-refresh.test.ts +265 -265
  105. package/src/token-refresh.ts +219 -214
  106. package/src/types.ts +30 -30
  107. package/dist/bun-proxy.mjs +0 -291
@@ -1,16 +1,16 @@
1
1
  import { describe, expect, it, vi } from "vitest";
2
2
 
3
3
  import {
4
- chunkUtf8AtOffsets,
5
- contentBlockDeltaEvent,
6
- contentBlockStartEvent,
7
- contentBlockStopEvent,
8
- encodeSSEEvent,
9
- encodeSSEStream,
10
- makeSSEResponse,
11
- messageDeltaEvent,
12
- messageStartEvent,
13
- messageStopEvent,
4
+ chunkUtf8AtOffsets,
5
+ contentBlockDeltaEvent,
6
+ contentBlockStartEvent,
7
+ contentBlockStopEvent,
8
+ encodeSSEEvent,
9
+ encodeSSEStream,
10
+ makeSSEResponse,
11
+ messageDeltaEvent,
12
+ messageStartEvent,
13
+ messageStopEvent,
14
14
  } from "../__tests__/helpers/sse.js";
15
15
  import { StreamTruncatedError, transformResponse } from "./streaming.js";
16
16
 
@@ -18,365 +18,367 @@ const encoder = new TextEncoder();
18
18
  const decoder = new TextDecoder();
19
19
 
20
20
  function joinBlocks(...blocks: string[]): string {
21
- return blocks.join("");
21
+ return blocks.join("");
22
22
  }
23
23
 
24
24
  function encodeToolUseStartEvent(
25
- name: string,
26
- input: Record<string, unknown> = { path: "/tmp/demo.txt" },
27
- multiline = false,
25
+ name: string,
26
+ input: Record<string, unknown> = { path: "/tmp/demo.txt" },
27
+ multiline = false,
28
28
  ): string {
29
- const payload = {
30
- type: "content_block_start",
31
- index: 0,
32
- content_block: {
33
- type: "tool_use",
34
- id: "toolu_123",
35
- name,
36
- input,
37
- },
38
- };
39
-
40
- return encodeSSEEvent({
41
- data: multiline ? JSON.stringify(payload, null, 2) : JSON.stringify(payload),
42
- });
29
+ const payload = {
30
+ type: "content_block_start",
31
+ index: 0,
32
+ content_block: {
33
+ type: "tool_use",
34
+ id: "toolu_123",
35
+ name,
36
+ input,
37
+ },
38
+ };
39
+
40
+ return encodeSSEEvent({
41
+ data: multiline ? JSON.stringify(payload, null, 2) : JSON.stringify(payload),
42
+ });
43
43
  }
44
44
 
45
45
  function encodeInputJsonDeltaEvent(partialJson: string): string {
46
- return encodeSSEEvent({
47
- data: JSON.stringify({
48
- type: "content_block_delta",
49
- index: 0,
50
- delta: {
51
- type: "input_json_delta",
52
- partial_json: partialJson,
53
- },
54
- }),
55
- });
46
+ return encodeSSEEvent({
47
+ data: JSON.stringify({
48
+ type: "content_block_delta",
49
+ index: 0,
50
+ delta: {
51
+ type: "input_json_delta",
52
+ partial_json: partialJson,
53
+ },
54
+ }),
55
+ });
56
56
  }
57
57
 
58
58
  function makeChunkedSSEResponse(text: string, byteOffsets: number[]): Response {
59
- const chunks = chunkUtf8AtOffsets(text, byteOffsets);
60
-
61
- return makeSSEResponse(
62
- new ReadableStream<Uint8Array>({
63
- start(controller) {
64
- for (const chunk of chunks) {
65
- controller.enqueue(chunk);
66
- }
67
- controller.close();
68
- },
69
- }),
70
- );
59
+ const chunks = chunkUtf8AtOffsets(text, byteOffsets);
60
+
61
+ return makeSSEResponse(
62
+ new ReadableStream<Uint8Array>({
63
+ start(controller) {
64
+ for (const chunk of chunks) {
65
+ controller.enqueue(chunk);
66
+ }
67
+ controller.close();
68
+ },
69
+ }),
70
+ );
71
71
  }
72
72
 
73
73
  function makeSSEFromByteChunks(chunks: Uint8Array[], onCancel?: (reason: unknown) => void): Response {
74
- return makeSSEResponse(
75
- new ReadableStream<Uint8Array>({
76
- start(controller) {
77
- for (const chunk of chunks) {
78
- controller.enqueue(chunk);
79
- }
80
- controller.close();
81
- },
82
- cancel(reason) {
83
- onCancel?.(reason);
84
- },
85
- }),
86
- );
74
+ return makeSSEResponse(
75
+ new ReadableStream<Uint8Array>({
76
+ start(controller) {
77
+ for (const chunk of chunks) {
78
+ controller.enqueue(chunk);
79
+ }
80
+ controller.close();
81
+ },
82
+ cancel(reason) {
83
+ onCancel?.(reason);
84
+ },
85
+ }),
86
+ );
87
87
  }
88
88
 
89
89
  function createControlledSSE(
90
- chunks: Uint8Array[],
91
- onCancel?: (reason: unknown) => void,
90
+ chunks: Uint8Array[],
91
+ onCancel?: (reason: unknown) => void,
92
92
  ): {
93
- response: Response;
94
- emit(index: number): void;
95
- close(): void;
93
+ response: Response;
94
+ emit(index: number): void;
95
+ close(): void;
96
96
  } {
97
- let controller: ReadableStreamDefaultController<Uint8Array> | null = null;
98
-
99
- const response = makeSSEResponse(
100
- new ReadableStream<Uint8Array>({
101
- start(innerController) {
102
- controller = innerController;
103
- },
104
- cancel(reason) {
105
- onCancel?.(reason);
106
- },
107
- }),
108
- );
109
-
110
- return {
111
- response,
112
- emit(index: number) {
113
- controller?.enqueue(chunks[index]!);
114
- },
115
- close() {
116
- controller?.close();
117
- },
118
- };
97
+ let controller: ReadableStreamDefaultController<Uint8Array> | null = null;
98
+
99
+ const response = makeSSEResponse(
100
+ new ReadableStream<Uint8Array>({
101
+ start(innerController) {
102
+ controller = innerController;
103
+ },
104
+ cancel(reason) {
105
+ onCancel?.(reason);
106
+ },
107
+ }),
108
+ );
109
+
110
+ return {
111
+ response,
112
+ emit(index: number) {
113
+ controller?.enqueue(chunks[index]!);
114
+ },
115
+ close() {
116
+ controller?.close();
117
+ },
118
+ };
119
119
  }
120
120
 
121
121
  async function getPromiseState<T>(promise: Promise<T>): Promise<"pending" | "fulfilled" | "rejected"> {
122
- let state: "pending" | "fulfilled" | "rejected" = "pending";
122
+ let state: "pending" | "fulfilled" | "rejected" = "pending";
123
123
 
124
- void promise.then(
125
- () => {
126
- state = "fulfilled";
127
- },
128
- () => {
129
- state = "rejected";
130
- },
131
- );
124
+ void promise.then(
125
+ () => {
126
+ state = "fulfilled";
127
+ },
128
+ () => {
129
+ state = "rejected";
130
+ },
131
+ );
132
132
 
133
- await Promise.resolve();
134
- await Promise.resolve();
133
+ await Promise.resolve();
134
+ await Promise.resolve();
135
135
 
136
- return state;
136
+ return state;
137
137
  }
138
138
 
139
139
  describe("transformResponse RED - SSE termination and framing", () => {
140
- it("accepts a multiline tool_use stream only when the final event is message_stop", async () => {
141
- const stream = joinBlocks(
142
- encodeSSEStream([messageStartEvent()]),
143
- encodeToolUseStartEvent("mcp_read_file", { path: "/tmp/demo.txt" }, true),
144
- encodeSSEStream([contentBlockStopEvent(0), messageDeltaEvent(), messageStopEvent()]),
145
- );
146
-
147
- const text = await transformResponse(makeSSEResponse(stream)).text();
148
-
149
- expect(text).toContain('"type": "message_stop"');
150
- expect(text).toContain('"name": "read_file"');
151
- expect(text).not.toContain("mcp_read_file");
152
- });
153
-
154
- it("preserves multi-byte UTF-8 payloads across chunk boundaries before message_stop", async () => {
155
- const stream = joinBlocks(
156
- encodeSSEStream([messageStartEvent()]),
157
- encodeToolUseStartEvent(
158
- "mcp_unicode_lookup",
159
- {
160
- path: "/tmp/café-🎉.txt",
161
- note: "привет мир",
162
- },
163
- true,
164
- ),
165
- encodeSSEStream([contentBlockStopEvent(0), messageStopEvent()]),
166
- );
167
-
168
- const text = await transformResponse(makeChunkedSSEResponse(stream, [1, 2, 3, 5, 8, 13, 21, 34])).text();
140
+ it("accepts a multiline tool_use stream only when the final event is message_stop", async () => {
141
+ const stream = joinBlocks(
142
+ encodeSSEStream([messageStartEvent()]),
143
+ encodeToolUseStartEvent("mcp_read_file", { path: "/tmp/demo.txt" }, true),
144
+ encodeSSEStream([contentBlockStopEvent(0), messageDeltaEvent(), messageStopEvent()]),
145
+ );
146
+
147
+ const text = await transformResponse(makeSSEResponse(stream)).text();
148
+
149
+ expect(text).toContain('"type": "message_stop"');
150
+ expect(text).toContain('"name": "read_file"');
151
+ expect(text).not.toContain("mcp_read_file");
152
+ });
169
153
 
170
- expect(text).toContain("café-🎉.txt");
171
- expect(text).toContain("привет мир");
172
- expect(text).toContain('"name": "unicode_lookup"');
173
- });
154
+ it("preserves multi-byte UTF-8 payloads across chunk boundaries before message_stop", async () => {
155
+ const stream = joinBlocks(
156
+ encodeSSEStream([messageStartEvent()]),
157
+ encodeToolUseStartEvent(
158
+ "mcp_unicode_lookup",
159
+ {
160
+ path: "/tmp/café-🎉.txt",
161
+ note: "привет мир",
162
+ },
163
+ true,
164
+ ),
165
+ encodeSSEStream([contentBlockStopEvent(0), messageStopEvent()]),
166
+ );
167
+
168
+ const text = await transformResponse(makeChunkedSSEResponse(stream, [1, 2, 3, 5, 8, 13, 21, 34])).text();
169
+
170
+ expect(text).toContain("café-🎉.txt");
171
+ expect(text).toContain("привет мир");
172
+ expect(text).toContain('"name": "unicode_lookup"');
173
+ });
174
174
 
175
- it("rewrites multiline data payloads as one event block instead of line-by-line", async () => {
176
- const stream = joinBlocks(
177
- encodeToolUseStartEvent("mcp_shell_exec", { command: "ls -la" }, true),
178
- encodeSSEStream([messageStopEvent()]),
179
- );
175
+ it("rewrites multiline data payloads as one event block instead of line-by-line", async () => {
176
+ const stream = joinBlocks(
177
+ encodeToolUseStartEvent("mcp_shell_exec", { command: "ls -la" }, true),
178
+ encodeSSEStream([messageStopEvent()]),
179
+ );
180
180
 
181
- const text = await transformResponse(makeSSEResponse(stream)).text();
181
+ const text = await transformResponse(makeSSEResponse(stream)).text();
182
182
 
183
- expect(text).toContain('"name": "shell_exec"');
184
- expect(text).not.toContain("mcp_shell_exec");
185
- });
183
+ expect(text).toContain('"name": "shell_exec"');
184
+ expect(text).not.toContain("mcp_shell_exec");
185
+ });
186
186
 
187
- it("does not emit rewritten bytes until a split event block is complete", async () => {
188
- const block = encodeToolUseStartEvent("mcp_read_file", { path: "/tmp/demo.txt" }, true);
189
- const splitPoint = block.indexOf("\n") + 1;
190
- const chunks = [encoder.encode(block.slice(0, splitPoint)), encoder.encode(block.slice(splitPoint))];
191
- const controlled = createControlledSSE(chunks);
187
+ it("does not emit rewritten bytes until a split event block is complete", async () => {
188
+ const block = encodeToolUseStartEvent("mcp_read_file", { path: "/tmp/demo.txt" }, true);
189
+ const splitPoint = block.indexOf("\n") + 1;
190
+ const chunks = [encoder.encode(block.slice(0, splitPoint)), encoder.encode(block.slice(splitPoint))];
191
+ const controlled = createControlledSSE(chunks);
192
192
 
193
- const transformed = transformResponse(controlled.response);
194
- const reader = transformed.body!.getReader();
193
+ const transformed = transformResponse(controlled.response);
194
+ const reader = transformed.body!.getReader();
195
195
 
196
- controlled.emit(0);
196
+ controlled.emit(0);
197
197
 
198
- const firstRead = reader.read();
199
- expect(await getPromiseState(firstRead)).toBe("pending");
198
+ const firstRead = reader.read();
199
+ expect(await getPromiseState(firstRead)).toBe("pending");
200
200
 
201
- controlled.emit(1);
202
- controlled.close();
201
+ controlled.emit(1);
202
+ controlled.close();
203
203
 
204
- const resolved = await firstRead;
205
- expect(decoder.decode(resolved.value)).toContain('"name": "read_file"');
206
- });
204
+ const resolved = await firstRead;
205
+ expect(decoder.decode(resolved.value)).toContain('"name": "read_file"');
206
+ });
207
207
  });
208
208
 
209
209
  describe("transformResponse RED - truncation and validation", () => {
210
- it("rejects a truncated stream that ends after message_delta without message_stop", async () => {
211
- const stream = encodeSSEStream([
212
- messageStartEvent(),
213
- contentBlockStartEvent(0),
214
- contentBlockDeltaEvent(0, "hello"),
215
- contentBlockStopEvent(0),
216
- messageDeltaEvent(),
217
- ]);
218
-
219
- const error = await transformResponse(makeSSEResponse(stream))
220
- .text()
221
- .catch((streamError: unknown) => streamError);
222
-
223
- expect(error).toBeInstanceOf(StreamTruncatedError);
224
- expect(error).toBeInstanceOf(Error);
225
- expect((error as StreamTruncatedError).message).toMatch(/message_stop|truncated/i);
226
- expect((error as StreamTruncatedError).context).toMatchObject({
227
- inFlightEvent: "message_delta",
228
- lastEventType: "message_delta",
210
+ it("rejects a truncated stream that ends after message_delta without message_stop", async () => {
211
+ const stream = encodeSSEStream([
212
+ messageStartEvent(),
213
+ contentBlockStartEvent(0),
214
+ contentBlockDeltaEvent(0, "hello"),
215
+ contentBlockStopEvent(0),
216
+ messageDeltaEvent(),
217
+ ]);
218
+
219
+ const error = await transformResponse(makeSSEResponse(stream))
220
+ .text()
221
+ .catch((streamError: unknown) => streamError);
222
+
223
+ expect(error).toBeInstanceOf(StreamTruncatedError);
224
+ expect(error).toBeInstanceOf(Error);
225
+ expect((error as StreamTruncatedError).message).toMatch(/message_stop|truncated/i);
226
+ expect((error as StreamTruncatedError).context).toMatchObject({
227
+ inFlightEvent: "message_delta",
228
+ lastEventType: "message_delta",
229
+ });
229
230
  });
230
- });
231
-
232
- it("rejects a final message_stop event that is missing its terminating blank line", async () => {
233
- const completeStop = encodeSSEStream([messageStopEvent()]);
234
- const truncatedStop = completeStop.slice(0, -2);
235
- const stream = joinBlocks(encodeSSEStream([messageStartEvent()]), truncatedStop);
236
-
237
- await expect(transformResponse(makeSSEResponse(stream)).text()).rejects.toThrow(/truncated|terminator/i);
238
- });
239
-
240
- it("rejects an event:error terminator with a descriptive stream failure", async () => {
241
- const errorTerminator = encodeSSEEvent({
242
- event: "error",
243
- data: JSON.stringify({
244
- type: "error",
245
- error: {
246
- type: "stream_error",
247
- message: "stream aborted by upstream",
248
- },
249
- }),
231
+
232
+ it("rejects a final message_stop event that is missing its terminating blank line", async () => {
233
+ const completeStop = encodeSSEStream([messageStopEvent()]);
234
+ const truncatedStop = completeStop.slice(0, -2);
235
+ const stream = joinBlocks(encodeSSEStream([messageStartEvent()]), truncatedStop);
236
+
237
+ await expect(transformResponse(makeSSEResponse(stream)).text()).rejects.toThrow(/truncated|terminator/i);
250
238
  });
251
- const stream = joinBlocks(encodeSSEStream([messageStartEvent()]), errorTerminator);
252
239
 
253
- await expect(transformResponse(makeSSEResponse(stream)).text()).rejects.toThrow(/stream aborted by upstream/i);
254
- });
240
+ it("rejects an event:error terminator with a descriptive stream failure", async () => {
241
+ const errorTerminator = encodeSSEEvent({
242
+ event: "error",
243
+ data: JSON.stringify({
244
+ type: "error",
245
+ error: {
246
+ type: "stream_error",
247
+ message: "stream aborted by upstream",
248
+ },
249
+ }),
250
+ });
251
+ const stream = joinBlocks(encodeSSEStream([messageStartEvent()]), errorTerminator);
252
+
253
+ await expect(transformResponse(makeSSEResponse(stream)).text()).rejects.toThrow(/stream aborted by upstream/i);
254
+ });
255
255
 
256
- it("rejects malformed JSON event blocks instead of silently ignoring them", async () => {
257
- const malformed = joinBlocks(
258
- encodeSSEStream([messageStartEvent()]),
259
- 'event: message\ndata: {"type":"content_block_start","index":0,\n\n',
260
- encodeSSEStream([messageStopEvent()]),
261
- );
256
+ it("rejects malformed JSON event blocks instead of silently ignoring them", async () => {
257
+ const malformed = joinBlocks(
258
+ encodeSSEStream([messageStartEvent()]),
259
+ 'event: message\ndata: {"type":"content_block_start","index":0,\n\n',
260
+ encodeSSEStream([messageStopEvent()]),
261
+ );
262
262
 
263
- await expect(transformResponse(makeSSEResponse(malformed)).text()).rejects.toThrow(/malformed|invalid sse/i);
264
- });
263
+ await expect(transformResponse(makeSSEResponse(malformed)).text()).rejects.toThrow(/malformed|invalid sse/i);
264
+ });
265
265
 
266
- it("rejects orphan content_block_delta events", async () => {
267
- const stream = encodeSSEStream([
268
- messageStartEvent(),
269
- contentBlockDeltaEvent(0, "orphan delta"),
270
- messageStopEvent(),
271
- ]);
266
+ it("rejects orphan content_block_delta events", async () => {
267
+ const stream = encodeSSEStream([
268
+ messageStartEvent(),
269
+ contentBlockDeltaEvent(0, "orphan delta"),
270
+ messageStopEvent(),
271
+ ]);
272
272
 
273
- await expect(transformResponse(makeSSEResponse(stream)).text()).rejects.toThrow(/orphan|content_block_delta/i);
274
- });
273
+ await expect(transformResponse(makeSSEResponse(stream)).text()).rejects.toThrow(/orphan|content_block_delta/i);
274
+ });
275
275
 
276
- it("rejects orphan content_block_stop events", async () => {
277
- const stream = encodeSSEStream([messageStartEvent(), contentBlockStopEvent(0), messageStopEvent()]);
276
+ it("rejects orphan content_block_stop events", async () => {
277
+ const stream = encodeSSEStream([messageStartEvent(), contentBlockStopEvent(0), messageStopEvent()]);
278
278
 
279
- await expect(transformResponse(makeSSEResponse(stream)).text()).rejects.toThrow(/orphan|content_block_stop/i);
280
- });
279
+ await expect(transformResponse(makeSSEResponse(stream)).text()).rejects.toThrow(/orphan|content_block_stop/i);
280
+ });
281
281
 
282
- it("rejects a truncated tool_use block at EOF", async () => {
283
- const stream = encodeSSEStream([
284
- messageStartEvent(),
285
- contentBlockStartEvent(0, {
286
- content_block: {
287
- type: "tool_use",
288
- id: "toolu_123",
289
- name: "mcp_read_file",
290
- input: { path: "/tmp/demo.txt" },
291
- },
292
- }),
293
- messageDeltaEvent(),
294
- ]);
295
-
296
- const error = await transformResponse(makeSSEResponse(stream))
297
- .text()
298
- .catch((streamError: unknown) => streamError);
299
-
300
- expect(error).toBeInstanceOf(StreamTruncatedError);
301
- expect((error as StreamTruncatedError).message).toMatch(/truncated/i);
302
- expect((error as StreamTruncatedError).context).toMatchObject({
303
- inFlightEvent: "content_block_start(tool_use)",
304
- openContentBlockIndex: 0,
305
- hasPartialJson: false,
282
+ it("rejects a truncated tool_use block at EOF", async () => {
283
+ const stream = encodeSSEStream([
284
+ messageStartEvent(),
285
+ contentBlockStartEvent(0, {
286
+ content_block: {
287
+ type: "tool_use",
288
+ id: "toolu_123",
289
+ name: "mcp_read_file",
290
+ input: { path: "/tmp/demo.txt" },
291
+ },
292
+ }),
293
+ messageDeltaEvent(),
294
+ ]);
295
+
296
+ const error = await transformResponse(makeSSEResponse(stream))
297
+ .text()
298
+ .catch((streamError: unknown) => streamError);
299
+
300
+ expect(error).toBeInstanceOf(StreamTruncatedError);
301
+ expect((error as StreamTruncatedError).message).toMatch(/truncated/i);
302
+ expect((error as StreamTruncatedError).context).toMatchObject({
303
+ inFlightEvent: "content_block_start(tool_use)",
304
+ openContentBlockIndex: 0,
305
+ hasPartialJson: false,
306
+ });
306
307
  });
307
- });
308
-
309
- it("rejects incomplete input_json_delta tool payloads even if message_stop arrives", async () => {
310
- const stream = joinBlocks(
311
- encodeSSEStream([messageStartEvent()]),
312
- encodeSSEStream([
313
- contentBlockStartEvent(0, {
314
- content_block: {
315
- type: "tool_use",
316
- id: "toolu_123",
317
- name: "mcp_read_file",
318
- input: {},
319
- },
320
- }),
321
- ]),
322
- encodeInputJsonDeltaEvent('{"path":"/tmp/demo.txt"'),
323
- encodeSSEStream([contentBlockStopEvent(0), messageStopEvent()]),
324
- );
325
308
 
326
- await expect(transformResponse(makeSSEResponse(stream)).text()).rejects.toThrow(
327
- /partial_json|incomplete tool_use/i,
328
- );
329
- });
309
+ it("rejects incomplete input_json_delta tool payloads even if message_stop arrives", async () => {
310
+ const stream = joinBlocks(
311
+ encodeSSEStream([messageStartEvent()]),
312
+ encodeSSEStream([
313
+ contentBlockStartEvent(0, {
314
+ content_block: {
315
+ type: "tool_use",
316
+ id: "toolu_123",
317
+ name: "mcp_read_file",
318
+ input: {},
319
+ },
320
+ }),
321
+ ]),
322
+ encodeInputJsonDeltaEvent('{"path":"/tmp/demo.txt"'),
323
+ encodeSSEStream([contentBlockStopEvent(0), messageStopEvent()]),
324
+ );
325
+
326
+ await expect(transformResponse(makeSSEResponse(stream)).text()).rejects.toThrow(
327
+ /partial_json|incomplete tool_use/i,
328
+ );
329
+ });
330
330
 
331
- it("rejects oversized unterminated event buffers before they grow unbounded", async () => {
332
- const oversized = `data: ${"x".repeat(256 * 1024)}`;
331
+ it("rejects oversized unterminated event buffers before they grow unbounded", async () => {
332
+ const oversized = `data: ${"x".repeat(256 * 1024)}`;
333
333
 
334
- await expect(transformResponse(makeSSEResponse(oversized)).text()).rejects.toThrow(/buffer|limit|unterminated/i);
335
- });
334
+ await expect(transformResponse(makeSSEResponse(oversized)).text()).rejects.toThrow(
335
+ /buffer|limit|unterminated/i,
336
+ );
337
+ });
336
338
 
337
- it("rejects malformed UTF-8 buffered at EOF during the final decoder flush", async () => {
338
- const validStream = encoder.encode(encodeSSEStream([messageStartEvent(), messageStopEvent()]));
339
- const invalidTail = new Uint8Array([0xc3]);
340
- const response = makeSSEFromByteChunks([validStream, invalidTail]);
339
+ it("rejects malformed UTF-8 buffered at EOF during the final decoder flush", async () => {
340
+ const validStream = encoder.encode(encodeSSEStream([messageStartEvent(), messageStopEvent()]));
341
+ const invalidTail = new Uint8Array([0xc3]);
342
+ const response = makeSSEFromByteChunks([validStream, invalidTail]);
341
343
 
342
- await expect(transformResponse(response).text()).rejects.toThrow(/utf-8|decoder|malformed/i);
343
- });
344
+ await expect(transformResponse(response).text()).rejects.toThrow(/utf-8|decoder|malformed/i);
345
+ });
344
346
  });
345
347
 
346
348
  describe("transformResponse RED - stream control", () => {
347
- it("propagates cancel() to the upstream body reader", async () => {
348
- const cancelSpy = vi.fn();
349
- const response = makeSSEResponse(
350
- new ReadableStream<Uint8Array>({
351
- pull() {
352
- return Promise.resolve();
353
- },
354
- cancel(reason) {
355
- cancelSpy(reason);
356
- },
357
- }),
358
- );
359
-
360
- const transformed = transformResponse(response);
361
- await transformed.body!.cancel("stop-now");
362
-
363
- expect(cancelSpy).toHaveBeenCalledWith("stop-now");
364
- });
365
-
366
- it("bypasses the SSE transform path for non-event-stream responses", () => {
367
- const response = new Response(
368
- JSON.stringify({
369
- content: [{ type: "tool_use", name: "mcp_read_file" }],
370
- }),
371
- {
372
- headers: {
373
- "content-type": "application/json",
374
- },
375
- },
376
- );
377
-
378
- const transformed = transformResponse(response);
349
+ it("propagates cancel() to the upstream body reader", async () => {
350
+ const cancelSpy = vi.fn();
351
+ const response = makeSSEResponse(
352
+ new ReadableStream<Uint8Array>({
353
+ pull() {
354
+ return Promise.resolve();
355
+ },
356
+ cancel(reason) {
357
+ cancelSpy(reason);
358
+ },
359
+ }),
360
+ );
361
+
362
+ const transformed = transformResponse(response);
363
+ await transformed.body!.cancel("stop-now");
364
+
365
+ expect(cancelSpy).toHaveBeenCalledWith("stop-now");
366
+ });
379
367
 
380
- expect(transformed).toBe(response);
381
- });
368
+ it("bypasses the SSE transform path for non-event-stream responses", () => {
369
+ const response = new Response(
370
+ JSON.stringify({
371
+ content: [{ type: "tool_use", name: "mcp_read_file" }],
372
+ }),
373
+ {
374
+ headers: {
375
+ "content-type": "application/json",
376
+ },
377
+ },
378
+ );
379
+
380
+ const transformed = transformResponse(response);
381
+
382
+ expect(transformed).toBe(response);
383
+ });
382
384
  });