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,68 +0,0 @@
1
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
- import { dirname, join } from "node:path";
3
- import { getConfigDir } from "./config.mjs";
4
-
5
- const FLAG_FILENAME = "context-hint-disabled.flag";
6
- const FLAG_VERSION = 1;
7
-
8
- /**
9
- * @typedef {object} ContextHintFlag
10
- * @property {true} disabled
11
- * @property {string} [reason]
12
- * @property {number} [status]
13
- * @property {number} [timestamp]
14
- * @property {number} [version]
15
- */
16
-
17
- /**
18
- * @returns {string}
19
- */
20
- export function getContextHintFlagPath() {
21
- return join(getConfigDir(), FLAG_FILENAME);
22
- }
23
-
24
- /**
25
- * @returns {{ disabled: boolean, reason?: string, status?: number, timestamp?: number }}
26
- */
27
- export function loadContextHintDisabledFlag() {
28
- const p = getContextHintFlagPath();
29
- if (!existsSync(p)) return { disabled: false };
30
- try {
31
- const raw = readFileSync(p, "utf-8");
32
- const parsed = JSON.parse(raw);
33
- if (parsed && parsed.disabled === true) {
34
- return {
35
- disabled: true,
36
- reason: typeof parsed.reason === "string" ? parsed.reason : undefined,
37
- status: typeof parsed.status === "number" ? parsed.status : undefined,
38
- timestamp: typeof parsed.timestamp === "number" ? parsed.timestamp : undefined,
39
- };
40
- }
41
- } catch {
42
- // Corrupted or unreadable — treat as not disabled.
43
- }
44
- return { disabled: false };
45
- }
46
-
47
- /**
48
- * Persist a permanent disable flag for the context-hint beta. Best-effort;
49
- * disk failures are swallowed so they never abort a session.
50
- * @param {{ reason: string, status: number }} params
51
- */
52
- export function saveContextHintDisabledFlag({ reason, status }) {
53
- const p = getContextHintFlagPath();
54
- try {
55
- mkdirSync(dirname(p), { recursive: true });
56
- /** @type {ContextHintFlag} */
57
- const payload = {
58
- disabled: true,
59
- reason,
60
- status,
61
- timestamp: Date.now(),
62
- version: FLAG_VERSION,
63
- };
64
- writeFileSync(p, JSON.stringify(payload, null, 2), "utf-8");
65
- } catch {
66
- // Best-effort.
67
- }
68
- }
@@ -1,122 +0,0 @@
1
- import { beforeEach, describe, expect, it, vi } from "vitest";
2
-
3
- const state = vi.hoisted(() => ({
4
- accounts: null,
5
- config: {},
6
- configDir: "/diagnose/config",
7
- debugHeadersExists: false,
8
- directoryExists: false,
9
- entries: [],
10
- fileContents: new Map(),
11
- home: "/diagnose/home",
12
- }));
13
-
14
- vi.mock("node:fs", () => ({
15
- existsSync: vi.fn((filePath) => {
16
- const value = String(filePath);
17
- if (value.endsWith("debug-headers.log")) return state.debugHeadersExists;
18
- if (value.endsWith("request-dumps")) return state.directoryExists;
19
- return false;
20
- }),
21
- readFileSync: vi.fn((filePath) => {
22
- const value = String(filePath);
23
- if (value.endsWith("package.json")) return JSON.stringify({ version: "1.2.3-test" });
24
- const file = value.split(/[\\/]/).at(-1);
25
- if (!state.fileContents.has(file)) throw new Error(`Missing fixture: ${file}`);
26
- return state.fileContents.get(file);
27
- }),
28
- readdirSync: vi.fn(() => [...state.entries]),
29
- statSync: vi.fn((filePath) => {
30
- const file = String(filePath).split(/[\\/]/).at(-1);
31
- const contents = state.fileContents.get(file) ?? "";
32
- return { size: Buffer.byteLength(contents) };
33
- }),
34
- }));
35
-
36
- vi.mock("node:os", () => ({
37
- default: { homedir: vi.fn(() => state.home) },
38
- }));
39
-
40
- vi.mock("./config.mjs", () => ({
41
- getConfigDir: vi.fn(() => state.configDir),
42
- loadConfig: vi.fn(() => state.config),
43
- }));
44
-
45
- vi.mock("./storage.mjs", () => ({
46
- loadAccounts: vi.fn(async () => state.accounts),
47
- }));
48
-
49
- import { buildDiagnosticBundle } from "./diagnose.mjs";
50
-
51
- describe("buildDiagnosticBundle", () => {
52
- beforeEach(() => {
53
- state.accounts = null;
54
- state.config = {};
55
- state.debugHeadersExists = false;
56
- state.directoryExists = false;
57
- state.entries = [];
58
- state.fileContents.clear();
59
- });
60
-
61
- it("returns all sections without leaking seeded secrets", async () => {
62
- const bearer = "Bearer diagnoseBearerToken123";
63
- const apiKey = "sk-ant-diagnoseSecret123";
64
- const oauthToken = "oat01DiagnoseSecret123";
65
- const refreshToken = "literal-refresh-secret";
66
- const access = "literal-access-secret";
67
-
68
- state.config = {
69
- authorization: bearer,
70
- apiKey,
71
- metadata: oauthToken,
72
- refreshToken,
73
- };
74
- state.accounts = {
75
- accounts: [
76
- {
77
- email: "alice@example.com",
78
- enabled: true,
79
- consecutiveFailures: 2,
80
- rateLimitResetTimes: { primary: 1234 },
81
- access,
82
- refreshToken,
83
- },
84
- ],
85
- };
86
-
87
- const bundle = await buildDiagnosticBundle();
88
- const serialized = JSON.stringify(bundle);
89
-
90
- expect(Object.keys(bundle)).toEqual(["meta", "config", "env", "accounts", "artifacts"]);
91
- expect(serialized).not.toMatch(/Bearer\s+[A-Za-z0-9]/);
92
- expect(serialized).not.toMatch(/sk-ant-[A-Za-z0-9]{4,}/);
93
- expect(serialized).not.toMatch(/\boat01[A-Za-z0-9]{8,}/);
94
- for (const secret of [bearer, apiKey, oauthToken, refreshToken, access]) {
95
- expect(serialized).not.toContain(secret);
96
- }
97
- expect(bundle.accounts.items[0].email).toBe("a***@example.com");
98
- expect(bundle.accounts.items[0]).not.toHaveProperty("access");
99
- expect(bundle.accounts.items[0]).not.toHaveProperty("refreshToken");
100
- });
101
-
102
- it("degrades gracefully when accounts and artifact directories are absent", async () => {
103
- const bundle = await buildDiagnosticBundle();
104
-
105
- expect(bundle.accounts).toEqual({ count: 0, items: [] });
106
- expect(bundle.artifacts.requestDumps).toEqual([]);
107
- expect(bundle.artifacts.responseDumps).toEqual([]);
108
- expect(bundle.artifacts.debugHeadersLog).toEqual({ exists: false, sizeBytes: 0 });
109
- });
110
-
111
- it("records a corrupted request dump and continues", async () => {
112
- const corruptFile = "req-2026-04-10T12-00-00-000Z-correlation.json";
113
- state.directoryExists = true;
114
- state.entries = [corruptFile];
115
- state.fileContents.set(corruptFile, "{not-json");
116
-
117
- const bundle = await buildDiagnosticBundle();
118
-
119
- expect(bundle.artifacts.requestDumps).toEqual([{ file: corruptFile, error: "unparseable" }]);
120
- expect(bundle.artifacts.responseDumps).toEqual([]);
121
- });
122
- });
package/lib/env.test.mjs DELETED
@@ -1,25 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
-
3
- import { isFalsyEnv, isTruthyEnv } from "./env.mjs";
4
-
5
- describe("environment value parsers", () => {
6
- describe("isTruthyEnv", () => {
7
- it.each(["1", "true", "yes", " TRUE ", "Yes"])("accepts %j", (value) => {
8
- expect(isTruthyEnv(value)).toBe(true);
9
- });
10
-
11
- it.each([undefined, "", "0", "false", "no", "on", "off", "anything"])("rejects %j", (value) => {
12
- expect(isTruthyEnv(value)).toBe(false);
13
- });
14
- });
15
-
16
- describe("isFalsyEnv", () => {
17
- it.each(["0", "false", "no", " FALSE ", "No"])("accepts %j", (value) => {
18
- expect(isFalsyEnv(value)).toBe(true);
19
- });
20
-
21
- it.each([undefined, "", "1", "true", "yes", "on", "off", "anything"])("rejects %j", (value) => {
22
- expect(isFalsyEnv(value)).toBe(false);
23
- });
24
- });
25
- });
@@ -1,72 +0,0 @@
1
- import { describe, expect, it, vi } from "vitest";
2
- import { callHaiku } from "./haiku-call.mjs";
3
-
4
- describe("callHaiku", () => {
5
- it("returns text + tokens + cost on 200 response", async () => {
6
- const fetchMock = vi.fn(async () => ({
7
- ok: true,
8
- status: 200,
9
- json: async () => ({
10
- content: [{ type: "text", text: "summary body here" }],
11
- usage: { input_tokens: 100, output_tokens: 30 },
12
- }),
13
- }));
14
- const getAccessToken = vi.fn(async () => "oauth-token-abc");
15
-
16
- const result = await callHaiku({
17
- prompt: "summarize this",
18
- fetch: fetchMock,
19
- getAccessToken,
20
- });
21
-
22
- expect(result.text).toBe("summary body here");
23
- expect(result.tokens).toEqual({ input: 100, output: 30 });
24
- // Haiku 4.5 pricing: $1/MTok input, $5/MTok output (2026-04-18)
25
- // cost = 100/1e6 * 1 + 30/1e6 * 5 = 0.0001 + 0.00015 = 0.00025
26
- expect(result.cost).toBeCloseTo(0.00025, 8);
27
-
28
- expect(fetchMock).toHaveBeenCalledTimes(1);
29
- const [url, opts] = fetchMock.mock.calls[0];
30
- expect(url).toBe("https://api.anthropic.com/v1/messages");
31
- expect(opts.method).toBe("POST");
32
- expect(opts.headers.authorization).toBe("Bearer oauth-token-abc");
33
- expect(opts.headers["anthropic-version"]).toBe("2023-06-01");
34
- const body = JSON.parse(opts.body);
35
- expect(body.model).toBe("claude-haiku-4-5-20251001");
36
- expect(body.temperature).toBe(0);
37
- expect(body.max_tokens).toBe(2048);
38
- expect(body.messages).toEqual([{ role: "user", content: [{ type: "text", text: "summarize this" }] }]);
39
- });
40
-
41
- it("throws on non-2xx HTTP response", async () => {
42
- const fetchMock = vi.fn(async () => ({
43
- ok: false,
44
- status: 429,
45
- text: async () => "rate limited",
46
- }));
47
- const getAccessToken = vi.fn(async () => "oauth-token-abc");
48
-
49
- await expect(callHaiku({ prompt: "x", fetch: fetchMock, getAccessToken })).rejects.toThrow(/HTTP 429/);
50
- });
51
-
52
- it("throws when response content is missing or not text", async () => {
53
- const fetchMock = vi.fn(async () => ({
54
- ok: true,
55
- status: 200,
56
- json: async () => ({ content: [], usage: { input_tokens: 0, output_tokens: 0 } }),
57
- }));
58
- const getAccessToken = vi.fn(async () => "oauth-token-abc");
59
-
60
- await expect(callHaiku({ prompt: "x", fetch: fetchMock, getAccessToken })).rejects.toThrow(/no text content/i);
61
- });
62
-
63
- it("throws when getAccessToken rejects", async () => {
64
- const fetchMock = vi.fn();
65
- const getAccessToken = vi.fn(async () => {
66
- throw new Error("oauth expired");
67
- });
68
-
69
- await expect(callHaiku({ prompt: "x", fetch: fetchMock, getAccessToken })).rejects.toThrow(/oauth expired/);
70
- expect(fetchMock).not.toHaveBeenCalled();
71
- });
72
- });
@@ -1,173 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { staleReadEviction, perToolClassPrune, STALE_READ_TOOLS, REPRODUCIBLE_TOOLS } from "./message-transform.mjs";
3
-
4
- /** Build a message with a single tool part. */
5
- function toolMsg(tool, output, { compacted = false, attachments } = {}) {
6
- return {
7
- info: { role: "assistant", id: `msg-${Math.random()}` },
8
- parts: [
9
- {
10
- type: "tool",
11
- callID: "c1",
12
- tool,
13
- state: {
14
- status: "completed",
15
- input: {},
16
- output,
17
- title: "t",
18
- metadata: {},
19
- time: { start: 0, end: 1, ...(compacted ? { compacted: 1 } : {}) },
20
- ...(attachments ? { attachments } : {}),
21
- },
22
- },
23
- ],
24
- };
25
- }
26
-
27
- describe("staleReadEviction", () => {
28
- it("is a no-op when messages <= threshold", () => {
29
- const msgs = [toolMsg("read", "content")];
30
- const { evicted } = staleReadEviction({ messages: msgs, threshold: 10 });
31
- expect(evicted).toBe(0);
32
- expect(msgs[0].parts[0].state.output).toBe("content");
33
- });
34
-
35
- it("replaces read output older than threshold with placeholder", () => {
36
- const msgs = Array.from({ length: 15 }, (_, i) => toolMsg("read", `file ${i} content`));
37
- const { evicted } = staleReadEviction({ messages: msgs, threshold: 10 });
38
- expect(evicted).toBe(5);
39
- expect(msgs[0].parts[0].state.output).toMatch(/re-read if you need/);
40
- expect(msgs[4].parts[0].state.output).toMatch(/re-read if you need/);
41
- expect(msgs[5].parts[0].state.output).toBe("file 5 content");
42
- expect(msgs[14].parts[0].state.output).toBe("file 14 content");
43
- });
44
-
45
- it("leaves non-read tools untouched", () => {
46
- const msgs = Array.from({ length: 15 }, (_, i) => toolMsg("bash", `cmd ${i} output`));
47
- const { evicted } = staleReadEviction({ messages: msgs, threshold: 10 });
48
- expect(evicted).toBe(0);
49
- expect(msgs[0].parts[0].state.output).toBe("cmd 0 output");
50
- });
51
-
52
- it("skips already-compacted tool parts", () => {
53
- const msgs = [
54
- toolMsg("read", "stale but compacted", { compacted: true }),
55
- ...Array.from({ length: 11 }, (_, i) => toolMsg("read", `x${i}`)),
56
- ];
57
- const { evicted } = staleReadEviction({ messages: msgs, threshold: 10 });
58
- expect(evicted).toBe(1);
59
- expect(msgs[0].parts[0].state.output).toBe("stale but compacted");
60
- expect(msgs[1].parts[0].state.output).toMatch(/re-read if you need/);
61
- });
62
-
63
- it("strips attachments when evicting", () => {
64
- const msgs = Array.from({ length: 15 }, (_, i) =>
65
- toolMsg("read", `file ${i}`, {
66
- attachments: [{ type: "file", mime: "image/png", url: "x" }],
67
- }),
68
- );
69
- staleReadEviction({ messages: msgs, threshold: 10 });
70
- expect(msgs[0].parts[0].state.attachments).toEqual([]);
71
- expect(msgs[14].parts[0].state.attachments).toHaveLength(1);
72
- });
73
-
74
- it("accepts `view` as a read-class tool", () => {
75
- expect(STALE_READ_TOOLS.has("view")).toBe(true);
76
- const msgs = Array.from({ length: 15 }, (_, i) => toolMsg("view", `content ${i}`));
77
- const { evicted } = staleReadEviction({ messages: msgs, threshold: 10 });
78
- expect(evicted).toBe(5);
79
- });
80
-
81
- it("handles empty/missing messages gracefully", () => {
82
- expect(staleReadEviction({ messages: [] }).evicted).toBe(0);
83
- expect(staleReadEviction({ messages: null }).evicted).toBe(0);
84
- });
85
- });
86
-
87
- describe("perToolClassPrune", () => {
88
- it("prunes old reproducible outputs over the 10k threshold", () => {
89
- const output = "x".repeat(30_000); // ~7_500 tokens (chars/4)
90
- const msgs = Array.from({ length: 3 }, () => toolMsg("read", output));
91
- const { pruned, tokensSaved } = perToolClassPrune({
92
- messages: msgs,
93
- reproducibleThreshold: 10_000,
94
- });
95
- // Walk backward: newest (7_500) fits under 10k, older 2 push total over → pruned.
96
- expect(pruned).toBe(2);
97
- expect(tokensSaved).toBeGreaterThan(10_000);
98
- expect(msgs[2].parts[0].state.output).toBe(output); // newest kept
99
- expect(msgs[0].parts[0].state.output).toBe("");
100
- expect(msgs[1].parts[0].state.output).toBe("");
101
- });
102
-
103
- it("uses higher threshold for stateful tools (bash)", () => {
104
- const mid = "x".repeat(50_000); // ~12_500 tokens
105
- const msgs = Array.from({ length: 3 }, () => toolMsg("bash", mid));
106
- const { pruned } = perToolClassPrune({
107
- messages: msgs,
108
- statefulThreshold: 40_000,
109
- });
110
- // 3 × 12_500 = 37_500 < 40_000 → nothing pruned
111
- expect(pruned).toBe(0);
112
- });
113
-
114
- it("never prunes protected tools (skill)", () => {
115
- const big = "x".repeat(500_000); // ~125_000 tokens — would bust any threshold
116
- const msgs = Array.from({ length: 3 }, () => toolMsg("skill", big));
117
- const { pruned } = perToolClassPrune({ messages: msgs });
118
- expect(pruned).toBe(0);
119
- expect(msgs[0].parts[0].state.output).toBe(big);
120
- });
121
-
122
- it("stops walking back when it hits a compacted part", () => {
123
- const big = "x".repeat(50_000);
124
- const msgs = [
125
- toolMsg("read", "must-not-prune-old", { compacted: true }),
126
- ...Array.from({ length: 3 }, () => toolMsg("read", big)),
127
- ];
128
- perToolClassPrune({ messages: msgs, reproducibleThreshold: 10_000 });
129
- // Compacted sentinel preserved; walk terminates before reaching it.
130
- expect(msgs[0].parts[0].state.output).toBe("must-not-prune-old");
131
- });
132
-
133
- it("tracks reproducible and stateful budgets independently", () => {
134
- const output = "x".repeat(30_000); // ~7_500 tokens each
135
- // interleave: read, bash, read, bash, read, bash (oldest to newest)
136
- const msgs = [
137
- toolMsg("read", output),
138
- toolMsg("bash", output),
139
- toolMsg("read", output),
140
- toolMsg("bash", output),
141
- toolMsg("read", output),
142
- toolMsg("bash", output),
143
- ];
144
- const { pruned } = perToolClassPrune({
145
- messages: msgs,
146
- reproducibleThreshold: 10_000,
147
- statefulThreshold: 40_000,
148
- });
149
- // reads: 7_500, 15_000 → over → prune; 22_500 → prune. 2 old reads pruned.
150
- // bash: 7_500, 15_000, 22_500 → all under 40k → 0 pruned.
151
- expect(pruned).toBe(2);
152
- expect(msgs[4].parts[0].state.output).toBe(output); // newest read kept
153
- expect(msgs[5].parts[0].state.output).toBe(output); // newest bash kept
154
- expect(msgs[0].parts[0].state.output).toBe(""); // oldest read pruned
155
- expect(msgs[2].parts[0].state.output).toBe(""); // middle read pruned
156
- });
157
-
158
- it("normalizes tool name case when classifying", () => {
159
- expect(REPRODUCIBLE_TOOLS.has("read")).toBe(true);
160
- const output = "x".repeat(30_000); // ~7_500 tokens
161
- const msgs = Array.from({ length: 3 }, () => toolMsg("READ", output));
162
- const { pruned } = perToolClassPrune({
163
- messages: msgs,
164
- reproducibleThreshold: 10_000,
165
- });
166
- expect(pruned).toBe(2);
167
- });
168
-
169
- it("handles empty/missing messages gracefully", () => {
170
- expect(perToolClassPrune({ messages: [] }).pruned).toBe(0);
171
- expect(perToolClassPrune({ messages: null }).pruned).toBe(0);
172
- });
173
- });
@@ -1,112 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
-
3
- import { resolveCacheTtl, shouldPlaceToolBreakpoint, updateBoundaryStability } from "./cache.mjs";
4
-
5
- describe("resolveCacheTtl", () => {
6
- const base = {
7
- configuredTtl: "1h",
8
- roleScopedTtl: true,
9
- isMainForCache: true,
10
- env: {},
11
- };
12
-
13
- it("returns the configured TTL for the main role", () => {
14
- expect(resolveCacheTtl(base)).toBe("1h");
15
- });
16
-
17
- it("downgrades side-query and subagent roles when role scoping is enabled", () => {
18
- expect(resolveCacheTtl({ ...base, isMainForCache: false })).toBe("5m");
19
- expect(resolveCacheTtl({ ...base, isSubagent: true })).toBe("5m");
20
- });
21
-
22
- it("honors the 5m environment override first", () => {
23
- expect(
24
- resolveCacheTtl({
25
- ...base,
26
- env: { FORCE_PROMPT_CACHING_5M: "1", ENABLE_PROMPT_CACHING_1H: "1" },
27
- }),
28
- ).toBe("5m");
29
- });
30
-
31
- it("honors the 1h environment override before role scoping", () => {
32
- expect(
33
- resolveCacheTtl({
34
- ...base,
35
- isMainForCache: false,
36
- env: { ENABLE_PROMPT_CACHING_1H: "true" },
37
- }),
38
- ).toBe("1h");
39
- });
40
-
41
- it("keeps the configured TTL when role scoping is disabled", () => {
42
- expect(resolveCacheTtl({ ...base, configuredTtl: "5m", roleScopedTtl: false })).toBe("5m");
43
- });
44
- });
45
-
46
- describe("shouldPlaceToolBreakpoint", () => {
47
- it("places a breakpoint when stability data is unavailable", () => {
48
- expect(shouldPlaceToolBreakpoint(undefined)).toBe(true);
49
- expect(shouldPlaceToolBreakpoint(new Map())).toBe(true);
50
- });
51
-
52
- it("places a breakpoint below the stable-system boundary", () => {
53
- expect(
54
- shouldPlaceToolBreakpoint(
55
- new Map([
56
- ["system_prompt", 1],
57
- ["tool:read", 0],
58
- ]),
59
- ),
60
- ).toBe(true);
61
- });
62
-
63
- it("skips a volatile tool breakpoint once the system is stable", () => {
64
- expect(
65
- shouldPlaceToolBreakpoint(
66
- new Map([
67
- ["system_prompt", 2],
68
- ["tool:read", 0],
69
- ]),
70
- ),
71
- ).toBe(false);
72
- });
73
-
74
- it("places the breakpoint when both system and tools are stable", () => {
75
- expect(
76
- shouldPlaceToolBreakpoint(
77
- new Map([
78
- ["system_prompt", 2],
79
- ["tool:read", 1],
80
- ]),
81
- ),
82
- ).toBe(true);
83
- });
84
- });
85
-
86
- describe("updateBoundaryStability", () => {
87
- it("mutates only the passed stability map across boundary transitions", () => {
88
- const previous = new Map([
89
- ["system_prompt", "system-v1"],
90
- ["tool:read", "read-v1"],
91
- ["tool:gone", "gone-v1"],
92
- ]);
93
- const current = new Map([
94
- ["system_prompt", "system-v1"],
95
- ["tool:read", "read-v2"],
96
- ["tool:new", "new-v1"],
97
- ]);
98
- const stability = new Map([
99
- ["system_prompt", 1],
100
- ["tool:read", 4],
101
- ["tool:gone", 3],
102
- ]);
103
-
104
- expect(updateBoundaryStability(current, previous, stability)).toBeUndefined();
105
-
106
- expect([...stability]).toEqual([
107
- ["system_prompt", 2],
108
- ["tool:read", 0],
109
- ["tool:new", 0],
110
- ]);
111
- });
112
- });
@@ -1,50 +0,0 @@
1
- import { afterEach, describe, expect, it, vi } from "vitest";
2
- import { buildAnthropicBetaHeader, buildRequestHeaders, detectProvider, isHaikuModel } from "./headers.mjs";
3
-
4
- describe("mimicry headers", () => {
5
- afterEach(() => {
6
- vi.unstubAllEnvs();
7
- });
8
-
9
- it("places the OAuth beta first when signature emulation is enabled", () => {
10
- const header = buildAnthropicBetaHeader("", true, "claude-sonnet-4-6", "anthropic");
11
-
12
- expect(header.split(",")[0]).toBe("oauth-2025-04-20");
13
- });
14
-
15
- it("detects Haiku model IDs", () => {
16
- expect(isHaikuModel("claude-3-5-haiku-20241022")).toBe(true);
17
- expect(isHaikuModel("claude-sonnet-4-6")).toBe(false);
18
- });
19
-
20
- it("detects the Anthropic provider from the API URL", () => {
21
- vi.stubEnv("CLAUDE_CODE_USE_BEDROCK", "");
22
- vi.stubEnv("CLAUDE_CODE_USE_FOUNDRY", "");
23
- vi.stubEnv("CLAUDE_CODE_USE_ANTHROPIC_AWS", "");
24
- vi.stubEnv("CLAUDE_CODE_USE_MANTLE", "");
25
- vi.stubEnv("CLAUDE_CODE_USE_VERTEX", "");
26
-
27
- expect(detectProvider(new URL("https://api.anthropic.com/v1/messages"))).toBe("anthropic");
28
- });
29
-
30
- it("builds foreground request headers with a user agent and beta header", () => {
31
- vi.stubEnv("CLAUDE_CODE_BACKGROUND", "");
32
- const body = JSON.stringify({
33
- model: "claude-sonnet-4-6",
34
- messages: [{ role: "user", content: "Hello" }],
35
- });
36
-
37
- const headers = buildRequestHeaders(
38
- "https://api.anthropic.com/v1/messages",
39
- { headers: {} },
40
- "oauth-token",
41
- body,
42
- new URL("https://api.anthropic.com/v1/messages"),
43
- { enabled: true, claudeCliVersion: "2.1.195" },
44
- );
45
-
46
- expect(headers.get("user-agent")).toBeTruthy();
47
- expect(headers.get("anthropic-beta")).toContain("oauth-2025-04-20");
48
- expect(headers.get("x-app")).toBe("cli");
49
- });
50
- });