memory-journal-mcp 4.3.1 → 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 (103) 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 +12 -11
  11. package/README.md +68 -33
  12. package/SECURITY.md +225 -158
  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 +140 -31
  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 +590 -7
  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/security-utils.d.ts +8 -0
  55. package/dist/utils/security-utils.d.ts.map +1 -1
  56. package/dist/utils/security-utils.js +32 -0
  57. package/dist/utils/security-utils.js.map +1 -1
  58. package/dist/vector/VectorSearchManager.d.ts +2 -1
  59. package/dist/vector/VectorSearchManager.d.ts.map +1 -1
  60. package/dist/vector/VectorSearchManager.js +100 -34
  61. package/dist/vector/VectorSearchManager.js.map +1 -1
  62. package/docker-compose.yml +46 -37
  63. package/mcp-config-example.json +0 -2
  64. package/package.json +12 -11
  65. package/releases/v4.3.1.md +69 -0
  66. package/releases/v4.4.0.md +120 -0
  67. package/server.json +3 -3
  68. package/src/cli.ts +11 -0
  69. package/src/constants/ServerInstructions.ts +70 -26
  70. package/src/constants/icons.ts +7 -0
  71. package/src/database/SqliteAdapter.ts +162 -32
  72. package/src/filtering/ToolFilter.ts +7 -1
  73. package/src/github/GitHubIntegration.ts +588 -8
  74. package/src/handlers/prompts/index.ts +1 -0
  75. package/src/handlers/resources/index.ts +318 -12
  76. package/src/handlers/tools/index.ts +681 -12
  77. package/src/server/McpServer.ts +79 -37
  78. package/src/types/index.ts +98 -0
  79. package/src/utils/logger.ts +10 -1
  80. package/src/utils/security-utils.ts +35 -0
  81. package/src/vector/VectorSearchManager.ts +110 -39
  82. package/tests/constants/icons.test.ts +102 -0
  83. package/tests/constants/server-instructions.test.ts +549 -0
  84. package/tests/database/sqlite-adapter.bench.ts +63 -0
  85. package/tests/database/sqlite-adapter.test.ts +555 -0
  86. package/tests/filtering/tool-filter.test.ts +266 -0
  87. package/tests/github/github-integration.test.ts +1024 -0
  88. package/tests/handlers/github-resource-handlers.test.ts +473 -0
  89. package/tests/handlers/github-tool-handlers.test.ts +556 -0
  90. package/tests/handlers/prompt-handlers.test.ts +91 -0
  91. package/tests/handlers/resource-handlers.test.ts +339 -0
  92. package/tests/handlers/tool-handlers.test.ts +497 -0
  93. package/tests/handlers/vector-tool-handlers.test.ts +238 -0
  94. package/tests/server/mcp-server.bench.ts +55 -0
  95. package/tests/server/mcp-server.test.ts +675 -0
  96. package/tests/utils/logger.test.ts +180 -0
  97. package/tests/utils/mcp-logger.test.ts +212 -0
  98. package/tests/utils/progress-utils.test.ts +156 -0
  99. package/tests/utils/security-utils.test.ts +82 -0
  100. package/tests/vector/vector-search-manager.test.ts +335 -0
  101. package/tests/vector/vector-search.bench.ts +53 -0
  102. package/.github/workflows/DOCKER_DEPLOYMENT_SETUP.md +0 -387
  103. package/.github/workflows/dependabot-auto-merge.yml +0 -42
