@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,243 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
extractTagFromLabels,
|
|
4
|
+
FLUX_MILESTONE_LABEL_PREFIX,
|
|
5
|
+
getMilestoneLabel,
|
|
6
|
+
type LINEAR_EPIC_STATUS_MAP,
|
|
7
|
+
type LINEAR_PRD_STATUS_MAP,
|
|
8
|
+
type LINEAR_PRIORITY_MAP,
|
|
9
|
+
type LINEAR_TASK_STATUS_MAP,
|
|
10
|
+
type LinearConfig,
|
|
11
|
+
type LinearIssue,
|
|
12
|
+
type LinearProject,
|
|
13
|
+
type LinearWorkflowState,
|
|
14
|
+
} from "../linear/types.js";
|
|
15
|
+
|
|
16
|
+
describe("Linear Types", () => {
|
|
17
|
+
describe("LinearConfig", () => {
|
|
18
|
+
test("has required fields", () => {
|
|
19
|
+
const config: LinearConfig = {
|
|
20
|
+
apiKey: "lin_api_test123",
|
|
21
|
+
teamId: "TEAM-123",
|
|
22
|
+
projectId: "proj_123",
|
|
23
|
+
defaultLabels: {
|
|
24
|
+
prd: "prd",
|
|
25
|
+
epic: "epic",
|
|
26
|
+
task: "task",
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
expect(config.apiKey).toBe("lin_api_test123");
|
|
31
|
+
expect(config.teamId).toBe("TEAM-123");
|
|
32
|
+
expect(config.projectId).toBe("proj_123");
|
|
33
|
+
expect(config.defaultLabels.prd).toBe("prd");
|
|
34
|
+
expect(config.defaultLabels.epic).toBe("epic");
|
|
35
|
+
expect(config.defaultLabels.task).toBe("task");
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe("LinearProject", () => {
|
|
40
|
+
test("has required Linear project fields", () => {
|
|
41
|
+
const project: LinearProject = {
|
|
42
|
+
id: "proj_abc123",
|
|
43
|
+
name: "Test Project",
|
|
44
|
+
description: "A test project",
|
|
45
|
+
state: "started",
|
|
46
|
+
createdAt: "2024-01-01T00:00:00Z",
|
|
47
|
+
updatedAt: "2024-01-02T00:00:00Z",
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
expect(project.id).toBe("proj_abc123");
|
|
51
|
+
expect(project.name).toBe("Test Project");
|
|
52
|
+
expect(project.state).toBe("started");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("allows optional description", () => {
|
|
56
|
+
const project: LinearProject = {
|
|
57
|
+
id: "proj_abc123",
|
|
58
|
+
name: "Test Project",
|
|
59
|
+
state: "started",
|
|
60
|
+
createdAt: "2024-01-01T00:00:00Z",
|
|
61
|
+
updatedAt: "2024-01-02T00:00:00Z",
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
expect(project.description).toBeUndefined();
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe("LinearIssue", () => {
|
|
69
|
+
test("has required Linear issue fields", () => {
|
|
70
|
+
const issue: LinearIssue = {
|
|
71
|
+
id: "issue_123",
|
|
72
|
+
identifier: "ENG-42",
|
|
73
|
+
title: "Test Issue",
|
|
74
|
+
description: "Issue description",
|
|
75
|
+
state: {
|
|
76
|
+
id: "state_1",
|
|
77
|
+
name: "In Progress",
|
|
78
|
+
type: "started",
|
|
79
|
+
},
|
|
80
|
+
priority: 2,
|
|
81
|
+
projectId: "proj_abc123",
|
|
82
|
+
createdAt: "2024-01-01T00:00:00Z",
|
|
83
|
+
updatedAt: "2024-01-02T00:00:00Z",
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
expect(issue.id).toBe("issue_123");
|
|
87
|
+
expect(issue.identifier).toBe("ENG-42");
|
|
88
|
+
expect(issue.state.name).toBe("In Progress");
|
|
89
|
+
expect(issue.priority).toBe(2);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("allows optional parent and labels", () => {
|
|
93
|
+
const issue: LinearIssue = {
|
|
94
|
+
id: "issue_123",
|
|
95
|
+
identifier: "ENG-43",
|
|
96
|
+
title: "Child Task",
|
|
97
|
+
state: {
|
|
98
|
+
id: "state_1",
|
|
99
|
+
name: "Backlog",
|
|
100
|
+
type: "backlog",
|
|
101
|
+
},
|
|
102
|
+
priority: 3,
|
|
103
|
+
createdAt: "2024-01-01T00:00:00Z",
|
|
104
|
+
updatedAt: "2024-01-02T00:00:00Z",
|
|
105
|
+
parentId: "issue_122",
|
|
106
|
+
labels: [{ id: "label_1", name: "task" }],
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
expect(issue.parentId).toBe("issue_122");
|
|
110
|
+
expect(issue.labels).toHaveLength(1);
|
|
111
|
+
expect(issue.labels?.[0].name).toBe("task");
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe("LinearWorkflowState", () => {
|
|
116
|
+
test("has required workflow state fields", () => {
|
|
117
|
+
const state: LinearWorkflowState = {
|
|
118
|
+
id: "state_1",
|
|
119
|
+
name: "In Progress",
|
|
120
|
+
type: "started",
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
expect(state.id).toBe("state_1");
|
|
124
|
+
expect(state.name).toBe("In Progress");
|
|
125
|
+
expect(state.type).toBe("started");
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe("Status Mapping Constants", () => {
|
|
130
|
+
test("LINEAR_PRD_STATUS_MAP maps all PRD statuses", () => {
|
|
131
|
+
const map: typeof LINEAR_PRD_STATUS_MAP = {
|
|
132
|
+
DRAFT: "backlog",
|
|
133
|
+
PENDING_REVIEW: "planned",
|
|
134
|
+
REVIEWED: "planned",
|
|
135
|
+
APPROVED: "started",
|
|
136
|
+
BREAKDOWN_READY: "started",
|
|
137
|
+
COMPLETED: "completed",
|
|
138
|
+
ARCHIVED: "canceled",
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
expect(map.DRAFT).toBe("backlog");
|
|
142
|
+
expect(map.PENDING_REVIEW).toBe("planned");
|
|
143
|
+
expect(map.REVIEWED).toBe("planned");
|
|
144
|
+
expect(map.APPROVED).toBe("started");
|
|
145
|
+
expect(map.BREAKDOWN_READY).toBe("started");
|
|
146
|
+
expect(map.COMPLETED).toBe("completed");
|
|
147
|
+
expect(map.ARCHIVED).toBe("canceled");
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test("LINEAR_EPIC_STATUS_MAP maps epic statuses", () => {
|
|
151
|
+
const map: typeof LINEAR_EPIC_STATUS_MAP = {
|
|
152
|
+
PENDING: "Todo",
|
|
153
|
+
IN_PROGRESS: "In Progress",
|
|
154
|
+
COMPLETED: "Done",
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
expect(map.PENDING).toBe("Todo");
|
|
158
|
+
expect(map.IN_PROGRESS).toBe("In Progress");
|
|
159
|
+
expect(map.COMPLETED).toBe("Done");
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("LINEAR_TASK_STATUS_MAP maps task statuses", () => {
|
|
163
|
+
const map: typeof LINEAR_TASK_STATUS_MAP = {
|
|
164
|
+
PENDING: "Todo",
|
|
165
|
+
IN_PROGRESS: "In Progress",
|
|
166
|
+
COMPLETED: "Done",
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
expect(map.PENDING).toBe("Todo");
|
|
170
|
+
expect(map.IN_PROGRESS).toBe("In Progress");
|
|
171
|
+
expect(map.COMPLETED).toBe("Done");
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test("LINEAR_PRIORITY_MAP maps priorities", () => {
|
|
175
|
+
const map: typeof LINEAR_PRIORITY_MAP = {
|
|
176
|
+
LOW: 4,
|
|
177
|
+
MEDIUM: 3,
|
|
178
|
+
HIGH: 2,
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
expect(map.LOW).toBe(4);
|
|
182
|
+
expect(map.MEDIUM).toBe(3);
|
|
183
|
+
expect(map.HIGH).toBe(2);
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
describe("Milestone Label Helpers", () => {
|
|
188
|
+
test("FLUX_MILESTONE_LABEL_PREFIX is correct", () => {
|
|
189
|
+
expect(FLUX_MILESTONE_LABEL_PREFIX).toBe("flux:milestone:");
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
describe("getMilestoneLabel", () => {
|
|
193
|
+
test("creates label from tag", () => {
|
|
194
|
+
expect(getMilestoneLabel("mvp")).toBe("flux:milestone:mvp");
|
|
195
|
+
expect(getMilestoneLabel("q1-release")).toBe(
|
|
196
|
+
"flux:milestone:q1-release",
|
|
197
|
+
);
|
|
198
|
+
expect(getMilestoneLabel("phase-1")).toBe("flux:milestone:phase-1");
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test("handles empty string", () => {
|
|
202
|
+
expect(getMilestoneLabel("")).toBe("flux:milestone:");
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test("handles special characters in tag", () => {
|
|
206
|
+
expect(getMilestoneLabel("v2.0.0")).toBe("flux:milestone:v2.0.0");
|
|
207
|
+
expect(getMilestoneLabel("release_2024")).toBe(
|
|
208
|
+
"flux:milestone:release_2024",
|
|
209
|
+
);
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
describe("extractTagFromLabels", () => {
|
|
214
|
+
test("extracts tag from milestone label", () => {
|
|
215
|
+
expect(extractTagFromLabels(["prd", "flux:milestone:mvp"])).toBe("mvp");
|
|
216
|
+
expect(extractTagFromLabels(["flux:milestone:q1-release", "prd"])).toBe(
|
|
217
|
+
"q1-release",
|
|
218
|
+
);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test("returns undefined when no milestone label exists", () => {
|
|
222
|
+
expect(extractTagFromLabels(["prd", "epic"])).toBeUndefined();
|
|
223
|
+
expect(extractTagFromLabels([])).toBeUndefined();
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test("returns first milestone label if multiple exist", () => {
|
|
227
|
+
expect(
|
|
228
|
+
extractTagFromLabels([
|
|
229
|
+
"prd",
|
|
230
|
+
"flux:milestone:first",
|
|
231
|
+
"flux:milestone:second",
|
|
232
|
+
]),
|
|
233
|
+
).toBe("first");
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test("handles labels similar to but not matching prefix", () => {
|
|
237
|
+
expect(extractTagFromLabels(["flux:status:reviewed"])).toBeUndefined();
|
|
238
|
+
expect(extractTagFromLabels(["flux-milestone-mvp"])).toBeUndefined();
|
|
239
|
+
expect(extractTagFromLabels(["milestone:mvp"])).toBeUndefined();
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
});
|
|
@@ -0,0 +1,441 @@
|
|
|
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: ["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 - Status Update and Delete 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
|
+
const { LinearAdapter: LA } = await import("../linear/adapter.js");
|
|
44
|
+
adapter = new LA(mockConfig);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe("updateStatus - PRD", () => {
|
|
48
|
+
test("updates PRD status to APPROVED (Linear: In Progress)", async () => {
|
|
49
|
+
const mockUpdate = mock(async () => ({}));
|
|
50
|
+
const mockIssue = createMockIssue({
|
|
51
|
+
id: "issue_prd_123",
|
|
52
|
+
identifier: "ENG-1",
|
|
53
|
+
title: "Test PRD",
|
|
54
|
+
stateName: "Backlog",
|
|
55
|
+
labels: ["prd"],
|
|
56
|
+
_raw: { update: mockUpdate },
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const mockUpdatedIssue = createMockIssue({
|
|
60
|
+
id: "issue_prd_123",
|
|
61
|
+
identifier: "ENG-1",
|
|
62
|
+
title: "Test PRD",
|
|
63
|
+
stateName: "In Progress",
|
|
64
|
+
stateType: "started",
|
|
65
|
+
labels: ["prd"],
|
|
66
|
+
_raw: { update: mockUpdate },
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// fetchIssue: 1st call returns original, subsequent calls return updated
|
|
70
|
+
let fetchCount = 0;
|
|
71
|
+
(adapter as any).fetchIssue = mock(async () => {
|
|
72
|
+
fetchCount++;
|
|
73
|
+
return fetchCount === 1 ? mockIssue : mockUpdatedIssue;
|
|
74
|
+
});
|
|
75
|
+
(adapter as any).getStateId = mock(async () => "state_progress");
|
|
76
|
+
(adapter as any).getLabelId = mock(
|
|
77
|
+
async (name: string) => `label_${name}`,
|
|
78
|
+
);
|
|
79
|
+
(adapter as any).client = {
|
|
80
|
+
execute: mock(async (fn: () => Promise<any>) => fn()),
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const result = await adapter.updatePrd("ENG-1", {
|
|
84
|
+
status: "APPROVED",
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
expect(result.status).toBe("APPROVED");
|
|
88
|
+
// Verify stateId is set in first update call
|
|
89
|
+
expect(mockUpdate).toHaveBeenCalledWith(
|
|
90
|
+
expect.objectContaining({ stateId: "state_progress" }),
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("updates PRD status to COMPLETED (Linear: Done)", async () => {
|
|
95
|
+
const mockUpdate = mock(async () => ({}));
|
|
96
|
+
const mockIssue = createMockIssue({
|
|
97
|
+
id: "issue_prd_123",
|
|
98
|
+
identifier: "ENG-1",
|
|
99
|
+
stateName: "In Progress",
|
|
100
|
+
labels: ["prd"],
|
|
101
|
+
_raw: { update: mockUpdate },
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const mockUpdatedIssue = createMockIssue({
|
|
105
|
+
id: "issue_prd_123",
|
|
106
|
+
identifier: "ENG-1",
|
|
107
|
+
stateName: "Done",
|
|
108
|
+
stateType: "completed",
|
|
109
|
+
labels: ["prd"],
|
|
110
|
+
_raw: { update: mockUpdate },
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// fetchIssue: 1st call returns original, subsequent calls return updated
|
|
114
|
+
let fetchCount = 0;
|
|
115
|
+
(adapter as any).fetchIssue = mock(async () => {
|
|
116
|
+
fetchCount++;
|
|
117
|
+
return fetchCount === 1 ? mockIssue : mockUpdatedIssue;
|
|
118
|
+
});
|
|
119
|
+
(adapter as any).getStateId = mock(async () => "state_done");
|
|
120
|
+
(adapter as any).getLabelId = mock(
|
|
121
|
+
async (name: string) => `label_${name}`,
|
|
122
|
+
);
|
|
123
|
+
(adapter as any).client = {
|
|
124
|
+
execute: mock(async (fn: () => Promise<any>) => fn()),
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const result = await adapter.updatePrd("ENG-1", {
|
|
128
|
+
status: "COMPLETED",
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
expect(result.status).toBe("COMPLETED");
|
|
132
|
+
// Verify stateId is set in first update call
|
|
133
|
+
expect(mockUpdate).toHaveBeenCalledWith(
|
|
134
|
+
expect.objectContaining({ stateId: "state_done" }),
|
|
135
|
+
);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe("updateStatus - Epic", () => {
|
|
140
|
+
test("updates Epic status to IN_PROGRESS (Linear: In Progress)", async () => {
|
|
141
|
+
const mockUpdate = mock(async () => ({}));
|
|
142
|
+
const mockIssue = createMockIssue({
|
|
143
|
+
id: "issue_123",
|
|
144
|
+
identifier: "ENG-42",
|
|
145
|
+
stateName: "Backlog",
|
|
146
|
+
labels: ["epic"],
|
|
147
|
+
parentIdentifier: "ENG-1",
|
|
148
|
+
_raw: { update: mockUpdate },
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
const mockUpdatedIssue = createMockIssue({
|
|
152
|
+
id: "issue_123",
|
|
153
|
+
identifier: "ENG-42",
|
|
154
|
+
stateName: "In Progress",
|
|
155
|
+
stateType: "started",
|
|
156
|
+
labels: ["epic"],
|
|
157
|
+
parentIdentifier: "ENG-1",
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// fetchIssue returns original first, then updated
|
|
161
|
+
let fetchCount = 0;
|
|
162
|
+
(adapter as any).fetchIssue = mock(async () => {
|
|
163
|
+
fetchCount++;
|
|
164
|
+
return fetchCount === 1 ? mockIssue : mockUpdatedIssue;
|
|
165
|
+
});
|
|
166
|
+
(adapter as any).getStateId = mock(async () => "state_progress");
|
|
167
|
+
(adapter as any).client = {
|
|
168
|
+
execute: mock(async (fn: () => Promise<any>) => fn()),
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const result = await adapter.updateEpic("ENG-42", {
|
|
172
|
+
status: "IN_PROGRESS",
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
expect(result.status).toBe("IN_PROGRESS");
|
|
176
|
+
expect(mockUpdate).toHaveBeenCalledWith({
|
|
177
|
+
stateId: "state_progress",
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test("updates Epic status to COMPLETED (Linear: Done)", async () => {
|
|
182
|
+
const mockUpdate = mock(async () => ({}));
|
|
183
|
+
const mockIssue = createMockIssue({
|
|
184
|
+
id: "issue_123",
|
|
185
|
+
identifier: "ENG-42",
|
|
186
|
+
stateName: "In Progress",
|
|
187
|
+
labels: ["epic"],
|
|
188
|
+
parentIdentifier: "ENG-1",
|
|
189
|
+
_raw: { update: mockUpdate },
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
const mockUpdatedIssue = createMockIssue({
|
|
193
|
+
id: "issue_123",
|
|
194
|
+
identifier: "ENG-42",
|
|
195
|
+
stateName: "Done",
|
|
196
|
+
stateType: "completed",
|
|
197
|
+
labels: ["epic"],
|
|
198
|
+
parentIdentifier: "ENG-1",
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// fetchIssue returns original first, then updated
|
|
202
|
+
let fetchCount = 0;
|
|
203
|
+
(adapter as any).fetchIssue = mock(async () => {
|
|
204
|
+
fetchCount++;
|
|
205
|
+
return fetchCount === 1 ? mockIssue : mockUpdatedIssue;
|
|
206
|
+
});
|
|
207
|
+
(adapter as any).getStateId = mock(async () => "state_done");
|
|
208
|
+
(adapter as any).client = {
|
|
209
|
+
execute: mock(async (fn: () => Promise<any>) => fn()),
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const result = await adapter.updateEpic("ENG-42", {
|
|
213
|
+
status: "COMPLETED",
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
expect(result.status).toBe("COMPLETED");
|
|
217
|
+
expect(mockUpdate).toHaveBeenCalledWith({ stateId: "state_done" });
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test("throws error when target workflow state not found", async () => {
|
|
221
|
+
const mockIssue = createMockIssue({
|
|
222
|
+
id: "issue_123",
|
|
223
|
+
identifier: "ENG-42",
|
|
224
|
+
labels: ["epic"],
|
|
225
|
+
parentIdentifier: "ENG-1",
|
|
226
|
+
_raw: { update: mock(async () => ({})) },
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
(adapter as any).fetchIssue = mock(async () => mockIssue);
|
|
230
|
+
(adapter as any).getStateId = mock(async () => {
|
|
231
|
+
throw new Error("Workflow state 'In Progress' not found");
|
|
232
|
+
});
|
|
233
|
+
(adapter as any).client = {
|
|
234
|
+
execute: mock(async (fn: () => Promise<any>) => fn()),
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
await expect(
|
|
238
|
+
adapter.updateEpic("ENG-42", { status: "IN_PROGRESS" }),
|
|
239
|
+
).rejects.toThrow("Workflow state 'In Progress' not found");
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
describe("updateStatus - Task", () => {
|
|
244
|
+
test("updates Task status to COMPLETED (Linear: Done)", async () => {
|
|
245
|
+
const mockUpdate = mock(async () => ({}));
|
|
246
|
+
const mockIssue = createMockIssue({
|
|
247
|
+
id: "issue_456",
|
|
248
|
+
identifier: "ENG-43",
|
|
249
|
+
stateName: "In Progress",
|
|
250
|
+
labels: ["task"],
|
|
251
|
+
parentIdentifier: "ENG-42",
|
|
252
|
+
_raw: { update: mockUpdate },
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
const mockUpdatedIssue = createMockIssue({
|
|
256
|
+
id: "issue_456",
|
|
257
|
+
identifier: "ENG-43",
|
|
258
|
+
stateName: "Done",
|
|
259
|
+
stateType: "completed",
|
|
260
|
+
labels: ["task"],
|
|
261
|
+
parentIdentifier: "ENG-42",
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// fetchIssue returns original first, then updated
|
|
265
|
+
let fetchCount = 0;
|
|
266
|
+
(adapter as any).fetchIssue = mock(async () => {
|
|
267
|
+
fetchCount++;
|
|
268
|
+
return fetchCount === 1 ? mockIssue : mockUpdatedIssue;
|
|
269
|
+
});
|
|
270
|
+
(adapter as any).client = {
|
|
271
|
+
execute: mock(async (fn: () => Promise<any>) => fn()),
|
|
272
|
+
client: {
|
|
273
|
+
team: mock(async () => ({
|
|
274
|
+
states: mock(async () => ({
|
|
275
|
+
nodes: [
|
|
276
|
+
{ id: "state_backlog", name: "Backlog" },
|
|
277
|
+
{ id: "state_progress", name: "In Progress" },
|
|
278
|
+
{ id: "state_done", name: "Done" },
|
|
279
|
+
],
|
|
280
|
+
})),
|
|
281
|
+
})),
|
|
282
|
+
},
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
const result = await adapter.updateTask("ENG-43", {
|
|
286
|
+
status: "COMPLETED",
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
expect(result.status).toBe("COMPLETED");
|
|
290
|
+
expect(mockUpdate).toHaveBeenCalledWith({ stateId: "state_done" });
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
describe("deletePrd", () => {
|
|
295
|
+
test("archives PRD and returns cascade count", async () => {
|
|
296
|
+
const mockTaskArchive = mock(async () => ({ success: true }));
|
|
297
|
+
const mockEpicArchive = mock(async () => ({ success: true }));
|
|
298
|
+
const mockPrdArchive = mock(async () => ({ success: true }));
|
|
299
|
+
|
|
300
|
+
const mockEpic1 = createMockIssue({
|
|
301
|
+
id: "issue_epic_1",
|
|
302
|
+
identifier: "ENG-42",
|
|
303
|
+
labels: ["epic"],
|
|
304
|
+
_raw: {
|
|
305
|
+
archive: mockEpicArchive,
|
|
306
|
+
children: mock(async () => ({
|
|
307
|
+
nodes: [
|
|
308
|
+
{ id: "task_1", archive: mockTaskArchive },
|
|
309
|
+
{ id: "task_2", archive: mockTaskArchive },
|
|
310
|
+
],
|
|
311
|
+
})),
|
|
312
|
+
},
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
const mockEpic2 = createMockIssue({
|
|
316
|
+
id: "issue_epic_2",
|
|
317
|
+
identifier: "ENG-43",
|
|
318
|
+
labels: ["epic"],
|
|
319
|
+
_raw: {
|
|
320
|
+
archive: mockEpicArchive,
|
|
321
|
+
children: mock(async () => ({ nodes: [] })),
|
|
322
|
+
},
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
const mockPrdIssue = createMockIssue({
|
|
326
|
+
id: "issue_prd_123",
|
|
327
|
+
identifier: "ENG-1",
|
|
328
|
+
labels: ["prd"],
|
|
329
|
+
_raw: {
|
|
330
|
+
archive: mockPrdArchive,
|
|
331
|
+
children: mock(async () => ({
|
|
332
|
+
nodes: [mockEpic1._raw, mockEpic2._raw],
|
|
333
|
+
})),
|
|
334
|
+
},
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
let hydrateCallCount = 0;
|
|
338
|
+
(adapter as any).fetchIssue = mock(async () => mockPrdIssue);
|
|
339
|
+
(adapter as any).hydrateIssue = mock(async () => {
|
|
340
|
+
hydrateCallCount++;
|
|
341
|
+
if (hydrateCallCount === 1) return mockEpic1;
|
|
342
|
+
return mockEpic2;
|
|
343
|
+
});
|
|
344
|
+
(adapter as any).client = {
|
|
345
|
+
execute: mock(async (fn: () => Promise<any>) => fn()),
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
const result = await adapter.deletePrd("ENG-1");
|
|
349
|
+
|
|
350
|
+
expect(result.deleted).toBe("ENG-1");
|
|
351
|
+
expect(result.cascade.epics).toBe(2);
|
|
352
|
+
expect(result.cascade.tasks).toBe(2);
|
|
353
|
+
expect(mockPrdArchive).toHaveBeenCalled();
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
describe("deleteEpic", () => {
|
|
358
|
+
test("archives Epic and its tasks, returns cascade count", async () => {
|
|
359
|
+
const mockTaskArchive = mock(async () => ({ success: true }));
|
|
360
|
+
const mockEpicArchive = mock(async () => ({ success: true }));
|
|
361
|
+
|
|
362
|
+
const mockEpic = createMockIssue({
|
|
363
|
+
id: "issue_123",
|
|
364
|
+
identifier: "ENG-42",
|
|
365
|
+
labels: ["epic"],
|
|
366
|
+
_raw: {
|
|
367
|
+
archive: mockEpicArchive,
|
|
368
|
+
children: mock(async () => ({
|
|
369
|
+
nodes: [
|
|
370
|
+
{ id: "task_1", archive: mockTaskArchive },
|
|
371
|
+
{ id: "task_2", archive: mockTaskArchive },
|
|
372
|
+
],
|
|
373
|
+
})),
|
|
374
|
+
},
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
(adapter as any).fetchIssue = mock(async () => mockEpic);
|
|
378
|
+
(adapter as any).client = {
|
|
379
|
+
execute: mock(async (fn: () => Promise<any>) => fn()),
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
const result = await adapter.deleteEpic("ENG-42");
|
|
383
|
+
|
|
384
|
+
expect(result.deleted).toBe("ENG-42");
|
|
385
|
+
expect(result.cascade.tasks).toBe(2);
|
|
386
|
+
expect(result.cascade.epics).toBe(0);
|
|
387
|
+
expect(mockEpicArchive).toHaveBeenCalled();
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
test("archives Epic with no tasks", async () => {
|
|
391
|
+
const mockEpicArchive = mock(async () => ({ success: true }));
|
|
392
|
+
|
|
393
|
+
const mockEpic = createMockIssue({
|
|
394
|
+
id: "issue_123",
|
|
395
|
+
identifier: "ENG-42",
|
|
396
|
+
labels: ["epic"],
|
|
397
|
+
_raw: {
|
|
398
|
+
archive: mockEpicArchive,
|
|
399
|
+
children: mock(async () => ({ nodes: [] })),
|
|
400
|
+
},
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
(adapter as any).fetchIssue = mock(async () => mockEpic);
|
|
404
|
+
(adapter as any).client = {
|
|
405
|
+
execute: mock(async (fn: () => Promise<any>) => fn()),
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
const result = await adapter.deleteEpic("ENG-42");
|
|
409
|
+
|
|
410
|
+
expect(result.deleted).toBe("ENG-42");
|
|
411
|
+
expect(result.cascade.tasks).toBe(0);
|
|
412
|
+
expect(mockEpicArchive).toHaveBeenCalled();
|
|
413
|
+
});
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
describe("deleteTask", () => {
|
|
417
|
+
test("archives Task", async () => {
|
|
418
|
+
const mockTaskArchive = mock(async () => ({ success: true }));
|
|
419
|
+
|
|
420
|
+
const mockTask = createMockIssue({
|
|
421
|
+
id: "issue_456",
|
|
422
|
+
identifier: "ENG-43",
|
|
423
|
+
labels: ["task"],
|
|
424
|
+
parentIdentifier: "ENG-42",
|
|
425
|
+
_raw: { archive: mockTaskArchive },
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
(adapter as any).fetchIssue = mock(async () => mockTask);
|
|
429
|
+
(adapter as any).client = {
|
|
430
|
+
execute: mock(async (fn: () => Promise<any>) => fn()),
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
const result = await adapter.deleteTask("ENG-43");
|
|
434
|
+
|
|
435
|
+
expect(result.deleted).toBe("ENG-43");
|
|
436
|
+
expect(result.cascade.tasks).toBe(0);
|
|
437
|
+
expect(result.cascade.epics).toBe(0);
|
|
438
|
+
expect(mockTaskArchive).toHaveBeenCalled();
|
|
439
|
+
});
|
|
440
|
+
});
|
|
441
|
+
});
|