memory-journal-mcp 4.3.0 → 4.4.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 (109) hide show
  1. package/.dockerignore +131 -122
  2. package/.gitattributes +29 -0
  3. package/.github/workflows/docker-publish.yml +1 -1
  4. package/.github/workflows/lint-and-test.yml +1 -2
  5. package/.github/workflows/secrets-scanning.yml +0 -1
  6. package/.github/workflows/security-update.yml +6 -6
  7. package/.vscode/settings.json +17 -15
  8. package/CHANGELOG.md +1065 -11
  9. package/DOCKER_README.md +51 -33
  10. package/Dockerfile +14 -12
  11. package/README.md +68 -33
  12. package/SECURITY.md +225 -220
  13. package/dist/cli.js +7 -0
  14. package/dist/cli.js.map +1 -1
  15. package/dist/constants/ServerInstructions.d.ts +1 -1
  16. package/dist/constants/ServerInstructions.d.ts.map +1 -1
  17. package/dist/constants/ServerInstructions.js +70 -26
  18. package/dist/constants/ServerInstructions.js.map +1 -1
  19. package/dist/constants/icons.d.ts +2 -0
  20. package/dist/constants/icons.d.ts.map +1 -1
  21. package/dist/constants/icons.js +6 -0
  22. package/dist/constants/icons.js.map +1 -1
  23. package/dist/database/SqliteAdapter.d.ts +51 -10
  24. package/dist/database/SqliteAdapter.d.ts.map +1 -1
  25. package/dist/database/SqliteAdapter.js +143 -43
  26. package/dist/database/SqliteAdapter.js.map +1 -1
  27. package/dist/filtering/ToolFilter.d.ts +1 -1
  28. package/dist/filtering/ToolFilter.d.ts.map +1 -1
  29. package/dist/filtering/ToolFilter.js +7 -1
  30. package/dist/filtering/ToolFilter.js.map +1 -1
  31. package/dist/github/GitHubIntegration.d.ts +74 -2
  32. package/dist/github/GitHubIntegration.d.ts.map +1 -1
  33. package/dist/github/GitHubIntegration.js +508 -7
  34. package/dist/github/GitHubIntegration.js.map +1 -1
  35. package/dist/handlers/prompts/index.js +1 -0
  36. package/dist/handlers/prompts/index.js.map +1 -1
  37. package/dist/handlers/resources/index.d.ts.map +1 -1
  38. package/dist/handlers/resources/index.js +257 -13
  39. package/dist/handlers/resources/index.js.map +1 -1
  40. package/dist/handlers/tools/index.d.ts.map +1 -1
  41. package/dist/handlers/tools/index.js +595 -8
  42. package/dist/handlers/tools/index.js.map +1 -1
  43. package/dist/server/McpServer.d.ts +2 -0
  44. package/dist/server/McpServer.d.ts.map +1 -1
  45. package/dist/server/McpServer.js +69 -26
  46. package/dist/server/McpServer.js.map +1 -1
  47. package/dist/types/index.d.ts +97 -0
  48. package/dist/types/index.d.ts.map +1 -1
  49. package/dist/types/index.js.map +1 -1
  50. package/dist/utils/logger.d.ts +1 -0
  51. package/dist/utils/logger.d.ts.map +1 -1
  52. package/dist/utils/logger.js +8 -1
  53. package/dist/utils/logger.js.map +1 -1
  54. package/dist/utils/progress-utils.d.ts +18 -3
  55. package/dist/utils/progress-utils.d.ts.map +1 -1
  56. package/dist/utils/progress-utils.js.map +1 -1
  57. package/dist/utils/security-utils.d.ts +91 -0
  58. package/dist/utils/security-utils.d.ts.map +1 -0
  59. package/dist/utils/security-utils.js +184 -0
  60. package/dist/utils/security-utils.js.map +1 -0
  61. package/dist/vector/VectorSearchManager.d.ts +2 -1
  62. package/dist/vector/VectorSearchManager.d.ts.map +1 -1
  63. package/dist/vector/VectorSearchManager.js +100 -34
  64. package/dist/vector/VectorSearchManager.js.map +1 -1
  65. package/docker-compose.yml +46 -37
  66. package/mcp-config-example.json +0 -2
  67. package/package.json +21 -14
  68. package/releases/v4.3.1.md +69 -0
  69. package/releases/v4.4.0.md +120 -0
  70. package/server.json +3 -3
  71. package/src/cli.ts +11 -0
  72. package/src/constants/ServerInstructions.ts +70 -26
  73. package/src/constants/icons.ts +7 -0
  74. package/src/database/SqliteAdapter.ts +165 -44
  75. package/src/filtering/ToolFilter.ts +7 -1
  76. package/src/github/GitHubIntegration.ts +588 -8
  77. package/src/handlers/prompts/index.ts +1 -0
  78. package/src/handlers/resources/index.ts +318 -12
  79. package/src/handlers/tools/index.ts +686 -13
  80. package/src/server/McpServer.ts +79 -37
  81. package/src/types/index.ts +98 -0
  82. package/src/utils/logger.ts +10 -1
  83. package/src/utils/progress-utils.ts +17 -6
  84. package/src/utils/security-utils.ts +205 -0
  85. package/src/vector/VectorSearchManager.ts +110 -39
  86. package/tests/constants/icons.test.ts +102 -0
  87. package/tests/constants/server-instructions.test.ts +549 -0
  88. package/tests/database/sqlite-adapter.bench.ts +63 -0
  89. package/tests/database/sqlite-adapter.test.ts +555 -0
  90. package/tests/filtering/tool-filter.test.ts +266 -0
  91. package/tests/github/github-integration.test.ts +1024 -0
  92. package/tests/handlers/github-resource-handlers.test.ts +473 -0
  93. package/tests/handlers/github-tool-handlers.test.ts +556 -0
  94. package/tests/handlers/prompt-handlers.test.ts +91 -0
  95. package/tests/handlers/resource-handlers.test.ts +339 -0
  96. package/tests/handlers/tool-handlers.test.ts +497 -0
  97. package/tests/handlers/vector-tool-handlers.test.ts +238 -0
  98. package/tests/security/sql-injection.test.ts +347 -0
  99. package/tests/server/mcp-server.bench.ts +55 -0
  100. package/tests/server/mcp-server.test.ts +675 -0
  101. package/tests/utils/logger.test.ts +180 -0
  102. package/tests/utils/mcp-logger.test.ts +212 -0
  103. package/tests/utils/progress-utils.test.ts +156 -0
  104. package/tests/utils/security-utils.test.ts +82 -0
  105. package/tests/vector/vector-search-manager.test.ts +335 -0
  106. package/tests/vector/vector-search.bench.ts +53 -0
  107. package/vitest.config.ts +15 -0
  108. package/.github/workflows/DOCKER_DEPLOYMENT_SETUP.md +0 -387
  109. package/.github/workflows/dependabot-auto-merge.yml +0 -42
