@diyor28/context 1.0.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 (280) hide show
  1. package/README.md +270 -0
  2. package/dist/__tests__/attachment-selector.test.d.ts +11 -0
  3. package/dist/__tests__/attachment-selector.test.d.ts.map +1 -0
  4. package/dist/__tests__/attachment-selector.test.js +449 -0
  5. package/dist/__tests__/attachment-selector.test.js.map +1 -0
  6. package/dist/__tests__/cache-breakpoints.test.d.ts +11 -0
  7. package/dist/__tests__/cache-breakpoints.test.d.ts.map +1 -0
  8. package/dist/__tests__/cache-breakpoints.test.js +398 -0
  9. package/dist/__tests__/cache-breakpoints.test.js.map +1 -0
  10. package/dist/__tests__/codecs.test.d.ts +7 -0
  11. package/dist/__tests__/codecs.test.d.ts.map +1 -0
  12. package/dist/__tests__/codecs.test.js +331 -0
  13. package/dist/__tests__/codecs.test.js.map +1 -0
  14. package/dist/__tests__/compactor.test.d.ts +11 -0
  15. package/dist/__tests__/compactor.test.d.ts.map +1 -0
  16. package/dist/__tests__/compactor.test.js +519 -0
  17. package/dist/__tests__/compactor.test.js.map +1 -0
  18. package/dist/__tests__/context-graph.test.d.ts +7 -0
  19. package/dist/__tests__/context-graph.test.d.ts.map +1 -0
  20. package/dist/__tests__/context-graph.test.js +262 -0
  21. package/dist/__tests__/context-graph.test.js.map +1 -0
  22. package/dist/__tests__/hash.test.d.ts +7 -0
  23. package/dist/__tests__/hash.test.d.ts.map +1 -0
  24. package/dist/__tests__/hash.test.js +228 -0
  25. package/dist/__tests__/hash.test.js.map +1 -0
  26. package/dist/__tests__/integration.test.d.ts +15 -0
  27. package/dist/__tests__/integration.test.d.ts.map +1 -0
  28. package/dist/__tests__/integration.test.js +728 -0
  29. package/dist/__tests__/integration.test.js.map +1 -0
  30. package/dist/__tests__/kind-order.test.d.ts +7 -0
  31. package/dist/__tests__/kind-order.test.d.ts.map +1 -0
  32. package/dist/__tests__/kind-order.test.js +243 -0
  33. package/dist/__tests__/kind-order.test.js.map +1 -0
  34. package/dist/__tests__/phase2-integration.test.d.ts +5 -0
  35. package/dist/__tests__/phase2-integration.test.d.ts.map +1 -0
  36. package/dist/__tests__/phase2-integration.test.js +222 -0
  37. package/dist/__tests__/phase2-integration.test.js.map +1 -0
  38. package/dist/__tests__/queries.test.d.ts +7 -0
  39. package/dist/__tests__/queries.test.d.ts.map +1 -0
  40. package/dist/__tests__/queries.test.js +254 -0
  41. package/dist/__tests__/queries.test.js.map +1 -0
  42. package/dist/__tests__/token-estimator.test.d.ts +7 -0
  43. package/dist/__tests__/token-estimator.test.d.ts.map +1 -0
  44. package/dist/__tests__/token-estimator.test.js +267 -0
  45. package/dist/__tests__/token-estimator.test.js.map +1 -0
  46. package/dist/adapters/anthropic-estimator.d.ts +38 -0
  47. package/dist/adapters/anthropic-estimator.d.ts.map +1 -0
  48. package/dist/adapters/anthropic-estimator.js +108 -0
  49. package/dist/adapters/anthropic-estimator.js.map +1 -0
  50. package/dist/adapters/attachment-resolver.d.ts +96 -0
  51. package/dist/adapters/attachment-resolver.d.ts.map +1 -0
  52. package/dist/adapters/attachment-resolver.js +176 -0
  53. package/dist/adapters/attachment-resolver.js.map +1 -0
  54. package/dist/adapters/attachment-selector.d.ts +59 -0
  55. package/dist/adapters/attachment-selector.d.ts.map +1 -0
  56. package/dist/adapters/attachment-selector.js +163 -0
  57. package/dist/adapters/attachment-selector.js.map +1 -0
  58. package/dist/adapters/gemini-estimator.d.ts +27 -0
  59. package/dist/adapters/gemini-estimator.d.ts.map +1 -0
  60. package/dist/adapters/gemini-estimator.js +80 -0
  61. package/dist/adapters/gemini-estimator.js.map +1 -0
  62. package/dist/adapters/index.d.ts +12 -0
  63. package/dist/adapters/index.d.ts.map +1 -0
  64. package/dist/adapters/index.js +28 -0
  65. package/dist/adapters/index.js.map +1 -0
  66. package/dist/adapters/memory-store.d.ts +139 -0
  67. package/dist/adapters/memory-store.d.ts.map +1 -0
  68. package/dist/adapters/memory-store.js +187 -0
  69. package/dist/adapters/memory-store.js.map +1 -0
  70. package/dist/adapters/openai-estimator.d.ts +35 -0
  71. package/dist/adapters/openai-estimator.d.ts.map +1 -0
  72. package/dist/adapters/openai-estimator.js +89 -0
  73. package/dist/adapters/openai-estimator.js.map +1 -0
  74. package/dist/adapters/summarizer.d.ts +121 -0
  75. package/dist/adapters/summarizer.d.ts.map +1 -0
  76. package/dist/adapters/summarizer.js +121 -0
  77. package/dist/adapters/summarizer.js.map +1 -0
  78. package/dist/adapters/token-estimator.d.ts +63 -0
  79. package/dist/adapters/token-estimator.d.ts.map +1 -0
  80. package/dist/adapters/token-estimator.js +37 -0
  81. package/dist/adapters/token-estimator.js.map +1 -0
  82. package/dist/builder/context-builder.d.ts +186 -0
  83. package/dist/builder/context-builder.d.ts.map +1 -0
  84. package/dist/builder/context-builder.js +305 -0
  85. package/dist/builder/context-builder.js.map +1 -0
  86. package/dist/builder/context-fork.d.ts +166 -0
  87. package/dist/builder/context-fork.d.ts.map +1 -0
  88. package/dist/builder/context-fork.js +282 -0
  89. package/dist/builder/context-fork.js.map +1 -0
  90. package/dist/builder/index.d.ts +6 -0
  91. package/dist/builder/index.d.ts.map +1 -0
  92. package/dist/builder/index.js +22 -0
  93. package/dist/builder/index.js.map +1 -0
  94. package/dist/codecs/base.d.ts +18 -0
  95. package/dist/codecs/base.d.ts.map +1 -0
  96. package/dist/codecs/base.js +39 -0
  97. package/dist/codecs/base.js.map +1 -0
  98. package/dist/codecs/conversation-history.codec.d.ts +81 -0
  99. package/dist/codecs/conversation-history.codec.d.ts.map +1 -0
  100. package/dist/codecs/conversation-history.codec.js +89 -0
  101. package/dist/codecs/conversation-history.codec.js.map +1 -0
  102. package/dist/codecs/index.d.ts +31 -0
  103. package/dist/codecs/index.d.ts.map +1 -0
  104. package/dist/codecs/index.js +71 -0
  105. package/dist/codecs/index.js.map +1 -0
  106. package/dist/codecs/redacted-stub.codec.d.ts +32 -0
  107. package/dist/codecs/redacted-stub.codec.d.ts.map +1 -0
  108. package/dist/codecs/redacted-stub.codec.js +64 -0
  109. package/dist/codecs/redacted-stub.codec.js.map +1 -0
  110. package/dist/codecs/structured-reference.codec.d.ts +40 -0
  111. package/dist/codecs/structured-reference.codec.d.ts.map +1 -0
  112. package/dist/codecs/structured-reference.codec.js +81 -0
  113. package/dist/codecs/structured-reference.codec.js.map +1 -0
  114. package/dist/codecs/system-rules.codec.d.ts +32 -0
  115. package/dist/codecs/system-rules.codec.d.ts.map +1 -0
  116. package/dist/codecs/system-rules.codec.js +62 -0
  117. package/dist/codecs/system-rules.codec.js.map +1 -0
  118. package/dist/codecs/tool-output.codec.d.ts +66 -0
  119. package/dist/codecs/tool-output.codec.d.ts.map +1 -0
  120. package/dist/codecs/tool-output.codec.js +95 -0
  121. package/dist/codecs/tool-output.codec.js.map +1 -0
  122. package/dist/codecs/tool-schema.codec.d.ts +36 -0
  123. package/dist/codecs/tool-schema.codec.d.ts.map +1 -0
  124. package/dist/codecs/tool-schema.codec.js +74 -0
  125. package/dist/codecs/tool-schema.codec.js.map +1 -0
  126. package/dist/codecs/unsafe-text.codec.d.ts +28 -0
  127. package/dist/codecs/unsafe-text.codec.d.ts.map +1 -0
  128. package/dist/codecs/unsafe-text.codec.js +63 -0
  129. package/dist/codecs/unsafe-text.codec.js.map +1 -0
  130. package/dist/graph/context-graph.d.ts +121 -0
  131. package/dist/graph/context-graph.d.ts.map +1 -0
  132. package/dist/graph/context-graph.js +166 -0
  133. package/dist/graph/context-graph.js.map +1 -0
  134. package/dist/graph/index.d.ts +8 -0
  135. package/dist/graph/index.d.ts.map +1 -0
  136. package/dist/graph/index.js +24 -0
  137. package/dist/graph/index.js.map +1 -0
  138. package/dist/graph/kind-order.d.ts +60 -0
  139. package/dist/graph/kind-order.d.ts.map +1 -0
  140. package/dist/graph/kind-order.js +113 -0
  141. package/dist/graph/kind-order.js.map +1 -0
  142. package/dist/graph/queries.d.ts +68 -0
  143. package/dist/graph/queries.d.ts.map +1 -0
  144. package/dist/graph/queries.js +240 -0
  145. package/dist/graph/queries.js.map +1 -0
  146. package/dist/graph/views.d.ts +90 -0
  147. package/dist/graph/views.d.ts.map +1 -0
  148. package/dist/graph/views.js +173 -0
  149. package/dist/graph/views.js.map +1 -0
  150. package/dist/index.d.ts +16 -0
  151. package/dist/index.d.ts.map +1 -0
  152. package/dist/index.js +40 -0
  153. package/dist/index.js.map +1 -0
  154. package/dist/pipeline/compactor.d.ts +128 -0
  155. package/dist/pipeline/compactor.d.ts.map +1 -0
  156. package/dist/pipeline/compactor.js +346 -0
  157. package/dist/pipeline/compactor.js.map +1 -0
  158. package/dist/pipeline/index.d.ts +6 -0
  159. package/dist/pipeline/index.d.ts.map +1 -0
  160. package/dist/pipeline/index.js +22 -0
  161. package/dist/pipeline/index.js.map +1 -0
  162. package/dist/pipeline/summarizer.d.ts +18 -0
  163. package/dist/pipeline/summarizer.d.ts.map +1 -0
  164. package/dist/pipeline/summarizer.js +68 -0
  165. package/dist/pipeline/summarizer.js.map +1 -0
  166. package/dist/policies/default-policy.d.ts +29 -0
  167. package/dist/policies/default-policy.d.ts.map +1 -0
  168. package/dist/policies/default-policy.js +58 -0
  169. package/dist/policies/default-policy.js.map +1 -0
  170. package/dist/policies/index.d.ts +5 -0
  171. package/dist/policies/index.d.ts.map +1 -0
  172. package/dist/policies/index.js +21 -0
  173. package/dist/policies/index.js.map +1 -0
  174. package/dist/providers/anthropic-compiler.d.ts +58 -0
  175. package/dist/providers/anthropic-compiler.d.ts.map +1 -0
  176. package/dist/providers/anthropic-compiler.js +182 -0
  177. package/dist/providers/anthropic-compiler.js.map +1 -0
  178. package/dist/providers/capabilities.d.ts +54 -0
  179. package/dist/providers/capabilities.d.ts.map +1 -0
  180. package/dist/providers/capabilities.js +87 -0
  181. package/dist/providers/capabilities.js.map +1 -0
  182. package/dist/providers/gemini-compiler.d.ts +51 -0
  183. package/dist/providers/gemini-compiler.d.ts.map +1 -0
  184. package/dist/providers/gemini-compiler.js +206 -0
  185. package/dist/providers/gemini-compiler.js.map +1 -0
  186. package/dist/providers/index.d.ts +8 -0
  187. package/dist/providers/index.d.ts.map +1 -0
  188. package/dist/providers/index.js +24 -0
  189. package/dist/providers/index.js.map +1 -0
  190. package/dist/providers/openai-compiler.d.ts +46 -0
  191. package/dist/providers/openai-compiler.d.ts.map +1 -0
  192. package/dist/providers/openai-compiler.js +149 -0
  193. package/dist/providers/openai-compiler.js.map +1 -0
  194. package/dist/types/attachment.d.ts +62 -0
  195. package/dist/types/attachment.d.ts.map +1 -0
  196. package/dist/types/attachment.js +6 -0
  197. package/dist/types/attachment.js.map +1 -0
  198. package/dist/types/block.d.ts +61 -0
  199. package/dist/types/block.d.ts.map +1 -0
  200. package/dist/types/block.js +8 -0
  201. package/dist/types/block.js.map +1 -0
  202. package/dist/types/codec.d.ts +58 -0
  203. package/dist/types/codec.d.ts.map +1 -0
  204. package/dist/types/codec.js +6 -0
  205. package/dist/types/codec.js.map +1 -0
  206. package/dist/types/compiled.d.ts +91 -0
  207. package/dist/types/compiled.d.ts.map +1 -0
  208. package/dist/types/compiled.js +6 -0
  209. package/dist/types/compiled.js.map +1 -0
  210. package/dist/types/hash.d.ts +24 -0
  211. package/dist/types/hash.d.ts.map +1 -0
  212. package/dist/types/hash.js +49 -0
  213. package/dist/types/hash.js.map +1 -0
  214. package/dist/types/index.d.ts +10 -0
  215. package/dist/types/index.d.ts.map +1 -0
  216. package/dist/types/index.js +26 -0
  217. package/dist/types/index.js.map +1 -0
  218. package/dist/types/policy.d.ts +128 -0
  219. package/dist/types/policy.d.ts.map +1 -0
  220. package/dist/types/policy.js +55 -0
  221. package/dist/types/policy.js.map +1 -0
  222. package/package.json +55 -0
  223. package/postcss.config.js +4 -0
  224. package/src/__tests__/attachment-selector.test.ts +559 -0
  225. package/src/__tests__/cache-breakpoints.test.ts +566 -0
  226. package/src/__tests__/codecs.test.ts +417 -0
  227. package/src/__tests__/compactor.test.ts +608 -0
  228. package/src/__tests__/context-graph.test.ts +383 -0
  229. package/src/__tests__/hash.test.ts +274 -0
  230. package/src/__tests__/integration.test.ts +866 -0
  231. package/src/__tests__/kind-order.test.ts +312 -0
  232. package/src/__tests__/phase2-integration.test.ts +253 -0
  233. package/src/__tests__/queries.test.ts +387 -0
  234. package/src/__tests__/token-estimator.test.ts +326 -0
  235. package/src/adapters/anthropic-estimator.ts +125 -0
  236. package/src/adapters/attachment-resolver.ts +295 -0
  237. package/src/adapters/attachment-selector.ts +218 -0
  238. package/src/adapters/gemini-estimator.ts +93 -0
  239. package/src/adapters/index.ts +12 -0
  240. package/src/adapters/memory-store.ts +299 -0
  241. package/src/adapters/openai-estimator.ts +105 -0
  242. package/src/adapters/summarizer.ts +250 -0
  243. package/src/adapters/token-estimator.ts +74 -0
  244. package/src/builder/context-builder.ts +467 -0
  245. package/src/builder/context-fork.ts +471 -0
  246. package/src/builder/index.ts +6 -0
  247. package/src/codecs/base.ts +36 -0
  248. package/src/codecs/conversation-history.codec.ts +108 -0
  249. package/src/codecs/index.ts +57 -0
  250. package/src/codecs/redacted-stub.codec.ts +76 -0
  251. package/src/codecs/structured-reference.codec.ts +96 -0
  252. package/src/codecs/system-rules.codec.ts +74 -0
  253. package/src/codecs/tool-output.codec.ts +109 -0
  254. package/src/codecs/tool-schema.codec.ts +87 -0
  255. package/src/codecs/unsafe-text.codec.ts +74 -0
  256. package/src/graph/context-graph.ts +205 -0
  257. package/src/graph/index.ts +8 -0
  258. package/src/graph/kind-order.ts +125 -0
  259. package/src/graph/queries.ts +306 -0
  260. package/src/graph/views.ts +255 -0
  261. package/src/index.ts +31 -0
  262. package/src/pipeline/compactor.ts +563 -0
  263. package/src/pipeline/index.ts +6 -0
  264. package/src/pipeline/summarizer.ts +76 -0
  265. package/src/policies/default-policy.ts +69 -0
  266. package/src/policies/index.ts +5 -0
  267. package/src/providers/anthropic-compiler.ts +294 -0
  268. package/src/providers/capabilities.ts +144 -0
  269. package/src/providers/gemini-compiler.ts +272 -0
  270. package/src/providers/index.ts +8 -0
  271. package/src/providers/openai-compiler.ts +191 -0
  272. package/src/types/attachment.ts +86 -0
  273. package/src/types/block.ts +84 -0
  274. package/src/types/codec.ts +68 -0
  275. package/src/types/compiled.ts +109 -0
  276. package/src/types/hash.ts +58 -0
  277. package/src/types/index.ts +10 -0
  278. package/src/types/policy.ts +194 -0
  279. package/tsconfig.json +21 -0
  280. package/vitest.config.ts +21 -0
