@cliangdev/flux-plugin 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 +11 -7
- package/agents/coder.md +150 -25
- package/bin/install.cjs +171 -16
- package/commands/breakdown.md +47 -10
- package/commands/dashboard.md +29 -0
- package/commands/flux.md +92 -12
- package/commands/implement.md +166 -17
- package/commands/linear.md +6 -5
- package/commands/prd.md +996 -82
- package/manifest.json +2 -1
- package/package.json +9 -11
- package/skills/flux-orchestrator/SKILL.md +11 -3
- package/skills/prd-writer/SKILL.md +761 -0
- package/skills/ux-ui-design/SKILL.md +346 -0
- package/skills/ux-ui-design/references/design-tokens.md +359 -0
- package/src/__tests__/version.test.ts +37 -0
- package/src/adapters/local/.gitkeep +0 -0
- package/src/dashboard/__tests__/api.test.ts +211 -0
- package/src/dashboard/browser.ts +35 -0
- package/src/dashboard/public/app.js +869 -0
- package/src/dashboard/public/index.html +90 -0
- package/src/dashboard/public/styles.css +807 -0
- package/src/dashboard/public/vendor/highlight.css +10 -0
- package/src/dashboard/public/vendor/highlight.min.js +8422 -0
- package/src/dashboard/public/vendor/marked.min.js +2210 -0
- package/src/dashboard/server.ts +296 -0
- package/src/dashboard/watchers.ts +83 -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 +429 -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 +1141 -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 +1009 -0
- package/src/server/adapters/types.ts +293 -0
- package/src/server/config.ts +73 -0
- package/src/server/db/__tests__/queries.test.ts +473 -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 +97 -0
- package/src/server/db/sqlite.ts +10 -0
- package/src/server/index.ts +81 -0
- package/src/server/tools/__tests__/crud.test.ts +411 -0
- package/src/server/tools/__tests__/get-version.test.ts +27 -0
- package/src/server/tools/__tests__/mcp-interface.test.ts +479 -0
- package/src/server/tools/__tests__/query.test.ts +405 -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 +44 -0
- package/src/server/tools/create-prd.ts +40 -0
- package/src/server/tools/create-task.ts +47 -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 +240 -0
- package/src/server/tools/get-linear-url.ts +28 -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 +158 -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 +219 -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 +214 -0
- package/src/version.ts +5 -0
- package/dist/server/index.js +0 -87063
- package/skills/prd-template/SKILL.md +0 -242
|
@@ -0,0 +1,620 @@
|
|
|
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 with PRD defaults
|
|
7
|
+
*/
|
|
8
|
+
function createMockPrdIssue(
|
|
9
|
+
overrides: Partial<HydratedIssue> = {},
|
|
10
|
+
): HydratedIssue {
|
|
11
|
+
return {
|
|
12
|
+
id: "issue_abc123",
|
|
13
|
+
identifier: "ENG-42",
|
|
14
|
+
title: "Test PRD",
|
|
15
|
+
description: "Test description",
|
|
16
|
+
stateName: "Backlog",
|
|
17
|
+
stateType: "backlog",
|
|
18
|
+
labels: ["prd"],
|
|
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 - PRD CRUD Operations", () => {
|
|
29
|
+
const mockConfig: LinearConfig = {
|
|
30
|
+
apiKey: "lin_api_test123",
|
|
31
|
+
teamId: "team_123",
|
|
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
|
+
// Dynamic import to avoid module caching issues
|
|
44
|
+
const { LinearAdapter: LA } = await import("../linear/adapter.js");
|
|
45
|
+
adapter = new LA(mockConfig);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe("createPrd", () => {
|
|
49
|
+
test("creates Linear Issue with prd label and returns Prd", async () => {
|
|
50
|
+
const mockCreatedIssue = createMockPrdIssue({
|
|
51
|
+
id: "issue_abc123",
|
|
52
|
+
identifier: "ENG-42",
|
|
53
|
+
title: "User Authentication",
|
|
54
|
+
description: "Implement user login and registration",
|
|
55
|
+
stateName: "Backlog",
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Mock getLabelId
|
|
59
|
+
(adapter as any).getLabelId = mock(async () => "label_prd_123");
|
|
60
|
+
|
|
61
|
+
// Mock client.execute and createIssue
|
|
62
|
+
(adapter as any).client = {
|
|
63
|
+
execute: mock(async (fn: () => Promise<any>) => fn()),
|
|
64
|
+
client: {
|
|
65
|
+
createIssue: mock(async () => ({ issue: { id: "issue_abc123" } })),
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// Mock hydrateIssue to return our prepared result
|
|
70
|
+
(adapter as any).hydrateIssue = mock(async () => mockCreatedIssue);
|
|
71
|
+
|
|
72
|
+
const prd = await adapter.createPrd({
|
|
73
|
+
title: "User Authentication",
|
|
74
|
+
description: "Implement user login and registration",
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
expect(prd.id).toBe("issue_abc123");
|
|
78
|
+
expect(prd.projectId).toBe("proj_container");
|
|
79
|
+
expect(prd.ref).toBe("ENG-42");
|
|
80
|
+
expect(prd.title).toBe("User Authentication");
|
|
81
|
+
expect(prd.description).toBe("Implement user login and registration");
|
|
82
|
+
expect(prd.status).toBe("DRAFT"); // Backlog -> DRAFT
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("creates PRD without optional description", async () => {
|
|
86
|
+
const mockCreatedIssue = createMockPrdIssue({
|
|
87
|
+
id: "issue_xyz",
|
|
88
|
+
identifier: "ENG-99",
|
|
89
|
+
title: "Minimal PRD",
|
|
90
|
+
description: undefined,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
(adapter as any).getLabelId = mock(async () => "label_prd_123");
|
|
94
|
+
(adapter as any).client = {
|
|
95
|
+
execute: mock(async (fn: () => Promise<any>) => fn()),
|
|
96
|
+
client: {
|
|
97
|
+
createIssue: mock(async () => ({ issue: { id: "issue_xyz" } })),
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
(adapter as any).hydrateIssue = mock(async () => mockCreatedIssue);
|
|
101
|
+
|
|
102
|
+
const prd = await adapter.createPrd({
|
|
103
|
+
title: "Minimal PRD",
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
expect(prd.title).toBe("Minimal PRD");
|
|
107
|
+
expect(prd.description).toBeUndefined();
|
|
108
|
+
expect(prd.status).toBe("DRAFT");
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe("getPrd", () => {
|
|
113
|
+
test("returns Prd when issue exists and has prd label", async () => {
|
|
114
|
+
const mockIssue = createMockPrdIssue({
|
|
115
|
+
id: "issue_abc123",
|
|
116
|
+
identifier: "ENG-42",
|
|
117
|
+
title: "User Authentication",
|
|
118
|
+
description: "Implement user login",
|
|
119
|
+
stateName: "In Progress",
|
|
120
|
+
stateType: "started",
|
|
121
|
+
labels: ["prd"],
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
(adapter as any).fetchIssue = mock(async () => mockIssue);
|
|
125
|
+
|
|
126
|
+
const prd = await adapter.getPrd("ENG-42");
|
|
127
|
+
|
|
128
|
+
expect(prd).not.toBeNull();
|
|
129
|
+
expect(prd?.id).toBe("issue_abc123");
|
|
130
|
+
expect(prd?.ref).toBe("ENG-42");
|
|
131
|
+
expect(prd?.title).toBe("User Authentication");
|
|
132
|
+
expect(prd?.status).toBe("APPROVED"); // In Progress -> APPROVED
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("returns null when issue does not exist", async () => {
|
|
136
|
+
(adapter as any).fetchIssue = mock(async () => null);
|
|
137
|
+
|
|
138
|
+
const prd = await adapter.getPrd("ENG-999");
|
|
139
|
+
|
|
140
|
+
expect(prd).toBeNull();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("returns null when issue exists but lacks prd label", async () => {
|
|
144
|
+
const mockIssue = createMockPrdIssue({
|
|
145
|
+
identifier: "ENG-42",
|
|
146
|
+
labels: ["epic"], // epic label, not prd
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
(adapter as any).fetchIssue = mock(async () => mockIssue);
|
|
150
|
+
|
|
151
|
+
const prd = await adapter.getPrd("ENG-42");
|
|
152
|
+
|
|
153
|
+
expect(prd).toBeNull();
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
describe("updatePrd", () => {
|
|
158
|
+
test("updates issue and returns updated Prd", async () => {
|
|
159
|
+
const mockIssue = createMockPrdIssue({
|
|
160
|
+
identifier: "ENG-42",
|
|
161
|
+
title: "Old Title",
|
|
162
|
+
labels: ["prd"],
|
|
163
|
+
_raw: {
|
|
164
|
+
update: mock(async () => ({})),
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
const mockUpdatedIssue = createMockPrdIssue({
|
|
169
|
+
identifier: "ENG-42",
|
|
170
|
+
title: "User Authentication v2",
|
|
171
|
+
description: "Updated description",
|
|
172
|
+
stateName: "In Progress",
|
|
173
|
+
stateType: "started",
|
|
174
|
+
labels: ["prd"],
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// fetchIssue returns original first, then updated
|
|
178
|
+
let fetchCount = 0;
|
|
179
|
+
(adapter as any).fetchIssue = mock(async () => {
|
|
180
|
+
fetchCount++;
|
|
181
|
+
return fetchCount === 1 ? mockIssue : mockUpdatedIssue;
|
|
182
|
+
});
|
|
183
|
+
(adapter as any).client = {
|
|
184
|
+
execute: mock(async (fn: () => Promise<any>) => fn()),
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const prd = await adapter.updatePrd("ENG-42", {
|
|
188
|
+
title: "User Authentication v2",
|
|
189
|
+
description: "Updated description",
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
expect(prd.title).toBe("User Authentication v2");
|
|
193
|
+
expect(prd.description).toBe("Updated description");
|
|
194
|
+
expect(prd.status).toBe("APPROVED"); // In Progress -> APPROVED
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test("throws error when PRD not found", async () => {
|
|
198
|
+
(adapter as any).fetchIssue = mock(async () => null);
|
|
199
|
+
|
|
200
|
+
await expect(
|
|
201
|
+
adapter.updatePrd("ENG-999", { title: "New Title" }),
|
|
202
|
+
).rejects.toThrow("PRD not found: ENG-999");
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test("throws error when issue is not a PRD", async () => {
|
|
206
|
+
const mockIssue = createMockPrdIssue({
|
|
207
|
+
identifier: "ENG-42",
|
|
208
|
+
labels: ["epic"], // Not a PRD
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
(adapter as any).fetchIssue = mock(async () => mockIssue);
|
|
212
|
+
|
|
213
|
+
await expect(
|
|
214
|
+
adapter.updatePrd("ENG-42", { title: "New Title" }),
|
|
215
|
+
).rejects.toThrow("Issue ENG-42 is not a PRD");
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
describe("listPrds", () => {
|
|
220
|
+
test("returns paginated list of PRDs filtered by prd label", async () => {
|
|
221
|
+
const mockIssues = [
|
|
222
|
+
createMockPrdIssue({
|
|
223
|
+
id: "issue_1",
|
|
224
|
+
identifier: "ENG-1",
|
|
225
|
+
title: "PRD 1",
|
|
226
|
+
description: "Description 1",
|
|
227
|
+
stateName: "Backlog",
|
|
228
|
+
labels: ["prd"],
|
|
229
|
+
}),
|
|
230
|
+
createMockPrdIssue({
|
|
231
|
+
id: "issue_2",
|
|
232
|
+
identifier: "ENG-2",
|
|
233
|
+
title: "PRD 2",
|
|
234
|
+
description: "Description 2",
|
|
235
|
+
stateName: "In Progress",
|
|
236
|
+
labels: ["prd"],
|
|
237
|
+
}),
|
|
238
|
+
];
|
|
239
|
+
|
|
240
|
+
(adapter as any).fetchIssues = mock(async () => mockIssues);
|
|
241
|
+
|
|
242
|
+
const result = await adapter.listPrds({}, { limit: 2, offset: 0 });
|
|
243
|
+
|
|
244
|
+
expect(result.items.length).toBe(2);
|
|
245
|
+
expect(result.items[0].ref).toBe("ENG-1");
|
|
246
|
+
expect(result.items[0].title).toBe("PRD 1");
|
|
247
|
+
expect(result.items[0].status).toBe("DRAFT");
|
|
248
|
+
expect(result.items[1].ref).toBe("ENG-2");
|
|
249
|
+
expect(result.items[1].title).toBe("PRD 2");
|
|
250
|
+
expect(result.items[1].status).toBe("APPROVED");
|
|
251
|
+
expect(result.limit).toBe(2);
|
|
252
|
+
expect(result.offset).toBe(0);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
test("returns empty list when no PRDs exist", async () => {
|
|
256
|
+
(adapter as any).fetchIssues = mock(async () => []);
|
|
257
|
+
|
|
258
|
+
const result = await adapter.listPrds({}, { limit: 10, offset: 0 });
|
|
259
|
+
|
|
260
|
+
expect(result.items.length).toBe(0);
|
|
261
|
+
expect(result.total).toBe(0);
|
|
262
|
+
expect(result.hasMore).toBe(false);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
test("uses default pagination when not specified", async () => {
|
|
266
|
+
(adapter as any).fetchIssues = mock(async () => []);
|
|
267
|
+
|
|
268
|
+
const result = await adapter.listPrds();
|
|
269
|
+
|
|
270
|
+
expect(result.limit).toBe(50); // default limit
|
|
271
|
+
expect(result.offset).toBe(0); // default offset
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
test("filters out non-PRD issues", async () => {
|
|
275
|
+
const mockIssues = [
|
|
276
|
+
createMockPrdIssue({
|
|
277
|
+
id: "issue_1",
|
|
278
|
+
identifier: "ENG-1",
|
|
279
|
+
title: "PRD 1",
|
|
280
|
+
labels: ["prd"],
|
|
281
|
+
}),
|
|
282
|
+
createMockPrdIssue({
|
|
283
|
+
id: "issue_2",
|
|
284
|
+
identifier: "ENG-2",
|
|
285
|
+
title: "Epic (not a PRD)",
|
|
286
|
+
labels: ["epic"], // This should be filtered out
|
|
287
|
+
}),
|
|
288
|
+
];
|
|
289
|
+
|
|
290
|
+
(adapter as any).fetchIssues = mock(async () => mockIssues);
|
|
291
|
+
|
|
292
|
+
const result = await adapter.listPrds({}, { limit: 10, offset: 0 });
|
|
293
|
+
|
|
294
|
+
expect(result.items.length).toBe(1);
|
|
295
|
+
expect(result.items[0].ref).toBe("ENG-1");
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
describe("deletePrd", () => {
|
|
300
|
+
test("archives PRD issue and its children", async () => {
|
|
301
|
+
const mockTaskArchive = mock(async () => ({ success: true }));
|
|
302
|
+
const mockEpicArchive = mock(async () => ({ success: true }));
|
|
303
|
+
const mockPrdArchive = mock(async () => ({ success: true }));
|
|
304
|
+
|
|
305
|
+
const mockChildEpic = createMockPrdIssue({
|
|
306
|
+
id: "epic_1",
|
|
307
|
+
identifier: "ENG-2",
|
|
308
|
+
labels: ["epic"],
|
|
309
|
+
_raw: {
|
|
310
|
+
archive: mockEpicArchive,
|
|
311
|
+
children: mock(async () => ({
|
|
312
|
+
nodes: [
|
|
313
|
+
{
|
|
314
|
+
id: "task_1",
|
|
315
|
+
archive: mockTaskArchive,
|
|
316
|
+
},
|
|
317
|
+
],
|
|
318
|
+
})),
|
|
319
|
+
},
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
const mockPrdIssue = createMockPrdIssue({
|
|
323
|
+
id: "issue_abc123",
|
|
324
|
+
identifier: "ENG-1",
|
|
325
|
+
labels: ["prd"],
|
|
326
|
+
_raw: {
|
|
327
|
+
archive: mockPrdArchive,
|
|
328
|
+
children: mock(async () => ({
|
|
329
|
+
nodes: [mockChildEpic._raw],
|
|
330
|
+
})),
|
|
331
|
+
},
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
(adapter as any).fetchIssue = mock(async () => mockPrdIssue);
|
|
335
|
+
(adapter as any).hydrateIssue = mock(async () => mockChildEpic);
|
|
336
|
+
(adapter as any).client = {
|
|
337
|
+
execute: mock(async (fn: () => Promise<any>) => fn()),
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
const result = await adapter.deletePrd("ENG-1");
|
|
341
|
+
|
|
342
|
+
expect(result.deleted).toBe("ENG-1");
|
|
343
|
+
expect(result.cascade.epics).toBe(1);
|
|
344
|
+
expect(result.cascade.tasks).toBe(1);
|
|
345
|
+
expect(mockPrdArchive).toHaveBeenCalled();
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
test("throws error when PRD does not exist", async () => {
|
|
349
|
+
(adapter as any).fetchIssue = mock(async () => null);
|
|
350
|
+
|
|
351
|
+
await expect(adapter.deletePrd("ENG-999")).rejects.toThrow(
|
|
352
|
+
"PRD not found: ENG-999",
|
|
353
|
+
);
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
test("handles PRD with no children", async () => {
|
|
357
|
+
const mockPrdArchive = mock(async () => ({ success: true }));
|
|
358
|
+
const mockPrdIssue = createMockPrdIssue({
|
|
359
|
+
id: "issue_abc123",
|
|
360
|
+
identifier: "ENG-1",
|
|
361
|
+
labels: ["prd"],
|
|
362
|
+
_raw: {
|
|
363
|
+
archive: mockPrdArchive,
|
|
364
|
+
children: mock(async () => ({ nodes: [] })),
|
|
365
|
+
},
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
(adapter as any).fetchIssue = mock(async () => mockPrdIssue);
|
|
369
|
+
(adapter as any).client = {
|
|
370
|
+
execute: mock(async (fn: () => Promise<any>) => fn()),
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
const result = await adapter.deletePrd("ENG-1");
|
|
374
|
+
|
|
375
|
+
expect(result.deleted).toBe("ENG-1");
|
|
376
|
+
expect(result.cascade.epics).toBe(0);
|
|
377
|
+
expect(result.cascade.tasks).toBe(0);
|
|
378
|
+
expect(mockPrdArchive).toHaveBeenCalled();
|
|
379
|
+
});
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
describe("PRD Tag Support", () => {
|
|
383
|
+
test("createPrd with tag adds milestone label", async () => {
|
|
384
|
+
const mockCreatedIssue = createMockPrdIssue({
|
|
385
|
+
id: "issue_abc123",
|
|
386
|
+
identifier: "ENG-42",
|
|
387
|
+
title: "MVP Feature",
|
|
388
|
+
labels: ["prd", "flux:milestone:mvp-phase-1"],
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
let capturedCreateInput: any = null;
|
|
392
|
+
(adapter as any).getLabelId = mock(async () => "label_prd_123");
|
|
393
|
+
(adapter as any).getOrCreateLabel = mock(async (name: string) => {
|
|
394
|
+
if (name === "flux:milestone:mvp-phase-1") return "label_milestone_123";
|
|
395
|
+
return "label_other";
|
|
396
|
+
});
|
|
397
|
+
(adapter as any).client = {
|
|
398
|
+
execute: mock(async (fn: () => Promise<any>) => fn()),
|
|
399
|
+
client: {
|
|
400
|
+
createIssue: mock(async (input: any) => {
|
|
401
|
+
capturedCreateInput = input;
|
|
402
|
+
return { issue: { id: "issue_abc123" } };
|
|
403
|
+
}),
|
|
404
|
+
},
|
|
405
|
+
};
|
|
406
|
+
(adapter as any).hydrateIssue = mock(async () => mockCreatedIssue);
|
|
407
|
+
|
|
408
|
+
const prd = await adapter.createPrd({
|
|
409
|
+
title: "MVP Feature",
|
|
410
|
+
tag: "mvp-phase-1",
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
expect(prd.tag).toBe("mvp-phase-1");
|
|
414
|
+
expect(capturedCreateInput.labelIds).toContain("label_prd_123");
|
|
415
|
+
expect(capturedCreateInput.labelIds).toContain("label_milestone_123");
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
test("createPrd without tag does not add milestone label", async () => {
|
|
419
|
+
const mockCreatedIssue = createMockPrdIssue({
|
|
420
|
+
id: "issue_abc123",
|
|
421
|
+
identifier: "ENG-42",
|
|
422
|
+
title: "No Tag PRD",
|
|
423
|
+
labels: ["prd"],
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
let capturedCreateInput: any = null;
|
|
427
|
+
(adapter as any).getLabelId = mock(async () => "label_prd_123");
|
|
428
|
+
(adapter as any).client = {
|
|
429
|
+
execute: mock(async (fn: () => Promise<any>) => fn()),
|
|
430
|
+
client: {
|
|
431
|
+
createIssue: mock(async (input: any) => {
|
|
432
|
+
capturedCreateInput = input;
|
|
433
|
+
return { issue: { id: "issue_abc123" } };
|
|
434
|
+
}),
|
|
435
|
+
},
|
|
436
|
+
};
|
|
437
|
+
(adapter as any).hydrateIssue = mock(async () => mockCreatedIssue);
|
|
438
|
+
|
|
439
|
+
const prd = await adapter.createPrd({
|
|
440
|
+
title: "No Tag PRD",
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
expect(prd.tag).toBeUndefined();
|
|
444
|
+
expect(capturedCreateInput.labelIds).toHaveLength(1);
|
|
445
|
+
expect(capturedCreateInput.labelIds).toContain("label_prd_123");
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
test("getPrd extracts tag from milestone label", async () => {
|
|
449
|
+
const mockIssue = createMockPrdIssue({
|
|
450
|
+
id: "issue_abc123",
|
|
451
|
+
identifier: "ENG-42",
|
|
452
|
+
title: "Tagged PRD",
|
|
453
|
+
labels: ["prd", "flux:milestone:q1-release"],
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
(adapter as any).fetchIssue = mock(async () => mockIssue);
|
|
457
|
+
|
|
458
|
+
const prd = await adapter.getPrd("ENG-42");
|
|
459
|
+
|
|
460
|
+
expect(prd).not.toBeNull();
|
|
461
|
+
expect(prd?.tag).toBe("q1-release");
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
test("updatePrd can update tag", async () => {
|
|
465
|
+
const mockIssue = createMockPrdIssue({
|
|
466
|
+
identifier: "ENG-42",
|
|
467
|
+
title: "Test PRD",
|
|
468
|
+
labels: ["prd", "flux:milestone:old-tag"],
|
|
469
|
+
_raw: {
|
|
470
|
+
update: mock(async () => ({})),
|
|
471
|
+
},
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
const mockUpdatedIssue = createMockPrdIssue({
|
|
475
|
+
identifier: "ENG-42",
|
|
476
|
+
title: "Test PRD",
|
|
477
|
+
labels: ["prd", "flux:milestone:new-tag"],
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
let fetchCount = 0;
|
|
481
|
+
(adapter as any).fetchIssue = mock(async () => {
|
|
482
|
+
fetchCount++;
|
|
483
|
+
return fetchCount === 1 ? mockIssue : mockUpdatedIssue;
|
|
484
|
+
});
|
|
485
|
+
(adapter as any).client = {
|
|
486
|
+
execute: mock(async (fn: () => Promise<any>) => fn()),
|
|
487
|
+
};
|
|
488
|
+
(adapter as any).getOrCreateLabel = mock(async (name: string) => {
|
|
489
|
+
if (name === "prd") return "label_prd";
|
|
490
|
+
if (name === "flux:milestone:new-tag") return "label_new_tag";
|
|
491
|
+
return `label_${name}`;
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
const prd = await adapter.updatePrd("ENG-42", {
|
|
495
|
+
tag: "new-tag",
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
expect(prd.tag).toBe("new-tag");
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
test("updatePrd can remove tag by setting to null", async () => {
|
|
502
|
+
const mockIssue = createMockPrdIssue({
|
|
503
|
+
identifier: "ENG-42",
|
|
504
|
+
title: "Test PRD",
|
|
505
|
+
labels: ["prd", "flux:milestone:old-tag"],
|
|
506
|
+
_raw: {
|
|
507
|
+
update: mock(async () => ({})),
|
|
508
|
+
},
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
const mockUpdatedIssue = createMockPrdIssue({
|
|
512
|
+
identifier: "ENG-42",
|
|
513
|
+
title: "Test PRD",
|
|
514
|
+
labels: ["prd"], // No milestone label
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
let fetchCount = 0;
|
|
518
|
+
const capturedLabelIds: string[] = [];
|
|
519
|
+
(adapter as any).fetchIssue = mock(async () => {
|
|
520
|
+
fetchCount++;
|
|
521
|
+
return fetchCount === 1 ? mockIssue : mockUpdatedIssue;
|
|
522
|
+
});
|
|
523
|
+
(adapter as any).client = {
|
|
524
|
+
execute: mock(async (fn: () => Promise<any>) => fn()),
|
|
525
|
+
};
|
|
526
|
+
(adapter as any).getOrCreateLabel = mock(async (name: string) => {
|
|
527
|
+
const id = `label_${name.replace(/[^a-z]/g, "_")}`;
|
|
528
|
+
capturedLabelIds.push(id);
|
|
529
|
+
return id;
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
const prd = await adapter.updatePrd("ENG-42", {
|
|
533
|
+
tag: null as any, // Explicitly setting to null removes the tag
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
expect(prd.tag).toBeUndefined();
|
|
537
|
+
// Should not contain any milestone label IDs
|
|
538
|
+
expect(capturedLabelIds.some((id) => id.includes("milestone"))).toBe(
|
|
539
|
+
false,
|
|
540
|
+
);
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
test("updatePrd preserves tag when only updating status", async () => {
|
|
544
|
+
const mockIssue = createMockPrdIssue({
|
|
545
|
+
identifier: "ENG-42",
|
|
546
|
+
title: "Test PRD",
|
|
547
|
+
stateName: "Backlog",
|
|
548
|
+
labels: ["prd", "flux:milestone:mvp"],
|
|
549
|
+
_raw: {
|
|
550
|
+
update: mock(async () => ({})),
|
|
551
|
+
},
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
const mockUpdatedIssue = createMockPrdIssue({
|
|
555
|
+
identifier: "ENG-42",
|
|
556
|
+
title: "Test PRD",
|
|
557
|
+
stateName: "In Progress",
|
|
558
|
+
labels: ["prd", "flux:milestone:mvp"],
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
let fetchCount = 0;
|
|
562
|
+
const labelNames: string[] = [];
|
|
563
|
+
(adapter as any).fetchIssue = mock(async () => {
|
|
564
|
+
fetchCount++;
|
|
565
|
+
return fetchCount === 1 ? mockIssue : mockUpdatedIssue;
|
|
566
|
+
});
|
|
567
|
+
(adapter as any).client = {
|
|
568
|
+
execute: mock(async (fn: () => Promise<any>) => fn()),
|
|
569
|
+
};
|
|
570
|
+
(adapter as any).getStateId = mock(async () => "state_in_progress");
|
|
571
|
+
(adapter as any).getOrCreateLabel = mock(async (name: string) => {
|
|
572
|
+
labelNames.push(name);
|
|
573
|
+
return `label_${name.replace(/[^a-z]/g, "_")}`;
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
const prd = await adapter.updatePrd("ENG-42", {
|
|
577
|
+
status: "APPROVED",
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
expect(prd.tag).toBe("mvp");
|
|
581
|
+
// Milestone label should be preserved
|
|
582
|
+
expect(labelNames).toContain("flux:milestone:mvp");
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
test("listPrds filters by tag", async () => {
|
|
586
|
+
const mockIssues = [
|
|
587
|
+
createMockPrdIssue({
|
|
588
|
+
id: "issue_1",
|
|
589
|
+
identifier: "ENG-1",
|
|
590
|
+
title: "MVP PRD",
|
|
591
|
+
labels: ["prd", "flux:milestone:mvp"],
|
|
592
|
+
}),
|
|
593
|
+
createMockPrdIssue({
|
|
594
|
+
id: "issue_2",
|
|
595
|
+
identifier: "ENG-2",
|
|
596
|
+
title: "Q2 PRD",
|
|
597
|
+
labels: ["prd", "flux:milestone:q2-release"],
|
|
598
|
+
}),
|
|
599
|
+
];
|
|
600
|
+
|
|
601
|
+
let capturedFilter: any = null;
|
|
602
|
+
(adapter as any).fetchIssues = mock(async (filter: any) => {
|
|
603
|
+
capturedFilter = filter;
|
|
604
|
+
// Return only the MVP PRD when filtering by tag
|
|
605
|
+
return mockIssues.filter((i) =>
|
|
606
|
+
i.labels.includes("flux:milestone:mvp"),
|
|
607
|
+
);
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
const result = await adapter.listPrds({ tag: "mvp" });
|
|
611
|
+
|
|
612
|
+
expect(result.items.length).toBe(1);
|
|
613
|
+
expect(result.items[0].ref).toBe("ENG-1");
|
|
614
|
+
expect(result.items[0].tag).toBe("mvp");
|
|
615
|
+
// Verify the filter was constructed with AND condition
|
|
616
|
+
expect(capturedFilter.labels.and).toBeDefined();
|
|
617
|
+
expect(capturedFilter.labels.and).toHaveLength(2);
|
|
618
|
+
});
|
|
619
|
+
});
|
|
620
|
+
});
|