@cliangdev/flux-plugin 0.0.0-dev.cbdf207 → 0.0.0-dev.df3e9bb
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 +8 -4
- package/bin/install.cjs +150 -16
- package/package.json +7 -11
- package/src/__tests__/version.test.ts +37 -0
- package/src/adapters/local/.gitkeep +0 -0
- package/src/server/__tests__/config.test.ts +163 -0
- package/src/server/adapters/__tests__/a-client-linear.test.ts +197 -0
- package/src/server/adapters/__tests__/adapter-factory.test.ts +230 -0
- package/src/server/adapters/__tests__/dependency-ops.test.ts +395 -0
- package/src/server/adapters/__tests__/document-ops.test.ts +306 -0
- package/src/server/adapters/__tests__/linear-adapter.test.ts +91 -0
- package/src/server/adapters/__tests__/linear-config.test.ts +425 -0
- package/src/server/adapters/__tests__/linear-criteria-parser.test.ts +287 -0
- package/src/server/adapters/__tests__/linear-description-test.ts +238 -0
- package/src/server/adapters/__tests__/linear-epic-crud.test.ts +496 -0
- package/src/server/adapters/__tests__/linear-mappers-description.test.ts +276 -0
- package/src/server/adapters/__tests__/linear-mappers-epic.test.ts +294 -0
- package/src/server/adapters/__tests__/linear-mappers-prd.test.ts +300 -0
- package/src/server/adapters/__tests__/linear-mappers-task.test.ts +197 -0
- package/src/server/adapters/__tests__/linear-prd-crud.test.ts +620 -0
- package/src/server/adapters/__tests__/linear-stats.test.ts +450 -0
- package/src/server/adapters/__tests__/linear-task-crud.test.ts +534 -0
- package/src/server/adapters/__tests__/linear-types.test.ts +243 -0
- package/src/server/adapters/__tests__/status-ops.test.ts +441 -0
- package/src/server/adapters/factory.ts +90 -0
- package/src/server/adapters/index.ts +9 -0
- package/src/server/adapters/linear/adapter.ts +1136 -0
- package/src/server/adapters/linear/client.ts +169 -0
- package/src/server/adapters/linear/config.ts +152 -0
- package/src/server/adapters/linear/helpers/criteria-parser.ts +197 -0
- package/src/server/adapters/linear/helpers/index.ts +7 -0
- package/src/server/adapters/linear/index.ts +16 -0
- package/src/server/adapters/linear/mappers/description.ts +136 -0
- package/src/server/adapters/linear/mappers/epic.ts +81 -0
- package/src/server/adapters/linear/mappers/index.ts +27 -0
- package/src/server/adapters/linear/mappers/prd.ts +178 -0
- package/src/server/adapters/linear/mappers/task.ts +82 -0
- package/src/server/adapters/linear/types.ts +264 -0
- package/src/server/adapters/local-adapter.ts +968 -0
- package/src/server/adapters/types.ts +293 -0
- package/src/server/config.ts +73 -0
- package/src/server/db/__tests__/queries.test.ts +472 -0
- package/src/server/db/ids.ts +17 -0
- package/src/server/db/index.ts +69 -0
- package/src/server/db/queries.ts +142 -0
- package/src/server/db/refs.ts +60 -0
- package/src/server/db/schema.ts +88 -0
- package/src/server/db/sqlite.ts +10 -0
- package/src/server/index.ts +83 -0
- package/src/server/tools/__tests__/crud.test.ts +301 -0
- package/src/server/tools/__tests__/get-version.test.ts +27 -0
- package/src/server/tools/__tests__/mcp-interface.test.ts +388 -0
- package/src/server/tools/__tests__/query.test.ts +353 -0
- package/src/server/tools/__tests__/z-configure-linear.test.ts +511 -0
- package/src/server/tools/__tests__/z-get-linear-url.test.ts +108 -0
- package/src/server/tools/configure-linear.ts +373 -0
- package/src/server/tools/create-epic.ts +35 -0
- package/src/server/tools/create-prd.ts +31 -0
- package/src/server/tools/create-task.ts +38 -0
- package/src/server/tools/criteria.ts +50 -0
- package/src/server/tools/delete-entity.ts +76 -0
- package/src/server/tools/dependencies.ts +55 -0
- package/src/server/tools/get-entity.ts +238 -0
- package/src/server/tools/get-linear-url.ts +28 -0
- package/src/server/tools/get-project-context.ts +33 -0
- package/src/server/tools/get-stats.ts +52 -0
- package/src/server/tools/get-version.ts +20 -0
- package/src/server/tools/index.ts +114 -0
- package/src/server/tools/init-project.ts +108 -0
- package/src/server/tools/query-entities.ts +167 -0
- package/src/server/tools/render-status.ts +201 -0
- package/src/server/tools/update-entity.ts +140 -0
- package/src/server/tools/update-status.ts +166 -0
- package/src/server/utils/__tests__/mcp-response.test.ts +331 -0
- package/src/server/utils/logger.ts +9 -0
- package/src/server/utils/mcp-response.ts +254 -0
- package/src/server/utils/status-transitions.ts +160 -0
- package/src/status-line/__tests__/status-line.test.ts +215 -0
- package/src/status-line/index.ts +147 -0
- package/src/utils/__tests__/chalk-import.test.ts +32 -0
- package/src/utils/__tests__/display.test.ts +97 -0
- package/src/utils/__tests__/status-renderer.test.ts +310 -0
- package/src/utils/display.ts +62 -0
- package/src/utils/status-renderer.ts +188 -0
- package/src/version.ts +5 -0
- package/dist/server/index.js +0 -87063
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
|
+
import type { HydratedIssue, LinearAdapter } from "../linear/adapter.js";
|
|
3
|
+
import type { LinearConfig } from "../linear/types.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Helper to create a mock HydratedIssue
|
|
7
|
+
*/
|
|
8
|
+
function createMockIssue(
|
|
9
|
+
overrides: Partial<HydratedIssue> = {},
|
|
10
|
+
): HydratedIssue {
|
|
11
|
+
return {
|
|
12
|
+
id: "issue_abc123",
|
|
13
|
+
identifier: "ENG-42",
|
|
14
|
+
title: "Test Issue",
|
|
15
|
+
description: "Test description",
|
|
16
|
+
stateName: "Backlog",
|
|
17
|
+
stateType: "backlog",
|
|
18
|
+
labels: ["epic"],
|
|
19
|
+
parentIdentifier: undefined,
|
|
20
|
+
priority: 3,
|
|
21
|
+
createdAt: new Date("2024-01-01T00:00:00Z"),
|
|
22
|
+
updatedAt: new Date("2024-01-01T00:00:00Z"),
|
|
23
|
+
_raw: {},
|
|
24
|
+
...overrides,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe("LinearAdapter - Dependency Operations", () => {
|
|
29
|
+
const mockConfig: LinearConfig = {
|
|
30
|
+
apiKey: "lin_api_test123",
|
|
31
|
+
teamId: "team_abc",
|
|
32
|
+
projectId: "proj_container",
|
|
33
|
+
defaultLabels: {
|
|
34
|
+
prd: "prd",
|
|
35
|
+
epic: "epic",
|
|
36
|
+
task: "task",
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
let adapter: LinearAdapter;
|
|
41
|
+
|
|
42
|
+
beforeEach(async () => {
|
|
43
|
+
const { LinearAdapter: LA } = await import("../linear/adapter.js");
|
|
44
|
+
adapter = new LA(mockConfig);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe("addDependency", () => {
|
|
48
|
+
test("creates blocked relation between two epics", async () => {
|
|
49
|
+
const blockerIssue = createMockIssue({
|
|
50
|
+
id: "issue_epic_1",
|
|
51
|
+
identifier: "ENG-42",
|
|
52
|
+
title: "Epic 1",
|
|
53
|
+
labels: ["epic"],
|
|
54
|
+
parentIdentifier: undefined,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const blockedIssue = createMockIssue({
|
|
58
|
+
id: "issue_epic_2",
|
|
59
|
+
identifier: "ENG-43",
|
|
60
|
+
title: "Epic 2",
|
|
61
|
+
labels: ["epic"],
|
|
62
|
+
parentIdentifier: undefined,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
let fetchCallCount = 0;
|
|
66
|
+
(adapter as any).fetchIssue = mock(async () => {
|
|
67
|
+
fetchCallCount++;
|
|
68
|
+
if (fetchCallCount === 1) return blockedIssue;
|
|
69
|
+
return blockerIssue;
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const mockCreateRelation = mock(async () => ({
|
|
73
|
+
id: "relation_1",
|
|
74
|
+
type: "blocks",
|
|
75
|
+
}));
|
|
76
|
+
|
|
77
|
+
(adapter as any).client = {
|
|
78
|
+
execute: mock(async (fn: () => Promise<any>) => fn()),
|
|
79
|
+
client: {
|
|
80
|
+
createIssueRelation: mockCreateRelation,
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
await adapter.addDependency("ENG-43", "ENG-42");
|
|
85
|
+
|
|
86
|
+
expect(mockCreateRelation).toHaveBeenCalledWith({
|
|
87
|
+
issueId: "issue_epic_1", // blocker
|
|
88
|
+
relatedIssueId: "issue_epic_2", // blocked
|
|
89
|
+
type: "blocks",
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("creates blocked relation between two tasks", async () => {
|
|
94
|
+
const blockerTask = createMockIssue({
|
|
95
|
+
id: "issue_task_1",
|
|
96
|
+
identifier: "ENG-44",
|
|
97
|
+
title: "Task 1",
|
|
98
|
+
labels: ["task"],
|
|
99
|
+
parentIdentifier: "ENG-42",
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const blockedTask = createMockIssue({
|
|
103
|
+
id: "issue_task_2",
|
|
104
|
+
identifier: "ENG-45",
|
|
105
|
+
title: "Task 2",
|
|
106
|
+
labels: ["task"],
|
|
107
|
+
parentIdentifier: "ENG-42",
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
let fetchCallCount = 0;
|
|
111
|
+
(adapter as any).fetchIssue = mock(async () => {
|
|
112
|
+
fetchCallCount++;
|
|
113
|
+
if (fetchCallCount === 1) return blockedTask;
|
|
114
|
+
return blockerTask;
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const mockCreateRelation = mock(async () => ({
|
|
118
|
+
id: "relation_2",
|
|
119
|
+
type: "blocks",
|
|
120
|
+
}));
|
|
121
|
+
|
|
122
|
+
(adapter as any).client = {
|
|
123
|
+
execute: mock(async (fn: () => Promise<any>) => fn()),
|
|
124
|
+
client: {
|
|
125
|
+
createIssueRelation: mockCreateRelation,
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
await adapter.addDependency("ENG-45", "ENG-44");
|
|
130
|
+
|
|
131
|
+
expect(mockCreateRelation).toHaveBeenCalledWith({
|
|
132
|
+
issueId: "issue_task_1",
|
|
133
|
+
relatedIssueId: "issue_task_2",
|
|
134
|
+
type: "blocks",
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("throws error if dependsOnRef issue not found", async () => {
|
|
139
|
+
const blockedIssue = createMockIssue({
|
|
140
|
+
id: "issue_epic_1",
|
|
141
|
+
identifier: "ENG-42",
|
|
142
|
+
labels: ["epic"],
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
let fetchCallCount = 0;
|
|
146
|
+
(adapter as any).fetchIssue = mock(async () => {
|
|
147
|
+
fetchCallCount++;
|
|
148
|
+
if (fetchCallCount === 1) return blockedIssue;
|
|
149
|
+
return null;
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
await expect(
|
|
153
|
+
adapter.addDependency("ENG-42", "INVALID-1"),
|
|
154
|
+
).rejects.toThrow("Issue not found: INVALID-1");
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test("throws error if entity types don't match (epic and task)", async () => {
|
|
158
|
+
const epicIssue = createMockIssue({
|
|
159
|
+
id: "issue_epic_1",
|
|
160
|
+
identifier: "ENG-42",
|
|
161
|
+
labels: ["epic"],
|
|
162
|
+
parentIdentifier: undefined,
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
const taskIssue = createMockIssue({
|
|
166
|
+
id: "issue_task_1",
|
|
167
|
+
identifier: "ENG-43",
|
|
168
|
+
labels: ["task"],
|
|
169
|
+
parentIdentifier: "ENG-42",
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
let fetchCallCount = 0;
|
|
173
|
+
(adapter as any).fetchIssue = mock(async () => {
|
|
174
|
+
fetchCallCount++;
|
|
175
|
+
if (fetchCallCount === 1) return epicIssue;
|
|
176
|
+
return taskIssue;
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
await expect(adapter.addDependency("ENG-42", "ENG-43")).rejects.toThrow(
|
|
180
|
+
"Cannot create dependency between different entity types (epic and task)",
|
|
181
|
+
);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test("throws error for self-dependency", async () => {
|
|
185
|
+
await expect(adapter.addDependency("ENG-42", "ENG-42")).rejects.toThrow(
|
|
186
|
+
"Entity cannot depend on itself",
|
|
187
|
+
);
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
describe("removeDependency", () => {
|
|
192
|
+
test("removes blocked relation between issues", async () => {
|
|
193
|
+
const mockDelete = mock(async () => ({ success: true }));
|
|
194
|
+
const mockRelation = {
|
|
195
|
+
id: "relation_1",
|
|
196
|
+
type: "blocks",
|
|
197
|
+
issue: { id: "issue_epic_1", identifier: "ENG-42" },
|
|
198
|
+
relatedIssue: { id: "issue_epic_2", identifier: "ENG-43" },
|
|
199
|
+
delete: mockDelete,
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
const blockerIssue = createMockIssue({
|
|
203
|
+
id: "issue_epic_1",
|
|
204
|
+
identifier: "ENG-42",
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
const blockedIssue = createMockIssue({
|
|
208
|
+
id: "issue_epic_2",
|
|
209
|
+
identifier: "ENG-43",
|
|
210
|
+
_raw: {
|
|
211
|
+
relations: mock(async () => ({
|
|
212
|
+
nodes: [mockRelation],
|
|
213
|
+
})),
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
let fetchCallCount = 0;
|
|
218
|
+
(adapter as any).fetchIssue = mock(async () => {
|
|
219
|
+
fetchCallCount++;
|
|
220
|
+
if (fetchCallCount === 1) return blockedIssue;
|
|
221
|
+
return blockerIssue;
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
(adapter as any).client = {
|
|
225
|
+
execute: mock(async (fn: () => Promise<any>) => fn()),
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
await adapter.removeDependency("ENG-43", "ENG-42");
|
|
229
|
+
|
|
230
|
+
expect(mockDelete).toHaveBeenCalled();
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test("throws error if dependsOnRef issue not found", async () => {
|
|
234
|
+
const blockedIssue = createMockIssue({
|
|
235
|
+
id: "issue_epic_1",
|
|
236
|
+
identifier: "ENG-42",
|
|
237
|
+
_raw: {
|
|
238
|
+
relations: mock(async () => ({ nodes: [] })),
|
|
239
|
+
},
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
let fetchCallCount = 0;
|
|
243
|
+
(adapter as any).fetchIssue = mock(async () => {
|
|
244
|
+
fetchCallCount++;
|
|
245
|
+
if (fetchCallCount === 1) return blockedIssue;
|
|
246
|
+
return null;
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
(adapter as any).client = {
|
|
250
|
+
execute: mock(async (fn: () => Promise<any>) => fn()),
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
await expect(
|
|
254
|
+
adapter.removeDependency("ENG-42", "INVALID-1"),
|
|
255
|
+
).rejects.toThrow("Issue not found: INVALID-1");
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test("throws error if relation not found", async () => {
|
|
259
|
+
const blockerIssue = createMockIssue({
|
|
260
|
+
id: "issue_epic_1",
|
|
261
|
+
identifier: "ENG-42",
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
const blockedIssue = createMockIssue({
|
|
265
|
+
id: "issue_epic_2",
|
|
266
|
+
identifier: "ENG-43",
|
|
267
|
+
_raw: {
|
|
268
|
+
relations: mock(async () => ({
|
|
269
|
+
nodes: [], // No relations
|
|
270
|
+
})),
|
|
271
|
+
},
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
let fetchCallCount = 0;
|
|
275
|
+
(adapter as any).fetchIssue = mock(async () => {
|
|
276
|
+
fetchCallCount++;
|
|
277
|
+
if (fetchCallCount === 1) return blockedIssue;
|
|
278
|
+
return blockerIssue;
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
(adapter as any).client = {
|
|
282
|
+
execute: mock(async (fn: () => Promise<any>) => fn()),
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
await expect(
|
|
286
|
+
adapter.removeDependency("ENG-43", "ENG-42"),
|
|
287
|
+
).rejects.toThrow("Dependency not found between ENG-43 and ENG-42");
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
describe("getDependencies", () => {
|
|
292
|
+
test("returns list of blocking issue refs", async () => {
|
|
293
|
+
const mockIssue = createMockIssue({
|
|
294
|
+
id: "issue_epic_1",
|
|
295
|
+
identifier: "ENG-43",
|
|
296
|
+
_raw: {
|
|
297
|
+
relations: mock(async () => ({
|
|
298
|
+
nodes: [
|
|
299
|
+
{
|
|
300
|
+
id: "relation_1",
|
|
301
|
+
type: "blocks",
|
|
302
|
+
issue: { id: "issue_epic_2", identifier: "ENG-42" },
|
|
303
|
+
relatedIssue: { id: "issue_epic_1", identifier: "ENG-43" },
|
|
304
|
+
},
|
|
305
|
+
{
|
|
306
|
+
id: "relation_2",
|
|
307
|
+
type: "blocks",
|
|
308
|
+
issue: { id: "issue_epic_3", identifier: "ENG-44" },
|
|
309
|
+
relatedIssue: { id: "issue_epic_1", identifier: "ENG-43" },
|
|
310
|
+
},
|
|
311
|
+
],
|
|
312
|
+
})),
|
|
313
|
+
},
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
(adapter as any).fetchIssue = mock(async () => mockIssue);
|
|
317
|
+
(adapter as any).client = {
|
|
318
|
+
execute: mock(async (fn: () => Promise<any>) => fn()),
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
const dependencies = await adapter.getDependencies("ENG-43");
|
|
322
|
+
|
|
323
|
+
expect(dependencies).toEqual(["ENG-42", "ENG-44"]);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
test("returns empty array if no dependencies", async () => {
|
|
327
|
+
const mockIssue = createMockIssue({
|
|
328
|
+
id: "issue_epic_1",
|
|
329
|
+
identifier: "ENG-42",
|
|
330
|
+
_raw: {
|
|
331
|
+
relations: mock(async () => ({
|
|
332
|
+
nodes: [],
|
|
333
|
+
})),
|
|
334
|
+
},
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
(adapter as any).fetchIssue = mock(async () => mockIssue);
|
|
338
|
+
(adapter as any).client = {
|
|
339
|
+
execute: mock(async (fn: () => Promise<any>) => fn()),
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
const dependencies = await adapter.getDependencies("ENG-42");
|
|
343
|
+
|
|
344
|
+
expect(dependencies).toEqual([]);
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
test("filters out non-blocking relations", async () => {
|
|
348
|
+
const mockIssue = createMockIssue({
|
|
349
|
+
id: "issue_epic_1",
|
|
350
|
+
identifier: "ENG-43",
|
|
351
|
+
_raw: {
|
|
352
|
+
relations: mock(async () => ({
|
|
353
|
+
nodes: [
|
|
354
|
+
{
|
|
355
|
+
id: "relation_1",
|
|
356
|
+
type: "blocks",
|
|
357
|
+
issue: { id: "issue_epic_2", identifier: "ENG-42" },
|
|
358
|
+
relatedIssue: { id: "issue_epic_1", identifier: "ENG-43" },
|
|
359
|
+
},
|
|
360
|
+
{
|
|
361
|
+
id: "relation_2",
|
|
362
|
+
type: "duplicate",
|
|
363
|
+
issue: { id: "issue_epic_3", identifier: "ENG-44" },
|
|
364
|
+
relatedIssue: { id: "issue_epic_1", identifier: "ENG-43" },
|
|
365
|
+
},
|
|
366
|
+
{
|
|
367
|
+
id: "relation_3",
|
|
368
|
+
type: "related",
|
|
369
|
+
issue: { id: "issue_epic_4", identifier: "ENG-45" },
|
|
370
|
+
relatedIssue: { id: "issue_epic_1", identifier: "ENG-43" },
|
|
371
|
+
},
|
|
372
|
+
],
|
|
373
|
+
})),
|
|
374
|
+
},
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
(adapter as any).fetchIssue = mock(async () => mockIssue);
|
|
378
|
+
(adapter as any).client = {
|
|
379
|
+
execute: mock(async (fn: () => Promise<any>) => fn()),
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
const dependencies = await adapter.getDependencies("ENG-43");
|
|
383
|
+
|
|
384
|
+
expect(dependencies).toEqual(["ENG-42"]);
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
test("throws error if issue not found", async () => {
|
|
388
|
+
(adapter as any).fetchIssue = mock(async () => null);
|
|
389
|
+
|
|
390
|
+
await expect(adapter.getDependencies("INVALID-1")).rejects.toThrow(
|
|
391
|
+
"Issue not found: INVALID-1",
|
|
392
|
+
);
|
|
393
|
+
});
|
|
394
|
+
});
|
|
395
|
+
});
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
|
+
import type { HydratedIssue, LinearAdapter } from "../linear/adapter.js";
|
|
3
|
+
import type { LinearConfig } from "../linear/types.js";
|
|
4
|
+
import type { Document } from "../types.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Helper to create a mock HydratedIssue
|
|
8
|
+
*/
|
|
9
|
+
function createMockIssue(
|
|
10
|
+
overrides: Partial<HydratedIssue> = {},
|
|
11
|
+
): HydratedIssue {
|
|
12
|
+
return {
|
|
13
|
+
id: "issue_abc123",
|
|
14
|
+
identifier: "ENG-42",
|
|
15
|
+
title: "Test Issue",
|
|
16
|
+
description: "Test description",
|
|
17
|
+
stateName: "Backlog",
|
|
18
|
+
stateType: "backlog",
|
|
19
|
+
labels: ["prd"],
|
|
20
|
+
parentIdentifier: undefined,
|
|
21
|
+
priority: 3,
|
|
22
|
+
createdAt: new Date("2024-01-01T00:00:00Z"),
|
|
23
|
+
updatedAt: new Date("2024-01-01T00:00:00Z"),
|
|
24
|
+
_raw: {},
|
|
25
|
+
...overrides,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe("LinearAdapter - Document Operations", () => {
|
|
30
|
+
const mockConfig: LinearConfig = {
|
|
31
|
+
apiKey: "lin_api_test123",
|
|
32
|
+
teamId: "team_123",
|
|
33
|
+
projectId: "proj_container",
|
|
34
|
+
defaultLabels: {
|
|
35
|
+
prd: "prd",
|
|
36
|
+
epic: "epic",
|
|
37
|
+
task: "task",
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
let adapter: LinearAdapter;
|
|
42
|
+
|
|
43
|
+
beforeEach(async () => {
|
|
44
|
+
const { LinearAdapter: LA } = await import("../linear/adapter.js");
|
|
45
|
+
adapter = new LA(mockConfig);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe("saveDocument", () => {
|
|
49
|
+
test("creates attachment for PRD and returns document with URL", async () => {
|
|
50
|
+
const mockPrdIssue = createMockIssue({
|
|
51
|
+
id: "issue_prd_123",
|
|
52
|
+
identifier: "ENG-1",
|
|
53
|
+
title: "Test PRD",
|
|
54
|
+
labels: ["prd"],
|
|
55
|
+
_raw: {
|
|
56
|
+
attachments: mock(async () => ({
|
|
57
|
+
nodes: [],
|
|
58
|
+
})),
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const mockAttachment = {
|
|
63
|
+
id: "att_123",
|
|
64
|
+
title: "prd.md",
|
|
65
|
+
url:
|
|
66
|
+
"data:text/markdown;base64," +
|
|
67
|
+
Buffer.from(
|
|
68
|
+
"# Product Requirements Document\n\nContent here...",
|
|
69
|
+
).toString("base64"),
|
|
70
|
+
createdAt: "2024-01-01T00:00:00Z",
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
(adapter as any).fetchIssue = mock(async () => mockPrdIssue);
|
|
74
|
+
(adapter as any).client = {
|
|
75
|
+
execute: mock(async (fn: () => Promise<any>) => fn()),
|
|
76
|
+
client: {
|
|
77
|
+
attachmentCreate: mock(async () => ({
|
|
78
|
+
attachment: mockAttachment,
|
|
79
|
+
success: true,
|
|
80
|
+
})),
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const doc: Document = {
|
|
85
|
+
prdRef: "ENG-1",
|
|
86
|
+
filename: "prd.md",
|
|
87
|
+
content: "# Product Requirements Document\n\nContent here...",
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const savedDoc = await adapter.saveDocument(doc);
|
|
91
|
+
|
|
92
|
+
expect(savedDoc.prdRef).toBe("ENG-1");
|
|
93
|
+
expect(savedDoc.filename).toBe("prd.md");
|
|
94
|
+
expect(savedDoc.content).toBe(doc.content);
|
|
95
|
+
expect(savedDoc.url).toContain("data:text/markdown;base64,");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("updates existing document if filename already exists", async () => {
|
|
99
|
+
const mockArchive = mock(async () => ({ success: true }));
|
|
100
|
+
const existingAttachment = {
|
|
101
|
+
id: "att_existing",
|
|
102
|
+
title: "prd.md",
|
|
103
|
+
url: "data:text/markdown;base64,b2xkIGNvbnRlbnQ=",
|
|
104
|
+
archive: mockArchive,
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const mockPrdIssue = createMockIssue({
|
|
108
|
+
id: "issue_prd_123",
|
|
109
|
+
identifier: "ENG-1",
|
|
110
|
+
title: "Test PRD",
|
|
111
|
+
labels: ["prd"],
|
|
112
|
+
_raw: {
|
|
113
|
+
attachments: mock(async () => ({
|
|
114
|
+
nodes: [existingAttachment],
|
|
115
|
+
})),
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const newAttachment = {
|
|
120
|
+
id: "att_new",
|
|
121
|
+
title: "prd.md",
|
|
122
|
+
url:
|
|
123
|
+
"data:text/markdown;base64," +
|
|
124
|
+
Buffer.from("# Updated Content").toString("base64"),
|
|
125
|
+
createdAt: "2024-01-02T00:00:00Z",
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
(adapter as any).fetchIssue = mock(async () => mockPrdIssue);
|
|
129
|
+
(adapter as any).client = {
|
|
130
|
+
execute: mock(async (fn: () => Promise<any>) => fn()),
|
|
131
|
+
client: {
|
|
132
|
+
attachmentCreate: mock(async () => ({
|
|
133
|
+
attachment: newAttachment,
|
|
134
|
+
success: true,
|
|
135
|
+
})),
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const doc: Document = {
|
|
140
|
+
prdRef: "ENG-1",
|
|
141
|
+
filename: "prd.md",
|
|
142
|
+
content: "# Updated Content",
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const savedDoc = await adapter.saveDocument(doc);
|
|
146
|
+
|
|
147
|
+
expect(savedDoc.url).toContain("data:text/markdown;base64,");
|
|
148
|
+
expect(mockArchive).toHaveBeenCalled();
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test("throws error when PRD does not exist", async () => {
|
|
152
|
+
(adapter as any).fetchIssue = mock(async () => null);
|
|
153
|
+
|
|
154
|
+
const doc: Document = {
|
|
155
|
+
prdRef: "ENG-999",
|
|
156
|
+
filename: "prd.md",
|
|
157
|
+
content: "Content",
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
await expect(adapter.saveDocument(doc)).rejects.toThrow(
|
|
161
|
+
"PRD ENG-999 not found",
|
|
162
|
+
);
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
describe("getDocuments", () => {
|
|
167
|
+
test("returns all documents for a PRD", async () => {
|
|
168
|
+
const mockPrdIssue = createMockIssue({
|
|
169
|
+
id: "issue_prd_123",
|
|
170
|
+
identifier: "ENG-1",
|
|
171
|
+
title: "Test PRD",
|
|
172
|
+
labels: ["prd"],
|
|
173
|
+
_raw: {
|
|
174
|
+
attachments: mock(async () => ({
|
|
175
|
+
nodes: [
|
|
176
|
+
{
|
|
177
|
+
id: "att_1",
|
|
178
|
+
title: "prd.md",
|
|
179
|
+
url:
|
|
180
|
+
"data:text/markdown;base64," +
|
|
181
|
+
Buffer.from("# PRD Content").toString("base64"),
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
id: "att_2",
|
|
185
|
+
title: "design.md",
|
|
186
|
+
url:
|
|
187
|
+
"data:text/markdown;base64," +
|
|
188
|
+
Buffer.from("# Design Doc").toString("base64"),
|
|
189
|
+
},
|
|
190
|
+
],
|
|
191
|
+
})),
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
(adapter as any).fetchIssue = mock(async () => mockPrdIssue);
|
|
196
|
+
(adapter as any).client = {
|
|
197
|
+
execute: mock(async (fn: () => Promise<any>) => fn()),
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const documents = await adapter.getDocuments("ENG-1");
|
|
201
|
+
|
|
202
|
+
expect(documents.length).toBe(2);
|
|
203
|
+
expect(documents[0].prdRef).toBe("ENG-1");
|
|
204
|
+
expect(documents[0].filename).toBe("prd.md");
|
|
205
|
+
expect(documents[0].content).toBe("# PRD Content");
|
|
206
|
+
expect(documents[0].url).toContain("data:text/markdown;base64,");
|
|
207
|
+
expect(documents[1].filename).toBe("design.md");
|
|
208
|
+
expect(documents[1].content).toBe("# Design Doc");
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test("returns empty array when PRD has no documents", async () => {
|
|
212
|
+
const mockPrdIssue = createMockIssue({
|
|
213
|
+
id: "issue_prd_123",
|
|
214
|
+
identifier: "ENG-1",
|
|
215
|
+
title: "Test PRD",
|
|
216
|
+
labels: ["prd"],
|
|
217
|
+
_raw: {
|
|
218
|
+
attachments: mock(async () => ({
|
|
219
|
+
nodes: [],
|
|
220
|
+
})),
|
|
221
|
+
},
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
(adapter as any).fetchIssue = mock(async () => mockPrdIssue);
|
|
225
|
+
(adapter as any).client = {
|
|
226
|
+
execute: mock(async (fn: () => Promise<any>) => fn()),
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const documents = await adapter.getDocuments("ENG-1");
|
|
230
|
+
|
|
231
|
+
expect(documents.length).toBe(0);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test("throws error when PRD does not exist", async () => {
|
|
235
|
+
(adapter as any).fetchIssue = mock(async () => null);
|
|
236
|
+
|
|
237
|
+
await expect(adapter.getDocuments("ENG-999")).rejects.toThrow(
|
|
238
|
+
"PRD ENG-999 not found",
|
|
239
|
+
);
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
describe("deleteDocument", () => {
|
|
244
|
+
test("archives attachment matching filename", async () => {
|
|
245
|
+
const mockArchive = mock(async () => ({ success: true }));
|
|
246
|
+
const mockAttachment = {
|
|
247
|
+
id: "att_123",
|
|
248
|
+
title: "prd.md",
|
|
249
|
+
url: "data:text/markdown;base64,Y29udGVudA==",
|
|
250
|
+
archive: mockArchive,
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const mockPrdIssue = createMockIssue({
|
|
254
|
+
id: "issue_prd_123",
|
|
255
|
+
identifier: "ENG-1",
|
|
256
|
+
title: "Test PRD",
|
|
257
|
+
labels: ["prd"],
|
|
258
|
+
_raw: {
|
|
259
|
+
attachments: mock(async () => ({
|
|
260
|
+
nodes: [mockAttachment],
|
|
261
|
+
})),
|
|
262
|
+
},
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
(adapter as any).fetchIssue = mock(async () => mockPrdIssue);
|
|
266
|
+
(adapter as any).client = {
|
|
267
|
+
execute: mock(async (fn: () => Promise<any>) => fn()),
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
await adapter.deleteDocument("ENG-1", "prd.md");
|
|
271
|
+
|
|
272
|
+
expect(mockArchive).toHaveBeenCalled();
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test("throws error when document not found", async () => {
|
|
276
|
+
const mockPrdIssue = createMockIssue({
|
|
277
|
+
id: "issue_prd_123",
|
|
278
|
+
identifier: "ENG-1",
|
|
279
|
+
title: "Test PRD",
|
|
280
|
+
labels: ["prd"],
|
|
281
|
+
_raw: {
|
|
282
|
+
attachments: mock(async () => ({
|
|
283
|
+
nodes: [],
|
|
284
|
+
})),
|
|
285
|
+
},
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
(adapter as any).fetchIssue = mock(async () => mockPrdIssue);
|
|
289
|
+
(adapter as any).client = {
|
|
290
|
+
execute: mock(async (fn: () => Promise<any>) => fn()),
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
await expect(
|
|
294
|
+
adapter.deleteDocument("ENG-1", "nonexistent.md"),
|
|
295
|
+
).rejects.toThrow("Document nonexistent.md not found");
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
test("throws error when PRD does not exist", async () => {
|
|
299
|
+
(adapter as any).fetchIssue = mock(async () => null);
|
|
300
|
+
|
|
301
|
+
await expect(adapter.deleteDocument("ENG-999", "prd.md")).rejects.toThrow(
|
|
302
|
+
"PRD ENG-999 not found",
|
|
303
|
+
);
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
});
|