@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
@@ -4,504 +4,504 @@ import { DEFAULT_CONFIG, getConfigDir, getConfigPath, loadConfig } from "./confi
4
4
 
5
5
  // Mock fs module
6
6
  vi.mock("node:fs", () => ({
7
- existsSync: vi.fn(),
8
- readFileSync: vi.fn(),
7
+ existsSync: vi.fn(),
8
+ readFileSync: vi.fn(),
9
9
  }));
10
10
 
11
11
  const mockExistsSync = existsSync as Mock;
12
12
  const mockReadFileSync = readFileSync as Mock;
13
13
 
14
14
  describe("DEFAULT_CONFIG", () => {
15
- it("has expected default strategy", () => {
16
- expect(DEFAULT_CONFIG.account_selection_strategy).toBe("sticky");
17
- });
18
-
19
- it("has expected health score defaults", () => {
20
- expect(DEFAULT_CONFIG.health_score.initial).toBe(70);
21
- expect(DEFAULT_CONFIG.health_score.success_reward).toBe(1);
22
- expect(DEFAULT_CONFIG.health_score.rate_limit_penalty).toBe(-10);
23
- expect(DEFAULT_CONFIG.health_score.failure_penalty).toBe(-20);
24
- expect(DEFAULT_CONFIG.health_score.min_usable).toBe(50);
25
- expect(DEFAULT_CONFIG.health_score.max_score).toBe(100);
26
- });
27
-
28
- it("has expected token bucket defaults", () => {
29
- expect(DEFAULT_CONFIG.token_bucket.max_tokens).toBe(50);
30
- expect(DEFAULT_CONFIG.token_bucket.regeneration_rate_per_minute).toBe(6);
31
- expect(DEFAULT_CONFIG.token_bucket.initial_tokens).toBe(50);
32
- });
33
-
34
- it("has debug disabled by default", () => {
35
- expect(DEFAULT_CONFIG.debug).toBe(false);
36
- });
37
-
38
- it("enables signature emulation defaults", () => {
39
- expect(DEFAULT_CONFIG.signature_emulation.enabled).toBe(true);
40
- expect(DEFAULT_CONFIG.signature_emulation.fetch_claude_code_version_on_startup).toBe(true);
41
- expect(DEFAULT_CONFIG.signature_emulation.prompt_compaction).toBe("minimal");
42
- });
43
-
44
- it("disables sanitize_system_prompt by default", () => {
45
- expect(DEFAULT_CONFIG.signature_emulation.sanitize_system_prompt).toBe(false);
46
- });
47
-
48
- it("has toast defaults", () => {
49
- expect(DEFAULT_CONFIG.toasts.quiet).toBe(false);
50
- expect(DEFAULT_CONFIG.toasts.debounce_seconds).toBe(30);
51
- });
15
+ it("has expected default strategy", () => {
16
+ expect(DEFAULT_CONFIG.account_selection_strategy).toBe("sticky");
17
+ });
18
+
19
+ it("has expected health score defaults", () => {
20
+ expect(DEFAULT_CONFIG.health_score.initial).toBe(70);
21
+ expect(DEFAULT_CONFIG.health_score.success_reward).toBe(1);
22
+ expect(DEFAULT_CONFIG.health_score.rate_limit_penalty).toBe(-10);
23
+ expect(DEFAULT_CONFIG.health_score.failure_penalty).toBe(-20);
24
+ expect(DEFAULT_CONFIG.health_score.min_usable).toBe(50);
25
+ expect(DEFAULT_CONFIG.health_score.max_score).toBe(100);
26
+ });
27
+
28
+ it("has expected token bucket defaults", () => {
29
+ expect(DEFAULT_CONFIG.token_bucket.max_tokens).toBe(50);
30
+ expect(DEFAULT_CONFIG.token_bucket.regeneration_rate_per_minute).toBe(6);
31
+ expect(DEFAULT_CONFIG.token_bucket.initial_tokens).toBe(50);
32
+ });
33
+
34
+ it("has debug disabled by default", () => {
35
+ expect(DEFAULT_CONFIG.debug).toBe(false);
36
+ });
37
+
38
+ it("enables signature emulation defaults", () => {
39
+ expect(DEFAULT_CONFIG.signature_emulation.enabled).toBe(true);
40
+ expect(DEFAULT_CONFIG.signature_emulation.fetch_claude_code_version_on_startup).toBe(true);
41
+ expect(DEFAULT_CONFIG.signature_emulation.prompt_compaction).toBe("minimal");
42
+ });
43
+
44
+ it("disables sanitize_system_prompt by default", () => {
45
+ expect(DEFAULT_CONFIG.signature_emulation.sanitize_system_prompt).toBe(false);
46
+ });
47
+
48
+ it("has toast defaults", () => {
49
+ expect(DEFAULT_CONFIG.toasts.quiet).toBe(false);
50
+ expect(DEFAULT_CONFIG.toasts.debounce_seconds).toBe(30);
51
+ });
52
52
  });
53
53
 
54
54
  describe("getConfigDir", () => {
55
- it("returns a path ending with opencode", () => {
56
- const dir = getConfigDir();
57
- expect(dir.endsWith("opencode")).toBe(true);
58
- });
55
+ it("returns a path ending with opencode", () => {
56
+ const dir = getConfigDir();
57
+ expect(dir.endsWith("opencode")).toBe(true);
58
+ });
59
59
  });
60
60
 
61
61
  describe("getConfigPath", () => {
62
- it("returns a path ending with anthropic-auth.json", () => {
63
- const path = getConfigPath();
64
- expect(path.endsWith("anthropic-auth.json")).toBe(true);
65
- });
62
+ it("returns a path ending with anthropic-auth.json", () => {
63
+ const path = getConfigPath();
64
+ expect(path.endsWith("anthropic-auth.json")).toBe(true);
65
+ });
66
66
  });
67
67
 
