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.
- 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 +12 -11
- package/README.md +68 -33
- package/SECURITY.md +225 -158
- 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 +140 -31
- 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 +590 -7
- 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/security-utils.d.ts +8 -0
- package/dist/utils/security-utils.d.ts.map +1 -1
- package/dist/utils/security-utils.js +32 -0
- package/dist/utils/security-utils.js.map +1 -1
- 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 +12 -11
- 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 +162 -32
- 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 +681 -12
- package/src/server/McpServer.ts +79 -37
- package/src/types/index.ts +98 -0
- package/src/utils/logger.ts +10 -1
- package/src/utils/security-utils.ts +35 -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/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/.github/workflows/DOCKER_DEPLOYMENT_SETUP.md +0 -387
- 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
|
+
})
|