@reqord/web 0.1.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/LICENSE +661 -0
- package/next-env.d.ts +6 -0
- package/next.config.ts +7 -0
- package/package.json +59 -0
- package/postcss.config.mjs +7 -0
- package/src/__tests__/components/dashboard/critical-path-display.test.tsx +129 -0
- package/src/__tests__/components/dashboard/progress-bar.test.tsx +87 -0
- package/src/__tests__/components/dashboard/project-health.test.tsx +57 -0
- package/src/__tests__/components/dashboard/warning-alert.test.tsx +75 -0
- package/src/__tests__/components/feedback/feedback-client-view.test.tsx +84 -0
- package/src/__tests__/components/feedback/feedback-filters.test.tsx +51 -0
- package/src/__tests__/components/feedback/feedback-linked-items.test.tsx +131 -0
- package/src/__tests__/components/feedback/feedback-list.test.tsx +49 -0
- package/src/__tests__/components/feedback/feedback-table.test.tsx +165 -0
- package/src/__tests__/components/flags/flag-badge.test.tsx +41 -0
- package/src/__tests__/components/flags/flag-list.test.tsx +51 -0
- package/src/__tests__/components/gantt/gantt-bar.test.tsx +190 -0
- package/src/__tests__/components/gantt/gantt-chart.test.tsx +141 -0
- package/src/__tests__/components/gantt/gantt-header.test.tsx +84 -0
- package/src/__tests__/components/gantt/gantt-legend.test.tsx +52 -0
- package/src/__tests__/components/graph/dag-layout.test.ts +129 -0
- package/src/__tests__/components/graph/dependency-graph.test.tsx +94 -0
- package/src/__tests__/components/graph/drilldown-breadcrumb.test.tsx +70 -0
- package/src/__tests__/components/graph/drilldown-graph.test.tsx +108 -0
- package/src/__tests__/components/graph/edge-styles.test.ts +27 -0
- package/src/__tests__/components/graph/graph-page-client.test.tsx +124 -0
- package/src/__tests__/components/graph/issue-node.test.tsx +173 -0
- package/src/__tests__/components/graph/requirement-node.test.tsx +151 -0
- package/src/__tests__/components/graph/specification-node.test.tsx +140 -0
- package/src/__tests__/components/specification/spec-tabs.test.tsx +153 -0
- package/src/__tests__/components/specification/tab-coverage.test.tsx +70 -0
- package/src/__tests__/components/specification/tab-design.test.tsx +42 -0
- package/src/__tests__/components/specification/tab-history.test.tsx +118 -0
- package/src/__tests__/components/specification/tab-issues.test.tsx +126 -0
- package/src/__tests__/components/specification/tab-research.test.tsx +42 -0
- package/src/__tests__/lib/dashboard-data.test.ts +334 -0
- package/src/__tests__/lib/drilldown-graph-data.test.ts +267 -0
- package/src/__tests__/lib/gantt-data.test.ts +299 -0
- package/src/__tests__/lib/graph-data.test.ts +309 -0
- package/src/__tests__/lib/local-feedback-repository.test.ts +74 -0
- package/src/__tests__/lib/local-specification-repository.test.ts +194 -0
- package/src/__tests__/lib/reqord-root.test.ts +31 -0
- package/src/__tests__/lib/specification-file.test.ts +63 -0
- package/src/__tests__/lib/tasks-data.test.ts +104 -0
- package/src/app/dashboard/loading.tsx +21 -0
- package/src/app/dashboard/page.tsx +50 -0
- package/src/app/error.tsx +22 -0
- package/src/app/feedback/loading.tsx +13 -0
- package/src/app/feedback/page.tsx +48 -0
- package/src/app/globals.css +2 -0
- package/src/app/graph/page.tsx +32 -0
- package/src/app/layout.tsx +25 -0
- package/src/app/page.tsx +5 -0
- package/src/app/requirements/[id]/edit/page.tsx +40 -0
- package/src/app/requirements/[id]/loading.tsx +14 -0
- package/src/app/requirements/[id]/not-found.tsx +18 -0
- package/src/app/requirements/[id]/page.tsx +43 -0
- package/src/app/requirements/loading.tsx +13 -0
- package/src/app/requirements/new/page.tsx +14 -0
- package/src/app/requirements/page.tsx +35 -0
- package/src/app/specifications/[id]/loading.tsx +14 -0
- package/src/app/specifications/[id]/not-found.tsx +18 -0
- package/src/app/specifications/[id]/page.tsx +52 -0
- package/src/app/specifications/loading.tsx +13 -0
- package/src/app/specifications/page.tsx +42 -0
- package/src/components/dashboard/critical-path-display.tsx +76 -0
- package/src/components/dashboard/progress-bar.tsx +45 -0
- package/src/components/dashboard/progress-section.tsx +57 -0
- package/src/components/dashboard/project-health.tsx +35 -0
- package/src/components/dashboard/status-card.tsx +27 -0
- package/src/components/dashboard/status-cards.tsx +28 -0
- package/src/components/dashboard/warning-alert.tsx +33 -0
- package/src/components/dashboard/warning-alerts.tsx +24 -0
- package/src/components/feedback/feedback-badge.tsx +48 -0
- package/src/components/feedback/feedback-client-view.tsx +38 -0
- package/src/components/feedback/feedback-filters.tsx +86 -0
- package/src/components/feedback/feedback-linked-items.tsx +93 -0
- package/src/components/feedback/feedback-list.tsx +40 -0
- package/src/components/feedback/feedback-table.tsx +115 -0
- package/src/components/gantt/gantt-bar.tsx +65 -0
- package/src/components/gantt/gantt-chart.tsx +88 -0
- package/src/components/gantt/gantt-constants.ts +15 -0
- package/src/components/gantt/gantt-critical-path.tsx +38 -0
- package/src/components/gantt/gantt-group.tsx +25 -0
- package/src/components/gantt/gantt-header.tsx +47 -0
- package/src/components/gantt/gantt-legend.tsx +26 -0
- package/src/components/graph/dag-layout.ts +131 -0
- package/src/components/graph/dependency-graph.tsx +88 -0
- package/src/components/graph/drilldown-breadcrumb.tsx +35 -0
- package/src/components/graph/drilldown-graph.tsx +45 -0
- package/src/components/graph/edge-styles.ts +16 -0
- package/src/components/graph/graph-loader.tsx +25 -0
- package/src/components/graph/graph-page-client.tsx +98 -0
- package/src/components/graph/issue-node.tsx +46 -0
- package/src/components/graph/multi-level-graph.tsx +91 -0
- package/src/components/graph/requirement-node.tsx +69 -0
- package/src/components/graph/specification-node.tsx +39 -0
- package/src/components/requirement/delete-button.tsx +46 -0
- package/src/components/requirement/dependency-editor.tsx +79 -0
- package/src/components/requirement/markdown-editor.tsx +47 -0
- package/src/components/requirement/markdown-renderer.tsx +12 -0
- package/src/components/requirement/requirement-detail.tsx +228 -0
- package/src/components/requirement/requirement-form.tsx +390 -0
- package/src/components/requirement/requirement-table.tsx +203 -0
- package/src/components/requirement/requirement-tabs.tsx +65 -0
- package/src/components/requirement/success-criteria-editor.tsx +53 -0
- package/src/components/specification/spec-detail.tsx +103 -0
- package/src/components/specification/spec-tabs.tsx +66 -0
- package/src/components/specification/specification-table.tsx +193 -0
- package/src/components/specification/tab-coverage.tsx +52 -0
- package/src/components/specification/tab-design.tsx +16 -0
- package/src/components/specification/tab-history.tsx +61 -0
- package/src/components/specification/tab-issues.tsx +111 -0
- package/src/components/specification/tab-research.tsx +16 -0
- package/src/components/ui/badge.tsx +64 -0
- package/src/components/ui/nav.tsx +49 -0
- package/src/components/ui/tabs.tsx +39 -0
- package/src/lib/actions.ts +222 -0
- package/src/lib/dashboard-data.ts +224 -0
- package/src/lib/data.ts +21 -0
- package/src/lib/drilldown-graph-data.ts +98 -0
- package/src/lib/feedback-data.ts +33 -0
- package/src/lib/feedback-repository.ts +6 -0
- package/src/lib/file-system.ts +167 -0
- package/src/lib/gantt-data.ts +168 -0
- package/src/lib/get-repository.ts +43 -0
- package/src/lib/graph-data.ts +161 -0
- package/src/lib/id-generator.ts +23 -0
- package/src/lib/local-feedback-repository.ts +36 -0
- package/src/lib/local-repository.ts +78 -0
- package/src/lib/local-specification-repository.ts +61 -0
- package/src/lib/repository.ts +11 -0
- package/src/lib/reqord-root.ts +33 -0
- package/src/lib/specification-data.ts +28 -0
- package/src/lib/specification-file.ts +12 -0
- package/src/lib/specification-repository.ts +8 -0
- package/src/lib/tasks-data.ts +32 -0
- package/tsconfig.json +27 -0
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import type { Requirement, Specification, TaskEntry } from "@reqord/shared";
|
|
3
|
+
import { buildDrillDownGraphData } from "../../lib/drilldown-graph-data";
|
|
4
|
+
import { EDGE_STYLES } from "../../components/graph/edge-styles";
|
|
5
|
+
|
|
6
|
+
function makeReq(
|
|
7
|
+
id: string,
|
|
8
|
+
overrides: Partial<Requirement> = {},
|
|
9
|
+
): Requirement {
|
|
10
|
+
return {
|
|
11
|
+
id,
|
|
12
|
+
version: "1.0.0",
|
|
13
|
+
title: `Requirement ${id}`,
|
|
14
|
+
status: "draft",
|
|
15
|
+
priority: "medium",
|
|
16
|
+
createdAt: "2026-01-01T00:00:00Z",
|
|
17
|
+
updatedAt: "2026-01-01T00:00:00Z",
|
|
18
|
+
versionHistory: [],
|
|
19
|
+
files: {
|
|
20
|
+
description: `requirements/${id}/description.md`,
|
|
21
|
+
supplementary: [],
|
|
22
|
+
},
|
|
23
|
+
successCriteria: [],
|
|
24
|
+
format: { type: "free-form" },
|
|
25
|
+
dependencies: { blockedBy: [], blocks: [], relatedTo: [] },
|
|
26
|
+
...overrides,
|
|
27
|
+
} as Requirement;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function makeSpec(
|
|
31
|
+
id: string,
|
|
32
|
+
reqId: string,
|
|
33
|
+
overrides: Partial<Specification> = {},
|
|
34
|
+
): Specification {
|
|
35
|
+
return {
|
|
36
|
+
id,
|
|
37
|
+
requirementId: reqId,
|
|
38
|
+
version: "1.0.0",
|
|
39
|
+
status: "draft",
|
|
40
|
+
createdAt: "2026-01-01T00:00:00Z",
|
|
41
|
+
updatedAt: "2026-01-01T00:00:00Z",
|
|
42
|
+
versionHistory: [],
|
|
43
|
+
files: { design: `specifications/${id}/design.md`, supplementary: [] },
|
|
44
|
+
...overrides,
|
|
45
|
+
} as Specification;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function makeTask(
|
|
49
|
+
number: number,
|
|
50
|
+
specIds: string[],
|
|
51
|
+
overrides: Partial<TaskEntry> = {},
|
|
52
|
+
): TaskEntry {
|
|
53
|
+
return {
|
|
54
|
+
number,
|
|
55
|
+
title: `Task ${number}`,
|
|
56
|
+
url: `https://github.com/test/repo/issues/${number}`,
|
|
57
|
+
linkedTo: { specifications: specIds },
|
|
58
|
+
status: "open",
|
|
59
|
+
syncedAt: "2026-01-01T00:00:00Z",
|
|
60
|
+
...overrides,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
describe("buildDrillDownGraphData", () => {
|
|
65
|
+
describe("requirement only (no specifications)", () => {
|
|
66
|
+
it("returns a single requirement node at x=0, y=0 and no edges", () => {
|
|
67
|
+
const req = makeReq("req-000001");
|
|
68
|
+
|
|
69
|
+
const result = buildDrillDownGraphData(req, [], []);
|
|
70
|
+
|
|
71
|
+
expect(result.nodes).toHaveLength(1);
|
|
72
|
+
expect(result.nodes[0]).toMatchObject({
|
|
73
|
+
id: "req-000001",
|
|
74
|
+
type: "requirement",
|
|
75
|
+
position: { x: 0, y: 0 },
|
|
76
|
+
});
|
|
77
|
+
expect(result.edges).toHaveLength(0);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("includes requirement data in the node matching RequirementNodeData", () => {
|
|
81
|
+
const req = makeReq("req-000001", {
|
|
82
|
+
title: "My Feature",
|
|
83
|
+
status: "approved",
|
|
84
|
+
priority: "high",
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const result = buildDrillDownGraphData(req, [], []);
|
|
88
|
+
|
|
89
|
+
expect(result.nodes[0].data).toMatchObject({
|
|
90
|
+
label: "My Feature",
|
|
91
|
+
status: "approved",
|
|
92
|
+
priority: "high",
|
|
93
|
+
specCount: 0,
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe("requirement with specifications", () => {
|
|
99
|
+
it("creates 3 nodes and 2 implements edges for 2 specs", () => {
|
|
100
|
+
const req = makeReq("req-000001");
|
|
101
|
+
const spec1 = makeSpec("spec-000001", "req-000001");
|
|
102
|
+
const spec2 = makeSpec("spec-000002", "req-000001");
|
|
103
|
+
|
|
104
|
+
const result = buildDrillDownGraphData(req, [spec1, spec2], []);
|
|
105
|
+
|
|
106
|
+
expect(result.nodes).toHaveLength(3);
|
|
107
|
+
expect(result.edges).toHaveLength(2);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("positions spec nodes at x=400 with vertical gap of 120", () => {
|
|
111
|
+
const req = makeReq("req-000001");
|
|
112
|
+
const spec1 = makeSpec("spec-000001", "req-000001");
|
|
113
|
+
const spec2 = makeSpec("spec-000002", "req-000001");
|
|
114
|
+
|
|
115
|
+
const result = buildDrillDownGraphData(req, [spec1, spec2], []);
|
|
116
|
+
|
|
117
|
+
const specNodes = result.nodes.filter((n) => n.type === "specification");
|
|
118
|
+
expect(specNodes[0].position).toEqual({ x: 400, y: 0 });
|
|
119
|
+
expect(specNodes[1].position).toEqual({ x: 400, y: 120 });
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("creates implements edges from requirement to spec with correct style", () => {
|
|
123
|
+
const req = makeReq("req-000001");
|
|
124
|
+
const spec1 = makeSpec("spec-000001", "req-000001");
|
|
125
|
+
|
|
126
|
+
const result = buildDrillDownGraphData(req, [spec1], []);
|
|
127
|
+
|
|
128
|
+
expect(result.edges[0]).toMatchObject({
|
|
129
|
+
id: "impl-req-000001-spec-000001",
|
|
130
|
+
source: "req-000001",
|
|
131
|
+
target: "spec-000001",
|
|
132
|
+
style: EDGE_STYLES.implements,
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("includes spec data in specification nodes matching SpecificationNodeData", () => {
|
|
137
|
+
const req = makeReq("req-000001");
|
|
138
|
+
const spec1 = makeSpec("spec-000001", "req-000001", {
|
|
139
|
+
status: "approved",
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
const result = buildDrillDownGraphData(req, [spec1], []);
|
|
143
|
+
|
|
144
|
+
const specNode = result.nodes.find((n) => n.type === "specification");
|
|
145
|
+
expect(specNode?.data).toMatchObject({
|
|
146
|
+
label: "spec-000001",
|
|
147
|
+
status: "approved",
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
describe("requirement with specifications and tasks", () => {
|
|
153
|
+
it("creates issue nodes at x=800 with tracks edges", () => {
|
|
154
|
+
const req = makeReq("req-000001");
|
|
155
|
+
const spec1 = makeSpec("spec-000001", "req-000001");
|
|
156
|
+
const task = makeTask(42, ["spec-000001"]);
|
|
157
|
+
|
|
158
|
+
const result = buildDrillDownGraphData(req, [spec1], [task]);
|
|
159
|
+
|
|
160
|
+
const issueNode = result.nodes.find((n) => n.type === "issue");
|
|
161
|
+
expect(issueNode).toMatchObject({
|
|
162
|
+
id: "issue-42",
|
|
163
|
+
type: "issue",
|
|
164
|
+
position: { x: 800, y: 0 },
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("includes issue data in the node matching IssueNodeData", () => {
|
|
169
|
+
const req = makeReq("req-000001");
|
|
170
|
+
const spec1 = makeSpec("spec-000001", "req-000001");
|
|
171
|
+
const task = makeTask(42, ["spec-000001"], {
|
|
172
|
+
title: "Implement feature",
|
|
173
|
+
status: "open",
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const result = buildDrillDownGraphData(req, [spec1], [task]);
|
|
177
|
+
|
|
178
|
+
const issueNode = result.nodes.find((n) => n.type === "issue");
|
|
179
|
+
expect(issueNode?.data).toMatchObject({
|
|
180
|
+
label: "Implement feature",
|
|
181
|
+
status: "open",
|
|
182
|
+
issueNumber: 42,
|
|
183
|
+
issueUrl: "https://github.com/test/repo/issues/42",
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("creates tracks edges from spec to issue", () => {
|
|
188
|
+
const req = makeReq("req-000001");
|
|
189
|
+
const spec1 = makeSpec("spec-000001", "req-000001");
|
|
190
|
+
const task = makeTask(42, ["spec-000001"]);
|
|
191
|
+
|
|
192
|
+
const result = buildDrillDownGraphData(req, [spec1], [task]);
|
|
193
|
+
|
|
194
|
+
const tracksEdge = result.edges.find((e) =>
|
|
195
|
+
e.id.startsWith("track-"),
|
|
196
|
+
);
|
|
197
|
+
expect(tracksEdge).toMatchObject({
|
|
198
|
+
id: "track-spec-000001-issue-42",
|
|
199
|
+
source: "spec-000001",
|
|
200
|
+
target: "issue-42",
|
|
201
|
+
style: EDGE_STYLES.tracks,
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("positions issues based on spec index and task index", () => {
|
|
206
|
+
const req = makeReq("req-000001");
|
|
207
|
+
const spec1 = makeSpec("spec-000001", "req-000001");
|
|
208
|
+
const spec2 = makeSpec("spec-000002", "req-000001");
|
|
209
|
+
const task10 = makeTask(10, ["spec-000001"]);
|
|
210
|
+
const task11 = makeTask(11, ["spec-000001"]);
|
|
211
|
+
const task20 = makeTask(20, ["spec-000002"]);
|
|
212
|
+
|
|
213
|
+
const result = buildDrillDownGraphData(req, [spec1, spec2], [task10, task11, task20]);
|
|
214
|
+
|
|
215
|
+
const issueNodes = result.nodes.filter((n) => n.type === "issue");
|
|
216
|
+
expect(issueNodes).toHaveLength(3);
|
|
217
|
+
expect(issueNodes[0].position).toEqual({ x: 800, y: 0 });
|
|
218
|
+
expect(issueNodes[1].position).toEqual({ x: 800, y: 80 });
|
|
219
|
+
expect(issueNodes[2].position).toEqual({ x: 800, y: 120 });
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("does not create issue nodes when no tasks are linked to spec", () => {
|
|
223
|
+
const req = makeReq("req-000001");
|
|
224
|
+
const spec1 = makeSpec("spec-000001", "req-000001");
|
|
225
|
+
|
|
226
|
+
const result = buildDrillDownGraphData(req, [spec1], []);
|
|
227
|
+
|
|
228
|
+
const issueNodes = result.nodes.filter((n) => n.type === "issue");
|
|
229
|
+
expect(issueNodes).toHaveLength(0);
|
|
230
|
+
expect(result.edges).toHaveLength(1);
|
|
231
|
+
expect(result.edges[0].id).toMatch(/^impl-/);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("does not create issue nodes for tasks linked to other specs", () => {
|
|
235
|
+
const req = makeReq("req-000001");
|
|
236
|
+
const spec1 = makeSpec("spec-000001", "req-000001");
|
|
237
|
+
const task = makeTask(42, ["spec-000002"]);
|
|
238
|
+
|
|
239
|
+
const result = buildDrillDownGraphData(req, [spec1], [task]);
|
|
240
|
+
|
|
241
|
+
const issueNodes = result.nodes.filter((n) => n.type === "issue");
|
|
242
|
+
expect(issueNodes).toHaveLength(0);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it("a task linked to multiple specs creates one issue node with edges to each spec", () => {
|
|
246
|
+
const req = makeReq("req-000001");
|
|
247
|
+
const spec1 = makeSpec("spec-000001", "req-000001");
|
|
248
|
+
const spec2 = makeSpec("spec-000002", "req-000001");
|
|
249
|
+
const task = makeTask(42, ["spec-000001", "spec-000002"]);
|
|
250
|
+
|
|
251
|
+
const result = buildDrillDownGraphData(req, [spec1, spec2], [task]);
|
|
252
|
+
|
|
253
|
+
const issueNodes = result.nodes.filter((n) => n.type === "issue");
|
|
254
|
+
expect(issueNodes).toHaveLength(1);
|
|
255
|
+
expect(issueNodes[0].id).toBe("issue-42");
|
|
256
|
+
|
|
257
|
+
const tracksEdges = result.edges.filter((e) => e.id.startsWith("track-"));
|
|
258
|
+
expect(tracksEdges).toHaveLength(2);
|
|
259
|
+
expect(tracksEdges.map((e) => e.id)).toEqual(
|
|
260
|
+
expect.arrayContaining([
|
|
261
|
+
"track-spec-000001-issue-42",
|
|
262
|
+
"track-spec-000002-issue-42",
|
|
263
|
+
]),
|
|
264
|
+
);
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
});
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import type { TaskEntry } from "@reqord/shared";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { DEFAULT_HOURS, transformToGanttData } from "../../lib/gantt-data";
|
|
4
|
+
|
|
5
|
+
function makeTask(overrides: Partial<TaskEntry> = {}): TaskEntry {
|
|
6
|
+
return {
|
|
7
|
+
number: 1,
|
|
8
|
+
title: "Test task",
|
|
9
|
+
url: "https://github.com/test/1",
|
|
10
|
+
linkedTo: { specifications: ["spec-000001"] },
|
|
11
|
+
priority: "P0",
|
|
12
|
+
status: "open",
|
|
13
|
+
syncedAt: "2026-01-01T00:00:00Z",
|
|
14
|
+
...overrides,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe("transformToGanttData", () => {
|
|
19
|
+
describe("P0 tasks: serial placement", () => {
|
|
20
|
+
it("places P0 tasks sequentially with cumulative startOffset", () => {
|
|
21
|
+
const tasks = [
|
|
22
|
+
makeTask({ number: 1, title: "P0 Task 1", priority: "P0" }),
|
|
23
|
+
makeTask({ number: 2, title: "P0 Task 2", priority: "P0" }),
|
|
24
|
+
makeTask({ number: 3, title: "P0 Task 3", priority: "P0" }),
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
const result = transformToGanttData("spec-000001", tasks);
|
|
28
|
+
|
|
29
|
+
const p0Group = result.groups.find((g) => g.priority === "P0");
|
|
30
|
+
expect(p0Group).toBeDefined();
|
|
31
|
+
expect(p0Group!.tasks).toHaveLength(3);
|
|
32
|
+
expect(p0Group!.tasks[0].startOffset).toBe(0);
|
|
33
|
+
expect(p0Group!.tasks[1].startOffset).toBe(4);
|
|
34
|
+
expect(p0Group!.tasks[2].startOffset).toBe(8);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("marks all P0 tasks as critical path", () => {
|
|
38
|
+
const tasks = [
|
|
39
|
+
makeTask({ number: 1, priority: "P0" }),
|
|
40
|
+
makeTask({ number: 2, priority: "P0" }),
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
const result = transformToGanttData("spec-000001", tasks);
|
|
44
|
+
|
|
45
|
+
const p0Group = result.groups.find((g) => g.priority === "P0");
|
|
46
|
+
expect(p0Group!.tasks.every((task) => task.isCriticalPath)).toBe(true);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe("P1 tasks: parallel placement after P0", () => {
|
|
51
|
+
it("places all P1 tasks at P0 total time", () => {
|
|
52
|
+
const tasks = [
|
|
53
|
+
makeTask({ number: 1, priority: "P0" }),
|
|
54
|
+
makeTask({ number: 2, priority: "P0" }),
|
|
55
|
+
makeTask({ number: 3, title: "P1 Task 1", priority: "P1" }),
|
|
56
|
+
makeTask({ number: 4, title: "P1 Task 2", priority: "P1" }),
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
const result = transformToGanttData("spec-000001", tasks);
|
|
60
|
+
|
|
61
|
+
const p1Group = result.groups.find((g) => g.priority === "P1");
|
|
62
|
+
expect(p1Group).toBeDefined();
|
|
63
|
+
expect(p1Group!.tasks).toHaveLength(2);
|
|
64
|
+
// P0 total: 2 * 4 = 8 hours
|
|
65
|
+
expect(p1Group!.tasks[0].startOffset).toBe(8);
|
|
66
|
+
expect(p1Group!.tasks[1].startOffset).toBe(8);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("marks P1 tasks as not on critical path", () => {
|
|
70
|
+
const tasks = [
|
|
71
|
+
makeTask({ number: 1, priority: "P0" }),
|
|
72
|
+
makeTask({ number: 2, priority: "P1" }),
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
const result = transformToGanttData("spec-000001", tasks);
|
|
76
|
+
|
|
77
|
+
const p1Group = result.groups.find((g) => g.priority === "P1");
|
|
78
|
+
expect(p1Group!.tasks.every((task) => !task.isCriticalPath)).toBe(true);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe("P2 tasks: parallel placement after P1", () => {
|
|
83
|
+
it("places all P2 tasks at P1 start + max P1 hours", () => {
|
|
84
|
+
const tasks = [
|
|
85
|
+
makeTask({ number: 1, priority: "P0" }),
|
|
86
|
+
makeTask({ number: 2, priority: "P0" }),
|
|
87
|
+
makeTask({ number: 3, priority: "P1" }),
|
|
88
|
+
makeTask({ number: 4, priority: "P1" }),
|
|
89
|
+
makeTask({ number: 5, title: "P2 Task 1", priority: "P2" }),
|
|
90
|
+
makeTask({ number: 6, title: "P2 Task 2", priority: "P2" }),
|
|
91
|
+
];
|
|
92
|
+
|
|
93
|
+
const result = transformToGanttData("spec-000001", tasks);
|
|
94
|
+
|
|
95
|
+
const p2Group = result.groups.find((g) => g.priority === "P2");
|
|
96
|
+
expect(p2Group).toBeDefined();
|
|
97
|
+
expect(p2Group!.tasks).toHaveLength(2);
|
|
98
|
+
// P0 total: 2 * 4 = 8, P1 start: 8, P1 max hours: 4
|
|
99
|
+
// P2 start: 8 + 4 = 12
|
|
100
|
+
expect(p2Group!.tasks[0].startOffset).toBe(12);
|
|
101
|
+
expect(p2Group!.tasks[1].startOffset).toBe(12);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("marks P2 tasks as not on critical path", () => {
|
|
105
|
+
const tasks = [
|
|
106
|
+
makeTask({ number: 1, priority: "P0" }),
|
|
107
|
+
makeTask({ number: 2, priority: "P2" }),
|
|
108
|
+
];
|
|
109
|
+
|
|
110
|
+
const result = transformToGanttData("spec-000001", tasks);
|
|
111
|
+
|
|
112
|
+
const p2Group = result.groups.find((g) => g.priority === "P2");
|
|
113
|
+
expect(p2Group!.tasks.every((task) => !task.isCriticalPath)).toBe(true);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe("P1 only (no P0): parallel placement at zero", () => {
|
|
118
|
+
it("places P1 tasks at startOffset 0 when no P0 tasks exist", () => {
|
|
119
|
+
const tasks = [
|
|
120
|
+
makeTask({ number: 1, priority: "P1" }),
|
|
121
|
+
makeTask({ number: 2, priority: "P1" }),
|
|
122
|
+
];
|
|
123
|
+
|
|
124
|
+
const result = transformToGanttData("spec-000001", tasks);
|
|
125
|
+
|
|
126
|
+
const p1Group = result.groups.find((g) => g.priority === "P1");
|
|
127
|
+
expect(p1Group).toBeDefined();
|
|
128
|
+
expect(p1Group!.tasks).toHaveLength(2);
|
|
129
|
+
expect(p1Group!.tasks[0].startOffset).toBe(0);
|
|
130
|
+
expect(p1Group!.tasks[1].startOffset).toBe(0);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe("timelineEnd calculation", () => {
|
|
135
|
+
it("calculates timelineEnd as max(startOffset + estimatedHours)", () => {
|
|
136
|
+
const tasks = [
|
|
137
|
+
makeTask({ number: 1, priority: "P0" }),
|
|
138
|
+
makeTask({ number: 2, priority: "P0" }),
|
|
139
|
+
makeTask({ number: 3, priority: "P1" }),
|
|
140
|
+
];
|
|
141
|
+
|
|
142
|
+
const result = transformToGanttData("spec-000001", tasks);
|
|
143
|
+
|
|
144
|
+
// P0: [0-4], [4-8]
|
|
145
|
+
// P1: [8-12]
|
|
146
|
+
// timelineEnd should be 12
|
|
147
|
+
expect(result.timelineEnd).toBe(12);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("handles P2 tasks in timelineEnd calculation", () => {
|
|
151
|
+
const tasks = [
|
|
152
|
+
makeTask({ number: 1, priority: "P0" }),
|
|
153
|
+
makeTask({ number: 2, priority: "P1" }),
|
|
154
|
+
makeTask({ number: 3, priority: "P2" }),
|
|
155
|
+
];
|
|
156
|
+
|
|
157
|
+
const result = transformToGanttData("spec-000001", tasks);
|
|
158
|
+
|
|
159
|
+
// P0: [0-4]
|
|
160
|
+
// P1: [4-8]
|
|
161
|
+
// P2: [8-12]
|
|
162
|
+
// timelineEnd should be 12
|
|
163
|
+
expect(result.timelineEnd).toBe(12);
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
describe("empty cases", () => {
|
|
168
|
+
it("returns empty groups and timelineEnd=0 for zero tasks", () => {
|
|
169
|
+
const result = transformToGanttData("spec-000001", []);
|
|
170
|
+
|
|
171
|
+
expect(result.groups).toEqual([]);
|
|
172
|
+
expect(result.timelineEnd).toBe(0);
|
|
173
|
+
expect(result.timelineStart).toBe(0);
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
describe("estimatedHours", () => {
|
|
178
|
+
it("uses DEFAULT_HOURS when task has no estimatedHours", () => {
|
|
179
|
+
const tasks = [
|
|
180
|
+
makeTask({ number: 1, priority: "P0" }),
|
|
181
|
+
makeTask({ number: 2, priority: "P1" }),
|
|
182
|
+
];
|
|
183
|
+
|
|
184
|
+
const result = transformToGanttData("spec-000001", tasks);
|
|
185
|
+
|
|
186
|
+
const allTasks = result.groups.flatMap((g) => g.tasks);
|
|
187
|
+
expect(
|
|
188
|
+
allTasks.every((task) => task.estimatedHours === DEFAULT_HOURS),
|
|
189
|
+
).toBe(true);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("uses task estimatedHours when provided", () => {
|
|
193
|
+
const tasks = [
|
|
194
|
+
makeTask({ number: 1, priority: "P0", estimatedHours: 8 }),
|
|
195
|
+
];
|
|
196
|
+
|
|
197
|
+
const result = transformToGanttData("spec-000001", tasks);
|
|
198
|
+
|
|
199
|
+
const task = result.groups[0].tasks[0];
|
|
200
|
+
expect(task.estimatedHours).toBe(8);
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
describe("group labels", () => {
|
|
205
|
+
it("assigns correct labels to priority groups", () => {
|
|
206
|
+
const tasks = [
|
|
207
|
+
makeTask({ number: 1, priority: "P0" }),
|
|
208
|
+
makeTask({ number: 2, priority: "P1" }),
|
|
209
|
+
makeTask({ number: 3, priority: "P2" }),
|
|
210
|
+
];
|
|
211
|
+
|
|
212
|
+
const result = transformToGanttData("spec-000001", tasks);
|
|
213
|
+
|
|
214
|
+
expect(result.groups.find((g) => g.priority === "P0")?.label).toBe(
|
|
215
|
+
"P0: Sequential",
|
|
216
|
+
);
|
|
217
|
+
expect(result.groups.find((g) => g.priority === "P1")?.label).toBe(
|
|
218
|
+
"P1: Parallel",
|
|
219
|
+
);
|
|
220
|
+
expect(result.groups.find((g) => g.priority === "P2")?.label).toBe(
|
|
221
|
+
"P2: Parallel",
|
|
222
|
+
);
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
describe("specId preservation", () => {
|
|
227
|
+
it("preserves specId in output", () => {
|
|
228
|
+
const tasks = [makeTask({ number: 1, priority: "P0" })];
|
|
229
|
+
|
|
230
|
+
const result = transformToGanttData("spec-000042", tasks);
|
|
231
|
+
|
|
232
|
+
expect(result.specId).toBe("spec-000042");
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
describe("task properties", () => {
|
|
237
|
+
it("maps all task properties to GanttTask", () => {
|
|
238
|
+
const tasks = [
|
|
239
|
+
makeTask({
|
|
240
|
+
number: 123,
|
|
241
|
+
title: "Test Feature",
|
|
242
|
+
url: "https://github.com/test/123",
|
|
243
|
+
priority: "P0",
|
|
244
|
+
status: "open",
|
|
245
|
+
}),
|
|
246
|
+
];
|
|
247
|
+
|
|
248
|
+
const result = transformToGanttData("spec-000001", tasks);
|
|
249
|
+
|
|
250
|
+
const task = result.groups[0].tasks[0];
|
|
251
|
+
expect(task.id).toBe("123");
|
|
252
|
+
expect(task.title).toBe("Test Feature");
|
|
253
|
+
expect(task.issueNumber).toBe(123);
|
|
254
|
+
expect(task.issueUrl).toBe("https://github.com/test/123");
|
|
255
|
+
expect(task.priority).toBe("P0");
|
|
256
|
+
expect(task.state).toBe("open");
|
|
257
|
+
expect(task.dependencies).toEqual([]);
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
describe("tasks without priority", () => {
|
|
262
|
+
it("skips tasks that have no priority field", () => {
|
|
263
|
+
const tasks = [
|
|
264
|
+
makeTask({ number: 1, priority: "P0" }),
|
|
265
|
+
makeTask({ number: 2, priority: undefined }),
|
|
266
|
+
];
|
|
267
|
+
|
|
268
|
+
const result = transformToGanttData("spec-000001", tasks);
|
|
269
|
+
|
|
270
|
+
const allTasks = result.groups.flatMap((g) => g.tasks);
|
|
271
|
+
expect(allTasks).toHaveLength(1);
|
|
272
|
+
expect(allTasks[0].issueNumber).toBe(1);
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
describe("totalEstimatedHours", () => {
|
|
277
|
+
it("sums estimated hours of all tasks", () => {
|
|
278
|
+
const tasks = [
|
|
279
|
+
makeTask({ number: 1, priority: "P0", estimatedHours: 2 }),
|
|
280
|
+
makeTask({ number: 2, priority: "P1", estimatedHours: 3 }),
|
|
281
|
+
];
|
|
282
|
+
|
|
283
|
+
const result = transformToGanttData("spec-000001", tasks);
|
|
284
|
+
|
|
285
|
+
expect(result.totalEstimatedHours).toBe(5);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it("uses DEFAULT_HOURS for tasks without estimatedHours in total", () => {
|
|
289
|
+
const tasks = [
|
|
290
|
+
makeTask({ number: 1, priority: "P0" }),
|
|
291
|
+
makeTask({ number: 2, priority: "P1" }),
|
|
292
|
+
];
|
|
293
|
+
|
|
294
|
+
const result = transformToGanttData("spec-000001", tasks);
|
|
295
|
+
|
|
296
|
+
expect(result.totalEstimatedHours).toBe(2 * DEFAULT_HOURS);
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
});
|