anvil-dev-framework 0.1.6

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 (190) hide show
  1. package/README.md +719 -0
  2. package/VERSION +1 -0
  3. package/docs/ANVIL-REPO-IMPLEMENTATION-PLAN.md +441 -0
  4. package/docs/FIRST-SKILL-TUTORIAL.md +408 -0
  5. package/docs/INSTALLATION-RETRO-NOTES.md +458 -0
  6. package/docs/INSTALLATION.md +984 -0
  7. package/docs/anvil-hud.md +469 -0
  8. package/docs/anvil-init.md +255 -0
  9. package/docs/anvil-state.md +210 -0
  10. package/docs/boris-cherny-ralph-wiggum-insights.md +608 -0
  11. package/docs/command-reference.md +2022 -0
  12. package/docs/hooks-tts.md +368 -0
  13. package/docs/implementation-guide.md +810 -0
  14. package/docs/linear-github-integration.md +247 -0
  15. package/docs/local-issues.md +677 -0
  16. package/docs/patterns/README.md +419 -0
  17. package/docs/planning-responsibilities.md +139 -0
  18. package/docs/session-workflow.md +573 -0
  19. package/docs/simplification-plan-template.md +297 -0
  20. package/docs/simplification-principles.md +129 -0
  21. package/docs/specifications/CCS-RALPH-INTEGRATION-DESIGN.md +633 -0
  22. package/docs/specifications/CCS-RESEARCH-REPORT.md +169 -0
  23. package/docs/specifications/PLAN-ANV-verification-ralph-wiggum.md +403 -0
  24. package/docs/specifications/PLAN-parallel-tracks-anvil-memory-ccs.md +494 -0
  25. package/docs/specifications/SPEC-ANV-VRW/component-01-verify.md +208 -0
  26. package/docs/specifications/SPEC-ANV-VRW/component-02-stop-gate.md +226 -0
  27. package/docs/specifications/SPEC-ANV-VRW/component-03-posttooluse.md +209 -0
  28. package/docs/specifications/SPEC-ANV-VRW/component-04-ralph-wiggum.md +604 -0
  29. package/docs/specifications/SPEC-ANV-VRW/component-05-atomic-actions.md +311 -0
  30. package/docs/specifications/SPEC-ANV-VRW/component-06-verify-subagent.md +264 -0
  31. package/docs/specifications/SPEC-ANV-VRW/component-07-claude-md.md +363 -0
  32. package/docs/specifications/SPEC-ANV-VRW/index.md +182 -0
  33. package/docs/specifications/SPEC-ANV-anvil-memory.md +573 -0
  34. package/docs/specifications/SPEC-ANV-context-checkpoints.md +781 -0
  35. package/docs/specifications/SPEC-ANV-verification-ralph-wiggum.md +789 -0
  36. package/docs/sync.md +122 -0
  37. package/global/CLAUDE.md +140 -0
  38. package/global/agents/verify-app.md +164 -0
  39. package/global/commands/anvil-settings.md +527 -0
  40. package/global/commands/anvil-sync.md +121 -0
  41. package/global/commands/change.md +197 -0
  42. package/global/commands/clarify.md +252 -0
  43. package/global/commands/cleanup.md +292 -0
  44. package/global/commands/commit-push-pr.md +207 -0
  45. package/global/commands/decay-review.md +127 -0
  46. package/global/commands/discover.md +158 -0
  47. package/global/commands/doc-coverage.md +122 -0
  48. package/global/commands/evidence.md +307 -0
  49. package/global/commands/explore.md +121 -0
  50. package/global/commands/force-exit.md +135 -0
  51. package/global/commands/handoff.md +191 -0
  52. package/global/commands/healthcheck.md +302 -0
  53. package/global/commands/hud.md +84 -0
  54. package/global/commands/insights.md +319 -0
  55. package/global/commands/linear-setup.md +184 -0
  56. package/global/commands/lint-fix.md +198 -0
  57. package/global/commands/orient.md +510 -0
  58. package/global/commands/plan.md +228 -0
  59. package/global/commands/ralph.md +346 -0
  60. package/global/commands/ready.md +182 -0
  61. package/global/commands/release.md +305 -0
  62. package/global/commands/retro.md +96 -0
  63. package/global/commands/shard.md +166 -0
  64. package/global/commands/spec.md +227 -0
  65. package/global/commands/sprint.md +184 -0
  66. package/global/commands/tasks.md +228 -0
  67. package/global/commands/test-and-commit.md +151 -0
  68. package/global/commands/validate.md +132 -0
  69. package/global/commands/verify.md +251 -0
  70. package/global/commands/weekly-review.md +156 -0
  71. package/global/hooks/__pycache__/ralph_context_monitor.cpython-314.pyc +0 -0
  72. package/global/hooks/__pycache__/statusline_agent_sync.cpython-314.pyc +0 -0
  73. package/global/hooks/anvil_memory_observe.ts +322 -0
  74. package/global/hooks/anvil_memory_session.ts +166 -0
  75. package/global/hooks/anvil_memory_stop.ts +187 -0
  76. package/global/hooks/parse_transcript.py +116 -0
  77. package/global/hooks/post_merge_cleanup.sh +132 -0
  78. package/global/hooks/post_tool_format.sh +215 -0
  79. package/global/hooks/ralph_context_monitor.py +240 -0
  80. package/global/hooks/ralph_stop.sh +502 -0
  81. package/global/hooks/statusline.sh +1110 -0
  82. package/global/hooks/statusline_agent_sync.py +224 -0
  83. package/global/hooks/stop_gate.sh +250 -0
  84. package/global/lib/.claude/anvil-state.json +21 -0
  85. package/global/lib/__pycache__/agent_registry.cpython-314.pyc +0 -0
  86. package/global/lib/__pycache__/claim_service.cpython-314.pyc +0 -0
  87. package/global/lib/__pycache__/coderabbit_service.cpython-314.pyc +0 -0
  88. package/global/lib/__pycache__/config_service.cpython-314.pyc +0 -0
  89. package/global/lib/__pycache__/coordination_service.cpython-314.pyc +0 -0
  90. package/global/lib/__pycache__/doc_coverage_service.cpython-314.pyc +0 -0
  91. package/global/lib/__pycache__/gate_logger.cpython-314.pyc +0 -0
  92. package/global/lib/__pycache__/github_service.cpython-314.pyc +0 -0
  93. package/global/lib/__pycache__/hygiene_service.cpython-314.pyc +0 -0
  94. package/global/lib/__pycache__/issue_models.cpython-314.pyc +0 -0
  95. package/global/lib/__pycache__/issue_provider.cpython-314.pyc +0 -0
  96. package/global/lib/__pycache__/linear_data_service.cpython-314.pyc +0 -0
  97. package/global/lib/__pycache__/linear_provider.cpython-314.pyc +0 -0
  98. package/global/lib/__pycache__/local_provider.cpython-314.pyc +0 -0
  99. package/global/lib/__pycache__/quality_service.cpython-314.pyc +0 -0
  100. package/global/lib/__pycache__/ralph_state.cpython-314.pyc +0 -0
  101. package/global/lib/__pycache__/state_manager.cpython-314.pyc +0 -0
  102. package/global/lib/__pycache__/transcript_parser.cpython-314.pyc +0 -0
  103. package/global/lib/__pycache__/verification_runner.cpython-314.pyc +0 -0
  104. package/global/lib/__pycache__/verify_iteration.cpython-314.pyc +0 -0
  105. package/global/lib/__pycache__/verify_subagent.cpython-314.pyc +0 -0
  106. package/global/lib/agent_registry.py +995 -0
  107. package/global/lib/anvil-state.sh +435 -0
  108. package/global/lib/claim_service.py +515 -0
  109. package/global/lib/coderabbit_service.py +314 -0
  110. package/global/lib/config_service.py +423 -0
  111. package/global/lib/coordination_service.py +331 -0
  112. package/global/lib/doc_coverage_service.py +1305 -0
  113. package/global/lib/gate_logger.py +316 -0
  114. package/global/lib/github_service.py +310 -0
  115. package/global/lib/handoff_generator.py +775 -0
  116. package/global/lib/hygiene_service.py +712 -0
  117. package/global/lib/issue_models.py +257 -0
  118. package/global/lib/issue_provider.py +339 -0
  119. package/global/lib/linear_data_service.py +210 -0
  120. package/global/lib/linear_provider.py +987 -0
  121. package/global/lib/linear_provider.py.backup +671 -0
  122. package/global/lib/local_provider.py +486 -0
  123. package/global/lib/orient_fast.py +457 -0
  124. package/global/lib/quality_service.py +470 -0
  125. package/global/lib/ralph_prompt_generator.py +563 -0
  126. package/global/lib/ralph_state.py +1202 -0
  127. package/global/lib/state_manager.py +417 -0
  128. package/global/lib/transcript_parser.py +597 -0
  129. package/global/lib/verification_runner.py +557 -0
  130. package/global/lib/verify_iteration.py +490 -0
  131. package/global/lib/verify_subagent.py +250 -0
  132. package/global/skills/README.md +155 -0
  133. package/global/skills/quality-gates/SKILL.md +252 -0
  134. package/global/skills/skill-template/SKILL.md +109 -0
  135. package/global/skills/testing-strategies/SKILL.md +337 -0
  136. package/global/templates/CHANGE-template.md +105 -0
  137. package/global/templates/HANDOFF-template.md +63 -0
  138. package/global/templates/PLAN-template.md +111 -0
  139. package/global/templates/SPEC-template.md +93 -0
  140. package/global/templates/ralph/PROMPT.md.template +89 -0
  141. package/global/templates/ralph/fix_plan.md.template +31 -0
  142. package/global/templates/ralph/progress.txt.template +23 -0
  143. package/global/tests/__pycache__/test_doc_coverage.cpython-314.pyc +0 -0
  144. package/global/tests/test_doc_coverage.py +520 -0
  145. package/global/tests/test_issue_models.py +299 -0
  146. package/global/tests/test_local_provider.py +323 -0
  147. package/global/tools/README.md +178 -0
  148. package/global/tools/__pycache__/anvil-hud.cpython-314.pyc +0 -0
  149. package/global/tools/anvil-hud.py +3622 -0
  150. package/global/tools/anvil-hud.py.bak +3318 -0
  151. package/global/tools/anvil-issue.py +432 -0
  152. package/global/tools/anvil-memory/CLAUDE.md +49 -0
  153. package/global/tools/anvil-memory/README.md +42 -0
  154. package/global/tools/anvil-memory/bun.lock +25 -0
  155. package/global/tools/anvil-memory/bunfig.toml +9 -0
  156. package/global/tools/anvil-memory/package.json +23 -0
  157. package/global/tools/anvil-memory/src/__tests__/ccs/context-monitor.test.ts +535 -0
  158. package/global/tools/anvil-memory/src/__tests__/ccs/edge-cases.test.ts +645 -0
  159. package/global/tools/anvil-memory/src/__tests__/ccs/fixtures.ts +363 -0
  160. package/global/tools/anvil-memory/src/__tests__/ccs/index.ts +8 -0
  161. package/global/tools/anvil-memory/src/__tests__/ccs/integration.test.ts +417 -0
  162. package/global/tools/anvil-memory/src/__tests__/ccs/prompt-generator.test.ts +571 -0
  163. package/global/tools/anvil-memory/src/__tests__/ccs/ralph-stop.test.ts +440 -0
  164. package/global/tools/anvil-memory/src/__tests__/ccs/test-utils.ts +252 -0
  165. package/global/tools/anvil-memory/src/__tests__/commands.test.ts +657 -0
  166. package/global/tools/anvil-memory/src/__tests__/db.test.ts +641 -0
  167. package/global/tools/anvil-memory/src/__tests__/hooks.test.ts +272 -0
  168. package/global/tools/anvil-memory/src/__tests__/performance.test.ts +427 -0
  169. package/global/tools/anvil-memory/src/__tests__/test-utils.ts +113 -0
  170. package/global/tools/anvil-memory/src/commands/checkpoint.ts +197 -0
  171. package/global/tools/anvil-memory/src/commands/get.ts +115 -0
  172. package/global/tools/anvil-memory/src/commands/init.ts +94 -0
  173. package/global/tools/anvil-memory/src/commands/observe.ts +163 -0
  174. package/global/tools/anvil-memory/src/commands/search.ts +112 -0
  175. package/global/tools/anvil-memory/src/db.ts +638 -0
  176. package/global/tools/anvil-memory/src/index.ts +205 -0
  177. package/global/tools/anvil-memory/src/types.ts +122 -0
  178. package/global/tools/anvil-memory/tsconfig.json +29 -0
  179. package/global/tools/ralph-loop.sh +359 -0
  180. package/package.json +45 -0
  181. package/scripts/anvil +822 -0
  182. package/scripts/extract_patterns.py +222 -0
  183. package/scripts/init-project.sh +541 -0
  184. package/scripts/install.sh +229 -0
  185. package/scripts/postinstall.js +41 -0
  186. package/scripts/rollback.sh +188 -0
  187. package/scripts/sync.sh +623 -0
  188. package/scripts/test-statusline.sh +248 -0
  189. package/scripts/update_claude_md.py +224 -0
  190. package/scripts/verify.sh +255 -0
