opencode-anthropic-fix 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +24 -0
  2. package/index.mjs +155 -149
  3. package/lib/config.mjs +47 -25
  4. package/lib/mimicry/adapter-input.mjs +671 -0
  5. package/lib/mimicry/context-hint-threshold.mjs +127 -0
  6. package/lib/mimicry/headers.mjs +71 -64
  7. package/lib/mimicry/request-body.mjs +23 -22
  8. package/lib/mimicry/system-prompt.mjs +54 -23
  9. package/lib/mimicry/wire-compat.mjs +305 -0
  10. package/package.json +3 -3
  11. package/lib/account-state.test.mjs +0 -109
  12. package/lib/accounts.test.mjs +0 -912
  13. package/lib/adaptive-thinking-models.test.mjs +0 -30
  14. package/lib/backoff.test.mjs +0 -411
  15. package/lib/cc-credentials.test.mjs +0 -391
  16. package/lib/config.test.mjs +0 -764
  17. package/lib/context-hint-persist.mjs +0 -68
  18. package/lib/diagnose.test.mjs +0 -122
  19. package/lib/env.test.mjs +0 -25
  20. package/lib/haiku-call.test.mjs +0 -72
  21. package/lib/message-transform.test.mjs +0 -173
  22. package/lib/mimicry/cache.test.mjs +0 -112
  23. package/lib/mimicry/headers.test.mjs +0 -50
  24. package/lib/mimicry/models.test.mjs +0 -132
  25. package/lib/mimicry/request-body.test.mjs +0 -34
  26. package/lib/mimicry/request-helpers.test.mjs +0 -155
  27. package/lib/mimicry/response-stream.test.mjs +0 -127
  28. package/lib/mimicry/system-prompt.test.mjs +0 -73
  29. package/lib/oauth.test.mjs +0 -343
  30. package/lib/redact.test.mjs +0 -154
  31. package/lib/refresh-lock.test.mjs +0 -131
  32. package/lib/request-headers.test.mjs +0 -164
  33. package/lib/retry/overload-loop.test.mjs +0 -107
  34. package/lib/rotation.test.mjs +0 -392
  35. package/lib/session-metrics.test.mjs +0 -67
  36. package/lib/storage.test.mjs +0 -696
  37. package/lib/token-economy/microcompact.test.mjs +0 -28
  38. package/lib/token-economy/transforms.test.mjs +0 -192
  39. package/lib/tuning.test.mjs +0 -89
