@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.
Files changed (108) hide show
  1. package/README.md +11 -7
  2. package/agents/coder.md +150 -25
  3. package/bin/install.cjs +171 -16
  4. package/commands/breakdown.md +47 -10
  5. package/commands/dashboard.md +29 -0
  6. package/commands/flux.md +92 -12
  7. package/commands/implement.md +166 -17
  8. package/commands/linear.md +6 -5
  9. package/commands/prd.md +996 -82
  10. package/manifest.json +2 -1
  11. package/package.json +9 -11
  12. package/skills/flux-orchestrator/SKILL.md +11 -3
  13. package/skills/prd-writer/SKILL.md +761 -0
  14. package/skills/ux-ui-design/SKILL.md +346 -0
  15. package/skills/ux-ui-design/references/design-tokens.md +359 -0
  16. package/src/__tests__/version.test.ts +37 -0
  17. package/src/adapters/local/.gitkeep +0 -0
  18. package/src/dashboard/__tests__/api.test.ts +211 -0
  19. package/src/dashboard/browser.ts +35 -0
  20. package/src/dashboard/public/app.js +869 -0
  21. package/src/dashboard/public/index.html +90 -0
  22. package/src/dashboard/public/styles.css +807 -0
  23. package/src/dashboard/public/vendor/highlight.css +10 -0
  24. package/src/dashboard/public/vendor/highlight.min.js +8422 -0
  25. package/src/dashboard/public/vendor/marked.min.js +2210 -0
  26. package/src/dashboard/server.ts +296 -0
  27. package/src/dashboard/watchers.ts +83 -0
  28. package/src/server/__tests__/config.test.ts +163 -0
  29. package/src/server/adapters/__tests__/a-client-linear.test.ts +197 -0
  30. package/src/server/adapters/__tests__/adapter-factory.test.ts +230 -0
  31. package/src/server/adapters/__tests__/dependency-ops.test.ts +429 -0
  32. package/src/server/adapters/__tests__/document-ops.test.ts +306 -0
  33. package/src/server/adapters/__tests__/linear-adapter.test.ts +91 -0
  34. package/src/server/adapters/__tests__/linear-config.test.ts +425 -0
  35. package/src/server/adapters/__tests__/linear-criteria-parser.test.ts +287 -0
  36. package/src/server/adapters/__tests__/linear-description-test.ts +238 -0
  37. package/src/server/adapters/__tests__/linear-epic-crud.test.ts +496 -0
  38. package/src/server/adapters/__tests__/linear-mappers-description.test.ts +276 -0
  39. package/src/server/adapters/__tests__/linear-mappers-epic.test.ts +294 -0
  40. package/src/server/adapters/__tests__/linear-mappers-prd.test.ts +300 -0
  41. package/src/server/adapters/__tests__/linear-mappers-task.test.ts +197 -0
  42. package/src/server/adapters/__tests__/linear-prd-crud.test.ts +620 -0
  43. package/src/server/adapters/__tests__/linear-stats.test.ts +450 -0
  44. package/src/server/adapters/__tests__/linear-task-crud.test.ts +534 -0
  45. package/src/server/adapters/__tests__/linear-types.test.ts +243 -0
  46. package/src/server/adapters/__tests__/status-ops.test.ts +441 -0
  47. package/src/server/adapters/factory.ts +90 -0
  48. package/src/server/adapters/index.ts +9 -0
  49. package/src/server/adapters/linear/adapter.ts +1141 -0
  50. package/src/server/adapters/linear/client.ts +169 -0
  51. package/src/server/adapters/linear/config.ts +152 -0
  52. package/src/server/adapters/linear/helpers/criteria-parser.ts +197 -0
  53. package/src/server/adapters/linear/helpers/index.ts +7 -0
  54. package/src/server/adapters/linear/index.ts +16 -0
  55. package/src/server/adapters/linear/mappers/description.ts +136 -0
  56. package/src/server/adapters/linear/mappers/epic.ts +81 -0
  57. package/src/server/adapters/linear/mappers/index.ts +27 -0
  58. package/src/server/adapters/linear/mappers/prd.ts +178 -0
  59. package/src/server/adapters/linear/mappers/task.ts +82 -0
  60. package/src/server/adapters/linear/types.ts +264 -0
  61. package/src/server/adapters/local-adapter.ts +1009 -0
  62. package/src/server/adapters/types.ts +293 -0
  63. package/src/server/config.ts +73 -0
  64. package/src/server/db/__tests__/queries.test.ts +473 -0
  65. package/src/server/db/ids.ts +17 -0
  66. package/src/server/db/index.ts +69 -0
  67. package/src/server/db/queries.ts +142 -0
  68. package/src/server/db/refs.ts +60 -0
  69. package/src/server/db/schema.ts +97 -0
  70. package/src/server/db/sqlite.ts +10 -0
  71. package/src/server/index.ts +81 -0
  72. package/src/server/tools/__tests__/crud.test.ts +411 -0
  73. package/src/server/tools/__tests__/get-version.test.ts +27 -0
  74. package/src/server/tools/__tests__/mcp-interface.test.ts +479 -0
  75. package/src/server/tools/__tests__/query.test.ts +405 -0
  76. package/src/server/tools/__tests__/z-configure-linear.test.ts +511 -0
  77. package/src/server/tools/__tests__/z-get-linear-url.test.ts +108 -0
  78. package/src/server/tools/configure-linear.ts +373 -0
  79. package/src/server/tools/create-epic.ts +44 -0
  80. package/src/server/tools/create-prd.ts +40 -0
  81. package/src/server/tools/create-task.ts +47 -0
  82. package/src/server/tools/criteria.ts +50 -0
  83. package/src/server/tools/delete-entity.ts +76 -0
  84. package/src/server/tools/dependencies.ts +55 -0
  85. package/src/server/tools/get-entity.ts +240 -0
  86. package/src/server/tools/get-linear-url.ts +28 -0
  87. package/src/server/tools/get-stats.ts +52 -0
  88. package/src/server/tools/get-version.ts +20 -0
  89. package/src/server/tools/index.ts +158 -0
  90. package/src/server/tools/init-project.ts +108 -0
  91. package/src/server/tools/query-entities.ts +167 -0
  92. package/src/server/tools/render-status.ts +219 -0
  93. package/src/server/tools/update-entity.ts +140 -0
  94. package/src/server/tools/update-status.ts +166 -0
  95. package/src/server/utils/__tests__/mcp-response.test.ts +331 -0
  96. package/src/server/utils/logger.ts +9 -0
  97. package/src/server/utils/mcp-response.ts +254 -0
  98. package/src/server/utils/status-transitions.ts +160 -0
  99. package/src/status-line/__tests__/status-line.test.ts +215 -0
  100. package/src/status-line/index.ts +147 -0
  101. package/src/utils/__tests__/chalk-import.test.ts +32 -0
  102. package/src/utils/__tests__/display.test.ts +97 -0
  103. package/src/utils/__tests__/status-renderer.test.ts +310 -0
  104. package/src/utils/display.ts +62 -0
  105. package/src/utils/status-renderer.ts +214 -0
  106. package/src/version.ts +5 -0
  107. package/dist/server/index.js +0 -87063
  108. package/skills/prd-template/SKILL.md +0 -242