68
68
  describe("loadConfig", () => {
69
- const originalEnv = { ...process.env };
70
-
71
- beforeEach(() => {
72
- vi.resetAllMocks();
73
- // Clean env overrides
74
- delete process.env.OPENCODE_ANTHROPIC_STRATEGY;
75
- delete process.env.OPENCODE_ANTHROPIC_DEBUG;
76
- delete process.env.OPENCODE_ANTHROPIC_QUIET;
77
- delete process.env.OPENCODE_ANTHROPIC_EMULATE_CLAUDE_CODE_SIGNATURE;
78
- delete process.env.OPENCODE_ANTHROPIC_FETCH_CLAUDE_CODE_VERSION;
79
- delete process.env.OPENCODE_ANTHROPIC_PROMPT_COMPACTION;
80
- });
81
-
82
- afterEach(() => {
83
- process.env = { ...originalEnv };
84
- });
85
-
86
- it("returns defaults when config file does not exist", () => {
87
- mockExistsSync.mockReturnValue(false);
88
- const config = loadConfig();
89
- expect(config).toEqual(DEFAULT_CONFIG);
90
- });
91
-
92
- it("returns defaults when config file is invalid JSON", () => {
93
- mockExistsSync.mockReturnValue(true);
94
- mockReadFileSync.mockReturnValue("not json {{{");
95
- const config = loadConfig();
96
- expect(config).toEqual(DEFAULT_CONFIG);
97
- });
98
-
99
- it("returns defaults when config file is an array", () => {
100
- mockExistsSync.mockReturnValue(true);
101
- mockReadFileSync.mockReturnValue("[]");
102
- const config = loadConfig();
103
- expect(config).toEqual(DEFAULT_CONFIG);
104
- });
105
-
106
- it("returns defaults when config file is null", () => {
107
- mockExistsSync.mockReturnValue(true);
108
- mockReadFileSync.mockReturnValue("null");
109
- const config = loadConfig();
110
- expect(config).toEqual(DEFAULT_CONFIG);
111
- });
112
-
113
- it("merges valid strategy from config file", () => {
114
- mockExistsSync.mockReturnValue(true);
115
- mockReadFileSync.mockReturnValue(JSON.stringify({ account_selection_strategy: "sticky" }));
116
- const config = loadConfig();
117
- expect(config.account_selection_strategy).toBe("sticky");
118
- });
119
-
120
- it("ignores invalid strategy", () => {
121
- mockExistsSync.mockReturnValue(true);
122
- mockReadFileSync.mockReturnValue(JSON.stringify({ account_selection_strategy: "invalid" }));
123
- const config = loadConfig();
124
- expect(config.account_selection_strategy).toBe("sticky");
125
- });
126
-
127
- it("accepts boolean debug", () => {
128
- mockExistsSync.mockReturnValue(true);
129
- mockReadFileSync.mockReturnValue(JSON.stringify({ debug: true }));
130
- const config = loadConfig();
131
- expect(config.debug).toBe(true);
132
- });
133
-
134
- it("merges signature emulation sub-config", () => {
135
- mockExistsSync.mockReturnValue(true);
136
- mockReadFileSync.mockReturnValue(
137
- JSON.stringify({
138
- signature_emulation: {
139
- enabled: false,
140
- fetch_claude_code_version_on_startup: false,
141
- prompt_compaction: "off",
142
- },
143
- }),
144
- );
145
- const config = loadConfig();
146
- expect(config.signature_emulation.enabled).toBe(false);
147
- expect(config.signature_emulation.fetch_claude_code_version_on_startup).toBe(false);
148
- expect(config.signature_emulation.prompt_compaction).toBe("off");
149
- });
150
-
151
- it("merges health_score sub-config", () => {
152
- mockExistsSync.mockReturnValue(true);
153
- mockReadFileSync.mockReturnValue(
154
- JSON.stringify({
155
- health_score: {
156
- initial: 80,
157
- success_reward: 5,
158
- },
159
- }),
160
- );
161
- const config = loadConfig();
162
- expect(config.health_score.initial).toBe(80);
163
- expect(config.health_score.success_reward).toBe(5);
164
- // Other fields should be defaults
165
- expect(config.health_score.rate_limit_penalty).toBe(-10);
166
- });
167
-
168
- it("clamps health_score values to valid ranges", () => {
169
- mockExistsSync.mockReturnValue(true);
170
- mockReadFileSync.mockReturnValue(
171
- JSON.stringify({
172
- health_score: {
173
- initial: 200, // max 100
174
- rate_limit_penalty: -999, // min -50
175
- },
176
- }),
177
- );
178
- const config = loadConfig();
179
- expect(config.health_score.initial).toBe(100);
180
- expect(config.health_score.rate_limit_penalty).toBe(-50);
181
- });
182
-
183
- it("merges token_bucket sub-config", () => {
184
- mockExistsSync.mockReturnValue(true);
185
- mockReadFileSync.mockReturnValue(
186
- JSON.stringify({
187
- token_bucket: {
188
- max_tokens: 100,
189
- },
190
- }),
191
- );
192
- const config = loadConfig();
193
- expect(config.token_bucket.max_tokens).toBe(100);
194
- expect(config.token_bucket.regeneration_rate_per_minute).toBe(6);
195
- });
196
-
197
- // Environment variable overrides
198
- it("overrides strategy from OPENCODE_ANTHROPIC_STRATEGY", () => {
199
- mockExistsSync.mockReturnValue(false);
200
- process.env.OPENCODE_ANTHROPIC_STRATEGY = "round-robin";
201
- const config = loadConfig();
202
- expect(config.account_selection_strategy).toBe("round-robin");
203
- });
204
-
205
- it("ignores invalid OPENCODE_ANTHROPIC_STRATEGY", () => {
206
- mockExistsSync.mockReturnValue(false);
207
- process.env.OPENCODE_ANTHROPIC_STRATEGY = "invalid";
208
- const config = loadConfig();
209
- expect(config.account_selection_strategy).toBe("sticky");
210
- });
211
-
212
- it("enables debug from OPENCODE_ANTHROPIC_DEBUG=1", () => {
213
- mockExistsSync.mockReturnValue(false);
214
- process.env.OPENCODE_ANTHROPIC_DEBUG = "1";
215
- const config = loadConfig();
216
- expect(config.debug).toBe(true);
217
- });
218
-
219
- it("enables debug from OPENCODE_ANTHROPIC_DEBUG=true", () => {
220
- mockExistsSync.mockReturnValue(false);
221
- process.env.OPENCODE_ANTHROPIC_DEBUG = "true";
222
- const config = loadConfig();
223
- expect(config.debug).toBe(true);
224
- });
225
-
226
- it("disables debug from OPENCODE_ANTHROPIC_DEBUG=0", () => {
227
- mockExistsSync.mockReturnValue(true);
228
- mockReadFileSync.mockReturnValue(JSON.stringify({ debug: true }));
229
- process.env.OPENCODE_ANTHROPIC_DEBUG = "0";
230
- const config = loadConfig();
231
- expect(config.debug).toBe(false);
232
- });
233
-
234
- it("disables signature emulation from OPENCODE_ANTHROPIC_EMULATE_CLAUDE_CODE_SIGNATURE=0", () => {
235
- mockExistsSync.mockReturnValue(false);
236
- process.env.OPENCODE_ANTHROPIC_EMULATE_CLAUDE_CODE_SIGNATURE = "0";
237
- const config = loadConfig();
238
- expect(config.signature_emulation.enabled).toBe(false);
239
- });
240
-
241
- it("disables version fetch from OPENCODE_ANTHROPIC_FETCH_CLAUDE_CODE_VERSION=0", () => {
242
- mockExistsSync.mockReturnValue(false);
243
- process.env.OPENCODE_ANTHROPIC_FETCH_CLAUDE_CODE_VERSION = "0";
244
- const config = loadConfig();
245
- expect(config.signature_emulation.fetch_claude_code_version_on_startup).toBe(false);
246
- });
247
-
248
- it("disables prompt compaction from OPENCODE_ANTHROPIC_PROMPT_COMPACTION=off", () => {
249
- mockExistsSync.mockReturnValue(false);
250
- process.env.OPENCODE_ANTHROPIC_PROMPT_COMPACTION = "off";
251
- const config = loadConfig();
252
- expect(config.signature_emulation.prompt_compaction).toBe("off");
253
- });
254
-
255
- it("env overrides take precedence over config file", () => {
256
- mockExistsSync.mockReturnValue(true);
257
- mockReadFileSync.mockReturnValue(JSON.stringify({ account_selection_strategy: "sticky" }));
258
- process.env.OPENCODE_ANTHROPIC_STRATEGY = "round-robin";
259
- const config = loadConfig();
260
- expect(config.account_selection_strategy).toBe("round-robin");
261
- });
262
-
263
- // Toast config
264
- it("has toast defaults", () => {
265
- mockExistsSync.mockReturnValue(false);
266
- const config = loadConfig();
267
- expect(config.toasts.quiet).toBe(false);
268
- expect(config.toasts.debounce_seconds).toBe(30);
269
- });
270
-
271
- it("merges toasts sub-config", () => {
272
- mockExistsSync.mockReturnValue(true);
273
- mockReadFileSync.mockReturnValue(JSON.stringify({ toasts: { quiet: true, debounce_seconds: 10 } }));
274
- const config = loadConfig();
275
- expect(config.toasts.quiet).toBe(true);
276
- expect(config.toasts.debounce_seconds).toBe(10);
277
- });
278
-
279
- it("clamps debounce_seconds to valid range", () => {
280
- mockExistsSync.mockReturnValue(true);
281
- mockReadFileSync.mockReturnValue(JSON.stringify({ toasts: { debounce_seconds: 999 } }));
282
- const config = loadConfig();
283
- expect(config.toasts.debounce_seconds).toBe(300);
284
- });
285
-
286
- it("clamps negative debounce_seconds to 0", () => {
287
- mockExistsSync.mockReturnValue(true);
288
- mockReadFileSync.mockReturnValue(JSON.stringify({ toasts: { debounce_seconds: -5 } }));
289
- const config = loadConfig();
290
- expect(config.toasts.debounce_seconds).toBe(0);
291
- });
292
-
293
- it("ignores non-boolean quiet", () => {
294
- mockExistsSync.mockReturnValue(true);
295
- mockReadFileSync.mockReturnValue(JSON.stringify({ toasts: { quiet: "yes" } }));
296
- const config = loadConfig();
297
- expect(config.toasts.quiet).toBe(false);
298
- });
299
-
300
- it("enables quiet from OPENCODE_ANTHROPIC_QUIET=1", () => {
301
- mockExistsSync.mockReturnValue(false);
302
- process.env.OPENCODE_ANTHROPIC_QUIET = "1";
303
- const config = loadConfig();
304
- expect(config.toasts.quiet).toBe(true);
305
- });
306
-
307
- it("disables quiet from OPENCODE_ANTHROPIC_QUIET=0", () => {
308
- mockExistsSync.mockReturnValue(true);
309
- mockReadFileSync.mockReturnValue(JSON.stringify({ toasts: { quiet: true } }));
310
- process.env.OPENCODE_ANTHROPIC_QUIET = "0";
311
- const config = loadConfig();
312
- expect(config.toasts.quiet).toBe(false);
313
- });
314
-
315
- // cc_credential_reuse config
316
- it("has cc_credential_reuse defaults", () => {
317
- mockExistsSync.mockReturnValue(false);
318
- const config = loadConfig();
319
- expect(config.cc_credential_reuse.enabled).toBe(true);
320
- expect(config.cc_credential_reuse.auto_detect).toBe(true);
321
- expect(config.cc_credential_reuse.prefer_over_oauth).toBe(true);
322
- });
323
-
324
- it("merges cc_credential_reuse sub-config", () => {
325
- mockExistsSync.mockReturnValue(true);
326
- mockReadFileSync.mockReturnValue(
327
- JSON.stringify({
328
- cc_credential_reuse: {
329
- enabled: false,
330
- auto_detect: false,
331
- prefer_over_oauth: false,
332
- },
333
- }),
334
- );
335
- const config = loadConfig();
336
- expect(config.cc_credential_reuse.enabled).toBe(false);
337
- expect(config.cc_credential_reuse.auto_detect).toBe(false);
338
- expect(config.cc_credential_reuse.prefer_over_oauth).toBe(false);
339
- });
340
-
341
- it("merges partial cc_credential_reuse sub-config", () => {
342
- mockExistsSync.mockReturnValue(true);
343
- mockReadFileSync.mockReturnValue(
344
- JSON.stringify({
345
- cc_credential_reuse: {
346
- enabled: false,
347
- },
348
- }),
349
- );
350
- const config = loadConfig();
351
- expect(config.cc_credential_reuse.enabled).toBe(false);
352
- // Other fields should be defaults
353
- expect(config.cc_credential_reuse.auto_detect).toBe(true);
354
- expect(config.cc_credential_reuse.prefer_over_oauth).toBe(true);
355
- });
356
-
357
- it("respects explicit true values in cc_credential_reuse", () => {
358
- mockExistsSync.mockReturnValue(true);
359
- mockReadFileSync.mockReturnValue(
360
- JSON.stringify({
361
- cc_credential_reuse: {
362
- enabled: true,
363
- auto_detect: true,
364
- prefer_over_oauth: true,
365
- },
366
- }),
367
- );
368
- const config = loadConfig();
369
- expect(config.cc_credential_reuse.enabled).toBe(true);
370
- expect(config.cc_credential_reuse.auto_detect).toBe(true);
371
- expect(config.cc_credential_reuse.prefer_over_oauth).toBe(true);
372
- });
373
-
374
- it("disables cc_credential_reuse from OPENCODE_ANTHROPIC_CC_REUSE_ENABLED=0", () => {
375
- mockExistsSync.mockReturnValue(false);
376
- process.env.OPENCODE_ANTHROPIC_CC_REUSE_ENABLED = "0";
377
- const config = loadConfig();
378
- expect(config.cc_credential_reuse.enabled).toBe(false);
379
- expect(config.cc_credential_reuse.auto_detect).toBe(false);
380
- });
381
-
382
- it("disables cc_credential_reuse from OPENCODE_ANTHROPIC_CC_REUSE_ENABLED=false", () => {
383
- mockExistsSync.mockReturnValue(true);
384
- mockReadFileSync.mockReturnValue(
385
- JSON.stringify({
386
- cc_credential_reuse: {
387
- enabled: true,
388
- auto_detect: true,
389
- },
390
- }),
391
- );
392
- process.env.OPENCODE_ANTHROPIC_CC_REUSE_ENABLED = "false";
393
- const config = loadConfig();
394
- expect(config.cc_credential_reuse.enabled).toBe(false);
395
- expect(config.cc_credential_reuse.auto_detect).toBe(false);
396
- });
69
+ const originalEnv = { ...process.env };
70
+
71
+ beforeEach(() => {
72
+ vi.resetAllMocks();
73
+ // Clean env overrides
74
+ delete process.env.OPENCODE_ANTHROPIC_STRATEGY;
75
+ delete process.env.OPENCODE_ANTHROPIC_DEBUG;
76
+ delete process.env.OPENCODE_ANTHROPIC_QUIET;
77
+ delete process.env.OPENCODE_ANTHROPIC_EMULATE_CLAUDE_CODE_SIGNATURE;
78
+ delete process.env.OPENCODE_ANTHROPIC_FETCH_CLAUDE_CODE_VERSION;
79
+ delete process.env.OPENCODE_ANTHROPIC_PROMPT_COMPACTION;
80
+ });
81
+
82
+ afterEach(() => {
83
+ process.env = { ...originalEnv };
84
+ });
85
+
86
+ it("returns defaults when config file does not exist", () => {
87
+ mockExistsSync.mockReturnValue(false);
88
+ const config = loadConfig();
89
+ expect(config).toEqual(DEFAULT_CONFIG);
90
+ });
91
+
92
+ it("returns defaults when config file is invalid JSON", () => {
93
+ mockExistsSync.mockReturnValue(true);
94
+ mockReadFileSync.mockReturnValue("not json {{{");
95
+ const config = loadConfig();
96
+ expect(config).toEqual(DEFAULT_CONFIG);
97
+ });
98
+
99
+ it("returns defaults when config file is an array", () => {
100
+ mockExistsSync.mockReturnValue(true);
101
+ mockReadFileSync.mockReturnValue("[]");
102
+ const config = loadConfig();
103
+ expect(config).toEqual(DEFAULT_CONFIG);
104
+ });
105
+
106
+ it("returns defaults when config file is null", () => {
107
+ mockExistsSync.mockReturnValue(true);
108
+ mockReadFileSync.mockReturnValue("null");
109
+ const config = loadConfig();
110
+ expect(config).toEqual(DEFAULT_CONFIG);
111
+ });
112
+
113
+ it("merges valid strategy from config file", () => {
114
+ mockExistsSync.mockReturnValue(true);
115
+ mockReadFileSync.mockReturnValue(JSON.stringify({ account_selection_strategy: "sticky" }));
116
+ const config = loadConfig();
117
+ expect(config.account_selection_strategy).toBe("sticky");
118
+ });
119
+
120
+ it("ignores invalid strategy", () => {
121
+ mockExistsSync.mockReturnValue(true);
122
+ mockReadFileSync.mockReturnValue(JSON.stringify({ account_selection_strategy: "invalid" }));
123
+ const config = loadConfig();
124
+ expect(config.account_selection_strategy).toBe("sticky");
125
+ });
126
+
127
+ it("accepts boolean debug", () => {
128
+ mockExistsSync.mockReturnValue(true);
129
+ mockReadFileSync.mockReturnValue(JSON.stringify({ debug: true }));
130
+ const config = loadConfig();
131
+ expect(config.debug).toBe(true);
132
+ });
133
+
134
+ it("merges signature emulation sub-config", () => {
135
+ mockExistsSync.mockReturnValue(true);
136
+ mockReadFileSync.mockReturnValue(
137
+ JSON.stringify({
138
+ signature_emulation: {
139
+ enabled: false,
140
+ fetch_claude_code_version_on_startup: false,
141
+ prompt_compaction: "off",
142
+ },
143
+ }),
144
+ );
145
+ const config = loadConfig();
146
+ expect(config.signature_emulation.enabled).toBe(false);
147
+ expect(config.signature_emulation.fetch_claude_code_version_on_startup).toBe(false);
148
+ expect(config.signature_emulation.prompt_compaction).toBe("off");
149
+ });
150
+
151
+ it("merges health_score sub-config", () => {
152
+ mockExistsSync.mockReturnValue(true);
153
+ mockReadFileSync.mockReturnValue(
154
+ JSON.stringify({
155
+ health_score: {
156
+ initial: 80,
157
+ success_reward: 5,
158
+ },
159
+ }),
160
+ );
161
+ const config = loadConfig();
162
+ expect(config.health_score.initial).toBe(80);
163
+ expect(config.health_score.success_reward).toBe(5);
164
+ // Other fields should be defaults
165
+ expect(config.health_score.rate_limit_penalty).toBe(-10);
166
+ });
167
+
168
+ it("clamps health_score values to valid ranges", () => {
169
+ mockExistsSync.mockReturnValue(true);
170
+ mockReadFileSync.mockReturnValue(
171
+ JSON.stringify({
172
+ health_score: {
173
+ initial: 200, // max 100
174
+ rate_limit_penalty: -999, // min -50
175
+ },
176
+ }),
177
+ );
178
+ const config = loadConfig();
179
+ expect(config.health_score.initial).toBe(100);
180
+ expect(config.health_score.rate_limit_penalty).toBe(-50);
181
+ });
182
+
183
+ it("merges token_bucket sub-config", () => {
184
+ mockExistsSync.mockReturnValue(true);
185
+ mockReadFileSync.mockReturnValue(
186
+ JSON.stringify({
187
+ token_bucket: {
188
+ max_tokens: 100,
189
+ },
190
+ }),
191
+ );
192
+ const config = loadConfig();
193
+ expect(config.token_bucket.max_tokens).toBe(100);
194
+ expect(config.token_bucket.regeneration_rate_per_minute).toBe(6);
195
+ });
196
+
197
+ // Environment variable overrides
198
+ it("overrides strategy from OPENCODE_ANTHROPIC_STRATEGY", () => {
199
+ mockExistsSync.mockReturnValue(false);
200
+ process.env.OPENCODE_ANTHROPIC_STRATEGY = "round-robin";
201
+ const config = loadConfig();
202
+ expect(config.account_selection_strategy).toBe("round-robin");
203
+ });
204
+
205
+ it("ignores invalid OPENCODE_ANTHROPIC_STRATEGY", () => {
206
+ mockExistsSync.mockReturnValue(false);
207
+ process.env.OPENCODE_ANTHROPIC_STRATEGY = "invalid";
208
+ const config = loadConfig();
209
+ expect(config.account_selection_strategy).toBe("sticky");
210
+ });
211
+
212
+ it("enables debug from OPENCODE_ANTHROPIC_DEBUG=1", () => {
213
+ mockExistsSync.mockReturnValue(false);
214
+ process.env.OPENCODE_ANTHROPIC_DEBUG = "1";
215
+ const config = loadConfig();
216
+ expect(config.debug).toBe(true);
217
+ });
218
+
219
+ it("enables debug from OPENCODE_ANTHROPIC_DEBUG=true", () => {
220
+ mockExistsSync.mockReturnValue(false);
221
+ process.env.OPENCODE_ANTHROPIC_DEBUG = "true";
222
+ const config = loadConfig();
223
+ expect(config.debug).toBe(true);
224
+ });
225
+
226
+ it("disables debug from OPENCODE_ANTHROPIC_DEBUG=0", () => {
227
+ mockExistsSync.mockReturnValue(true);
228
+ mockReadFileSync.mockReturnValue(JSON.stringify({ debug: true }));
229
+ process.env.OPENCODE_ANTHROPIC_DEBUG = "0";
230
+ const config = loadConfig();
231
+ expect(config.debug).toBe(false);
232
+ });
233
+
234
+ it("disables signature emulation from OPENCODE_ANTHROPIC_EMULATE_CLAUDE_CODE_SIGNATURE=0", () => {
235
+ mockExistsSync.mockReturnValue(false);
236
+ process.env.OPENCODE_ANTHROPIC_EMULATE_CLAUDE_CODE_SIGNATURE = "0";
237
+ const config = loadConfig();
238
+ expect(config.signature_emulation.enabled).toBe(false);
239
+ });
240
+
241
+ it("disables version fetch from OPENCODE_ANTHROPIC_FETCH_CLAUDE_CODE_VERSION=0", () => {
242
+ mockExistsSync.mockReturnValue(false);
243
+ process.env.OPENCODE_ANTHROPIC_FETCH_CLAUDE_CODE_VERSION = "0";
244
+ const config = loadConfig();
245
+ expect(config.signature_emulation.fetch_claude_code_version_on_startup).toBe(false);
246
+ });
247
+
248
+ it("disables prompt compaction from OPENCODE_ANTHROPIC_PROMPT_COMPACTION=off", () => {
249
+ mockExistsSync.mockReturnValue(false);
250
+ process.env.OPENCODE_ANTHROPIC_PROMPT_COMPACTION = "off";
251
+ const config = loadConfig();
252
+ expect(config.signature_emulation.prompt_compaction).toBe("off");
253
+ });
254
+
255
+ it("env overrides take precedence over config file", () => {
256
+ mockExistsSync.mockReturnValue(true);
257
+ mockReadFileSync.mockReturnValue(JSON.stringify({ account_selection_strategy: "sticky" }));
258
+ process.env.OPENCODE_ANTHROPIC_STRATEGY = "round-robin";
259
+ const config = loadConfig();
260
+ expect(config.account_selection_strategy).toBe("round-robin");
261
+ });
262
+
263
+ // Toast config
264
+ it("has toast defaults", () => {
265
+ mockExistsSync.mockReturnValue(false);
266
+ const config = loadConfig();
267
+ expect(config.toasts.quiet).toBe(false);
268
+ expect(config.toasts.debounce_seconds).toBe(30);
269
+ });
270
+
271
+ it("merges toasts sub-config", () => {
272
+ mockExistsSync.mockReturnValue(true);
273
+ mockReadFileSync.mockReturnValue(JSON.stringify({ toasts: { quiet: true, debounce_seconds: 10 } }));
274
+ const config = loadConfig();
275
+ expect(config.toasts.quiet).toBe(true);
276
+ expect(config.toasts.debounce_seconds).toBe(10);
277
+ });
278
+
279
+ it("clamps debounce_seconds to valid range", () => {
280
+ mockExistsSync.mockReturnValue(true);
281
+ mockReadFileSync.mockReturnValue(JSON.stringify({ toasts: { debounce_seconds: 999 } }));
282
+ const config = loadConfig();
283
+ expect(config.toasts.debounce_seconds).toBe(300);
284
+ });
285
+
286
+ it("clamps negative debounce_seconds to 0", () => {
287
+ mockExistsSync.mockReturnValue(true);
288
+ mockReadFileSync.mockReturnValue(JSON.stringify({ toasts: { debounce_seconds: -5 } }));
289
+ const config = loadConfig();
290
+ expect(config.toasts.debounce_seconds).toBe(0);
291
+ });
292
+
293
+ it("ignores non-boolean quiet", () => {
294
+ mockExistsSync.mockReturnValue(true);
295
+ mockReadFileSync.mockReturnValue(JSON.stringify({ toasts: { quiet: "yes" } }));
296
+ const config = loadConfig();
297
+ expect(config.toasts.quiet).toBe(false);
298
+ });
299
+
300
+ it("enables quiet from OPENCODE_ANTHROPIC_QUIET=1", () => {
301
+ mockExistsSync.mockReturnValue(false);
302
+ process.env.OPENCODE_ANTHROPIC_QUIET = "1";
303
+ const config = loadConfig();
304
+ expect(config.toasts.quiet).toBe(true);
305
+ });
306
+
307
+ it("disables quiet from OPENCODE_ANTHROPIC_QUIET=0", () => {
308
+ mockExistsSync.mockReturnValue(true);
309
+ mockReadFileSync.mockReturnValue(JSON.stringify({ toasts: { quiet: true } }));
310
+ process.env.OPENCODE_ANTHROPIC_QUIET = "0";
311
+ const config = loadConfig();
312
+ expect(config.toasts.quiet).toBe(false);
313
+ });
314
+
315
+ // cc_credential_reuse config
316
+ it("has cc_credential_reuse defaults", () => {
317
+ mockExistsSync.mockReturnValue(false);
318
+ const config = loadConfig();
319
+ expect(config.cc_credential_reuse.enabled).toBe(true);
320
+ expect(config.cc_credential_reuse.auto_detect).toBe(true);
321
+ expect(config.cc_credential_reuse.prefer_over_oauth).toBe(true);
322
+ });
323
+
324
+ it("merges cc_credential_reuse sub-config", () => {
325
+ mockExistsSync.mockReturnValue(true);
326
+ mockReadFileSync.mockReturnValue(
327
+ JSON.stringify({
328
+ cc_credential_reuse: {
329
+ enabled: false,
330
+ auto_detect: false,
331
+ prefer_over_oauth: false,
332
+ },
333
+ }),
334
+ );
335
+ const config = loadConfig();
336
+ expect(config.cc_credential_reuse.enabled).toBe(false);
337
+ expect(config.cc_credential_reuse.auto_detect).toBe(false);
338
+ expect(config.cc_credential_reuse.prefer_over_oauth).toBe(false);
339
+ });
340
+
341
+ it("merges partial cc_credential_reuse sub-config", () => {
342
+ mockExistsSync.mockReturnValue(true);
343
+ mockReadFileSync.mockReturnValue(
344
+ JSON.stringify({
345
+ cc_credential_reuse: {
346
+ enabled: false,
347
+ },
348
+ }),
349
+ );
350
+ const config = loadConfig();
351
+ expect(config.cc_credential_reuse.enabled).toBe(false);
352
+ // Other fields should be defaults
353
+ expect(config.cc_credential_reuse.auto_detect).toBe(true);
354
+ expect(config.cc_credential_reuse.prefer_over_oauth).toBe(true);
355
+ });
356
+
357
+ it("respects explicit true values in cc_credential_reuse", () => {
358
+ mockExistsSync.mockReturnValue(true);
359
+ mockReadFileSync.mockReturnValue(
360
+ JSON.stringify({
361
+ cc_credential_reuse: {
362
+ enabled: true,
363
+ auto_detect: true,
364
+ prefer_over_oauth: true,
365
+ },
366
+ }),
367
+ );
368
+ const config = loadConfig();
369
+ expect(config.cc_credential_reuse.enabled).toBe(true);
370
+ expect(config.cc_credential_reuse.auto_detect).toBe(true);
371
+ expect(config.cc_credential_reuse.prefer_over_oauth).toBe(true);
372
+ });
373
+
374
+ it("disables cc_credential_reuse from OPENCODE_ANTHROPIC_CC_REUSE_ENABLED=0", () => {
375
+ mockExistsSync.mockReturnValue(false);
376
+ process.env.OPENCODE_ANTHROPIC_CC_REUSE_ENABLED = "0";
377
+ const config = loadConfig();
378
+ expect(config.cc_credential_reuse.enabled).toBe(false);
379
+ expect(config.cc_credential_reuse.auto_detect).toBe(false);
380
+ });
381
+
382
+ it("disables cc_credential_reuse from OPENCODE_ANTHROPIC_CC_REUSE_ENABLED=false", () => {
383
+ mockExistsSync.mockReturnValue(true);
384
+ mockReadFileSync.mockReturnValue(
385
+ JSON.stringify({
386
+ cc_credential_reuse: {
387
+ enabled: true,
388
+ auto_detect: true,
389
+ },
390
+ }),
391
+ );
392
+ process.env.OPENCODE_ANTHROPIC_CC_REUSE_ENABLED = "false";
393
+ const config = loadConfig();
394
+ expect(config.cc_credential_reuse.enabled).toBe(false);
395
+ expect(config.cc_credential_reuse.auto_detect).toBe(false);
396
+ });
397
397
  });
