@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,387 @@
1
+ /**
2
+ * Unit tests for BlockQuery filtering logic.
3
+ *
4
+ * Tests matchesQuery, compareSensitivity, mergeQueries, and emptyQuery.
5
+ */
6
+
7
+ import { describe, it, expect } from 'vitest';
8
+ import {
9
+ matchesQuery,
10
+ compareSensitivity,
11
+ mergeQueries,
12
+ emptyQuery,
13
+ } from '../graph/queries.js';
14
+ import { ContextGraph } from '../graph/context-graph.js';
15
+ import type { ContextBlock, BlockKind } from '../types/block.js';
16
+ import type { BlockQuery } from '../graph/queries.js';
17
+
18
+ describe('BlockQuery', () => {
19
+ describe('compareSensitivity', () => {
20
+ it('should order public < internal < restricted', () => {
21
+ expect(compareSensitivity('public', 'internal')).toBeLessThan(0);
22
+ expect(compareSensitivity('internal', 'restricted')).toBeLessThan(0);
23
+ expect(compareSensitivity('public', 'restricted')).toBeLessThan(0);
24
+ });
25
+
26
+ it('should be symmetric', () => {
27
+ expect(compareSensitivity('internal', 'public')).toBeGreaterThan(0);
28
+ expect(compareSensitivity('restricted', 'internal')).toBeGreaterThan(0);
29
+ expect(compareSensitivity('restricted', 'public')).toBeGreaterThan(0);
30
+ });
31
+
32
+ it('should return zero for equal levels', () => {
33
+ expect(compareSensitivity('public', 'public')).toBe(0);
34
+ expect(compareSensitivity('internal', 'internal')).toBe(0);
35
+ expect(compareSensitivity('restricted', 'restricted')).toBe(0);
36
+ });
37
+ });
38
+
39
+ describe('matchesQuery', () => {
40
+ const graph = new ContextGraph();
41
+
42
+ describe('Kind filtering', () => {
43
+ it('should match blocks with specified kinds', () => {
44
+ const block = createBlock('hash1', 'pinned', 'test');
45
+
46
+ expect(matchesQuery(block, { kinds: ['pinned'] }, graph)).toBe(true);
47
+ expect(matchesQuery(block, { kinds: ['memory'] }, graph)).toBe(false);
48
+ });
49
+
50
+ it('should use OR logic for multiple kinds', () => {
51
+ const block = createBlock('hash1', 'memory', 'test');
52
+
53
+ expect(
54
+ matchesQuery(block, { kinds: ['pinned', 'memory'] }, graph)
55
+ ).toBe(true);
56
+ });
57
+
58
+ it('should match all blocks when kinds is empty', () => {
59
+ const block = createBlock('hash1', 'pinned', 'test');
60
+
61
+ expect(matchesQuery(block, { kinds: [] }, graph)).toBe(true);
62
+ });
63
+ });
64
+
65
+ describe('Tag filtering', () => {
66
+ it('should match blocks with all specified tags', () => {
67
+ const block = createBlock('hash1', 'pinned', 'test');
68
+ block.meta.tags = ['important', 'cached'];
69
+
70
+ expect(
71
+ matchesQuery(block, { tags: ['important'] }, graph)
72
+ ).toBe(true);
73
+ expect(
74
+ matchesQuery(block, { tags: ['important', 'cached'] }, graph)
75
+ ).toBe(true);
76
+ });
77
+
78
+ it('should use AND logic for multiple tags', () => {
79
+ const block = createBlock('hash1', 'pinned', 'test');
80
+ block.meta.tags = ['important'];
81
+
82
+ expect(
83
+ matchesQuery(block, { tags: ['important', 'cached'] }, graph)
84
+ ).toBe(false);
85
+ });
86
+
87
+ it('should match blocks without tags when tags filter is empty', () => {
88
+ const block = createBlock('hash1', 'pinned', 'test');
89
+
90
+ expect(matchesQuery(block, { tags: [] }, graph)).toBe(true);
91
+ });
92
+ });
93
+
94
+ describe('Sensitivity filtering', () => {
95
+ it('should filter by maximum sensitivity', () => {
96
+ const publicBlock = createBlock('hash1', 'pinned', 'test');
97
+ publicBlock.meta.sensitivity = 'public';
98
+
99
+ const internalBlock = createBlock('hash2', 'memory', 'test');
100
+ internalBlock.meta.sensitivity = 'internal';
101
+
102
+ expect(
103
+ matchesQuery(publicBlock, { maxSensitivity: 'public' }, graph)
104
+ ).toBe(true);
105
+ expect(
106
+ matchesQuery(internalBlock, { maxSensitivity: 'public' }, graph)
107
+ ).toBe(false);
108
+ });
109
+
110
+ it('should filter by minimum sensitivity', () => {
111
+ const publicBlock = createBlock('hash1', 'pinned', 'test');
112
+ publicBlock.meta.sensitivity = 'public';
113
+
114
+ const internalBlock = createBlock('hash2', 'memory', 'test');
115
+ internalBlock.meta.sensitivity = 'internal';
116
+
117
+ expect(
118
+ matchesQuery(publicBlock, { minSensitivity: 'internal' }, graph)
119
+ ).toBe(false);
120
+ expect(
121
+ matchesQuery(internalBlock, { minSensitivity: 'internal' }, graph)
122
+ ).toBe(true);
123
+ });
124
+ });
125
+
126
+ describe('Source filtering', () => {
127
+ it('should filter by source', () => {
128
+ const block = createBlock('hash1', 'pinned', 'test');
129
+ block.meta.source = 'test-source';
130
+
131
+ expect(
132
+ matchesQuery(block, { source: 'test-source' }, graph)
133
+ ).toBe(true);
134
+ expect(
135
+ matchesQuery(block, { source: 'other-source' }, graph)
136
+ ).toBe(false);
137
+ });
138
+ });
139
+
140
+ describe('Timestamp filtering', () => {
141
+ it('should filter by minimum creation timestamp', () => {
142
+ const block = createBlock('hash1', 'pinned', 'test');
143
+ block.meta.createdAt = 1000;
144
+
145
+ expect(
146
+ matchesQuery(block, { minCreatedAt: 900 }, graph)
147
+ ).toBe(true);
148
+ expect(
149
+ matchesQuery(block, { minCreatedAt: 1100 }, graph)
150
+ ).toBe(false);
151
+ });
152
+
153
+ it('should filter by maximum creation timestamp', () => {
154
+ const block = createBlock('hash1', 'pinned', 'test');
155
+ block.meta.createdAt = 1000;
156
+
157
+ expect(
158
+ matchesQuery(block, { maxCreatedAt: 1100 }, graph)
159
+ ).toBe(true);
160
+ expect(
161
+ matchesQuery(block, { maxCreatedAt: 900 }, graph)
162
+ ).toBe(false);
163
+ });
164
+ });
165
+
166
+ describe('Provenance filtering', () => {
167
+ it('should filter by derivedFromAny', () => {
168
+ const parent = createBlock('parent', 'pinned', 'parent');
169
+ const child = createBlock('child', 'memory', 'child');
170
+
171
+ const testGraph = new ContextGraph();
172
+ testGraph.addBlock(parent);
173
+ testGraph.addBlock(child, [{ blockHash: 'parent' }]);
174
+
175
+ expect(
176
+ matchesQuery(child, { derivedFromAny: ['parent'] }, testGraph)
177
+ ).toBe(true);
178
+ expect(
179
+ matchesQuery(child, { derivedFromAny: ['other'] }, testGraph)
180
+ ).toBe(false);
181
+ });
182
+
183
+ it('should filter by notDerivedFromAny', () => {
184
+ const parent = createBlock('parent', 'pinned', 'parent');
185
+ const child = createBlock('child', 'memory', 'child');
186
+
187
+ const testGraph = new ContextGraph();
188
+ testGraph.addBlock(parent);
189
+ testGraph.addBlock(child, [{ blockHash: 'parent' }]);
190
+
191
+ expect(
192
+ matchesQuery(child, { notDerivedFromAny: ['parent'] }, testGraph)
193
+ ).toBe(false);
194
+ expect(
195
+ matchesQuery(child, { notDerivedFromAny: ['other'] }, testGraph)
196
+ ).toBe(true);
197
+ });
198
+ });
199
+
200
+ describe('Reference filtering', () => {
201
+ it('should filter by referencesAny', () => {
202
+ const ref = createBlock('ref', 'reference', 'ref');
203
+ const block = createBlock('block', 'memory', 'block');
204
+
205
+ const testGraph = new ContextGraph();
206
+ testGraph.addBlock(ref);
207
+ testGraph.addBlock(block, undefined, ['ref']);
208
+
209
+ expect(
210
+ matchesQuery(block, { referencesAny: ['ref'] }, testGraph)
211
+ ).toBe(true);
212
+ expect(
213
+ matchesQuery(block, { referencesAny: ['other'] }, testGraph)
214
+ ).toBe(false);
215
+ });
216
+ });
217
+
218
+ describe('Exclude hashes', () => {
219
+ it('should exclude specific block hashes', () => {
220
+ const block = createBlock('hash1', 'pinned', 'test');
221
+
222
+ expect(
223
+ matchesQuery(block, { excludeHashes: ['hash2'] }, graph)
224
+ ).toBe(true);
225
+ expect(
226
+ matchesQuery(block, { excludeHashes: ['hash1'] }, graph)
227
+ ).toBe(false);
228
+ });
229
+ });
230
+
231
+ describe('Combined filters', () => {
232
+ it('should apply multiple filters with AND logic', () => {
233
+ const block = createBlock('hash1', 'pinned', 'test');
234
+ block.meta.tags = ['important'];
235
+ block.meta.sensitivity = 'public';
236
+
237
+ const query: BlockQuery = {
238
+ kinds: ['pinned'],
239
+ tags: ['important'],
240
+ maxSensitivity: 'public',
241
+ };
242
+
243
+ expect(matchesQuery(block, query, graph)).toBe(true);
244
+ });
245
+
246
+ it('should reject if any filter fails', () => {
247
+ const block = createBlock('hash1', 'pinned', 'test');
248
+ block.meta.tags = ['important'];
249
+ block.meta.sensitivity = 'internal';
250
+
251
+ const query: BlockQuery = {
252
+ kinds: ['pinned'],
253
+ tags: ['important'],
254
+ maxSensitivity: 'public', // Fails this filter
255
+ };
256
+
257
+ expect(matchesQuery(block, query, graph)).toBe(false);
258
+ });
259
+ });
260
+ });
261
+
262
+ describe('emptyQuery', () => {
263
+ it('should create empty query object', () => {
264
+ const query = emptyQuery();
265
+
266
+ expect(query).toEqual({});
267
+ });
268
+ });
269
+
270
+ describe('mergeQueries', () => {
271
+ it('should merge kinds with intersection', () => {
272
+ const q1: BlockQuery = { kinds: ['pinned', 'memory'] };
273
+ const q2: BlockQuery = { kinds: ['memory', 'history'] };
274
+
275
+ const merged = mergeQueries(q1, q2);
276
+
277
+ expect(merged.kinds).toEqual(['memory']);
278
+ });
279
+
280
+ it('should merge tags with union', () => {
281
+ const q1: BlockQuery = { tags: ['tag1'] };
282
+ const q2: BlockQuery = { tags: ['tag2'] };
283
+
284
+ const merged = mergeQueries(q1, q2);
285
+
286
+ expect(merged.tags).toEqual(['tag1', 'tag2']);
287
+ });
288
+
289
+ it('should merge sensitivity with most restrictive', () => {
290
+ const q1: BlockQuery = { minSensitivity: 'public' };
291
+ const q2: BlockQuery = { minSensitivity: 'internal' };
292
+
293
+ const merged = mergeQueries(q1, q2);
294
+
295
+ expect(merged.minSensitivity).toBe('internal'); // Higher minimum
296
+ });
297
+
298
+ it('should merge maxSensitivity with lower maximum', () => {
299
+ const q1: BlockQuery = { maxSensitivity: 'internal' };
300
+ const q2: BlockQuery = { maxSensitivity: 'public' };
301
+
302
+ const merged = mergeQueries(q1, q2);
303
+
304
+ expect(merged.maxSensitivity).toBe('public'); // Lower maximum
305
+ });
306
+
307
+ it('should merge timestamps with most restrictive range', () => {
308
+ const q1: BlockQuery = { minCreatedAt: 100, maxCreatedAt: 500 };
309
+ const q2: BlockQuery = { minCreatedAt: 200, maxCreatedAt: 400 };
310
+
311
+ const merged = mergeQueries(q1, q2);
312
+
313
+ expect(merged.minCreatedAt).toBe(200); // Max of minimums
314
+ expect(merged.maxCreatedAt).toBe(400); // Min of maximums
315
+ });
316
+
317
+ it('should merge provenance with union', () => {
318
+ const q1: BlockQuery = { derivedFromAny: ['hash1'] };
319
+ const q2: BlockQuery = { derivedFromAny: ['hash2'] };
320
+
321
+ const merged = mergeQueries(q1, q2);
322
+
323
+ expect(merged.derivedFromAny).toEqual(['hash1', 'hash2']);
324
+ });
325
+
326
+ it('should merge excludeHashes with union', () => {
327
+ const q1: BlockQuery = { excludeHashes: ['hash1'] };
328
+ const q2: BlockQuery = { excludeHashes: ['hash2'] };
329
+
330
+ const merged = mergeQueries(q1, q2);
331
+
332
+ expect(merged.excludeHashes).toEqual(['hash1', 'hash2']);
333
+ });
334
+
335
+ it('should merge maxTokens with minimum', () => {
336
+ const q1: BlockQuery = { maxTokens: 1000 };
337
+ const q2: BlockQuery = { maxTokens: 500 };
338
+
339
+ const merged = mergeQueries(q1, q2);
340
+
341
+ expect(merged.maxTokens).toBe(500);
342
+ });
343
+
344
+ it('should handle conflicting sources', () => {
345
+ const q1: BlockQuery = { source: 'source1' };
346
+ const q2: BlockQuery = { source: 'source2' };
347
+
348
+ const merged = mergeQueries(q1, q2);
349
+
350
+ // Conflicting sources = impossible query (empty kinds array)
351
+ expect(merged.kinds).toEqual([]);
352
+ });
353
+
354
+ it('should merge empty queries', () => {
355
+ const merged = mergeQueries({}, {});
356
+
357
+ expect(merged).toEqual({});
358
+ });
359
+
360
+ it('should handle single query', () => {
361
+ const q1: BlockQuery = { kinds: ['pinned'] };
362
+
363
+ const merged = mergeQueries(q1);
364
+
365
+ expect(merged).toEqual({ kinds: ['pinned'] });
366
+ });
367
+ });
368
+ });
369
+
370
+ // Helper to create test blocks
371
+ function createBlock(
372
+ hash: string,
373
+ kind: BlockKind,
374
+ payload: string
375
+ ): ContextBlock<string> {
376
+ return {
377
+ blockHash: hash,
378
+ meta: {
379
+ kind,
380
+ sensitivity: 'public',
381
+ codecId: 'test',
382
+ codecVersion: '1.0.0',
383
+ createdAt: Date.now(),
384
+ },
385
+ payload,
386
+ };
387
+ }
@@ -0,0 +1,326 @@
1
+ /**
2
+ * Unit tests for token estimators.
3
+ *
4
+ * Tests AnthropicTokenEstimator, OpenAITokenEstimator, GeminiTokenEstimator.
5
+ */
6
+
7
+ import { describe, it, expect, vi } from 'vitest';
8
+ import {
9
+ heuristicTokenCount,
10
+ serializeBlockForEstimation,
11
+ LOW_CONFIDENCE_MULTIPLIER,
12
+ } from '../adapters/token-estimator.js';
13
+ import { OpenAITokenEstimator } from '../adapters/openai-estimator.js';
14
+ import { GeminiTokenEstimator } from '../adapters/gemini-estimator.js';
15
+ import type { ContextBlock } from '../types/block.js';
16
+
17
+ describe('Token Estimator', () => {
18
+ describe('heuristicTokenCount', () => {
19
+ it('should estimate tokens using chars/4 with multiplier', () => {
20
+ const text = 'a'.repeat(100); // 100 chars
21
+ const tokens = heuristicTokenCount(text);
22
+
23
+ // Expected: (100 / 4) * 1.2 = 30
24
+ expect(tokens).toBe(30);
25
+ });
26
+
27
+ it('should round up fractional tokens', () => {
28
+ const text = 'a'.repeat(7); // 7 chars
29
+ const tokens = heuristicTokenCount(text);
30
+
31
+ // Expected: ceil((7 / 4) * 1.2) = ceil(2.1) = 3
32
+ expect(tokens).toBe(3);
33
+ });
34
+
35
+ it('should apply safety multiplier', () => {
36
+ const text = 'a'.repeat(40); // 40 chars
37
+
38
+ const tokens = heuristicTokenCount(text);
39
+
40
+ // Without multiplier: 40 / 4 = 10
41
+ // With multiplier: 10 * 1.2 = 12
42
+ expect(tokens).toBe(12);
43
+ });
44
+
45
+ it('should handle empty string', () => {
46
+ expect(heuristicTokenCount('')).toBe(0);
47
+ });
48
+
49
+ it('should handle long text', () => {
50
+ const text = 'a'.repeat(10000);
51
+ const tokens = heuristicTokenCount(text);
52
+
53
+ // Expected: (10000 / 4) * 1.2 = 3000
54
+ expect(tokens).toBe(3000);
55
+ });
56
+ });
57
+
58
+ describe('serializeBlockForEstimation', () => {
59
+ it('should serialize payload as JSON', () => {
60
+ const block: ContextBlock<any> = {
61
+ blockHash: 'test',
62
+ meta: {
63
+ kind: 'pinned',
64
+ sensitivity: 'public',
65
+ codecId: 'test',
66
+ codecVersion: '1.0.0',
67
+ createdAt: Date.now(),
68
+ },
69
+ payload: { text: 'Hello, world!' },
70
+ };
71
+
72
+ const serialized = serializeBlockForEstimation(block);
73
+
74
+ expect(serialized).toBe('{"text":"Hello, world!"}');
75
+ });
76
+
77
+ it('should handle complex payloads', () => {
78
+ const block: ContextBlock<any> = {
79
+ blockHash: 'test',
80
+ meta: {
81
+ kind: 'memory',
82
+ sensitivity: 'public',
83
+ codecId: 'test',
84
+ codecVersion: '1.0.0',
85
+ createdAt: Date.now(),
86
+ },
87
+ payload: {
88
+ nested: {
89
+ array: [1, 2, 3],
90
+ text: 'test',
91
+ },
92
+ },
93
+ };
94
+
95
+ const serialized = serializeBlockForEstimation(block);
96
+
97
+ expect(serialized).toContain('"nested"');
98
+ expect(serialized).toContain('"array"');
99
+ });
100
+ });
101
+
102
+ describe('OpenAITokenEstimator', () => {
103
+ it('should create estimator with model', () => {
104
+ const estimator = new OpenAITokenEstimator('gpt-4');
105
+
106
+ expect(estimator).toBeDefined();
107
+ });
108
+
109
+ it('should estimate block tokens with high confidence', async () => {
110
+ const estimator = new OpenAITokenEstimator('gpt-4');
111
+
112
+ const block: ContextBlock<any> = {
113
+ blockHash: 'test',
114
+ meta: {
115
+ kind: 'pinned',
116
+ sensitivity: 'public',
117
+ codecId: 'test',
118
+ codecVersion: '1.0.0',
119
+ createdAt: Date.now(),
120
+ },
121
+ payload: { text: 'Hello, world!' },
122
+ };
123
+
124
+ const estimate = await estimator.estimateBlock(block);
125
+
126
+ expect(estimate.tokens).toBeGreaterThan(0);
127
+ expect(estimate.confidence).toBe('high'); // tiktoken-based
128
+ });
129
+
130
+ it('should estimate multiple blocks', async () => {
131
+ const estimator = new OpenAITokenEstimator('gpt-4');
132
+
133
+ const blocks: ContextBlock<any>[] = [
134
+ {
135
+ blockHash: 'test1',
136
+ meta: {
137
+ kind: 'pinned',
138
+ sensitivity: 'public',
139
+ codecId: 'test',
140
+ codecVersion: '1.0.0',
141
+ createdAt: Date.now(),
142
+ },
143
+ payload: { text: 'Block 1' },
144
+ },
145
+ {
146
+ blockHash: 'test2',
147
+ meta: {
148
+ kind: 'memory',
149
+ sensitivity: 'public',
150
+ codecId: 'test',
151
+ codecVersion: '1.0.0',
152
+ createdAt: Date.now(),
153
+ },
154
+ payload: { text: 'Block 2' },
155
+ },
156
+ ];
157
+
158
+ const estimate = await estimator.estimate(blocks);
159
+
160
+ expect(estimate.tokens).toBeGreaterThan(0);
161
+ expect(estimate.confidence).toBe('high');
162
+ });
163
+
164
+ it('should return zero tokens for empty array', async () => {
165
+ const estimator = new OpenAITokenEstimator('gpt-4');
166
+
167
+ const estimate = await estimator.estimate([]);
168
+
169
+ expect(estimate.tokens).toBe(0);
170
+ expect(estimate.confidence).toBe('high');
171
+ });
172
+
173
+ it('should estimate different token counts for different content', async () => {
174
+ const estimator = new OpenAITokenEstimator('gpt-4');
175
+
176
+ const block1: ContextBlock<any> = {
177
+ blockHash: 'test1',
178
+ meta: {
179
+ kind: 'pinned',
180
+ sensitivity: 'public',
181
+ codecId: 'test',
182
+ codecVersion: '1.0.0',
183
+ createdAt: Date.now(),
184
+ },
185
+ payload: { text: 'Short' },
186
+ };
187
+
188
+ const block2: ContextBlock<any> = {
189
+ blockHash: 'test2',
190
+ meta: {
191
+ kind: 'pinned',
192
+ sensitivity: 'public',
193
+ codecId: 'test',
194
+ codecVersion: '1.0.0',
195
+ createdAt: Date.now(),
196
+ },
197
+ payload: { text: 'This is a much longer text with many more tokens' },
198
+ };
199
+
200
+ const estimate1 = await estimator.estimateBlock(block1);
201
+ const estimate2 = await estimator.estimateBlock(block2);
202
+
203
+ expect(estimate2.tokens).toBeGreaterThan(estimate1.tokens);
204
+ });
205
+ });
206
+
207
+ describe('GeminiTokenEstimator', () => {
208
+ it('should create estimator', () => {
209
+ const estimator = new GeminiTokenEstimator();
210
+
211
+ expect(estimator).toBeDefined();
212
+ });
213
+
214
+ it('should estimate block tokens with high confidence', async () => {
215
+ const estimator = new GeminiTokenEstimator();
216
+
217
+ const block: ContextBlock<any> = {
218
+ blockHash: 'test',
219
+ meta: {
220
+ kind: 'pinned',
221
+ sensitivity: 'public',
222
+ codecId: 'test',
223
+ codecVersion: '1.0.0',
224
+ createdAt: Date.now(),
225
+ },
226
+ payload: { text: 'Hello, Gemini!' },
227
+ };
228
+
229
+ const estimate = await estimator.estimateBlock(block);
230
+
231
+ expect(estimate.tokens).toBeGreaterThan(0);
232
+ expect(estimate.confidence).toBe('high'); // tiktoken-based
233
+ });
234
+
235
+ it('should estimate multiple blocks', async () => {
236
+ const estimator = new GeminiTokenEstimator();
237
+
238
+ const blocks: ContextBlock<any>[] = [
239
+ {
240
+ blockHash: 'test1',
241
+ meta: {
242
+ kind: 'pinned',
243
+ sensitivity: 'public',
244
+ codecId: 'test',
245
+ codecVersion: '1.0.0',
246
+ createdAt: Date.now(),
247
+ },
248
+ payload: { text: 'Block 1' },
249
+ },
250
+ {
251
+ blockHash: 'test2',
252
+ meta: {
253
+ kind: 'memory',
254
+ sensitivity: 'public',
255
+ codecId: 'test',
256
+ codecVersion: '1.0.0',
257
+ createdAt: Date.now(),
258
+ },
259
+ payload: { text: 'Block 2' },
260
+ },
261
+ ];
262
+
263
+ const estimate = await estimator.estimate(blocks);
264
+
265
+ expect(estimate.tokens).toBeGreaterThan(0);
266
+ expect(estimate.confidence).toBe('high');
267
+ });
268
+
269
+ it('should return zero tokens for empty array', async () => {
270
+ const estimator = new GeminiTokenEstimator();
271
+
272
+ const estimate = await estimator.estimate([]);
273
+
274
+ expect(estimate.tokens).toBe(0);
275
+ expect(estimate.confidence).toBe('high');
276
+ });
277
+ });
278
+
279
+ describe('Confidence levels', () => {
280
+ it('should use correct confidence for OpenAI tiktoken', async () => {
281
+ const estimator = new OpenAITokenEstimator('gpt-4');
282
+
283
+ const block: ContextBlock<any> = {
284
+ blockHash: 'test',
285
+ meta: {
286
+ kind: 'pinned',
287
+ sensitivity: 'public',
288
+ codecId: 'test',
289
+ codecVersion: '1.0.0',
290
+ createdAt: Date.now(),
291
+ },
292
+ payload: { text: 'Test' },
293
+ };
294
+
295
+ const estimate = await estimator.estimateBlock(block);
296
+
297
+ expect(estimate.confidence).toBe('high');
298
+ });
299
+
300
+ it('should use correct confidence for Gemini tiktoken', async () => {
301
+ const estimator = new GeminiTokenEstimator();
302
+
303
+ const block: ContextBlock<any> = {
304
+ blockHash: 'test',
305
+ meta: {
306
+ kind: 'pinned',
307
+ sensitivity: 'public',
308
+ codecId: 'test',
309
+ codecVersion: '1.0.0',
310
+ createdAt: Date.now(),
311
+ },
312
+ payload: { text: 'Test' },
313
+ };
314
+
315
+ const estimate = await estimator.estimateBlock(block);
316
+
317
+ expect(estimate.confidence).toBe('high');
318
+ });
319
+ });
320
+
321
+ describe('LOW_CONFIDENCE_MULTIPLIER', () => {
322
+ it('should be 1.2 for safety', () => {
323
+ expect(LOW_CONFIDENCE_MULTIPLIER).toBe(1.2);
324
+ });
325
+ });
326
+ });