@supaku/agentfactory-linear 0.7.9 → 0.7.11
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/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 +149 -0
- package/dist/src/agent-client.d.ts +11 -0
- package/dist/src/agent-client.d.ts.map +1 -1
- package/dist/src/agent-client.js +30 -0
- package/dist/src/constants.d.ts +5 -0
- package/dist/src/constants.d.ts.map +1 -1
- package/dist/src/constants.js +7 -0
- package/dist/src/defaults/index.d.ts +1 -1
- package/dist/src/defaults/index.d.ts.map +1 -1
- package/dist/src/defaults/index.js +1 -1
- package/dist/src/defaults/prompts.d.ts +18 -1
- package/dist/src/defaults/prompts.d.ts.map +1 -1
- package/dist/src/defaults/prompts.js +108 -5
- 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 +130 -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 +6 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +6 -2
- package/dist/src/platform-adapter.d.ts +119 -0
- package/dist/src/platform-adapter.d.ts.map +1 -0
- package/dist/src/platform-adapter.js +229 -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 +435 -0
- package/package.json +1 -1
|
@@ -0,0 +1,435 @@
|
|
|
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
|
+
};
|
|
55
|
+
const linearClient = {
|
|
56
|
+
issues: vi.fn(),
|
|
57
|
+
issueLabels: vi.fn(),
|
|
58
|
+
};
|
|
59
|
+
const client = {
|
|
60
|
+
...mocks,
|
|
61
|
+
linearClient,
|
|
62
|
+
};
|
|
63
|
+
mocks.linearClientIssues = linearClient.issues;
|
|
64
|
+
return { client, mocks };
|
|
65
|
+
}
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
// Webhook payload factories
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
function makeIssueUpdatePayload(overrides = {}) {
|
|
70
|
+
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;
|
|
71
|
+
return {
|
|
72
|
+
action,
|
|
73
|
+
type: 'Issue',
|
|
74
|
+
data: {
|
|
75
|
+
id,
|
|
76
|
+
identifier,
|
|
77
|
+
title,
|
|
78
|
+
description,
|
|
79
|
+
url: `https://linear.app/team/issue/${identifier}`,
|
|
80
|
+
state: { id: 'state-1', name: stateName, type: 'started' },
|
|
81
|
+
labels: labels.map((l) => ({ id: `label-${l.name}`, ...l })),
|
|
82
|
+
parent: parentId ? { id: parentId, identifier: 'SUP-99', title: 'Parent' } : undefined,
|
|
83
|
+
project: projectName ? { id: 'proj-1', name: projectName } : undefined,
|
|
84
|
+
createdAt,
|
|
85
|
+
},
|
|
86
|
+
updatedFrom: hasStateChange ? { stateId: 'old-state-id' } : {},
|
|
87
|
+
createdAt: '2025-01-15T12:00:00.000Z',
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
function makeCommentPayload(overrides = {}) {
|
|
91
|
+
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;
|
|
92
|
+
return {
|
|
93
|
+
action,
|
|
94
|
+
type: 'Comment',
|
|
95
|
+
data: {
|
|
96
|
+
id: commentId,
|
|
97
|
+
body,
|
|
98
|
+
issue: {
|
|
99
|
+
id: issueId,
|
|
100
|
+
identifier: issueIdentifier,
|
|
101
|
+
title: issueTitle,
|
|
102
|
+
url: `https://linear.app/team/issue/${issueIdentifier}`,
|
|
103
|
+
state: { id: 'state-1', name: issueStateName, type: 'started' },
|
|
104
|
+
labels: [{ id: 'label-1', name: 'Feature' }],
|
|
105
|
+
createdAt: '2025-01-15T10:00:00.000Z',
|
|
106
|
+
},
|
|
107
|
+
user: { id: userId, name: userName },
|
|
108
|
+
},
|
|
109
|
+
createdAt: '2025-01-15T12:00:00.000Z',
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
// Tests
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
describe('LinearPlatformAdapter', () => {
|
|
116
|
+
let adapter;
|
|
117
|
+
let mocks;
|
|
118
|
+
beforeEach(() => {
|
|
119
|
+
const { client, mocks: m } = createMockClient();
|
|
120
|
+
mocks = m;
|
|
121
|
+
adapter = new LinearPlatformAdapter(client);
|
|
122
|
+
});
|
|
123
|
+
// ========================================================================
|
|
124
|
+
// name
|
|
125
|
+
// ========================================================================
|
|
126
|
+
it('has name "linear"', () => {
|
|
127
|
+
expect(adapter.name).toBe('linear');
|
|
128
|
+
});
|
|
129
|
+
// ========================================================================
|
|
130
|
+
// normalizeWebhookEvent — Issue updates
|
|
131
|
+
// ========================================================================
|
|
132
|
+
describe('normalizeWebhookEvent — issue updates', () => {
|
|
133
|
+
it('returns IssueStatusChangedEvent for issue update with state change', () => {
|
|
134
|
+
const payload = makeIssueUpdatePayload({ stateName: 'Started' });
|
|
135
|
+
const events = adapter.normalizeWebhookEvent(payload);
|
|
136
|
+
expect(events).not.toBeNull();
|
|
137
|
+
expect(events).toHaveLength(1);
|
|
138
|
+
const event = events[0];
|
|
139
|
+
expect(event.type).toBe('issue-status-changed');
|
|
140
|
+
expect(event.issueId).toBe('issue-uuid-1');
|
|
141
|
+
expect(event.source).toBe('webhook');
|
|
142
|
+
if (event.type === 'issue-status-changed') {
|
|
143
|
+
expect(event.newStatus).toBe('Started');
|
|
144
|
+
expect(event.issue.id).toBe('issue-uuid-1');
|
|
145
|
+
expect(event.issue.identifier).toBe('SUP-100');
|
|
146
|
+
expect(event.issue.title).toBe('Test Issue');
|
|
147
|
+
expect(event.issue.status).toBe('Started');
|
|
148
|
+
expect(event.issue.labels).toEqual(['Feature']);
|
|
149
|
+
expect(event.issue.project).toBe('MyProject');
|
|
150
|
+
expect(event.timestamp).toBeTruthy();
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
it('returns null for issue update without state change', () => {
|
|
154
|
+
const payload = makeIssueUpdatePayload({ hasStateChange: false });
|
|
155
|
+
const events = adapter.normalizeWebhookEvent(payload);
|
|
156
|
+
expect(events).toBeNull();
|
|
157
|
+
});
|
|
158
|
+
it('returns null for issue create action', () => {
|
|
159
|
+
const payload = makeIssueUpdatePayload({ action: 'create' });
|
|
160
|
+
const events = adapter.normalizeWebhookEvent(payload);
|
|
161
|
+
expect(events).toBeNull();
|
|
162
|
+
});
|
|
163
|
+
it('returns null for issue remove action', () => {
|
|
164
|
+
const payload = makeIssueUpdatePayload({ action: 'remove' });
|
|
165
|
+
const events = adapter.normalizeWebhookEvent(payload);
|
|
166
|
+
expect(events).toBeNull();
|
|
167
|
+
});
|
|
168
|
+
it('includes parentId when issue has a parent', () => {
|
|
169
|
+
const payload = makeIssueUpdatePayload({ parentId: 'parent-uuid-1' });
|
|
170
|
+
const events = adapter.normalizeWebhookEvent(payload);
|
|
171
|
+
expect(events).not.toBeNull();
|
|
172
|
+
const event = events[0];
|
|
173
|
+
if (event.type === 'issue-status-changed') {
|
|
174
|
+
expect(event.issue.parentId).toBe('parent-uuid-1');
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
it('handles issue with no description', () => {
|
|
178
|
+
const payload = makeIssueUpdatePayload();
|
|
179
|
+
// Remove description from data
|
|
180
|
+
delete payload.data.description;
|
|
181
|
+
const events = adapter.normalizeWebhookEvent(payload);
|
|
182
|
+
expect(events).not.toBeNull();
|
|
183
|
+
const event = events[0];
|
|
184
|
+
if (event.type === 'issue-status-changed') {
|
|
185
|
+
expect(event.issue.description).toBeUndefined();
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
it('handles issue with no labels', () => {
|
|
189
|
+
const payload = makeIssueUpdatePayload({ labels: [] });
|
|
190
|
+
const events = adapter.normalizeWebhookEvent(payload);
|
|
191
|
+
expect(events).not.toBeNull();
|
|
192
|
+
const event = events[0];
|
|
193
|
+
if (event.type === 'issue-status-changed') {
|
|
194
|
+
expect(event.issue.labels).toEqual([]);
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
it('parses createdAt from webhook data into milliseconds', () => {
|
|
198
|
+
const payload = makeIssueUpdatePayload({ createdAt: '2025-06-01T00:00:00.000Z' });
|
|
199
|
+
const events = adapter.normalizeWebhookEvent(payload);
|
|
200
|
+
expect(events).not.toBeNull();
|
|
201
|
+
const event = events[0];
|
|
202
|
+
if (event.type === 'issue-status-changed') {
|
|
203
|
+
expect(event.issue.createdAt).toBe(new Date('2025-06-01T00:00:00.000Z').getTime());
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
// ========================================================================
|
|
208
|
+
// normalizeWebhookEvent — Comments
|
|
209
|
+
// ========================================================================
|
|
210
|
+
describe('normalizeWebhookEvent — comments', () => {
|
|
211
|
+
it('returns CommentAddedEvent for comment creation', () => {
|
|
212
|
+
const payload = makeCommentPayload();
|
|
213
|
+
const events = adapter.normalizeWebhookEvent(payload);
|
|
214
|
+
expect(events).not.toBeNull();
|
|
215
|
+
expect(events).toHaveLength(1);
|
|
216
|
+
const event = events[0];
|
|
217
|
+
expect(event.type).toBe('comment-added');
|
|
218
|
+
expect(event.issueId).toBe('issue-uuid-1');
|
|
219
|
+
expect(event.source).toBe('webhook');
|
|
220
|
+
if (event.type === 'comment-added') {
|
|
221
|
+
expect(event.commentId).toBe('comment-uuid-1');
|
|
222
|
+
expect(event.commentBody).toBe('This is a comment');
|
|
223
|
+
expect(event.userId).toBe('user-1');
|
|
224
|
+
expect(event.userName).toBe('Test User');
|
|
225
|
+
expect(event.issue.identifier).toBe('SUP-100');
|
|
226
|
+
expect(event.issue.status).toBe('Started');
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
it('returns null for comment update action', () => {
|
|
230
|
+
const payload = makeCommentPayload({ action: 'update' });
|
|
231
|
+
const events = adapter.normalizeWebhookEvent(payload);
|
|
232
|
+
expect(events).toBeNull();
|
|
233
|
+
});
|
|
234
|
+
it('returns null for comment remove action', () => {
|
|
235
|
+
const payload = makeCommentPayload({ action: 'remove' });
|
|
236
|
+
const events = adapter.normalizeWebhookEvent(payload);
|
|
237
|
+
expect(events).toBeNull();
|
|
238
|
+
});
|
|
239
|
+
it('handles comment without user', () => {
|
|
240
|
+
const payload = makeCommentPayload();
|
|
241
|
+
delete payload.data.user;
|
|
242
|
+
const events = adapter.normalizeWebhookEvent(payload);
|
|
243
|
+
expect(events).not.toBeNull();
|
|
244
|
+
const event = events[0];
|
|
245
|
+
if (event.type === 'comment-added') {
|
|
246
|
+
expect(event.userId).toBeUndefined();
|
|
247
|
+
expect(event.userName).toBeUndefined();
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
// ========================================================================
|
|
252
|
+
// normalizeWebhookEvent — Unrecognized payloads
|
|
253
|
+
// ========================================================================
|
|
254
|
+
describe('normalizeWebhookEvent — unrecognized payloads', () => {
|
|
255
|
+
it('returns null for null payload', () => {
|
|
256
|
+
expect(adapter.normalizeWebhookEvent(null)).toBeNull();
|
|
257
|
+
});
|
|
258
|
+
it('returns null for undefined payload', () => {
|
|
259
|
+
expect(adapter.normalizeWebhookEvent(undefined)).toBeNull();
|
|
260
|
+
});
|
|
261
|
+
it('returns null for non-object payload', () => {
|
|
262
|
+
expect(adapter.normalizeWebhookEvent('string')).toBeNull();
|
|
263
|
+
expect(adapter.normalizeWebhookEvent(42)).toBeNull();
|
|
264
|
+
});
|
|
265
|
+
it('returns null for AgentSessionEvent payloads', () => {
|
|
266
|
+
const payload = {
|
|
267
|
+
action: 'created',
|
|
268
|
+
type: 'AgentSessionEvent',
|
|
269
|
+
data: { id: 'session-1' },
|
|
270
|
+
};
|
|
271
|
+
expect(adapter.normalizeWebhookEvent(payload)).toBeNull();
|
|
272
|
+
});
|
|
273
|
+
it('returns null for unknown resource types', () => {
|
|
274
|
+
const payload = {
|
|
275
|
+
action: 'update',
|
|
276
|
+
type: 'Project',
|
|
277
|
+
data: { id: 'proj-1' },
|
|
278
|
+
};
|
|
279
|
+
expect(adapter.normalizeWebhookEvent(payload)).toBeNull();
|
|
280
|
+
});
|
|
281
|
+
it('returns null for payload without data', () => {
|
|
282
|
+
const payload = {
|
|
283
|
+
action: 'update',
|
|
284
|
+
type: 'Issue',
|
|
285
|
+
};
|
|
286
|
+
expect(adapter.normalizeWebhookEvent(payload)).toBeNull();
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
// ========================================================================
|
|
290
|
+
// scanProjectIssues
|
|
291
|
+
// ========================================================================
|
|
292
|
+
describe('scanProjectIssues', () => {
|
|
293
|
+
it('returns GovernorIssues for all non-terminal issues', async () => {
|
|
294
|
+
const issues = [
|
|
295
|
+
mockLinearIssue({ id: 'i-1', identifier: 'SUP-1', stateName: 'Backlog' }),
|
|
296
|
+
mockLinearIssue({ id: 'i-2', identifier: 'SUP-2', stateName: 'Started' }),
|
|
297
|
+
mockLinearIssue({ id: 'i-3', identifier: 'SUP-3', stateName: 'Finished' }),
|
|
298
|
+
];
|
|
299
|
+
mocks.linearClientIssues.mockResolvedValue({ nodes: issues });
|
|
300
|
+
const result = await adapter.scanProjectIssues('MyProject');
|
|
301
|
+
expect(result).toHaveLength(3);
|
|
302
|
+
expect(result[0].id).toBe('i-1');
|
|
303
|
+
expect(result[0].identifier).toBe('SUP-1');
|
|
304
|
+
expect(result[0].status).toBe('Backlog');
|
|
305
|
+
expect(result[1].id).toBe('i-2');
|
|
306
|
+
expect(result[1].status).toBe('Started');
|
|
307
|
+
expect(result[2].id).toBe('i-3');
|
|
308
|
+
expect(result[2].status).toBe('Finished');
|
|
309
|
+
});
|
|
310
|
+
it('passes correct filter to Linear API', async () => {
|
|
311
|
+
mocks.linearClientIssues.mockResolvedValue({ nodes: [] });
|
|
312
|
+
await adapter.scanProjectIssues('TestProject');
|
|
313
|
+
expect(mocks.linearClientIssues).toHaveBeenCalledWith({
|
|
314
|
+
filter: {
|
|
315
|
+
project: { name: { eq: 'TestProject' } },
|
|
316
|
+
state: { name: { nin: ['Accepted', 'Canceled', 'Duplicate'] } },
|
|
317
|
+
},
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
it('returns empty array when no issues found', async () => {
|
|
321
|
+
mocks.linearClientIssues.mockResolvedValue({ nodes: [] });
|
|
322
|
+
const result = await adapter.scanProjectIssues('EmptyProject');
|
|
323
|
+
expect(result).toEqual([]);
|
|
324
|
+
});
|
|
325
|
+
it('resolves lazy-loaded issue properties', async () => {
|
|
326
|
+
const issue = mockLinearIssue({
|
|
327
|
+
labels: ['Bug', 'Urgent'],
|
|
328
|
+
parentId: 'parent-1',
|
|
329
|
+
projectName: 'MyProject',
|
|
330
|
+
});
|
|
331
|
+
mocks.linearClientIssues.mockResolvedValue({ nodes: [issue] });
|
|
332
|
+
const result = await adapter.scanProjectIssues('MyProject');
|
|
333
|
+
expect(result[0].labels).toEqual(['Bug', 'Urgent']);
|
|
334
|
+
expect(result[0].parentId).toBe('parent-1');
|
|
335
|
+
expect(result[0].project).toBe('MyProject');
|
|
336
|
+
});
|
|
337
|
+
it('converts createdAt Date to epoch milliseconds', async () => {
|
|
338
|
+
const issue = mockLinearIssue({
|
|
339
|
+
createdAt: new Date('2025-03-01T08:00:00Z'),
|
|
340
|
+
});
|
|
341
|
+
mocks.linearClientIssues.mockResolvedValue({ nodes: [issue] });
|
|
342
|
+
const result = await adapter.scanProjectIssues('MyProject');
|
|
343
|
+
expect(result[0].createdAt).toBe(new Date('2025-03-01T08:00:00Z').getTime());
|
|
344
|
+
});
|
|
345
|
+
});
|
|
346
|
+
// ========================================================================
|
|
347
|
+
// toGovernorIssue
|
|
348
|
+
// ========================================================================
|
|
349
|
+
describe('toGovernorIssue', () => {
|
|
350
|
+
it('converts a Linear SDK Issue to GovernorIssue', async () => {
|
|
351
|
+
const issue = mockLinearIssue({
|
|
352
|
+
id: 'conv-1',
|
|
353
|
+
identifier: 'SUP-50',
|
|
354
|
+
title: 'Convert me',
|
|
355
|
+
description: 'Some description',
|
|
356
|
+
stateName: 'Delivered',
|
|
357
|
+
labels: ['Feature', 'Frontend'],
|
|
358
|
+
parentId: 'parent-2',
|
|
359
|
+
projectName: 'ConvertProject',
|
|
360
|
+
createdAt: new Date('2025-02-20T00:00:00Z'),
|
|
361
|
+
});
|
|
362
|
+
const result = await adapter.toGovernorIssue(issue);
|
|
363
|
+
expect(result).toEqual({
|
|
364
|
+
id: 'conv-1',
|
|
365
|
+
identifier: 'SUP-50',
|
|
366
|
+
title: 'Convert me',
|
|
367
|
+
description: 'Some description',
|
|
368
|
+
status: 'Delivered',
|
|
369
|
+
labels: ['Feature', 'Frontend'],
|
|
370
|
+
parentId: 'parent-2',
|
|
371
|
+
project: 'ConvertProject',
|
|
372
|
+
createdAt: new Date('2025-02-20T00:00:00Z').getTime(),
|
|
373
|
+
});
|
|
374
|
+
});
|
|
375
|
+
it('handles issue with no parent', async () => {
|
|
376
|
+
const issue = mockLinearIssue({ parentId: null });
|
|
377
|
+
const result = await adapter.toGovernorIssue(issue);
|
|
378
|
+
expect(result.parentId).toBeUndefined();
|
|
379
|
+
});
|
|
380
|
+
it('handles issue with no project', async () => {
|
|
381
|
+
const issue = mockLinearIssue({ projectName: null });
|
|
382
|
+
const result = await adapter.toGovernorIssue(issue);
|
|
383
|
+
expect(result.project).toBeUndefined();
|
|
384
|
+
});
|
|
385
|
+
it('handles issue with no state (defaults to Backlog)', async () => {
|
|
386
|
+
const issue = mockLinearIssue({ stateName: '' });
|
|
387
|
+
// Simulate null state
|
|
388
|
+
Object.defineProperty(issue, 'state', {
|
|
389
|
+
get: () => Promise.resolve(null),
|
|
390
|
+
});
|
|
391
|
+
const result = await adapter.toGovernorIssue(issue);
|
|
392
|
+
expect(result.status).toBe('Backlog');
|
|
393
|
+
});
|
|
394
|
+
it('handles issue with null description', async () => {
|
|
395
|
+
const issue = mockLinearIssue({ description: null });
|
|
396
|
+
const result = await adapter.toGovernorIssue(issue);
|
|
397
|
+
expect(result.description).toBeUndefined();
|
|
398
|
+
});
|
|
399
|
+
it('throws for invalid native object', async () => {
|
|
400
|
+
await expect(adapter.toGovernorIssue(null)).rejects.toThrow('expected a Linear SDK Issue object');
|
|
401
|
+
});
|
|
402
|
+
it('throws for object without id', async () => {
|
|
403
|
+
await expect(adapter.toGovernorIssue({ title: 'no id' })).rejects.toThrow('expected a Linear SDK Issue object');
|
|
404
|
+
});
|
|
405
|
+
});
|
|
406
|
+
// ========================================================================
|
|
407
|
+
// isParentIssue (inherited from LinearFrontendAdapter)
|
|
408
|
+
// ========================================================================
|
|
409
|
+
describe('isParentIssue', () => {
|
|
410
|
+
it('delegates to client.isParentIssue', async () => {
|
|
411
|
+
mocks.isParentIssue.mockResolvedValue(true);
|
|
412
|
+
const result = await adapter.isParentIssue('issue-1');
|
|
413
|
+
expect(result).toBe(true);
|
|
414
|
+
expect(mocks.isParentIssue).toHaveBeenCalledWith('issue-1');
|
|
415
|
+
});
|
|
416
|
+
it('returns false for non-parent issues', async () => {
|
|
417
|
+
mocks.isParentIssue.mockResolvedValue(false);
|
|
418
|
+
const result = await adapter.isParentIssue('leaf-issue');
|
|
419
|
+
expect(result).toBe(false);
|
|
420
|
+
});
|
|
421
|
+
});
|
|
422
|
+
// ========================================================================
|
|
423
|
+
// Inherited methods still work
|
|
424
|
+
// ========================================================================
|
|
425
|
+
describe('inherits LinearFrontendAdapter', () => {
|
|
426
|
+
it('resolveStatus works', () => {
|
|
427
|
+
expect(adapter.resolveStatus('backlog')).toBe('Backlog');
|
|
428
|
+
expect(adapter.resolveStatus('started')).toBe('Started');
|
|
429
|
+
});
|
|
430
|
+
it('abstractStatus works', () => {
|
|
431
|
+
expect(adapter.abstractStatus('Backlog')).toBe('backlog');
|
|
432
|
+
expect(adapter.abstractStatus('Finished')).toBe('finished');
|
|
433
|
+
});
|
|
434
|
+
});
|
|
435
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@supaku/agentfactory-linear",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.11",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Linear issue tracker integration for AgentFactory — status transitions, agent sessions, work routing",
|
|
6
6
|
"author": "Supaku (https://supaku.com)",
|