@@ -0,0 +1,641 @@
1
+ /**
2
+ * Unit tests for Anvil Memory Database
3
+ *
4
+ * Tests all CRUD operations, FTS5 search, triggers, and integrity.
5
+ */
6
+
7
+ import { describe, test, expect, beforeAll, afterAll, beforeEach, afterEach } from 'bun:test';
8
+ import { AnvilMemoryDb, getDefaultDbPath, dbExists } from '../db';
9
+ import { existsSync } from 'fs';
10
+ import type { Observation, Session, Checkpoint, RalphIteration, Prompt } from '../types';
11
+ import { cleanupDb, getTestDbPath, ensureTestDir } from './test-utils';
12
+
13
+ describe('AnvilMemoryDb - Initialization', () => {
14
+ let testDbPath: string;
15
+
16
+ beforeAll(() => {
17
+ ensureTestDir();
18
+ });
19
+
20
+ beforeEach(() => {
21
+ testDbPath = getTestDbPath('db-init');
22
+ });
23
+
24
+ afterEach(() => {
25
+ cleanupDb(testDbPath);
26
+ });
27
+
28
+ test('creates new database on init', () => {
29
+ const db = new AnvilMemoryDb(testDbPath);
30
+ const result = db.init();
31
+
32
+ expect(result.created).toBe(true);
33
+ expect(result.version).toBe(1);
34
+ expect(existsSync(testDbPath)).toBe(true);
35
+
36
+ db.close();
37
+ });
38
+
39
+ test('returns existing version on re-init', () => {
40
+ const db = new AnvilMemoryDb(testDbPath);
41
+ db.init();
42
+
43
+ const result = db.init();
44
+ expect(result.created).toBe(false);
45
+ expect(result.version).toBe(1);
46
+
47
+ db.close();
48
+ });
49
+
50
+ test('getSchemaVersion returns 0 for new database', () => {
51
+ const db = new AnvilMemoryDb(testDbPath);
52
+ expect(db.getSchemaVersion()).toBe(0);
53
+ db.close();
54
+ });
55
+
56
+ test('getSchemaVersion returns version after init', () => {
57
+ const db = new AnvilMemoryDb(testDbPath);
58
+ db.init();
59
+ expect(db.getSchemaVersion()).toBe(1);
60
+ db.close();
61
+ });
62
+
63
+ test('getConfig returns correct info', () => {
64
+ const db = new AnvilMemoryDb(testDbPath);
65
+ db.init();
66
+
67
+ const config = db.getConfig();
68
+ expect(config.path).toBe(testDbPath);
69
+ expect(config.migrated).toBe(true);
70
+ expect(config.version).toBe(1);
71
+
72
+ db.close();
73
+ });
74
+
75
+ test('integrityCheck passes on fresh database', () => {
76
+ const db = new AnvilMemoryDb(testDbPath);
77
+ db.init();
78
+
79
+ const result = db.integrityCheck();
80
+ expect(result.ok).toBe(true);
81
+ expect(result.errors).toEqual([]);
82
+
83
+ db.close();
84
+ });
85
+ });
86
+
87
+ describe('AnvilMemoryDb - Observations CRUD', () => {
88
+ let db: AnvilMemoryDb;
89
+ let testDbPath: string;
90
+
91
+ beforeAll(() => {
92
+ ensureTestDir();
93
+ testDbPath = getTestDbPath('db-obs');
94
+ db = new AnvilMemoryDb(testDbPath);
95
+ db.init();
96
+ });
97
+
98
+ afterAll(() => {
99
+ db.close();
100
+ cleanupDb(testDbPath);
101
+ });
102
+
103
+ test('createObservation returns observation with id', () => {
104
+ const obs = db.createObservation({
105
+ timestamp: new Date().toISOString(),
106
+ type: 'discovery',
107
+ title: 'Test discovery',
108
+ content: 'Testing observation creation',
109
+ });
110
+
111
+ expect(obs.id).toBeDefined();
112
+ expect(obs.id).toBeGreaterThan(0);
113
+ expect(obs.type).toBe('discovery');
114
+ expect(obs.title).toBe('Test discovery');
115
+ });
116
+
117
+ test('createObservation handles all observation types', () => {
118
+ const types: Observation['type'][] = [
119
+ 'bugfix', 'feature', 'refactor', 'discovery', 'decision',
120
+ 'change', 'checkpoint', 'ralph_iteration', 'handoff',
121
+ 'shard', 'linear_sync', 'session_request',
122
+ ];
123
+
124
+ for (const type of types) {
125
+ const obs = db.createObservation({
126
+ timestamp: new Date().toISOString(),
127
+ type,
128
+ title: `Test ${type}`,
129
+ content: `Content for ${type}`,
130
+ });
131
+ expect(obs.type).toBe(type);
132
+ }
133
+ });
134
+
135
+ test('createObservation stores files as JSON array', () => {
136
+ const obs = db.createObservation({
137
+ timestamp: new Date().toISOString(),
138
+ type: 'feature',
139
+ title: 'Test with files',
140
+ content: 'Has files',
141
+ files: ['src/file1.ts', 'src/file2.ts'],
142
+ });
143
+
144
+ const retrieved = db.getObservation(obs.id!);
145
+ expect(retrieved?.files).toEqual(['src/file1.ts', 'src/file2.ts']);
146
+ });
147
+
148
+ test('createObservation stores concepts as JSON array', () => {
149
+ const obs = db.createObservation({
150
+ timestamp: new Date().toISOString(),
151
+ type: 'discovery',
152
+ title: 'Test with concepts',
153
+ content: 'Has concepts',
154
+ concepts: ['database', 'testing'],
155
+ });
156
+
157
+ const retrieved = db.getObservation(obs.id!);
158
+ expect(retrieved?.concepts).toEqual(['database', 'testing']);
159
+ });
160
+
161
+ test('getObservation returns null for non-existent id', () => {
162
+ const result = db.getObservation(999999);
163
+ expect(result).toBeNull();
164
+ });
165
+
166
+ test('getObservation retrieves observation by id', () => {
167
+ const obs = db.createObservation({
168
+ timestamp: new Date().toISOString(),
169
+ type: 'bugfix',
170
+ title: 'Test retrieval',
171
+ content: 'Content to retrieve',
172
+ project: 'test-project',
173
+ work_tokens: 500,
174
+ });
175
+
176
+ const retrieved = db.getObservation(obs.id!);
177
+ expect(retrieved).not.toBeNull();
178
+ expect(retrieved?.title).toBe('Test retrieval');
179
+ expect(retrieved?.project).toBe('test-project');
180
+ expect(retrieved?.work_tokens).toBe(500);
181
+ });
182
+
183
+ test('getRecentObservations returns observations in descending order', () => {
184
+ // Create observations with clearly different timestamps
185
+ const now = Date.now();
186
+ for (let i = 0; i < 5; i++) {
187
+ db.createObservation({
188
+ // Use 1 hour intervals to ensure distinct timestamps
189
+ timestamp: new Date(now - i * 3600000).toISOString(),
190
+ type: 'discovery',
191
+ title: `Recent test ${i}`,
192
+ content: `Content ${i}`,
193
+ });
194
+ }
195
+
196
+ const recent = db.getRecentObservations(3);
197
+ expect(recent.length).toBe(3);
198
+ // Most recent should be first
199
+ expect(new Date(recent[0]!.timestamp).getTime()).toBeGreaterThanOrEqual(
200
+ new Date(recent[1]!.timestamp).getTime()
201
+ );
202
+ });
203
+
204
+ test('getRecentObservations filters by type', () => {
205
+ db.createObservation({
206
+ timestamp: new Date().toISOString(),
207
+ type: 'bugfix',
208
+ title: 'Bugfix obs',
209
+ content: 'Bugfix content',
210
+ });
211
+
212
+ db.createObservation({
213
+ timestamp: new Date().toISOString(),
214
+ type: 'feature',
215
+ title: 'Feature obs',
216
+ content: 'Feature content',
217
+ });
218
+
219
+ const bugfixes = db.getRecentObservations(10, ['bugfix']);
220
+ expect(bugfixes.every(o => o.type === 'bugfix')).toBe(true);
221
+ });
222
+
223
+ test('getRecentObservations filters by project', () => {
224
+ db.createObservation({
225
+ timestamp: new Date().toISOString(),
226
+ type: 'discovery',
227
+ title: 'Project A obs',
228
+ content: 'Content',
229
+ project: 'project-a',
230
+ });
231
+
232
+ db.createObservation({
233
+ timestamp: new Date().toISOString(),
234
+ type: 'discovery',
235
+ title: 'Project B obs',
236
+ content: 'Content',
237
+ project: 'project-b',
238
+ });
239
+
240
+ const projectA = db.getRecentObservations(10, undefined, 'project-a');
241
+ expect(projectA.every(o => o.project === 'project-a')).toBe(true);
242
+ });
243
+ });
244
+
245
+ describe('AnvilMemoryDb - FTS5 Search', () => {
246
+ let db: AnvilMemoryDb;
247
+ let testDbPath: string;
248
+
249
+ beforeAll(() => {
250
+ ensureTestDir();
251
+ testDbPath = getTestDbPath('db-fts');
252
+ db = new AnvilMemoryDb(testDbPath);
253
+ db.init();
254
+
255
+ // Add test observations for search
256
+ db.createObservation({
257
+ timestamp: new Date().toISOString(),
258
+ type: 'discovery',
259
+ title: 'SQLite FTS5 full-text search implementation',
260
+ content: 'Implemented full-text search using FTS5 with Porter stemmer',
261
+ concepts: ['database', 'search', 'fts5'],
262
+ });
263
+
264
+ db.createObservation({
265
+ timestamp: new Date().toISOString(),
266
+ type: 'bugfix',
267
+ title: 'Fixed database connection leak',
268
+ content: 'Resolved issue where database connections were not properly closed',
269
+ concepts: ['database', 'memory', 'leak'],
270
+ });
271
+
272
+ db.createObservation({
273
+ timestamp: new Date().toISOString(),
274
+ type: 'feature',
275
+ title: 'Added user authentication',
276
+ content: 'Implemented JWT-based authentication for API endpoints',
277
+ concepts: ['auth', 'jwt', 'security'],
278
+ });
279
+ });
280
+
281
+ afterAll(() => {
282
+ db.close();
283
+ cleanupDb(testDbPath);
284
+ });
285
+
286
+ test('searchObservations finds exact matches', () => {
287
+ const results = db.searchObservations('FTS5');
288
+ expect(results.length).toBeGreaterThan(0);
289
+ expect(results[0]!.title).toContain('FTS5');
290
+ });
291
+
292
+ test('searchObservations finds stemmed matches', () => {
293
+ // "searching" should match "search" due to Porter stemmer
294
+ const results = db.searchObservations('searching');
295
+ expect(results.length).toBeGreaterThan(0);
296
+ });
297
+
298
+ test('searchObservations finds matches in concepts', () => {
299
+ const results = db.searchObservations('jwt');
300
+ expect(results.length).toBeGreaterThan(0);
301
+ expect(results[0]!.concepts).toContain('jwt');
302
+ });
303
+
304
+ test('searchObservations handles multi-word queries', () => {
305
+ const results = db.searchObservations('database connection');
306
+ expect(results.length).toBeGreaterThan(0);
307
+ });
308
+
309
+ test('searchObservations returns results in relevance order', () => {
310
+ const results = db.searchObservations('database');
311
+ // Should find multiple results with "database"
312
+ expect(results.length).toBeGreaterThanOrEqual(2);
313
+ });
314
+
315
+ test('searchObservations returns empty array for no matches', () => {
316
+ const results = db.searchObservations('xyznonexistent123');
317
+ expect(results).toEqual([]);
318
+ });
319
+
320
+ test('searchObservations respects limit', () => {
321
+ const results = db.searchObservations('database', 1);
322
+ expect(results.length).toBe(1);
323
+ });
324
+ });
325
+
326
+ describe('AnvilMemoryDb - Sessions CRUD', () => {
327
+ let db: AnvilMemoryDb;
328
+ let testDbPath: string;
329
+
330
+ beforeAll(() => {
331
+ ensureTestDir();
332
+ testDbPath = getTestDbPath('db-session');
333
+ db = new AnvilMemoryDb(testDbPath);
334
+ db.init();
335
+ });
336
+
337
+ afterAll(() => {
338
+ db.close();
339
+ cleanupDb(testDbPath);
340
+ });
341
+
342
+ test('createSession returns session with id', () => {
343
+ const session = db.createSession({
344
+ started_at: new Date().toISOString(),
345
+ project: 'test-project',
346
+ });
347
+
348
+ expect(session.id).toBeDefined();
349
+ expect(session.id).toBeGreaterThan(0);
350
+ expect(session.project).toBe('test-project');
351
+ });
352
+
353
+ test('createSession stores optional fields', () => {
354
+ const session = db.createSession({
355
+ started_at: new Date().toISOString(),
356
+ project: 'test-project',
357
+ branch: 'feature/test',
358
+ git_hash: 'abc123',
359
+ });
360
+
361
+ const retrieved = db.getSession(session.id!);
362
+ expect(retrieved?.branch).toBe('feature/test');
363
+ expect(retrieved?.git_hash).toBe('abc123');
364
+ });
365
+
366
+ test('getSession returns null for non-existent id', () => {
367
+ const result = db.getSession(999999);
368
+ expect(result).toBeNull();
369
+ });
370
+
371
+ test('endSession updates session', () => {
372
+ const session = db.createSession({
373
+ started_at: new Date().toISOString(),
374
+ project: 'test-project',
375
+ });
376
+
377
+ db.endSession(session.id!, 'Session summary', 75);
378
+
379
+ const retrieved = db.getSession(session.id!);
380
+ expect(retrieved?.ended_at).toBeDefined();
381
+ expect(retrieved?.summary).toBe('Session summary');
382
+ expect(retrieved?.context_peak_percent).toBe(75);
383
+ });
384
+
385
+ test('endSession handles null summary (FTS5 trigger fix)', () => {
386
+ // This tests the FTS5 trigger fix - ending a session without summary should not error
387
+ const session = db.createSession({
388
+ started_at: new Date().toISOString(),
389
+ project: 'test-project',
390
+ });
391
+
392
+ // End without summary first
393
+ db.endSession(session.id!);
394
+
395
+ const retrieved = db.getSession(session.id!);
396
+ expect(retrieved?.ended_at).toBeDefined();
397
+
398
+ // Now add summary (should trigger sessions_au_insert)
399
+ db.endSession(session.id!, 'Late summary', 80);
400
+
401
+ const updated = db.getSession(session.id!);
402
+ expect(updated?.summary).toBe('Late summary');
403
+ });
404
+
405
+ test('session with initial summary can be updated', () => {
406
+ const session = db.createSession({
407
+ started_at: new Date().toISOString(),
408
+ project: 'test-project',
409
+ summary: 'Initial summary',
410
+ });
411
+
412
+ // Update summary (should trigger sessions_au)
413
+ db.endSession(session.id!, 'Updated summary', 90);
414
+
415
+ const retrieved = db.getSession(session.id!);
416
+ expect(retrieved?.summary).toBe('Updated summary');
417
+ });
418
+ });
419
+
420
+ describe('AnvilMemoryDb - Checkpoints CRUD', () => {
421
+ let db: AnvilMemoryDb;
422
+ let testDbPath: string;
423
+
424
+ beforeAll(() => {
425
+ ensureTestDir();
426
+ testDbPath = getTestDbPath('db-checkpoint');
427
+ db = new AnvilMemoryDb(testDbPath);
428
+ db.init();
429
+ });
430
+
431
+ afterAll(() => {
432
+ db.close();
433
+ cleanupDb(testDbPath);
434
+ });
435
+
436
+ test('createCheckpoint returns checkpoint with id', () => {
437
+ const checkpoint = db.createCheckpoint({
438
+ timestamp: new Date().toISOString(),
439
+ level: 'L1',
440
+ context_percent: 50,
441
+ });
442
+
443
+ expect(checkpoint.id).toBeDefined();
444
+ expect(checkpoint.id).toBeGreaterThan(0);
445
+ expect(checkpoint.level).toBe('L1');
446
+ });
447
+
448
+ test('createCheckpoint handles all levels', () => {
449
+ const levels: Checkpoint['level'][] = ['L1', 'L2', 'L3'];
450
+
451
+ for (const level of levels) {
452
+ const checkpoint = db.createCheckpoint({
453
+ timestamp: new Date().toISOString(),
454
+ level,
455
+ context_percent: 60,
456
+ });
457
+ expect(checkpoint.level).toBe(level);
458
+ }
459
+ });
460
+
461
+ test('createCheckpoint stores optional fields', () => {
462
+ const session = db.createSession({
463
+ started_at: new Date().toISOString(),
464
+ project: 'test-project',
465
+ });
466
+
467
+ const checkpoint = db.createCheckpoint({
468
+ timestamp: new Date().toISOString(),
469
+ level: 'L2',
470
+ context_percent: 80,
471
+ handoff_file: '.claude/handoffs/test.md',
472
+ session_id: session.id,
473
+ resumed: true,
474
+ });
475
+
476
+ const retrieved = db.getCheckpoint(checkpoint.id!);
477
+ expect(retrieved?.handoff_file).toBe('.claude/handoffs/test.md');
478
+ expect(retrieved?.session_id).toBe(session.id);
479
+ expect(retrieved?.resumed).toBe(true);
480
+ });
481
+
482
+ test('getCheckpoint returns null for non-existent id', () => {
483
+ const result = db.getCheckpoint(999999);
484
+ expect(result).toBeNull();
485
+ });
486
+ });
487
+
488
+ describe('AnvilMemoryDb - Ralph Iterations CRUD', () => {
489
+ let db: AnvilMemoryDb;
490
+ let testDbPath: string;
491
+
492
+ beforeAll(() => {
493
+ ensureTestDir();
494
+ testDbPath = getTestDbPath('db-ralph');
495
+ db = new AnvilMemoryDb(testDbPath);
496
+ db.init();
497
+ });
498
+
499
+ afterAll(() => {
500
+ db.close();
501
+ cleanupDb(testDbPath);
502
+ });
503
+
504
+ test('createRalphIteration returns iteration with id', () => {
505
+ const iteration = db.createRalphIteration({
506
+ session_id: 'ralph-session-123',
507
+ iteration: 1,
508
+ started_at: new Date().toISOString(),
509
+ status: 'running',
510
+ });
511
+
512
+ expect(iteration.id).toBeDefined();
513
+ expect(iteration.id).toBeGreaterThan(0);
514
+ expect(iteration.session_id).toBe('ralph-session-123');
515
+ expect(iteration.status).toBe('running');
516
+ });
517
+
518
+ test('createRalphIteration handles all statuses', () => {
519
+ const statuses: RalphIteration['status'][] = ['running', 'completed', 'failed', 'checkpointed'];
520
+
521
+ for (const status of statuses) {
522
+ const iteration = db.createRalphIteration({
523
+ session_id: `ralph-${status}`,
524
+ iteration: 1,
525
+ started_at: new Date().toISOString(),
526
+ status,
527
+ });
528
+ expect(iteration.status).toBe(status);
529
+ }
530
+ });
531
+
532
+ test('updateRalphIteration updates fields', () => {
533
+ const iteration = db.createRalphIteration({
534
+ session_id: 'ralph-update-test',
535
+ iteration: 1,
536
+ started_at: new Date().toISOString(),
537
+ status: 'running',
538
+ });
539
+
540
+ db.updateRalphIteration(iteration.id!, {
541
+ ended_at: new Date().toISOString(),
542
+ status: 'completed',
543
+ items_completed: 10,
544
+ items_total: 10,
545
+ });
546
+
547
+ // Verify using getRalphIteration method
548
+ const retrieved = db.getRalphIteration(iteration.id!);
549
+ expect(retrieved).not.toBeNull();
550
+ expect(retrieved?.status).toBe('completed');
551
+ expect(retrieved?.items_completed).toBe(10);
552
+ });
553
+
554
+ test('getRalphIteration returns null for non-existent id', () => {
555
+ const result = db.getRalphIteration(999999);
556
+ expect(result).toBeNull();
557
+ });
558
+ });
559
+
560
+ describe('AnvilMemoryDb - Prompts CRUD', () => {
561
+ let db: AnvilMemoryDb;
562
+ let testDbPath: string;
563
+
564
+ beforeAll(() => {
565
+ ensureTestDir();
566
+ testDbPath = getTestDbPath('db-prompt');
567
+ db = new AnvilMemoryDb(testDbPath);
568
+ db.init();
569
+
570
+ // Add prompts for search tests
571
+ db.createPrompt({
572
+ timestamp: new Date().toISOString(),
573
+ content: 'Help me implement a new feature for user authentication',
574
+ project: 'test-project',
575
+ });
576
+
577
+ db.createPrompt({
578
+ timestamp: new Date().toISOString(),
579
+ content: 'Fix the database connection pooling issue',
580
+ project: 'test-project',
581
+ });
582
+ });
583
+
584
+ afterAll(() => {
585
+ db.close();
586
+ cleanupDb(testDbPath);
587
+ });
588
+
589
+ test('createPrompt returns prompt with id', () => {
590
+ const prompt = db.createPrompt({
591
+ timestamp: new Date().toISOString(),
592
+ content: 'Test prompt content',
593
+ project: 'test-project',
594
+ });
595
+
596
+ expect(prompt.id).toBeDefined();
597
+ expect(prompt.id).toBeGreaterThan(0);
598
+ expect(prompt.content).toBe('Test prompt content');
599
+ });
600
+
601
+ test('searchPrompts finds matching prompts', () => {
602
+ const results = db.searchPrompts('authentication');
603
+ expect(results.length).toBeGreaterThan(0);
604
+ expect(results[0]!.content).toContain('authentication');
605
+ });
606
+
607
+ test('searchPrompts uses FTS5 stemming', () => {
608
+ // "connecting" should match "connection"
609
+ const results = db.searchPrompts('connecting');
610
+ expect(results.length).toBeGreaterThan(0);
611
+ });
612
+
613
+ test('searchPrompts returns empty array for no matches', () => {
614
+ const results = db.searchPrompts('xyznonexistent456');
615
+ expect(results).toEqual([]);
616
+ });
617
+ });
618
+
619
+ describe('AnvilMemoryDb - Utility Functions', () => {
620
+ test('getDefaultDbPath returns expected path', () => {
621
+ const path = getDefaultDbPath();
622
+ expect(path).toContain('.anvil');
623
+ expect(path).toContain('memory.db');
624
+ });
625
+
626
+ test('dbExists returns false for non-existent database', () => {
627
+ expect(dbExists('/tmp/nonexistent-db-12345.db')).toBe(false);
628
+ });
629
+
630
+ test('dbExists returns true for existing database', () => {
631
+ ensureTestDir();
632
+ const testPath = getTestDbPath('db-exists');
633
+ const db = new AnvilMemoryDb(testPath);
634
+ db.init();
635
+ db.close();
636
+
637
+ expect(dbExists(testPath)).toBe(true);
638
+
639
+ cleanupDb(testPath);
640
+ });
641
+ });