@runfusion/fusion 0.22.0 → 0.24.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/dist/bin.js +30071 -20735
- package/dist/client/assets/AgentDetailView-BwJaLqZh.css +1 -0
- package/dist/client/assets/AgentDetailView-gy_5SUj2.js +18 -0
- package/dist/client/assets/AgentsView-BkB9FiMT.js +29 -0
- package/dist/client/assets/AgentsView-CV3vm7Qk.css +1 -0
- package/dist/client/assets/ChatView-B_-B8fqu.js +1 -0
- package/dist/client/assets/ChatView-DwJAd5G1.css +1 -0
- package/dist/client/assets/{DevServerView-l8RCyL2k.js → DevServerView-BkvtjZBa.js} +1 -1
- package/dist/client/assets/{DirectoryPicker-CS1dwqcC.js → DirectoryPicker-BK-KbnhP.js} +1 -1
- package/dist/client/assets/{DocumentsView-DmthQWDZ.js → DocumentsView-BEg1CQAk.js} +1 -1
- package/dist/client/assets/{DocumentsView-BrhyOdeE.css → DocumentsView-gv4zG3aT.css} +1 -1
- package/dist/client/assets/EvalsView-Berf9bQm.js +1 -0
- package/dist/client/assets/EvalsView-CUNJ1TLc.css +1 -0
- package/dist/client/assets/{agentSkills-DDHJnrkn.css → ExperimentalAgentOnboardingModal-B-APN_lM.css} +1 -1
- package/dist/client/assets/ExperimentalAgentOnboardingModal-jcInE50G.js +499 -0
- package/dist/client/assets/InsightsView-B0J4mhzV.css +1 -0
- package/dist/client/assets/InsightsView-BX5bSF1J.js +11 -0
- package/dist/client/assets/{MemoryView-CPwlKnUI.js → MemoryView-CKElJY_3.js} +2 -2
- package/dist/client/assets/NodesView-DLUOBLf6.js +14 -0
- package/dist/client/assets/NodesView-DT4pXowv.css +1 -0
- package/dist/client/assets/{PiExtensionsManager-j8rPXqmB.js → PiExtensionsManager-COlJf0Kx.js} +2 -2
- package/dist/client/assets/PluginManager-CfW55BF4.js +1 -0
- package/dist/client/assets/PluginManager-DtRQXia5.css +1 -0
- package/dist/client/assets/{ResearchView-D9DNJYDq.js → ResearchView-B256Lr8I.js} +1 -1
- package/dist/client/assets/SettingsModal-BeA_nQtW.js +31 -0
- package/dist/client/assets/SettingsModal-DzsLquBu.css +1 -0
- package/dist/client/assets/{SettingsModal-fxvTFLtR.js → SettingsModal-yRqM4DV8.js} +1 -1
- package/dist/client/assets/SetupWizardModal-uUZk3TKT.js +1 -0
- package/dist/client/assets/{SkillsView-Ddf0YL8z.js → SkillsView-CP8JX0P_.js} +1 -1
- package/dist/client/assets/TodoView-Cx9cVhq7.css +1 -0
- package/dist/client/assets/TodoView-DCRIkDZ-.js +6 -0
- package/dist/client/assets/createLucideIcon-BazL2hk5.js +21 -0
- package/dist/client/assets/dashboard-view-BkTMSZYn.css +1 -0
- package/dist/client/assets/dashboard-view-CyWN-d02.js +63 -0
- package/dist/client/assets/dashboard-view-lR7YYmSC.js +21 -0
- package/dist/client/assets/{folder-open-BiJpmnaT.js → folder-open-DHjELt8-.js} +1 -1
- package/dist/client/assets/index-CQyVRLOb.js +692 -0
- package/dist/client/assets/index-CxA2Nn0_.css +1 -0
- package/dist/client/assets/projectDetection-G3XuxD2X.js +1 -0
- package/dist/client/assets/{star-BwRZmiuZ.js → star-DYesq1AV.js} +1 -1
- package/dist/client/assets/{upload-D4NwZhPp.js → upload-DTWF3Db5.js} +1 -1
- package/dist/client/assets/{users-DNISDtI1.js → users--syrel4l.js} +1 -1
- package/dist/client/index.html +12 -20
- package/dist/client/theme-data.css +106 -0
- package/dist/client/version.json +1 -1
- package/dist/droid-cli/package.json +1 -1
- package/dist/extension.js +17072 -9627
- package/dist/pi-claude-cli/package.json +1 -1
- package/dist/plugins/fusion-plugin-cursor-runtime/bundled.js +218 -0
- package/dist/plugins/fusion-plugin-cursor-runtime/manifest.json +6 -0
- package/dist/plugins/fusion-plugin-cursor-runtime/package.json +11 -0
- package/dist/plugins/fusion-plugin-dependency-graph/manifest.json +1 -1
- package/dist/plugins/fusion-plugin-dependency-graph/package.json +6 -4
- package/dist/plugins/fusion-plugin-dependency-graph/src/DependencyGraph.css +58 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/DependencyGraph.tsx +301 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/GraphHighlight.css +27 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/GraphTaskNode.css +157 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/GraphTaskNode.tsx +126 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/GraphToolbar.css +35 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/GraphToolbar.tsx +36 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/DependencyGraph.highlighting.test.tsx +112 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/DependencyGraph.persistence.test.tsx +115 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/DependencyGraph.test.tsx +128 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/GraphTaskNode.drag.test.tsx +82 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/GraphTaskNode.test.tsx +307 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/GraphToolbar.test.tsx +60 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/edges.test.tsx +75 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/filtering.test.tsx +62 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/filters.test.ts +78 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/graphPositionStorage.test.ts +95 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/host-integration.test.ts +74 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/index.test.ts +58 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/interactions.test.tsx +121 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/layout.test.ts +70 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/persistence.test.tsx +89 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/useGraphData.test.ts +86 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/useGraphInteraction.test.ts +167 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/useGraphPositions.test.ts +66 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/useNodeDrag.test.ts +81 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/dashboard-interop.d.ts +35 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/dashboard-view.tsx +19 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/edges.tsx +70 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/filters.ts +8 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/hooks/__tests__/useDependencyChain.test.ts +53 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/hooks/useDependencyChain.ts +60 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/hooks/useGraphPositions.ts +45 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/hooks/useNodeDrag.ts +114 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/index.ts +1 -2
- package/dist/plugins/fusion-plugin-dependency-graph/src/layout.ts +91 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/styles/drag.css +15 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/types.ts +21 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/useGraphData.ts +17 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/useGraphInteraction.ts +292 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/utils/graphPositionStorage.ts +65 -0
- package/dist/plugins/fusion-plugin-droid-runtime/bundled.js +136680 -0
- package/dist/plugins/fusion-plugin-droid-runtime/manifest.json +13 -0
- package/dist/plugins/fusion-plugin-droid-runtime/mcp-schema-server.cjs +49 -0
- package/dist/plugins/fusion-plugin-droid-runtime/package.json +11 -0
- package/dist/plugins/fusion-plugin-hermes-runtime/bundled.js +176 -7
- package/dist/plugins/fusion-plugin-hermes-runtime/package.json +1 -1
- package/dist/plugins/fusion-plugin-openclaw-runtime/bundled.js +93 -6
- package/dist/plugins/fusion-plugin-openclaw-runtime/mcp-schema-server.cjs +59 -0
- package/dist/plugins/fusion-plugin-openclaw-runtime/package.json +1 -1
- package/dist/plugins/fusion-plugin-paperclip-runtime/package.json +1 -1
- package/dist/plugins/fusion-plugin-reports/manifest.json +33 -0
- package/dist/plugins/fusion-plugin-reports/package.json +26 -0
- package/dist/plugins/fusion-plugin-reports/src/__tests__/manifest.test.ts +51 -0
- package/dist/plugins/fusion-plugin-reports/src/__tests__/review-panel.test.ts +166 -0
- package/dist/plugins/fusion-plugin-reports/src/__tests__/settings.test.ts +157 -0
- package/dist/plugins/fusion-plugin-reports/src/index.ts +41 -0
- package/dist/plugins/fusion-plugin-reports/src/review-panel.ts +294 -0
- package/dist/plugins/fusion-plugin-reports/src/review-types.ts +75 -0
- package/dist/plugins/fusion-plugin-reports/src/settings.ts +105 -0
- package/dist/plugins/fusion-plugin-roadmap/manifest.json +16 -0
- package/dist/plugins/fusion-plugin-roadmap/package.json +48 -0
- package/dist/plugins/fusion-plugin-roadmap/src/__tests__/api-client.test.ts +101 -0
- package/dist/plugins/fusion-plugin-roadmap/src/__tests__/index.test.ts +92 -0
- package/dist/plugins/fusion-plugin-roadmap/src/__tests__/roadmap-routes.test.ts +48 -0
- package/dist/plugins/fusion-plugin-roadmap/src/__tests__/roadmap-suggestions.test.ts +31 -0
- package/dist/plugins/fusion-plugin-roadmap/src/dashboard/RoadmapsView.css +1299 -0
- package/dist/plugins/fusion-plugin-roadmap/src/dashboard/RoadmapsView.tsx +2559 -0
- package/dist/plugins/fusion-plugin-roadmap/src/dashboard/__tests__/RoadmapsView.test.tsx +1144 -0
- package/dist/plugins/fusion-plugin-roadmap/src/dashboard/__tests__/useRoadmaps.test.ts +1756 -0
- package/dist/plugins/fusion-plugin-roadmap/src/dashboard/api.ts +70 -0
- package/dist/plugins/fusion-plugin-roadmap/src/dashboard/test-setup.ts +7 -0
- package/dist/plugins/fusion-plugin-roadmap/src/dashboard/types.ts +1 -0
- package/dist/plugins/fusion-plugin-roadmap/src/dashboard/useConfirm.ts +8 -0
- package/dist/plugins/fusion-plugin-roadmap/src/dashboard/useRoadmaps.ts +1188 -0
- package/dist/plugins/fusion-plugin-roadmap/src/dashboard/useViewportMode.ts +20 -0
- package/dist/plugins/fusion-plugin-roadmap/src/dashboard-view.tsx +6 -0
- package/dist/plugins/fusion-plugin-roadmap/src/index.ts +74 -0
- package/dist/plugins/fusion-plugin-roadmap/src/roadmap-routes.ts +1 -0
- package/dist/plugins/fusion-plugin-roadmap/src/roadmap-schema.ts +41 -0
- package/dist/plugins/fusion-plugin-roadmap/src/roadmap-suggestions.d.ts +15 -0
- package/dist/plugins/fusion-plugin-roadmap/src/roadmap-suggestions.ts +15 -0
- package/dist/plugins/fusion-plugin-roadmap/src/roadmap-types.d.ts +283 -0
- package/dist/plugins/fusion-plugin-roadmap/src/roadmap-types.d.ts.map +1 -0
- package/dist/plugins/fusion-plugin-roadmap/src/roadmap-types.js +21 -0
- package/dist/plugins/fusion-plugin-roadmap/src/roadmap-types.js.map +1 -0
- package/dist/plugins/fusion-plugin-roadmap/src/roadmap-types.ts +310 -0
- package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-routes.d.ts +5 -0
- package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-routes.d.ts.map +1 -0
- package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-routes.js +361 -0
- package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-routes.js.map +1 -0
- package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-routes.ts +408 -0
- package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-suggestions.d.ts +68 -0
- package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-suggestions.d.ts.map +1 -0
- package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-suggestions.js +300 -0
- package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-suggestions.js.map +1 -0
- package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-suggestions.ts +381 -0
- package/dist/plugins/fusion-plugin-roadmap/src/server/index.d.ts +3 -0
- package/dist/plugins/fusion-plugin-roadmap/src/server/index.ts +1 -0
- package/dist/plugins/fusion-plugin-roadmap/src/store/__tests__/roadmap-handoff.test.ts +445 -0
- package/dist/plugins/fusion-plugin-roadmap/src/store/__tests__/roadmap-ordering.test.ts +334 -0
- package/dist/plugins/fusion-plugin-roadmap/src/store/__tests__/roadmap-store.test.ts +1318 -0
- package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-handoff.ts +163 -0
- package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-ordering.d.ts +37 -0
- package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-ordering.d.ts.map +1 -0
- package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-ordering.js +188 -0
- package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-ordering.js.map +1 -0
- package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-ordering.ts +311 -0
- package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-store.d.ts +299 -0
- package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-store.d.ts.map +1 -0
- package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-store.js +765 -0
- package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-store.js.map +1 -0
- package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-store.ts +1001 -0
- package/dist/plugins/fusion-plugin-whatsapp-chat/manifest.json +8 -0
- package/dist/plugins/fusion-plugin-whatsapp-chat/package.json +34 -0
- package/dist/plugins/fusion-plugin-whatsapp-chat/src/__tests__/auth-state.test.ts +99 -0
- package/dist/plugins/fusion-plugin-whatsapp-chat/src/__tests__/connection.test.ts +145 -0
- package/dist/plugins/fusion-plugin-whatsapp-chat/src/__tests__/index.test.ts +216 -0
- package/dist/plugins/fusion-plugin-whatsapp-chat/src/__tests__/reply.test.ts +52 -0
- package/dist/plugins/fusion-plugin-whatsapp-chat/src/auth-state.ts +89 -0
- package/dist/plugins/fusion-plugin-whatsapp-chat/src/connection.ts +253 -0
- package/dist/plugins/fusion-plugin-whatsapp-chat/src/index.ts +262 -0
- package/dist/plugins/fusion-plugin-whatsapp-chat/src/qrcode.d.ts +1 -0
- package/dist/plugins/fusion-plugin-whatsapp-chat/src/reply.ts +37 -0
- package/package.json +2 -2
- package/skill/fusion/SKILL.md +2 -2
- package/skill/fusion/references/engine-tools.md +8 -2
- package/skill/fusion/references/extension-tools.md +39 -0
- package/skill/fusion/references/fusion-capabilities.md +3 -0
- package/dist/client/assets/AgentDetailView-BKKpbp1S.js +0 -18
- package/dist/client/assets/AgentDetailView-CeO_1MK7.css +0 -1
- package/dist/client/assets/AgentsView-BRXFmrcJ.js +0 -527
- package/dist/client/assets/AgentsView-Bs03ptrd.css +0 -1
- package/dist/client/assets/ChatView-D7L2e_qu.js +0 -1
- package/dist/client/assets/InsightsView-AWo5o_81.css +0 -1
- package/dist/client/assets/InsightsView-DvXpMKmH.js +0 -11
- package/dist/client/assets/NodesView-BLlfUfsy.js +0 -14
- package/dist/client/assets/NodesView-fXqDk9ur.css +0 -1
- package/dist/client/assets/PluginManager-DA_T0GHn.css +0 -1
- package/dist/client/assets/PluginManager-pW6RMz5z.js +0 -1
- package/dist/client/assets/RoadmapsView-Djc_X35v.js +0 -6
- package/dist/client/assets/SettingsModal-BWe0KrGY.css +0 -1
- package/dist/client/assets/SettingsModal-WGCF_pk8.js +0 -31
- package/dist/client/assets/SetupWizardModal-tG_MF_nA.js +0 -1
- package/dist/client/assets/agentSkills-EwIwBlG8.js +0 -1
- package/dist/client/assets/index-D6ebxTPF.css +0 -1
- package/dist/client/assets/index-DYDLmOcK.js +0 -694
- package/dist/plugins/fusion-plugin-dependency-graph/src/DependencyGraphView.css +0 -132
- package/dist/plugins/fusion-plugin-dependency-graph/src/DependencyGraphView.tsx +0 -428
- package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/DependencyGraphView.test.tsx +0 -261
- package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/storage.test.ts +0 -31
- package/dist/plugins/fusion-plugin-dependency-graph/src/storage.ts +0 -23
- /package/dist/client/assets/{RoadmapsView-DdGlfuu-.css → dashboard-view-DdGlfuu-.css} +0 -0
|
@@ -0,0 +1,1318 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
|
2
|
+
import { Database, createDatabase } from "@fusion/core";
|
|
3
|
+
import { RoadmapStore } from "../roadmap-store.js";
|
|
4
|
+
import { ensureRoadmapSchema } from "../../roadmap-schema.js";
|
|
5
|
+
import type {
|
|
6
|
+
RoadmapCreateInput,
|
|
7
|
+
RoadmapUpdateInput,
|
|
8
|
+
RoadmapMilestoneCreateInput,
|
|
9
|
+
RoadmapMilestoneUpdateInput,
|
|
10
|
+
RoadmapFeatureCreateInput,
|
|
11
|
+
RoadmapFeatureUpdateInput,
|
|
12
|
+
RoadmapMilestoneReorderInput,
|
|
13
|
+
RoadmapFeatureReorderInput,
|
|
14
|
+
RoadmapFeatureMoveInput,
|
|
15
|
+
} from "../../roadmap-types.js";
|
|
16
|
+
import { mkdtempSync } from "node:fs";
|
|
17
|
+
import { join } from "node:path";
|
|
18
|
+
import { tmpdir } from "node:os";
|
|
19
|
+
import { rm } from "node:fs/promises";
|
|
20
|
+
|
|
21
|
+
function makeTmpDir(): string {
|
|
22
|
+
return mkdtempSync(join(tmpdir(), "roadmap-store-test-"));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe("RoadmapStore", () => {
|
|
26
|
+
let tmpDir: string;
|
|
27
|
+
let db: Database;
|
|
28
|
+
let store: RoadmapStore;
|
|
29
|
+
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
tmpDir = makeTmpDir();
|
|
32
|
+
// In-memory SQLite for test speed; see store.test.ts beforeEach.
|
|
33
|
+
// Cross-instance persistence sub-tests below construct disk-backed
|
|
34
|
+
// Database instances explicitly (search for `persistDb`).
|
|
35
|
+
db = new Database(join(tmpDir, ".fusion"), { inMemory: true });
|
|
36
|
+
db.init();
|
|
37
|
+
ensureRoadmapSchema(db);
|
|
38
|
+
store = new RoadmapStore(db);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
afterEach(async () => {
|
|
42
|
+
db.close();
|
|
43
|
+
await rm(tmpDir, { recursive: true, force: true });
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe("roadmap CRUD", () => {
|
|
47
|
+
it("creates a roadmap", () => {
|
|
48
|
+
const input: RoadmapCreateInput = { title: "Test Roadmap" };
|
|
49
|
+
const roadmap = store.createRoadmap(input);
|
|
50
|
+
|
|
51
|
+
expect(roadmap.id).toMatch(/^RM-/);
|
|
52
|
+
expect(roadmap.title).toBe("Test Roadmap");
|
|
53
|
+
expect(roadmap.description).toBeUndefined();
|
|
54
|
+
expect(roadmap.createdAt).toBeTruthy();
|
|
55
|
+
expect(roadmap.updatedAt).toBeTruthy();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("creates a roadmap with description", () => {
|
|
59
|
+
const input: RoadmapCreateInput = {
|
|
60
|
+
title: "Test Roadmap",
|
|
61
|
+
description: "A detailed description",
|
|
62
|
+
};
|
|
63
|
+
const roadmap = store.createRoadmap(input);
|
|
64
|
+
|
|
65
|
+
expect(roadmap.title).toBe("Test Roadmap");
|
|
66
|
+
expect(roadmap.description).toBe("A detailed description");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("gets a roadmap by id", () => {
|
|
70
|
+
const created = store.createRoadmap({ title: "Test" });
|
|
71
|
+
const retrieved = store.getRoadmap(created.id);
|
|
72
|
+
|
|
73
|
+
expect(retrieved).toEqual(created);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("returns undefined for non-existent roadmap", () => {
|
|
77
|
+
const retrieved = store.getRoadmap("RM-nonexistent");
|
|
78
|
+
expect(retrieved).toBeUndefined();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("lists all roadmaps", () => {
|
|
82
|
+
const r1 = store.createRoadmap({ title: "Roadmap 1" });
|
|
83
|
+
const r2 = store.createRoadmap({ title: "Roadmap 2" });
|
|
84
|
+
const r3 = store.createRoadmap({ title: "Roadmap 3" });
|
|
85
|
+
|
|
86
|
+
const roadmaps = store.listRoadmaps();
|
|
87
|
+
|
|
88
|
+
expect(roadmaps.length).toBe(3);
|
|
89
|
+
// Should contain all three (order depends on createdAt timestamps)
|
|
90
|
+
const titles = roadmaps.map((r) => r.title);
|
|
91
|
+
expect(titles).toContain("Roadmap 1");
|
|
92
|
+
expect(titles).toContain("Roadmap 2");
|
|
93
|
+
expect(titles).toContain("Roadmap 3");
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("updates a roadmap", () => {
|
|
97
|
+
const created = store.createRoadmap({ title: "Original" });
|
|
98
|
+
const updated = store.updateRoadmap(created.id, { title: "Updated" } as RoadmapUpdateInput);
|
|
99
|
+
|
|
100
|
+
expect(updated.id).toBe(created.id);
|
|
101
|
+
expect(updated.title).toBe("Updated");
|
|
102
|
+
expect(updated.createdAt).toBe(created.createdAt);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("throws when updating non-existent roadmap", () => {
|
|
106
|
+
expect(() => store.updateRoadmap("RM-nonexistent", { title: "Test" } as RoadmapUpdateInput))
|
|
107
|
+
.toThrow("Roadmap RM-nonexistent not found");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("deletes a roadmap", () => {
|
|
111
|
+
const created = store.createRoadmap({ title: "Test" });
|
|
112
|
+
store.deleteRoadmap(created.id);
|
|
113
|
+
|
|
114
|
+
expect(store.getRoadmap(created.id)).toBeUndefined();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("throws when deleting non-existent roadmap", () => {
|
|
118
|
+
expect(() => store.deleteRoadmap("RM-nonexistent"))
|
|
119
|
+
.toThrow("Roadmap RM-nonexistent not found");
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("emits roadmap:created event", () => {
|
|
123
|
+
const listener = vi.fn();
|
|
124
|
+
store.on("roadmap:created", listener);
|
|
125
|
+
|
|
126
|
+
const roadmap = store.createRoadmap({ title: "Test" });
|
|
127
|
+
|
|
128
|
+
expect(listener).toHaveBeenCalledTimes(1);
|
|
129
|
+
expect(listener).toHaveBeenCalledWith(roadmap);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("emits roadmap:updated event", () => {
|
|
133
|
+
const created = store.createRoadmap({ title: "Original" });
|
|
134
|
+
const listener = vi.fn();
|
|
135
|
+
store.on("roadmap:updated", listener);
|
|
136
|
+
|
|
137
|
+
store.updateRoadmap(created.id, { title: "Updated" } as RoadmapUpdateInput);
|
|
138
|
+
|
|
139
|
+
expect(listener).toHaveBeenCalledTimes(1);
|
|
140
|
+
expect(listener).toHaveBeenCalledWith(expect.objectContaining({ title: "Updated" }));
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("emits roadmap:deleted event", () => {
|
|
144
|
+
const created = store.createRoadmap({ title: "Test" });
|
|
145
|
+
const listener = vi.fn();
|
|
146
|
+
store.on("roadmap:deleted", listener);
|
|
147
|
+
|
|
148
|
+
store.deleteRoadmap(created.id);
|
|
149
|
+
|
|
150
|
+
expect(listener).toHaveBeenCalledTimes(1);
|
|
151
|
+
expect(listener).toHaveBeenCalledWith(created.id);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
describe("milestone CRUD", () => {
|
|
156
|
+
let roadmapId: string;
|
|
157
|
+
|
|
158
|
+
beforeEach(() => {
|
|
159
|
+
roadmapId = store.createRoadmap({ title: "Test Roadmap" }).id;
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("creates a milestone with auto-computed orderIndex", () => {
|
|
163
|
+
const m1 = store.createMilestone(roadmapId, { title: "Milestone 1" });
|
|
164
|
+
const m2 = store.createMilestone(roadmapId, { title: "Milestone 2" });
|
|
165
|
+
const m3 = store.createMilestone(roadmapId, { title: "Milestone 3" });
|
|
166
|
+
|
|
167
|
+
expect(m1.orderIndex).toBe(0);
|
|
168
|
+
expect(m2.orderIndex).toBe(1);
|
|
169
|
+
expect(m3.orderIndex).toBe(2);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("creates a milestone with description", () => {
|
|
173
|
+
const milestone = store.createMilestone(roadmapId, {
|
|
174
|
+
title: "Milestone",
|
|
175
|
+
description: "A detailed description",
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
expect(milestone.title).toBe("Milestone");
|
|
179
|
+
expect(milestone.description).toBe("A detailed description");
|
|
180
|
+
expect(milestone.roadmapId).toBe(roadmapId);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("throws when creating milestone for non-existent roadmap", () => {
|
|
184
|
+
expect(() => store.createMilestone("RM-nonexistent", { title: "Test" }))
|
|
185
|
+
.toThrow("Roadmap RM-nonexistent not found");
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("gets a milestone by id", () => {
|
|
189
|
+
const created = store.createMilestone(roadmapId, { title: "Test" });
|
|
190
|
+
const retrieved = store.getMilestone(created.id);
|
|
191
|
+
|
|
192
|
+
expect(retrieved).toEqual(created);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("lists milestones with deterministic ordering", () => {
|
|
196
|
+
store.createMilestone(roadmapId, { title: "First" });
|
|
197
|
+
store.createMilestone(roadmapId, { title: "Second" });
|
|
198
|
+
store.createMilestone(roadmapId, { title: "Third" });
|
|
199
|
+
|
|
200
|
+
const milestones = store.listMilestones(roadmapId);
|
|
201
|
+
|
|
202
|
+
expect(milestones.length).toBe(3);
|
|
203
|
+
expect(milestones[0].title).toBe("First");
|
|
204
|
+
expect(milestones[1].title).toBe("Second");
|
|
205
|
+
expect(milestones[2].title).toBe("Third");
|
|
206
|
+
expect(milestones[0].orderIndex).toBe(0);
|
|
207
|
+
expect(milestones[1].orderIndex).toBe(1);
|
|
208
|
+
expect(milestones[2].orderIndex).toBe(2);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("updates a milestone", () => {
|
|
212
|
+
const created = store.createMilestone(roadmapId, { title: "Original" });
|
|
213
|
+
const updated = store.updateMilestone(created.id, { title: "Updated" } as RoadmapMilestoneUpdateInput);
|
|
214
|
+
|
|
215
|
+
expect(updated.id).toBe(created.id);
|
|
216
|
+
expect(updated.title).toBe("Updated");
|
|
217
|
+
expect(updated.roadmapId).toBe(roadmapId);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("throws when updating non-existent milestone", () => {
|
|
221
|
+
expect(() => store.updateMilestone("RMS-nonexistent", { title: "Test" } as RoadmapMilestoneUpdateInput))
|
|
222
|
+
.toThrow("Milestone RMS-nonexistent not found");
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it("deletes a milestone", () => {
|
|
226
|
+
const created = store.createMilestone(roadmapId, { title: "Test" });
|
|
227
|
+
store.deleteMilestone(created.id);
|
|
228
|
+
|
|
229
|
+
expect(store.getMilestone(created.id)).toBeUndefined();
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it("cascade-deletes features when deleting milestone", () => {
|
|
233
|
+
const milestone = store.createMilestone(roadmapId, { title: "Test" });
|
|
234
|
+
const feature = store.createFeature(milestone.id, { title: "Feature" });
|
|
235
|
+
|
|
236
|
+
store.deleteMilestone(milestone.id);
|
|
237
|
+
|
|
238
|
+
expect(store.getFeature(feature.id)).toBeUndefined();
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it("cascade-deletes milestones when deleting roadmap", () => {
|
|
242
|
+
const m1 = store.createMilestone(roadmapId, { title: "Milestone 1" });
|
|
243
|
+
const m2 = store.createMilestone(roadmapId, { title: "Milestone 2" });
|
|
244
|
+
|
|
245
|
+
store.deleteRoadmap(roadmapId);
|
|
246
|
+
|
|
247
|
+
expect(store.getMilestone(m1.id)).toBeUndefined();
|
|
248
|
+
expect(store.getMilestone(m2.id)).toBeUndefined();
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
describe("feature CRUD", () => {
|
|
253
|
+
let milestoneId: string;
|
|
254
|
+
|
|
255
|
+
beforeEach(() => {
|
|
256
|
+
const roadmapId = store.createRoadmap({ title: "Test Roadmap" }).id;
|
|
257
|
+
milestoneId = store.createMilestone(roadmapId, { title: "Test Milestone" }).id;
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it("creates a feature with auto-computed orderIndex", () => {
|
|
261
|
+
const f1 = store.createFeature(milestoneId, { title: "Feature 1" });
|
|
262
|
+
const f2 = store.createFeature(milestoneId, { title: "Feature 2" });
|
|
263
|
+
const f3 = store.createFeature(milestoneId, { title: "Feature 3" });
|
|
264
|
+
|
|
265
|
+
expect(f1.orderIndex).toBe(0);
|
|
266
|
+
expect(f2.orderIndex).toBe(1);
|
|
267
|
+
expect(f3.orderIndex).toBe(2);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it("creates a feature with description", () => {
|
|
271
|
+
const feature = store.createFeature(milestoneId, {
|
|
272
|
+
title: "Feature",
|
|
273
|
+
description: "A detailed description",
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
expect(feature.title).toBe("Feature");
|
|
277
|
+
expect(feature.description).toBe("A detailed description");
|
|
278
|
+
expect(feature.milestoneId).toBe(milestoneId);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it("throws when creating feature for non-existent milestone", () => {
|
|
282
|
+
expect(() => store.createFeature("RMS-nonexistent", { title: "Test" }))
|
|
283
|
+
.toThrow("Milestone RMS-nonexistent not found");
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it("gets a feature by id", () => {
|
|
287
|
+
const created = store.createFeature(milestoneId, { title: "Test" });
|
|
288
|
+
const retrieved = store.getFeature(created.id);
|
|
289
|
+
|
|
290
|
+
expect(retrieved).toEqual(created);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it("lists features with deterministic ordering", () => {
|
|
294
|
+
store.createFeature(milestoneId, { title: "First" });
|
|
295
|
+
store.createFeature(milestoneId, { title: "Second" });
|
|
296
|
+
store.createFeature(milestoneId, { title: "Third" });
|
|
297
|
+
|
|
298
|
+
const features = store.listFeatures(milestoneId);
|
|
299
|
+
|
|
300
|
+
expect(features.length).toBe(3);
|
|
301
|
+
expect(features[0].title).toBe("First");
|
|
302
|
+
expect(features[1].title).toBe("Second");
|
|
303
|
+
expect(features[2].title).toBe("Third");
|
|
304
|
+
expect(features[0].orderIndex).toBe(0);
|
|
305
|
+
expect(features[1].orderIndex).toBe(1);
|
|
306
|
+
expect(features[2].orderIndex).toBe(2);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it("updates a feature", () => {
|
|
310
|
+
const created = store.createFeature(milestoneId, { title: "Original" });
|
|
311
|
+
const updated = store.updateFeature(created.id, { title: "Updated" } as RoadmapFeatureUpdateInput);
|
|
312
|
+
|
|
313
|
+
expect(updated.id).toBe(created.id);
|
|
314
|
+
expect(updated.title).toBe("Updated");
|
|
315
|
+
expect(updated.milestoneId).toBe(milestoneId);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it("throws when updating non-existent feature", () => {
|
|
319
|
+
expect(() => store.updateFeature("RF-nonexistent", { title: "Test" } as RoadmapFeatureUpdateInput))
|
|
320
|
+
.toThrow("Feature RF-nonexistent not found");
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it("deletes a feature", () => {
|
|
324
|
+
const created = store.createFeature(milestoneId, { title: "Test" });
|
|
325
|
+
store.deleteFeature(created.id);
|
|
326
|
+
|
|
327
|
+
expect(store.getFeature(created.id)).toBeUndefined();
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
describe("milestone reorder", () => {
|
|
332
|
+
let roadmapId: string;
|
|
333
|
+
let milestoneIds: string[];
|
|
334
|
+
|
|
335
|
+
beforeEach(() => {
|
|
336
|
+
roadmapId = store.createRoadmap({ title: "Test Roadmap" }).id;
|
|
337
|
+
milestoneIds = [
|
|
338
|
+
store.createMilestone(roadmapId, { title: "M1" }).id,
|
|
339
|
+
store.createMilestone(roadmapId, { title: "M2" }).id,
|
|
340
|
+
store.createMilestone(roadmapId, { title: "M3" }).id,
|
|
341
|
+
];
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it("reorders milestones with complete list", () => {
|
|
345
|
+
const input: RoadmapMilestoneReorderInput = {
|
|
346
|
+
roadmapId,
|
|
347
|
+
orderedMilestoneIds: [milestoneIds[2], milestoneIds[0], milestoneIds[1]],
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
const reordered = store.reorderMilestones(input);
|
|
351
|
+
|
|
352
|
+
expect(reordered.length).toBe(3);
|
|
353
|
+
expect(reordered[0].id).toBe(milestoneIds[2]);
|
|
354
|
+
expect(reordered[0].orderIndex).toBe(0);
|
|
355
|
+
expect(reordered[1].id).toBe(milestoneIds[0]);
|
|
356
|
+
expect(reordered[1].orderIndex).toBe(1);
|
|
357
|
+
expect(reordered[2].id).toBe(milestoneIds[1]);
|
|
358
|
+
expect(reordered[2].orderIndex).toBe(2);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it("rejects partial reorder list", () => {
|
|
362
|
+
const input: RoadmapMilestoneReorderInput = {
|
|
363
|
+
roadmapId,
|
|
364
|
+
orderedMilestoneIds: [milestoneIds[2], milestoneIds[0]], // Missing milestoneIds[1]
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
expect(() => store.reorderMilestones(input))
|
|
368
|
+
.toThrow("Expected 3 milestone ids but received 2");
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
it("rejects duplicate reorder list", () => {
|
|
372
|
+
const input: RoadmapMilestoneReorderInput = {
|
|
373
|
+
roadmapId,
|
|
374
|
+
orderedMilestoneIds: [milestoneIds[0], milestoneIds[0], milestoneIds[1]], // Duplicate
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
expect(() => store.reorderMilestones(input))
|
|
378
|
+
.toThrow("Duplicate milestone id in requested order");
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
it("rejects non-existent milestone in reorder list", () => {
|
|
382
|
+
const input: RoadmapMilestoneReorderInput = {
|
|
383
|
+
roadmapId,
|
|
384
|
+
orderedMilestoneIds: [milestoneIds[0], milestoneIds[1], "RMS-nonexistent"],
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
expect(() => store.reorderMilestones(input))
|
|
388
|
+
.toThrow("Milestone RMS-nonexistent not found");
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
it("emits milestone:reordered event", () => {
|
|
392
|
+
const listener = vi.fn();
|
|
393
|
+
store.on("milestone:reordered", listener);
|
|
394
|
+
|
|
395
|
+
const input: RoadmapMilestoneReorderInput = {
|
|
396
|
+
roadmapId,
|
|
397
|
+
orderedMilestoneIds: [milestoneIds[1], milestoneIds[0], milestoneIds[2]],
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
store.reorderMilestones(input);
|
|
401
|
+
|
|
402
|
+
expect(listener).toHaveBeenCalledTimes(1);
|
|
403
|
+
expect(listener).toHaveBeenCalledWith({
|
|
404
|
+
roadmapId,
|
|
405
|
+
milestones: expect.any(Array),
|
|
406
|
+
});
|
|
407
|
+
});
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
describe("feature reorder", () => {
|
|
411
|
+
let roadmapId: string;
|
|
412
|
+
let milestoneId: string;
|
|
413
|
+
let featureIds: string[];
|
|
414
|
+
|
|
415
|
+
beforeEach(() => {
|
|
416
|
+
roadmapId = store.createRoadmap({ title: "Test Roadmap" }).id;
|
|
417
|
+
milestoneId = store.createMilestone(roadmapId, { title: "Test Milestone" }).id;
|
|
418
|
+
featureIds = [
|
|
419
|
+
store.createFeature(milestoneId, { title: "F1" }).id,
|
|
420
|
+
store.createFeature(milestoneId, { title: "F2" }).id,
|
|
421
|
+
store.createFeature(milestoneId, { title: "F3" }).id,
|
|
422
|
+
];
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
it("reorders features with complete list", () => {
|
|
426
|
+
const input: RoadmapFeatureReorderInput = {
|
|
427
|
+
roadmapId,
|
|
428
|
+
milestoneId,
|
|
429
|
+
orderedFeatureIds: [featureIds[2], featureIds[0], featureIds[1]],
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
const reordered = store.reorderFeatures(input);
|
|
433
|
+
|
|
434
|
+
expect(reordered.length).toBe(3);
|
|
435
|
+
expect(reordered[0].id).toBe(featureIds[2]);
|
|
436
|
+
expect(reordered[0].orderIndex).toBe(0);
|
|
437
|
+
expect(reordered[1].id).toBe(featureIds[0]);
|
|
438
|
+
expect(reordered[1].orderIndex).toBe(1);
|
|
439
|
+
expect(reordered[2].id).toBe(featureIds[1]);
|
|
440
|
+
expect(reordered[2].orderIndex).toBe(2);
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
it("rejects partial reorder list", () => {
|
|
444
|
+
const input: RoadmapFeatureReorderInput = {
|
|
445
|
+
roadmapId,
|
|
446
|
+
milestoneId,
|
|
447
|
+
orderedFeatureIds: [featureIds[2], featureIds[0]], // Missing featureIds[1]
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
expect(() => store.reorderFeatures(input))
|
|
451
|
+
.toThrow("Expected 3 feature ids but received 2");
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
it("rejects duplicate reorder list", () => {
|
|
455
|
+
const input: RoadmapFeatureReorderInput = {
|
|
456
|
+
roadmapId,
|
|
457
|
+
milestoneId,
|
|
458
|
+
orderedFeatureIds: [featureIds[0], featureIds[0], featureIds[1]], // Duplicate
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
expect(() => store.reorderFeatures(input))
|
|
462
|
+
.toThrow("Duplicate feature id in requested order");
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
it("rejects feature from wrong milestone", () => {
|
|
466
|
+
const m2 = store.createMilestone(roadmapId, { title: "M2" });
|
|
467
|
+
const fWrongMilestone = store.createFeature(m2.id, { title: "Wrong" });
|
|
468
|
+
|
|
469
|
+
const input: RoadmapFeatureReorderInput = {
|
|
470
|
+
roadmapId,
|
|
471
|
+
milestoneId,
|
|
472
|
+
orderedFeatureIds: [featureIds[0], featureIds[1], fWrongMilestone.id],
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
expect(() => store.reorderFeatures(input))
|
|
476
|
+
.toThrow(`Feature ${fWrongMilestone.id} not found in scoped list`);
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
it("emits feature:reordered event", () => {
|
|
480
|
+
const listener = vi.fn();
|
|
481
|
+
store.on("feature:reordered", listener);
|
|
482
|
+
|
|
483
|
+
const input: RoadmapFeatureReorderInput = {
|
|
484
|
+
roadmapId,
|
|
485
|
+
milestoneId,
|
|
486
|
+
orderedFeatureIds: [featureIds[1], featureIds[0], featureIds[2]],
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
store.reorderFeatures(input);
|
|
490
|
+
|
|
491
|
+
expect(listener).toHaveBeenCalledTimes(1);
|
|
492
|
+
expect(listener).toHaveBeenCalledWith({
|
|
493
|
+
milestoneId,
|
|
494
|
+
features: expect.any(Array),
|
|
495
|
+
});
|
|
496
|
+
});
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
describe("feature move", () => {
|
|
500
|
+
let roadmapId: string;
|
|
501
|
+
let milestoneA: string;
|
|
502
|
+
let milestoneB: string;
|
|
503
|
+
let featureA1: string;
|
|
504
|
+
let featureA2: string;
|
|
505
|
+
let featureB1: string;
|
|
506
|
+
|
|
507
|
+
beforeEach(() => {
|
|
508
|
+
roadmapId = store.createRoadmap({ title: "Test Roadmap" }).id;
|
|
509
|
+
milestoneA = store.createMilestone(roadmapId, { title: "Milestone A" }).id;
|
|
510
|
+
milestoneB = store.createMilestone(roadmapId, { title: "Milestone B" }).id;
|
|
511
|
+
featureA1 = store.createFeature(milestoneA, { title: "A1" }).id;
|
|
512
|
+
featureA2 = store.createFeature(milestoneA, { title: "A2" }).id;
|
|
513
|
+
featureB1 = store.createFeature(milestoneB, { title: "B1" }).id;
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
it("moves feature within same milestone", () => {
|
|
517
|
+
const input: RoadmapFeatureMoveInput = {
|
|
518
|
+
roadmapId,
|
|
519
|
+
featureId: featureA1,
|
|
520
|
+
fromMilestoneId: milestoneA,
|
|
521
|
+
toMilestoneId: milestoneA,
|
|
522
|
+
targetOrderIndex: 1,
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
const result = store.moveFeature(input);
|
|
526
|
+
|
|
527
|
+
expect(result.movedFeature.id).toBe(featureA1);
|
|
528
|
+
expect(result.movedFeature.milestoneId).toBe(milestoneA);
|
|
529
|
+
// Same milestone move: source and target are the same list
|
|
530
|
+
expect(result.sourceMilestoneFeatures.length).toBe(2);
|
|
531
|
+
expect(result.targetMilestoneFeatures.length).toBe(2);
|
|
532
|
+
expect(result.sourceMilestoneFeatures).toEqual(result.targetMilestoneFeatures);
|
|
533
|
+
|
|
534
|
+
// featureA1 should now be at index 1 (A2 at 0, A1 at 1)
|
|
535
|
+
const moved = result.sourceMilestoneFeatures.find((f) => f.id === featureA1);
|
|
536
|
+
expect(moved?.orderIndex).toBe(1);
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
it("moves feature across milestones", () => {
|
|
540
|
+
const input: RoadmapFeatureMoveInput = {
|
|
541
|
+
roadmapId,
|
|
542
|
+
featureId: featureA1,
|
|
543
|
+
fromMilestoneId: milestoneA,
|
|
544
|
+
toMilestoneId: milestoneB,
|
|
545
|
+
targetOrderIndex: 1,
|
|
546
|
+
};
|
|
547
|
+
|
|
548
|
+
const result = store.moveFeature(input);
|
|
549
|
+
|
|
550
|
+
expect(result.movedFeature.id).toBe(featureA1);
|
|
551
|
+
expect(result.movedFeature.milestoneId).toBe(milestoneB);
|
|
552
|
+
expect(result.movedFeature.orderIndex).toBe(1);
|
|
553
|
+
|
|
554
|
+
// Source milestone should have featureA2 only
|
|
555
|
+
expect(result.sourceMilestoneFeatures.length).toBe(1);
|
|
556
|
+
expect(result.sourceMilestoneFeatures[0].id).toBe(featureA2);
|
|
557
|
+
|
|
558
|
+
// Target milestone should have B1 and A1
|
|
559
|
+
expect(result.targetMilestoneFeatures.length).toBe(2);
|
|
560
|
+
expect(result.targetMilestoneFeatures[0].id).toBe(featureB1);
|
|
561
|
+
expect(result.targetMilestoneFeatures[1].id).toBe(featureA1);
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
it("atomically renumbers both milestones on cross-milestone move", () => {
|
|
565
|
+
const input: RoadmapFeatureMoveInput = {
|
|
566
|
+
roadmapId,
|
|
567
|
+
featureId: featureA1,
|
|
568
|
+
fromMilestoneId: milestoneA,
|
|
569
|
+
toMilestoneId: milestoneB,
|
|
570
|
+
targetOrderIndex: 0,
|
|
571
|
+
};
|
|
572
|
+
|
|
573
|
+
const result = store.moveFeature(input);
|
|
574
|
+
|
|
575
|
+
// Verify source milestone renumbered
|
|
576
|
+
const sourceOrder = result.sourceMilestoneFeatures.map((f) => f.orderIndex);
|
|
577
|
+
expect(sourceOrder).toEqual([0]); // Only A2 remains, should be 0
|
|
578
|
+
|
|
579
|
+
// Verify target milestone renumbered
|
|
580
|
+
const targetOrder = result.targetMilestoneFeatures.map((f) => f.orderIndex);
|
|
581
|
+
expect(targetOrder).toEqual([0, 1]); // A1 at 0, B1 at 1
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
it("rejects move of non-existent feature", () => {
|
|
585
|
+
const input: RoadmapFeatureMoveInput = {
|
|
586
|
+
roadmapId,
|
|
587
|
+
featureId: "RF-nonexistent",
|
|
588
|
+
fromMilestoneId: milestoneA,
|
|
589
|
+
toMilestoneId: milestoneB,
|
|
590
|
+
targetOrderIndex: 0,
|
|
591
|
+
};
|
|
592
|
+
|
|
593
|
+
expect(() => store.moveFeature(input))
|
|
594
|
+
.toThrow("Feature RF-nonexistent not found in affected milestone scope");
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
it("rejects move from non-existent milestone", () => {
|
|
598
|
+
const input: RoadmapFeatureMoveInput = {
|
|
599
|
+
roadmapId,
|
|
600
|
+
featureId: featureA1,
|
|
601
|
+
fromMilestoneId: "RMS-nonexistent",
|
|
602
|
+
toMilestoneId: milestoneB,
|
|
603
|
+
targetOrderIndex: 0,
|
|
604
|
+
};
|
|
605
|
+
|
|
606
|
+
expect(() => store.moveFeature(input))
|
|
607
|
+
.toThrow("Source milestone RMS-nonexistent not found");
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
it("rejects move to non-existent milestone", () => {
|
|
611
|
+
const input: RoadmapFeatureMoveInput = {
|
|
612
|
+
roadmapId,
|
|
613
|
+
featureId: featureA1,
|
|
614
|
+
fromMilestoneId: milestoneA,
|
|
615
|
+
toMilestoneId: "RMS-nonexistent",
|
|
616
|
+
targetOrderIndex: 0,
|
|
617
|
+
};
|
|
618
|
+
|
|
619
|
+
expect(() => store.moveFeature(input))
|
|
620
|
+
.toThrow("Destination milestone RMS-nonexistent not found");
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
it("rejects move to milestone in different roadmap", () => {
|
|
624
|
+
const otherRoadmap = store.createRoadmap({ title: "Other" }).id;
|
|
625
|
+
const otherMilestone = store.createMilestone(otherRoadmap, { title: "Other M" }).id;
|
|
626
|
+
|
|
627
|
+
const input: RoadmapFeatureMoveInput = {
|
|
628
|
+
roadmapId,
|
|
629
|
+
featureId: featureA1,
|
|
630
|
+
fromMilestoneId: milestoneA,
|
|
631
|
+
toMilestoneId: otherMilestone,
|
|
632
|
+
targetOrderIndex: 0,
|
|
633
|
+
};
|
|
634
|
+
|
|
635
|
+
expect(() => store.moveFeature(input))
|
|
636
|
+
.toThrow(`Destination milestone ${otherMilestone} does not belong to roadmap ${roadmapId}`);
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
it("emits feature:moved event", () => {
|
|
640
|
+
const listener = vi.fn();
|
|
641
|
+
store.on("feature:moved", listener);
|
|
642
|
+
|
|
643
|
+
const input: RoadmapFeatureMoveInput = {
|
|
644
|
+
roadmapId,
|
|
645
|
+
featureId: featureA1,
|
|
646
|
+
fromMilestoneId: milestoneA,
|
|
647
|
+
toMilestoneId: milestoneB,
|
|
648
|
+
targetOrderIndex: 0,
|
|
649
|
+
};
|
|
650
|
+
|
|
651
|
+
store.moveFeature(input);
|
|
652
|
+
|
|
653
|
+
expect(listener).toHaveBeenCalledTimes(1);
|
|
654
|
+
expect(listener).toHaveBeenCalledWith({
|
|
655
|
+
feature: expect.objectContaining({ id: featureA1, milestoneId: milestoneB }),
|
|
656
|
+
fromMilestoneId: milestoneA,
|
|
657
|
+
toMilestoneId: milestoneB,
|
|
658
|
+
});
|
|
659
|
+
});
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
describe("hierarchy operations", () => {
|
|
663
|
+
let roadmapId: string;
|
|
664
|
+
let milestoneId1: string;
|
|
665
|
+
let milestoneId2: string;
|
|
666
|
+
|
|
667
|
+
beforeEach(() => {
|
|
668
|
+
roadmapId = store.createRoadmap({ title: "Test Roadmap" }).id;
|
|
669
|
+
milestoneId1 = store.createMilestone(roadmapId, { title: "M1" }).id;
|
|
670
|
+
milestoneId2 = store.createMilestone(roadmapId, { title: "M2" }).id;
|
|
671
|
+
store.createFeature(milestoneId1, { title: "F1" });
|
|
672
|
+
store.createFeature(milestoneId1, { title: "F2" });
|
|
673
|
+
store.createFeature(milestoneId2, { title: "F3" });
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
it("gets milestone with features", () => {
|
|
677
|
+
const result = store.getMilestoneWithFeatures(milestoneId1);
|
|
678
|
+
|
|
679
|
+
expect(result).toBeDefined();
|
|
680
|
+
expect(result!.id).toBe(milestoneId1);
|
|
681
|
+
expect(result!.features.length).toBe(2);
|
|
682
|
+
expect(result!.features[0].title).toBe("F1");
|
|
683
|
+
expect(result!.features[1].title).toBe("F2");
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
it("returns undefined for non-existent milestone in getMilestoneWithFeatures", () => {
|
|
687
|
+
const result = store.getMilestoneWithFeatures("RMS-nonexistent");
|
|
688
|
+
expect(result).toBeUndefined();
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
it("gets roadmap with full hierarchy", () => {
|
|
692
|
+
const result = store.getRoadmapWithHierarchy(roadmapId);
|
|
693
|
+
|
|
694
|
+
expect(result).toBeDefined();
|
|
695
|
+
expect(result!.id).toBe(roadmapId);
|
|
696
|
+
expect(result!.title).toBe("Test Roadmap");
|
|
697
|
+
expect(result!.milestones.length).toBe(2);
|
|
698
|
+
|
|
699
|
+
// Milestones should be in order
|
|
700
|
+
expect(result!.milestones[0].id).toBe(milestoneId1);
|
|
701
|
+
expect(result!.milestones[1].id).toBe(milestoneId2);
|
|
702
|
+
|
|
703
|
+
// Features should be in order
|
|
704
|
+
expect(result!.milestones[0].features.length).toBe(2);
|
|
705
|
+
expect(result!.milestones[1].features.length).toBe(1);
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
it("returns undefined for non-existent roadmap in getRoadmapWithHierarchy", () => {
|
|
709
|
+
const result = store.getRoadmapWithHierarchy("RM-nonexistent");
|
|
710
|
+
expect(result).toBeUndefined();
|
|
711
|
+
});
|
|
712
|
+
});
|
|
713
|
+
|
|
714
|
+
describe("deterministic ordering", () => {
|
|
715
|
+
it("orders by orderIndex, createdAt, id when orderIndex values are equal", () => {
|
|
716
|
+
const roadmapId = store.createRoadmap({ title: "Test" }).id;
|
|
717
|
+
const milestoneId = store.createMilestone(roadmapId, { title: "M1" }).id;
|
|
718
|
+
|
|
719
|
+
// Create features rapidly (same millisecond timestamps possible)
|
|
720
|
+
const f1 = store.createFeature(milestoneId, { title: "Alpha" });
|
|
721
|
+
const f2 = store.createFeature(milestoneId, { title: "Beta" });
|
|
722
|
+
const f3 = store.createFeature(milestoneId, { title: "Gamma" });
|
|
723
|
+
|
|
724
|
+
// Verify deterministic ordering
|
|
725
|
+
const features = store.listFeatures(milestoneId);
|
|
726
|
+
expect(features.map((f) => f.id)).toEqual([f1.id, f2.id, f3.id]);
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
it("handles gap in orderIndex values", () => {
|
|
730
|
+
const roadmapId = store.createRoadmap({ title: "Test" }).id;
|
|
731
|
+
const milestoneId = store.createMilestone(roadmapId, { title: "M" }).id;
|
|
732
|
+
|
|
733
|
+
// Manually create gaps
|
|
734
|
+
db.prepare("UPDATE roadmap_milestones SET orderIndex = 10 WHERE id = ?").run(milestoneId);
|
|
735
|
+
db.prepare("UPDATE roadmap_milestones SET orderIndex = 20 WHERE id = ?").run(
|
|
736
|
+
store.createMilestone(roadmapId, { title: "Second" }).id
|
|
737
|
+
);
|
|
738
|
+
|
|
739
|
+
const milestones = store.listMilestones(roadmapId);
|
|
740
|
+
expect(milestones[0].orderIndex).toBe(10);
|
|
741
|
+
expect(milestones[1].orderIndex).toBe(20);
|
|
742
|
+
});
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
describe("schema version", () => {
|
|
746
|
+
it("schema version is 40 after init", () => {
|
|
747
|
+
expect(db.getSchemaVersion()).toBe(70);
|
|
748
|
+
});
|
|
749
|
+
});
|
|
750
|
+
|
|
751
|
+
describe("export / handoff", () => {
|
|
752
|
+
describe("getRoadmapExport", () => {
|
|
753
|
+
it("returns flat export bundle with all entities", () => {
|
|
754
|
+
const roadmap = store.createRoadmap({ title: "Export Test", description: "Test description" });
|
|
755
|
+
const m1 = store.createMilestone(roadmap.id, { title: "Milestone 1" });
|
|
756
|
+
const m2 = store.createMilestone(roadmap.id, { title: "Milestone 2" });
|
|
757
|
+
const f1 = store.createFeature(m1.id, { title: "Feature 1" });
|
|
758
|
+
const f2 = store.createFeature(m1.id, { title: "Feature 2" });
|
|
759
|
+
const f3 = store.createFeature(m2.id, { title: "Feature 3" });
|
|
760
|
+
|
|
761
|
+
const export_ = store.getRoadmapExport(roadmap.id);
|
|
762
|
+
|
|
763
|
+
expect(export_.roadmap.id).toBe(roadmap.id);
|
|
764
|
+
expect(export_.roadmap.title).toBe("Export Test");
|
|
765
|
+
expect(export_.milestones.length).toBe(2);
|
|
766
|
+
expect(export_.features.length).toBe(3);
|
|
767
|
+
expect(export_.features.map((f) => f.id)).toEqual([f1.id, f2.id, f3.id]);
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
it("returns milestones in deterministic order", () => {
|
|
771
|
+
const roadmap = store.createRoadmap({ title: "Order Test" });
|
|
772
|
+
// Create in reverse order to test deterministic sorting
|
|
773
|
+
const m2 = store.createMilestone(roadmap.id, { title: "Second" });
|
|
774
|
+
const m1 = store.createMilestone(roadmap.id, { title: "First" });
|
|
775
|
+
|
|
776
|
+
// m2 was created first (orderIndex 0), m1 was created second (orderIndex 1)
|
|
777
|
+
// Deterministic order: orderIndex ASC → m2 comes first
|
|
778
|
+
const export_ = store.getRoadmapExport(roadmap.id);
|
|
779
|
+
|
|
780
|
+
expect(export_.milestones[0].id).toBe(m2.id);
|
|
781
|
+
expect(export_.milestones[1].id).toBe(m1.id);
|
|
782
|
+
});
|
|
783
|
+
|
|
784
|
+
it("returns features grouped by milestone in deterministic order", () => {
|
|
785
|
+
const roadmap = store.createRoadmap({ title: "Features Order" });
|
|
786
|
+
const m1 = store.createMilestone(roadmap.id, { title: "M1" });
|
|
787
|
+
// Create in reverse order to test deterministic sorting
|
|
788
|
+
const f2 = store.createFeature(m1.id, { title: "F2" });
|
|
789
|
+
const f1 = store.createFeature(m1.id, { title: "F1" });
|
|
790
|
+
|
|
791
|
+
// f2 was created first (orderIndex 0), f1 was created second (orderIndex 1)
|
|
792
|
+
// Deterministic order: orderIndex ASC → f2 comes first
|
|
793
|
+
const export_ = store.getRoadmapExport(roadmap.id);
|
|
794
|
+
|
|
795
|
+
expect(export_.features.length).toBe(2);
|
|
796
|
+
expect(export_.features[0].id).toBe(f2.id);
|
|
797
|
+
expect(export_.features[1].id).toBe(f1.id);
|
|
798
|
+
});
|
|
799
|
+
|
|
800
|
+
it("throws for non-existent roadmap", () => {
|
|
801
|
+
expect(() => store.getRoadmapExport("RM-nonexistent")).toThrow("Roadmap RM-nonexistent not found");
|
|
802
|
+
});
|
|
803
|
+
|
|
804
|
+
it("returns empty arrays when roadmap has no milestones", () => {
|
|
805
|
+
const roadmap = store.createRoadmap({ title: "Empty" });
|
|
806
|
+
const export_ = store.getRoadmapExport(roadmap.id);
|
|
807
|
+
|
|
808
|
+
expect(export_.milestones).toEqual([]);
|
|
809
|
+
expect(export_.features).toEqual([]);
|
|
810
|
+
});
|
|
811
|
+
});
|
|
812
|
+
|
|
813
|
+
describe("getRoadmapMissionHandoff", () => {
|
|
814
|
+
it("returns mission planning handoff with source IDs preserved", () => {
|
|
815
|
+
const roadmap = store.createRoadmap({ title: "Mission Handoff", description: "Mission desc" });
|
|
816
|
+
const m1 = store.createMilestone(roadmap.id, { title: "Phase 1", description: "Phase 1 desc" });
|
|
817
|
+
const m2 = store.createMilestone(roadmap.id, { title: "Phase 2" });
|
|
818
|
+
const f1 = store.createFeature(m1.id, { title: "Task A", description: "Task A desc" });
|
|
819
|
+
const f2 = store.createFeature(m2.id, { title: "Task B" });
|
|
820
|
+
|
|
821
|
+
const handoff = store.getRoadmapMissionHandoff(roadmap.id);
|
|
822
|
+
|
|
823
|
+
expect(handoff.sourceRoadmapId).toBe(roadmap.id);
|
|
824
|
+
expect(handoff.title).toBe("Mission Handoff");
|
|
825
|
+
expect(handoff.description).toBe("Mission desc");
|
|
826
|
+
expect(handoff.milestones.length).toBe(2);
|
|
827
|
+
expect(handoff.milestones[0].sourceMilestoneId).toBe(m1.id);
|
|
828
|
+
expect(handoff.milestones[0].title).toBe("Phase 1");
|
|
829
|
+
expect(handoff.milestones[0].description).toBe("Phase 1 desc");
|
|
830
|
+
expect(handoff.milestones[0].features.length).toBe(1);
|
|
831
|
+
expect(handoff.milestones[0].features[0].sourceFeatureId).toBe(f1.id);
|
|
832
|
+
expect(handoff.milestones[0].features[0].title).toBe("Task A");
|
|
833
|
+
expect(handoff.milestones[1].features.length).toBe(1);
|
|
834
|
+
expect(handoff.milestones[1].features[0].sourceFeatureId).toBe(f2.id);
|
|
835
|
+
});
|
|
836
|
+
|
|
837
|
+
it("preserves deterministic ordering in handoff", () => {
|
|
838
|
+
const roadmap = store.createRoadmap({ title: "Order Check" });
|
|
839
|
+
const m1 = store.createMilestone(roadmap.id, { title: "M1" });
|
|
840
|
+
const f1 = store.createFeature(m1.id, { title: "First" });
|
|
841
|
+
const f2 = store.createFeature(m1.id, { title: "Second" });
|
|
842
|
+
const f3 = store.createFeature(m1.id, { title: "Third" });
|
|
843
|
+
|
|
844
|
+
const handoff = store.getRoadmapMissionHandoff(roadmap.id);
|
|
845
|
+
|
|
846
|
+
expect(handoff.milestones[0].features[0].sourceFeatureId).toBe(f1.id);
|
|
847
|
+
expect(handoff.milestones[0].features[1].sourceFeatureId).toBe(f2.id);
|
|
848
|
+
expect(handoff.milestones[0].features[2].sourceFeatureId).toBe(f3.id);
|
|
849
|
+
});
|
|
850
|
+
|
|
851
|
+
it("throws for non-existent roadmap", () => {
|
|
852
|
+
expect(() => store.getRoadmapMissionHandoff("RM-nonexistent")).toThrow("Roadmap RM-nonexistent not found");
|
|
853
|
+
});
|
|
854
|
+
|
|
855
|
+
it("includes orderIndex for milestones and features", () => {
|
|
856
|
+
const roadmap = store.createRoadmap({ title: "Index Test" });
|
|
857
|
+
const m1 = store.createMilestone(roadmap.id, { title: "First" });
|
|
858
|
+
const m2 = store.createMilestone(roadmap.id, { title: "Second" });
|
|
859
|
+
const f1 = store.createFeature(m1.id, { title: "F1" });
|
|
860
|
+
const f2 = store.createFeature(m2.id, { title: "F2" });
|
|
861
|
+
|
|
862
|
+
const handoff = store.getRoadmapMissionHandoff(roadmap.id);
|
|
863
|
+
|
|
864
|
+
// Milestones should be in deterministic order (m1 created first, so orderIndex 0)
|
|
865
|
+
expect(handoff.milestones[0].sourceMilestoneId).toBe(m1.id);
|
|
866
|
+
expect(handoff.milestones[0].orderIndex).toBeDefined();
|
|
867
|
+
expect(handoff.milestones[1].sourceMilestoneId).toBe(m2.id);
|
|
868
|
+
expect(handoff.milestones[1].orderIndex).toBeDefined();
|
|
869
|
+
// Features should be in deterministic order
|
|
870
|
+
expect(handoff.milestones[0].features[0].sourceFeatureId).toBe(f1.id);
|
|
871
|
+
expect(handoff.milestones[0].features[0].orderIndex).toBeDefined();
|
|
872
|
+
expect(handoff.milestones[1].features[0].sourceFeatureId).toBe(f2.id);
|
|
873
|
+
expect(handoff.milestones[1].features[0].orderIndex).toBeDefined();
|
|
874
|
+
});
|
|
875
|
+
});
|
|
876
|
+
|
|
877
|
+
describe("getRoadmapFeatureHandoff", () => {
|
|
878
|
+
it("returns task planning handoff for a feature", () => {
|
|
879
|
+
const roadmap = store.createRoadmap({ title: "Feature Handoff" });
|
|
880
|
+
const m1 = store.createMilestone(roadmap.id, { title: "Phase 1" });
|
|
881
|
+
const f1 = store.createFeature(m1.id, { title: "Feature A", description: "Feature A desc" });
|
|
882
|
+
|
|
883
|
+
const handoff = store.getRoadmapFeatureHandoff(roadmap.id, m1.id, f1.id);
|
|
884
|
+
|
|
885
|
+
expect(handoff.source.roadmapId).toBe(roadmap.id);
|
|
886
|
+
expect(handoff.source.milestoneId).toBe(m1.id);
|
|
887
|
+
expect(handoff.source.featureId).toBe(f1.id);
|
|
888
|
+
expect(handoff.source.roadmapTitle).toBe("Feature Handoff");
|
|
889
|
+
expect(handoff.source.milestoneTitle).toBe("Phase 1");
|
|
890
|
+
expect(handoff.source.milestoneOrderIndex).toBeDefined();
|
|
891
|
+
expect(handoff.source.featureOrderIndex).toBeDefined();
|
|
892
|
+
expect(handoff.title).toBe("Feature A");
|
|
893
|
+
expect(handoff.description).toBe("Feature A desc");
|
|
894
|
+
});
|
|
895
|
+
|
|
896
|
+
it("throws for non-existent roadmap", () => {
|
|
897
|
+
const roadmap = store.createRoadmap({ title: "Test" });
|
|
898
|
+
const m1 = store.createMilestone(roadmap.id, { title: "M1" });
|
|
899
|
+
const f1 = store.createFeature(m1.id, { title: "F1" });
|
|
900
|
+
|
|
901
|
+
expect(() => store.getRoadmapFeatureHandoff("RM-nonexistent", m1.id, f1.id)).toThrow("Roadmap RM-nonexistent not found");
|
|
902
|
+
});
|
|
903
|
+
|
|
904
|
+
it("throws for non-existent milestone", () => {
|
|
905
|
+
const roadmap = store.createRoadmap({ title: "Test" });
|
|
906
|
+
|
|
907
|
+
expect(() => store.getRoadmapFeatureHandoff(roadmap.id, "RMS-nonexistent", "RF-nonexistent")).toThrow("Milestone RMS-nonexistent not found");
|
|
908
|
+
});
|
|
909
|
+
|
|
910
|
+
it("throws when milestone does not belong to roadmap", () => {
|
|
911
|
+
const roadmap1 = store.createRoadmap({ title: "Roadmap 1" });
|
|
912
|
+
const roadmap2 = store.createRoadmap({ title: "Roadmap 2" });
|
|
913
|
+
const m2 = store.createMilestone(roadmap2.id, { title: "M2" });
|
|
914
|
+
const f2 = store.createFeature(m2.id, { title: "F2" });
|
|
915
|
+
|
|
916
|
+
expect(() => store.getRoadmapFeatureHandoff(roadmap1.id, m2.id, f2.id)).toThrow(`Milestone ${m2.id} does not belong to roadmap ${roadmap1.id}`);
|
|
917
|
+
});
|
|
918
|
+
|
|
919
|
+
it("throws for non-existent feature", () => {
|
|
920
|
+
const roadmap = store.createRoadmap({ title: "Test" });
|
|
921
|
+
const m1 = store.createMilestone(roadmap.id, { title: "M1" });
|
|
922
|
+
|
|
923
|
+
expect(() => store.getRoadmapFeatureHandoff(roadmap.id, m1.id, "RF-nonexistent")).toThrow("Feature RF-nonexistent not found");
|
|
924
|
+
});
|
|
925
|
+
|
|
926
|
+
it("throws when feature does not belong to milestone", () => {
|
|
927
|
+
const roadmap = store.createRoadmap({ title: "Test" });
|
|
928
|
+
const m1 = store.createMilestone(roadmap.id, { title: "M1" });
|
|
929
|
+
const m2 = store.createMilestone(roadmap.id, { title: "M2" });
|
|
930
|
+
const f2 = store.createFeature(m2.id, { title: "F2" });
|
|
931
|
+
|
|
932
|
+
expect(() => store.getRoadmapFeatureHandoff(roadmap.id, m1.id, f2.id)).toThrow(`Feature ${f2.id} does not belong to milestone ${m1.id}`);
|
|
933
|
+
});
|
|
934
|
+
|
|
935
|
+
it("includes order indices in source reference", () => {
|
|
936
|
+
const roadmap = store.createRoadmap({ title: "Order Test" });
|
|
937
|
+
const m1 = store.createMilestone(roadmap.id, { title: "First" });
|
|
938
|
+
const m2 = store.createMilestone(roadmap.id, { title: "Second" });
|
|
939
|
+
const f1 = store.createFeature(m1.id, { title: "F1" });
|
|
940
|
+
const f2 = store.createFeature(m2.id, { title: "F2" });
|
|
941
|
+
|
|
942
|
+
// M1 is created first so has orderIndex 0, M2 has orderIndex 1
|
|
943
|
+
const handoff1 = store.getRoadmapFeatureHandoff(roadmap.id, m1.id, f1.id);
|
|
944
|
+
const handoff2 = store.getRoadmapFeatureHandoff(roadmap.id, m2.id, f2.id);
|
|
945
|
+
|
|
946
|
+
// m1 was created first so it has lower orderIndex
|
|
947
|
+
expect(handoff1.source.milestoneOrderIndex).toBeLessThan(handoff2.source.milestoneOrderIndex);
|
|
948
|
+
});
|
|
949
|
+
});
|
|
950
|
+
|
|
951
|
+
describe("getMissionPlanningHandoff", () => {
|
|
952
|
+
it("is an alias for getRoadmapMissionHandoff with same behavior", () => {
|
|
953
|
+
const roadmap = store.createRoadmap({ title: "Alias Test" });
|
|
954
|
+
const m1 = store.createMilestone(roadmap.id, { title: "M1" });
|
|
955
|
+
const f1 = store.createFeature(m1.id, { title: "F1" });
|
|
956
|
+
|
|
957
|
+
const result1 = store.getRoadmapMissionHandoff(roadmap.id);
|
|
958
|
+
const result2 = store.getMissionPlanningHandoff(roadmap.id);
|
|
959
|
+
|
|
960
|
+
// Both should return equivalent results
|
|
961
|
+
expect(result1.sourceRoadmapId).toBe(result2.sourceRoadmapId);
|
|
962
|
+
expect(result1.title).toBe(result2.title);
|
|
963
|
+
expect(result1.description).toBe(result2.description);
|
|
964
|
+
expect(result1.milestones.length).toBe(result2.milestones.length);
|
|
965
|
+
expect(result1.milestones[0].sourceMilestoneId).toBe(result2.milestones[0].sourceMilestoneId);
|
|
966
|
+
});
|
|
967
|
+
});
|
|
968
|
+
|
|
969
|
+
describe("listFeatureTaskPlanningHandoffs", () => {
|
|
970
|
+
it("returns empty array for roadmap with no milestones", () => {
|
|
971
|
+
const roadmap = store.createRoadmap({ title: "Empty" });
|
|
972
|
+
const handoffs = store.listFeatureTaskPlanningHandoffs(roadmap.id);
|
|
973
|
+
expect(handoffs).toEqual([]);
|
|
974
|
+
});
|
|
975
|
+
|
|
976
|
+
it("returns empty array for roadmap with milestones but no features", () => {
|
|
977
|
+
const roadmap = store.createRoadmap({ title: "No Features" });
|
|
978
|
+
store.createMilestone(roadmap.id, { title: "M1" });
|
|
979
|
+
store.createMilestone(roadmap.id, { title: "M2" });
|
|
980
|
+
|
|
981
|
+
const handoffs = store.listFeatureTaskPlanningHandoffs(roadmap.id);
|
|
982
|
+
expect(handoffs).toEqual([]);
|
|
983
|
+
});
|
|
984
|
+
|
|
985
|
+
it("returns flattened feature handoffs in deterministic order", () => {
|
|
986
|
+
const roadmap = store.createRoadmap({ title: "Flat Test" });
|
|
987
|
+
const m1 = store.createMilestone(roadmap.id, { title: "M1" });
|
|
988
|
+
const m2 = store.createMilestone(roadmap.id, { title: "M2" });
|
|
989
|
+
const f1 = store.createFeature(m1.id, { title: "F1", description: "Desc 1" });
|
|
990
|
+
const f2 = store.createFeature(m1.id, { title: "F2", description: "Desc 2" });
|
|
991
|
+
const f3 = store.createFeature(m2.id, { title: "F3" });
|
|
992
|
+
|
|
993
|
+
const handoffs = store.listFeatureTaskPlanningHandoffs(roadmap.id);
|
|
994
|
+
|
|
995
|
+
expect(handoffs).toHaveLength(3);
|
|
996
|
+
// Milestone order: m1 (orderIndex 0), m2 (orderIndex 1)
|
|
997
|
+
// Feature order within milestone: f1 (orderIndex 0), f2 (orderIndex 1)
|
|
998
|
+
// Flattened: f1, f2, f3
|
|
999
|
+
expect(handoffs[0].source.featureId).toBe(f1.id);
|
|
1000
|
+
expect(handoffs[0].title).toBe("F1");
|
|
1001
|
+
expect(handoffs[0].description).toBe("Desc 1");
|
|
1002
|
+
expect(handoffs[1].source.featureId).toBe(f2.id);
|
|
1003
|
+
expect(handoffs[1].title).toBe("F2");
|
|
1004
|
+
expect(handoffs[2].source.featureId).toBe(f3.id);
|
|
1005
|
+
});
|
|
1006
|
+
|
|
1007
|
+
it("preserves source lineage in each handoff", () => {
|
|
1008
|
+
const roadmap = store.createRoadmap({ title: "Lineage Test", description: "Roadmap desc" });
|
|
1009
|
+
const m1 = store.createMilestone(roadmap.id, { title: "Milestone Title" });
|
|
1010
|
+
const f1 = store.createFeature(m1.id, { title: "Feature Title", description: "Feature desc" });
|
|
1011
|
+
|
|
1012
|
+
const handoffs = store.listFeatureTaskPlanningHandoffs(roadmap.id);
|
|
1013
|
+
|
|
1014
|
+
expect(handoffs).toHaveLength(1);
|
|
1015
|
+
expect(handoffs[0].source.roadmapId).toBe(roadmap.id);
|
|
1016
|
+
expect(handoffs[0].source.roadmapTitle).toBe("Lineage Test");
|
|
1017
|
+
expect(handoffs[0].source.milestoneId).toBe(m1.id);
|
|
1018
|
+
expect(handoffs[0].source.milestoneTitle).toBe("Milestone Title");
|
|
1019
|
+
expect(handoffs[0].source.featureId).toBe(f1.id);
|
|
1020
|
+
expect(handoffs[0].source.milestoneOrderIndex).toBe(0);
|
|
1021
|
+
expect(handoffs[0].source.featureOrderIndex).toBe(0);
|
|
1022
|
+
});
|
|
1023
|
+
|
|
1024
|
+
it("throws for non-existent roadmap", () => {
|
|
1025
|
+
expect(() => store.listFeatureTaskPlanningHandoffs("RM-nonexistent")).toThrow("Roadmap RM-nonexistent not found");
|
|
1026
|
+
});
|
|
1027
|
+
});
|
|
1028
|
+
});
|
|
1029
|
+
|
|
1030
|
+
describe("persistence re-instantiation", () => {
|
|
1031
|
+
// These tests manage their own setup/teardown to avoid conflicts with shared afterEach
|
|
1032
|
+
it("survives store re-instantiation with all entities intact", async () => {
|
|
1033
|
+
// Create own temp directory for this test
|
|
1034
|
+
const persistTmpDir = makeTmpDir();
|
|
1035
|
+
try {
|
|
1036
|
+
const persistDb = new Database(join(persistTmpDir, ".fusion"));
|
|
1037
|
+
persistDb.init();
|
|
1038
|
+
const persistStore = new RoadmapStore(persistDb);
|
|
1039
|
+
|
|
1040
|
+
// Create a roadmap, milestones, and features
|
|
1041
|
+
const roadmap = persistStore.createRoadmap({ title: "Persistence Test", description: "Test description" });
|
|
1042
|
+
const m1 = persistStore.createMilestone(roadmap.id, { title: "Milestone 1", description: "M1 desc" });
|
|
1043
|
+
const m2 = persistStore.createMilestone(roadmap.id, { title: "Milestone 2" });
|
|
1044
|
+
const f1 = persistStore.createFeature(m1.id, { title: "Feature 1", description: "F1 desc" });
|
|
1045
|
+
const f2 = persistStore.createFeature(m1.id, { title: "Feature 2" });
|
|
1046
|
+
const f3 = persistStore.createFeature(m2.id, { title: "Feature 3" });
|
|
1047
|
+
|
|
1048
|
+
// Close and reopen the store from the same database
|
|
1049
|
+
persistDb.close();
|
|
1050
|
+
|
|
1051
|
+
const reopenedDb = new Database(join(persistTmpDir, ".fusion"));
|
|
1052
|
+
reopenedDb.init();
|
|
1053
|
+
const reopenedStore = new RoadmapStore(reopenedDb);
|
|
1054
|
+
|
|
1055
|
+
// Verify all data persisted correctly
|
|
1056
|
+
const persistedRoadmap = reopenedStore.getRoadmap(roadmap.id);
|
|
1057
|
+
expect(persistedRoadmap).toBeDefined();
|
|
1058
|
+
expect(persistedRoadmap!.title).toBe("Persistence Test");
|
|
1059
|
+
expect(persistedRoadmap!.description).toBe("Test description");
|
|
1060
|
+
|
|
1061
|
+
const persistedMilestones = reopenedStore.listMilestones(roadmap.id);
|
|
1062
|
+
expect(persistedMilestones).toHaveLength(2);
|
|
1063
|
+
expect(persistedMilestones[0].title).toBe("Milestone 1");
|
|
1064
|
+
expect(persistedMilestones[1].title).toBe("Milestone 2");
|
|
1065
|
+
|
|
1066
|
+
const persistedFeaturesM1 = reopenedStore.listFeatures(m1.id);
|
|
1067
|
+
expect(persistedFeaturesM1).toHaveLength(2);
|
|
1068
|
+
expect(persistedFeaturesM1[0].title).toBe("Feature 1");
|
|
1069
|
+
|
|
1070
|
+
const persistedFeaturesM2 = reopenedStore.listFeatures(m2.id);
|
|
1071
|
+
expect(persistedFeaturesM2).toHaveLength(1);
|
|
1072
|
+
expect(persistedFeaturesM2[0].title).toBe("Feature 3");
|
|
1073
|
+
|
|
1074
|
+
reopenedDb.close();
|
|
1075
|
+
} finally {
|
|
1076
|
+
await rm(persistTmpDir, { recursive: true, force: true });
|
|
1077
|
+
}
|
|
1078
|
+
});
|
|
1079
|
+
|
|
1080
|
+
it("survives re-instantiation with reordered entities", async () => {
|
|
1081
|
+
const persistTmpDir = makeTmpDir();
|
|
1082
|
+
try {
|
|
1083
|
+
const persistDb = new Database(join(persistTmpDir, ".fusion"));
|
|
1084
|
+
persistDb.init();
|
|
1085
|
+
const persistStore = new RoadmapStore(persistDb);
|
|
1086
|
+
|
|
1087
|
+
const roadmap = persistStore.createRoadmap({ title: "Reorder Persistence" });
|
|
1088
|
+
const m1 = persistStore.createMilestone(roadmap.id, { title: "M1" });
|
|
1089
|
+
const m2 = persistStore.createMilestone(roadmap.id, { title: "M2" });
|
|
1090
|
+
const m3 = persistStore.createMilestone(roadmap.id, { title: "M3" });
|
|
1091
|
+
const f1 = persistStore.createFeature(m1.id, { title: "F1" });
|
|
1092
|
+
const f2 = persistStore.createFeature(m1.id, { title: "F2" });
|
|
1093
|
+
|
|
1094
|
+
// Reorder milestones
|
|
1095
|
+
persistStore.reorderMilestones({
|
|
1096
|
+
roadmapId: roadmap.id,
|
|
1097
|
+
orderedMilestoneIds: [m3.id, m1.id, m2.id],
|
|
1098
|
+
});
|
|
1099
|
+
|
|
1100
|
+
// Reorder features
|
|
1101
|
+
persistStore.reorderFeatures({
|
|
1102
|
+
roadmapId: roadmap.id,
|
|
1103
|
+
milestoneId: m1.id,
|
|
1104
|
+
orderedFeatureIds: [f2.id, f1.id],
|
|
1105
|
+
});
|
|
1106
|
+
|
|
1107
|
+
// Close and reopen
|
|
1108
|
+
persistDb.close();
|
|
1109
|
+
|
|
1110
|
+
const reopenedDb = new Database(join(persistTmpDir, ".fusion"));
|
|
1111
|
+
reopenedDb.init();
|
|
1112
|
+
const reopenedStore = new RoadmapStore(reopenedDb);
|
|
1113
|
+
|
|
1114
|
+
// Verify reorder persisted
|
|
1115
|
+
const milestones = reopenedStore.listMilestones(roadmap.id);
|
|
1116
|
+
expect(milestones.map((m) => m.id)).toEqual([m3.id, m1.id, m2.id]);
|
|
1117
|
+
expect(milestones.map((m) => m.orderIndex)).toEqual([0, 1, 2]); // Contiguous
|
|
1118
|
+
|
|
1119
|
+
const features = reopenedStore.listFeatures(m1.id);
|
|
1120
|
+
expect(features.map((f) => f.id)).toEqual([f2.id, f1.id]);
|
|
1121
|
+
expect(features.map((f) => f.orderIndex)).toEqual([0, 1]); // Contiguous
|
|
1122
|
+
|
|
1123
|
+
reopenedDb.close();
|
|
1124
|
+
} finally {
|
|
1125
|
+
await rm(persistTmpDir, { recursive: true, force: true });
|
|
1126
|
+
}
|
|
1127
|
+
});
|
|
1128
|
+
|
|
1129
|
+
it("survives re-instantiation with cross-milestone moves", async () => {
|
|
1130
|
+
const persistTmpDir = makeTmpDir();
|
|
1131
|
+
try {
|
|
1132
|
+
const persistDb = new Database(join(persistTmpDir, ".fusion"));
|
|
1133
|
+
persistDb.init();
|
|
1134
|
+
const persistStore = new RoadmapStore(persistDb);
|
|
1135
|
+
|
|
1136
|
+
const roadmap = persistStore.createRoadmap({ title: "Move Persistence" });
|
|
1137
|
+
const m1 = persistStore.createMilestone(roadmap.id, { title: "M1" });
|
|
1138
|
+
const m2 = persistStore.createMilestone(roadmap.id, { title: "M2" });
|
|
1139
|
+
const f1 = persistStore.createFeature(m1.id, { title: "F1" });
|
|
1140
|
+
const f2 = persistStore.createFeature(m1.id, { title: "F2" });
|
|
1141
|
+
const f3 = persistStore.createFeature(m2.id, { title: "F3" });
|
|
1142
|
+
|
|
1143
|
+
// Move f2 from m1 to m2
|
|
1144
|
+
persistStore.moveFeature({
|
|
1145
|
+
roadmapId: roadmap.id,
|
|
1146
|
+
featureId: f2.id,
|
|
1147
|
+
fromMilestoneId: m1.id,
|
|
1148
|
+
toMilestoneId: m2.id,
|
|
1149
|
+
targetOrderIndex: 0,
|
|
1150
|
+
});
|
|
1151
|
+
|
|
1152
|
+
// Close and reopen
|
|
1153
|
+
persistDb.close();
|
|
1154
|
+
|
|
1155
|
+
const reopenedDb = new Database(join(persistTmpDir, ".fusion"));
|
|
1156
|
+
reopenedDb.init();
|
|
1157
|
+
const reopenedStore = new RoadmapStore(reopenedDb);
|
|
1158
|
+
|
|
1159
|
+
// Verify move persisted
|
|
1160
|
+
const f2Persisted = reopenedStore.getFeature(f2.id);
|
|
1161
|
+
expect(f2Persisted!.milestoneId).toBe(m2.id);
|
|
1162
|
+
expect(f2Persisted!.orderIndex).toBe(0);
|
|
1163
|
+
|
|
1164
|
+
// Verify m1 renumbered correctly
|
|
1165
|
+
const m1Features = reopenedStore.listFeatures(m1.id);
|
|
1166
|
+
expect(m1Features).toHaveLength(1);
|
|
1167
|
+
expect(m1Features[0].id).toBe(f1.id);
|
|
1168
|
+
expect(m1Features[0].orderIndex).toBe(0);
|
|
1169
|
+
|
|
1170
|
+
// Verify m2 renumbered correctly
|
|
1171
|
+
const m2Features = reopenedStore.listFeatures(m2.id);
|
|
1172
|
+
expect(m2Features).toHaveLength(2);
|
|
1173
|
+
expect(m2Features[0].id).toBe(f2.id);
|
|
1174
|
+
expect(m2Features[0].orderIndex).toBe(0);
|
|
1175
|
+
expect(m2Features[1].id).toBe(f3.id);
|
|
1176
|
+
expect(m2Features[1].orderIndex).toBe(1);
|
|
1177
|
+
|
|
1178
|
+
reopenedDb.close();
|
|
1179
|
+
} finally {
|
|
1180
|
+
await rm(persistTmpDir, { recursive: true, force: true });
|
|
1181
|
+
}
|
|
1182
|
+
});
|
|
1183
|
+
|
|
1184
|
+
it("cascade-deletes persist after re-instantiation", async () => {
|
|
1185
|
+
const persistTmpDir = makeTmpDir();
|
|
1186
|
+
try {
|
|
1187
|
+
const persistDb = new Database(join(persistTmpDir, ".fusion"));
|
|
1188
|
+
persistDb.init();
|
|
1189
|
+
const persistStore = new RoadmapStore(persistDb);
|
|
1190
|
+
|
|
1191
|
+
const roadmap = persistStore.createRoadmap({ title: "Cascade Test" });
|
|
1192
|
+
const m1 = persistStore.createMilestone(roadmap.id, { title: "M1" });
|
|
1193
|
+
const m2 = persistStore.createMilestone(roadmap.id, { title: "M2" });
|
|
1194
|
+
const f1 = persistStore.createFeature(m1.id, { title: "F1" });
|
|
1195
|
+
const f2 = persistStore.createFeature(m2.id, { title: "F2" });
|
|
1196
|
+
|
|
1197
|
+
// Delete m1 (should cascade delete f1)
|
|
1198
|
+
persistStore.deleteMilestone(m1.id);
|
|
1199
|
+
|
|
1200
|
+
// Close and reopen
|
|
1201
|
+
persistDb.close();
|
|
1202
|
+
|
|
1203
|
+
const reopenedDb = new Database(join(persistTmpDir, ".fusion"));
|
|
1204
|
+
reopenedDb.init();
|
|
1205
|
+
const reopenedStore = new RoadmapStore(reopenedDb);
|
|
1206
|
+
|
|
1207
|
+
// Verify cascade delete persisted
|
|
1208
|
+
expect(reopenedStore.getMilestone(m1.id)).toBeUndefined();
|
|
1209
|
+
expect(reopenedStore.getFeature(f1.id)).toBeUndefined();
|
|
1210
|
+
|
|
1211
|
+
// Verify other data intact
|
|
1212
|
+
expect(reopenedStore.getMilestone(m2.id)).toBeDefined();
|
|
1213
|
+
expect(reopenedStore.getFeature(f2.id)).toBeDefined();
|
|
1214
|
+
|
|
1215
|
+
reopenedDb.close();
|
|
1216
|
+
} finally {
|
|
1217
|
+
await rm(persistTmpDir, { recursive: true, force: true });
|
|
1218
|
+
}
|
|
1219
|
+
});
|
|
1220
|
+
});
|
|
1221
|
+
|
|
1222
|
+
describe("negative ordering tests", () => {
|
|
1223
|
+
it("rejects reorder milestones with wrong roadmapId", () => {
|
|
1224
|
+
const roadmap1 = store.createRoadmap({ title: "R1" });
|
|
1225
|
+
const roadmap2 = store.createRoadmap({ title: "R2" });
|
|
1226
|
+
const m1 = store.createMilestone(roadmap1.id, { title: "M1" });
|
|
1227
|
+
const m2 = store.createMilestone(roadmap1.id, { title: "M2" });
|
|
1228
|
+
|
|
1229
|
+
// Try to reorder with wrong roadmapId
|
|
1230
|
+
expect(() =>
|
|
1231
|
+
store.reorderMilestones({
|
|
1232
|
+
roadmapId: roadmap2.id, // Wrong roadmap!
|
|
1233
|
+
orderedMilestoneIds: [m2.id, m1.id],
|
|
1234
|
+
}),
|
|
1235
|
+
).toThrow(); // Should fail because m1 and m2 belong to roadmap1
|
|
1236
|
+
});
|
|
1237
|
+
|
|
1238
|
+
it("rejects reorder features with wrong roadmapId", () => {
|
|
1239
|
+
const roadmap = store.createRoadmap({ title: "R1" });
|
|
1240
|
+
const m1 = store.createMilestone(roadmap.id, { title: "M1" });
|
|
1241
|
+
const f1 = store.createFeature(m1.id, { title: "F1" });
|
|
1242
|
+
const f2 = store.createFeature(m1.id, { title: "F2" });
|
|
1243
|
+
|
|
1244
|
+
// Try to reorder with wrong roadmapId
|
|
1245
|
+
expect(() =>
|
|
1246
|
+
store.reorderFeatures({
|
|
1247
|
+
roadmapId: "RM-wrong", // Wrong roadmap!
|
|
1248
|
+
milestoneId: m1.id,
|
|
1249
|
+
orderedFeatureIds: [f2.id, f1.id],
|
|
1250
|
+
}),
|
|
1251
|
+
).toThrow();
|
|
1252
|
+
});
|
|
1253
|
+
|
|
1254
|
+
it("rejects move feature with wrong fromMilestoneId", () => {
|
|
1255
|
+
const roadmap = store.createRoadmap({ title: "R1" });
|
|
1256
|
+
const m1 = store.createMilestone(roadmap.id, { title: "M1" });
|
|
1257
|
+
const m2 = store.createMilestone(roadmap.id, { title: "M2" });
|
|
1258
|
+
const f1 = store.createFeature(m1.id, { title: "F1" });
|
|
1259
|
+
|
|
1260
|
+
// Try to move with wrong fromMilestoneId
|
|
1261
|
+
expect(() =>
|
|
1262
|
+
store.moveFeature({
|
|
1263
|
+
roadmapId: roadmap.id,
|
|
1264
|
+
featureId: f1.id,
|
|
1265
|
+
fromMilestoneId: m2.id, // Wrong! f1 belongs to m1
|
|
1266
|
+
toMilestoneId: m2.id,
|
|
1267
|
+
targetOrderIndex: 0,
|
|
1268
|
+
}),
|
|
1269
|
+
).toThrow(); // The feature is not found in the affected scope
|
|
1270
|
+
});
|
|
1271
|
+
|
|
1272
|
+
it("produces contiguous orderIndex after milestone reorder", () => {
|
|
1273
|
+
const roadmap = store.createRoadmap({ title: "Contiguous Test" });
|
|
1274
|
+
const m1 = store.createMilestone(roadmap.id, { title: "M1" });
|
|
1275
|
+
const m2 = store.createMilestone(roadmap.id, { title: "M2" });
|
|
1276
|
+
const m3 = store.createMilestone(roadmap.id, { title: "M3" });
|
|
1277
|
+
|
|
1278
|
+
// Reorder to different positions
|
|
1279
|
+
store.reorderMilestones({
|
|
1280
|
+
roadmapId: roadmap.id,
|
|
1281
|
+
orderedMilestoneIds: [m2.id, m3.id, m1.id],
|
|
1282
|
+
});
|
|
1283
|
+
|
|
1284
|
+
const milestones = store.listMilestones(roadmap.id);
|
|
1285
|
+
const orderIndices = milestones.map((m) => m.orderIndex);
|
|
1286
|
+
|
|
1287
|
+
// Verify contiguous [0, 1, 2]
|
|
1288
|
+
expect(orderIndices).toEqual([0, 1, 2]);
|
|
1289
|
+
// Verify no gaps or duplicates
|
|
1290
|
+
const uniqueIndices = new Set(orderIndices);
|
|
1291
|
+
expect(uniqueIndices.size).toBe(orderIndices.length);
|
|
1292
|
+
});
|
|
1293
|
+
|
|
1294
|
+
it("produces contiguous orderIndex after feature reorder", () => {
|
|
1295
|
+
const roadmap = store.createRoadmap({ title: "Contiguous Feature" });
|
|
1296
|
+
const m1 = store.createMilestone(roadmap.id, { title: "M1" });
|
|
1297
|
+
const f1 = store.createFeature(m1.id, { title: "F1" });
|
|
1298
|
+
const f2 = store.createFeature(m1.id, { title: "F2" });
|
|
1299
|
+
const f3 = store.createFeature(m1.id, { title: "F3" });
|
|
1300
|
+
|
|
1301
|
+
// Reorder to different positions
|
|
1302
|
+
store.reorderFeatures({
|
|
1303
|
+
roadmapId: roadmap.id,
|
|
1304
|
+
milestoneId: m1.id,
|
|
1305
|
+
orderedFeatureIds: [f3.id, f1.id, f2.id],
|
|
1306
|
+
});
|
|
1307
|
+
|
|
1308
|
+
const features = store.listFeatures(m1.id);
|
|
1309
|
+
const orderIndices = features.map((f) => f.orderIndex);
|
|
1310
|
+
|
|
1311
|
+
// Verify contiguous [0, 1, 2]
|
|
1312
|
+
expect(orderIndices).toEqual([0, 1, 2]);
|
|
1313
|
+
// Verify no gaps or duplicates
|
|
1314
|
+
const uniqueIndices = new Set(orderIndices);
|
|
1315
|
+
expect(uniqueIndices.size).toBe(orderIndices.length);
|
|
1316
|
+
});
|
|
1317
|
+
});
|
|
1318
|
+
});
|