398
398
 
399
399
  describe("loadConfig - sanitize_system_prompt field", () => {
400
- beforeEach(() => {
401
- vi.resetAllMocks();
402
- delete process.env.OPENCODE_ANTHROPIC_SANITIZE_SYSTEM_PROMPT;
403
- });
404
-
405
- afterEach(() => {
406
- delete process.env.OPENCODE_ANTHROPIC_SANITIZE_SYSTEM_PROMPT;
407
- });
408
-
409
- it("honors nested signature_emulation.sanitize_system_prompt = true", () => {
410
- mockExistsSync.mockReturnValue(true);
411
- mockReadFileSync.mockReturnValue(
412
- JSON.stringify({
413
- signature_emulation: { sanitize_system_prompt: true },
414
- }),
415
- );
416
- const config = loadConfig();
417
- expect(config.signature_emulation.sanitize_system_prompt).toBe(true);
418
- });
419
-
420
- it("honors nested signature_emulation.sanitize_system_prompt = false", () => {
421
- mockExistsSync.mockReturnValue(true);
422
- mockReadFileSync.mockReturnValue(
423
- JSON.stringify({
424
- signature_emulation: { sanitize_system_prompt: false },
425
- }),
426
- );
427
- const config = loadConfig();
428
- expect(config.signature_emulation.sanitize_system_prompt).toBe(false);
429
- });
430
-
431
- it("honors top-level sanitize_system_prompt alias = true", () => {
432
- mockExistsSync.mockReturnValue(true);
433
- mockReadFileSync.mockReturnValue(
434
- JSON.stringify({
435
- sanitize_system_prompt: true,
436
- }),
437
- );
438
- const config = loadConfig();
439
- expect(config.signature_emulation.sanitize_system_prompt).toBe(true);
440
- });
441
-
442
- it("honors top-level sanitize_system_prompt alias = false (the user's existing config)", () => {
443
- mockExistsSync.mockReturnValue(true);
444
- mockReadFileSync.mockReturnValue(
445
- JSON.stringify({
446
- debug: false,
447
- sanitize_system_prompt: false,
448
- }),
449
- );
450
- const config = loadConfig();
451
- expect(config.signature_emulation.sanitize_system_prompt).toBe(false);
452
- expect(config.debug).toBe(false);
453
- });
454
-
455
- it("top-level alias takes precedence over nested signature_emulation.sanitize_system_prompt", () => {
456
- mockExistsSync.mockReturnValue(true);
457
- mockReadFileSync.mockReturnValue(
458
- JSON.stringify({
459
- signature_emulation: { sanitize_system_prompt: true },
460
- sanitize_system_prompt: false,
461
- }),
462
- );
463
- const config = loadConfig();
464
- expect(config.signature_emulation.sanitize_system_prompt).toBe(false);
465
- });
466
-
467
- it("ignores non-boolean top-level sanitize_system_prompt value", () => {
468
- mockExistsSync.mockReturnValue(true);
469
- mockReadFileSync.mockReturnValue(
470
- JSON.stringify({
471
- sanitize_system_prompt: "yes",
472
- }),
473
- );
474
- const config = loadConfig();
475
- expect(config.signature_emulation.sanitize_system_prompt).toBe(false);
476
- });
477
-
478
- it("OPENCODE_ANTHROPIC_SANITIZE_SYSTEM_PROMPT=1 forces it on", () => {
479
- mockExistsSync.mockReturnValue(true);
480
- mockReadFileSync.mockReturnValue(JSON.stringify({ sanitize_system_prompt: false }));
481
- process.env.OPENCODE_ANTHROPIC_SANITIZE_SYSTEM_PROMPT = "1";
482
- const config = loadConfig();
483
- expect(config.signature_emulation.sanitize_system_prompt).toBe(true);
484
- });
485
-
486
- it("OPENCODE_ANTHROPIC_SANITIZE_SYSTEM_PROMPT=0 forces it off", () => {
487
- mockExistsSync.mockReturnValue(true);
488
- mockReadFileSync.mockReturnValue(JSON.stringify({ sanitize_system_prompt: true }));
489
- process.env.OPENCODE_ANTHROPIC_SANITIZE_SYSTEM_PROMPT = "0";
490
- const config = loadConfig();
491
- expect(config.signature_emulation.sanitize_system_prompt).toBe(false);
492
- });
493
-
494
- it("OPENCODE_ANTHROPIC_SANITIZE_SYSTEM_PROMPT=true forces it on", () => {
495
- mockExistsSync.mockReturnValue(false);
496
- process.env.OPENCODE_ANTHROPIC_SANITIZE_SYSTEM_PROMPT = "true";
497
- const config = loadConfig();
498
- expect(config.signature_emulation.sanitize_system_prompt).toBe(true);
499
- });
500
-
501
- it("OPENCODE_ANTHROPIC_SANITIZE_SYSTEM_PROMPT=false forces it off", () => {
502
- mockExistsSync.mockReturnValue(false);
503
- process.env.OPENCODE_ANTHROPIC_SANITIZE_SYSTEM_PROMPT = "false";
504
- const config = loadConfig();
505
- expect(config.signature_emulation.sanitize_system_prompt).toBe(false);
506
- });
400
+ beforeEach(() => {
401
+ vi.resetAllMocks();
402
+ delete process.env.OPENCODE_ANTHROPIC_SANITIZE_SYSTEM_PROMPT;
403
+ });
404
+
405
+ afterEach(() => {
406
+ delete process.env.OPENCODE_ANTHROPIC_SANITIZE_SYSTEM_PROMPT;
407
+ });
408
+
409
+ it("honors nested signature_emulation.sanitize_system_prompt = true", () => {
410
+ mockExistsSync.mockReturnValue(true);
411
+ mockReadFileSync.mockReturnValue(
412
+ JSON.stringify({
413
+ signature_emulation: { sanitize_system_prompt: true },
414
+ }),
415
+ );
416
+ const config = loadConfig();
417
+ expect(config.signature_emulation.sanitize_system_prompt).toBe(true);
418
+ });
419
+
420
+ it("honors nested signature_emulation.sanitize_system_prompt = false", () => {
421
+ mockExistsSync.mockReturnValue(true);
422
+ mockReadFileSync.mockReturnValue(
423
+ JSON.stringify({
424
+ signature_emulation: { sanitize_system_prompt: false },
425
+ }),
426
+ );
427
+ const config = loadConfig();
428
+ expect(config.signature_emulation.sanitize_system_prompt).toBe(false);
429
+ });
430
+
431
+ it("honors top-level sanitize_system_prompt alias = true", () => {
432
+ mockExistsSync.mockReturnValue(true);
433
+ mockReadFileSync.mockReturnValue(
434
+ JSON.stringify({
435
+ sanitize_system_prompt: true,
436
+ }),
437
+ );
438
+ const config = loadConfig();
439
+ expect(config.signature_emulation.sanitize_system_prompt).toBe(true);
440
+ });
441
+
442
+ it("honors top-level sanitize_system_prompt alias = false (the user's existing config)", () => {
443
+ mockExistsSync.mockReturnValue(true);
444
+ mockReadFileSync.mockReturnValue(
445
+ JSON.stringify({
446
+ debug: false,
447
+ sanitize_system_prompt: false,
448
+ }),
449
+ );
450
+ const config = loadConfig();
451
+ expect(config.signature_emulation.sanitize_system_prompt).toBe(false);
452
+ expect(config.debug).toBe(false);
453
+ });
454
+
455
+ it("top-level alias takes precedence over nested signature_emulation.sanitize_system_prompt", () => {
456
+ mockExistsSync.mockReturnValue(true);
457
+ mockReadFileSync.mockReturnValue(
458
+ JSON.stringify({
459
+ signature_emulation: { sanitize_system_prompt: true },
460
+ sanitize_system_prompt: false,
461
+ }),
462
+ );
463
+ const config = loadConfig();
464
+ expect(config.signature_emulation.sanitize_system_prompt).toBe(false);
465
+ });
466
+
467
+ it("ignores non-boolean top-level sanitize_system_prompt value", () => {
468
+ mockExistsSync.mockReturnValue(true);
469
+ mockReadFileSync.mockReturnValue(
470
+ JSON.stringify({
471
+ sanitize_system_prompt: "yes",
472
+ }),
473
+ );
474
+ const config = loadConfig();
475
+ expect(config.signature_emulation.sanitize_system_prompt).toBe(false);
476
+ });
477
+
478
+ it("OPENCODE_ANTHROPIC_SANITIZE_SYSTEM_PROMPT=1 forces it on", () => {
479
+ mockExistsSync.mockReturnValue(true);
480
+ mockReadFileSync.mockReturnValue(JSON.stringify({ sanitize_system_prompt: false }));
481
+ process.env.OPENCODE_ANTHROPIC_SANITIZE_SYSTEM_PROMPT = "1";
482
+ const config = loadConfig();
483
+ expect(config.signature_emulation.sanitize_system_prompt).toBe(true);
484
+ });
485
+
486
+ it("OPENCODE_ANTHROPIC_SANITIZE_SYSTEM_PROMPT=0 forces it off", () => {
487
+ mockExistsSync.mockReturnValue(true);
488
+ mockReadFileSync.mockReturnValue(JSON.stringify({ sanitize_system_prompt: true }));
489
+ process.env.OPENCODE_ANTHROPIC_SANITIZE_SYSTEM_PROMPT = "0";
490
+ const config = loadConfig();
491
+ expect(config.signature_emulation.sanitize_system_prompt).toBe(false);
492
+ });
493
+
494
+ it("OPENCODE_ANTHROPIC_SANITIZE_SYSTEM_PROMPT=true forces it on", () => {
495
+ mockExistsSync.mockReturnValue(false);
496
+ process.env.OPENCODE_ANTHROPIC_SANITIZE_SYSTEM_PROMPT = "true";
497
+ const config = loadConfig();
498
+ expect(config.signature_emulation.sanitize_system_prompt).toBe(true);
499
+ });
500
+
501
+ it("OPENCODE_ANTHROPIC_SANITIZE_SYSTEM_PROMPT=false forces it off", () => {
502
+ mockExistsSync.mockReturnValue(false);
503
+ process.env.OPENCODE_ANTHROPIC_SANITIZE_SYSTEM_PROMPT = "false";
504
+ const config = loadConfig();
505
+ expect(config.signature_emulation.sanitize_system_prompt).toBe(false);
506
+ });
507
507
  });