@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,383 @@
1
+ /**
2
+ * Unit tests for ContextGraph.
3
+ *
4
+ * Tests block management, relationship tracking, queries, and view creation.
5
+ */
6
+
7
+ import { describe, it, expect } from 'vitest';
8
+ import { ContextGraph } from '../graph/context-graph.js';
9
+ import type { ContextBlock, BlockRef } from '../types/block.js';
10
+
11
+ describe('ContextGraph', () => {
12
+ describe('Block management', () => {
13
+ it('should add and retrieve blocks', () => {
14
+ const graph = new ContextGraph();
15
+
16
+ const block: ContextBlock<string> = createBlock('hash1', 'pinned', 'test');
17
+
18
+ graph.addBlock(block);
19
+
20
+ expect(graph.hasBlock('hash1')).toBe(true);
21
+ expect(graph.getBlock('hash1')).toEqual(block);
22
+ });
23
+
24
+ it('should be idempotent when adding same block', () => {
25
+ const graph = new ContextGraph();
26
+
27
+ const block = createBlock('hash1', 'pinned', 'test');
28
+
29
+ graph.addBlock(block);
30
+ graph.addBlock(block); // Add again
31
+
32
+ expect(graph.getBlockCount()).toBe(1);
33
+ });
34
+
35
+ it('should remove blocks', () => {
36
+ const graph = new ContextGraph();
37
+
38
+ const block = createBlock('hash1', 'pinned', 'test');
39
+ graph.addBlock(block);
40
+
41
+ const removed = graph.removeBlock('hash1');
42
+
43
+ expect(removed).toBe(true);
44
+ expect(graph.hasBlock('hash1')).toBe(false);
45
+ expect(graph.getBlockCount()).toBe(0);
46
+ });
47
+
48
+ it('should return false when removing non-existent block', () => {
49
+ const graph = new ContextGraph();
50
+
51
+ const removed = graph.removeBlock('non-existent');
52
+
53
+ expect(removed).toBe(false);
54
+ });
55
+
56
+ it('should count blocks correctly', () => {
57
+ const graph = new ContextGraph();
58
+
59
+ expect(graph.getBlockCount()).toBe(0);
60
+
61
+ graph.addBlock(createBlock('hash1', 'pinned', 'test1'));
62
+ expect(graph.getBlockCount()).toBe(1);
63
+
64
+ graph.addBlock(createBlock('hash2', 'memory', 'test2'));
65
+ expect(graph.getBlockCount()).toBe(2);
66
+
67
+ graph.removeBlock('hash1');
68
+ expect(graph.getBlockCount()).toBe(1);
69
+ });
70
+
71
+ it('should return undefined for non-existent block', () => {
72
+ const graph = new ContextGraph();
73
+
74
+ expect(graph.getBlock('non-existent')).toBeUndefined();
75
+ });
76
+
77
+ it('should list all block hashes', () => {
78
+ const graph = new ContextGraph();
79
+
80
+ graph.addBlock(createBlock('hash1', 'pinned', 'test1'));
81
+ graph.addBlock(createBlock('hash2', 'memory', 'test2'));
82
+ graph.addBlock(createBlock('hash3', 'history', 'test3'));
83
+
84
+ const blocks = graph.getAllBlocks();
85
+
86
+ expect(blocks).toHaveLength(3);
87
+ expect(blocks.map((b) => b.blockHash)).toContain('hash1');
88
+ expect(blocks.map((b) => b.blockHash)).toContain('hash2');
89
+ expect(blocks.map((b) => b.blockHash)).toContain('hash3');
90
+ });
91
+ });
92
+
93
+ describe('Derivation edges', () => {
94
+ it('should track derivation relationships', () => {
95
+ const graph = new ContextGraph();
96
+
97
+ const parent = createBlock('parent', 'pinned', 'parent');
98
+ const child = createBlock('child', 'memory', 'child');
99
+
100
+ graph.addBlock(parent);
101
+ graph.addBlock(child, [{ blockHash: 'parent' }]);
102
+
103
+ const derivedFrom = graph.getDerivedFrom('child');
104
+
105
+ expect(derivedFrom).toHaveLength(1);
106
+ expect(derivedFrom[0].blockHash).toBe('parent');
107
+ });
108
+
109
+ it('should track multiple parent blocks', () => {
110
+ const graph = new ContextGraph();
111
+
112
+ graph.addBlock(createBlock('parent1', 'pinned', 'p1'));
113
+ graph.addBlock(createBlock('parent2', 'pinned', 'p2'));
114
+ graph.addBlock(createBlock('child', 'memory', 'child'), [
115
+ { blockHash: 'parent1' },
116
+ { blockHash: 'parent2' },
117
+ ]);
118
+
119
+ const derivedFrom = graph.getDerivedFrom('child');
120
+
121
+ expect(derivedFrom).toHaveLength(2);
122
+ expect(derivedFrom.map((p) => p.blockHash)).toContain('parent1');
123
+ expect(derivedFrom.map((p) => p.blockHash)).toContain('parent2');
124
+ });
125
+
126
+ it('should return empty array for blocks with no parents', () => {
127
+ const graph = new ContextGraph();
128
+
129
+ graph.addBlock(createBlock('hash1', 'pinned', 'test'));
130
+
131
+ const derivedFrom = graph.getDerivedFrom('hash1');
132
+
133
+ expect(derivedFrom).toEqual([]);
134
+ });
135
+
136
+ it('should clean up derivation edges when removing block', () => {
137
+ const graph = new ContextGraph();
138
+
139
+ graph.addBlock(createBlock('parent', 'pinned', 'parent'));
140
+ graph.addBlock(createBlock('child', 'memory', 'child'), [
141
+ { blockHash: 'parent' },
142
+ ]);
143
+
144
+ graph.removeBlock('child');
145
+
146
+ expect(graph.getDerivedFrom('child')).toEqual([]);
147
+ });
148
+ });
149
+
150
+ describe('Reference edges', () => {
151
+ it('should track reference relationships', () => {
152
+ const graph = new ContextGraph();
153
+
154
+ graph.addBlock(createBlock('ref1', 'reference', 'ref1'));
155
+ graph.addBlock(
156
+ createBlock('block', 'memory', 'block'),
157
+ undefined,
158
+ ['ref1']
159
+ );
160
+
161
+ const references = graph.getReferences('block');
162
+
163
+ expect(references).toHaveLength(1);
164
+ expect(references).toContain('ref1');
165
+ });
166
+
167
+ it('should track multiple references', () => {
168
+ const graph = new ContextGraph();
169
+
170
+ graph.addBlock(createBlock('ref1', 'reference', 'r1'));
171
+ graph.addBlock(createBlock('ref2', 'reference', 'r2'));
172
+ graph.addBlock(
173
+ createBlock('block', 'memory', 'block'),
174
+ undefined,
175
+ ['ref1', 'ref2']
176
+ );
177
+
178
+ const references = graph.getReferences('block');
179
+
180
+ expect(references).toHaveLength(2);
181
+ expect(references).toContain('ref1');
182
+ expect(references).toContain('ref2');
183
+ });
184
+
185
+ it('should return empty array for blocks with no references', () => {
186
+ const graph = new ContextGraph();
187
+
188
+ graph.addBlock(createBlock('hash1', 'pinned', 'test'));
189
+
190
+ const references = graph.getReferences('hash1');
191
+
192
+ expect(references).toEqual([]);
193
+ });
194
+
195
+ it('should clean up reference edges when removing block', () => {
196
+ const graph = new ContextGraph();
197
+
198
+ graph.addBlock(createBlock('ref', 'reference', 'ref'));
199
+ graph.addBlock(
200
+ createBlock('block', 'memory', 'block'),
201
+ undefined,
202
+ ['ref']
203
+ );
204
+
205
+ graph.removeBlock('block');
206
+
207
+ expect(graph.getReferences('block')).toEqual([]);
208
+ });
209
+ });
210
+
211
+ describe('Block selection with queries', () => {
212
+ it('should select all blocks with empty query', () => {
213
+ const graph = new ContextGraph();
214
+
215
+ graph.addBlock(createBlock('hash1', 'pinned', 'test1'));
216
+ graph.addBlock(createBlock('hash2', 'memory', 'test2'));
217
+
218
+ const selected = graph.select({});
219
+
220
+ expect(selected).toHaveLength(2);
221
+ });
222
+
223
+ it('should filter by kind', () => {
224
+ const graph = new ContextGraph();
225
+
226
+ graph.addBlock(createBlock('hash1', 'pinned', 'test1'));
227
+ graph.addBlock(createBlock('hash2', 'memory', 'test2'));
228
+ graph.addBlock(createBlock('hash3', 'history', 'test3'));
229
+
230
+ const selected = graph.select({ kinds: ['pinned', 'memory'] });
231
+
232
+ expect(selected).toHaveLength(2);
233
+ expect(selected.map((b) => b.blockHash)).toContain('hash1');
234
+ expect(selected.map((b) => b.blockHash)).toContain('hash2');
235
+ });
236
+
237
+ it('should filter by tags', () => {
238
+ const graph = new ContextGraph();
239
+
240
+ const block1 = createBlock('hash1', 'pinned', 'test1');
241
+ block1.meta.tags = ['important'];
242
+
243
+ const block2 = createBlock('hash2', 'memory', 'test2');
244
+ block2.meta.tags = ['memory'];
245
+
246
+ graph.addBlock(block1);
247
+ graph.addBlock(block2);
248
+
249
+ const selected = graph.select({ tags: ['important'] });
250
+
251
+ expect(selected).toHaveLength(1);
252
+ expect(selected[0].blockHash).toBe('hash1');
253
+ });
254
+
255
+ it('should filter by sensitivity', () => {
256
+ const graph = new ContextGraph();
257
+
258
+ const block1 = createBlock('hash1', 'pinned', 'test1');
259
+ block1.meta.sensitivity = 'public';
260
+
261
+ const block2 = createBlock('hash2', 'memory', 'test2');
262
+ block2.meta.sensitivity = 'internal';
263
+
264
+ graph.addBlock(block1);
265
+ graph.addBlock(block2);
266
+
267
+ const selected = graph.select({ maxSensitivity: 'public' });
268
+
269
+ expect(selected).toHaveLength(1);
270
+ expect(selected[0].blockHash).toBe('hash1');
271
+ });
272
+
273
+ it('should filter by excludeHashes', () => {
274
+ const graph = new ContextGraph();
275
+
276
+ graph.addBlock(createBlock('hash1', 'pinned', 'test1'));
277
+ graph.addBlock(createBlock('hash2', 'memory', 'test2'));
278
+ graph.addBlock(createBlock('hash3', 'history', 'test3'));
279
+
280
+ const selected = graph.select({ excludeHashes: ['hash2'] });
281
+
282
+ expect(selected).toHaveLength(2);
283
+ expect(selected.map((b) => b.blockHash)).not.toContain('hash2');
284
+ });
285
+ });
286
+
287
+ describe('View creation', () => {
288
+ it('should create view with deterministic ordering', async () => {
289
+ const graph = new ContextGraph();
290
+
291
+ // Add blocks in random order
292
+ graph.addBlock(createBlock('hash-history', 'history', 'history'));
293
+ graph.addBlock(createBlock('hash-pinned', 'pinned', 'pinned'));
294
+ graph.addBlock(createBlock('hash-memory', 'memory', 'memory'));
295
+
296
+ const view = await graph.createView({});
297
+
298
+ // Should be ordered by KIND_ORDER
299
+ expect(view.blocks).toHaveLength(3);
300
+ expect(view.blocks[0].meta.kind).toBe('pinned');
301
+ expect(view.blocks[1].meta.kind).toBe('memory');
302
+ expect(view.blocks[2].meta.kind).toBe('history');
303
+ });
304
+
305
+ it('should compute stable prefix hash', async () => {
306
+ const graph = new ContextGraph();
307
+
308
+ graph.addBlock(createBlock('hash1', 'pinned', 'test1'));
309
+ graph.addBlock(createBlock('hash2', 'memory', 'test2'));
310
+
311
+ const view = await graph.createView({});
312
+
313
+ expect(view.stablePrefixHash).toBeDefined();
314
+ expect(view.stablePrefixHash.length).toBeGreaterThan(0);
315
+ });
316
+
317
+ it('should apply query filter in view', async () => {
318
+ const graph = new ContextGraph();
319
+
320
+ graph.addBlock(createBlock('hash1', 'pinned', 'test1'));
321
+ graph.addBlock(createBlock('hash2', 'memory', 'test2'));
322
+ graph.addBlock(createBlock('hash3', 'history', 'test3'));
323
+
324
+ const view = await graph.createView({ query: { kinds: ['pinned'] } });
325
+
326
+ expect(view.blocks).toHaveLength(1);
327
+ expect(view.blocks[0].meta.kind).toBe('pinned');
328
+ });
329
+
330
+ it('should create empty view for empty graph', async () => {
331
+ const graph = new ContextGraph();
332
+
333
+ const view = await graph.createView({});
334
+
335
+ expect(view.blocks).toEqual([]);
336
+ expect(view.stablePrefixHash).toBeDefined();
337
+ });
338
+ });
339
+
340
+ describe('Edge cases', () => {
341
+ it('should handle large number of blocks', () => {
342
+ const graph = new ContextGraph();
343
+
344
+ // Add 1000 blocks
345
+ for (let i = 0; i < 1000; i++) {
346
+ graph.addBlock(createBlock(`hash${i}`, 'memory', `block${i}`));
347
+ }
348
+
349
+ expect(graph.getBlockCount()).toBe(1000);
350
+ });
351
+
352
+ it('should handle blocks with same content but different hashes', () => {
353
+ const graph = new ContextGraph();
354
+
355
+ const block1 = createBlock('hash1', 'pinned', 'same content');
356
+ const block2 = createBlock('hash2', 'pinned', 'same content');
357
+
358
+ graph.addBlock(block1);
359
+ graph.addBlock(block2);
360
+
361
+ expect(graph.getBlockCount()).toBe(2);
362
+ });
363
+ });
364
+ });
365
+
366
+ // Helper to create test blocks
367
+ function createBlock(
368
+ hash: string,
369
+ kind: 'pinned' | 'reference' | 'memory' | 'state' | 'tool_output' | 'history' | 'turn',
370
+ payload: string
371
+ ): ContextBlock<string> {
372
+ return {
373
+ blockHash: hash,
374
+ meta: {
375
+ kind,
376
+ sensitivity: 'public',
377
+ codecId: 'test',
378
+ codecVersion: '1.0.0',
379
+ createdAt: Date.now(),
380
+ },
381
+ payload,
382
+ };
383
+ }
@@ -0,0 +1,274 @@
1
+ /**
2
+ * Unit tests for block hashing utilities.
3
+ *
4
+ * Tests computeBlockHash behavior, stable metadata extraction, and hash consistency.
5
+ */
6
+
7
+ import { describe, it, expect } from 'vitest';
8
+ import { computeBlockHash, extractStableMetaSubset } from '../types/hash.js';
9
+ import type { BlockMeta, StableMetaSubset } from '../types/block.js';
10
+ import { SystemRulesCodec } from '../codecs/system-rules.codec.js';
11
+
12
+ describe('Block Hashing', () => {
13
+ describe('extractStableMetaSubset', () => {
14
+ it('should extract stable metadata fields', () => {
15
+ const fullMeta: BlockMeta = {
16
+ kind: 'pinned',
17
+ sensitivity: 'public',
18
+ codecId: 'system-rules',
19
+ codecVersion: '1.0.0',
20
+ createdAt: Date.now(),
21
+ source: 'test-source',
22
+ tags: ['tag1', 'tag2'],
23
+ };
24
+
25
+ const stable = extractStableMetaSubset(fullMeta);
26
+
27
+ expect(stable).toEqual({
28
+ kind: 'pinned',
29
+ sensitivity: 'public',
30
+ codecId: 'system-rules',
31
+ codecVersion: '1.0.0',
32
+ });
33
+ });
34
+
35
+ it('should exclude volatile fields', () => {
36
+ const fullMeta: BlockMeta = {
37
+ kind: 'memory',
38
+ sensitivity: 'internal',
39
+ codecId: 'test',
40
+ codecVersion: '1.0.0',
41
+ createdAt: 1234567890,
42
+ source: 'volatile-source',
43
+ tags: ['volatile-tag'],
44
+ };
45
+
46
+ const stable = extractStableMetaSubset(fullMeta);
47
+
48
+ expect(stable).not.toHaveProperty('createdAt');
49
+ expect(stable).not.toHaveProperty('source');
50
+ expect(stable).not.toHaveProperty('tags');
51
+ });
52
+ });
53
+
54
+ describe('computeBlockHash', () => {
55
+ it('should produce consistent hash for same input', () => {
56
+ const meta: StableMetaSubset = {
57
+ kind: 'pinned',
58
+ sensitivity: 'public',
59
+ codecId: 'test',
60
+ codecVersion: '1.0.0',
61
+ };
62
+
63
+ const payload = { test: 'data' };
64
+
65
+ const hash1 = computeBlockHash(meta, payload);
66
+ const hash2 = computeBlockHash(meta, payload);
67
+
68
+ expect(hash1).toBe(hash2);
69
+ expect(hash1).toHaveLength(64); // SHA-256 hex = 64 chars
70
+ });
71
+
72
+ it('should produce different hash for different content', () => {
73
+ const meta: StableMetaSubset = {
74
+ kind: 'pinned',
75
+ sensitivity: 'public',
76
+ codecId: 'test',
77
+ codecVersion: '1.0.0',
78
+ };
79
+
80
+ const hash1 = computeBlockHash(meta, { test: 'data1' });
81
+ const hash2 = computeBlockHash(meta, { test: 'data2' });
82
+
83
+ expect(hash1).not.toBe(hash2);
84
+ });
85
+
86
+ it('should produce different hash for different metadata', () => {
87
+ const meta1: StableMetaSubset = {
88
+ kind: 'pinned',
89
+ sensitivity: 'public',
90
+ codecId: 'test',
91
+ codecVersion: '1.0.0',
92
+ };
93
+
94
+ const meta2: StableMetaSubset = {
95
+ kind: 'memory',
96
+ sensitivity: 'public',
97
+ codecId: 'test',
98
+ codecVersion: '1.0.0',
99
+ };
100
+
101
+ const payload = { test: 'data' };
102
+
103
+ const hash1 = computeBlockHash(meta1, payload);
104
+ const hash2 = computeBlockHash(meta2, payload);
105
+
106
+ expect(hash1).not.toBe(hash2);
107
+ });
108
+
109
+ it('should ignore volatile fields in full BlockMeta', () => {
110
+ const meta1: BlockMeta = {
111
+ kind: 'pinned',
112
+ sensitivity: 'public',
113
+ codecId: 'test',
114
+ codecVersion: '1.0.0',
115
+ createdAt: 1000,
116
+ tags: ['tag1'],
117
+ };
118
+
119
+ const meta2: BlockMeta = {
120
+ kind: 'pinned',
121
+ sensitivity: 'public',
122
+ codecId: 'test',
123
+ codecVersion: '1.0.0',
124
+ createdAt: 2000, // different timestamp
125
+ tags: ['tag2'], // different tags
126
+ };
127
+
128
+ const payload = { test: 'data' };
129
+
130
+ const hash1 = computeBlockHash(meta1, payload);
131
+ const hash2 = computeBlockHash(meta2, payload);
132
+
133
+ // Should be equal because volatile fields are excluded
134
+ expect(hash1).toBe(hash2);
135
+ });
136
+
137
+ it('should use codec canonicalize if provided', () => {
138
+ const meta: StableMetaSubset = {
139
+ kind: 'pinned',
140
+ sensitivity: 'public',
141
+ codecId: 'system-rules',
142
+ codecVersion: '1.0.0',
143
+ };
144
+
145
+ // Different payload representations that canonicalize to same value
146
+ const payload1 = { text: ' Hello ', priority: 1 };
147
+ const payload2 = { text: 'Hello', priority: 1 };
148
+
149
+ const hash1 = computeBlockHash(meta, payload1, SystemRulesCodec as any);
150
+ const hash2 = computeBlockHash(meta, payload2, SystemRulesCodec as any);
151
+
152
+ // Should be equal after canonicalization
153
+ expect(hash1).toBe(hash2);
154
+ });
155
+
156
+ it('should produce different hash without codec canonicalization', () => {
157
+ const meta: StableMetaSubset = {
158
+ kind: 'pinned',
159
+ sensitivity: 'public',
160
+ codecId: 'test',
161
+ codecVersion: '1.0.0',
162
+ };
163
+
164
+ const payload1 = { text: ' Hello ' };
165
+ const payload2 = { text: 'Hello' };
166
+
167
+ const hash1 = computeBlockHash(meta, payload1); // no codec
168
+ const hash2 = computeBlockHash(meta, payload2); // no codec
169
+
170
+ // Should be different without canonicalization
171
+ expect(hash1).not.toBe(hash2);
172
+ });
173
+
174
+ it('should be deterministic across multiple runs', () => {
175
+ const meta: StableMetaSubset = {
176
+ kind: 'memory',
177
+ sensitivity: 'internal',
178
+ codecId: 'test',
179
+ codecVersion: '2.0.0',
180
+ };
181
+
182
+ const payload = {
183
+ nested: {
184
+ data: [1, 2, 3],
185
+ text: 'test',
186
+ },
187
+ };
188
+
189
+ const hashes = new Set<string>();
190
+ for (let i = 0; i < 10; i++) {
191
+ hashes.add(computeBlockHash(meta, payload));
192
+ }
193
+
194
+ // All hashes should be identical
195
+ expect(hashes.size).toBe(1);
196
+ });
197
+
198
+ it('should handle complex nested payloads', () => {
199
+ const meta: StableMetaSubset = {
200
+ kind: 'state',
201
+ sensitivity: 'public',
202
+ codecId: 'test',
203
+ codecVersion: '1.0.0',
204
+ };
205
+
206
+ const complexPayload = {
207
+ array: [1, 2, { nested: true }],
208
+ object: {
209
+ deep: {
210
+ nesting: {
211
+ level: 4,
212
+ },
213
+ },
214
+ },
215
+ string: 'test',
216
+ number: 42,
217
+ boolean: true,
218
+ null: null,
219
+ };
220
+
221
+ const hash = computeBlockHash(meta, complexPayload);
222
+
223
+ expect(hash).toBeDefined();
224
+ expect(hash).toHaveLength(64);
225
+ });
226
+
227
+ it('should produce same hash for equivalent object key orders', () => {
228
+ const meta: StableMetaSubset = {
229
+ kind: 'pinned',
230
+ sensitivity: 'public',
231
+ codecId: 'test',
232
+ codecVersion: '1.0.0',
233
+ };
234
+
235
+ // Note: computeBlockHash uses JSON.stringify which does NOT guarantee
236
+ // key order consistency. For true determinism, codecs must canonicalize.
237
+ const payload1 = { a: 1, b: 2 };
238
+ const payload2 = { a: 1, b: 2 };
239
+
240
+ const hash1 = computeBlockHash(meta, payload1);
241
+ const hash2 = computeBlockHash(meta, payload2);
242
+
243
+ expect(hash1).toBe(hash2);
244
+ });
245
+ });
246
+
247
+ describe('Hash collision resistance', () => {
248
+ it('should produce unique hashes for different payloads', () => {
249
+ const meta: StableMetaSubset = {
250
+ kind: 'pinned',
251
+ sensitivity: 'public',
252
+ codecId: 'test',
253
+ codecVersion: '1.0.0',
254
+ };
255
+
256
+ const payloads = [
257
+ { value: 1 },
258
+ { value: 2 },
259
+ { value: 'string' },
260
+ { nested: { value: 1 } },
261
+ { array: [1, 2, 3] },
262
+ { array: [3, 2, 1] },
263
+ ];
264
+
265
+ const hashes = new Set<string>();
266
+ for (const payload of payloads) {
267
+ hashes.add(computeBlockHash(meta, payload));
268
+ }
269
+
270
+ // All hashes should be unique
271
+ expect(hashes.size).toBe(payloads.length);
272
+ });
273
+ });
274
+ });