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.
- package/.dockerignore +131 -122
- package/.gitattributes +29 -0
- package/.github/workflows/docker-publish.yml +1 -1
- package/.github/workflows/lint-and-test.yml +1 -2
- package/.github/workflows/secrets-scanning.yml +0 -1
- package/.github/workflows/security-update.yml +6 -6
- package/.vscode/settings.json +17 -15
- package/CHANGELOG.md +1065 -11
- package/DOCKER_README.md +51 -33
- package/Dockerfile +14 -12
- package/README.md +68 -33
- package/SECURITY.md +225 -220
- package/dist/cli.js +7 -0
- package/dist/cli.js.map +1 -1
- package/dist/constants/ServerInstructions.d.ts +1 -1
- package/dist/constants/ServerInstructions.d.ts.map +1 -1
- package/dist/constants/ServerInstructions.js +70 -26
- package/dist/constants/ServerInstructions.js.map +1 -1
- package/dist/constants/icons.d.ts +2 -0
- package/dist/constants/icons.d.ts.map +1 -1
- package/dist/constants/icons.js +6 -0
- package/dist/constants/icons.js.map +1 -1
- package/dist/database/SqliteAdapter.d.ts +51 -10
- package/dist/database/SqliteAdapter.d.ts.map +1 -1
- package/dist/database/SqliteAdapter.js +143 -43
- package/dist/database/SqliteAdapter.js.map +1 -1
- package/dist/filtering/ToolFilter.d.ts +1 -1
- package/dist/filtering/ToolFilter.d.ts.map +1 -1
- package/dist/filtering/ToolFilter.js +7 -1
- package/dist/filtering/ToolFilter.js.map +1 -1
- package/dist/github/GitHubIntegration.d.ts +74 -2
- package/dist/github/GitHubIntegration.d.ts.map +1 -1
- package/dist/github/GitHubIntegration.js +508 -7
- package/dist/github/GitHubIntegration.js.map +1 -1
- package/dist/handlers/prompts/index.js +1 -0
- package/dist/handlers/prompts/index.js.map +1 -1
- package/dist/handlers/resources/index.d.ts.map +1 -1
- package/dist/handlers/resources/index.js +257 -13
- package/dist/handlers/resources/index.js.map +1 -1
- package/dist/handlers/tools/index.d.ts.map +1 -1
- package/dist/handlers/tools/index.js +595 -8
- package/dist/handlers/tools/index.js.map +1 -1
- package/dist/server/McpServer.d.ts +2 -0
- package/dist/server/McpServer.d.ts.map +1 -1
- package/dist/server/McpServer.js +69 -26
- package/dist/server/McpServer.js.map +1 -1
- package/dist/types/index.d.ts +97 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js.map +1 -1
- package/dist/utils/logger.d.ts +1 -0
- package/dist/utils/logger.d.ts.map +1 -1
- package/dist/utils/logger.js +8 -1
- package/dist/utils/logger.js.map +1 -1
- package/dist/utils/progress-utils.d.ts +18 -3
- package/dist/utils/progress-utils.d.ts.map +1 -1
- package/dist/utils/progress-utils.js.map +1 -1
- package/dist/utils/security-utils.d.ts +91 -0
- package/dist/utils/security-utils.d.ts.map +1 -0
- package/dist/utils/security-utils.js +184 -0
- package/dist/utils/security-utils.js.map +1 -0
- package/dist/vector/VectorSearchManager.d.ts +2 -1
- package/dist/vector/VectorSearchManager.d.ts.map +1 -1
- package/dist/vector/VectorSearchManager.js +100 -34
- package/dist/vector/VectorSearchManager.js.map +1 -1
- package/docker-compose.yml +46 -37
- package/mcp-config-example.json +0 -2
- package/package.json +21 -14
- package/releases/v4.3.1.md +69 -0
- package/releases/v4.4.0.md +120 -0
- package/server.json +3 -3
- package/src/cli.ts +11 -0
- package/src/constants/ServerInstructions.ts +70 -26
- package/src/constants/icons.ts +7 -0
- package/src/database/SqliteAdapter.ts +165 -44
- package/src/filtering/ToolFilter.ts +7 -1
- package/src/github/GitHubIntegration.ts +588 -8
- package/src/handlers/prompts/index.ts +1 -0
- package/src/handlers/resources/index.ts +318 -12
- package/src/handlers/tools/index.ts +686 -13
- package/src/server/McpServer.ts +79 -37
- package/src/types/index.ts +98 -0
- package/src/utils/logger.ts +10 -1
- package/src/utils/progress-utils.ts +17 -6
- package/src/utils/security-utils.ts +205 -0
- package/src/vector/VectorSearchManager.ts +110 -39
- package/tests/constants/icons.test.ts +102 -0
- package/tests/constants/server-instructions.test.ts +549 -0
- package/tests/database/sqlite-adapter.bench.ts +63 -0
- package/tests/database/sqlite-adapter.test.ts +555 -0
- package/tests/filtering/tool-filter.test.ts +266 -0
- package/tests/github/github-integration.test.ts +1024 -0
- package/tests/handlers/github-resource-handlers.test.ts +473 -0
- package/tests/handlers/github-tool-handlers.test.ts +556 -0
- package/tests/handlers/prompt-handlers.test.ts +91 -0
- package/tests/handlers/resource-handlers.test.ts +339 -0
- package/tests/handlers/tool-handlers.test.ts +497 -0
- package/tests/handlers/vector-tool-handlers.test.ts +238 -0
- package/tests/security/sql-injection.test.ts +347 -0
- package/tests/server/mcp-server.bench.ts +55 -0
- package/tests/server/mcp-server.test.ts +675 -0
- package/tests/utils/logger.test.ts +180 -0
- package/tests/utils/mcp-logger.test.ts +212 -0
- package/tests/utils/progress-utils.test.ts +156 -0
- package/tests/utils/security-utils.test.ts +82 -0
- package/tests/vector/vector-search-manager.test.ts +335 -0
- package/tests/vector/vector-search.bench.ts +53 -0
- package/vitest.config.ts +15 -0
- package/.github/workflows/DOCKER_DEPLOYMENT_SETUP.md +0 -387
- 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
|
+
})
|