@@ -0,0 +1,556 @@
1
+ /**
2
+ * GitHub Tool Handler Tests
3
+ *
4
+ * Tests GitHub-dependent tools using a mock GitHubIntegration object
5
+ * passed to callTool().
6
+ */
7
+
8
+ import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest'
9
+ import { callTool } from '../../src/handlers/tools/index.js'
10
+ import { SqliteAdapter } from '../../src/database/SqliteAdapter.js'
11
+ import type { GitHubIntegration } from '../../src/github/GitHubIntegration.js'
12
+
13
+ /**
14
+ * Creates a mock GitHubIntegration with controllable method responses.
15
+ */
16
+ function createMockGitHub(overrides: Partial<Record<string, unknown>> = {}): GitHubIntegration {
17
+ const defaults = {
18
+ isApiAvailable: vi.fn().mockReturnValue(true),
19
+ getRepoInfo: vi.fn().mockResolvedValue({
20
+ owner: 'testowner',
21
+ repo: 'testrepo',
22
+ branch: 'main',
23
+ remoteUrl: 'git@github.com:testowner/testrepo.git',
24
+ }),
25
+ getCachedRepoInfo: vi.fn().mockReturnValue({
26
+ owner: 'testowner',
27
+ repo: 'testrepo',
28
+ branch: 'main',
29
+ remoteUrl: 'git@github.com:testowner/testrepo.git',
30
+ }),
31
+ getRepoContext: vi.fn().mockResolvedValue({
32
+ repoName: 'testrepo',
33
+ branch: 'main',
34
+ commit: 'abc1234',
35
+ remoteUrl: 'url',
36
+ projects: [],
37
+ issues: [],
38
+ pullRequests: [],
39
+ workflowRuns: [],
40
+ milestones: [],
41
+ }),
42
+ getIssues: vi
43
+ .fn()
44
+ .mockResolvedValue([
45
+ { number: 1, title: 'Test Issue', url: 'url1', state: 'OPEN', milestone: null },
46
+ ]),
47
+ getIssue: vi.fn().mockResolvedValue({
48
+ number: 1,
49
+ title: 'Test Issue',
50
+ url: 'url1',
51
+ state: 'OPEN',
52
+ body: 'Issue body',
53
+ labels: ['bug'],
54
+ assignees: ['dev1'],
55
+ createdAt: '2025-01-01T00:00:00Z',
56
+ updatedAt: '2025-01-02T00:00:00Z',
57
+ closedAt: null,
58
+ commentsCount: 3,
59
+ milestone: null,
60
+ }),
61
+ createIssue: vi.fn().mockResolvedValue({
62
+ number: 42,
63
+ url: 'https://github.com/testowner/testrepo/issues/42',
64
+ title: 'New Issue',
65
+ nodeId: 'NODE_42',
66
+ }),
67
+ closeIssue: vi.fn().mockResolvedValue({
68
+ success: true,
69
+ url: 'https://github.com/testowner/testrepo/issues/1',
70
+ }),
71
+ getPullRequests: vi
72
+ .fn()
73
+ .mockResolvedValue([{ number: 10, title: 'Feature', url: 'url10', state: 'OPEN' }]),
74
+ getPullRequest: vi.fn().mockResolvedValue({
75
+ number: 10,
76
+ title: 'Feature',
77
+ url: 'url10',
78
+ state: 'OPEN',
79
+ body: 'PR body',
80
+ draft: false,
81
+ headBranch: 'feature',
82
+ baseBranch: 'main',
83
+ author: 'dev1',
84
+ createdAt: '2025-01-01T00:00:00Z',
85
+ updatedAt: '2025-01-02T00:00:00Z',
86
+ mergedAt: null,
87
+ closedAt: null,
88
+ additions: 100,
89
+ deletions: 50,
90
+ changedFiles: 5,
91
+ }),
92
+ getWorkflowRuns: vi.fn().mockResolvedValue([]),
93
+ getProjectKanban: vi.fn().mockResolvedValue(null),
94
+ getMilestones: vi.fn().mockResolvedValue([
95
+ {
96
+ number: 1,
97
+ title: 'v1.0',
98
+ description: 'First',
99
+ state: 'open',
100
+ url: 'url',
101
+ dueOn: null,
102
+ openIssues: 5,
103
+ closedIssues: 10,
104
+ createdAt: '2025-01-01T00:00:00Z',
105
+ updatedAt: '2025-01-01T00:00:00Z',
106
+ creator: 'owner1',
107
+ },
108
+ ]),
109
+ getMilestone: vi.fn().mockResolvedValue({
110
+ number: 1,
111
+ title: 'v1.0',
112
+ description: 'First',
113
+ state: 'open',
114
+ url: 'url',
115
+ dueOn: null,
116
+ openIssues: 5,
117
+ closedIssues: 10,
118
+ createdAt: '2025-01-01T00:00:00Z',
119
+ updatedAt: '2025-01-01T00:00:00Z',
120
+ creator: 'owner1',
121
+ }),
122
+ createMilestone: vi.fn().mockResolvedValue({
123
+ number: 3,
124
+ title: 'v2.0',
125
+ description: null,
126
+ state: 'open',
127
+ url: 'url',
128
+ dueOn: null,
129
+ openIssues: 0,
130
+ closedIssues: 0,
131
+ createdAt: '2025-01-01T00:00:00Z',
132
+ updatedAt: '2025-01-01T00:00:00Z',
133
+ creator: 'dev1',
134
+ }),
135
+ updateMilestone: vi.fn().mockResolvedValue({
136
+ number: 1,
137
+ title: 'v1.1',
138
+ description: null,
139
+ state: 'closed',
140
+ url: 'url',
141
+ dueOn: null,
142
+ openIssues: 0,
143
+ closedIssues: 15,
144
+ createdAt: '2025-01-01T00:00:00Z',
145
+ updatedAt: '2025-02-01T00:00:00Z',
146
+ creator: 'dev1',
147
+ }),
148
+ deleteMilestone: vi.fn().mockResolvedValue({ success: true }),
149
+ moveProjectItem: vi.fn().mockResolvedValue({ success: true }),
150
+ addProjectItem: vi.fn().mockResolvedValue({ success: true, itemId: 'PVTITEM_NEW' }),
151
+ clearCache: vi.fn(),
152
+ invalidateCache: vi.fn(),
153
+ }
154
+ return { ...defaults, ...overrides } as unknown as GitHubIntegration
155
+ }
156
+
157
+ describe('GitHub Tool Handlers', () => {
158
+ let db: SqliteAdapter
159
+ const testDbPath = './test-gh-tools.db'
160
+
161
+ beforeAll(async () => {
162
+ db = new SqliteAdapter(testDbPath)
163
+ await db.initialize()
164
+ })
165
+
166
+ afterAll(() => {
167
+ db.close()
168
+ try {
169
+ const fs = require('node:fs')
170
+ if (fs.existsSync(testDbPath)) fs.unlinkSync(testDbPath)
171
+ } catch {
172
+ // Ignore cleanup errors
173
+ }
174
+ })
175
+
176
+ // ========================================================================
177
+ // Issues
178
+ // ========================================================================
179
+
180
+ describe('get_github_issues', () => {
181
+ it('should return issues from mock', async () => {
182
+ const github = createMockGitHub()
183
+ const result = (await callTool('get_github_issues', {}, db, undefined, github)) as {
184
+ issues: unknown[]
185
+ count: number
186
+ }
187
+
188
+ expect(result.issues).toBeDefined()
189
+ expect(result.count).toBe(1)
190
+ })
191
+
192
+ it('should return error when no github', async () => {
193
+ const result = (await callTool('get_github_issues', {}, db, undefined, undefined)) as {
194
+ error: string
195
+ }
196
+
197
+ expect(result.error).toContain('GitHub integration not available')
198
+ })
199
+ })
200
+
201
+ describe('get_github_issue', () => {
202
+ it('should return issue details', async () => {
203
+ const github = createMockGitHub()
204
+ const result = (await callTool(
205
+ 'get_github_issue',
206
+ { issue_number: 1 },
207
+ db,
208
+ undefined,
209
+ github
210
+ )) as { issue: { number: number }; journalEntries?: unknown[] }
211
+
212
+ expect(result.issue.number).toBe(1)
213
+ })
214
+ })
215
+
216
+ describe('get_github_prs', () => {
217
+ it('should return pull requests', async () => {
218
+ const github = createMockGitHub()
219
+ const result = (await callTool('get_github_prs', {}, db, undefined, github)) as {
220
+ pullRequests: unknown[]
221
+ count: number
222
+ }
223
+
224
+ expect(result.pullRequests).toBeDefined()
225
+ expect(result.count).toBe(1)
226
+ })
227
+ })
228
+
229
+ describe('get_github_pr', () => {
230
+ it('should return PR details', async () => {
231
+ const github = createMockGitHub()
232
+ const result = (await callTool(
233
+ 'get_github_pr',
234
+ { pr_number: 10 },
235
+ db,
236
+ undefined,
237
+ github
238
+ )) as { pullRequest: { number: number } }
239
+
240
+ expect(result.pullRequest.number).toBe(10)
241
+ })
242
+ })
243
+
244
+ // ========================================================================
245
+ // Context
246
+ // ========================================================================
247
+
248
+ describe('get_github_context', () => {
249
+ it('should return repo context', async () => {
250
+ const github = createMockGitHub()
251
+ const result = (await callTool('get_github_context', {}, db, undefined, github)) as {
252
+ repoName: string
253
+ branch: string
254
+ }
255
+
256
+ expect(result.repoName).toBe('testrepo')
257
+ })
258
+
259
+ it('should return error when no github', async () => {
260
+ const result = (await callTool('get_github_context', {}, db, undefined, undefined)) as {
261
+ error: string
262
+ }
263
+
264
+ expect(result.error).toContain('GitHub integration not available')
265
+ })
266
+ })
267
+
268
+ // ========================================================================
269
+ // Kanban
270
+ // ========================================================================
271
+
272
+ describe('get_kanban_board', () => {
273
+ it('should return error when project not found', async () => {
274
+ const github = createMockGitHub()
275
+ const result = (await callTool(
276
+ 'get_kanban_board',
277
+ { project_number: 1 },
278
+ db,
279
+ undefined,
280
+ github
281
+ )) as { error: string }
282
+
283
+ // getProjectKanban returns null by default
284
+ expect(result.error).toContain('not found')
285
+ })
286
+ })
287
+
288
+ // ========================================================================
289
+ // Milestones
290
+ // ========================================================================
291
+
292
+ describe('get_github_milestones', () => {
293
+ it('should return milestones', async () => {
294
+ const github = createMockGitHub()
295
+ const result = (await callTool('get_github_milestones', {}, db, undefined, github)) as {
296
+ milestones: unknown[]
297
+ count: number
298
+ }
299
+
300
+ expect(result.milestones).toBeDefined()
301
+ expect(result.count).toBe(1)
302
+ })
303
+ })
304
+
305
+ describe('get_github_milestone', () => {
306
+ it('should return single milestone', async () => {
307
+ const github = createMockGitHub()
308
+ const result = (await callTool(
309
+ 'get_github_milestone',
310
+ { milestone_number: 1 },
311
+ db,
312
+ undefined,
313
+ github
314
+ )) as { milestone: { number: number } }
315
+
316
+ expect(result.milestone.number).toBe(1)
317
+ })
318
+ })
319
+
320
+ describe('create_github_milestone', () => {
321
+ it('should create milestone', async () => {
322
+ const github = createMockGitHub()
323
+ const result = (await callTool(
324
+ 'create_github_milestone',
325
+ { title: 'v2.0' },
326
+ db,
327
+ undefined,
328
+ github
329
+ )) as { success: boolean; milestone: { number: number } }
330
+
331
+ expect(result.success).toBe(true)
332
+ expect(result.milestone.number).toBe(3)
333
+ })
334
+ })
335
+
336
+ describe('update_github_milestone', () => {
337
+ it('should update milestone', async () => {
338
+ const github = createMockGitHub()
339
+ const result = (await callTool(
340
+ 'update_github_milestone',
341
+ { milestone_number: 1, title: 'v1.1' },
342
+ db,
343
+ undefined,
344
+ github
345
+ )) as { success: boolean; milestone: { title: string } }
346
+
347
+ expect(result.success).toBe(true)
348
+ expect(result.milestone.title).toBe('v1.1')
349
+ })
350
+ })
351
+
352
+ describe('delete_github_milestone', () => {
353
+ it('should delete milestone', async () => {
354
+ const github = createMockGitHub()
355
+ const result = (await callTool(
356
+ 'delete_github_milestone',
357
+ { milestone_number: 1, confirm: true },
358
+ db,
359
+ undefined,
360
+ github
361
+ )) as { success: boolean }
362
+
363
+ expect(result.success).toBe(true)
364
+ })
365
+ })
366
+
367
+ // ========================================================================
368
+ // Issue with entry tools
369
+ // ========================================================================
370
+
371
+ describe('create_github_issue_with_entry', () => {
372
+ it('should create issue and journal entry', async () => {
373
+ const github = createMockGitHub()
374
+ const result = (await callTool(
375
+ 'create_github_issue_with_entry',
376
+ { title: 'Test Issue', journal_content: 'Created test issue for tracking' },
377
+ db,
378
+ undefined,
379
+ github
380
+ )) as { success: boolean; issue: { number: number }; journalEntry: { id: number } }
381
+
382
+ expect(result.success).toBe(true)
383
+ expect(result.issue.number).toBe(42)
384
+ expect(result.journalEntry.id).toBeGreaterThan(0)
385
+ })
386
+ })
387
+
388
+ // ========================================================================
389
+ // Kanban operations
390
+ // ========================================================================
391
+
392
+ describe('move_kanban_item', () => {
393
+ it('should move item when board and status found', async () => {
394
+ const github = createMockGitHub({
395
+ getProjectKanban: vi.fn().mockResolvedValue({
396
+ projectId: 'PVT_1',
397
+ projectTitle: 'Board',
398
+ statusFieldId: 'FIELD_1',
399
+ statusOptions: [
400
+ { id: 'OPT_TODO', name: 'Todo' },
401
+ { id: 'OPT_DONE', name: 'Done' },
402
+ ],
403
+ columns: [],
404
+ totalItems: 0,
405
+ }),
406
+ })
407
+
408
+ const result = (await callTool(
409
+ 'move_kanban_item',
410
+ {
411
+ project_number: 1,
412
+ item_id: 'PVTITEM_1',
413
+ target_status: 'Done',
414
+ },
415
+ db,
416
+ undefined,
417
+ github
418
+ )) as { success: boolean; newStatus: string }
419
+
420
+ expect(result.success).toBe(true)
421
+ expect(result.newStatus).toBe('Done')
422
+ })
423
+
424
+ it('should return error when status not found', async () => {
425
+ const github = createMockGitHub({
426
+ getProjectKanban: vi.fn().mockResolvedValue({
427
+ projectId: 'PVT_1',
428
+ statusFieldId: 'FIELD_1',
429
+ statusOptions: [{ id: 'OPT_TODO', name: 'Todo' }],
430
+ columns: [],
431
+ totalItems: 0,
432
+ }),
433
+ })
434
+
435
+ const result = (await callTool(
436
+ 'move_kanban_item',
437
+ {
438
+ project_number: 1,
439
+ item_id: 'PVTITEM_1',
440
+ target_status: 'Nonexistent',
441
+ },
442
+ db,
443
+ undefined,
444
+ github
445
+ )) as { error: string; availableStatuses: string[] }
446
+
447
+ expect(result.error).toContain('not found')
448
+ expect(result.availableStatuses).toEqual(['Todo'])
449
+ })
450
+
451
+ it('should return error when no github', async () => {
452
+ const result = (await callTool(
453
+ 'move_kanban_item',
454
+ {
455
+ project_number: 1,
456
+ item_id: 'X',
457
+ target_status: 'Done',
458
+ },
459
+ db
460
+ )) as { error: string }
461
+
462
+ expect(result.error).toContain('GitHub integration not available')
463
+ })
464
+
465
+ it('should return error when project not found', async () => {
466
+ const github = createMockGitHub()
467
+ const result = (await callTool(
468
+ 'move_kanban_item',
469
+ {
470
+ project_number: 999,
471
+ item_id: 'X',
472
+ target_status: 'Done',
473
+ },
474
+ db,
475
+ undefined,
476
+ github
477
+ )) as { error: string }
478
+
479
+ expect(result.error).toContain('not found')
480
+ })
481
+ })
482
+
483
+ // ========================================================================
484
+ // Close issue with entry
485
+ // ========================================================================
486
+
487
+ describe('close_github_issue_with_entry', () => {
488
+ it('should close issue and create journal entry', async () => {
489
+ const github = createMockGitHub()
490
+
491
+ const result = (await callTool(
492
+ 'close_github_issue_with_entry',
493
+ { issue_number: 1, resolution_notes: 'Fixed the bug' },
494
+ db,
495
+ undefined,
496
+ github
497
+ )) as {
498
+ success: boolean
499
+ issue: { number: number; newState: string }
500
+ journalEntry: { id: number }
501
+ }
502
+
503
+ expect(result.success).toBe(true)
504
+ expect(result.issue.number).toBe(1)
505
+ expect(result.issue.newState).toBe('CLOSED')
506
+ expect(result.journalEntry.id).toBeGreaterThan(0)
507
+ })
508
+
509
+ it('should return error when issue not found', async () => {
510
+ const github = createMockGitHub({
511
+ getIssue: vi.fn().mockResolvedValue(null),
512
+ })
513
+
514
+ const result = (await callTool(
515
+ 'close_github_issue_with_entry',
516
+ { issue_number: 999 },
517
+ db,
518
+ undefined,
519
+ github
520
+ )) as { error: string }
521
+
522
+ expect(result.error).toContain('not found')
523
+ })
524
+
525
+ it('should return error when issue already closed', async () => {
526
+ const github = createMockGitHub({
527
+ getIssue: vi.fn().mockResolvedValue({
528
+ number: 1,
529
+ title: 'Test',
530
+ url: 'url',
531
+ state: 'CLOSED',
532
+ }),
533
+ })
534
+
535
+ const result = (await callTool(
536
+ 'close_github_issue_with_entry',
537
+ { issue_number: 1 },
538
+ db,
539
+ undefined,
540
+ github
541
+ )) as { error: string }
542
+
543
+ expect(result.error).toContain('already closed')
544
+ })
545
+
546
+ it('should return error when no github', async () => {
547
+ const result = (await callTool(
548
+ 'close_github_issue_with_entry',
549
+ { issue_number: 1 },
550
+ db
551
+ )) as { error: string }
552
+
553
+ expect(result.error).toContain('GitHub integration not available')
554
+ })
555
+ })
556
+ })
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Prompt Handler Tests
3
+ *
4
+ * Tests the prompt listing and execution handlers.
5
+ */
6
+
7
+ import { describe, it, expect, beforeAll, afterAll } from 'vitest'
8
+ import { getPrompts, getPrompt } from '../../src/handlers/prompts/index.js'
9
+ import { SqliteAdapter } from '../../src/database/SqliteAdapter.js'
10
+
11
+ describe('Prompt Handlers', () => {
12
+ let db: SqliteAdapter
13
+ const testDbPath = './test-prompts.db'
14
+
15
+ beforeAll(async () => {
16
+ db = new SqliteAdapter(testDbPath)
17
+ await db.initialize()
18
+ // Seed some data for prompts that read from the DB
19
+ db.createEntry({ content: 'Test entry for prompts', tags: ['test-tag'] })
20
+ })
21
+
22
+ afterAll(() => {
23
+ db.close()
24
+ try {
25
+ const fs = require('node:fs')
26
+ if (fs.existsSync(testDbPath)) {
27
+ fs.unlinkSync(testDbPath)
28
+ }
29
+ } catch {
30
+ // Ignore cleanup errors
31
+ }
32
+ })
33
+
34
+ // ========================================================================
35
+ // getPrompts
36
+ // ========================================================================
37
+
38
+ describe('getPrompts', () => {
39
+ it('should return an array of prompt definitions', () => {
40
+ const prompts = getPrompts()
41
+ expect(Array.isArray(prompts)).toBe(true)
42
+ expect(prompts.length).toBeGreaterThan(0)
43
+ })
44
+
45
+ it('should have name and description on each prompt', () => {
46
+ const prompts = getPrompts()
47
+ for (const prompt of prompts) {
48
+ const p = prompt as { name: string; description: string }
49
+ expect(typeof p.name).toBe('string')
50
+ expect(p.name.length).toBeGreaterThan(0)
51
+ expect(typeof p.description).toBe('string')
52
+ }
53
+ })
54
+
55
+ it('should include known prompt names', () => {
56
+ const prompts = getPrompts()
57
+ const names = prompts.map((p) => (p as { name: string }).name)
58
+
59
+ // At least some expected prompts should be present
60
+ expect(names.length).toBeGreaterThan(5)
61
+ })
62
+ })
63
+
64
+ // ========================================================================
65
+ // getPrompt
66
+ // ========================================================================
67
+
68
+ describe('getPrompt', () => {
69
+ it('should return messages for each known prompt', () => {
70
+ const prompts = getPrompts()
71
+ const names = prompts.map((p) => (p as { name: string }).name)
72
+
73
+ for (const name of names) {
74
+ const result = getPrompt(name, {}, db)
75
+ expect(result.messages).toBeDefined()
76
+ expect(Array.isArray(result.messages)).toBe(true)
77
+ expect(result.messages.length).toBeGreaterThan(0)
78
+
79
+ // Each message should have role and content
80
+ for (const msg of result.messages) {
81
+ expect(msg.role).toBeDefined()
82
+ expect(msg.content).toBeDefined()
83
+ }
84
+ }
85
+ })
86
+
87
+ it('should throw for unknown prompt name', () => {
88
+ expect(() => getPrompt('nonexistent_prompt_xyz', {}, db)).toThrow()
89
+ })
90
+ })
91
+ })