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,132 +0,0 @@
1
- import { afterEach, describe, expect, it, vi } from "vitest";
2
-
3
- import {
4
- CLAUDE_3_MODEL_RE,
5
- hasOneMillionContext,
6
- isAdaptiveThinkingModel,
7
- isEligibleFor1MContext,
8
- isFable5Model,
9
- isMythos5Model,
10
- isOpus46Model,
11
- isOpus47Model,
12
- isOpus48Model,
13
- isSonnet46Model,
14
- normalizeThinkingBlock,
15
- } from "./models.mjs";
16
-
17
- afterEach(() => {
18
- vi.unstubAllEnvs();
19
- });
20
-
21
- describe("model detection", () => {
22
- it("detects Claude 3 model IDs", () => {
23
- expect(CLAUDE_3_MODEL_RE.test("claude-3-5-sonnet-20241022")).toBe(true);
24
- expect(CLAUDE_3_MODEL_RE.test("claude-sonnet-4-6")).toBe(false);
25
- });
26
-
27
- it("detects Opus 4.6 model IDs", () => {
28
- expect(isOpus46Model("claude-opus-4-6-20260205")).toBe(true);
29
- expect(isOpus46Model("arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-opus-4.6-v1")).toBe(true);
30
- expect(isOpus46Model("claude-opus-4-7")).toBe(false);
31
- });
32
-
33
- it("detects Opus 4.7 model IDs", () => {
34
- expect(isOpus47Model("claude-opus-4.7")).toBe(true);
35
- expect(isOpus47Model("opus_4_7-eap")).toBe(true);
36
- expect(isOpus47Model("claude-opus-4-8")).toBe(false);
37
- });
38
-
39
- it("detects Opus 4.8 model IDs", () => {
40
- expect(isOpus48Model("claude-opus-4-8")).toBe(true);
41
- expect(isOpus48Model("opus.4.8-fast")).toBe(true);
42
- expect(isOpus48Model("claude-opus-4-7")).toBe(false);
43
- });
44
-
45
- it("detects Sonnet 4.6 model IDs", () => {
46
- expect(isSonnet46Model("claude-sonnet-4-6")).toBe(true);
47
- expect(isSonnet46Model("sonnet.4.6")).toBe(true);
48
- expect(isSonnet46Model("claude-sonnet-4-5")).toBe(false);
49
- });
50
-
51
- it("detects 1M-context eligibility and explicit context markers", () => {
52
- expect(isEligibleFor1MContext("claude-sonnet-4-5")).toBe(true);
53
- expect(isEligibleFor1MContext("claude-opus-4-6[1m]")).toBe(true);
54
- expect(hasOneMillionContext("claude-opus-4-6-1m")).toBe(true);
55
- expect(hasOneMillionContext("claude-opus-4-6[1m]")).toBe(false);
56
- expect(hasOneMillionContext("claude-opus-4-6")).toBe(false);
57
- });
58
-
59
- it("detects Fable 5 model IDs", () => {
60
- expect(isFable5Model("claude-fable-5")).toBe(true);
61
- expect(isFable5Model("fable_5-preview")).toBe(true);
62
- expect(isFable5Model("claude-mythos-5")).toBe(false);
63
- });
64
-
65
- it("detects Mythos 5 model IDs", () => {
66
- expect(isMythos5Model("claude-mythos-5")).toBe(true);
67
- expect(isMythos5Model("mythos.5-preview")).toBe(true);
68
- expect(isMythos5Model("claude-fable-5")).toBe(false);
69
- });
70
-
71
- it.each([
72
- "claude-opus-4-6",
73
- "claude-opus-4-7",
74
- "claude-opus-4-8",
75
- "claude-sonnet-4-6",
76
- "claude-fable-5",
77
- "claude-mythos-5",
78
- ])("recognizes %s as an adaptive-thinking model", (model) => {
79
- expect(isAdaptiveThinkingModel(model)).toBe(true);
80
- });
81
-
82
- it("rejects non-adaptive models", () => {
83
- expect(isAdaptiveThinkingModel("claude-sonnet-4-5")).toBe(false);
84
- expect(isAdaptiveThinkingModel(undefined)).toBe(false);
85
- });
86
- });
87
-
88
- describe("normalizeThinkingBlock", () => {
89
- it("passes absent and non-object values through", () => {
90
- expect(normalizeThinkingBlock(undefined, "claude-opus-4-8")).toBeUndefined();
91
- expect(normalizeThinkingBlock("enabled", "claude-opus-4-8")).toBe("enabled");
92
- });
93
-
94
- it("passes a non-adaptive model's block through unchanged", () => {
95
- const thinking = { type: "enabled", budget_tokens: 8000 };
96
-
97
- expect(normalizeThinkingBlock(thinking, "claude-sonnet-4-5")).toBe(thinking);
98
- });
99
-
100
- it("normalizes adaptive models to adaptive thinking", () => {
101
- expect(normalizeThinkingBlock({ type: "enabled", budget_tokens: 8000 }, "claude-opus-4-8")).toEqual({
102
- type: "adaptive",
103
- });
104
- });
105
-
106
- it("preserves an enabled block when adaptive thinking is disabled", () => {
107
- vi.stubEnv("OPENCODE_ANTHROPIC_DISABLE_ADAPTIVE_THINKING", "1");
108
- const thinking = { type: "enabled", budget_tokens: 12000 };
109
-
110
- expect(normalizeThinkingBlock(thinking, "claude-opus-4-8")).toBe(thinking);
111
- });
112
-
113
- it("uses the configured fallback budget when adaptive thinking is disabled", () => {
114
- vi.stubEnv("OPENCODE_ANTHROPIC_DISABLE_ADAPTIVE_THINKING", "true");
115
- vi.stubEnv("MAX_THINKING_TOKENS", "24000");
116
-
117
- expect(normalizeThinkingBlock({ type: "adaptive" }, "claude-sonnet-4-6")).toEqual({
118
- type: "enabled",
119
- budget_tokens: 24000,
120
- });
121
- });
122
-
123
- it("uses the default fallback budget when the configured value is invalid", () => {
124
- vi.stubEnv("OPENCODE_ANTHROPIC_DISABLE_ADAPTIVE_THINKING", "yes");
125
- vi.stubEnv("MAX_THINKING_TOKENS", "invalid");
126
-
127
- expect(normalizeThinkingBlock({}, "claude-fable-5")).toEqual({
128
- type: "enabled",
129
- budget_tokens: 16000,
130
- });
131
- });
132
- });
@@ -1,34 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { CORE_TOOL_NAMES, transformRequestBody } from "./request-body.mjs";
3
-
4
- describe("request body mimicry", () => {
5
- it("exports core tools and renames opencode tool names", () => {
6
- expect(CORE_TOOL_NAMES).toBeInstanceOf(Set);
7
- expect(CORE_TOOL_NAMES.has("Bash")).toBe(true);
8
- expect(CORE_TOOL_NAMES.has("Read")).toBe(true);
9
- expect(CORE_TOOL_NAMES.has("Task")).toBe(true);
10
-
11
- const body = {
12
- model: "claude-opus-4-1",
13
- max_tokens: 8000,
14
- system: [{ type: "text", text: "You are opencode" }],
15
- messages: [{ role: "user", content: "hi" }],
16
- tools: [{ name: "bash", description: "run", input_schema: { type: "object" } }],
17
- };
18
- const signature = { enabled: false };
19
- const runtime = {
20
- turns: 1,
21
- usedTools: new Set(),
22
- tokenEconomySession: null,
23
- cacheBoundaryStability: new Map(),
24
- persistentUserId: "d",
25
- accountId: "a",
26
- sessionId: "s",
27
- };
28
-
29
- const result = transformRequestBody(JSON.stringify(body), signature, runtime, "oauth-2025-04-20", {});
30
-
31
- expect(typeof result).toBe("string");
32
- expect(JSON.parse(result).tools[0].name).toBe("Bash");
33
- });
34
- });
@@ -1,155 +0,0 @@
1
- import { describe, it, expect, beforeEach } from "vitest";
2
- import {
3
- repairOrphanedToolUseBlocks,
4
- stripSlashCommandMessages,
5
- extractFirstUserMessageText,
6
- buildRequestMetadata,
7
- resolveMaxTokens,
8
- } from "./request-helpers.mjs";
9
- import { sessionMetrics } from "../session-metrics.mjs";
10
-
11
- describe("repairOrphanedToolUseBlocks", () => {
12
- it("injects placeholder tool_result into the following user message when missing", () => {
13
- const messages = [
14
- { role: "assistant", content: [{ type: "tool_use", id: "t1", name: "Read", input: {} }] },
15
- { role: "user", content: [{ type: "text", text: "no result here" }] },
16
- ];
17
- const out = repairOrphanedToolUseBlocks(messages);
18
- const userBlocks = out[1].content;
19
- expect(userBlocks[0]).toMatchObject({ type: "tool_result", tool_use_id: "t1" });
20
- expect(userBlocks[0].content).toContain("interrupted");
21
- // original text block preserved after the injected result
22
- expect(userBlocks.some((b) => b.type === "text")).toBe(true);
23
- });
24
-
25
- it("synthesizes a user tool_result message when the assistant is the last message", () => {
26
- const messages = [{ role: "assistant", content: [{ type: "tool_use", id: "t9", name: "Bash", input: {} }] }];
27
- const out = repairOrphanedToolUseBlocks(messages);
28
- expect(out).toHaveLength(2);
29
- expect(out[1]).toMatchObject({ role: "user" });
30
- expect(out[1].content[0]).toMatchObject({ type: "tool_result", tool_use_id: "t9" });
31
- });
32
-
33
- it("leaves already-paired tool_use/tool_result untouched", () => {
34
- const messages = [
35
- { role: "assistant", content: [{ type: "tool_use", id: "t1", name: "Read", input: {} }] },
36
- { role: "user", content: [{ type: "tool_result", tool_use_id: "t1", content: "ok" }] },
37
- ];
38
- const out = repairOrphanedToolUseBlocks(messages);
39
- expect(out).toEqual(messages);
40
- });
41
-
42
- it("passes non-array / empty through", () => {
43
- expect(repairOrphanedToolUseBlocks([])).toEqual([]);
44
- expect(repairOrphanedToolUseBlocks(null)).toBe(null);
45
- });
46
- });
47
-
48
- describe("stripSlashCommandMessages", () => {
49
- it("drops a /anthropic command and its ▣ Anthropic response", () => {
50
- const messages = [
51
- { role: "user", content: "/anthropic status" },
52
- { role: "assistant", content: "▣ Anthropic — account 1 active" },
53
- { role: "user", content: "real question" },
54
- ];
55
- const out = stripSlashCommandMessages(messages);
56
- expect(out).toEqual([{ role: "user", content: "real question" }]);
57
- });
58
-
59
- it("drops orphaned ▣ Anthropic responses", () => {
60
- const messages = [
61
- { role: "assistant", content: [{ type: "text", text: "▣ Anthropic done" }] },
62
- { role: "user", content: "hi" },
63
- ];
64
- const out = stripSlashCommandMessages(messages);
65
- expect(out).toEqual([{ role: "user", content: "hi" }]);
66
- });
67
-
68
- it("returns the original array when filtering would remove everything", () => {
69
- const messages = [{ role: "user", content: "/anthropic login" }];
70
- expect(stripSlashCommandMessages(messages)).toBe(messages);
71
- });
72
- });
73
-
74
- describe("extractFirstUserMessageText", () => {
75
- it("returns string content of the first user message", () => {
76
- expect(extractFirstUserMessageText([{ role: "user", content: "hello" }])).toBe("hello");
77
- });
78
-
79
- it("returns first text block for array content", () => {
80
- expect(
81
- extractFirstUserMessageText([
82
- { role: "assistant", content: "ignored" },
83
- { role: "user", content: [{ type: "image" }, { type: "text", text: "found" }] },
84
- ]),
85
- ).toBe("found");
86
- });
87
-
88
- it("returns empty string when no user message", () => {
89
- expect(extractFirstUserMessageText([{ role: "assistant", content: "x" }])).toBe("");
90
- expect(extractFirstUserMessageText(null)).toBe("");
91
- });
92
- });
93
-
94
- describe("buildRequestMetadata", () => {
95
- const ORIG = { ...process.env };
96
- beforeEach(() => {
97
- delete process.env.OPENCODE_ANTHROPIC_SIGNATURE_USER_ID;
98
- delete process.env.CLAUDE_CODE_EXTRA_METADATA;
99
- process.env = { ...ORIG };
100
- delete process.env.OPENCODE_ANTHROPIC_SIGNATURE_USER_ID;
101
- delete process.env.CLAUDE_CODE_EXTRA_METADATA;
102
- });
103
-
104
- it("encodes device/account/session ids as JSON user_id", () => {
105
- const md = buildRequestMetadata({ persistentUserId: "dev", accountId: "acct", sessionId: "sess" });
106
- expect(JSON.parse(md.user_id)).toEqual({ device_id: "dev", account_uuid: "acct", session_id: "sess" });
107
- });
108
-
109
- it("honors the raw OPENCODE_ANTHROPIC_SIGNATURE_USER_ID override verbatim", () => {
110
- process.env.OPENCODE_ANTHROPIC_SIGNATURE_USER_ID = "raw-id";
111
- expect(buildRequestMetadata({ persistentUserId: "d", accountId: "a", sessionId: "s" })).toEqual({
112
- user_id: "raw-id",
113
- });
114
- });
115
-
116
- it("merges CLAUDE_CODE_EXTRA_METADATA object into the encoded user_id", () => {
117
- process.env.CLAUDE_CODE_EXTRA_METADATA = JSON.stringify({ org: "acme" });
118
- const md = buildRequestMetadata({ persistentUserId: "d", accountId: "a", sessionId: "s" });
119
- expect(JSON.parse(md.user_id)).toMatchObject({ org: "acme", device_id: "d" });
120
- });
121
-
122
- it("ignores malformed extra metadata JSON", () => {
123
- process.env.CLAUDE_CODE_EXTRA_METADATA = "{not json";
124
- const md = buildRequestMetadata({ persistentUserId: "d", accountId: "a", sessionId: "s" });
125
- expect(JSON.parse(md.user_id)).toEqual({ device_id: "d", account_uuid: "a", session_id: "s" });
126
- });
127
- });
128
-
129
- describe("resolveMaxTokens", () => {
130
- beforeEach(() => {
131
- sessionMetrics.lastStopReason = null;
132
- });
133
-
134
- it("passes through when output_cap disabled", () => {
135
- expect(resolveMaxTokens({ max_tokens: 123 }, {})).toBe(123);
136
- });
137
-
138
- it("honors caller-specified max_tokens even when cap enabled", () => {
139
- expect(resolveMaxTokens({ max_tokens: 500 }, { output_cap: { enabled: true } })).toBe(500);
140
- });
141
-
142
- it("uses default_max_tokens when unset and not escalating", () => {
143
- expect(resolveMaxTokens({}, { output_cap: { enabled: true, default_max_tokens: 8000 } })).toBe(8000);
144
- expect(resolveMaxTokens({}, { output_cap: { enabled: true } })).toBe(8000);
145
- });
146
-
147
- it("escalates for exactly one turn after a max_tokens stop, then resets", () => {
148
- sessionMetrics.lastStopReason = "max_tokens";
149
- const cfg = { output_cap: { enabled: true, default_max_tokens: 8000, escalated_max_tokens: 64000 } };
150
- expect(resolveMaxTokens({}, cfg)).toBe(64000);
151
- // sticky for one turn only
152
- expect(sessionMetrics.lastStopReason).toBe(null);
153
- expect(resolveMaxTokens({}, cfg)).toBe(8000);
154
- });
155
- });
@@ -1,127 +0,0 @@
1
- import { afterEach, describe, expect, it, vi } from "vitest";
2
- import {
3
- createTransformedSSEStream,
4
- extractUsageFromSSEEvent,
5
- getMidStreamAccountError,
6
- getSSEDataPayload,
7
- resolveStreamIdleTimeoutMs,
8
- stripMcpPrefixFromParsedEvent,
9
- } from "./response-stream.mjs";
10
-
11
- describe("response stream mimicry", () => {
12
- afterEach(() => {
13
- delete process.env.OPENCODE_ANTHROPIC_STREAM_IDLE_TIMEOUT_MS;
14
- });
15
-
16
- it("reverse-maps tool names in every supported tool_use location", () => {
17
- const parsed = {
18
- type: "message_start",
19
- content_block: { type: "tool_use", name: "mcp_write_file" },
20
- message: {
21
- content: [
22
- { type: "tool_use", name: "Bash" },
23
- { type: "text", text: "call mcp_write_file", name: "mcp_keep_me" },
24
- ],
25
- },
26
- content: [{ type: "tool_use", name: "WebFetch" }],
27
- };
28
-
29
- expect(stripMcpPrefixFromParsedEvent(parsed)).toBe(true);
30
- expect(parsed.content_block.name).toBe("write_file");
31
- expect(parsed.message.content[0].name).toBe("bash");
32
- expect(parsed.message.content[1]).toEqual({ type: "text", text: "call mcp_write_file", name: "mcp_keep_me" });
33
- expect(parsed.content[0].name).toBe("webfetch");
34
- });
35
-
36
- it("combines multiline data payloads and ignores the done sentinel", () => {
37
- expect(getSSEDataPayload("event: message\ndata: first\ndata: second")).toBe("first\n second");
38
- expect(getSSEDataPayload("data: [DONE]")).toBeNull();
39
- });
40
-
41
- it("extracts final usage and stop reason from message_delta", () => {
42
- const stats = { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 };
43
-
44
- extractUsageFromSSEEvent(
45
- {
46
- type: "message_delta",
47
- delta: { stop_reason: "max_tokens" },
48
- usage: {
49
- input_tokens: 11,
50
- output_tokens: 7,
51
- cache_read_input_tokens: 5,
52
- cache_creation_input_tokens: 3,
53
- server_tool_use: { web_search_requests: 2 },
54
- },
55
- },
56
- stats,
57
- );
58
-
59
- expect(stats).toEqual({
60
- inputTokens: 11,
61
- outputTokens: 7,
62
- cacheReadTokens: 5,
63
- cacheWriteTokens: 3,
64
- webSearchRequests: 2,
65
- lastStopReason: "max_tokens",
66
- });
67
- });
68
-
69
- it("identifies an account-specific mid-stream authentication error", () => {
70
- expect(
71
- getMidStreamAccountError({
72
- type: "error",
73
- error: { type: "authentication_error", message: "invalid bearer token" },
74
- }),
75
- ).toEqual({ reason: "AUTH_FAILED", invalidateToken: true });
76
- });
77
-
78
- it("resolves idle timeout from env, config, then the disabled default", () => {
79
- expect(resolveStreamIdleTimeoutMs({ streaming: { idle_timeout_ms: 1234.9 } })).toBe(1234);
80
- expect(resolveStreamIdleTimeoutMs({})).toBe(0);
81
-
82
- process.env.OPENCODE_ANTHROPIC_STREAM_IDLE_TIMEOUT_MS = "456.8";
83
- expect(resolveStreamIdleTimeoutMs({ streaming: { idle_timeout_ms: 1234 } })).toBe(456);
84
- });
85
-
86
- it("round-trips thinking bytes, rewrites tool names, and reports final usage", async () => {
87
- const thinkingFrame =
88
- 'data: {"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":"consider mcp_write_file carefully"}}\n\n';
89
- const toolFrame =
90
- 'data: {"type":"content_block_start","content_block":{"type":"tool_use","name":"mcp_write_file","id":"t1"}}\n\n';
91
- const usageFrame =
92
- 'data: {"type":"message_delta","delta":{"stop_reason":"end_turn"},"usage":{"input_tokens":4,"output_tokens":2,"cache_read_input_tokens":1,"cache_creation_input_tokens":0}}\n\n';
93
- const encoder = new TextEncoder();
94
- const response = new Response(
95
- new ReadableStream({
96
- start(controller) {
97
- controller.enqueue(encoder.encode(thinkingFrame.slice(0, 47)));
98
- controller.enqueue(encoder.encode(thinkingFrame.slice(47) + toolFrame + usageFrame));
99
- controller.close();
100
- },
101
- }),
102
- );
103
- const onUsage = vi.fn();
104
- const stream = createTransformedSSEStream(response, {
105
- onUsage,
106
- onAccountError: null,
107
- correlationId: undefined,
108
- idleTimeoutMs: 0,
109
- captureEnabled: false,
110
- writeSseCapture: vi.fn(),
111
- });
112
-
113
- const output = await new Response(stream).text();
114
-
115
- expect(output.startsWith(thinkingFrame)).toBe(true);
116
- expect(output).toContain('"name":"write_file"');
117
- expect(output).not.toContain('"name":"mcp_write_file"');
118
- expect(onUsage).toHaveBeenCalledOnce();
119
- expect(onUsage).toHaveBeenCalledWith({
120
- inputTokens: 4,
121
- outputTokens: 2,
122
- cacheReadTokens: 1,
123
- cacheWriteTokens: 0,
124
- lastStopReason: "end_turn",
125
- });
126
- });
127
- });
@@ -1,73 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import {
3
- buildSystemPromptBlocks,
4
- dedupeSystemBlocks,
5
- getCacheControlForScope,
6
- getCachedCCPrompt,
7
- isTitleGeneratorSystemBlocks,
8
- resetCachedCCPrompt,
9
- sanitizeSystemText,
10
- tailSystemBlock,
11
- } from "./system-prompt.mjs";
12
-
13
- describe("system prompt mimicry", () => {
14
- it("sanitizes OpenCode branding with the existing case rules", () => {
15
- expect(sanitizeSystemText("OpenCode opencode openCode opencode_helper")).toBe(
16
- "Claude Code Claude Claude opencode_helper",
17
- );
18
- });
19
-
20
- it("removes duplicate normalized text blocks", () => {
21
- const first = { type: "text", text: "First line\nSecond line" };
22
- const duplicate = { type: "text", text: " First line \r\n Second line " };
23
- const unique = { type: "text", text: "Unique" };
24
-
25
- expect(dedupeSystemBlocks([first, duplicate, unique])).toEqual([first, unique]);
26
- });
27
-
28
- it("tails a system block while retaining key constraints", () => {
29
- expect(tailSystemBlock("Identity\n\ndiscard this\n# Keep\nMUST remain", 200, 3)).toBe(
30
- "Identity\n\n# Keep\nMUST remain\n\n[Verbose instructions trimmed after turn 3. Key constraints preserved above.]",
31
- );
32
- });
33
-
34
- it("detects title-generator system blocks", () => {
35
- expect(isTitleGeneratorSystemBlocks([{ type: "text", text: "You are a title generator." }])).toBe(true);
36
- expect(isTitleGeneratorSystemBlocks([{ type: "text", text: "You are a coding assistant." }])).toBe(false);
37
- });
38
-
39
- it("builds cache-control wire shapes for each scope", () => {
40
- const policy = { ttl: "1h", ttl_supported: true };
41
-
42
- expect(getCacheControlForScope("global", policy)).toEqual({ type: "ephemeral", ttl: "1h", scope: "global" });
43
- expect(getCacheControlForScope("org", policy)).toEqual({ type: "ephemeral", ttl: "1h" });
44
- expect(getCacheControlForScope(null, policy)).toBeNull();
45
- expect(getCacheControlForScope("org", { ttl: "off", ttl_supported: true })).toEqual({ type: "ephemeral" });
46
- });
47
-
48
- it("builds system prompt blocks from a minimal prompt and signature", () => {
49
- expect(
50
- buildSystemPromptBlocks([{ type: "text", text: "OpenCode assistant" }], {
51
- enabled: false,
52
- claudeCliVersion: "2.1.0",
53
- promptCompactionMode: "off",
54
- }),
55
- ).toEqual([{ type: "text", text: "Claude Code assistant" }]);
56
- });
57
-
58
- it("exposes cache reset and round-trip helpers", () => {
59
- resetCachedCCPrompt();
60
- expect(getCachedCCPrompt()).toBeNull();
61
-
62
- buildSystemPromptBlocks([{ type: "text", text: "You are an interactive assistant." }], {
63
- enabled: true,
64
- claudeCliVersion: "2.1.0",
65
- promptCompactionMode: "off",
66
- cachePolicy: { ttl: "1h", ttl_supported: true },
67
- });
68
-
69
- expect(getCachedCCPrompt()).toBe("You are an interactive assistant.");
70
- resetCachedCCPrompt();
71
- expect(getCachedCCPrompt()).toBeNull();
72
- });
73
- });