@@ -0,0 +1,566 @@
1
+ /**
2
+ * Unit tests for Anthropic cache breakpoint resolution.
3
+ *
4
+ * Tests:
5
+ * - "After last matching block" rule
6
+ * - Selector matching (kind, codecId, tag, source)
7
+ * - Diagnostics for no matches / many matches
8
+ * - Cache control placement in system messages
9
+ */
10
+
11
+ import { describe, it, expect } from 'vitest';
12
+ import { compileAnthropicContext } from '../providers/anthropic-compiler.js';
13
+ import type { ContextBlock } from '../types/block.js';
14
+ import type { ContextPolicy } from '../types/policy.js';
15
+ import type { BlockCodec } from '../types/codec.js';
16
+
17
+ // Mock codec for testing
18
+ const mockCodec: BlockCodec<any> = {
19
+ codecId: 'test-codec',
20
+ version: '1.0.0',
21
+ payloadSchema: null as any,
22
+ canonicalize: (payload) => payload,
23
+ hash: (canon) => 'test-hash',
24
+ validate: (payload) => payload,
25
+ render: (block) => ({
26
+ anthropic: [{ type: 'text', text: JSON.stringify(block.payload) }],
27
+ openai: { role: 'system', content: JSON.stringify(block.payload) },
28
+ gemini: JSON.stringify(block.payload),
29
+ }),
30
+ };
31
+
32
+ describe('Anthropic Cache Breakpoints', () => {
33
+ describe('Cache Breakpoint Resolution', () => {
34
+ it('should resolve to last matching block by kind', () => {
35
+ const blocks: ContextBlock[] = [
36
+ createBlock('hash1', 'pinned', 'block1'),
37
+ createBlock('hash2', 'pinned', 'block2'),
38
+ createBlock('hash3', 'pinned', 'block3'),
39
+ ];
40
+
41
+ const codecRegistry = new Map([['test-codec', mockCodec]]);
42
+
43
+ const result = compileAnthropicContext(
44
+ blocks,
45
+ createTestPolicy(),
46
+ {
47
+ codecRegistry,
48
+ cacheBreakpoint: { kind: 'pinned' },
49
+ }
50
+ );
51
+
52
+ // Cache control should be on last pinned block (index 2)
53
+ const systemMessages = result.system!;
54
+ expect(systemMessages).toHaveLength(3);
55
+ expect(systemMessages[0].cache_control).toBeUndefined();
56
+ expect(systemMessages[1].cache_control).toBeUndefined();
57
+ expect(systemMessages[2].cache_control).toEqual({ type: 'ephemeral' });
58
+ });
59
+
60
+ it('should resolve to last matching block by codecId', () => {
61
+ const blocks: ContextBlock[] = [
62
+ createBlockWithCodec('hash1', 'pinned', 'codec-a', 'block1'),
63
+ createBlockWithCodec('hash2', 'pinned', 'codec-b', 'block2'),
64
+ createBlockWithCodec('hash3', 'pinned', 'codec-a', 'block3'),
65
+ ];
66
+
67
+ const codecA = { ...mockCodec, codecId: 'codec-a' };
68
+ const codecB = { ...mockCodec, codecId: 'codec-b' };
69
+ const codecRegistry = new Map([
70
+ ['codec-a', codecA],
71
+ ['codec-b', codecB],
72
+ ]);
73
+
74
+ const result = compileAnthropicContext(
75
+ blocks,
76
+ createTestPolicy(),
77
+ {
78
+ codecRegistry,
79
+ cacheBreakpoint: { codecId: 'codec-a' },
80
+ }
81
+ );
82
+
83
+ // Cache control should be on last codec-a block (index 2)
84
+ const systemMessages = result.system!;
85
+ expect(systemMessages[2].cache_control).toEqual({ type: 'ephemeral' });
86
+ });
87
+
88
+ it('should resolve to last matching block by tag', () => {
89
+ const blocks: ContextBlock[] = [
90
+ createBlockWithTags('hash1', 'pinned', ['tag1'], 'block1'),
91
+ createBlockWithTags('hash2', 'pinned', ['tag2'], 'block2'),
92
+ createBlockWithTags('hash3', 'pinned', ['tag1', 'tag2'], 'block3'),
93
+ ];
94
+
95
+ const codecRegistry = new Map([['test-codec', mockCodec]]);
96
+
97
+ const result = compileAnthropicContext(
98
+ blocks,
99
+ createTestPolicy(),
100
+ {
101
+ codecRegistry,
102
+ cacheBreakpoint: { tag: 'tag1' },
103
+ }
104
+ );
105
+
106
+ // Cache control should be on last block with tag1 (index 2)
107
+ const systemMessages = result.system!;
108
+ expect(systemMessages[2].cache_control).toEqual({ type: 'ephemeral' });
109
+ });
110
+
111
+ it('should resolve to last matching block by source', () => {
112
+ const blocks: ContextBlock[] = [
113
+ createBlockWithSource('hash1', 'pinned', 'source-a', 'block1'),
114
+ createBlockWithSource('hash2', 'pinned', 'source-b', 'block2'),
115
+ createBlockWithSource('hash3', 'pinned', 'source-a', 'block3'),
116
+ ];
117
+
118
+ const codecRegistry = new Map([['test-codec', mockCodec]]);
119
+
120
+ const result = compileAnthropicContext(
121
+ blocks,
122
+ createTestPolicy(),
123
+ {
124
+ codecRegistry,
125
+ cacheBreakpoint: { source: 'source-a' },
126
+ }
127
+ );
128
+
129
+ // Cache control should be on last source-a block (index 2)
130
+ const systemMessages = result.system!;
131
+ expect(systemMessages[2].cache_control).toEqual({ type: 'ephemeral' });
132
+ });
133
+
134
+ it('should match blocks with multiple criteria (AND logic)', () => {
135
+ const blocks: ContextBlock[] = [
136
+ createBlockWithDetails('hash1', 'pinned', 'codec-a', ['tag1'], 'source-a', 'block1'),
137
+ createBlockWithDetails('hash2', 'pinned', 'codec-a', ['tag2'], 'source-a', 'block2'),
138
+ createBlockWithDetails('hash3', 'pinned', 'codec-a', ['tag1'], 'source-b', 'block3'),
139
+ createBlockWithDetails('hash4', 'pinned', 'codec-a', ['tag1'], 'source-a', 'block4'),
140
+ ];
141
+
142
+ const codecRegistry = new Map([['codec-a', mockCodec]]);
143
+
144
+ const result = compileAnthropicContext(
145
+ blocks,
146
+ createTestPolicy(),
147
+ {
148
+ codecRegistry,
149
+ cacheBreakpoint: {
150
+ kind: 'pinned',
151
+ codecId: 'codec-a',
152
+ tag: 'tag1',
153
+ source: 'source-a',
154
+ },
155
+ }
156
+ );
157
+
158
+ // Only hash1 and hash4 match all criteria, cache control on hash4 (last)
159
+ const systemMessages = result.system!;
160
+ expect(systemMessages[3].cache_control).toEqual({ type: 'ephemeral' });
161
+ });
162
+ });
163
+
164
+ describe('No Matches', () => {
165
+ it('should not add cache_control when no blocks match', () => {
166
+ const blocks: ContextBlock[] = [
167
+ createBlock('hash1', 'pinned', 'block1'),
168
+ createBlock('hash2', 'pinned', 'block2'),
169
+ ];
170
+
171
+ const codecRegistry = new Map([['test-codec', mockCodec]]);
172
+
173
+ const result = compileAnthropicContext(
174
+ blocks,
175
+ createTestPolicy(),
176
+ {
177
+ codecRegistry,
178
+ cacheBreakpoint: { kind: 'memory' }, // No memory blocks
179
+ }
180
+ );
181
+
182
+ // No cache_control on any block
183
+ const systemMessages = result.system!;
184
+ expect(systemMessages.every((m) => !m.cache_control)).toBe(true);
185
+ });
186
+
187
+ it('should handle empty blocks array', () => {
188
+ const blocks: ContextBlock[] = [];
189
+ const codecRegistry = new Map([['test-codec', mockCodec]]);
190
+
191
+ const result = compileAnthropicContext(
192
+ blocks,
193
+ createTestPolicy(),
194
+ {
195
+ codecRegistry,
196
+ cacheBreakpoint: { kind: 'pinned' },
197
+ }
198
+ );
199
+
200
+ expect(result.system).toBeUndefined();
201
+ });
202
+ });
203
+
204
+ describe('No Cache Breakpoint', () => {
205
+ it('should not add cache_control when no breakpoint provided', () => {
206
+ const blocks: ContextBlock[] = [
207
+ createBlock('hash1', 'pinned', 'block1'),
208
+ createBlock('hash2', 'pinned', 'block2'),
209
+ ];
210
+
211
+ const codecRegistry = new Map([['test-codec', mockCodec]]);
212
+
213
+ const result = compileAnthropicContext(
214
+ blocks,
215
+ createTestPolicy(),
216
+ {
217
+ codecRegistry,
218
+ // No cacheBreakpoint
219
+ }
220
+ );
221
+
222
+ const systemMessages = result.system!;
223
+ expect(systemMessages.every((m) => !m.cache_control)).toBe(true);
224
+ });
225
+ });
226
+
227
+ describe('System vs Message Blocks', () => {
228
+ it('should only apply cache control to system blocks (pinned)', () => {
229
+ const blocks: ContextBlock[] = [
230
+ createBlock('hash1', 'pinned', 'system1'),
231
+ createBlock('hash2', 'pinned', 'system2'),
232
+ createBlock('hash3', 'memory', 'memory1'), // Not pinned
233
+ createBlock('hash4', 'history', 'history1'), // Not pinned
234
+ ];
235
+
236
+ const codecRegistry = new Map([['test-codec', mockCodec]]);
237
+
238
+ const result = compileAnthropicContext(
239
+ blocks,
240
+ createTestPolicy(),
241
+ {
242
+ codecRegistry,
243
+ cacheBreakpoint: { kind: 'pinned' },
244
+ }
245
+ );
246
+
247
+ // Only system messages should have cache_control possibility
248
+ expect(result.system).toHaveLength(2);
249
+ expect(result.messages).toHaveLength(2);
250
+ expect(result.system![1].cache_control).toEqual({ type: 'ephemeral' });
251
+ });
252
+
253
+ it('should not add cache control to non-pinned blocks', () => {
254
+ const blocks: ContextBlock[] = [
255
+ createBlock('hash1', 'memory', 'memory1'),
256
+ createBlock('hash2', 'history', 'history1'),
257
+ ];
258
+
259
+ const codecRegistry = new Map([['test-codec', mockCodec]]);
260
+
261
+ const result = compileAnthropicContext(
262
+ blocks,
263
+ createTestPolicy(),
264
+ {
265
+ codecRegistry,
266
+ cacheBreakpoint: { kind: 'memory' },
267
+ }
268
+ );
269
+
270
+ // No system blocks, so no cache control
271
+ expect(result.system).toBeUndefined();
272
+ expect(result.messages).toHaveLength(2);
273
+ });
274
+ });
275
+
276
+ describe('Edge Cases', () => {
277
+ it('should handle single matching block', () => {
278
+ const blocks: ContextBlock[] = [
279
+ createBlock('hash1', 'pinned', 'block1'),
280
+ ];
281
+
282
+ const codecRegistry = new Map([['test-codec', mockCodec]]);
283
+
284
+ const result = compileAnthropicContext(
285
+ blocks,
286
+ createTestPolicy(),
287
+ {
288
+ codecRegistry,
289
+ cacheBreakpoint: { kind: 'pinned' },
290
+ }
291
+ );
292
+
293
+ const systemMessages = result.system!;
294
+ expect(systemMessages).toHaveLength(1);
295
+ expect(systemMessages[0].cache_control).toEqual({ type: 'ephemeral' });
296
+ });
297
+
298
+ it('should handle many matching blocks (use last)', () => {
299
+ const blocks: ContextBlock[] = Array.from({ length: 100 }, (_, i) =>
300
+ createBlock(`hash${i}`, 'pinned', `block${i}`)
301
+ );
302
+
303
+ const codecRegistry = new Map([['test-codec', mockCodec]]);
304
+
305
+ const result = compileAnthropicContext(
306
+ blocks,
307
+ createTestPolicy(),
308
+ {
309
+ codecRegistry,
310
+ cacheBreakpoint: { kind: 'pinned' },
311
+ }
312
+ );
313
+
314
+ // Cache control on last block
315
+ const systemMessages = result.system!;
316
+ expect(systemMessages).toHaveLength(100);
317
+ expect(systemMessages[99].cache_control).toEqual({ type: 'ephemeral' });
318
+ // All others should not have cache_control
319
+ for (let i = 0; i < 99; i++) {
320
+ expect(systemMessages[i].cache_control).toBeUndefined();
321
+ }
322
+ });
323
+
324
+ it('should handle interleaved matching and non-matching blocks', () => {
325
+ const blocks: ContextBlock[] = [
326
+ createBlockWithCodec('hash1', 'pinned', 'codec-a', 'block1'),
327
+ createBlockWithCodec('hash2', 'pinned', 'codec-b', 'block2'),
328
+ createBlockWithCodec('hash3', 'pinned', 'codec-a', 'block3'),
329
+ createBlockWithCodec('hash4', 'pinned', 'codec-b', 'block4'),
330
+ createBlockWithCodec('hash5', 'pinned', 'codec-a', 'block5'),
331
+ ];
332
+
333
+ const codecA = { ...mockCodec, codecId: 'codec-a' };
334
+ const codecB = { ...mockCodec, codecId: 'codec-b' };
335
+ const codecRegistry = new Map([
336
+ ['codec-a', codecA],
337
+ ['codec-b', codecB],
338
+ ]);
339
+
340
+ const result = compileAnthropicContext(
341
+ blocks,
342
+ createTestPolicy(),
343
+ {
344
+ codecRegistry,
345
+ cacheBreakpoint: { codecId: 'codec-a' },
346
+ }
347
+ );
348
+
349
+ // Cache control on last codec-a block (index 4)
350
+ const systemMessages = result.system!;
351
+ expect(systemMessages[4].cache_control).toEqual({ type: 'ephemeral' });
352
+ });
353
+ });
354
+
355
+ describe('Selector Combinations', () => {
356
+ it('should match when kind AND codecId both match', () => {
357
+ const blocks: ContextBlock[] = [
358
+ createBlockWithCodec('hash1', 'pinned', 'codec-a', 'block1'),
359
+ createBlockWithCodec('hash2', 'memory', 'codec-a', 'block2'), // Wrong kind
360
+ createBlockWithCodec('hash3', 'pinned', 'codec-b', 'block3'), // Wrong codec
361
+ createBlockWithCodec('hash4', 'pinned', 'codec-a', 'block4'), // Match
362
+ ];
363
+
364
+ const codecA = { ...mockCodec, codecId: 'codec-a' };
365
+ const codecB = { ...mockCodec, codecId: 'codec-b' };
366
+ const codecRegistry = new Map([
367
+ ['codec-a', codecA],
368
+ ['codec-b', codecB],
369
+ ]);
370
+
371
+ const result = compileAnthropicContext(
372
+ blocks.filter((b) => b.meta.kind === 'pinned'), // Only pinned blocks become system messages
373
+ createTestPolicy(),
374
+ {
375
+ codecRegistry,
376
+ cacheBreakpoint: { kind: 'pinned', codecId: 'codec-a' },
377
+ }
378
+ );
379
+
380
+ // hash1 and hash4 match, cache control on hash4 (last)
381
+ const systemMessages = result.system!;
382
+ // Only 3 pinned blocks: hash1, hash3, hash4
383
+ expect(systemMessages).toHaveLength(3);
384
+ expect(systemMessages[2].cache_control).toEqual({ type: 'ephemeral' });
385
+ });
386
+
387
+ it('should not match when only some criteria match', () => {
388
+ const blocks: ContextBlock[] = [
389
+ createBlockWithDetails('hash1', 'pinned', 'codec-a', ['tag1'], 'source-a', 'block1'),
390
+ createBlockWithDetails('hash2', 'pinned', 'codec-a', ['tag2'], 'source-a', 'block2'),
391
+ ];
392
+
393
+ const codecRegistry = new Map([['codec-a', mockCodec]]);
394
+
395
+ const result = compileAnthropicContext(
396
+ blocks,
397
+ createTestPolicy(),
398
+ {
399
+ codecRegistry,
400
+ cacheBreakpoint: {
401
+ codecId: 'codec-a',
402
+ tag: 'tag1',
403
+ source: 'source-b', // Doesn't match
404
+ },
405
+ }
406
+ );
407
+
408
+ // No blocks match all criteria
409
+ const systemMessages = result.system!;
410
+ expect(systemMessages.every((m) => !m.cache_control)).toBe(true);
411
+ });
412
+ });
413
+
414
+ describe('Determinism', () => {
415
+ it('should produce same cache placement for same input', () => {
416
+ const blocks: ContextBlock[] = [
417
+ createBlock('hash1', 'pinned', 'block1'),
418
+ createBlock('hash2', 'pinned', 'block2'),
419
+ createBlock('hash3', 'pinned', 'block3'),
420
+ ];
421
+
422
+ const codecRegistry = new Map([['test-codec', mockCodec]]);
423
+
424
+ const result1 = compileAnthropicContext(
425
+ blocks,
426
+ createTestPolicy(),
427
+ {
428
+ codecRegistry,
429
+ cacheBreakpoint: { kind: 'pinned' },
430
+ }
431
+ );
432
+
433
+ const result2 = compileAnthropicContext(
434
+ blocks,
435
+ createTestPolicy(),
436
+ {
437
+ codecRegistry,
438
+ cacheBreakpoint: { kind: 'pinned' },
439
+ }
440
+ );
441
+
442
+ expect(result1.system).toEqual(result2.system);
443
+ });
444
+ });
445
+ });
446
+
447
+ // Test helpers
448
+
449
+ function createBlock(
450
+ hash: string,
451
+ kind: 'pinned' | 'memory' | 'history',
452
+ payload: string
453
+ ): ContextBlock {
454
+ return {
455
+ blockHash: hash,
456
+ meta: {
457
+ kind,
458
+ sensitivity: 'public',
459
+ codecId: 'test-codec',
460
+ codecVersion: '1.0.0',
461
+ createdAt: Date.now(),
462
+ },
463
+ payload,
464
+ };
465
+ }
466
+
467
+ function createBlockWithCodec(
468
+ hash: string,
469
+ kind: 'pinned' | 'memory' | 'history',
470
+ codecId: string,
471
+ payload: string
472
+ ): ContextBlock {
473
+ return {
474
+ blockHash: hash,
475
+ meta: {
476
+ kind,
477
+ sensitivity: 'public',
478
+ codecId,
479
+ codecVersion: '1.0.0',
480
+ createdAt: Date.now(),
481
+ },
482
+ payload,
483
+ };
484
+ }
485
+
486
+ function createBlockWithTags(
487
+ hash: string,
488
+ kind: 'pinned' | 'memory' | 'history',
489
+ tags: string[],
490
+ payload: string
491
+ ): ContextBlock {
492
+ return {
493
+ blockHash: hash,
494
+ meta: {
495
+ kind,
496
+ sensitivity: 'public',
497
+ codecId: 'test-codec',
498
+ codecVersion: '1.0.0',
499
+ createdAt: Date.now(),
500
+ tags,
501
+ },
502
+ payload,
503
+ };
504
+ }
505
+
506
+ function createBlockWithSource(
507
+ hash: string,
508
+ kind: 'pinned' | 'memory' | 'history',
509
+ source: string,
510
+ payload: string
511
+ ): ContextBlock {
512
+ return {
513
+ blockHash: hash,
514
+ meta: {
515
+ kind,
516
+ sensitivity: 'public',
517
+ codecId: 'test-codec',
518
+ codecVersion: '1.0.0',
519
+ createdAt: Date.now(),
520
+ source,
521
+ },
522
+ payload,
523
+ };
524
+ }
525
+
526
+ function createBlockWithDetails(
527
+ hash: string,
528
+ kind: 'pinned' | 'memory' | 'history',
529
+ codecId: string,
530
+ tags: string[],
531
+ source: string,
532
+ payload: string
533
+ ): ContextBlock {
534
+ return {
535
+ blockHash: hash,
536
+ meta: {
537
+ kind,
538
+ sensitivity: 'public',
539
+ codecId,
540
+ codecVersion: '1.0.0',
541
+ createdAt: Date.now(),
542
+ tags,
543
+ source,
544
+ },
545
+ payload,
546
+ };
547
+ }
548
+
549
+ function createTestPolicy(): ContextPolicy {
550
+ return {
551
+ modelId: 'test-model',
552
+ provider: 'anthropic',
553
+ contextWindow: 100000,
554
+ completionReserve: 4000,
555
+ overflowStrategy: 'error',
556
+ kindPriorities: [],
557
+ sensitivity: {
558
+ maxSensitivity: 'public',
559
+ redactRestricted: true,
560
+ },
561
+ compaction: {
562
+ pruneToolOutputs: false,
563
+ summarizeHistory: false,
564
+ },
565
+ };
566
+ }