@renseiai/agentfactory-linear 0.8.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/LICENSE +21 -0
- package/README.md +91 -0
- package/dist/src/agent-client-project-repo.test.d.ts +2 -0
- package/dist/src/agent-client-project-repo.test.d.ts.map +1 -0
- package/dist/src/agent-client-project-repo.test.js +153 -0
- package/dist/src/agent-client.d.ts +261 -0
- package/dist/src/agent-client.d.ts.map +1 -0
- package/dist/src/agent-client.js +902 -0
- package/dist/src/agent-session.d.ts +303 -0
- package/dist/src/agent-session.d.ts.map +1 -0
- package/dist/src/agent-session.js +969 -0
- package/dist/src/checkbox-utils.d.ts +88 -0
- package/dist/src/checkbox-utils.d.ts.map +1 -0
- package/dist/src/checkbox-utils.js +120 -0
- package/dist/src/circuit-breaker.d.ts +76 -0
- package/dist/src/circuit-breaker.d.ts.map +1 -0
- package/dist/src/circuit-breaker.js +229 -0
- package/dist/src/circuit-breaker.test.d.ts +2 -0
- package/dist/src/circuit-breaker.test.d.ts.map +1 -0
- package/dist/src/circuit-breaker.test.js +292 -0
- package/dist/src/constants.d.ts +87 -0
- package/dist/src/constants.d.ts.map +1 -0
- package/dist/src/constants.js +101 -0
- package/dist/src/defaults/auto-trigger.d.ts +35 -0
- package/dist/src/defaults/auto-trigger.d.ts.map +1 -0
- package/dist/src/defaults/auto-trigger.js +36 -0
- package/dist/src/defaults/index.d.ts +12 -0
- package/dist/src/defaults/index.d.ts.map +1 -0
- package/dist/src/defaults/index.js +11 -0
- package/dist/src/defaults/priority.d.ts +20 -0
- package/dist/src/defaults/priority.d.ts.map +1 -0
- package/dist/src/defaults/priority.js +37 -0
- package/dist/src/defaults/prompts.d.ts +42 -0
- package/dist/src/defaults/prompts.d.ts.map +1 -0
- package/dist/src/defaults/prompts.js +310 -0
- package/dist/src/defaults/prompts.test.d.ts +2 -0
- package/dist/src/defaults/prompts.test.d.ts.map +1 -0
- package/dist/src/defaults/prompts.test.js +263 -0
- package/dist/src/defaults/work-type-detection.d.ts +19 -0
- package/dist/src/defaults/work-type-detection.d.ts.map +1 -0
- package/dist/src/defaults/work-type-detection.js +93 -0
- package/dist/src/errors.d.ts +91 -0
- package/dist/src/errors.d.ts.map +1 -0
- package/dist/src/errors.js +173 -0
- package/dist/src/frontend-adapter.d.ts +168 -0
- package/dist/src/frontend-adapter.d.ts.map +1 -0
- package/dist/src/frontend-adapter.js +314 -0
- package/dist/src/frontend-adapter.test.d.ts +2 -0
- package/dist/src/frontend-adapter.test.d.ts.map +1 -0
- package/dist/src/frontend-adapter.test.js +545 -0
- package/dist/src/index.d.ts +28 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +30 -0
- package/dist/src/issue-tracker-proxy.d.ts +140 -0
- package/dist/src/issue-tracker-proxy.d.ts.map +1 -0
- package/dist/src/issue-tracker-proxy.js +10 -0
- package/dist/src/platform-adapter.d.ts +132 -0
- package/dist/src/platform-adapter.d.ts.map +1 -0
- package/dist/src/platform-adapter.js +260 -0
- package/dist/src/platform-adapter.test.d.ts +2 -0
- package/dist/src/platform-adapter.test.d.ts.map +1 -0
- package/dist/src/platform-adapter.test.js +468 -0
- package/dist/src/proxy-client.d.ts +103 -0
- package/dist/src/proxy-client.d.ts.map +1 -0
- package/dist/src/proxy-client.js +191 -0
- package/dist/src/rate-limiter.d.ts +64 -0
- package/dist/src/rate-limiter.d.ts.map +1 -0
- package/dist/src/rate-limiter.js +163 -0
- package/dist/src/rate-limiter.test.d.ts +2 -0
- package/dist/src/rate-limiter.test.d.ts.map +1 -0
- package/dist/src/rate-limiter.test.js +217 -0
- package/dist/src/retry.d.ts +59 -0
- package/dist/src/retry.d.ts.map +1 -0
- package/dist/src/retry.js +82 -0
- package/dist/src/types.d.ts +492 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +143 -0
- package/dist/src/utils.d.ts +52 -0
- package/dist/src/utils.d.ts.map +1 -0
- package/dist/src/utils.js +277 -0
- package/dist/src/webhook-types.d.ts +308 -0
- package/dist/src/webhook-types.d.ts.map +1 -0
- package/dist/src/webhook-types.js +46 -0
- package/package.json +70 -0
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { LinearPlatformAdapter } from './platform-adapter.js';
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Mock helpers
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
/**
|
|
7
|
+
* Create a mock Linear SDK Issue object (matching the pattern from frontend-adapter.test.ts).
|
|
8
|
+
*/
|
|
9
|
+
function mockLinearIssue(overrides = {}) {
|
|
10
|
+
const { id = 'issue-uuid-1', identifier = 'SUP-100', title = 'Test Issue', description = 'A test issue description', url = 'https://linear.app/team/issue/SUP-100', priority = 2, createdAt = new Date('2025-01-15T10:00:00Z'), stateName = 'Backlog', labels = ['Feature'], parentId = null, projectName = 'MyProject', } = overrides;
|
|
11
|
+
return {
|
|
12
|
+
id,
|
|
13
|
+
identifier,
|
|
14
|
+
title,
|
|
15
|
+
description,
|
|
16
|
+
url,
|
|
17
|
+
priority,
|
|
18
|
+
createdAt,
|
|
19
|
+
get state() {
|
|
20
|
+
return Promise.resolve(stateName ? { name: stateName } : null);
|
|
21
|
+
},
|
|
22
|
+
labels: () => Promise.resolve({ nodes: labels.map((name) => ({ name })) }),
|
|
23
|
+
get parent() {
|
|
24
|
+
return Promise.resolve(parentId ? { id: parentId } : null);
|
|
25
|
+
},
|
|
26
|
+
get project() {
|
|
27
|
+
return Promise.resolve(projectName ? { name: projectName } : null);
|
|
28
|
+
},
|
|
29
|
+
get team() {
|
|
30
|
+
return Promise.resolve({ id: 'team-1', name: 'Engineering' });
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Create a mock LinearAgentClient for the platform adapter.
|
|
36
|
+
*/
|
|
37
|
+
function createMockClient() {
|
|
38
|
+
const mocks = {
|
|
39
|
+
getIssue: vi.fn(),
|
|
40
|
+
updateIssue: vi.fn(),
|
|
41
|
+
getTeamStatuses: vi.fn(),
|
|
42
|
+
updateIssueStatus: vi.fn(),
|
|
43
|
+
createComment: vi.fn(),
|
|
44
|
+
getIssueComments: vi.fn(),
|
|
45
|
+
createIssue: vi.fn(),
|
|
46
|
+
isParentIssue: vi.fn(),
|
|
47
|
+
isChildIssue: vi.fn(),
|
|
48
|
+
getSubIssues: vi.fn(),
|
|
49
|
+
getIssueRelations: vi.fn(),
|
|
50
|
+
createIssueRelation: vi.fn(),
|
|
51
|
+
createAgentSessionOnIssue: vi.fn(),
|
|
52
|
+
updateAgentSession: vi.fn(),
|
|
53
|
+
createAgentActivity: vi.fn(),
|
|
54
|
+
listProjectIssues: vi.fn(),
|
|
55
|
+
};
|
|
56
|
+
const linearClient = {
|
|
57
|
+
issues: vi.fn(),
|
|
58
|
+
issueLabels: vi.fn(),
|
|
59
|
+
};
|
|
60
|
+
const client = {
|
|
61
|
+
...mocks,
|
|
62
|
+
linearClient,
|
|
63
|
+
};
|
|
64
|
+
mocks.linearClientIssues = linearClient.issues;
|
|
65
|
+
return { client, mocks };
|
|
66
|
+
}
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
// Webhook payload factories
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
function makeIssueUpdatePayload(overrides = {}) {
|
|
71
|
+
const { stateName = 'Started', hasStateChange = true, action = 'update', id = 'issue-uuid-1', identifier = 'SUP-100', title = 'Test Issue', description = 'Issue description', labels = [{ name: 'Feature' }], parentId, projectName = 'MyProject', createdAt = '2025-01-15T10:00:00.000Z', } = overrides;
|
|
72
|
+
return {
|
|
73
|
+
action,
|
|
74
|
+
type: 'Issue',
|
|
75
|
+
data: {
|
|
76
|
+
id,
|
|
77
|
+
identifier,
|
|
78
|
+
title,
|
|
79
|
+
description,
|
|
80
|
+
url: `https://linear.app/team/issue/${identifier}`,
|
|
81
|
+
state: { id: 'state-1', name: stateName, type: 'started' },
|
|
82
|
+
labels: labels.map((l) => ({ id: `label-${l.name}`, ...l })),
|
|
83
|
+
parent: parentId ? { id: parentId, identifier: 'SUP-99', title: 'Parent' } : undefined,
|
|
84
|
+
project: projectName ? { id: 'proj-1', name: projectName } : undefined,
|
|
85
|
+
createdAt,
|
|
86
|
+
},
|
|
87
|
+
updatedFrom: hasStateChange ? { stateId: 'old-state-id' } : {},
|
|
88
|
+
createdAt: '2025-01-15T12:00:00.000Z',
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function makeCommentPayload(overrides = {}) {
|
|
92
|
+
const { action = 'create', commentId = 'comment-uuid-1', body = 'This is a comment', issueId = 'issue-uuid-1', issueIdentifier = 'SUP-100', issueTitle = 'Test Issue', issueStateName = 'Started', userId = 'user-1', userName = 'Test User', } = overrides;
|
|
93
|
+
return {
|
|
94
|
+
action,
|
|
95
|
+
type: 'Comment',
|
|
96
|
+
data: {
|
|
97
|
+
id: commentId,
|
|
98
|
+
body,
|
|
99
|
+
issue: {
|
|
100
|
+
id: issueId,
|
|
101
|
+
identifier: issueIdentifier,
|
|
102
|
+
title: issueTitle,
|
|
103
|
+
url: `https://linear.app/team/issue/${issueIdentifier}`,
|
|
104
|
+
state: { id: 'state-1', name: issueStateName, type: 'started' },
|
|
105
|
+
labels: [{ id: 'label-1', name: 'Feature' }],
|
|
106
|
+
createdAt: '2025-01-15T10:00:00.000Z',
|
|
107
|
+
},
|
|
108
|
+
user: { id: userId, name: userName },
|
|
109
|
+
},
|
|
110
|
+
createdAt: '2025-01-15T12:00:00.000Z',
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
// Tests
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
describe('LinearPlatformAdapter', () => {
|
|
117
|
+
let adapter;
|
|
118
|
+
let mocks;
|
|
119
|
+
beforeEach(() => {
|
|
120
|
+
const { client, mocks: m } = createMockClient();
|
|
121
|
+
mocks = m;
|
|
122
|
+
adapter = new LinearPlatformAdapter(client);
|
|
123
|
+
});
|
|
124
|
+
// ========================================================================
|
|
125
|
+
// name
|
|
126
|
+
// ========================================================================
|
|
127
|
+
it('has name "linear"', () => {
|
|
128
|
+
expect(adapter.name).toBe('linear');
|
|
129
|
+
});
|
|
130
|
+
// ========================================================================
|
|
131
|
+
// normalizeWebhookEvent — Issue updates
|
|
132
|
+
// ========================================================================
|
|
133
|
+
describe('normalizeWebhookEvent — issue updates', () => {
|
|
134
|
+
it('returns IssueStatusChangedEvent for issue update with state change', () => {
|
|
135
|
+
const payload = makeIssueUpdatePayload({ stateName: 'Started' });
|
|
136
|
+
const events = adapter.normalizeWebhookEvent(payload);
|
|
137
|
+
expect(events).not.toBeNull();
|
|
138
|
+
expect(events).toHaveLength(1);
|
|
139
|
+
const event = events[0];
|
|
140
|
+
expect(event.type).toBe('issue-status-changed');
|
|
141
|
+
expect(event.issueId).toBe('issue-uuid-1');
|
|
142
|
+
expect(event.source).toBe('webhook');
|
|
143
|
+
if (event.type === 'issue-status-changed') {
|
|
144
|
+
expect(event.newStatus).toBe('Started');
|
|
145
|
+
expect(event.issue.id).toBe('issue-uuid-1');
|
|
146
|
+
expect(event.issue.identifier).toBe('SUP-100');
|
|
147
|
+
expect(event.issue.title).toBe('Test Issue');
|
|
148
|
+
expect(event.issue.status).toBe('Started');
|
|
149
|
+
expect(event.issue.labels).toEqual(['Feature']);
|
|
150
|
+
expect(event.issue.project).toBe('MyProject');
|
|
151
|
+
expect(event.timestamp).toBeTruthy();
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
it('returns null for issue update without state change', () => {
|
|
155
|
+
const payload = makeIssueUpdatePayload({ hasStateChange: false });
|
|
156
|
+
const events = adapter.normalizeWebhookEvent(payload);
|
|
157
|
+
expect(events).toBeNull();
|
|
158
|
+
});
|
|
159
|
+
it('returns null for issue create action', () => {
|
|
160
|
+
const payload = makeIssueUpdatePayload({ action: 'create' });
|
|
161
|
+
const events = adapter.normalizeWebhookEvent(payload);
|
|
162
|
+
expect(events).toBeNull();
|
|
163
|
+
});
|
|
164
|
+
it('returns null for issue remove action', () => {
|
|
165
|
+
const payload = makeIssueUpdatePayload({ action: 'remove' });
|
|
166
|
+
const events = adapter.normalizeWebhookEvent(payload);
|
|
167
|
+
expect(events).toBeNull();
|
|
168
|
+
});
|
|
169
|
+
it('includes parentId when issue has a parent', () => {
|
|
170
|
+
const payload = makeIssueUpdatePayload({ parentId: 'parent-uuid-1' });
|
|
171
|
+
const events = adapter.normalizeWebhookEvent(payload);
|
|
172
|
+
expect(events).not.toBeNull();
|
|
173
|
+
const event = events[0];
|
|
174
|
+
if (event.type === 'issue-status-changed') {
|
|
175
|
+
expect(event.issue.parentId).toBe('parent-uuid-1');
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
it('handles issue with no description', () => {
|
|
179
|
+
const payload = makeIssueUpdatePayload();
|
|
180
|
+
// Remove description from data
|
|
181
|
+
delete payload.data.description;
|
|
182
|
+
const events = adapter.normalizeWebhookEvent(payload);
|
|
183
|
+
expect(events).not.toBeNull();
|
|
184
|
+
const event = events[0];
|
|
185
|
+
if (event.type === 'issue-status-changed') {
|
|
186
|
+
expect(event.issue.description).toBeUndefined();
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
it('handles issue with no labels', () => {
|
|
190
|
+
const payload = makeIssueUpdatePayload({ labels: [] });
|
|
191
|
+
const events = adapter.normalizeWebhookEvent(payload);
|
|
192
|
+
expect(events).not.toBeNull();
|
|
193
|
+
const event = events[0];
|
|
194
|
+
if (event.type === 'issue-status-changed') {
|
|
195
|
+
expect(event.issue.labels).toEqual([]);
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
it('parses createdAt from webhook data into milliseconds', () => {
|
|
199
|
+
const payload = makeIssueUpdatePayload({ createdAt: '2025-06-01T00:00:00.000Z' });
|
|
200
|
+
const events = adapter.normalizeWebhookEvent(payload);
|
|
201
|
+
expect(events).not.toBeNull();
|
|
202
|
+
const event = events[0];
|
|
203
|
+
if (event.type === 'issue-status-changed') {
|
|
204
|
+
expect(event.issue.createdAt).toBe(new Date('2025-06-01T00:00:00.000Z').getTime());
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
// ========================================================================
|
|
209
|
+
// normalizeWebhookEvent — Comments
|
|
210
|
+
// ========================================================================
|
|
211
|
+
describe('normalizeWebhookEvent — comments', () => {
|
|
212
|
+
it('returns CommentAddedEvent for comment creation', () => {
|
|
213
|
+
const payload = makeCommentPayload();
|
|
214
|
+
const events = adapter.normalizeWebhookEvent(payload);
|
|
215
|
+
expect(events).not.toBeNull();
|
|
216
|
+
expect(events).toHaveLength(1);
|
|
217
|
+
const event = events[0];
|
|
218
|
+
expect(event.type).toBe('comment-added');
|
|
219
|
+
expect(event.issueId).toBe('issue-uuid-1');
|
|
220
|
+
expect(event.source).toBe('webhook');
|
|
221
|
+
if (event.type === 'comment-added') {
|
|
222
|
+
expect(event.commentId).toBe('comment-uuid-1');
|
|
223
|
+
expect(event.commentBody).toBe('This is a comment');
|
|
224
|
+
expect(event.userId).toBe('user-1');
|
|
225
|
+
expect(event.userName).toBe('Test User');
|
|
226
|
+
expect(event.issue.identifier).toBe('SUP-100');
|
|
227
|
+
expect(event.issue.status).toBe('Started');
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
it('returns null for comment update action', () => {
|
|
231
|
+
const payload = makeCommentPayload({ action: 'update' });
|
|
232
|
+
const events = adapter.normalizeWebhookEvent(payload);
|
|
233
|
+
expect(events).toBeNull();
|
|
234
|
+
});
|
|
235
|
+
it('returns null for comment remove action', () => {
|
|
236
|
+
const payload = makeCommentPayload({ action: 'remove' });
|
|
237
|
+
const events = adapter.normalizeWebhookEvent(payload);
|
|
238
|
+
expect(events).toBeNull();
|
|
239
|
+
});
|
|
240
|
+
it('handles comment without user', () => {
|
|
241
|
+
const payload = makeCommentPayload();
|
|
242
|
+
delete payload.data.user;
|
|
243
|
+
const events = adapter.normalizeWebhookEvent(payload);
|
|
244
|
+
expect(events).not.toBeNull();
|
|
245
|
+
const event = events[0];
|
|
246
|
+
if (event.type === 'comment-added') {
|
|
247
|
+
expect(event.userId).toBeUndefined();
|
|
248
|
+
expect(event.userName).toBeUndefined();
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
// ========================================================================
|
|
253
|
+
// normalizeWebhookEvent — Unrecognized payloads
|
|
254
|
+
// ========================================================================
|
|
255
|
+
describe('normalizeWebhookEvent — unrecognized payloads', () => {
|
|
256
|
+
it('returns null for null payload', () => {
|
|
257
|
+
expect(adapter.normalizeWebhookEvent(null)).toBeNull();
|
|
258
|
+
});
|
|
259
|
+
it('returns null for undefined payload', () => {
|
|
260
|
+
expect(adapter.normalizeWebhookEvent(undefined)).toBeNull();
|
|
261
|
+
});
|
|
262
|
+
it('returns null for non-object payload', () => {
|
|
263
|
+
expect(adapter.normalizeWebhookEvent('string')).toBeNull();
|
|
264
|
+
expect(adapter.normalizeWebhookEvent(42)).toBeNull();
|
|
265
|
+
});
|
|
266
|
+
it('returns null for AgentSessionEvent payloads', () => {
|
|
267
|
+
const payload = {
|
|
268
|
+
action: 'created',
|
|
269
|
+
type: 'AgentSessionEvent',
|
|
270
|
+
data: { id: 'session-1' },
|
|
271
|
+
};
|
|
272
|
+
expect(adapter.normalizeWebhookEvent(payload)).toBeNull();
|
|
273
|
+
});
|
|
274
|
+
it('returns null for unknown resource types', () => {
|
|
275
|
+
const payload = {
|
|
276
|
+
action: 'update',
|
|
277
|
+
type: 'Project',
|
|
278
|
+
data: { id: 'proj-1' },
|
|
279
|
+
};
|
|
280
|
+
expect(adapter.normalizeWebhookEvent(payload)).toBeNull();
|
|
281
|
+
});
|
|
282
|
+
it('returns null for payload without data', () => {
|
|
283
|
+
const payload = {
|
|
284
|
+
action: 'update',
|
|
285
|
+
type: 'Issue',
|
|
286
|
+
};
|
|
287
|
+
expect(adapter.normalizeWebhookEvent(payload)).toBeNull();
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
// ========================================================================
|
|
291
|
+
// scanProjectIssues
|
|
292
|
+
// ========================================================================
|
|
293
|
+
describe('scanProjectIssues', () => {
|
|
294
|
+
/** Helper: create a listProjectIssues result entry */
|
|
295
|
+
function mockListProjectIssue(overrides = {}) {
|
|
296
|
+
return {
|
|
297
|
+
id: overrides.id ?? 'issue-uuid-1',
|
|
298
|
+
identifier: overrides.identifier ?? 'SUP-100',
|
|
299
|
+
title: overrides.title ?? 'Test Issue',
|
|
300
|
+
description: overrides.description ?? 'A test issue description',
|
|
301
|
+
status: overrides.status ?? 'Backlog',
|
|
302
|
+
labels: overrides.labels ?? ['Feature'],
|
|
303
|
+
createdAt: overrides.createdAt ?? Date.now(),
|
|
304
|
+
parentId: overrides.parentId,
|
|
305
|
+
project: overrides.project ?? 'MyProject',
|
|
306
|
+
childCount: overrides.childCount ?? 0,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
it('returns GovernorIssues for all non-terminal issues', async () => {
|
|
310
|
+
const issues = [
|
|
311
|
+
mockListProjectIssue({ id: 'i-1', identifier: 'SUP-1', status: 'Backlog' }),
|
|
312
|
+
mockListProjectIssue({ id: 'i-2', identifier: 'SUP-2', status: 'Started' }),
|
|
313
|
+
mockListProjectIssue({ id: 'i-3', identifier: 'SUP-3', status: 'Finished' }),
|
|
314
|
+
];
|
|
315
|
+
mocks.listProjectIssues.mockResolvedValue(issues);
|
|
316
|
+
const result = await adapter.scanProjectIssues('MyProject');
|
|
317
|
+
expect(result).toHaveLength(3);
|
|
318
|
+
expect(result[0].id).toBe('i-1');
|
|
319
|
+
expect(result[0].identifier).toBe('SUP-1');
|
|
320
|
+
expect(result[0].status).toBe('Backlog');
|
|
321
|
+
expect(result[1].id).toBe('i-2');
|
|
322
|
+
expect(result[1].status).toBe('Started');
|
|
323
|
+
expect(result[2].id).toBe('i-3');
|
|
324
|
+
expect(result[2].status).toBe('Finished');
|
|
325
|
+
});
|
|
326
|
+
it('delegates to listProjectIssues with project name', async () => {
|
|
327
|
+
mocks.listProjectIssues.mockResolvedValue([]);
|
|
328
|
+
await adapter.scanProjectIssues('TestProject');
|
|
329
|
+
expect(mocks.listProjectIssues).toHaveBeenCalledWith('TestProject');
|
|
330
|
+
});
|
|
331
|
+
it('returns empty array when no issues found', async () => {
|
|
332
|
+
mocks.listProjectIssues.mockResolvedValue([]);
|
|
333
|
+
const result = await adapter.scanProjectIssues('EmptyProject');
|
|
334
|
+
expect(result).toEqual([]);
|
|
335
|
+
});
|
|
336
|
+
it('maps issue properties correctly', async () => {
|
|
337
|
+
const issue = mockListProjectIssue({
|
|
338
|
+
labels: ['Bug', 'Urgent'],
|
|
339
|
+
parentId: 'parent-1',
|
|
340
|
+
project: 'MyProject',
|
|
341
|
+
});
|
|
342
|
+
mocks.listProjectIssues.mockResolvedValue([issue]);
|
|
343
|
+
const result = await adapter.scanProjectIssues('MyProject');
|
|
344
|
+
expect(result[0].labels).toEqual(['Bug', 'Urgent']);
|
|
345
|
+
expect(result[0].parentId).toBe('parent-1');
|
|
346
|
+
expect(result[0].project).toBe('MyProject');
|
|
347
|
+
});
|
|
348
|
+
it('preserves createdAt as epoch milliseconds', async () => {
|
|
349
|
+
const createdAt = new Date('2025-03-01T08:00:00Z').getTime();
|
|
350
|
+
const issue = mockListProjectIssue({ createdAt });
|
|
351
|
+
mocks.listProjectIssues.mockResolvedValue([issue]);
|
|
352
|
+
const result = await adapter.scanProjectIssues('MyProject');
|
|
353
|
+
expect(result[0].createdAt).toBe(createdAt);
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
// ========================================================================
|
|
357
|
+
// scanProjectIssuesWithParents
|
|
358
|
+
// ========================================================================
|
|
359
|
+
describe('scanProjectIssuesWithParents', () => {
|
|
360
|
+
it('returns issues and parent IDs set', async () => {
|
|
361
|
+
mocks.listProjectIssues.mockResolvedValue([
|
|
362
|
+
{
|
|
363
|
+
id: 'p-1', identifier: 'SUP-10', title: 'Parent',
|
|
364
|
+
status: 'Backlog', labels: [], createdAt: Date.now(),
|
|
365
|
+
project: 'MyProject', childCount: 3,
|
|
366
|
+
},
|
|
367
|
+
{
|
|
368
|
+
id: 'c-1', identifier: 'SUP-11', title: 'Child',
|
|
369
|
+
status: 'Backlog', labels: [], createdAt: Date.now(),
|
|
370
|
+
parentId: 'p-1', project: 'MyProject', childCount: 0,
|
|
371
|
+
},
|
|
372
|
+
]);
|
|
373
|
+
const { issues, parentIssueIds } = await adapter.scanProjectIssuesWithParents('MyProject');
|
|
374
|
+
expect(issues).toHaveLength(2);
|
|
375
|
+
expect(parentIssueIds.has('p-1')).toBe(true);
|
|
376
|
+
expect(parentIssueIds.has('c-1')).toBe(false);
|
|
377
|
+
});
|
|
378
|
+
});
|
|
379
|
+
// ========================================================================
|
|
380
|
+
// toGovernorIssue
|
|
381
|
+
// ========================================================================
|
|
382
|
+
describe('toGovernorIssue', () => {
|
|
383
|
+
it('converts a Linear SDK Issue to GovernorIssue', async () => {
|
|
384
|
+
const issue = mockLinearIssue({
|
|
385
|
+
id: 'conv-1',
|
|
386
|
+
identifier: 'SUP-50',
|
|
387
|
+
title: 'Convert me',
|
|
388
|
+
description: 'Some description',
|
|
389
|
+
stateName: 'Delivered',
|
|
390
|
+
labels: ['Feature', 'Frontend'],
|
|
391
|
+
parentId: 'parent-2',
|
|
392
|
+
projectName: 'ConvertProject',
|
|
393
|
+
createdAt: new Date('2025-02-20T00:00:00Z'),
|
|
394
|
+
});
|
|
395
|
+
const result = await adapter.toGovernorIssue(issue);
|
|
396
|
+
expect(result).toEqual({
|
|
397
|
+
id: 'conv-1',
|
|
398
|
+
identifier: 'SUP-50',
|
|
399
|
+
title: 'Convert me',
|
|
400
|
+
description: 'Some description',
|
|
401
|
+
status: 'Delivered',
|
|
402
|
+
labels: ['Feature', 'Frontend'],
|
|
403
|
+
parentId: 'parent-2',
|
|
404
|
+
project: 'ConvertProject',
|
|
405
|
+
createdAt: new Date('2025-02-20T00:00:00Z').getTime(),
|
|
406
|
+
});
|
|
407
|
+
});
|
|
408
|
+
it('handles issue with no parent', async () => {
|
|
409
|
+
const issue = mockLinearIssue({ parentId: null });
|
|
410
|
+
const result = await adapter.toGovernorIssue(issue);
|
|
411
|
+
expect(result.parentId).toBeUndefined();
|
|
412
|
+
});
|
|
413
|
+
it('handles issue with no project', async () => {
|
|
414
|
+
const issue = mockLinearIssue({ projectName: null });
|
|
415
|
+
const result = await adapter.toGovernorIssue(issue);
|
|
416
|
+
expect(result.project).toBeUndefined();
|
|
417
|
+
});
|
|
418
|
+
it('handles issue with no state (defaults to Backlog)', async () => {
|
|
419
|
+
const issue = mockLinearIssue({ stateName: '' });
|
|
420
|
+
// Simulate null state
|
|
421
|
+
Object.defineProperty(issue, 'state', {
|
|
422
|
+
get: () => Promise.resolve(null),
|
|
423
|
+
});
|
|
424
|
+
const result = await adapter.toGovernorIssue(issue);
|
|
425
|
+
expect(result.status).toBe('Backlog');
|
|
426
|
+
});
|
|
427
|
+
it('handles issue with null description', async () => {
|
|
428
|
+
const issue = mockLinearIssue({ description: null });
|
|
429
|
+
const result = await adapter.toGovernorIssue(issue);
|
|
430
|
+
expect(result.description).toBeUndefined();
|
|
431
|
+
});
|
|
432
|
+
it('throws for invalid native object', async () => {
|
|
433
|
+
await expect(adapter.toGovernorIssue(null)).rejects.toThrow('expected a Linear SDK Issue object');
|
|
434
|
+
});
|
|
435
|
+
it('throws for object without id', async () => {
|
|
436
|
+
await expect(adapter.toGovernorIssue({ title: 'no id' })).rejects.toThrow('expected a Linear SDK Issue object');
|
|
437
|
+
});
|
|
438
|
+
});
|
|
439
|
+
// ========================================================================
|
|
440
|
+
// isParentIssue (inherited from LinearFrontendAdapter)
|
|
441
|
+
// ========================================================================
|
|
442
|
+
describe('isParentIssue', () => {
|
|
443
|
+
it('delegates to client.isParentIssue', async () => {
|
|
444
|
+
mocks.isParentIssue.mockResolvedValue(true);
|
|
445
|
+
const result = await adapter.isParentIssue('issue-1');
|
|
446
|
+
expect(result).toBe(true);
|
|
447
|
+
expect(mocks.isParentIssue).toHaveBeenCalledWith('issue-1');
|
|
448
|
+
});
|
|
449
|
+
it('returns false for non-parent issues', async () => {
|
|
450
|
+
mocks.isParentIssue.mockResolvedValue(false);
|
|
451
|
+
const result = await adapter.isParentIssue('leaf-issue');
|
|
452
|
+
expect(result).toBe(false);
|
|
453
|
+
});
|
|
454
|
+
});
|
|
455
|
+
// ========================================================================
|
|
456
|
+
// Inherited methods still work
|
|
457
|
+
// ========================================================================
|
|
458
|
+
describe('inherits LinearFrontendAdapter', () => {
|
|
459
|
+
it('resolveStatus works', () => {
|
|
460
|
+
expect(adapter.resolveStatus('backlog')).toBe('Backlog');
|
|
461
|
+
expect(adapter.resolveStatus('started')).toBe('Started');
|
|
462
|
+
});
|
|
463
|
+
it('abstractStatus works', () => {
|
|
464
|
+
expect(adapter.abstractStatus('Backlog')).toBe('backlog');
|
|
465
|
+
expect(adapter.abstractStatus('Finished')).toBe('finished');
|
|
466
|
+
});
|
|
467
|
+
});
|
|
468
|
+
});
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Proxy Issue Tracker Client
|
|
3
|
+
*
|
|
4
|
+
* Drop-in replacement for LinearAgentClient that routes all calls
|
|
5
|
+
* through the centralized dashboard proxy endpoint instead of calling
|
|
6
|
+
* the issue tracker API directly.
|
|
7
|
+
*
|
|
8
|
+
* Used when `AGENTFACTORY_API_URL` env var is set.
|
|
9
|
+
*
|
|
10
|
+
* Benefits:
|
|
11
|
+
* - Zero direct API credentials needed on the agent side
|
|
12
|
+
* - Single shared rate limiter and circuit breaker on the proxy
|
|
13
|
+
* - OAuth token resolution stays server-side
|
|
14
|
+
* - Platform-agnostic: agents don't need to know Linear exists
|
|
15
|
+
*/
|
|
16
|
+
import type { AgentActivityCreateInput, AgentActivityResult, AgentSessionUpdateInput, AgentSessionUpdateResult, AgentSessionCreateOnIssueInput, AgentSessionCreateResult, IssueRelationCreateInput, IssueRelationResult, IssueRelationsResult, LinearWorkflowStatus, StatusMapping, SubIssueGraph, SubIssueStatus } from './types.js';
|
|
17
|
+
import type { SerializedIssue, SerializedComment, SerializedViewer, SerializedTeam } from './issue-tracker-proxy.js';
|
|
18
|
+
export interface ProxyClientConfig {
|
|
19
|
+
/** Base URL of the dashboard (e.g., 'https://my-dashboard.vercel.app') */
|
|
20
|
+
apiUrl: string;
|
|
21
|
+
/** Worker API key for authentication */
|
|
22
|
+
apiKey: string;
|
|
23
|
+
/** Workspace/organization ID for multi-tenant routing */
|
|
24
|
+
organizationId?: string;
|
|
25
|
+
/** Request timeout in ms (default: 30_000) */
|
|
26
|
+
timeoutMs?: number;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Issue tracker client that proxies all calls through the dashboard server.
|
|
30
|
+
*
|
|
31
|
+
* Implements the same public interface as LinearAgentClient but serializes
|
|
32
|
+
* calls as JSON and sends them to POST /api/issue-tracker-proxy.
|
|
33
|
+
*
|
|
34
|
+
* All returned objects are plain JSON (no lazy-loaded SDK relations).
|
|
35
|
+
*/
|
|
36
|
+
export declare class ProxyIssueTrackerClient {
|
|
37
|
+
private readonly apiUrl;
|
|
38
|
+
private readonly apiKey;
|
|
39
|
+
private readonly organizationId?;
|
|
40
|
+
private readonly timeoutMs;
|
|
41
|
+
constructor(config: ProxyClientConfig);
|
|
42
|
+
getIssue(issueIdOrIdentifier: string): Promise<SerializedIssue>;
|
|
43
|
+
updateIssue(issueId: string, data: {
|
|
44
|
+
title?: string;
|
|
45
|
+
description?: string;
|
|
46
|
+
stateId?: string;
|
|
47
|
+
assigneeId?: string | null;
|
|
48
|
+
priority?: number;
|
|
49
|
+
labelIds?: string[];
|
|
50
|
+
}): Promise<SerializedIssue>;
|
|
51
|
+
createIssue(input: {
|
|
52
|
+
title: string;
|
|
53
|
+
description?: string;
|
|
54
|
+
teamId: string;
|
|
55
|
+
projectId?: string;
|
|
56
|
+
stateId?: string;
|
|
57
|
+
labelIds?: string[];
|
|
58
|
+
parentId?: string;
|
|
59
|
+
priority?: number;
|
|
60
|
+
}): Promise<SerializedIssue>;
|
|
61
|
+
unassignIssue(issueId: string): Promise<SerializedIssue>;
|
|
62
|
+
getTeamStatuses(teamId: string): Promise<StatusMapping>;
|
|
63
|
+
updateIssueStatus(issueId: string, statusName: LinearWorkflowStatus): Promise<SerializedIssue>;
|
|
64
|
+
createComment(issueId: string, body: string): Promise<SerializedComment>;
|
|
65
|
+
getIssueComments(issueId: string): Promise<SerializedComment[]>;
|
|
66
|
+
createAgentActivity(input: AgentActivityCreateInput): Promise<AgentActivityResult>;
|
|
67
|
+
updateAgentSession(input: AgentSessionUpdateInput): Promise<AgentSessionUpdateResult>;
|
|
68
|
+
createAgentSessionOnIssue(input: AgentSessionCreateOnIssueInput): Promise<AgentSessionCreateResult>;
|
|
69
|
+
createIssueRelation(input: IssueRelationCreateInput): Promise<IssueRelationResult>;
|
|
70
|
+
getIssueRelations(issueId: string): Promise<IssueRelationsResult>;
|
|
71
|
+
deleteIssueRelation(relationId: string): Promise<{
|
|
72
|
+
success: boolean;
|
|
73
|
+
}>;
|
|
74
|
+
getSubIssues(issueIdOrIdentifier: string): Promise<SerializedIssue[]>;
|
|
75
|
+
getSubIssueStatuses(issueIdOrIdentifier: string): Promise<SubIssueStatus[]>;
|
|
76
|
+
getSubIssueGraph(issueIdOrIdentifier: string): Promise<SubIssueGraph>;
|
|
77
|
+
isParentIssue(issueIdOrIdentifier: string): Promise<boolean>;
|
|
78
|
+
isChildIssue(issueIdOrIdentifier: string): Promise<boolean>;
|
|
79
|
+
listProjectIssues(project: string): Promise<Array<{
|
|
80
|
+
id: string;
|
|
81
|
+
identifier: string;
|
|
82
|
+
title: string;
|
|
83
|
+
description?: string;
|
|
84
|
+
status: string;
|
|
85
|
+
labels: string[];
|
|
86
|
+
createdAt: number;
|
|
87
|
+
parentId?: string;
|
|
88
|
+
project?: string;
|
|
89
|
+
childCount: number;
|
|
90
|
+
}>>;
|
|
91
|
+
getProjectRepositoryUrl(projectId: string): Promise<string | null>;
|
|
92
|
+
getViewer(): Promise<SerializedViewer>;
|
|
93
|
+
getTeam(teamIdOrKey: string): Promise<SerializedTeam>;
|
|
94
|
+
private call;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Create a proxy client if AGENTFACTORY_API_URL is set, otherwise return null.
|
|
98
|
+
*
|
|
99
|
+
* @param fallbackApiKey - API key to use (default: WORKER_API_KEY env var)
|
|
100
|
+
* @param organizationId - Workspace ID for multi-tenant routing
|
|
101
|
+
*/
|
|
102
|
+
export declare function createProxyClientIfConfigured(fallbackApiKey?: string, organizationId?: string): ProxyIssueTrackerClient | null;
|
|
103
|
+
//# sourceMappingURL=proxy-client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"proxy-client.d.ts","sourceRoot":"","sources":["../../src/proxy-client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EACV,wBAAwB,EACxB,mBAAmB,EACnB,uBAAuB,EACvB,wBAAwB,EACxB,8BAA8B,EAC9B,wBAAwB,EACxB,wBAAwB,EACxB,mBAAmB,EACnB,oBAAoB,EACpB,oBAAoB,EACpB,aAAa,EACb,aAAa,EACb,cAAc,EACf,MAAM,YAAY,CAAA;AACnB,OAAO,KAAK,EAIV,eAAe,EACf,iBAAiB,EACjB,gBAAgB,EAChB,cAAc,EACf,MAAM,0BAA0B,CAAA;AAEjC,MAAM,WAAW,iBAAiB;IAChC,0EAA0E;IAC1E,MAAM,EAAE,MAAM,CAAA;IACd,wCAAwC;IACxC,MAAM,EAAE,MAAM,CAAA;IACd,yDAAyD;IACzD,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,8CAA8C;IAC9C,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED;;;;;;;GAOG;AACH,qBAAa,uBAAuB;IAClC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAQ;IAC/B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAQ;IAC/B,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAC,CAAQ;IACxC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAQ;gBAEtB,MAAM,EAAE,iBAAiB;IAW/B,QAAQ,CAAC,mBAAmB,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC;IAI/D,WAAW,CACf,OAAO,EAAE,MAAM,EACf,IAAI,EAAE;QACJ,KAAK,CAAC,EAAE,MAAM,CAAA;QACd,WAAW,CAAC,EAAE,MAAM,CAAA;QACpB,OAAO,CAAC,EAAE,MAAM,CAAA;QAChB,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;QAC1B,QAAQ,CAAC,EAAE,MAAM,CAAA;QACjB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAA;KACpB,GACA,OAAO,CAAC,eAAe,CAAC;IAIrB,WAAW,CAAC,KAAK,EAAE;QACvB,KAAK,EAAE,MAAM,CAAA;QACb,WAAW,CAAC,EAAE,MAAM,CAAA;QACpB,MAAM,EAAE,MAAM,CAAA;QACd,SAAS,CAAC,EAAE,MAAM,CAAA;QAClB,OAAO,CAAC,EAAE,MAAM,CAAA;QAChB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAA;QACnB,QAAQ,CAAC,EAAE,MAAM,CAAA;QACjB,QAAQ,CAAC,EAAE,MAAM,CAAA;KAClB,GAAG,OAAO,CAAC,eAAe,CAAC;IAItB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC;IAQxD,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC;IAIvD,iBAAiB,CACrB,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,oBAAoB,GAC/B,OAAO,CAAC,eAAe,CAAC;IAQrB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,CAAC;IAIxE,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,EAAE,CAAC;IAQ/D,mBAAmB,CAAC,KAAK,EAAE,wBAAwB,GAAG,OAAO,CAAC,mBAAmB,CAAC;IAIlF,kBAAkB,CAAC,KAAK,EAAE,uBAAuB,GAAG,OAAO,CAAC,wBAAwB,CAAC;IAIrF,yBAAyB,CAC7B,KAAK,EAAE,8BAA8B,GACpC,OAAO,CAAC,wBAAwB,CAAC;IAQ9B,mBAAmB,CAAC,KAAK,EAAE,wBAAwB,GAAG,OAAO,CAAC,mBAAmB,CAAC;IAIlF,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,oBAAoB,CAAC;IAIjE,mBAAmB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC;IAQtE,YAAY,CAAC,mBAAmB,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC;IAIrE,mBAAmB,CAAC,mBAAmB,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC;IAI3E,gBAAgB,CAAC,mBAAmB,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC;IAIrE,aAAa,CAAC,mBAAmB,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAI5D,YAAY,CAAC,mBAAmB,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAQ3D,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAC/C,KAAK,CAAC;QACJ,EAAE,EAAE,MAAM,CAAA;QACV,UAAU,EAAE,MAAM,CAAA;QAClB,KAAK,EAAE,MAAM,CAAA;QACb,WAAW,CAAC,EAAE,MAAM,CAAA;QACpB,MAAM,EAAE,MAAM,CAAA;QACd,MAAM,EAAE,MAAM,EAAE,CAAA;QAChB,SAAS,EAAE,MAAM,CAAA;QACjB,QAAQ,CAAC,EAAE,MAAM,CAAA;QACjB,OAAO,CAAC,EAAE,MAAM,CAAA;QAChB,UAAU,EAAE,MAAM,CAAA;KACnB,CAAC,CACH;IAIK,uBAAuB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAQlE,SAAS,IAAI,OAAO,CAAC,gBAAgB,CAAC;IAItC,OAAO,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC;YAQ7C,IAAI;CA8CnB;AAED;;;;;GAKG;AACH,wBAAgB,6BAA6B,CAC3C,cAAc,CAAC,EAAE,MAAM,EACvB,cAAc,CAAC,EAAE,MAAM,GACtB,uBAAuB,GAAG,IAAI,CAehC"}
|