@@ -0,0 +1,555 @@
1
+ /**
2
+ * SqliteAdapter Tests
3
+ *
4
+ * Functional tests for database adapter methods not covered by
5
+ * tests/security/sql-injection.test.ts.
6
+ */
7
+
8
+ import { describe, it, expect, beforeAll, afterAll } from 'vitest'
9
+ import { SqliteAdapter } from '../../src/database/SqliteAdapter.js'
10
+ import type { RelationshipType } from '../../src/types/index.js'
11
+
12
+ describe('SqliteAdapter', () => {
13
+ let db: SqliteAdapter
14
+ const testDbPath = './test-adapter.db'
15
+
16
+ beforeAll(async () => {
17
+ db = new SqliteAdapter(testDbPath)
18
+ await db.initialize()
19
+ })
20
+
21
+ afterAll(() => {
22
+ db.close()
23
+ try {
24
+ const fs = require('node:fs')
25
+ if (fs.existsSync(testDbPath)) {
26
+ fs.unlinkSync(testDbPath)
27
+ }
28
+ } catch {
29
+ // Ignore cleanup errors
30
+ }
31
+ })
32
+
33
+ // ========================================================================
34
+ // Initialize
35
+ // ========================================================================
36
+
37
+ describe('initialize', () => {
38
+ it('should be idempotent on re-init', async () => {
39
+ // Second init should not throw
40
+ await db.initialize()
41
+ })
42
+
43
+ it('should throw when accessing uninitalized db', async () => {
44
+ const uninit = new SqliteAdapter('./uninit-test.db')
45
+ expect(() => uninit.getActiveEntryCount()).toThrow('Database not initialized')
46
+ })
47
+ })
48
+
49
+ // ========================================================================
50
+ // Entry CRUD
51
+ // ========================================================================
52
+
53
+ describe('createEntry', () => {
54
+ it('should create an entry with defaults', () => {
55
+ const entry = db.createEntry({ content: 'Test entry' })
56
+
57
+ expect(entry.id).toBeGreaterThan(0)
58
+ expect(entry.content).toBe('Test entry')
59
+ expect(entry.entryType).toBe('personal_reflection')
60
+ expect(entry.isPersonal).toBe(true)
61
+ expect(entry.tags).toEqual([])
62
+ })
63
+
64
+ it('should create an entry with all fields', () => {
65
+ const entry = db.createEntry({
66
+ content: 'Full entry',
67
+ entryType: 'decision',
68
+ tags: ['tag-a', 'tag-b'],
69
+ isPersonal: false,
70
+ significanceType: 'milestone',
71
+ autoContext: 'test-context',
72
+ projectNumber: 42,
73
+ issueNumber: 7,
74
+ })
75
+
76
+ expect(entry.entryType).toBe('decision')
77
+ expect(entry.isPersonal).toBe(false)
78
+ expect(entry.tags).toContain('tag-a')
79
+ expect(entry.tags).toContain('tag-b')
80
+ })
81
+ })
82
+
83
+ describe('getEntryById', () => {
84
+ it('should return entry by ID', () => {
85
+ const created = db.createEntry({ content: 'Find me' })
86
+ const found = db.getEntryById(created.id)
87
+
88
+ expect(found).not.toBeNull()
89
+ expect(found?.content).toBe('Find me')
90
+ })
91
+
92
+ it('should return null for nonexistent ID', () => {
93
+ expect(db.getEntryById(99999)).toBeNull()
94
+ })
95
+
96
+ it('should exclude soft-deleted entries', () => {
97
+ const entry = db.createEntry({ content: 'Will be deleted' })
98
+ db.deleteEntry(entry.id)
99
+
100
+ expect(db.getEntryById(entry.id)).toBeNull()
101
+ })
102
+ })
103
+
104
+ describe('getEntryByIdIncludeDeleted', () => {
105
+ it('should return soft-deleted entries', () => {
106
+ const entry = db.createEntry({ content: 'Soft deleted' })
107
+ db.deleteEntry(entry.id)
108
+
109
+ const found = db.getEntryByIdIncludeDeleted(entry.id)
110
+ expect(found).not.toBeNull()
111
+ expect(found?.content).toBe('Soft deleted')
112
+ })
113
+
114
+ it('should return null for nonexistent ID', () => {
115
+ expect(db.getEntryByIdIncludeDeleted(99999)).toBeNull()
116
+ })
117
+ })
118
+
119
+ // ========================================================================
120
+ // calculateImportance
121
+ // ========================================================================
122
+
123
+ describe('calculateImportance', () => {
124
+ it('should return 0 for nonexistent entry', () => {
125
+ const result = db.calculateImportance(99999)
126
+ expect(result.score).toBe(0)
127
+ expect(result.breakdown.significance).toBe(0)
128
+ })
129
+
130
+ it('should include recency component for fresh entries', () => {
131
+ const entry = db.createEntry({ content: 'Fresh entry' })
132
+ const result = db.calculateImportance(entry.id)
133
+
134
+ // Fresh entry should have non-zero recency
135
+ expect(result.breakdown.recency).toBeGreaterThan(0)
136
+ })
137
+
138
+ it('should include significance component when set', () => {
139
+ const entry = db.createEntry({
140
+ content: 'Important entry',
141
+ significanceType: 'milestone',
142
+ })
143
+ const result = db.calculateImportance(entry.id)
144
+
145
+ expect(result.breakdown.significance).toBe(0.3)
146
+ })
147
+
148
+ it('should include relationship component', () => {
149
+ const entry1 = db.createEntry({ content: 'Entry A' })
150
+ const entry2 = db.createEntry({ content: 'Entry B' })
151
+ db.linkEntries(entry1.id, entry2.id, 'references')
152
+
153
+ const result = db.calculateImportance(entry1.id)
154
+ expect(result.breakdown.relationships).toBeGreaterThan(0)
155
+ })
156
+
157
+ it('should include causal component for causal relationships', () => {
158
+ const entry1 = db.createEntry({ content: 'Blocker' })
159
+ const entry2 = db.createEntry({ content: 'Resolution' })
160
+ db.linkEntries(entry1.id, entry2.id, 'blocked_by')
161
+
162
+ const result = db.calculateImportance(entry1.id)
163
+ expect(result.breakdown.causal).toBeGreaterThan(0)
164
+ })
165
+
166
+ it('should have score between 0 and 1', () => {
167
+ const entry = db.createEntry({
168
+ content: 'Scored entry',
169
+ significanceType: 'milestone',
170
+ })
171
+ const result = db.calculateImportance(entry.id)
172
+
173
+ expect(result.score).toBeGreaterThanOrEqual(0)
174
+ expect(result.score).toBeLessThanOrEqual(1)
175
+ })
176
+ })
177
+
178
+ // ========================================================================
179
+ // getRecentEntries / pagination
180
+ // ========================================================================
181
+
182
+ describe('getRecentEntries', () => {
183
+ it('should respect limit', () => {
184
+ const entries = db.getRecentEntries(2)
185
+ expect(entries.length).toBeLessThanOrEqual(2)
186
+ })
187
+
188
+ it('should filter by isPersonal', () => {
189
+ db.createEntry({ content: 'Personal entry', isPersonal: true })
190
+ db.createEntry({ content: 'Team entry', isPersonal: false })
191
+
192
+ const personal = db.getRecentEntries(100, true)
193
+ const team = db.getRecentEntries(100, false)
194
+
195
+ expect(personal.every((e) => e.isPersonal)).toBe(true)
196
+ expect(team.every((e) => !e.isPersonal)).toBe(true)
197
+ })
198
+ })
199
+
200
+ describe('getEntriesPage / getActiveEntryCount', () => {
201
+ it('should return correct active count', () => {
202
+ const count = db.getActiveEntryCount()
203
+ expect(count).toBeGreaterThan(0)
204
+ })
205
+
206
+ it('should paginate through entries', () => {
207
+ const page1 = db.getEntriesPage(0, 2)
208
+ const page2 = db.getEntriesPage(2, 2)
209
+
210
+ expect(page1.length).toBeLessThanOrEqual(2)
211
+ // Pages should not overlap (different IDs)
212
+ if (page2.length > 0) {
213
+ expect(page1[0]?.id).not.toBe(page2[0]?.id)
214
+ }
215
+ })
216
+ })
217
+
218
+ // ========================================================================
219
+ // deleteEntry
220
+ // ========================================================================
221
+
222
+ describe('deleteEntry', () => {
223
+ it('should soft delete an entry', () => {
224
+ const entry = db.createEntry({ content: 'To soft delete' })
225
+ const result = db.deleteEntry(entry.id)
226
+
227
+ expect(result).toBe(true)
228
+ expect(db.getEntryById(entry.id)).toBeNull()
229
+ expect(db.getEntryByIdIncludeDeleted(entry.id)).not.toBeNull()
230
+ })
231
+
232
+ it('should permanently delete an entry', () => {
233
+ const entry = db.createEntry({ content: 'To permanently delete' })
234
+ const result = db.deleteEntry(entry.id, true)
235
+
236
+ expect(result).toBe(true)
237
+ expect(db.getEntryByIdIncludeDeleted(entry.id)).toBeNull()
238
+ })
239
+
240
+ it('should return false for nonexistent entry', () => {
241
+ expect(db.deleteEntry(99999)).toBe(false)
242
+ })
243
+
244
+ it('should permanently delete a soft-deleted entry', () => {
245
+ const entry = db.createEntry({ content: 'Soft then hard' })
246
+ db.deleteEntry(entry.id) // soft delete
247
+ const result = db.deleteEntry(entry.id, true) // permanent delete
248
+
249
+ expect(result).toBe(true)
250
+ expect(db.getEntryByIdIncludeDeleted(entry.id)).toBeNull()
251
+ })
252
+ })
253
+
254
+ // ========================================================================
255
+ // Search
256
+ // ========================================================================
257
+
258
+ describe('searchEntries', () => {
259
+ it('should find entries by content', () => {
260
+ db.createEntry({ content: 'Unique search term xyz123' })
261
+ const results = db.searchEntries('xyz123')
262
+
263
+ expect(results.length).toBeGreaterThan(0)
264
+ expect(results[0]?.content).toContain('xyz123')
265
+ })
266
+
267
+ it('should respect limit', () => {
268
+ const results = db.searchEntries('entry', { limit: 1 })
269
+ expect(results.length).toBeLessThanOrEqual(1)
270
+ })
271
+
272
+ it('should filter by projectNumber', () => {
273
+ db.createEntry({ content: 'Project specific qwer', projectNumber: 99 })
274
+ const results = db.searchEntries('qwer', { projectNumber: 99 })
275
+
276
+ expect(results.length).toBeGreaterThan(0)
277
+ })
278
+
279
+ it('should return empty for non-matching query', () => {
280
+ const results = db.searchEntries('nonexistent_term_that_has_no_matches')
281
+ expect(results).toEqual([])
282
+ })
283
+ })
284
+
285
+ describe('searchByDateRange', () => {
286
+ it('should find entries within date range', () => {
287
+ db.createEntry({ content: 'Date range entry' })
288
+ const now = new Date()
289
+ const start = now.toISOString().split('T')[0]!
290
+ const end = start
291
+
292
+ const results = db.searchByDateRange(start, end)
293
+ expect(results.length).toBeGreaterThan(0)
294
+ })
295
+
296
+ it('should return empty for future date range', () => {
297
+ const results = db.searchByDateRange('2099-01-01', '2099-12-31')
298
+ expect(results).toEqual([])
299
+ })
300
+ })
301
+
302
+ // ========================================================================
303
+ // Relationships
304
+ // ========================================================================
305
+
306
+ describe('linkEntries / getRelationships', () => {
307
+ it('should create and retrieve a relationship', () => {
308
+ const e1 = db.createEntry({ content: 'Rel source' })
309
+ const e2 = db.createEntry({ content: 'Rel target' })
310
+
311
+ const rel = db.linkEntries(e1.id, e2.id, 'references', 'Test link')
312
+
313
+ expect(rel.id).toBeGreaterThan(0)
314
+ expect(rel.relationshipType).toBe('references')
315
+ expect(rel.description).toBe('Test link')
316
+ })
317
+
318
+ it('should retrieve relationships for an entry', () => {
319
+ const e1 = db.createEntry({ content: 'Has rels' })
320
+ const e2 = db.createEntry({ content: 'Also has rels' })
321
+ db.linkEntries(e1.id, e2.id, 'evolves_from')
322
+
323
+ const rels = db.getRelationships(e1.id)
324
+ expect(rels.length).toBeGreaterThan(0)
325
+ expect(rels.some((r) => r.relationshipType === 'evolves_from')).toBe(true)
326
+ })
327
+
328
+ it('should throw for nonexistent source entry', () => {
329
+ const e2 = db.createEntry({ content: 'Target exists' })
330
+ expect(() => db.linkEntries(99999, e2.id, 'references')).toThrow()
331
+ })
332
+
333
+ it('should throw for nonexistent target entry', () => {
334
+ const e1 = db.createEntry({ content: 'Source exists' })
335
+ expect(() => db.linkEntries(e1.id, 99999, 'references')).toThrow()
336
+ })
337
+
338
+ it('should support all relationship types', () => {
339
+ const types: RelationshipType[] = [
340
+ 'evolves_from',
341
+ 'references',
342
+ 'implements',
343
+ 'blocked_by',
344
+ 'resolved',
345
+ 'caused',
346
+ ]
347
+
348
+ for (const type of types) {
349
+ const e1 = db.createEntry({ content: `From ${type}` })
350
+ const e2 = db.createEntry({ content: `To ${type}` })
351
+ const rel = db.linkEntries(e1.id, e2.id, type)
352
+ expect(rel.relationshipType).toBe(type)
353
+ }
354
+ })
355
+ })
356
+
357
+ // ========================================================================
358
+ // Tags
359
+ // ========================================================================
360
+
361
+ describe('tag operations', () => {
362
+ it('should list tags with usage', () => {
363
+ db.createEntry({ content: 'Tagged', tags: ['unique-tag-abc'] })
364
+ const tags = db.listTags()
365
+
366
+ const found = tags.find((t) => t.name === 'unique-tag-abc')
367
+ expect(found).toBeDefined()
368
+ expect(found?.usageCount).toBeGreaterThan(0)
369
+ })
370
+
371
+ it('should get tags for an entry', () => {
372
+ const entry = db.createEntry({ content: 'Multi tag', tags: ['mt-1', 'mt-2'] })
373
+ const tags = db.getTagsForEntry(entry.id)
374
+
375
+ expect(tags).toContain('mt-1')
376
+ expect(tags).toContain('mt-2')
377
+ })
378
+
379
+ it('should merge tags', () => {
380
+ db.createEntry({ content: 'Merge source', tags: ['old-tag'] })
381
+ const result = db.mergeTags('old-tag', 'new-tag')
382
+
383
+ expect(result.sourceDeleted).toBe(true)
384
+ expect(result.entriesUpdated).toBeGreaterThanOrEqual(0)
385
+ })
386
+
387
+ it('should throw when merging nonexistent source tag', () => {
388
+ expect(() => db.mergeTags('nonexistent-tag-xyz', 'any-target')).toThrow(
389
+ 'Source tag not found'
390
+ )
391
+ })
392
+ })
393
+
394
+ // ========================================================================
395
+ // Statistics
396
+ // ========================================================================
397
+
398
+ describe('getStatistics', () => {
399
+ it('should return statistics with day grouping', () => {
400
+ const stats = db.getStatistics('day')
401
+
402
+ expect(stats.totalEntries).toBeGreaterThan(0)
403
+ expect(stats.entriesByType).toBeDefined()
404
+ expect(stats.entriesByPeriod).toBeDefined()
405
+ })
406
+
407
+ it('should return statistics with week grouping', () => {
408
+ const stats = db.getStatistics('week')
409
+ expect(stats.totalEntries).toBeGreaterThan(0)
410
+ })
411
+
412
+ it('should return statistics with month grouping', () => {
413
+ const stats = db.getStatistics('month')
414
+ expect(stats.totalEntries).toBeGreaterThan(0)
415
+ })
416
+
417
+ it('should include causal metrics', () => {
418
+ const stats = db.getStatistics()
419
+ expect(stats.causalMetrics).toBeDefined()
420
+ expect(typeof stats.causalMetrics.blocked_by).toBe('number')
421
+ expect(typeof stats.causalMetrics.resolved).toBe('number')
422
+ expect(typeof stats.causalMetrics.caused).toBe('number')
423
+ })
424
+ })
425
+
426
+ // ========================================================================
427
+ // Health Status
428
+ // ========================================================================
429
+
430
+ describe('getHealthStatus', () => {
431
+ it('should return health status', () => {
432
+ const health = db.getHealthStatus()
433
+
434
+ expect(health.database.path).toBe(testDbPath)
435
+ expect(health.database.entryCount).toBeGreaterThan(0)
436
+ expect(typeof health.database.sizeBytes).toBe('number')
437
+ expect(typeof health.database.deletedEntryCount).toBe('number')
438
+ expect(typeof health.database.relationshipCount).toBe('number')
439
+ expect(typeof health.database.tagCount).toBe('number')
440
+ })
441
+ })
442
+
443
+ // ========================================================================
444
+ // Backup operations
445
+ // ========================================================================
446
+
447
+ describe('backup operations', () => {
448
+ it('should export to backup file', () => {
449
+ const backup = db.exportToFile('test-backup')
450
+
451
+ expect(backup.filename).toContain('test-backup')
452
+ expect(backup.sizeBytes).toBeGreaterThan(0)
453
+
454
+ // Cleanup
455
+ const fs = require('node:fs')
456
+ if (fs.existsSync(backup.path)) {
457
+ fs.unlinkSync(backup.path)
458
+ }
459
+ })
460
+
461
+ it('should list backup files', () => {
462
+ const backup = db.exportToFile('list-test')
463
+ const backups = db.listBackups()
464
+
465
+ expect(backups.length).toBeGreaterThan(0)
466
+ expect(backups.some((b) => b.filename.includes('list-test'))).toBe(true)
467
+
468
+ // Cleanup
469
+ const fs = require('node:fs')
470
+ if (fs.existsSync(backup.path)) {
471
+ fs.unlinkSync(backup.path)
472
+ }
473
+ })
474
+ })
475
+
476
+ // ========================================================================
477
+ // Close
478
+ // ========================================================================
479
+
480
+ describe('close', () => {
481
+ it('should close without error', () => {
482
+ const tempDb = new SqliteAdapter('./test-close.db')
483
+ // Close without init should not throw
484
+ tempDb.close()
485
+ })
486
+ })
487
+
488
+ // ========================================================================
489
+ // Additional branch coverage
490
+ // ========================================================================
491
+
492
+ describe('updateEntry', () => {
493
+ it('should update entry content', () => {
494
+ const entry = db.createEntry({ content: 'Original content' })
495
+ const updated = db.updateEntry(entry.id, { content: 'Updated content' })
496
+
497
+ expect(updated).not.toBeNull()
498
+ expect(updated?.content).toBe('Updated content')
499
+ })
500
+
501
+ it('should update tags', () => {
502
+ const entry = db.createEntry({ content: 'Tag update', tags: ['initial'] })
503
+ const updated = db.updateEntry(entry.id, { tags: ['updated-tag'] })
504
+
505
+ expect(updated).not.toBeNull()
506
+ const tags = db.getTagsForEntry(entry.id)
507
+ expect(tags).toContain('updated-tag')
508
+ })
509
+
510
+ it('should return null for nonexistent entry', () => {
511
+ const result = db.updateEntry(99999, { content: 'Nope' })
512
+ expect(result).toBeNull()
513
+ })
514
+ })
515
+
516
+ describe('searchEntries - advanced filters', () => {
517
+ it('should filter by issueNumber', () => {
518
+ db.createEntry({ content: 'Issue filter test', issueNumber: 888 })
519
+ const results = db.searchEntries('', { issueNumber: 888 })
520
+ expect(results.length).toBeGreaterThan(0)
521
+ })
522
+
523
+ it('should filter by prNumber', () => {
524
+ db.createEntry({ content: 'PR filter test', prNumber: 77 })
525
+ const results = db.searchEntries('', { prNumber: 77 })
526
+ expect(results.length).toBeGreaterThan(0)
527
+ })
528
+
529
+ it('should filter by isPersonal', () => {
530
+ const results = db.searchEntries('', { isPersonal: false })
531
+ expect(results.every((e) => !e.isPersonal)).toBe(true)
532
+ })
533
+ })
534
+
535
+ describe('searchByDateRange - with type filter', () => {
536
+ it('should filter by entry type', () => {
537
+ const today = new Date().toISOString().split('T')[0]!
538
+ const results = db.searchByDateRange(today, today, {
539
+ entryType: 'decision',
540
+ })
541
+ for (const r of results) {
542
+ expect(r.entryType).toBe('decision')
543
+ }
544
+ })
545
+
546
+ it('should filter by tags', () => {
547
+ db.createEntry({ content: 'Tag date range', tags: ['daterange-tag'] })
548
+ const today = new Date().toISOString().split('T')[0]!
549
+ const results = db.searchByDateRange(today, today, {
550
+ tags: ['daterange-tag'],
551
+ })
552
+ expect(results.length).toBeGreaterThan(0)
553
+ })
554
+ })
555
+ })