@andypai/agent-kanban 0.2.0 → 0.3.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/README.md +89 -22
- package/package.json +4 -2
- package/src/__tests__/activity.test.ts +15 -9
- package/src/__tests__/api.test.ts +96 -0
- package/src/__tests__/board-utils.test.ts +100 -0
- package/src/__tests__/commands/board.test.ts +6 -13
- package/src/__tests__/conflict.test.ts +64 -0
- package/src/__tests__/index.test.ts +233 -56
- package/src/__tests__/jira-adf.test.ts +168 -0
- package/src/__tests__/jira-cache.test.ts +304 -0
- package/src/__tests__/jira-client.test.ts +169 -0
- package/src/__tests__/jira-provider-comment.test.ts +281 -0
- package/src/__tests__/jira-provider-mutations.test.ts +771 -0
- package/src/__tests__/jira-provider-read.test.ts +594 -0
- package/src/__tests__/jira-wiring.test.ts +187 -0
- package/src/__tests__/linear-cache-description-activity.test.ts +142 -0
- package/src/__tests__/linear-provider-comment.test.ts +243 -0
- package/src/__tests__/linear-provider-sync.test.ts +493 -0
- package/src/__tests__/local-provider-comment.test.ts +60 -0
- package/src/__tests__/mcp-core.test.ts +164 -0
- package/src/__tests__/mcp-server.test.ts +252 -0
- package/src/__tests__/server.test.ts +298 -0
- package/src/__tests__/webhooks.test.ts +604 -0
- package/src/activity.ts +1 -11
- package/src/api.ts +154 -19
- package/src/commands/board.ts +1 -11
- package/src/commands/mcp.ts +87 -0
- package/src/db.ts +115 -3
- package/src/errors.ts +2 -0
- package/src/id.ts +1 -1
- package/src/index.ts +72 -18
- package/src/mcp/core.ts +193 -0
- package/src/mcp/errors.ts +109 -0
- package/src/mcp/index.ts +13 -0
- package/src/mcp/server.ts +512 -0
- package/src/mcp/types.ts +72 -0
- package/src/providers/capabilities.ts +15 -0
- package/src/providers/index.ts +31 -1
- package/src/providers/jira-adf.ts +275 -0
- package/src/providers/jira-cache.ts +625 -0
- package/src/providers/jira-client.ts +390 -0
- package/src/providers/jira.ts +778 -0
- package/src/providers/linear-cache.ts +249 -70
- package/src/providers/linear-client.ts +256 -13
- package/src/providers/linear.ts +337 -14
- package/src/providers/local.ts +68 -17
- package/src/providers/types.ts +18 -2
- package/src/server.ts +139 -11
- package/src/tunnel.ts +79 -0
- package/src/types.ts +18 -2
- package/src/webhooks.ts +36 -0
- package/ui/dist/assets/index-DBnoKL_k.css +1 -0
- package/ui/dist/assets/index-qNVJ6clH.js +40 -0
- package/ui/dist/index.html +2 -2
- package/src/__tests__/commands/task.test.ts +0 -144
- package/src/commands/task.ts +0 -117
- package/src/fixtures.ts +0 -128
- package/ui/dist/assets/index-B8f9NB4z.css +0 -1
- package/ui/dist/assets/index-zWp-rB7b.js +0 -40
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
|
|
2
|
+
import { Database } from 'bun:sqlite'
|
|
3
|
+
import { JiraClient } from '../providers/jira-client.ts'
|
|
4
|
+
import { JiraProvider, type JiraProviderConfig } from '../providers/jira.ts'
|
|
5
|
+
import {
|
|
6
|
+
initJiraCacheSchema,
|
|
7
|
+
saveJiraSyncMeta,
|
|
8
|
+
saveTeamInfo,
|
|
9
|
+
upsertJiraIssues,
|
|
10
|
+
} from '../providers/jira-cache.ts'
|
|
11
|
+
|
|
12
|
+
const baseConfig: JiraProviderConfig = {
|
|
13
|
+
baseUrl: 'https://example.atlassian.net',
|
|
14
|
+
email: 'user@example.com',
|
|
15
|
+
apiToken: 'token',
|
|
16
|
+
projectKey: 'ENG',
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let db: Database
|
|
20
|
+
let originalFetch: typeof fetch
|
|
21
|
+
let requests: Array<{ url: string; init?: RequestInit }>
|
|
22
|
+
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
db = new Database(':memory:')
|
|
25
|
+
initJiraCacheSchema(db)
|
|
26
|
+
saveJiraSyncMeta(db, {
|
|
27
|
+
projectKey: 'ENG',
|
|
28
|
+
boardId: null,
|
|
29
|
+
lastSyncAt: new Date().toISOString(),
|
|
30
|
+
lastIssueUpdatedAt: '2026-01-02T00:00:00Z',
|
|
31
|
+
})
|
|
32
|
+
saveTeamInfo(db, { id: '10000', key: 'ENG', name: 'Engineering' })
|
|
33
|
+
upsertJiraIssues(db, [
|
|
34
|
+
{
|
|
35
|
+
id: '10001',
|
|
36
|
+
key: 'ENG-1',
|
|
37
|
+
summary: 'Issue 1',
|
|
38
|
+
descriptionText: '',
|
|
39
|
+
statusId: '10000',
|
|
40
|
+
priorityName: 'High',
|
|
41
|
+
issueTypeName: 'Task',
|
|
42
|
+
assigneeAccountId: null,
|
|
43
|
+
assigneeName: '',
|
|
44
|
+
labels: [],
|
|
45
|
+
commentCount: 0,
|
|
46
|
+
projectKey: 'ENG',
|
|
47
|
+
url: null,
|
|
48
|
+
createdAt: '2026-01-01T00:00:00Z',
|
|
49
|
+
updatedAt: '2026-01-02T00:00:00Z',
|
|
50
|
+
},
|
|
51
|
+
])
|
|
52
|
+
originalFetch = globalThis.fetch
|
|
53
|
+
requests = []
|
|
54
|
+
globalThis.fetch = (async (input: string | URL | Request, init?: RequestInit) => {
|
|
55
|
+
const request = {
|
|
56
|
+
url: typeof input === 'string' ? input : input instanceof URL ? input.toString() : input.url,
|
|
57
|
+
init,
|
|
58
|
+
}
|
|
59
|
+
requests.push(request)
|
|
60
|
+
|
|
61
|
+
if (init?.method === 'POST') {
|
|
62
|
+
return new Response(
|
|
63
|
+
JSON.stringify({
|
|
64
|
+
id: 'comment-1',
|
|
65
|
+
body: {
|
|
66
|
+
type: 'doc',
|
|
67
|
+
content: [{ type: 'paragraph', content: [{ type: 'text', text: 'hello from jira' }] }],
|
|
68
|
+
},
|
|
69
|
+
created: '2026-01-03T00:00:00Z',
|
|
70
|
+
updated: '2026-01-03T00:00:00Z',
|
|
71
|
+
author: { displayName: 'Jira User' },
|
|
72
|
+
}),
|
|
73
|
+
{
|
|
74
|
+
status: 201,
|
|
75
|
+
headers: { 'content-type': 'application/json' },
|
|
76
|
+
},
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (init?.method === 'GET' && /\/comment\/[^/?]+$/.test(request.url)) {
|
|
81
|
+
return new Response(
|
|
82
|
+
JSON.stringify({
|
|
83
|
+
id: 'comment-1',
|
|
84
|
+
body: {
|
|
85
|
+
type: 'doc',
|
|
86
|
+
content: [{ type: 'paragraph', content: [{ type: 'text', text: 'one jira comment' }] }],
|
|
87
|
+
},
|
|
88
|
+
created: '2026-01-03T00:00:00Z',
|
|
89
|
+
updated: '2026-01-05T00:00:00Z',
|
|
90
|
+
author: { displayName: 'Jira User' },
|
|
91
|
+
}),
|
|
92
|
+
{
|
|
93
|
+
status: 200,
|
|
94
|
+
headers: { 'content-type': 'application/json' },
|
|
95
|
+
},
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (init?.method === 'GET') {
|
|
100
|
+
return new Response(
|
|
101
|
+
JSON.stringify({
|
|
102
|
+
startAt: 0,
|
|
103
|
+
maxResults: 100,
|
|
104
|
+
total: 2,
|
|
105
|
+
comments: [
|
|
106
|
+
{
|
|
107
|
+
id: 'comment-1',
|
|
108
|
+
body: {
|
|
109
|
+
type: 'doc',
|
|
110
|
+
content: [
|
|
111
|
+
{ type: 'paragraph', content: [{ type: 'text', text: 'first jira comment' }] },
|
|
112
|
+
],
|
|
113
|
+
},
|
|
114
|
+
created: '2026-01-03T00:00:00Z',
|
|
115
|
+
updated: '2026-01-03T00:00:00Z',
|
|
116
|
+
author: { displayName: 'Jira User' },
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
id: 'comment-2',
|
|
120
|
+
body: {
|
|
121
|
+
type: 'doc',
|
|
122
|
+
content: [
|
|
123
|
+
{ type: 'paragraph', content: [{ type: 'text', text: 'second jira comment' }] },
|
|
124
|
+
],
|
|
125
|
+
},
|
|
126
|
+
created: '2026-01-04T00:00:00Z',
|
|
127
|
+
updated: '2026-01-04T00:00:00Z',
|
|
128
|
+
author: { displayName: 'Reviewer' },
|
|
129
|
+
},
|
|
130
|
+
],
|
|
131
|
+
}),
|
|
132
|
+
{
|
|
133
|
+
status: 200,
|
|
134
|
+
headers: { 'content-type': 'application/json' },
|
|
135
|
+
},
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (init?.method === 'PUT') {
|
|
140
|
+
return new Response(
|
|
141
|
+
JSON.stringify({
|
|
142
|
+
id: 'comment-1',
|
|
143
|
+
body: {
|
|
144
|
+
type: 'doc',
|
|
145
|
+
content: [
|
|
146
|
+
{ type: 'paragraph', content: [{ type: 'text', text: 'edited jira comment' }] },
|
|
147
|
+
],
|
|
148
|
+
},
|
|
149
|
+
created: '2026-01-03T00:00:00Z',
|
|
150
|
+
updated: '2026-01-04T00:00:00Z',
|
|
151
|
+
author: { displayName: 'Jira User' },
|
|
152
|
+
}),
|
|
153
|
+
{
|
|
154
|
+
status: 200,
|
|
155
|
+
headers: { 'content-type': 'application/json' },
|
|
156
|
+
},
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (init?.method === 'DELETE') {
|
|
161
|
+
return new Response(null, { status: 204 })
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
throw new Error(`Unexpected Jira request: ${request.url}`)
|
|
165
|
+
}) as unknown as typeof fetch
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
afterEach(() => {
|
|
169
|
+
globalThis.fetch = originalFetch
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
describe('JiraProvider.comment', () => {
|
|
173
|
+
test('posts an ADF comment to the Jira issue endpoint and advertises comment capability', async () => {
|
|
174
|
+
const client = new JiraClient({
|
|
175
|
+
baseUrl: baseConfig.baseUrl,
|
|
176
|
+
email: baseConfig.email,
|
|
177
|
+
apiToken: baseConfig.apiToken,
|
|
178
|
+
})
|
|
179
|
+
const provider = new JiraProvider(db, baseConfig, client)
|
|
180
|
+
|
|
181
|
+
const comment = await provider.comment('ENG-1', 'hello from jira')
|
|
182
|
+
|
|
183
|
+
expect(requests[0]?.url).toBe('https://example.atlassian.net/rest/api/3/issue/ENG-1/comment')
|
|
184
|
+
expect(requests[0]?.init?.method).toBe('POST')
|
|
185
|
+
const body = JSON.parse(String(requests[0]?.init?.body)) as {
|
|
186
|
+
body: { type: string; content: unknown[] }
|
|
187
|
+
}
|
|
188
|
+
expect(body.body.type).toBe('doc')
|
|
189
|
+
expect(body.body.content).toEqual([
|
|
190
|
+
{ type: 'paragraph', content: [{ type: 'text', text: 'hello from jira' }] },
|
|
191
|
+
])
|
|
192
|
+
expect(comment).toMatchObject({
|
|
193
|
+
id: 'comment-1',
|
|
194
|
+
task_id: 'jira:10001',
|
|
195
|
+
body: 'hello from jira',
|
|
196
|
+
author: 'Jira User',
|
|
197
|
+
})
|
|
198
|
+
expect((await provider.getTask('ENG-1')).comment_count).toBe(1)
|
|
199
|
+
|
|
200
|
+
const context = await provider.getContext()
|
|
201
|
+
expect(context.capabilities.comment).toBe(true)
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
test('gets Jira issue comments and normalizes them into TaskComment rows', async () => {
|
|
205
|
+
const client = new JiraClient({
|
|
206
|
+
baseUrl: baseConfig.baseUrl,
|
|
207
|
+
email: baseConfig.email,
|
|
208
|
+
apiToken: baseConfig.apiToken,
|
|
209
|
+
})
|
|
210
|
+
const provider = new JiraProvider(db, baseConfig, client)
|
|
211
|
+
|
|
212
|
+
const comments = await provider.listComments('ENG-1')
|
|
213
|
+
|
|
214
|
+
expect(requests[0]?.url).toBe(
|
|
215
|
+
'https://example.atlassian.net/rest/api/3/issue/ENG-1/comment?startAt=0&maxResults=100',
|
|
216
|
+
)
|
|
217
|
+
expect(requests[0]?.init?.method).toBe('GET')
|
|
218
|
+
expect(comments).toEqual([
|
|
219
|
+
{
|
|
220
|
+
id: 'comment-1',
|
|
221
|
+
task_id: 'jira:10001',
|
|
222
|
+
body: 'first jira comment',
|
|
223
|
+
author: 'Jira User',
|
|
224
|
+
created_at: '2026-01-03T00:00:00Z',
|
|
225
|
+
updated_at: '2026-01-03T00:00:00Z',
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
id: 'comment-2',
|
|
229
|
+
task_id: 'jira:10001',
|
|
230
|
+
body: 'second jira comment',
|
|
231
|
+
author: 'Reviewer',
|
|
232
|
+
created_at: '2026-01-04T00:00:00Z',
|
|
233
|
+
updated_at: '2026-01-04T00:00:00Z',
|
|
234
|
+
},
|
|
235
|
+
])
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
test('gets a single Jira issue comment by id', async () => {
|
|
239
|
+
const client = new JiraClient({
|
|
240
|
+
baseUrl: baseConfig.baseUrl,
|
|
241
|
+
email: baseConfig.email,
|
|
242
|
+
apiToken: baseConfig.apiToken,
|
|
243
|
+
})
|
|
244
|
+
const provider = new JiraProvider(db, baseConfig, client)
|
|
245
|
+
|
|
246
|
+
const comment = await provider.getComment('ENG-1', 'comment-1')
|
|
247
|
+
|
|
248
|
+
expect(requests[0]?.url).toBe(
|
|
249
|
+
'https://example.atlassian.net/rest/api/3/issue/ENG-1/comment/comment-1',
|
|
250
|
+
)
|
|
251
|
+
expect(requests[0]?.init?.method).toBe('GET')
|
|
252
|
+
expect(comment).toMatchObject({
|
|
253
|
+
id: 'comment-1',
|
|
254
|
+
task_id: 'jira:10001',
|
|
255
|
+
body: 'one jira comment',
|
|
256
|
+
author: 'Jira User',
|
|
257
|
+
updated_at: '2026-01-05T00:00:00Z',
|
|
258
|
+
})
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
test('puts an updated ADF comment to the Jira issue comment endpoint', async () => {
|
|
262
|
+
const client = new JiraClient({
|
|
263
|
+
baseUrl: baseConfig.baseUrl,
|
|
264
|
+
email: baseConfig.email,
|
|
265
|
+
apiToken: baseConfig.apiToken,
|
|
266
|
+
})
|
|
267
|
+
const provider = new JiraProvider(db, baseConfig, client)
|
|
268
|
+
|
|
269
|
+
const comment = await provider.updateComment('ENG-1', 'comment-1', 'edited jira comment')
|
|
270
|
+
|
|
271
|
+
expect(requests[0]?.url).toBe(
|
|
272
|
+
'https://example.atlassian.net/rest/api/3/issue/ENG-1/comment/comment-1',
|
|
273
|
+
)
|
|
274
|
+
expect(requests[0]?.init?.method).toBe('PUT')
|
|
275
|
+
expect(comment).toMatchObject({
|
|
276
|
+
id: 'comment-1',
|
|
277
|
+
task_id: 'jira:10001',
|
|
278
|
+
body: 'edited jira comment',
|
|
279
|
+
})
|
|
280
|
+
})
|
|
281
|
+
})
|