@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.
- package/README.md +88 -88
- package/dist/opencode-anthropic-auth-cli.mjs +804 -507
- package/dist/opencode-anthropic-auth-plugin.js +4751 -4109
- package/package.json +67 -59
- package/src/__tests__/billing-edge-cases.test.ts +59 -59
- package/src/__tests__/bun-proxy.parallel.test.ts +388 -382
- package/src/__tests__/cc-comparison.test.ts +87 -87
- package/src/__tests__/cc-credentials.test.ts +254 -250
- package/src/__tests__/cch-drift-checker.test.ts +51 -51
- package/src/__tests__/cch-native-style.test.ts +56 -56
- package/src/__tests__/debug-gating.test.ts +42 -42
- package/src/__tests__/decomposition-smoke.test.ts +68 -68
- package/src/__tests__/fingerprint-regression.test.ts +575 -566
- package/src/__tests__/helpers/conversation-history.smoke.test.ts +271 -271
- package/src/__tests__/helpers/conversation-history.ts +119 -119
- package/src/__tests__/helpers/deferred.smoke.test.ts +103 -103
- package/src/__tests__/helpers/deferred.ts +69 -69
- package/src/__tests__/helpers/in-memory-storage.smoke.test.ts +155 -155
- package/src/__tests__/helpers/in-memory-storage.ts +88 -88
- package/src/__tests__/helpers/mock-bun-proxy.smoke.test.ts +68 -68
- package/src/__tests__/helpers/mock-bun-proxy.ts +189 -189
- package/src/__tests__/helpers/plugin-fetch-harness.smoke.test.ts +273 -273
- package/src/__tests__/helpers/plugin-fetch-harness.ts +288 -288
- package/src/__tests__/helpers/sse.smoke.test.ts +236 -236
- package/src/__tests__/helpers/sse.ts +209 -209
- package/src/__tests__/index.parallel.test.ts +605 -595
- package/src/__tests__/sanitization-regex.test.ts +112 -112
- package/src/__tests__/state-bounds.test.ts +90 -90
- package/src/account-identity.test.ts +197 -192
- package/src/account-identity.ts +69 -67
- package/src/account-state.test.ts +86 -86
- package/src/account-state.ts +25 -25
- package/src/accounts/matching.test.ts +335 -0
- package/src/accounts/matching.ts +167 -0
- package/src/accounts/persistence.test.ts +345 -0
- package/src/accounts/persistence.ts +432 -0
- package/src/accounts/repair.test.ts +276 -0
- package/src/accounts/repair.ts +407 -0
- package/src/accounts.dedup.test.ts +621 -621
- package/src/accounts.test.ts +933 -929
- package/src/accounts.ts +633 -989
- package/src/backoff.test.ts +345 -345
- package/src/backoff.ts +219 -219
- package/src/betas.ts +124 -124
- package/src/bun-fetch.test.ts +345 -342
- package/src/bun-fetch.ts +424 -424
- package/src/bun-proxy.test.ts +25 -25
- package/src/bun-proxy.ts +209 -209
- package/src/cc-credentials.ts +111 -111
- package/src/circuit-breaker.test.ts +184 -184
- package/src/circuit-breaker.ts +169 -169
- package/src/cli/commands/auth.ts +963 -0
- package/src/cli/commands/config.ts +547 -0
- package/src/cli/formatting.test.ts +406 -0
- package/src/cli/formatting.ts +219 -0
- package/src/cli.ts +255 -2022
- package/src/commands/handlers/betas.ts +100 -0
- package/src/commands/handlers/config.ts +99 -0
- package/src/commands/handlers/files.ts +375 -0
- package/src/commands/oauth-flow.ts +181 -166
- package/src/commands/prompts.ts +61 -61
- package/src/commands/router.test.ts +421 -0
- package/src/commands/router.ts +143 -635
- package/src/config.test.ts +482 -482
- package/src/config.ts +412 -404
- package/src/constants.ts +48 -48
- package/src/drift/cch-constants.ts +95 -95
- package/src/env.ts +111 -105
- package/src/headers/billing.ts +33 -33
- package/src/headers/builder.ts +130 -130
- package/src/headers/cch.ts +75 -75
- package/src/headers/stainless.ts +25 -25
- package/src/headers/user-agent.ts +23 -23
- package/src/index.ts +436 -828
- package/src/models.ts +27 -27
- package/src/oauth.test.ts +102 -102
- package/src/oauth.ts +178 -178
- package/src/parent-pid-watcher.test.ts +148 -148
- package/src/parent-pid-watcher.ts +69 -69
- package/src/plugin-helpers.ts +82 -82
- package/src/refresh-helpers.ts +145 -139
- package/src/refresh-lock.test.ts +94 -94
- package/src/refresh-lock.ts +93 -93
- package/src/request/body.history.test.ts +579 -571
- package/src/request/body.ts +255 -255
- package/src/request/metadata.ts +65 -65
- package/src/request/retry.test.ts +156 -156
- package/src/request/retry.ts +67 -67
- package/src/request/url.ts +21 -21
- package/src/request-orchestration-helpers.ts +648 -0
- package/src/response/index.ts +5 -5
- package/src/response/mcp.ts +58 -58
- package/src/response/streaming.test.ts +313 -311
- package/src/response/streaming.ts +412 -410
- package/src/rotation.test.ts +304 -301
- package/src/rotation.ts +205 -205
- package/src/storage.test.ts +547 -547
- package/src/storage.ts +315 -291
- package/src/system-prompt/builder.ts +38 -38
- package/src/system-prompt/index.ts +5 -5
- package/src/system-prompt/normalize.ts +60 -60
- package/src/system-prompt/sanitize.ts +30 -30
- package/src/thinking.ts +21 -20
- package/src/token-refresh.test.ts +265 -265
- package/src/token-refresh.ts +219 -214
- package/src/types.ts +30 -30
- package/dist/bun-proxy.mjs +0 -291
|
@@ -1,243 +1,243 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
3
|
+
encodeSSEEvent,
|
|
4
|
+
encodeSSEStream,
|
|
5
|
+
chunkUtf8AtOffsets,
|
|
6
|
+
makeSSEResponse,
|
|
7
|
+
makeTruncatedSSEResponse,
|
|
8
|
+
makeMalformedSSEResponse,
|
|
9
|
+
messageStartEvent,
|
|
10
|
+
contentBlockStartEvent,
|
|
11
|
+
contentBlockDeltaEvent,
|
|
12
|
+
contentBlockStopEvent,
|
|
13
|
+
messageDeltaEvent,
|
|
14
|
+
messageStopEvent,
|
|
15
|
+
errorEvent,
|
|
16
16
|
} from "./sse";
|
|
17
17
|
|
|
18
18
|
describe("sse helpers", () => {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
19
|
+
describe("encodeSSEEvent", () => {
|
|
20
|
+
it("formats a basic event with data", () => {
|
|
21
|
+
const event = { data: "hello world" };
|
|
22
|
+
const encoded = encodeSSEEvent(event);
|
|
23
|
+
expect(encoded).toBe("data: hello world\n\n");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("includes event type when provided", () => {
|
|
27
|
+
const event = { event: "message", data: "hello" };
|
|
28
|
+
const encoded = encodeSSEEvent(event);
|
|
29
|
+
expect(encoded).toBe("event: message\ndata: hello\n\n");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("splits multiline data into multiple data lines", () => {
|
|
33
|
+
const event = { data: "line1\nline2\nline3" };
|
|
34
|
+
const encoded = encodeSSEEvent(event);
|
|
35
|
+
expect(encoded).toBe("data: line1\ndata: line2\ndata: line3\n\n");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("includes id when provided", () => {
|
|
39
|
+
const event = { data: "hello", id: "123" };
|
|
40
|
+
const encoded = encodeSSEEvent(event);
|
|
41
|
+
expect(encoded).toBe("data: hello\nid: 123\n\n");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("includes retry when provided", () => {
|
|
45
|
+
const event = { data: "hello", retry: 5000 };
|
|
46
|
+
const encoded = encodeSSEEvent(event);
|
|
47
|
+
expect(encoded).toBe("data: hello\nretry: 5000\n\n");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("includes all fields when provided", () => {
|
|
51
|
+
const event = { event: "message", data: "hello", id: "123", retry: 5000 };
|
|
52
|
+
const encoded = encodeSSEEvent(event);
|
|
53
|
+
expect(encoded).toBe("event: message\ndata: hello\nid: 123\nretry: 5000\n\n");
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe("encodeSSEStream", () => {
|
|
58
|
+
it("joins multiple events", () => {
|
|
59
|
+
const events = [{ data: "event1" }, { data: "event2" }];
|
|
60
|
+
const encoded = encodeSSEStream(events);
|
|
61
|
+
expect(encoded).toBe("data: event1\n\ndata: event2\n\n");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("returns empty string for empty events array", () => {
|
|
65
|
+
const encoded = encodeSSEStream([]);
|
|
66
|
+
expect(encoded).toBe("");
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe("chunkUtf8AtOffsets", () => {
|
|
71
|
+
it("splits ASCII text at byte offsets", () => {
|
|
72
|
+
const chunks = chunkUtf8AtOffsets("hello world", [5, 8]);
|
|
73
|
+
const decoder = new TextDecoder();
|
|
74
|
+
expect(chunks.map((c) => decoder.decode(c))).toEqual(["hello", " wo", "rld"]);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("handles multi-byte UTF-8 characters safely", () => {
|
|
78
|
+
const chunks = chunkUtf8AtOffsets("café", [1, 2, 3]);
|
|
79
|
+
const decoder = new TextDecoder();
|
|
80
|
+
const decoded = chunks.map((c) => decoder.decode(c));
|
|
81
|
+
expect(decoded).toEqual(["c", "a", "f", "é"]);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("handles emoji (4-byte UTF-8)", () => {
|
|
85
|
+
const chunks = chunkUtf8AtOffsets("hello 🎉 world", [5, 6]);
|
|
86
|
+
const decoder = new TextDecoder();
|
|
87
|
+
const decoded = chunks.map((c) => decoder.decode(c));
|
|
88
|
+
expect(decoded).toEqual(["hello", " ", "🎉 world"]);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("deduplicates and sorts offsets", () => {
|
|
92
|
+
const chunks = chunkUtf8AtOffsets("hello", [3, 1, 3, 2]);
|
|
93
|
+
const decoder = new TextDecoder();
|
|
94
|
+
expect(chunks.map((c) => decoder.decode(c))).toEqual(["h", "e", "l", "lo"]);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("ignores offsets beyond string length", () => {
|
|
98
|
+
const chunks = chunkUtf8AtOffsets("hi", [1, 5, 10]);
|
|
99
|
+
const decoder = new TextDecoder();
|
|
100
|
+
expect(chunks.map((c) => decoder.decode(c))).toEqual(["h", "i"]);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("ignores offsets at or before current position", () => {
|
|
104
|
+
const chunks = chunkUtf8AtOffsets("hello", [0, 0, 1]);
|
|
105
|
+
const decoder = new TextDecoder();
|
|
106
|
+
expect(chunks.map((c) => decoder.decode(c))).toEqual(["h", "ello"]);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe("makeSSEResponse", () => {
|
|
111
|
+
it("creates response with text/event-stream content-type", () => {
|
|
112
|
+
const response = makeSSEResponse("data: hello\n\n");
|
|
113
|
+
expect(response.headers.get("content-type")).toBe("text/event-stream");
|
|
114
|
+
expect(response.status).toBe(200);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("accepts custom status code", () => {
|
|
118
|
+
const response = makeSSEResponse("data: hello\n\n", { status: 201 });
|
|
119
|
+
expect(response.status).toBe(201);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("accepts ReadableStream body", async () => {
|
|
123
|
+
const encoder = new TextEncoder();
|
|
124
|
+
const stream = new ReadableStream<Uint8Array>({
|
|
125
|
+
start(controller) {
|
|
126
|
+
controller.enqueue(encoder.encode("data: hello\n\n"));
|
|
127
|
+
controller.close();
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
const response = makeSSEResponse(stream);
|
|
131
|
+
const text = await response.text();
|
|
132
|
+
expect(text).toBe("data: hello\n\n");
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe("makeTruncatedSSEResponse", () => {
|
|
137
|
+
it("emits only specified number of events", async () => {
|
|
138
|
+
const events = [{ data: "event1" }, { data: "event2" }, { data: "event3" }];
|
|
139
|
+
const response = makeTruncatedSSEResponse(events, 2);
|
|
140
|
+
const text = await response.text();
|
|
141
|
+
expect(text).toContain("data: event1");
|
|
142
|
+
expect(text).toContain("data: event2");
|
|
143
|
+
expect(text).not.toContain("data: event3");
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("has text/event-stream content-type", () => {
|
|
147
|
+
const events = [{ data: "event1" }];
|
|
148
|
+
const response = makeTruncatedSSEResponse(events, 1);
|
|
149
|
+
expect(response.headers.get("content-type")).toBe("text/event-stream");
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
describe("makeMalformedSSEResponse", () => {
|
|
154
|
+
it("emits malformed content directly", async () => {
|
|
155
|
+
const response = makeMalformedSSEResponse("not valid sse");
|
|
156
|
+
const text = await response.text();
|
|
157
|
+
expect(text).toBe("not valid sse");
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("has text/event-stream content-type", () => {
|
|
161
|
+
const response = makeMalformedSSEResponse("malformed");
|
|
162
|
+
expect(response.headers.get("content-type")).toBe("text/event-stream");
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
describe("typed event factories", () => {
|
|
167
|
+
describe("messageStartEvent", () => {
|
|
168
|
+
it("creates a message_start event", () => {
|
|
169
|
+
const event = messageStartEvent();
|
|
170
|
+
const parsed = JSON.parse(event.data);
|
|
171
|
+
expect(parsed.type).toBe("message_start");
|
|
172
|
+
expect(parsed.message.role).toBe("assistant");
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("accepts overrides", () => {
|
|
176
|
+
const event = messageStartEvent({ message: { id: "custom_id" } as any });
|
|
177
|
+
const parsed = JSON.parse(event.data);
|
|
178
|
+
expect(parsed.message.id).toBe("custom_id");
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
describe("contentBlockStartEvent", () => {
|
|
183
|
+
it("creates a content_block_start event", () => {
|
|
184
|
+
const event = contentBlockStartEvent(0);
|
|
185
|
+
const parsed = JSON.parse(event.data);
|
|
186
|
+
expect(parsed.type).toBe("content_block_start");
|
|
187
|
+
expect(parsed.index).toBe(0);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("accepts overrides", () => {
|
|
191
|
+
const event = contentBlockStartEvent(1, { content_block: { type: "tool_use", name: "read_file" } });
|
|
192
|
+
const parsed = JSON.parse(event.data);
|
|
193
|
+
expect(parsed.index).toBe(1);
|
|
194
|
+
expect(parsed.content_block.type).toBe("tool_use");
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
describe("contentBlockDeltaEvent", () => {
|
|
199
|
+
it("creates a content_block_delta event", () => {
|
|
200
|
+
const event = contentBlockDeltaEvent(0, "hello");
|
|
201
|
+
const parsed = JSON.parse(event.data);
|
|
202
|
+
expect(parsed.type).toBe("content_block_delta");
|
|
203
|
+
expect(parsed.delta.text).toBe("hello");
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
describe("contentBlockStopEvent", () => {
|
|
208
|
+
it("creates a content_block_stop event", () => {
|
|
209
|
+
const event = contentBlockStopEvent(0);
|
|
210
|
+
const parsed = JSON.parse(event.data);
|
|
211
|
+
expect(parsed.type).toBe("content_block_stop");
|
|
212
|
+
expect(parsed.index).toBe(0);
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
describe("messageDeltaEvent", () => {
|
|
217
|
+
it("creates a message_delta event", () => {
|
|
218
|
+
const event = messageDeltaEvent();
|
|
219
|
+
const parsed = JSON.parse(event.data);
|
|
220
|
+
expect(parsed.type).toBe("message_delta");
|
|
221
|
+
expect(parsed.delta.stop_reason).toBe("end_turn");
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
describe("messageStopEvent", () => {
|
|
226
|
+
it("creates a message_stop event", () => {
|
|
227
|
+
const event = messageStopEvent();
|
|
228
|
+
const parsed = JSON.parse(event.data);
|
|
229
|
+
expect(parsed.type).toBe("message_stop");
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
describe("errorEvent", () => {
|
|
234
|
+
it("creates an error event", () => {
|
|
235
|
+
const event = errorEvent("rate_limit_error", "Rate limit exceeded");
|
|
236
|
+
const parsed = JSON.parse(event.data);
|
|
237
|
+
expect(parsed.type).toBe("error");
|
|
238
|
+
expect(parsed.error.type).toBe("rate_limit_error");
|
|
239
|
+
expect(parsed.error.message).toBe("Rate limit exceeded");
|
|
240
|
+
});
|
|
241
|
+
});
|
|
24
242
|
});
|
|
25
|
-
|
|
26
|
-
it("includes event type when provided", () => {
|
|
27
|
-
const event = { event: "message", data: "hello" };
|
|
28
|
-
const encoded = encodeSSEEvent(event);
|
|
29
|
-
expect(encoded).toBe("event: message\ndata: hello\n\n");
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it("splits multiline data into multiple data lines", () => {
|
|
33
|
-
const event = { data: "line1\nline2\nline3" };
|
|
34
|
-
const encoded = encodeSSEEvent(event);
|
|
35
|
-
expect(encoded).toBe("data: line1\ndata: line2\ndata: line3\n\n");
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it("includes id when provided", () => {
|
|
39
|
-
const event = { data: "hello", id: "123" };
|
|
40
|
-
const encoded = encodeSSEEvent(event);
|
|
41
|
-
expect(encoded).toBe("data: hello\nid: 123\n\n");
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it("includes retry when provided", () => {
|
|
45
|
-
const event = { data: "hello", retry: 5000 };
|
|
46
|
-
const encoded = encodeSSEEvent(event);
|
|
47
|
-
expect(encoded).toBe("data: hello\nretry: 5000\n\n");
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it("includes all fields when provided", () => {
|
|
51
|
-
const event = { event: "message", data: "hello", id: "123", retry: 5000 };
|
|
52
|
-
const encoded = encodeSSEEvent(event);
|
|
53
|
-
expect(encoded).toBe("event: message\ndata: hello\nid: 123\nretry: 5000\n\n");
|
|
54
|
-
});
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
describe("encodeSSEStream", () => {
|
|
58
|
-
it("joins multiple events", () => {
|
|
59
|
-
const events = [{ data: "event1" }, { data: "event2" }];
|
|
60
|
-
const encoded = encodeSSEStream(events);
|
|
61
|
-
expect(encoded).toBe("data: event1\n\ndata: event2\n\n");
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it("returns empty string for empty events array", () => {
|
|
65
|
-
const encoded = encodeSSEStream([]);
|
|
66
|
-
expect(encoded).toBe("");
|
|
67
|
-
});
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
describe("chunkUtf8AtOffsets", () => {
|
|
71
|
-
it("splits ASCII text at byte offsets", () => {
|
|
72
|
-
const chunks = chunkUtf8AtOffsets("hello world", [5, 8]);
|
|
73
|
-
const decoder = new TextDecoder();
|
|
74
|
-
expect(chunks.map((c) => decoder.decode(c))).toEqual(["hello", " wo", "rld"]);
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it("handles multi-byte UTF-8 characters safely", () => {
|
|
78
|
-
const chunks = chunkUtf8AtOffsets("café", [1, 2, 3]);
|
|
79
|
-
const decoder = new TextDecoder();
|
|
80
|
-
const decoded = chunks.map((c) => decoder.decode(c));
|
|
81
|
-
expect(decoded).toEqual(["c", "a", "f", "é"]);
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it("handles emoji (4-byte UTF-8)", () => {
|
|
85
|
-
const chunks = chunkUtf8AtOffsets("hello 🎉 world", [5, 6]);
|
|
86
|
-
const decoder = new TextDecoder();
|
|
87
|
-
const decoded = chunks.map((c) => decoder.decode(c));
|
|
88
|
-
expect(decoded).toEqual(["hello", " ", "🎉 world"]);
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it("deduplicates and sorts offsets", () => {
|
|
92
|
-
const chunks = chunkUtf8AtOffsets("hello", [3, 1, 3, 2]);
|
|
93
|
-
const decoder = new TextDecoder();
|
|
94
|
-
expect(chunks.map((c) => decoder.decode(c))).toEqual(["h", "e", "l", "lo"]);
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
it("ignores offsets beyond string length", () => {
|
|
98
|
-
const chunks = chunkUtf8AtOffsets("hi", [1, 5, 10]);
|
|
99
|
-
const decoder = new TextDecoder();
|
|
100
|
-
expect(chunks.map((c) => decoder.decode(c))).toEqual(["h", "i"]);
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it("ignores offsets at or before current position", () => {
|
|
104
|
-
const chunks = chunkUtf8AtOffsets("hello", [0, 0, 1]);
|
|
105
|
-
const decoder = new TextDecoder();
|
|
106
|
-
expect(chunks.map((c) => decoder.decode(c))).toEqual(["h", "ello"]);
|
|
107
|
-
});
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
describe("makeSSEResponse", () => {
|
|
111
|
-
it("creates response with text/event-stream content-type", () => {
|
|
112
|
-
const response = makeSSEResponse("data: hello\n\n");
|
|
113
|
-
expect(response.headers.get("content-type")).toBe("text/event-stream");
|
|
114
|
-
expect(response.status).toBe(200);
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
it("accepts custom status code", () => {
|
|
118
|
-
const response = makeSSEResponse("data: hello\n\n", { status: 201 });
|
|
119
|
-
expect(response.status).toBe(201);
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
it("accepts ReadableStream body", async () => {
|
|
123
|
-
const encoder = new TextEncoder();
|
|
124
|
-
const stream = new ReadableStream<Uint8Array>({
|
|
125
|
-
start(controller) {
|
|
126
|
-
controller.enqueue(encoder.encode("data: hello\n\n"));
|
|
127
|
-
controller.close();
|
|
128
|
-
},
|
|
129
|
-
});
|
|
130
|
-
const response = makeSSEResponse(stream);
|
|
131
|
-
const text = await response.text();
|
|
132
|
-
expect(text).toBe("data: hello\n\n");
|
|
133
|
-
});
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
describe("makeTruncatedSSEResponse", () => {
|
|
137
|
-
it("emits only specified number of events", async () => {
|
|
138
|
-
const events = [{ data: "event1" }, { data: "event2" }, { data: "event3" }];
|
|
139
|
-
const response = makeTruncatedSSEResponse(events, 2);
|
|
140
|
-
const text = await response.text();
|
|
141
|
-
expect(text).toContain("data: event1");
|
|
142
|
-
expect(text).toContain("data: event2");
|
|
143
|
-
expect(text).not.toContain("data: event3");
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
it("has text/event-stream content-type", () => {
|
|
147
|
-
const events = [{ data: "event1" }];
|
|
148
|
-
const response = makeTruncatedSSEResponse(events, 1);
|
|
149
|
-
expect(response.headers.get("content-type")).toBe("text/event-stream");
|
|
150
|
-
});
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
describe("makeMalformedSSEResponse", () => {
|
|
154
|
-
it("emits malformed content directly", async () => {
|
|
155
|
-
const response = makeMalformedSSEResponse("not valid sse");
|
|
156
|
-
const text = await response.text();
|
|
157
|
-
expect(text).toBe("not valid sse");
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
it("has text/event-stream content-type", () => {
|
|
161
|
-
const response = makeMalformedSSEResponse("malformed");
|
|
162
|
-
expect(response.headers.get("content-type")).toBe("text/event-stream");
|
|
163
|
-
});
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
describe("typed event factories", () => {
|
|
167
|
-
describe("messageStartEvent", () => {
|
|
168
|
-
it("creates a message_start event", () => {
|
|
169
|
-
const event = messageStartEvent();
|
|
170
|
-
const parsed = JSON.parse(event.data);
|
|
171
|
-
expect(parsed.type).toBe("message_start");
|
|
172
|
-
expect(parsed.message.role).toBe("assistant");
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
it("accepts overrides", () => {
|
|
176
|
-
const event = messageStartEvent({ message: { id: "custom_id" } as any });
|
|
177
|
-
const parsed = JSON.parse(event.data);
|
|
178
|
-
expect(parsed.message.id).toBe("custom_id");
|
|
179
|
-
});
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
describe("contentBlockStartEvent", () => {
|
|
183
|
-
it("creates a content_block_start event", () => {
|
|
184
|
-
const event = contentBlockStartEvent(0);
|
|
185
|
-
const parsed = JSON.parse(event.data);
|
|
186
|
-
expect(parsed.type).toBe("content_block_start");
|
|
187
|
-
expect(parsed.index).toBe(0);
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
it("accepts overrides", () => {
|
|
191
|
-
const event = contentBlockStartEvent(1, { content_block: { type: "tool_use", name: "read_file" } });
|
|
192
|
-
const parsed = JSON.parse(event.data);
|
|
193
|
-
expect(parsed.index).toBe(1);
|
|
194
|
-
expect(parsed.content_block.type).toBe("tool_use");
|
|
195
|
-
});
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
describe("contentBlockDeltaEvent", () => {
|
|
199
|
-
it("creates a content_block_delta event", () => {
|
|
200
|
-
const event = contentBlockDeltaEvent(0, "hello");
|
|
201
|
-
const parsed = JSON.parse(event.data);
|
|
202
|
-
expect(parsed.type).toBe("content_block_delta");
|
|
203
|
-
expect(parsed.delta.text).toBe("hello");
|
|
204
|
-
});
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
describe("contentBlockStopEvent", () => {
|
|
208
|
-
it("creates a content_block_stop event", () => {
|
|
209
|
-
const event = contentBlockStopEvent(0);
|
|
210
|
-
const parsed = JSON.parse(event.data);
|
|
211
|
-
expect(parsed.type).toBe("content_block_stop");
|
|
212
|
-
expect(parsed.index).toBe(0);
|
|
213
|
-
});
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
describe("messageDeltaEvent", () => {
|
|
217
|
-
it("creates a message_delta event", () => {
|
|
218
|
-
const event = messageDeltaEvent();
|
|
219
|
-
const parsed = JSON.parse(event.data);
|
|
220
|
-
expect(parsed.type).toBe("message_delta");
|
|
221
|
-
expect(parsed.delta.stop_reason).toBe("end_turn");
|
|
222
|
-
});
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
describe("messageStopEvent", () => {
|
|
226
|
-
it("creates a message_stop event", () => {
|
|
227
|
-
const event = messageStopEvent();
|
|
228
|
-
const parsed = JSON.parse(event.data);
|
|
229
|
-
expect(parsed.type).toBe("message_stop");
|
|
230
|
-
});
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
describe("errorEvent", () => {
|
|
234
|
-
it("creates an error event", () => {
|
|
235
|
-
const event = errorEvent("rate_limit_error", "Rate limit exceeded");
|
|
236
|
-
const parsed = JSON.parse(event.data);
|
|
237
|
-
expect(parsed.type).toBe("error");
|
|
238
|
-
expect(parsed.error.type).toBe("rate_limit_error");
|
|
239
|
-
expect(parsed.error.message).toBe("Rate limit exceeded");
|
|
240
|
-
});
|
|
241
|
-
});
|
|
242
|
-
});
|
|
243
243
|
});
|