@@ -0,0 +1,276 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import {
3
+ formatDescription,
4
+ parseDescription,
5
+ updateAcCompletion,
6
+ } from "../linear/mappers/description.js";
7
+
8
+ describe("Description Formatters", () => {
9
+ describe("formatDescription", () => {
10
+ test("embeds acceptance criteria as markdown checklist", () => {
11
+ const result = formatDescription({
12
+ description: "This is the epic description.",
13
+ acceptanceCriteria: ["First criterion", "Second criterion"],
14
+ });
15
+
16
+ expect(result).toContain("This is the epic description.");
17
+ expect(result).toContain("## Acceptance Criteria");
18
+ expect(result).toContain("- [ ] First criterion");
19
+ expect(result).toContain("- [ ] Second criterion");
20
+ });
21
+
22
+ test("handles empty description with AC", () => {
23
+ const result = formatDescription({
24
+ acceptanceCriteria: ["Only criterion"],
25
+ });
26
+
27
+ expect(result).toContain("## Acceptance Criteria");
28
+ expect(result).toContain("- [ ] Only criterion");
29
+ expect(result).not.toContain("undefined");
30
+ });
31
+
32
+ test("handles description with no AC", () => {
33
+ const result = formatDescription({
34
+ description: "Just a description",
35
+ });
36
+
37
+ expect(result).toBe("Just a description");
38
+ expect(result).not.toContain("## Acceptance Criteria");
39
+ });
40
+
41
+ test("handles empty AC array", () => {
42
+ const result = formatDescription({
43
+ description: "Description only",
44
+ acceptanceCriteria: [],
45
+ });
46
+
47
+ expect(result).toBe("Description only");
48
+ expect(result).not.toContain("## Acceptance Criteria");
49
+ });
50
+
51
+ test("does not duplicate AC section if already present", () => {
52
+ const existing = `Description text.
53
+
54
+ ## Acceptance Criteria
55
+ - [ ] Existing criterion`;
56
+
57
+ const result = formatDescription({
58
+ description: existing,
59
+ acceptanceCriteria: ["New criterion"],
60
+ });
61
+
62
+ // Should replace existing AC section
63
+ expect(result).toContain("- [ ] New criterion");
64
+ expect(result).not.toContain("Existing criterion");
65
+
66
+ // Should only have one AC section
67
+ const matches = result.match(/## Acceptance Criteria/g);
68
+ expect(matches).toHaveLength(1);
69
+ });
70
+
71
+ test("preserves newlines in description before AC section", () => {
72
+ const result = formatDescription({
73
+ description: "Line 1\n\nLine 2\n\nLine 3",
74
+ acceptanceCriteria: ["Criterion"],
75
+ });
76
+
77
+ expect(result).toContain("Line 1\n\nLine 2\n\nLine 3");
78
+ expect(result).toContain("## Acceptance Criteria");
79
+ expect(result).toContain("- [ ] Criterion");
80
+ });
81
+ });
82
+
83
+ describe("parseDescription", () => {
84
+ test("extracts AC from description", () => {
85
+ const content = `This is the description.
86
+
87
+ ## Acceptance Criteria
88
+ - [ ] First criterion
89
+ - [ ] Second criterion`;
90
+
91
+ const result = parseDescription(content);
92
+
93
+ expect(result.description).toBe("This is the description.");
94
+ expect(result.acceptanceCriteria).toHaveLength(2);
95
+ expect(result.acceptanceCriteria[0]).toEqual({
96
+ text: "First criterion",
97
+ isCompleted: false,
98
+ });
99
+ expect(result.acceptanceCriteria[1]).toEqual({
100
+ text: "Second criterion",
101
+ isCompleted: false,
102
+ });
103
+ });
104
+
105
+ test("extracts completed AC", () => {
106
+ const content = `Description.
107
+
108
+ ## Acceptance Criteria
109
+ - [x] Completed criterion
110
+ - [ ] Pending criterion`;
111
+
112
+ const result = parseDescription(content);
113
+
114
+ expect(result.acceptanceCriteria).toHaveLength(2);
115
+ expect(result.acceptanceCriteria[0]).toEqual({
116
+ text: "Completed criterion",
117
+ isCompleted: true,
118
+ });
119
+ expect(result.acceptanceCriteria[1]).toEqual({
120
+ text: "Pending criterion",
121
+ isCompleted: false,
122
+ });
123
+ });
124
+
125
+ test("handles description without AC section", () => {
126
+ const content = "Just a plain description.";
127
+
128
+ const result = parseDescription(content);
129
+
130
+ expect(result.description).toBe("Just a plain description.");
131
+ expect(result.acceptanceCriteria).toHaveLength(0);
132
+ });
133
+
134
+ test("handles empty description", () => {
135
+ const result = parseDescription("");
136
+
137
+ expect(result.description).toBe("");
138
+ expect(result.acceptanceCriteria).toHaveLength(0);
139
+ });
140
+
141
+ test("handles AC section with no items", () => {
142
+ const content = `Description.
143
+
144
+ ## Acceptance Criteria`;
145
+
146
+ const result = parseDescription(content);
147
+
148
+ expect(result.description).toBe("Description.");
149
+ expect(result.acceptanceCriteria).toHaveLength(0);
150
+ });
151
+
152
+ test("trims whitespace from description", () => {
153
+ const content = ` Description text
154
+
155
+ ## Acceptance Criteria
156
+ - [ ] Criterion`;
157
+
158
+ const result = parseDescription(content);
159
+
160
+ expect(result.description).toBe("Description text");
161
+ });
162
+
163
+ test("handles various checkbox formats", () => {
164
+ const content = `Description.
165
+
166
+ ## Acceptance Criteria
167
+ - [x] Completed with x
168
+ - [X] Completed with X
169
+ - [ ] Pending
170
+ - [ ] Extra spaces
171
+ - [ ] Empty checkbox`;
172
+
173
+ const result = parseDescription(content);
174
+
175
+ expect(result.acceptanceCriteria).toHaveLength(5);
176
+ expect(result.acceptanceCriteria[0].isCompleted).toBe(true);
177
+ expect(result.acceptanceCriteria[1].isCompleted).toBe(true);
178
+ expect(result.acceptanceCriteria[2].isCompleted).toBe(false);
179
+ expect(result.acceptanceCriteria[3].isCompleted).toBe(false);
180
+ expect(result.acceptanceCriteria[4].isCompleted).toBe(false);
181
+ });
182
+
183
+ test("preserves multi-line description before AC section", () => {
184
+ const content = `First line.
185
+
186
+ Second paragraph.
187
+
188
+ Third paragraph.
189
+
190
+ ## Acceptance Criteria
191
+ - [ ] Criterion`;
192
+
193
+ const result = parseDescription(content);
194
+
195
+ expect(result.description).toBe(
196
+ "First line.\n\nSecond paragraph.\n\nThird paragraph.",
197
+ );
198
+ });
199
+ });
200
+
201
+ describe("updateAcCompletion", () => {
202
+ test("marks AC as completed", () => {
203
+ const content = `Description.
204
+
205
+ ## Acceptance Criteria
206
+ - [ ] First criterion
207
+ - [ ] Second criterion`;
208
+
209
+ const result = updateAcCompletion(content, "First criterion", true);
210
+
211
+ expect(result).toContain("- [x] First criterion");
212
+ expect(result).toContain("- [ ] Second criterion");
213
+ });
214
+
215
+ test("marks AC as incomplete", () => {
216
+ const content = `Description.
217
+
218
+ ## Acceptance Criteria
219
+ - [x] First criterion
220
+ - [x] Second criterion`;
221
+
222
+ const result = updateAcCompletion(content, "Second criterion", false);
223
+
224
+ expect(result).toContain("- [x] First criterion");
225
+ expect(result).toContain("- [ ] Second criterion");
226
+ });
227
+
228
+ test("handles partial text matches by exact match only", () => {
229
+ const content = `Description.
230
+
231
+ ## Acceptance Criteria
232
+ - [ ] Test criterion
233
+ - [ ] Test criterion extended`;
234
+
235
+ const result = updateAcCompletion(content, "Test criterion", true);
236
+
237
+ expect(result).toContain("- [x] Test criterion\n");
238
+ expect(result).toContain("- [ ] Test criterion extended");
239
+ });
240
+
241
+ test("returns content unchanged if AC not found", () => {
242
+ const content = `Description.
243
+
244
+ ## Acceptance Criteria
245
+ - [ ] First criterion`;
246
+
247
+ const result = updateAcCompletion(content, "Nonexistent", true);
248
+
249
+ expect(result).toBe(content);
250
+ });
251
+
252
+ test("returns content unchanged if no AC section", () => {
253
+ const content = "Just a description.";
254
+
255
+ const result = updateAcCompletion(content, "Anything", true);
256
+
257
+ expect(result).toBe(content);
258
+ });
259
+
260
+ test("handles multiple updates", () => {
261
+ const content = `Description.
262
+
263
+ ## Acceptance Criteria
264
+ - [ ] First criterion
265
+ - [ ] Second criterion
266
+ - [ ] Third criterion`;
267
+
268
+ let result = updateAcCompletion(content, "First criterion", true);
269
+ result = updateAcCompletion(result, "Third criterion", true);
270
+
271
+ expect(result).toContain("- [x] First criterion");
272
+ expect(result).toContain("- [ ] Second criterion");
273
+ expect(result).toContain("- [x] Third criterion");
274
+ });
275
+ });
276
+ });
@@ -0,0 +1,294 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ // Import mapper functions - these will be implemented
3
+ import {
4
+ isEpicIssue,
5
+ toEpic,
6
+ toFluxEpicStatus,
7
+ toLinearEpicState,
8
+ } from "../linear/mappers/epic.js";
9
+ import type { LinearIssue } from "../linear/types.js";
10
+ import type { Epic } from "../types.js";
11
+
12
+ describe("Epic Mapper", () => {
13
+ describe("isEpicIssue", () => {
14
+ test("returns true when issue has epic label", () => {
15
+ const issue: LinearIssue = {
16
+ id: "issue_123",
17
+ identifier: "ENG-42",
18
+ title: "Epic Issue",
19
+ state: {
20
+ id: "state_1",
21
+ name: "Backlog",
22
+ type: "backlog",
23
+ },
24
+ priority: 2,
25
+ createdAt: "2024-01-01T00:00:00Z",
26
+ updatedAt: "2024-01-02T00:00:00Z",
27
+ labels: [
28
+ { id: "label_1", name: "epic" },
29
+ { id: "label_2", name: "backend" },
30
+ ],
31
+ };
32
+
33
+ expect(isEpicIssue(issue, "epic")).toBe(true);
34
+ });
35
+
36
+ test("returns false when issue does not have epic label", () => {
37
+ const issue: LinearIssue = {
38
+ id: "issue_123",
39
+ identifier: "ENG-42",
40
+ title: "Regular Issue",
41
+ state: {
42
+ id: "state_1",
43
+ name: "Backlog",
44
+ type: "backlog",
45
+ },
46
+ priority: 2,
47
+ createdAt: "2024-01-01T00:00:00Z",
48
+ updatedAt: "2024-01-02T00:00:00Z",
49
+ labels: [{ id: "label_2", name: "backend" }],
50
+ };
51
+
52
+ expect(isEpicIssue(issue, "epic")).toBe(false);
53
+ });
54
+
55
+ test("returns false when issue has no labels", () => {
56
+ const issue: LinearIssue = {
57
+ id: "issue_123",
58
+ identifier: "ENG-42",
59
+ title: "Regular Issue",
60
+ state: {
61
+ id: "state_1",
62
+ name: "Backlog",
63
+ type: "backlog",
64
+ },
65
+ priority: 2,
66
+ createdAt: "2024-01-01T00:00:00Z",
67
+ updatedAt: "2024-01-02T00:00:00Z",
68
+ };
69
+
70
+ expect(isEpicIssue(issue, "epic")).toBe(false);
71
+ });
72
+
73
+ test("matches custom epic label name", () => {
74
+ const issue: LinearIssue = {
75
+ id: "issue_123",
76
+ identifier: "ENG-42",
77
+ title: "Epic Issue",
78
+ state: {
79
+ id: "state_1",
80
+ name: "Backlog",
81
+ type: "backlog",
82
+ },
83
+ priority: 2,
84
+ createdAt: "2024-01-01T00:00:00Z",
85
+ updatedAt: "2024-01-02T00:00:00Z",
86
+ labels: [{ id: "label_1", name: "feature-epic" }],
87
+ };
88
+
89
+ expect(isEpicIssue(issue, "feature-epic")).toBe(true);
90
+ expect(isEpicIssue(issue, "epic")).toBe(false);
91
+ });
92
+ });
93
+
94
+ describe("toFluxEpicStatus", () => {
95
+ test("maps Backlog to PENDING", () => {
96
+ expect(toFluxEpicStatus("Backlog")).toBe("PENDING");
97
+ });
98
+
99
+ test("maps In Progress to IN_PROGRESS", () => {
100
+ expect(toFluxEpicStatus("In Progress")).toBe("IN_PROGRESS");
101
+ });
102
+
103
+ test("maps Done to COMPLETED", () => {
104
+ expect(toFluxEpicStatus("Done")).toBe("COMPLETED");
105
+ });
106
+
107
+ test("defaults to PENDING for unknown states", () => {
108
+ expect(toFluxEpicStatus("Unknown State")).toBe("PENDING");
109
+ expect(toFluxEpicStatus("Canceled")).toBe("PENDING");
110
+ expect(toFluxEpicStatus("")).toBe("PENDING");
111
+ });
112
+
113
+ test("is case-sensitive", () => {
114
+ expect(toFluxEpicStatus("backlog")).toBe("PENDING"); // lowercase
115
+ expect(toFluxEpicStatus("BACKLOG")).toBe("PENDING"); // uppercase
116
+ expect(toFluxEpicStatus("in progress")).toBe("PENDING"); // wrong case
117
+ });
118
+ });
119
+
120
+ describe("toLinearEpicState", () => {
121
+ test("maps PENDING to Todo", () => {
122
+ expect(toLinearEpicState("PENDING")).toBe("Todo");
123
+ });
124
+
125
+ test("maps IN_PROGRESS to In Progress", () => {
126
+ expect(toLinearEpicState("IN_PROGRESS")).toBe("In Progress");
127
+ });
128
+
129
+ test("maps COMPLETED to Done", () => {
130
+ expect(toLinearEpicState("COMPLETED")).toBe("Done");
131
+ });
132
+ });
133
+
134
+ describe("toEpic", () => {
135
+ test("transforms Linear Issue to Flux Epic with PENDING status", () => {
136
+ const linearIssue: LinearIssue = {
137
+ id: "issue_123",
138
+ identifier: "ENG-42",
139
+ title: "User Authentication Epic",
140
+ description: "Implement user auth system",
141
+ state: {
142
+ id: "state_1",
143
+ name: "Backlog",
144
+ type: "backlog",
145
+ },
146
+ priority: 2,
147
+ projectId: "proj_abc123",
148
+ createdAt: "2024-01-01T10:00:00Z",
149
+ updatedAt: "2024-01-02T15:30:00Z",
150
+ labels: [{ id: "label_1", name: "epic" }],
151
+ };
152
+
153
+ const prdId = "prd_xyz789";
154
+ const epic: Epic = toEpic(linearIssue, prdId);
155
+
156
+ expect(epic.id).toBe("issue_123");
157
+ expect(epic.prdId).toBe("prd_xyz789");
158
+ expect(epic.ref).toBe("ENG-42");
159
+ expect(epic.title).toBe("User Authentication Epic");
160
+ expect(epic.description).toBe("Implement user auth system");
161
+ expect(epic.status).toBe("PENDING");
162
+ expect(epic.createdAt).toBe("2024-01-01T10:00:00Z");
163
+ expect(epic.updatedAt).toBe("2024-01-02T15:30:00Z");
164
+ });
165
+
166
+ test("transforms Linear Issue to Flux Epic with IN_PROGRESS status", () => {
167
+ const linearIssue: LinearIssue = {
168
+ id: "issue_124",
169
+ identifier: "ENG-43",
170
+ title: "API Development",
171
+ description: "Build REST API",
172
+ state: {
173
+ id: "state_2",
174
+ name: "In Progress",
175
+ type: "started",
176
+ },
177
+ priority: 2,
178
+ projectId: "proj_abc123",
179
+ createdAt: "2024-01-03T10:00:00Z",
180
+ updatedAt: "2024-01-04T15:30:00Z",
181
+ };
182
+
183
+ const epic: Epic = toEpic(linearIssue, "prd_xyz789");
184
+
185
+ expect(epic.id).toBe("issue_124");
186
+ expect(epic.ref).toBe("ENG-43");
187
+ expect(epic.status).toBe("IN_PROGRESS");
188
+ });
189
+
190
+ test("transforms Linear Issue to Flux Epic with COMPLETED status", () => {
191
+ const linearIssue: LinearIssue = {
192
+ id: "issue_125",
193
+ identifier: "ENG-44",
194
+ title: "Database Setup",
195
+ state: {
196
+ id: "state_3",
197
+ name: "Done",
198
+ type: "completed",
199
+ },
200
+ priority: 2,
201
+ createdAt: "2024-01-05T10:00:00Z",
202
+ updatedAt: "2024-01-06T15:30:00Z",
203
+ };
204
+
205
+ const epic: Epic = toEpic(linearIssue, "prd_xyz789");
206
+
207
+ expect(epic.id).toBe("issue_125");
208
+ expect(epic.ref).toBe("ENG-44");
209
+ expect(epic.status).toBe("COMPLETED");
210
+ });
211
+
212
+ test("handles missing optional description", () => {
213
+ const linearIssue: LinearIssue = {
214
+ id: "issue_126",
215
+ identifier: "ENG-45",
216
+ title: "Epic without description",
217
+ state: {
218
+ id: "state_1",
219
+ name: "Backlog",
220
+ type: "backlog",
221
+ },
222
+ priority: 2,
223
+ createdAt: "2024-01-07T10:00:00Z",
224
+ updatedAt: "2024-01-08T15:30:00Z",
225
+ };
226
+
227
+ const epic: Epic = toEpic(linearIssue, "prd_xyz789");
228
+
229
+ expect(epic.description).toBeUndefined();
230
+ });
231
+
232
+ test("uses Linear issue ID as Flux Epic ID", () => {
233
+ const linearIssue: LinearIssue = {
234
+ id: "unique_linear_id_123",
235
+ identifier: "ENG-46",
236
+ title: "Test Epic",
237
+ state: {
238
+ id: "state_1",
239
+ name: "Backlog",
240
+ type: "backlog",
241
+ },
242
+ priority: 2,
243
+ createdAt: "2024-01-09T10:00:00Z",
244
+ updatedAt: "2024-01-10T15:30:00Z",
245
+ };
246
+
247
+ const epic: Epic = toEpic(linearIssue, "prd_xyz789");
248
+
249
+ expect(epic.id).toBe("unique_linear_id_123");
250
+ });
251
+
252
+ test("uses Linear identifier as Flux Epic ref", () => {
253
+ const linearIssue: LinearIssue = {
254
+ id: "issue_127",
255
+ identifier: "TEAM-999",
256
+ title: "Test Epic",
257
+ state: {
258
+ id: "state_1",
259
+ name: "Backlog",
260
+ type: "backlog",
261
+ },
262
+ priority: 2,
263
+ createdAt: "2024-01-11T10:00:00Z",
264
+ updatedAt: "2024-01-12T15:30:00Z",
265
+ };
266
+
267
+ const epic: Epic = toEpic(linearIssue, "prd_xyz789");
268
+
269
+ expect(epic.ref).toBe("TEAM-999");
270
+ });
271
+
272
+ test("associates epic with provided prdId", () => {
273
+ const linearIssue: LinearIssue = {
274
+ id: "issue_128",
275
+ identifier: "ENG-47",
276
+ title: "Test Epic",
277
+ state: {
278
+ id: "state_1",
279
+ name: "Backlog",
280
+ type: "backlog",
281
+ },
282
+ priority: 2,
283
+ createdAt: "2024-01-13T10:00:00Z",
284
+ updatedAt: "2024-01-14T15:30:00Z",
285
+ };
286
+
287
+ const epic1: Epic = toEpic(linearIssue, "prd_first");
288
+ const epic2: Epic = toEpic(linearIssue, "prd_second");
289
+
290
+ expect(epic1.prdId).toBe("prd_first");
291
+ expect(epic2.prdId).toBe("prd_second");
292
+ });
293
+ });
294
+ });