@ex-machina/opencode-anthropic-auth 1.2.0 → 1.4.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.
@@ -1,621 +0,0 @@
1
- import { afterEach, describe, expect, mock, test } from 'bun:test';
2
- import { CLAUDE_CODE_IDENTITY, OPENCODE_IDENTITY, REQUIRED_BETAS, } from '../constants';
3
- import { createStrippedStream, isInsecure, mergeBetaHeaders, mergeHeaders, prefixToolNames, prependClaudeCodeIdentity, rewriteRequestBody, rewriteUrl, sanitizeSystemText, setOAuthHeaders, stripToolPrefix, } from '../transform';
4
- describe('mergeHeaders', () => {
5
- test('copies headers from a Request object', () => {
6
- const request = new Request('https://example.com', {
7
- headers: { 'x-custom': 'value' },
8
- });
9
- const headers = mergeHeaders(request);
10
- expect(headers.get('x-custom')).toBe('value');
11
- });
12
- test('copies headers from init Headers object', () => {
13
- const headers = mergeHeaders('https://example.com', {
14
- headers: new Headers({ 'x-init': 'from-headers' }),
15
- });
16
- expect(headers.get('x-init')).toBe('from-headers');
17
- });
18
- test('copies headers from init array', () => {
19
- const headers = mergeHeaders('https://example.com', {
20
- headers: [['x-arr', 'from-array']],
21
- });
22
- expect(headers.get('x-arr')).toBe('from-array');
23
- });
24
- test('copies headers from init plain object', () => {
25
- const headers = mergeHeaders('https://example.com', {
26
- headers: { 'x-obj': 'from-object' },
27
- });
28
- expect(headers.get('x-obj')).toBe('from-object');
29
- });
30
- test('init headers override Request headers', () => {
31
- const request = new Request('https://example.com', {
32
- headers: { 'x-key': 'from-request' },
33
- });
34
- const headers = mergeHeaders(request, {
35
- headers: { 'x-key': 'from-init' },
36
- });
37
- expect(headers.get('x-key')).toBe('from-init');
38
- });
39
- test('handles string input without init', () => {
40
- const headers = mergeHeaders('https://example.com');
41
- expect([...headers.entries()]).toHaveLength(0);
42
- });
43
- test('handles URL input', () => {
44
- const headers = mergeHeaders(new URL('https://example.com'));
45
- expect([...headers.entries()]).toHaveLength(0);
46
- });
47
- });
48
- describe('mergeBetaHeaders', () => {
49
- test('includes required betas when no incoming betas', () => {
50
- const headers = new Headers();
51
- const result = mergeBetaHeaders(headers);
52
- expect(result).toBe(REQUIRED_BETAS.join(','));
53
- });
54
- test('merges incoming betas with required betas', () => {
55
- const headers = new Headers({ 'anthropic-beta': 'custom-beta-1' });
56
- const result = mergeBetaHeaders(headers);
57
- for (const beta of REQUIRED_BETAS) {
58
- expect(result).toContain(beta);
59
- }
60
- expect(result).toContain('custom-beta-1');
61
- });
62
- test('deduplicates betas', () => {
63
- const beta = REQUIRED_BETAS[0] ?? '';
64
- const headers = new Headers({
65
- 'anthropic-beta': beta,
66
- });
67
- const result = mergeBetaHeaders(headers);
68
- const parts = result.split(',');
69
- const occurrences = parts.filter((p) => p === REQUIRED_BETAS[0]);
70
- expect(occurrences).toHaveLength(1);
71
- });
72
- test('handles comma-separated incoming betas', () => {
73
- const headers = new Headers({
74
- 'anthropic-beta': 'beta-a, beta-b',
75
- });
76
- const result = mergeBetaHeaders(headers);
77
- expect(result).toContain('beta-a');
78
- expect(result).toContain('beta-b');
79
- });
80
- });
81
- describe('setOAuthHeaders', () => {
82
- test('sets authorization bearer token', () => {
83
- const headers = new Headers();
84
- setOAuthHeaders(headers, 'my-token');
85
- expect(headers.get('authorization')).toBe('Bearer my-token');
86
- });
87
- test('sets user-agent', () => {
88
- const headers = new Headers();
89
- setOAuthHeaders(headers, 'token');
90
- expect(headers.get('user-agent')).toContain('claude-cli');
91
- });
92
- test('removes x-api-key', () => {
93
- const headers = new Headers({ 'x-api-key': 'sk-ant-xxx' });
94
- setOAuthHeaders(headers, 'token');
95
- expect(headers.get('x-api-key')).toBeNull();
96
- });
97
- test('sets anthropic-beta header', () => {
98
- const headers = new Headers();
99
- setOAuthHeaders(headers, 'token');
100
- expect(headers.get('anthropic-beta')).toBeString();
101
- for (const beta of REQUIRED_BETAS) {
102
- expect(headers.get('anthropic-beta')).toContain(beta);
103
- }
104
- });
105
- });
106
- describe('prefixToolNames', () => {
107
- test('prefixes tool definition names', () => {
108
- const body = JSON.stringify({
109
- tools: [
110
- { name: 'read_file', type: 'function' },
111
- { name: 'write_file', type: 'function' },
112
- ],
113
- });
114
- const result = JSON.parse(prefixToolNames(body));
115
- expect(result.tools[0].name).toBe('mcp_read_file');
116
- expect(result.tools[1].name).toBe('mcp_write_file');
117
- });
118
- test('prefixes tool_use block names in messages', () => {
119
- const body = JSON.stringify({
120
- messages: [
121
- {
122
- role: 'assistant',
123
- content: [
124
- { type: 'tool_use', name: 'bash', id: '1' },
125
- { type: 'text', text: 'hello' },
126
- ],
127
- },
128
- ],
129
- });
130
- const result = JSON.parse(prefixToolNames(body));
131
- expect(result.messages[0].content[0].name).toBe('mcp_bash');
132
- expect(result.messages[0].content[1].type).toBe('text');
133
- });
134
- test('does not prefix non-tool_use blocks', () => {
135
- const body = JSON.stringify({
136
- messages: [
137
- {
138
- role: 'user',
139
- content: [{ type: 'text', text: 'hello' }],
140
- },
141
- ],
142
- });
143
- const result = JSON.parse(prefixToolNames(body));
144
- expect(result.messages[0].content[0]).toEqual({
145
- type: 'text',
146
- text: 'hello',
147
- });
148
- });
149
- test('handles missing tools and messages gracefully', () => {
150
- const body = JSON.stringify({ model: 'claude-3' });
151
- const result = JSON.parse(prefixToolNames(body));
152
- expect(result.model).toBe('claude-3');
153
- });
154
- test('returns original string on invalid JSON', () => {
155
- const body = 'not valid json';
156
- expect(prefixToolNames(body)).toBe(body);
157
- });
158
- test('handles tools without names', () => {
159
- const body = JSON.stringify({
160
- tools: [{ type: 'function' }],
161
- });
162
- const result = JSON.parse(prefixToolNames(body));
163
- expect(result.tools[0].name).toBeUndefined();
164
- });
165
- });
166
- describe('stripToolPrefix', () => {
167
- test('strips mcp_ prefix from tool names', () => {
168
- const text = '{"name": "mcp_read_file"}';
169
- expect(stripToolPrefix(text)).toBe('{"name": "read_file"}');
170
- });
171
- test('strips multiple prefixed names', () => {
172
- const text = '{"name": "mcp_tool_a"} and {"name": "mcp_tool_b"}';
173
- const result = stripToolPrefix(text);
174
- expect(result).toContain('"name": "tool_a"');
175
- expect(result).toContain('"name": "tool_b"');
176
- });
177
- test('does not strip names without mcp_ prefix', () => {
178
- const text = '{"name": "regular_tool"}';
179
- expect(stripToolPrefix(text)).toBe(text);
180
- });
181
- test('handles whitespace variations in JSON', () => {
182
- const text = '{"name" : "mcp_tool"}';
183
- expect(stripToolPrefix(text)).toBe('{"name": "tool"}');
184
- });
185
- });
186
- describe('rewriteUrl', () => {
187
- const originalEnv = process.env.ANTHROPIC_BASE_URL;
188
- afterEach(() => {
189
- if (originalEnv === undefined) {
190
- delete process.env.ANTHROPIC_BASE_URL;
191
- }
192
- else {
193
- process.env.ANTHROPIC_BASE_URL = originalEnv;
194
- }
195
- });
196
- test('adds beta=true to /v1/messages URL string', () => {
197
- const { input } = rewriteUrl('https://api.anthropic.com/v1/messages');
198
- const url = new URL(input.toString());
199
- expect(url.searchParams.get('beta')).toBe('true');
200
- });
201
- test('adds beta=true to /v1/messages URL object', () => {
202
- const { input } = rewriteUrl(new URL('https://api.anthropic.com/v1/messages'));
203
- const url = input instanceof URL ? input : new URL(input.toString());
204
- expect(url.searchParams.get('beta')).toBe('true');
205
- });
206
- test('adds beta=true to /v1/messages Request', () => {
207
- const request = new Request('https://api.anthropic.com/v1/messages');
208
- const { input } = rewriteUrl(request);
209
- const url = new URL(input.url);
210
- expect(url.searchParams.get('beta')).toBe('true');
211
- });
212
- test('does not modify URL if beta param already exists', () => {
213
- const original = 'https://api.anthropic.com/v1/messages?beta=false';
214
- const { input } = rewriteUrl(original);
215
- const url = new URL(input.toString());
216
- expect(url.searchParams.get('beta')).toBe('false');
217
- });
218
- test('does not modify non-/v1/messages URLs', () => {
219
- const original = 'https://api.anthropic.com/v1/complete';
220
- const { input } = rewriteUrl(original);
221
- const url = new URL(input.toString());
222
- expect(url.searchParams.has('beta')).toBe(false);
223
- });
224
- test('overrides origin when ANTHROPIC_BASE_URL is set', () => {
225
- process.env.ANTHROPIC_BASE_URL = 'http://localhost:8080';
226
- const { input } = rewriteUrl('https://api.anthropic.com/v1/messages');
227
- const url = new URL(input.toString());
228
- expect(url.origin).toBe('http://localhost:8080');
229
- expect(url.pathname).toBe('/v1/messages');
230
- });
231
- test('preserves beta=true when ANTHROPIC_BASE_URL is set', () => {
232
- process.env.ANTHROPIC_BASE_URL = 'http://localhost:8080';
233
- const { input } = rewriteUrl('https://api.anthropic.com/v1/messages');
234
- const url = new URL(input.toString());
235
- expect(url.searchParams.get('beta')).toBe('true');
236
- });
237
- test('preserves existing query params when ANTHROPIC_BASE_URL is set', () => {
238
- process.env.ANTHROPIC_BASE_URL = 'http://localhost:8080';
239
- const { input } = rewriteUrl('https://api.anthropic.com/v1/messages?foo=bar');
240
- const url = new URL(input.toString());
241
- expect(url.origin).toBe('http://localhost:8080');
242
- expect(url.searchParams.get('foo')).toBe('bar');
243
- });
244
- test('handles ANTHROPIC_BASE_URL with trailing slash', () => {
245
- process.env.ANTHROPIC_BASE_URL = 'http://localhost:8080/';
246
- const { input } = rewriteUrl('https://api.anthropic.com/v1/messages');
247
- const url = new URL(input.toString());
248
- expect(url.pathname).toBe('/v1/messages');
249
- expect(url.origin).toBe('http://localhost:8080');
250
- });
251
- test('ignores invalid ANTHROPIC_BASE_URL', () => {
252
- process.env.ANTHROPIC_BASE_URL = 'not-a-url';
253
- const { input } = rewriteUrl('https://api.anthropic.com/v1/messages');
254
- const url = new URL(input.toString());
255
- expect(url.origin).toBe('https://api.anthropic.com');
256
- });
257
- test('ignores empty ANTHROPIC_BASE_URL', () => {
258
- process.env.ANTHROPIC_BASE_URL = '';
259
- const { input } = rewriteUrl('https://api.anthropic.com/v1/messages');
260
- const url = new URL(input.toString());
261
- expect(url.origin).toBe('https://api.anthropic.com');
262
- });
263
- test('rejects file: scheme in ANTHROPIC_BASE_URL', () => {
264
- process.env.ANTHROPIC_BASE_URL = 'file:///etc/passwd';
265
- const { input } = rewriteUrl('https://api.anthropic.com/v1/messages');
266
- const url = new URL(input.toString());
267
- expect(url.origin).toBe('https://api.anthropic.com');
268
- });
269
- test('rejects ANTHROPIC_BASE_URL with embedded credentials', () => {
270
- process.env.ANTHROPIC_BASE_URL = 'http://user:pass@localhost:8080';
271
- const { input } = rewriteUrl('https://api.anthropic.com/v1/messages');
272
- const url = new URL(input.toString());
273
- expect(url.origin).toBe('https://api.anthropic.com');
274
- });
275
- test('returns original input when no URL changes are needed', () => {
276
- const original = 'https://api.anthropic.com/v1/complete';
277
- const { input } = rewriteUrl(original);
278
- expect(input).toBe(original);
279
- });
280
- test('returns original Request when no URL changes are needed', () => {
281
- const request = new Request('https://api.anthropic.com/v1/complete');
282
- const { input } = rewriteUrl(request);
283
- expect(input).toBe(request);
284
- });
285
- test('overrides origin for Request input when ANTHROPIC_BASE_URL is set', () => {
286
- process.env.ANTHROPIC_BASE_URL = 'http://localhost:8080';
287
- const request = new Request('https://api.anthropic.com/v1/messages');
288
- const { input } = rewriteUrl(request);
289
- const url = new URL(input.url);
290
- expect(url.origin).toBe('http://localhost:8080');
291
- expect(url.pathname).toBe('/v1/messages');
292
- });
293
- });
294
- describe('isInsecure', () => {
295
- const originalBaseUrl = process.env.ANTHROPIC_BASE_URL;
296
- const originalInsecure = process.env.ANTHROPIC_INSECURE;
297
- afterEach(() => {
298
- if (originalBaseUrl === undefined) {
299
- delete process.env.ANTHROPIC_BASE_URL;
300
- }
301
- else {
302
- process.env.ANTHROPIC_BASE_URL = originalBaseUrl;
303
- }
304
- if (originalInsecure === undefined) {
305
- delete process.env.ANTHROPIC_INSECURE;
306
- }
307
- else {
308
- process.env.ANTHROPIC_INSECURE = originalInsecure;
309
- }
310
- });
311
- test('returns false when neither env var is set', () => {
312
- delete process.env.ANTHROPIC_BASE_URL;
313
- delete process.env.ANTHROPIC_INSECURE;
314
- expect(isInsecure()).toBe(false);
315
- });
316
- test('returns false when only ANTHROPIC_INSECURE is set (no base URL)', () => {
317
- delete process.env.ANTHROPIC_BASE_URL;
318
- process.env.ANTHROPIC_INSECURE = '1';
319
- expect(isInsecure()).toBe(false);
320
- });
321
- test('returns false when ANTHROPIC_BASE_URL is set but ANTHROPIC_INSECURE is not', () => {
322
- process.env.ANTHROPIC_BASE_URL = 'https://proxy.local';
323
- delete process.env.ANTHROPIC_INSECURE;
324
- expect(isInsecure()).toBe(false);
325
- });
326
- test('returns true when both are set and ANTHROPIC_INSECURE is "1"', () => {
327
- process.env.ANTHROPIC_BASE_URL = 'https://proxy.local';
328
- process.env.ANTHROPIC_INSECURE = '1';
329
- expect(isInsecure()).toBe(true);
330
- });
331
- test('returns true when ANTHROPIC_INSECURE is "true"', () => {
332
- process.env.ANTHROPIC_BASE_URL = 'https://proxy.local';
333
- process.env.ANTHROPIC_INSECURE = 'true';
334
- expect(isInsecure()).toBe(true);
335
- });
336
- test('returns false for other ANTHROPIC_INSECURE values', () => {
337
- process.env.ANTHROPIC_BASE_URL = 'https://proxy.local';
338
- process.env.ANTHROPIC_INSECURE = 'yes';
339
- expect(isInsecure()).toBe(false);
340
- });
341
- });
342
- describe('createStrippedStream', () => {
343
- test('strips tool prefixes from streamed response body', async () => {
344
- const chunks = [
345
- 'data: {"type":"content_block_start","content_block":{"type":"tool_use","name":"mcp_bash"}}\n\n',
346
- 'data: {"type":"content_block_start","content_block":{"type":"tool_use","name":"mcp_read"}}\n\n',
347
- ];
348
- const stream = new ReadableStream({
349
- start(controller) {
350
- const encoder = new TextEncoder();
351
- for (const chunk of chunks) {
352
- controller.enqueue(encoder.encode(chunk));
353
- }
354
- controller.close();
355
- },
356
- });
357
- const original = new Response(stream, { status: 200 });
358
- const stripped = createStrippedStream(original);
359
- const text = await stripped.text();
360
- expect(text).toContain('"name": "bash"');
361
- expect(text).toContain('"name": "read"');
362
- expect(text).not.toContain('mcp_bash');
363
- expect(text).not.toContain('mcp_read');
364
- });
365
- test('preserves response status and headers', async () => {
366
- const stream = new ReadableStream({
367
- start(controller) {
368
- controller.close();
369
- },
370
- });
371
- const original = new Response(stream, {
372
- status: 201,
373
- statusText: 'Created',
374
- headers: { 'x-custom': 'value' },
375
- });
376
- const stripped = createStrippedStream(original);
377
- expect(stripped.status).toBe(201);
378
- expect(stripped.headers.get('x-custom')).toBe('value');
379
- });
380
- test('returns original response if no body', () => {
381
- const original = new Response(null, { status: 204 });
382
- const result = createStrippedStream(original);
383
- expect(result).toBe(original);
384
- });
385
- });
386
- describe('sanitizeSystemText', () => {
387
- // A realistic OpenCode system prompt showing what gets stripped vs preserved.
388
- //
389
- // STRIPPED: everything from the identity line down to the first tail marker
390
- // KEPT: everything before the identity + everything from the tail marker on
391
- //
392
- // ┌─────────────────────────────────────────────────────────────┐
393
- // │ You are OpenCode, the best coding agent on the planet. │ ← STRIPPED
394
- // │ │
395
- // │ OpenCode-specific instructions, tool docs, etc. │ ← STRIPPED
396
- // │ All of this is removed. │ ← STRIPPED
397
- // │ │
398
- // │ Instructions from: ~/.config/opencode/preamble.md │ ← KEPT
399
- // │ Be concise. Prefer TypeScript. │ ← KEPT
400
- // │ │
401
- // │ # Code References │ ← KEPT
402
- // │ src/index.ts ... │ ← KEPT
403
- // └─────────────────────────────────────────────────────────────┘
404
- const REALISTIC_PROMPT = [
405
- 'You are OpenCode, the best coding agent on the planet.',
406
- '',
407
- 'You have access to tools for reading files, running commands,',
408
- 'and editing code. Always explain before acting.',
409
- '',
410
- 'Instructions from: ~/.config/opencode/preamble.md',
411
- 'Be concise. Prefer TypeScript.',
412
- '',
413
- '# Code References',
414
- 'src/index.ts (1-50)',
415
- ].join('\n');
416
- test('strips OpenCode section, keeps user instructions and code refs', () => {
417
- const result = sanitizeSystemText(REALISTIC_PROMPT);
418
- // Stripped
419
- expect(result).not.toContain('OpenCode');
420
- expect(result).not.toContain('explain before acting');
421
- // Kept
422
- expect(result).toContain('Instructions from: ~/.config/opencode/preamble.md');
423
- expect(result).toContain('Be concise. Prefer TypeScript.');
424
- expect(result).toContain('# Code References');
425
- expect(result).toContain('src/index.ts (1-50)');
426
- });
427
- test('returns text unchanged when OpenCode identity not present', () => {
428
- const text = 'Just a normal system prompt';
429
- expect(sanitizeSystemText(text)).toBe(text);
430
- });
431
- test('calls onError when no preserved tail marker is found', () => {
432
- const onError = mock(() => { });
433
- const text = [
434
- 'You are OpenCode, the best coding agent on the planet.',
435
- 'No markers at all in this prompt.',
436
- ].join('\n');
437
- const result = sanitizeSystemText(text, onError);
438
- expect(result).toBe(text);
439
- expect(onError).toHaveBeenCalledTimes(1);
440
- });
441
- test('preserves content before OpenCode identity', () => {
442
- const text = [
443
- 'Some prefix content',
444
- 'You are OpenCode, the best coding agent on the planet.',
445
- 'OpenCode stuff to strip',
446
- '# Code References',
447
- 'file contents',
448
- ].join('\n');
449
- const result = sanitizeSystemText(text);
450
- expect(result).toBe('Some prefix content\n# Code References\nfile contents');
451
- });
452
- test('prefers earliest tail marker (instructions before code refs)', () => {
453
- // "Instructions from:" appears before "# Code References",
454
- // so we cut there — keeping the user's custom instructions.
455
- const text = [
456
- 'You are OpenCode, the best coding agent on the planet.',
457
- 'OpenCode internal stuff',
458
- 'Instructions from: preamble.md',
459
- 'user-authored content',
460
- '# Code References',
461
- 'files',
462
- ].join('\n');
463
- const result = sanitizeSystemText(text);
464
- expect(result).not.toContain('OpenCode');
465
- expect(result).toContain('Instructions from: preamble.md');
466
- expect(result).toContain('user-authored content');
467
- expect(result).toContain('# Code References');
468
- });
469
- test('preserves instructions from command', () => {
470
- const text = [
471
- 'You are OpenCode, the best coding agent on the planet.',
472
- 'Internal details',
473
- 'Instructions from command: my-script',
474
- 'Script output here',
475
- '# Code References',
476
- ].join('\n');
477
- const result = sanitizeSystemText(text);
478
- expect(result).toContain('Instructions from command: my-script');
479
- expect(result).toContain('Script output here');
480
- });
481
- test('falls back to # Code References when no instruction markers', () => {
482
- const text = [
483
- 'You are OpenCode, the best coding agent on the planet.',
484
- 'Stuff to strip',
485
- '# Code References',
486
- 'src/main.ts',
487
- ].join('\n');
488
- const result = sanitizeSystemText(text);
489
- expect(result).toBe('# Code References\nsrc/main.ts');
490
- });
491
- });
492
- describe('prependClaudeCodeIdentity', () => {
493
- test('returns identity block for undefined system', () => {
494
- const result = prependClaudeCodeIdentity(undefined);
495
- expect(result).toEqual([{ type: 'text', text: CLAUDE_CODE_IDENTITY }]);
496
- });
497
- test('sanitizes and prepends for string system', () => {
498
- const result = prependClaudeCodeIdentity('Some assistant prompt');
499
- expect(result).toHaveLength(2);
500
- expect(result[0]?.text).toBe(CLAUDE_CODE_IDENTITY);
501
- expect(result[1]?.text).toBe('Some assistant prompt');
502
- });
503
- test('sanitizes array of text blocks', () => {
504
- const system = [
505
- {
506
- type: 'text',
507
- text: `${OPENCODE_IDENTITY}\nstuff\n# Code References\nrest`,
508
- },
509
- { type: 'text', text: 'other block' },
510
- ];
511
- const result = prependClaudeCodeIdentity(system);
512
- expect(result[0]?.text).toBe(CLAUDE_CODE_IDENTITY);
513
- expect(result[1]?.text).not.toContain(OPENCODE_IDENTITY);
514
- expect(result[1]?.text).toContain('# Code References');
515
- });
516
- test('does not double-prepend if identity already present', () => {
517
- const system = [
518
- { type: 'text', text: CLAUDE_CODE_IDENTITY },
519
- { type: 'text', text: 'other' },
520
- ];
521
- const result = prependClaudeCodeIdentity(system);
522
- expect(result).toHaveLength(2);
523
- expect(result[0]?.text).toBe(CLAUDE_CODE_IDENTITY);
524
- });
525
- test('handles string elements in array', () => {
526
- const system = ['some text', 'more text'];
527
- const result = prependClaudeCodeIdentity(system);
528
- expect(result[0]?.text).toBe(CLAUDE_CODE_IDENTITY);
529
- expect(result[1]).toEqual({ type: 'text', text: 'some text' });
530
- });
531
- });
532
- describe('rewriteRequestBody', () => {
533
- test('prefixes tool names and rewrites system prompt', () => {
534
- const body = JSON.stringify({
535
- tools: [{ name: 'bash', type: 'function' }],
536
- messages: [{ role: 'user', content: 'hello world test message' }],
537
- system: 'You are a helpful assistant.',
538
- });
539
- const result = JSON.parse(rewriteRequestBody(body));
540
- expect(result.tools[0].name).toBe('mcp_bash');
541
- expect(result.system[0].text).toContain(CLAUDE_CODE_IDENTITY);
542
- });
543
- test('handles missing system field', () => {
544
- const body = JSON.stringify({
545
- messages: [{ role: 'user', content: 'hi' }],
546
- });
547
- const result = JSON.parse(rewriteRequestBody(body));
548
- expect(result.system).toHaveLength(1);
549
- expect(result.system[0].text).toContain(CLAUDE_CODE_IDENTITY);
550
- });
551
- test('returns original string on invalid JSON', () => {
552
- const body = 'not valid json';
553
- expect(rewriteRequestBody(body)).toBe(body);
554
- });
555
- test('passes onError through to sanitization', () => {
556
- const onError = mock(() => { });
557
- const body = JSON.stringify({
558
- messages: [],
559
- system: `${OPENCODE_IDENTITY}\nno code refs marker here`,
560
- });
561
- rewriteRequestBody(body, onError);
562
- expect(onError).toHaveBeenCalledTimes(1);
563
- });
564
- test('rewrites realistic OpenCode request end-to-end', () => {
565
- // Input system prompt (array of blocks):
566
- // [0] "You are OpenCode..." + internal stuff + "# Code References\n..."
567
- // [1] "Additional context block"
568
- //
569
- // Expected output:
570
- // [0] Claude Code identity (prepended)
571
- // [1] "# Code References\n..." (OpenCode section stripped)
572
- // [2] "Additional context block" (untouched)
573
- const systemPrompt = [
574
- 'You are OpenCode, the best coding agent on the planet.',
575
- '',
576
- 'You have access to tools.',
577
- '',
578
- '# Code References',
579
- '',
580
- 'Here are some files.',
581
- ].join('\n');
582
- const body = JSON.stringify({
583
- tools: [
584
- { name: 'bash', type: 'function' },
585
- { name: 'read_file', type: 'function' },
586
- ],
587
- messages: [
588
- { role: 'user', content: 'Help me fix this bug' },
589
- {
590
- role: 'assistant',
591
- content: [
592
- { type: 'tool_use', name: 'bash', id: 'tool_1' },
593
- { type: 'text', text: 'Let me check' },
594
- ],
595
- },
596
- ],
597
- system: [
598
- { type: 'text', text: systemPrompt },
599
- { type: 'text', text: 'Additional context block' },
600
- ],
601
- });
602
- const result = JSON.parse(rewriteRequestBody(body));
603
- // System prompt rewritten
604
- expect(result.system[0].text).toBe(CLAUDE_CODE_IDENTITY);
605
- expect(result.system[1].text).toContain('# Code References');
606
- expect(result.system[1].text).not.toContain('OpenCode');
607
- expect(result.system[2].text).toBe('Additional context block');
608
- // Tool names prefixed
609
- expect(result.tools[0].name).toBe('mcp_bash');
610
- expect(result.tools[1].name).toBe('mcp_read_file');
611
- // tool_use blocks in messages prefixed, text untouched
612
- expect(result.messages[1].content[0].name).toBe('mcp_bash');
613
- expect(result.messages[1].content[1].text).toBe('Let me check');
614
- expect(result.messages[0].content).toBe('Help me fix this bug');
615
- });
616
- test('handles body with no messages array', () => {
617
- const body = JSON.stringify({ model: 'claude-3' });
618
- const result = JSON.parse(rewriteRequestBody(body));
619
- expect(result.system[0].text).toContain(CLAUDE_CODE_IDENTITY);
620
- });
621
- });