@@ -1,764 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
- import { loadConfig, loadRawConfig, DEFAULT_CONFIG, getConfigDir, getConfigPath } from "./config.mjs";
3
- import { existsSync, readFileSync } from "node:fs";
4
-
5
- // Mock fs module
6
- vi.mock("node:fs", () => ({
7
- existsSync: vi.fn(),
8
- readFileSync: vi.fn(),
9
- }));
10
-
11
- describe("DEFAULT_CONFIG", () => {
12
- it("has expected default strategy", () => {
13
- expect(DEFAULT_CONFIG.account_selection_strategy).toBe("sticky");
14
- });
15
-
16
- it("has expected health score defaults", () => {
17
- expect(DEFAULT_CONFIG.health_score.initial).toBe(70);
18
- expect(DEFAULT_CONFIG.health_score.success_reward).toBe(1);
19
- expect(DEFAULT_CONFIG.health_score.rate_limit_penalty).toBe(-10);
20
- expect(DEFAULT_CONFIG.health_score.failure_penalty).toBe(-20);
21
- expect(DEFAULT_CONFIG.health_score.min_usable).toBe(50);
22
- expect(DEFAULT_CONFIG.health_score.max_score).toBe(100);
23
- });
24
-
25
- it("has expected token bucket defaults", () => {
26
- expect(DEFAULT_CONFIG.token_bucket.max_tokens).toBe(50);
27
- expect(DEFAULT_CONFIG.token_bucket.regeneration_rate_per_minute).toBe(6);
28
- expect(DEFAULT_CONFIG.token_bucket.initial_tokens).toBe(50);
29
- });
30
-
31
- it("has debug disabled by default", () => {
32
- expect(DEFAULT_CONFIG.debug).toBe(false);
33
- });
34
-
35
- it("enables signature emulation defaults", () => {
36
- expect(DEFAULT_CONFIG.signature_emulation.enabled).toBe(true);
37
- expect(DEFAULT_CONFIG.signature_emulation.fetch_claude_code_version_on_startup).toBe(true);
38
- expect(DEFAULT_CONFIG.signature_emulation.prompt_compaction).toBe("minimal");
39
- });
40
-
41
- it("has toast defaults", () => {
42
- expect(DEFAULT_CONFIG.toasts.quiet).toBe(false);
43
- expect(DEFAULT_CONFIG.toasts.debounce_seconds).toBe(30);
44
- });
45
- });
46
-
47
- describe("getConfigDir", () => {
48
- it("returns a path ending with opencode", () => {
49
- const dir = getConfigDir();
50
- expect(dir.endsWith("opencode")).toBe(true);
51
- });
52
-
53
- it("uses APPDATA on Windows", () => {
54
- const originalAppData = process.env.APPDATA;
55
- process.env.APPDATA = "C:\\Users\\test\\AppData\\Roaming";
56
-
57
- expect(getConfigDir()).toBe("C:\\Users\\test\\AppData\\Roaming\\opencode");
58
-
59
- if (originalAppData === undefined) delete process.env.APPDATA;
60
- else process.env.APPDATA = originalAppData;
61
- });
62
-
63
- it("uses XDG_CONFIG_HOME on non-Windows platforms", () => {
64
- const platform = vi.spyOn(process, "platform", "get").mockReturnValue("linux");
65
- const originalXdgConfigHome = process.env.XDG_CONFIG_HOME;
66
- process.env.XDG_CONFIG_HOME = "/tmp/custom-config";
67
-
68
- expect(getConfigDir()).toBe("\\tmp\\custom-config\\opencode");
69
-
70
- platform.mockRestore();
71
- if (originalXdgConfigHome === undefined) delete process.env.XDG_CONFIG_HOME;
72
- else process.env.XDG_CONFIG_HOME = originalXdgConfigHome;
73
- });
74
- });
75
-
76
- describe("getConfigPath", () => {
77
- it("returns a path ending with anthropic-auth.json", () => {
78
- const path = getConfigPath();
79
- expect(path.endsWith("anthropic-auth.json")).toBe(true);
80
- });
81
- });
82
-
83
- describe("loadConfig", () => {
84
- const originalEnv = { ...process.env };
85
-
86
- beforeEach(() => {
87
- vi.resetAllMocks();
88
- // Clean env overrides
89
- delete process.env.OPENCODE_ANTHROPIC_STRATEGY;
90
- delete process.env.OPENCODE_ANTHROPIC_DEBUG;
91
- delete process.env.OPENCODE_ANTHROPIC_QUIET;
92
- delete process.env.OPENCODE_ANTHROPIC_EMULATE_CLAUDE_CODE_SIGNATURE;
93
- delete process.env.OPENCODE_ANTHROPIC_FETCH_CLAUDE_CODE_VERSION;
94
- delete process.env.OPENCODE_ANTHROPIC_PROMPT_COMPACTION;
95
- delete process.env.OPENCODE_ANTHROPIC_OVERRIDE_MODEL_LIMITS;
96
- delete process.env.OPENCODE_ANTHROPIC_CC_CREDENTIALS;
97
- delete process.env.OPENCODE_ANTHROPIC_ADAPTIVE_CONTEXT;
98
- delete process.env.OPENCODE_ANTHROPIC_PROACTIVE_DISABLED;
99
- delete process.env.OPENCODE_ANTHROPIC_ANTI_VERBOSITY;
100
- delete process.env.OPENCODE_ANTHROPIC_LENGTH_ANCHORS;
101
- });
102
-
103
- afterEach(() => {
104
- process.env = { ...originalEnv };
105
- });
106
-
107
- it("returns defaults when config file does not exist", () => {
108
- existsSync.mockReturnValue(false);
109
- const config = loadConfig();
110
- expect(config).toEqual(DEFAULT_CONFIG);
111
- });
112
-
113
- it("returns defaults when config file is invalid JSON", () => {
114
- existsSync.mockReturnValue(true);
115
- readFileSync.mockReturnValue("not json {{{");
116
- const config = loadConfig();
117
- expect(config).toEqual(DEFAULT_CONFIG);
118
- });
119
-
120
- it("returns defaults when config file is an array", () => {
121
- existsSync.mockReturnValue(true);
122
- readFileSync.mockReturnValue("[]");
123
- const config = loadConfig();
124
- expect(config).toEqual(DEFAULT_CONFIG);
125
- });
126
-
127
- it("returns defaults when config file is null", () => {
128
- existsSync.mockReturnValue(true);
129
- readFileSync.mockReturnValue("null");
130
- const config = loadConfig();
131
- expect(config).toEqual(DEFAULT_CONFIG);
132
- });
133
-
134
- it("merges valid strategy from config file", () => {
135
- existsSync.mockReturnValue(true);
136
- readFileSync.mockReturnValue(JSON.stringify({ account_selection_strategy: "sticky" }));
137
- const config = loadConfig();
138
- expect(config.account_selection_strategy).toBe("sticky");
139
- });
140
-
141
- it("accepts 'single' strategy from config file", () => {
142
- existsSync.mockReturnValue(true);
143
- readFileSync.mockReturnValue(JSON.stringify({ account_selection_strategy: "single" }));
144
- const config = loadConfig();
145
- expect(config.account_selection_strategy).toBe("single");
146
- });
147
-
148
- it("ignores invalid strategy", () => {
149
- existsSync.mockReturnValue(true);
150
- readFileSync.mockReturnValue(JSON.stringify({ account_selection_strategy: "invalid" }));
151
- const config = loadConfig();
152
- expect(config.account_selection_strategy).toBe("sticky");
153
- });
154
-
155
- it("accepts boolean debug", () => {
156
- existsSync.mockReturnValue(true);
157
- readFileSync.mockReturnValue(JSON.stringify({ debug: true }));
158
- const config = loadConfig();
159
- expect(config.debug).toBe(true);
160
- });
161
-
162
- it("merges signature emulation sub-config", () => {
163
- existsSync.mockReturnValue(true);
164
- readFileSync.mockReturnValue(
165
- JSON.stringify({
166
- signature_emulation: {
167
- enabled: false,
168
- fetch_claude_code_version_on_startup: false,
169
- prompt_compaction: "off",
170
- },
171
- }),
172
- );
173
- const config = loadConfig();
174
- expect(config.signature_emulation.enabled).toBe(false);
175
- expect(config.signature_emulation.fetch_claude_code_version_on_startup).toBe(false);
176
- expect(config.signature_emulation.prompt_compaction).toBe("off");
177
- });
178
-
179
- it("merges health_score sub-config", () => {
180
- existsSync.mockReturnValue(true);
181
- readFileSync.mockReturnValue(
182
- JSON.stringify({
183
- health_score: {
184
- initial: 80,
185
- success_reward: 5,
186
- },
187
- }),
188
- );
189
- const config = loadConfig();
190
- expect(config.health_score.initial).toBe(80);
191
- expect(config.health_score.success_reward).toBe(5);
192
- // Other fields should be defaults
193
- expect(config.health_score.rate_limit_penalty).toBe(-10);
194
- });
195
-
196
- it("clamps health_score values to valid ranges", () => {
197
- existsSync.mockReturnValue(true);
198
- readFileSync.mockReturnValue(
199
- JSON.stringify({
200
- health_score: {
201
- initial: 200, // max 100
202
- rate_limit_penalty: -999, // min -50
203
- },
204
- }),
205
- );
206
- const config = loadConfig();
207
- expect(config.health_score.initial).toBe(100);
208
- expect(config.health_score.rate_limit_penalty).toBe(-50);
209
- });
210
-
211
- it("merges token_bucket sub-config", () => {
212
- existsSync.mockReturnValue(true);
213
- readFileSync.mockReturnValue(
214
- JSON.stringify({
215
- token_bucket: {
216
- max_tokens: 100,
217
- },
218
- }),
219
- );
220
- const config = loadConfig();
221
- expect(config.token_bucket.max_tokens).toBe(100);
222
- expect(config.token_bucket.regeneration_rate_per_minute).toBe(6);
223
- });
224
-
225
- it("validates and merges every supported nested config section", () => {
226
- existsSync.mockReturnValue(true);
227
- readFileSync.mockReturnValue(
228
- JSON.stringify({
229
- failure_ttl_seconds: 9_000,
230
- signature_emulation: {
231
- enabled: false,
232
- fetch_claude_code_version_on_startup: false,
233
- prompt_compaction: "off",
234
- workload: " nightly-agent ",
235
- },
236
- override_model_limits: { enabled: true, context: 3_000_000, output: -1 },
237
- custom_betas: [" beta-one ", "", 42, "beta-two"],
238
- health_score: {
239
- initial: 80,
240
- success_reward: 5,
241
- rate_limit_penalty: -15,
242
- failure_penalty: -30,
243
- recovery_rate_per_hour: 4,
244
- min_usable: 60,
245
- max_score: 90,
246
- },
247
- token_bucket: { max_tokens: 75, regeneration_rate_per_minute: 8, initial_tokens: 70 },
248
- toasts: { quiet: true, debounce_seconds: 12 },
249
- headers: {
250
- emulation_profile: " 2.1.0 ",
251
- overrides: { "x-test": "value", ignored: 7, "": "empty-key" },
252
- disable: [" X-REMOVE ", 10, "", "X-OTHER"],
253
- billing_header: false,
254
- },
255
- idle_refresh: { enabled: false, window_minutes: 90, min_interval_minutes: 45 },
256
- cache_policy: {
257
- ttl: "5m",
258
- ttl_supported: false,
259
- boundary_marker: true,
260
- hit_rate_warning_threshold: 2,
261
- },
262
- fast_mode: true,
263
- telemetry: { emulate_minimal: true },
264
- usage_toast: true,
265
- cc_credential_reuse: { enabled: true, auto_detect: false, prefer_over_oauth: true },
266
- adaptive_context: { enabled: false, escalation_threshold: 200_000, deescalation_threshold: 220_000 },
267
- willow_mode: { enabled: false, idle_threshold_minutes: 45, cooldown_minutes: 90, min_turns_before_suggest: 5 },
268
- token_economy: {
269
- token_efficient_tools: true,
270
- redact_thinking: false,
271
- context_hint: false,
272
- conservative: false,
273
- debug_dump_bodies: true,
274
- ttl_thinking_strip: false,
275
- proactive_microcompact: false,
276
- microcompact_percent: 20,
277
- microcompact_keep_recent: 100,
278
- stable_tool_ordering: false,
279
- deferred_tool_names: ["read", 12, "grep"],
280
- adaptive_thinking_zero_simple: false,
281
- tool_result_dedupe: true,
282
- fast_mode_auto: true,
283
- trailing_summary_trim: true,
284
- role_scoped_cache_ttl: false,
285
- lean_system_non_main: false,
286
- simple_system_prompt: false,
287
- },
288
- token_economy_strategies: {
289
- system_prompt_tailing: true,
290
- system_prompt_tail_turns: 0,
291
- system_prompt_tail_max_chars: 100_000,
292
- tool_deferral: true,
293
- tool_description_compaction: true,
294
- adaptive_tool_set: true,
295
- tool_result_dedupe_session_wide: true,
296
- haiku_rolling_summary: true,
297
- stale_read_eviction: true,
298
- per_tool_class_prune: true,
299
- },
300
- output_cap: { enabled: false, default_max_tokens: 100, escalated_max_tokens: 300_000 },
301
- preconnect: { enabled: false, timeout_ms: 50 },
302
- overflow_recovery: { enabled: false, safety_margin: 20_000 },
303
- cache_break_detection: { enabled: false, alert_threshold: 1_500, adaptive_breakpoint: false },
304
- request_classification: {
305
- enabled: false,
306
- background_max_service_retries: 3,
307
- background_max_should_retries: 4,
308
- },
309
- token_budget: { enabled: true, default: 25_000, completion_threshold: 2 },
310
- microcompact: { enabled: false, threshold_percent: 120 },
311
- overload_recovery: { enabled: false, default_cooldown_ms: 500, poll_quota_on_overload: false },
312
- account_management: { proactive_disabled: false },
313
- anti_verbosity: { enabled: false, length_anchors: false },
314
- oauth: { sdk_token_useragent: false },
315
- }),
316
- );
317
-
318
- const config = loadConfig();
319
-
320
- expect(config.failure_ttl_seconds).toBe(7_200);
321
- expect(config.signature_emulation).toEqual({
322
- enabled: false,
323
- fetch_claude_code_version_on_startup: false,
324
- prompt_compaction: "off",
325
- workload: "nightly-agent",
326
- });
327
- expect(config.override_model_limits).toEqual({ enabled: true, context: 2_000_000, output: 0 });
328
- expect(config.custom_betas).toEqual(["beta-one", "beta-two"]);
329
- expect(config.health_score).toEqual({
330
- initial: 80,
331
- success_reward: 5,
332
- rate_limit_penalty: -15,
333
- failure_penalty: -30,
334
- recovery_rate_per_hour: 4,
335
- min_usable: 60,
336
- max_score: 90,
337
- });
338
- expect(config.token_bucket).toEqual({ max_tokens: 75, regeneration_rate_per_minute: 8, initial_tokens: 70 });
339
- expect(config.toasts).toEqual({ quiet: true, debounce_seconds: 12 });
340
- expect(config.headers).toEqual({
341
- emulation_profile: "2.1.0",
342
- overrides: { "x-test": "value" },
343
- disable: ["x-remove", "x-other"],
344
- billing_header: false,
345
- });
346
- expect(config.idle_refresh).toEqual({ enabled: false, window_minutes: 90, min_interval_minutes: 45 });
347
- expect(config.cache_policy).toEqual({
348
- ttl: "5m",
349
- ttl_supported: false,
350
- boundary_marker: true,
351
- hit_rate_warning_threshold: 1,
352
- });
353
- expect(config.fast_mode).toBe(true);
354
- expect(config.telemetry.emulate_minimal).toBe(true);
355
- expect(config.usage_toast).toBe(true);
356
- expect(config.cc_credential_reuse).toEqual({ enabled: true, auto_detect: false, prefer_over_oauth: true });
357
- expect(config.adaptive_context).toEqual({
358
- enabled: false,
359
- escalation_threshold: 200_000,
360
- deescalation_threshold: 150_000,
361
- });
362
- expect(config.willow_mode).toEqual({
363
- enabled: false,
364
- idle_threshold_minutes: 45,
365
- cooldown_minutes: 90,
366
- min_turns_before_suggest: 5,
367
- });
368
- expect(config.token_economy).toEqual({
369
- token_efficient_tools: true,
370
- redact_thinking: false,
371
- context_hint: false,
372
- conservative: false,
373
- debug_dump_bodies: true,
374
- ttl_thinking_strip: false,
375
- proactive_microcompact: false,
376
- microcompact_percent: 30,
377
- microcompact_keep_recent: 64,
378
- stable_tool_ordering: false,
379
- deferred_tool_names: ["read", "grep"],
380
- adaptive_thinking_zero_simple: false,
381
- tool_result_dedupe: true,
382
- fast_mode_auto: true,
383
- trailing_summary_trim: true,
384
- role_scoped_cache_ttl: false,
385
- lean_system_non_main: false,
386
- simple_system_prompt: false,
387
- });
388
- expect(config.token_economy_strategies).toEqual({
389
- system_prompt_tailing: true,
390
- system_prompt_tail_turns: 1,
391
- system_prompt_tail_max_chars: 50_000,
392
- tool_deferral: true,
393
- tool_description_compaction: true,
394
- adaptive_tool_set: true,
395
- tool_result_dedupe_session_wide: true,
396
- haiku_rolling_summary: true,
397
- stale_read_eviction: true,
398
- per_tool_class_prune: true,
399
- });
400
- expect(config.output_cap).toEqual({ enabled: false, default_max_tokens: 256, escalated_max_tokens: 200_000 });
401
- expect(config.preconnect).toEqual({ enabled: false, timeout_ms: 100 });
402
- expect(config.overflow_recovery).toEqual({ enabled: false, safety_margin: 10_000 });
403
- expect(config.cache_break_detection).toEqual({
404
- enabled: false,
405
- alert_threshold: 1_500,
406
- adaptive_breakpoint: false,
407
- });
408
- expect(config.request_classification).toEqual({
409
- enabled: false,
410
- background_max_service_retries: 3,
411
- background_max_should_retries: 4,
412
- });
413
- expect(config.token_budget).toEqual({ enabled: true, default: 25_000, completion_threshold: 1 });
414
- expect(config.microcompact).toEqual({ enabled: false, threshold_percent: 100 });
415
- expect(config.overload_recovery).toEqual({
416
- enabled: false,
417
- default_cooldown_ms: 1_000,
418
- poll_quota_on_overload: false,
419
- });
420
- expect(config.account_management.proactive_disabled).toBe(false);
421
- expect(config.anti_verbosity).toEqual({ enabled: false, length_anchors: false });
422
- expect(config.oauth.sdk_token_useragent).toBe(false);
423
- });
424
-
425
- // Environment variable overrides
426
- it("overrides strategy from OPENCODE_ANTHROPIC_STRATEGY", () => {
427
- existsSync.mockReturnValue(false);
428
- process.env.OPENCODE_ANTHROPIC_STRATEGY = "round-robin";
429
- const config = loadConfig();
430
- expect(config.account_selection_strategy).toBe("round-robin");
431
- });
432
-
433
- it("ignores invalid OPENCODE_ANTHROPIC_STRATEGY", () => {
434
- existsSync.mockReturnValue(false);
435
- process.env.OPENCODE_ANTHROPIC_STRATEGY = "invalid";
436
- const config = loadConfig();
437
- expect(config.account_selection_strategy).toBe("sticky");
438
- });
439
-
440
- it("enables debug from OPENCODE_ANTHROPIC_DEBUG=1", () => {
441
- existsSync.mockReturnValue(false);
442
- process.env.OPENCODE_ANTHROPIC_DEBUG = "1";
443
- const config = loadConfig();
444
- expect(config.debug).toBe(true);
445
- });
446
-
447
- it("enables debug from OPENCODE_ANTHROPIC_DEBUG=true", () => {
448
- existsSync.mockReturnValue(false);
449
- process.env.OPENCODE_ANTHROPIC_DEBUG = "true";
450
- const config = loadConfig();
451
- expect(config.debug).toBe(true);
452
- });
453
-
454
- it("disables debug from OPENCODE_ANTHROPIC_DEBUG=0", () => {
455
- existsSync.mockReturnValue(true);
456
- readFileSync.mockReturnValue(JSON.stringify({ debug: true }));
457
- process.env.OPENCODE_ANTHROPIC_DEBUG = "0";
458
- const config = loadConfig();
459
- expect(config.debug).toBe(false);
460
- });
461
-
462
- it("disables signature emulation from OPENCODE_ANTHROPIC_EMULATE_CLAUDE_CODE_SIGNATURE=0", () => {
463
- existsSync.mockReturnValue(false);
464
- process.env.OPENCODE_ANTHROPIC_EMULATE_CLAUDE_CODE_SIGNATURE = "0";
465
- const config = loadConfig();
466
- expect(config.signature_emulation.enabled).toBe(false);
467
- });
468
-
469
- it("disables version fetch from OPENCODE_ANTHROPIC_FETCH_CLAUDE_CODE_VERSION=0", () => {
470
- existsSync.mockReturnValue(false);
471
- process.env.OPENCODE_ANTHROPIC_FETCH_CLAUDE_CODE_VERSION = "0";
472
- const config = loadConfig();
473
- expect(config.signature_emulation.fetch_claude_code_version_on_startup).toBe(false);
474
- });
475
-
476
- it("disables prompt compaction from OPENCODE_ANTHROPIC_PROMPT_COMPACTION=off", () => {
477
- existsSync.mockReturnValue(false);
478
- process.env.OPENCODE_ANTHROPIC_PROMPT_COMPACTION = "off";
479
- const config = loadConfig();
480
- expect(config.signature_emulation.prompt_compaction).toBe("off");
481
- });
482
-
483
- it("env overrides take precedence over config file", () => {
484
- existsSync.mockReturnValue(true);
485
- readFileSync.mockReturnValue(JSON.stringify({ account_selection_strategy: "sticky" }));
486
- process.env.OPENCODE_ANTHROPIC_STRATEGY = "round-robin";
487
- const config = loadConfig();
488
- expect(config.account_selection_strategy).toBe("round-robin");
489
- });
490
-
491
- // Toast config
492
- it("has toast defaults", () => {
493
- existsSync.mockReturnValue(false);
494
- const config = loadConfig();
495
- expect(config.toasts.quiet).toBe(false);
496
- expect(config.toasts.debounce_seconds).toBe(30);
497
- });
498
-
499
- it("merges toasts sub-config", () => {
500
- existsSync.mockReturnValue(true);
501
- readFileSync.mockReturnValue(JSON.stringify({ toasts: { quiet: true, debounce_seconds: 10 } }));
502
- const config = loadConfig();
503
- expect(config.toasts.quiet).toBe(true);
504
- expect(config.toasts.debounce_seconds).toBe(10);
505
- });
506
-
507
- it("clamps debounce_seconds to valid range", () => {
508
- existsSync.mockReturnValue(true);
509
- readFileSync.mockReturnValue(JSON.stringify({ toasts: { debounce_seconds: 999 } }));
510
- const config = loadConfig();
511
- expect(config.toasts.debounce_seconds).toBe(300);
512
- });
513
-
514
- it("clamps negative debounce_seconds to 0", () => {
515
- existsSync.mockReturnValue(true);
516
- readFileSync.mockReturnValue(JSON.stringify({ toasts: { debounce_seconds: -5 } }));
517
- const config = loadConfig();
518
- expect(config.toasts.debounce_seconds).toBe(0);
519
- });
520
-
521
- it("ignores non-boolean quiet", () => {
522
- existsSync.mockReturnValue(true);
523
- readFileSync.mockReturnValue(JSON.stringify({ toasts: { quiet: "yes" } }));
524
- const config = loadConfig();
525
- expect(config.toasts.quiet).toBe(false);
526
- });
527
-
528
- it("enables quiet from OPENCODE_ANTHROPIC_QUIET=1", () => {
529
- existsSync.mockReturnValue(false);
530
- process.env.OPENCODE_ANTHROPIC_QUIET = "1";
531
- const config = loadConfig();
532
- expect(config.toasts.quiet).toBe(true);
533
- });
534
-
535
- it("disables quiet from OPENCODE_ANTHROPIC_QUIET=0", () => {
536
- existsSync.mockReturnValue(true);
537
- readFileSync.mockReturnValue(JSON.stringify({ toasts: { quiet: true } }));
538
- process.env.OPENCODE_ANTHROPIC_QUIET = "0";
539
- const config = loadConfig();
540
- expect(config.toasts.quiet).toBe(false);
541
- });
542
-
543
- // --- adaptive_context config tests ---
544
-
545
- it("has adaptive_context defaults (enabled)", () => {
546
- existsSync.mockReturnValue(false);
547
- const config = loadConfig();
548
- expect(config.adaptive_context).toEqual({
549
- enabled: true,
550
- escalation_threshold: 150_000,
551
- deescalation_threshold: 100_000,
552
- });
553
- });
554
-
555
- it("merges adaptive_context from config file", () => {
556
- existsSync.mockReturnValue(true);
557
- readFileSync.mockReturnValue(
558
- JSON.stringify({ adaptive_context: { enabled: true, escalation_threshold: 200_000 } }),
559
- );
560
- const config = loadConfig();
561
- expect(config.adaptive_context.enabled).toBe(true);
562
- expect(config.adaptive_context.escalation_threshold).toBe(200_000);
563
- expect(config.adaptive_context.deescalation_threshold).toBe(100_000); // default preserved
564
- });
565
-
566
- it("clamps adaptive_context thresholds to valid ranges", () => {
567
- existsSync.mockReturnValue(true);
568
- readFileSync.mockReturnValue(
569
- JSON.stringify({
570
- adaptive_context: {
571
- enabled: true,
572
- escalation_threshold: 999_999, // above 500K max → clamped
573
- deescalation_threshold: 1, // below 20K min → clamped
574
- },
575
- }),
576
- );
577
- const config = loadConfig();
578
- expect(config.adaptive_context.escalation_threshold).toBe(500_000);
579
- expect(config.adaptive_context.deescalation_threshold).toBe(20_000);
580
- });
581
-
582
- it("ignores non-boolean adaptive_context.enabled", () => {
583
- existsSync.mockReturnValue(true);
584
- readFileSync.mockReturnValue(JSON.stringify({ adaptive_context: { enabled: "yes" } }));
585
- const config = loadConfig();
586
- expect(config.adaptive_context.enabled).toBe(true);
587
- });
588
-
589
- it("ignores non-number adaptive_context thresholds", () => {
590
- existsSync.mockReturnValue(true);
591
- readFileSync.mockReturnValue(
592
- JSON.stringify({
593
- adaptive_context: { escalation_threshold: "high", deescalation_threshold: null },
594
- }),
595
- );
596
- const config = loadConfig();
597
- expect(config.adaptive_context.escalation_threshold).toBe(150_000);
598
- expect(config.adaptive_context.deescalation_threshold).toBe(100_000);
599
- });
600
-
601
- it("enables adaptive context from OPENCODE_ANTHROPIC_ADAPTIVE_CONTEXT=1", () => {
602
- existsSync.mockReturnValue(false);
603
- process.env.OPENCODE_ANTHROPIC_ADAPTIVE_CONTEXT = "1";
604
- const config = loadConfig();
605
- expect(config.adaptive_context.enabled).toBe(true);
606
- });
607
-
608
- it("disables adaptive context from OPENCODE_ANTHROPIC_ADAPTIVE_CONTEXT=0", () => {
609
- existsSync.mockReturnValue(true);
610
- readFileSync.mockReturnValue(JSON.stringify({ adaptive_context: { enabled: true } }));
611
- process.env.OPENCODE_ANTHROPIC_ADAPTIVE_CONTEXT = "0";
612
- const config = loadConfig();
613
- expect(config.adaptive_context.enabled).toBe(false);
614
- });
615
-
616
- it("applies true-form environment overrides for feature switches", () => {
617
- existsSync.mockReturnValue(true);
618
- readFileSync.mockReturnValue(
619
- JSON.stringify({
620
- signature_emulation: { enabled: false, fetch_claude_code_version_on_startup: false, prompt_compaction: "off" },
621
- override_model_limits: { enabled: false },
622
- cc_credential_reuse: { enabled: false },
623
- account_management: { proactive_disabled: false },
624
- anti_verbosity: { enabled: false },
625
- }),
626
- );
627
- process.env.OPENCODE_ANTHROPIC_EMULATE_CLAUDE_CODE_SIGNATURE = "true";
628
- process.env.OPENCODE_ANTHROPIC_FETCH_CLAUDE_CODE_VERSION = "true";
629
- process.env.OPENCODE_ANTHROPIC_PROMPT_COMPACTION = "minimal";
630
- process.env.OPENCODE_ANTHROPIC_OVERRIDE_MODEL_LIMITS = "true";
631
- process.env.OPENCODE_ANTHROPIC_CC_CREDENTIALS = "true";
632
- process.env.OPENCODE_ANTHROPIC_PROACTIVE_DISABLED = "true";
633
- process.env.OPENCODE_ANTHROPIC_ANTI_VERBOSITY = "true";
634
-
635
- const config = loadConfig();
636
-
637
- expect(config.signature_emulation).toMatchObject({
638
- enabled: true,
639
- fetch_claude_code_version_on_startup: true,
640
- prompt_compaction: "minimal",
641
- });
642
- expect(config.override_model_limits.enabled).toBe(true);
643
- expect(config.cc_credential_reuse.enabled).toBe(true);
644
- expect(config.account_management.proactive_disabled).toBe(true);
645
- expect(config.anti_verbosity.enabled).toBe(true);
646
- });
647
-
648
- it("applies false-form environment overrides for feature switches", () => {
649
- existsSync.mockReturnValue(false);
650
- process.env.OPENCODE_ANTHROPIC_OVERRIDE_MODEL_LIMITS = "false";
651
- process.env.OPENCODE_ANTHROPIC_CC_CREDENTIALS = "false";
652
- process.env.OPENCODE_ANTHROPIC_PROACTIVE_DISABLED = "false";
653
- process.env.OPENCODE_ANTHROPIC_ANTI_VERBOSITY = "false";
654
- process.env.OPENCODE_ANTHROPIC_LENGTH_ANCHORS = "false";
655
-
656
- const config = loadConfig();
657
-
658
- expect(config.override_model_limits.enabled).toBe(false);
659
- expect(config.cc_credential_reuse.enabled).toBe(false);
660
- expect(config.account_management.proactive_disabled).toBe(false);
661
- expect(config.anti_verbosity).toEqual({ enabled: false, length_anchors: false });
662
- });
663
- });
664
-
665
- describe("loadRawConfig", () => {
666
- beforeEach(() => {
667
- vi.resetAllMocks();
668
- });
669
-
670
- it("returns parsed values without applying validation", () => {
671
- existsSync.mockReturnValue(true);
672
- readFileSync.mockReturnValue(JSON.stringify({ failure_ttl_seconds: -10, custom_key: "preserved" }));
673
-
674
- expect(loadRawConfig()).toEqual({ failure_ttl_seconds: -10, custom_key: "preserved" });
675
- });
676
-
677
- it("returns an empty object for missing, malformed, or non-object config", () => {
678
- existsSync.mockReturnValueOnce(false).mockReturnValue(true);
679
- readFileSync.mockReturnValueOnce("invalid json").mockReturnValueOnce("[]");
680
-
681
- expect(loadRawConfig()).toEqual({});
682
- expect(loadRawConfig()).toEqual({});
683
- expect(loadRawConfig()).toEqual({});
684
- });
685
- });
686
-
687
- describe("token_economy_strategies.haiku_rolling_summary", () => {
688
- beforeEach(() => {
689
- vi.resetAllMocks();
690
- });
691
-
692
- it("defaults to false in DEFAULT_CONFIG", () => {
693
- expect(DEFAULT_CONFIG.token_economy_strategies.haiku_rolling_summary).toBe(false);
694
- });
695
-
696
- it("defaults to false when config file omits the flag", () => {
697
- existsSync.mockReturnValue(true);
698
- readFileSync.mockReturnValue(JSON.stringify({}));
699
- const cfg = loadConfig();
700
- expect(cfg.token_economy_strategies.haiku_rolling_summary).toBe(false);
701
- });
702
-
703
- it("accepts true when explicitly set in config file", () => {
704
- existsSync.mockReturnValue(true);
705
- readFileSync.mockReturnValue(JSON.stringify({ token_economy_strategies: { haiku_rolling_summary: true } }));
706
- const cfg = loadConfig();
707
- expect(cfg.token_economy_strategies.haiku_rolling_summary).toBe(true);
708
- });
709
-
710
- it("falls back to default on non-boolean value (graceful, does not throw)", () => {
711
- existsSync.mockReturnValue(true);
712
- readFileSync.mockReturnValue(JSON.stringify({ token_economy_strategies: { haiku_rolling_summary: "yes" } }));
713
- const cfg = loadConfig();
714
- expect(cfg.token_economy_strategies.haiku_rolling_summary).toBe(false);
715
- });
716
- });
717
-
718
- describe("token_economy_strategies.stale_read_eviction", () => {
719
- beforeEach(() => {
720
- vi.resetAllMocks();
721
- });
722
-
723
- it("defaults to false in DEFAULT_CONFIG", () => {
724
- expect(DEFAULT_CONFIG.token_economy_strategies.stale_read_eviction).toBe(false);
725
- });
726
-
727
- it("accepts true when explicitly set", () => {
728
- existsSync.mockReturnValue(true);
729
- readFileSync.mockReturnValue(JSON.stringify({ token_economy_strategies: { stale_read_eviction: true } }));
730
- const cfg = loadConfig();
731
- expect(cfg.token_economy_strategies.stale_read_eviction).toBe(true);
732
- });
733
-
734
- it("falls back to default on non-boolean value", () => {
735
- existsSync.mockReturnValue(true);
736
- readFileSync.mockReturnValue(JSON.stringify({ token_economy_strategies: { stale_read_eviction: 1 } }));
737
- const cfg = loadConfig();
738
- expect(cfg.token_economy_strategies.stale_read_eviction).toBe(false);
739
- });
740
- });
741
-
742
- describe("token_economy_strategies.per_tool_class_prune", () => {
743
- beforeEach(() => {
744
- vi.resetAllMocks();
745
- });
746
-
747
- it("defaults to false in DEFAULT_CONFIG", () => {
748
- expect(DEFAULT_CONFIG.token_economy_strategies.per_tool_class_prune).toBe(false);
749
- });
750
-
751
- it("accepts true when explicitly set", () => {
752
- existsSync.mockReturnValue(true);
753
- readFileSync.mockReturnValue(JSON.stringify({ token_economy_strategies: { per_tool_class_prune: true } }));
754
- const cfg = loadConfig();
755
- expect(cfg.token_economy_strategies.per_tool_class_prune).toBe(true);
756
- });
757
-
758
- it("falls back to default on non-boolean value", () => {
759
- existsSync.mockReturnValue(true);
760
- readFileSync.mockReturnValue(JSON.stringify({ token_economy_strategies: { per_tool_class_prune: "on" } }));
761
- const cfg = loadConfig();
762
- expect(cfg.token_economy_strategies.per_tool_class_prune).toBe(false);
763
- });
764
- });