smartlisa 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/README.md +77 -0
- package/dist/cli.js +6517 -0
- package/dist/src/adapters/cli/formatter.d.ts +34 -0
- package/dist/src/adapters/cli/formatter.d.ts.map +1 -0
- package/dist/src/adapters/cli/formatter.js +254 -0
- package/dist/src/adapters/cli/formatter.js.map +1 -0
- package/dist/src/adapters/cli/index.d.ts +14 -0
- package/dist/src/adapters/cli/index.d.ts.map +1 -0
- package/dist/src/adapters/cli/index.js +781 -0
- package/dist/src/adapters/cli/index.js.map +1 -0
- package/dist/src/adapters/state/__tests__/api.test.d.ts +8 -0
- package/dist/src/adapters/state/__tests__/api.test.d.ts.map +1 -0
- package/dist/src/adapters/state/__tests__/api.test.js +500 -0
- package/dist/src/adapters/state/__tests__/api.test.js.map +1 -0
- package/dist/src/adapters/state/__tests__/filesystem.test.d.ts +7 -0
- package/dist/src/adapters/state/__tests__/filesystem.test.d.ts.map +1 -0
- package/dist/src/adapters/state/__tests__/filesystem.test.js +418 -0
- package/dist/src/adapters/state/__tests__/filesystem.test.js.map +1 -0
- package/dist/src/adapters/state/api.d.ts +148 -0
- package/dist/src/adapters/state/api.d.ts.map +1 -0
- package/dist/src/adapters/state/api.js +337 -0
- package/dist/src/adapters/state/api.js.map +1 -0
- package/dist/src/adapters/state/filesystem.d.ts +80 -0
- package/dist/src/adapters/state/filesystem.d.ts.map +1 -0
- package/dist/src/adapters/state/filesystem.js +228 -0
- package/dist/src/adapters/state/filesystem.js.map +1 -0
- package/dist/src/adapters/state/index.d.ts +9 -0
- package/dist/src/adapters/state/index.d.ts.map +1 -0
- package/dist/src/adapters/state/index.js +10 -0
- package/dist/src/adapters/state/index.js.map +1 -0
- package/dist/src/adapters/state/types.d.ts +131 -0
- package/dist/src/adapters/state/types.d.ts.map +1 -0
- package/dist/src/adapters/state/types.js +11 -0
- package/dist/src/adapters/state/types.js.map +1 -0
- package/dist/src/core/__tests__/context.test.d.ts +12 -0
- package/dist/src/core/__tests__/context.test.d.ts.map +1 -0
- package/dist/src/core/__tests__/context.test.js +528 -0
- package/dist/src/core/__tests__/context.test.js.map +1 -0
- package/dist/src/core/__tests__/discover.test.d.ts +2 -0
- package/dist/src/core/__tests__/discover.test.d.ts.map +1 -0
- package/dist/src/core/__tests__/discover.test.js +667 -0
- package/dist/src/core/__tests__/discover.test.js.map +1 -0
- package/dist/src/core/__tests__/engine.test.d.ts +2 -0
- package/dist/src/core/__tests__/engine.test.d.ts.map +1 -0
- package/dist/src/core/__tests__/engine.test.js +444 -0
- package/dist/src/core/__tests__/engine.test.js.map +1 -0
- package/dist/src/core/__tests__/feedback.test.d.ts +2 -0
- package/dist/src/core/__tests__/feedback.test.d.ts.map +1 -0
- package/dist/src/core/__tests__/feedback.test.js +351 -0
- package/dist/src/core/__tests__/feedback.test.js.map +1 -0
- package/dist/src/core/__tests__/plan.test.d.ts +2 -0
- package/dist/src/core/__tests__/plan.test.d.ts.map +1 -0
- package/dist/src/core/__tests__/plan.test.js +429 -0
- package/dist/src/core/__tests__/plan.test.js.map +1 -0
- package/dist/src/core/__tests__/schemas.test.d.ts +2 -0
- package/dist/src/core/__tests__/schemas.test.d.ts.map +1 -0
- package/dist/src/core/__tests__/schemas.test.js +614 -0
- package/dist/src/core/__tests__/schemas.test.js.map +1 -0
- package/dist/src/core/__tests__/state.test.d.ts +2 -0
- package/dist/src/core/__tests__/state.test.d.ts.map +1 -0
- package/dist/src/core/__tests__/state.test.js +1107 -0
- package/dist/src/core/__tests__/state.test.js.map +1 -0
- package/dist/src/core/__tests__/status.test.d.ts +2 -0
- package/dist/src/core/__tests__/status.test.d.ts.map +1 -0
- package/dist/src/core/__tests__/status.test.js +600 -0
- package/dist/src/core/__tests__/status.test.js.map +1 -0
- package/dist/src/core/__tests__/test-helpers.d.ts +28 -0
- package/dist/src/core/__tests__/test-helpers.d.ts.map +1 -0
- package/dist/src/core/__tests__/test-helpers.js +185 -0
- package/dist/src/core/__tests__/test-helpers.js.map +1 -0
- package/dist/src/core/__tests__/utils.test.d.ts +2 -0
- package/dist/src/core/__tests__/utils.test.d.ts.map +1 -0
- package/dist/src/core/__tests__/utils.test.js +276 -0
- package/dist/src/core/__tests__/utils.test.js.map +1 -0
- package/dist/src/core/__tests__/validate.test.d.ts +2 -0
- package/dist/src/core/__tests__/validate.test.d.ts.map +1 -0
- package/dist/src/core/__tests__/validate.test.js +354 -0
- package/dist/src/core/__tests__/validate.test.js.map +1 -0
- package/dist/src/core/commands/discover.d.ts +71 -0
- package/dist/src/core/commands/discover.d.ts.map +1 -0
- package/dist/src/core/commands/discover.js +687 -0
- package/dist/src/core/commands/discover.js.map +1 -0
- package/dist/src/core/commands/feedback.d.ts +49 -0
- package/dist/src/core/commands/feedback.d.ts.map +1 -0
- package/dist/src/core/commands/feedback.js +283 -0
- package/dist/src/core/commands/feedback.js.map +1 -0
- package/dist/src/core/commands/index.d.ts +11 -0
- package/dist/src/core/commands/index.d.ts.map +1 -0
- package/dist/src/core/commands/index.js +11 -0
- package/dist/src/core/commands/index.js.map +1 -0
- package/dist/src/core/commands/plan.d.ts +108 -0
- package/dist/src/core/commands/plan.d.ts.map +1 -0
- package/dist/src/core/commands/plan.js +621 -0
- package/dist/src/core/commands/plan.js.map +1 -0
- package/dist/src/core/commands/status.d.ts +72 -0
- package/dist/src/core/commands/status.d.ts.map +1 -0
- package/dist/src/core/commands/status.js +720 -0
- package/dist/src/core/commands/status.js.map +1 -0
- package/dist/src/core/commands/validate.d.ts +47 -0
- package/dist/src/core/commands/validate.d.ts.map +1 -0
- package/dist/src/core/commands/validate.js +608 -0
- package/dist/src/core/commands/validate.js.map +1 -0
- package/dist/src/core/engine.d.ts +294 -0
- package/dist/src/core/engine.d.ts.map +1 -0
- package/dist/src/core/engine.js +219 -0
- package/dist/src/core/engine.js.map +1 -0
- package/dist/src/core/index.d.ts +14 -0
- package/dist/src/core/index.d.ts.map +1 -0
- package/dist/src/core/index.js +18 -0
- package/dist/src/core/index.js.map +1 -0
- package/dist/src/core/prompts/context-helpers.d.ts +48 -0
- package/dist/src/core/prompts/context-helpers.d.ts.map +1 -0
- package/dist/src/core/prompts/context-helpers.js +206 -0
- package/dist/src/core/prompts/context-helpers.js.map +1 -0
- package/dist/src/core/prompts/discovery.d.ts +11 -0
- package/dist/src/core/prompts/discovery.d.ts.map +1 -0
- package/dist/src/core/prompts/discovery.js +179 -0
- package/dist/src/core/prompts/discovery.js.map +1 -0
- package/dist/src/core/prompts/feedback.d.ts +38 -0
- package/dist/src/core/prompts/feedback.d.ts.map +1 -0
- package/dist/src/core/prompts/feedback.js +292 -0
- package/dist/src/core/prompts/feedback.js.map +1 -0
- package/dist/src/core/prompts/index.d.ts +12 -0
- package/dist/src/core/prompts/index.d.ts.map +1 -0
- package/dist/src/core/prompts/index.js +12 -0
- package/dist/src/core/prompts/index.js.map +1 -0
- package/dist/src/core/prompts/planning.d.ts +15 -0
- package/dist/src/core/prompts/planning.d.ts.map +1 -0
- package/dist/src/core/prompts/planning.js +293 -0
- package/dist/src/core/prompts/planning.js.map +1 -0
- package/dist/src/core/prompts/status.d.ts +41 -0
- package/dist/src/core/prompts/status.d.ts.map +1 -0
- package/dist/src/core/prompts/status.js +270 -0
- package/dist/src/core/prompts/status.js.map +1 -0
- package/dist/src/core/prompts/validate.d.ts +62 -0
- package/dist/src/core/prompts/validate.d.ts.map +1 -0
- package/dist/src/core/prompts/validate.js +302 -0
- package/dist/src/core/prompts/validate.js.map +1 -0
- package/dist/src/core/schemas.d.ts +5045 -0
- package/dist/src/core/schemas.d.ts.map +1 -0
- package/dist/src/core/schemas.js +492 -0
- package/dist/src/core/schemas.js.map +1 -0
- package/dist/src/core/state.d.ts +156 -0
- package/dist/src/core/state.d.ts.map +1 -0
- package/dist/src/core/state.js +608 -0
- package/dist/src/core/state.js.map +1 -0
- package/dist/src/core/types.d.ts +167 -0
- package/dist/src/core/types.d.ts.map +1 -0
- package/dist/src/core/types.js +102 -0
- package/dist/src/core/types.js.map +1 -0
- package/dist/src/core/utils.d.ts +39 -0
- package/dist/src/core/utils.d.ts.map +1 -0
- package/dist/src/core/utils.js +208 -0
- package/dist/src/core/utils.js.map +1 -0
- package/package.json +77 -0
|
@@ -0,0 +1,1107 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import * as fs from "fs/promises";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import * as os from "os";
|
|
5
|
+
import { createStateManager, RALPH_DIR, PATHS } from "../state.js";
|
|
6
|
+
import { FileSystemStateAdapter } from "../../adapters/state/index.js";
|
|
7
|
+
describe("StateManager", () => {
|
|
8
|
+
let testDir;
|
|
9
|
+
let state;
|
|
10
|
+
beforeEach(async () => {
|
|
11
|
+
// Create a temporary directory for each test
|
|
12
|
+
testDir = await fs.mkdtemp(path.join(os.tmpdir(), "lisa-test-"));
|
|
13
|
+
const adapter = new FileSystemStateAdapter({ root: testDir });
|
|
14
|
+
state = createStateManager(adapter);
|
|
15
|
+
});
|
|
16
|
+
afterEach(async () => {
|
|
17
|
+
// Clean up test directory
|
|
18
|
+
await fs.rm(testDir, { recursive: true, force: true });
|
|
19
|
+
});
|
|
20
|
+
describe("isInitialized", () => {
|
|
21
|
+
it("should return false for uninitialized project", async () => {
|
|
22
|
+
const result = await state.isInitialized();
|
|
23
|
+
expect(result).toBe(false);
|
|
24
|
+
});
|
|
25
|
+
it("should return true after initialization", async () => {
|
|
26
|
+
await state.initialize("Test Project");
|
|
27
|
+
const result = await state.isInitialized();
|
|
28
|
+
expect(result).toBe(true);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
describe("initialize", () => {
|
|
32
|
+
it("should create .lisa directory", async () => {
|
|
33
|
+
await state.initialize("Test Project");
|
|
34
|
+
const lisaDir = path.join(testDir, RALPH_DIR);
|
|
35
|
+
const stat = await fs.stat(lisaDir);
|
|
36
|
+
expect(stat.isDirectory()).toBe(true);
|
|
37
|
+
});
|
|
38
|
+
it("should create subdirectories", async () => {
|
|
39
|
+
await state.initialize("Test Project");
|
|
40
|
+
const discoveryDir = path.join(testDir, RALPH_DIR, "discovery");
|
|
41
|
+
const milestonesDir = path.join(testDir, RALPH_DIR, "milestones");
|
|
42
|
+
const epicsDir = path.join(testDir, RALPH_DIR, "epics");
|
|
43
|
+
const validationDir = path.join(testDir, RALPH_DIR, "validation");
|
|
44
|
+
expect((await fs.stat(discoveryDir)).isDirectory()).toBe(true);
|
|
45
|
+
expect((await fs.stat(milestonesDir)).isDirectory()).toBe(true);
|
|
46
|
+
expect((await fs.stat(epicsDir)).isDirectory()).toBe(true);
|
|
47
|
+
expect((await fs.stat(validationDir)).isDirectory()).toBe(true);
|
|
48
|
+
});
|
|
49
|
+
it("should create project.json with correct data", async () => {
|
|
50
|
+
const project = await state.initialize("Test Project");
|
|
51
|
+
expect(project.name).toBe("Test Project");
|
|
52
|
+
expect(project.status).toBe("active");
|
|
53
|
+
expect(project.stats.milestones).toBe(0);
|
|
54
|
+
});
|
|
55
|
+
it("should use default name if not provided", async () => {
|
|
56
|
+
const project = await state.initialize();
|
|
57
|
+
expect(project.name).toBe("Untitled Project");
|
|
58
|
+
});
|
|
59
|
+
it("should create empty queue files", async () => {
|
|
60
|
+
await state.initialize("Test");
|
|
61
|
+
const taskQueue = await state.readTaskQueue();
|
|
62
|
+
const stuckQueue = await state.readStuckQueue();
|
|
63
|
+
const feedbackQueue = await state.readFeedbackQueue();
|
|
64
|
+
expect(taskQueue?.tasks).toEqual([]);
|
|
65
|
+
expect(stuckQueue?.stuck).toEqual([]);
|
|
66
|
+
expect(feedbackQueue?.feedback).toEqual([]);
|
|
67
|
+
});
|
|
68
|
+
it("should create empty discovery files", async () => {
|
|
69
|
+
await state.initialize("Test");
|
|
70
|
+
const context = await state.readDiscoveryContext();
|
|
71
|
+
const constraints = await state.readConstraints();
|
|
72
|
+
const history = await state.readDiscoveryHistory();
|
|
73
|
+
expect(context?.values).toEqual([]);
|
|
74
|
+
expect(constraints?.constraints).toEqual([]);
|
|
75
|
+
expect(history?.entries).toEqual([]);
|
|
76
|
+
expect(history?.is_complete).toBe(false);
|
|
77
|
+
});
|
|
78
|
+
it("should create default config", async () => {
|
|
79
|
+
await state.initialize("Test");
|
|
80
|
+
const config = await state.readConfig();
|
|
81
|
+
expect(config?.checkpoints).toContain("after_epic_breakdown");
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
describe("Project CRUD", () => {
|
|
85
|
+
beforeEach(async () => {
|
|
86
|
+
await state.initialize("Test Project");
|
|
87
|
+
});
|
|
88
|
+
it("should read project", async () => {
|
|
89
|
+
const project = await state.readProject();
|
|
90
|
+
expect(project?.name).toBe("Test Project");
|
|
91
|
+
});
|
|
92
|
+
it("should write project", async () => {
|
|
93
|
+
const project = await state.readProject();
|
|
94
|
+
project.name = "Updated Name";
|
|
95
|
+
await state.writeProject(project);
|
|
96
|
+
const updated = await state.readProject();
|
|
97
|
+
expect(updated?.name).toBe("Updated Name");
|
|
98
|
+
});
|
|
99
|
+
it("should update project fields", async () => {
|
|
100
|
+
const updated = await state.updateProject({ status: "paused" });
|
|
101
|
+
expect(updated.status).toBe("paused");
|
|
102
|
+
const read = await state.readProject();
|
|
103
|
+
expect(read?.status).toBe("paused");
|
|
104
|
+
});
|
|
105
|
+
it("should update timestamp on write", async () => {
|
|
106
|
+
const original = await state.readProject();
|
|
107
|
+
const originalUpdated = original?.updated;
|
|
108
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
109
|
+
await state.updateProject({ status: "paused" });
|
|
110
|
+
const updated = await state.readProject();
|
|
111
|
+
expect(updated?.updated).not.toBe(originalUpdated);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
describe("Discovery CRUD", () => {
|
|
115
|
+
beforeEach(async () => {
|
|
116
|
+
await state.initialize("Test Project");
|
|
117
|
+
});
|
|
118
|
+
it("should read and write discovery context", async () => {
|
|
119
|
+
const context = await state.readDiscoveryContext();
|
|
120
|
+
context.problem = "Users struggle with planning";
|
|
121
|
+
context.vision = "Seamless planning experience";
|
|
122
|
+
await state.writeDiscoveryContext(context);
|
|
123
|
+
const updated = await state.readDiscoveryContext();
|
|
124
|
+
expect(updated?.problem).toBe("Users struggle with planning");
|
|
125
|
+
expect(updated?.vision).toBe("Seamless planning experience");
|
|
126
|
+
});
|
|
127
|
+
it("should read and write constraints", async () => {
|
|
128
|
+
const constraints = await state.readConstraints();
|
|
129
|
+
constraints.constraints.push({
|
|
130
|
+
id: "C1",
|
|
131
|
+
type: "technical",
|
|
132
|
+
constraint: "Must use PostgreSQL",
|
|
133
|
+
impact: [],
|
|
134
|
+
});
|
|
135
|
+
await state.writeConstraints(constraints);
|
|
136
|
+
const updated = await state.readConstraints();
|
|
137
|
+
expect(updated?.constraints).toHaveLength(1);
|
|
138
|
+
expect(updated?.constraints[0].type).toBe("technical");
|
|
139
|
+
});
|
|
140
|
+
it("should read and write discovery history", async () => {
|
|
141
|
+
const history = await state.readDiscoveryHistory();
|
|
142
|
+
history.entries.push({
|
|
143
|
+
timestamp: new Date().toISOString(),
|
|
144
|
+
question: "What problem?",
|
|
145
|
+
answer: "Planning is hard",
|
|
146
|
+
category: "problem",
|
|
147
|
+
});
|
|
148
|
+
history.is_complete = true;
|
|
149
|
+
await state.writeDiscoveryHistory(history);
|
|
150
|
+
const updated = await state.readDiscoveryHistory();
|
|
151
|
+
expect(updated?.entries).toHaveLength(1);
|
|
152
|
+
expect(updated?.is_complete).toBe(true);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
describe("Milestones CRUD", () => {
|
|
156
|
+
beforeEach(async () => {
|
|
157
|
+
await state.initialize("Test Project");
|
|
158
|
+
});
|
|
159
|
+
it("should read and write milestone index", async () => {
|
|
160
|
+
const index = await state.readMilestoneIndex();
|
|
161
|
+
index.milestones.push({
|
|
162
|
+
id: "M1",
|
|
163
|
+
slug: "foundation",
|
|
164
|
+
name: "Foundation",
|
|
165
|
+
description: "Core infrastructure",
|
|
166
|
+
order: 1,
|
|
167
|
+
epics: [],
|
|
168
|
+
created: new Date().toISOString(),
|
|
169
|
+
updated: new Date().toISOString(),
|
|
170
|
+
});
|
|
171
|
+
await state.writeMilestoneIndex(index);
|
|
172
|
+
const updated = await state.readMilestoneIndex();
|
|
173
|
+
expect(updated?.milestones).toHaveLength(1);
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
describe("Epics CRUD", () => {
|
|
177
|
+
beforeEach(async () => {
|
|
178
|
+
await state.initialize("Test Project");
|
|
179
|
+
});
|
|
180
|
+
it("should create epic directory", async () => {
|
|
181
|
+
await state.createEpicDir("E1", "auth");
|
|
182
|
+
const dirs = await state.listEpicDirs();
|
|
183
|
+
expect(dirs).toContain("E1-auth");
|
|
184
|
+
});
|
|
185
|
+
it("should read and write epic", async () => {
|
|
186
|
+
await state.createEpicDir("E1", "auth");
|
|
187
|
+
const epic = {
|
|
188
|
+
id: "E1",
|
|
189
|
+
slug: "auth",
|
|
190
|
+
name: "Authentication",
|
|
191
|
+
description: "User authentication",
|
|
192
|
+
milestone: "M1",
|
|
193
|
+
deferred: false,
|
|
194
|
+
created: new Date().toISOString(),
|
|
195
|
+
updated: new Date().toISOString(),
|
|
196
|
+
artifacts: {
|
|
197
|
+
prd: { status: "pending", version: 1 },
|
|
198
|
+
architecture: { status: "pending", version: 1 },
|
|
199
|
+
stories: { status: "pending", count: 0 },
|
|
200
|
+
},
|
|
201
|
+
dependencies: [],
|
|
202
|
+
stats: { requirements: 0, stories: 0, coverage: 0 },
|
|
203
|
+
};
|
|
204
|
+
await state.writeEpic(epic);
|
|
205
|
+
const read = await state.readEpic("E1", "auth");
|
|
206
|
+
expect(read?.name).toBe("Authentication");
|
|
207
|
+
});
|
|
208
|
+
it("should read and write PRD", async () => {
|
|
209
|
+
await state.createEpicDir("E1", "auth");
|
|
210
|
+
const prdContent = "# E1: Authentication\n\n## Requirements\n\n### R1: Login";
|
|
211
|
+
await state.writePrd("E1", "auth", prdContent);
|
|
212
|
+
const read = await state.readPrd("E1", "auth");
|
|
213
|
+
expect(read).toContain("# E1: Authentication");
|
|
214
|
+
});
|
|
215
|
+
it("should read and write architecture", async () => {
|
|
216
|
+
await state.createEpicDir("E1", "auth");
|
|
217
|
+
const archContent = "# Architecture\n\n## Data Model";
|
|
218
|
+
await state.writeArchitecture("E1", "auth", archContent);
|
|
219
|
+
const read = await state.readArchitecture("E1", "auth");
|
|
220
|
+
expect(read).toContain("# Architecture");
|
|
221
|
+
});
|
|
222
|
+
it("should read and write stories", async () => {
|
|
223
|
+
await state.createEpicDir("E1", "auth");
|
|
224
|
+
const stories = {
|
|
225
|
+
epic_id: "E1",
|
|
226
|
+
stories: [
|
|
227
|
+
{
|
|
228
|
+
id: "E1.S1",
|
|
229
|
+
title: "Login API",
|
|
230
|
+
description: "Create login endpoint",
|
|
231
|
+
type: "feature",
|
|
232
|
+
requirements: ["E1.R1"],
|
|
233
|
+
acceptance_criteria: ["Can login"],
|
|
234
|
+
dependencies: [],
|
|
235
|
+
status: "todo",
|
|
236
|
+
assignee: null,
|
|
237
|
+
},
|
|
238
|
+
],
|
|
239
|
+
coverage: { "E1.R1": ["E1.S1"] },
|
|
240
|
+
validation: {
|
|
241
|
+
coverage_complete: false,
|
|
242
|
+
all_links_valid: false,
|
|
243
|
+
},
|
|
244
|
+
};
|
|
245
|
+
await state.writeStories("E1", "auth", stories);
|
|
246
|
+
const read = await state.readStories("E1", "auth");
|
|
247
|
+
expect(read?.stories).toHaveLength(1);
|
|
248
|
+
expect(read?.stories[0].title).toBe("Login API");
|
|
249
|
+
});
|
|
250
|
+
it("should list epic directories", async () => {
|
|
251
|
+
await state.createEpicDir("E1", "auth");
|
|
252
|
+
await state.createEpicDir("E2", "habits");
|
|
253
|
+
const dirs = await state.listEpicDirs();
|
|
254
|
+
expect(dirs).toContain("E1-auth");
|
|
255
|
+
expect(dirs).toContain("E2-habits");
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
describe("Queues CRUD", () => {
|
|
259
|
+
beforeEach(async () => {
|
|
260
|
+
await state.initialize("Test Project");
|
|
261
|
+
});
|
|
262
|
+
it("should read and write task queue", async () => {
|
|
263
|
+
const queue = await state.readTaskQueue();
|
|
264
|
+
queue.tasks.push({
|
|
265
|
+
id: "task-001",
|
|
266
|
+
type: "generate_prd",
|
|
267
|
+
target: { type: "epic", id: "E1" },
|
|
268
|
+
priority: 1,
|
|
269
|
+
status: "pending",
|
|
270
|
+
depends_on: [],
|
|
271
|
+
created: new Date().toISOString(),
|
|
272
|
+
created_by: "system",
|
|
273
|
+
attempts: 0,
|
|
274
|
+
});
|
|
275
|
+
await state.writeTaskQueue(queue);
|
|
276
|
+
const updated = await state.readTaskQueue();
|
|
277
|
+
expect(updated?.tasks).toHaveLength(1);
|
|
278
|
+
});
|
|
279
|
+
it("should read and write stuck queue", async () => {
|
|
280
|
+
const queue = await state.readStuckQueue();
|
|
281
|
+
queue.stuck.push({
|
|
282
|
+
id: "stuck-001",
|
|
283
|
+
task_id: "task-001",
|
|
284
|
+
type: "ambiguous_requirement",
|
|
285
|
+
summary: "Cannot determine target",
|
|
286
|
+
attempts: [],
|
|
287
|
+
created: new Date().toISOString(),
|
|
288
|
+
priority: "high",
|
|
289
|
+
});
|
|
290
|
+
await state.writeStuckQueue(queue);
|
|
291
|
+
const updated = await state.readStuckQueue();
|
|
292
|
+
expect(updated?.stuck).toHaveLength(1);
|
|
293
|
+
});
|
|
294
|
+
it("should read and write feedback queue", async () => {
|
|
295
|
+
const queue = await state.readFeedbackQueue();
|
|
296
|
+
queue.feedback.push({
|
|
297
|
+
id: "fb-001",
|
|
298
|
+
type: "blocker",
|
|
299
|
+
source: { type: "execution", story_id: "E1.S1" },
|
|
300
|
+
summary: "API not available",
|
|
301
|
+
affects: [],
|
|
302
|
+
status: "pending",
|
|
303
|
+
created: new Date().toISOString(),
|
|
304
|
+
});
|
|
305
|
+
await state.writeFeedbackQueue(queue);
|
|
306
|
+
const updated = await state.readFeedbackQueue();
|
|
307
|
+
expect(updated?.feedback).toHaveLength(1);
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
describe("Validation CRUD", () => {
|
|
311
|
+
beforeEach(async () => {
|
|
312
|
+
await state.initialize("Test Project");
|
|
313
|
+
});
|
|
314
|
+
it("should read and write coverage", async () => {
|
|
315
|
+
const coverage = {
|
|
316
|
+
coverage: {
|
|
317
|
+
E1: {
|
|
318
|
+
"E1.R1": { stories: ["E1.S1"], status: "covered" },
|
|
319
|
+
},
|
|
320
|
+
},
|
|
321
|
+
summary: { total_requirements: 1, covered: 1, gaps: 0, coverage_percent: 100 },
|
|
322
|
+
gaps: [],
|
|
323
|
+
};
|
|
324
|
+
await state.writeCoverage(coverage);
|
|
325
|
+
const read = await state.readCoverage();
|
|
326
|
+
expect(read?.summary.coverage_percent).toBe(100);
|
|
327
|
+
});
|
|
328
|
+
it("should read and write links", async () => {
|
|
329
|
+
const links = {
|
|
330
|
+
links: [
|
|
331
|
+
{
|
|
332
|
+
from: { type: "story", id: "E1.S1" },
|
|
333
|
+
to: { type: "requirement", id: "E1.R1" },
|
|
334
|
+
type: "implements",
|
|
335
|
+
valid: true,
|
|
336
|
+
},
|
|
337
|
+
],
|
|
338
|
+
broken: [],
|
|
339
|
+
orphans: [],
|
|
340
|
+
summary: { total_links: 1, valid: 1, broken: 0, orphans: 0 },
|
|
341
|
+
};
|
|
342
|
+
await state.writeLinks(links);
|
|
343
|
+
const read = await state.readLinks();
|
|
344
|
+
expect(read?.links).toHaveLength(1);
|
|
345
|
+
});
|
|
346
|
+
it("should read and write validation issues", async () => {
|
|
347
|
+
const issues = {
|
|
348
|
+
issues: [
|
|
349
|
+
{
|
|
350
|
+
id: "issue-001",
|
|
351
|
+
severity: "error",
|
|
352
|
+
type: "broken_link",
|
|
353
|
+
location: { type: "story", id: "E1.S1" },
|
|
354
|
+
message: "Broken link",
|
|
355
|
+
},
|
|
356
|
+
],
|
|
357
|
+
summary: { errors: 1, warnings: 0, info: 0 },
|
|
358
|
+
};
|
|
359
|
+
await state.writeValidationIssues(issues);
|
|
360
|
+
const read = await state.readValidationIssues();
|
|
361
|
+
expect(read?.issues).toHaveLength(1);
|
|
362
|
+
});
|
|
363
|
+
});
|
|
364
|
+
describe("Lock Management", () => {
|
|
365
|
+
beforeEach(async () => {
|
|
366
|
+
await state.initialize("Test Project");
|
|
367
|
+
});
|
|
368
|
+
it("should acquire lock successfully", async () => {
|
|
369
|
+
const acquired = await state.acquireLock("worker", "task-001");
|
|
370
|
+
expect(acquired).toBe(true);
|
|
371
|
+
});
|
|
372
|
+
it("should read lock", async () => {
|
|
373
|
+
await state.acquireLock("worker", "task-001");
|
|
374
|
+
const lock = await state.readLock();
|
|
375
|
+
expect(lock?.holder).toBe("worker");
|
|
376
|
+
expect(lock?.task).toBe("task-001");
|
|
377
|
+
});
|
|
378
|
+
it("should prevent acquiring lock when already held", async () => {
|
|
379
|
+
await state.acquireLock("worker", "task-001");
|
|
380
|
+
const secondAttempt = await state.acquireLock("user", "task-002");
|
|
381
|
+
expect(secondAttempt).toBe(false);
|
|
382
|
+
});
|
|
383
|
+
it("should release lock", async () => {
|
|
384
|
+
await state.acquireLock("worker", "task-001");
|
|
385
|
+
await state.releaseLock();
|
|
386
|
+
const lock = await state.readLock();
|
|
387
|
+
expect(lock).toBeNull();
|
|
388
|
+
});
|
|
389
|
+
it("should allow acquiring after release", async () => {
|
|
390
|
+
await state.acquireLock("worker", "task-001");
|
|
391
|
+
await state.releaseLock();
|
|
392
|
+
const acquired = await state.acquireLock("user", "task-002");
|
|
393
|
+
expect(acquired).toBe(true);
|
|
394
|
+
});
|
|
395
|
+
});
|
|
396
|
+
describe("Path Helpers", () => {
|
|
397
|
+
it("should return correct path", () => {
|
|
398
|
+
const p = state.getPath("test/file.json");
|
|
399
|
+
expect(p).toBe(path.join(testDir, RALPH_DIR, "test/file.json"));
|
|
400
|
+
});
|
|
401
|
+
it("should return correct epic directory", () => {
|
|
402
|
+
const dir = state.getEpicDir("E1", "auth");
|
|
403
|
+
expect(dir).toBe(path.join(testDir, RALPH_DIR, "epics", "E1-auth"));
|
|
404
|
+
});
|
|
405
|
+
it("should return correct epic file path", () => {
|
|
406
|
+
const p = state.getEpicPath("E1", "auth", "prd.md");
|
|
407
|
+
expect(p).toBe(path.join(testDir, RALPH_DIR, "epics", "E1-auth", "prd.md"));
|
|
408
|
+
});
|
|
409
|
+
});
|
|
410
|
+
describe("Utilities", () => {
|
|
411
|
+
beforeEach(async () => {
|
|
412
|
+
await state.initialize("Test Project");
|
|
413
|
+
});
|
|
414
|
+
it("should check file existence", async () => {
|
|
415
|
+
const exists = await state.exists(PATHS.project);
|
|
416
|
+
expect(exists).toBe(true);
|
|
417
|
+
});
|
|
418
|
+
it("should return false for non-existent file", async () => {
|
|
419
|
+
const exists = await state.exists("nonexistent.json");
|
|
420
|
+
expect(exists).toBe(false);
|
|
421
|
+
});
|
|
422
|
+
it("should list directory contents", async () => {
|
|
423
|
+
const contents = await state.listDirectory(PATHS.discovery.dir);
|
|
424
|
+
expect(contents).toContain("context.json");
|
|
425
|
+
expect(contents).toContain("constraints.json");
|
|
426
|
+
expect(contents).toContain("history.json");
|
|
427
|
+
});
|
|
428
|
+
it("should return empty array for non-existent directory", async () => {
|
|
429
|
+
const contents = await state.listDirectory("nonexistent");
|
|
430
|
+
expect(contents).toEqual([]);
|
|
431
|
+
});
|
|
432
|
+
});
|
|
433
|
+
describe("Error Handling", () => {
|
|
434
|
+
it("should return null for non-existent files", async () => {
|
|
435
|
+
const project = await state.readProject();
|
|
436
|
+
expect(project).toBeNull();
|
|
437
|
+
});
|
|
438
|
+
it("should throw error on updateProject when not initialized", async () => {
|
|
439
|
+
await expect(state.updateProject({ status: "active" })).rejects.toThrow();
|
|
440
|
+
});
|
|
441
|
+
});
|
|
442
|
+
// ==========================================================================
|
|
443
|
+
// Element Discovery (Nested Discovery for Epic/Milestone)
|
|
444
|
+
// ==========================================================================
|
|
445
|
+
describe("Epic Discovery CRUD", () => {
|
|
446
|
+
beforeEach(async () => {
|
|
447
|
+
await state.initialize("Test Project");
|
|
448
|
+
await state.createEpicDir("E1", "auth");
|
|
449
|
+
});
|
|
450
|
+
it("should return null for epic without discovery", async () => {
|
|
451
|
+
const discovery = await state.readEpicDiscovery("E1", "auth");
|
|
452
|
+
expect(discovery).toBeNull();
|
|
453
|
+
});
|
|
454
|
+
it("should write and read epic discovery", async () => {
|
|
455
|
+
const discovery = {
|
|
456
|
+
element_type: "epic",
|
|
457
|
+
element_id: "E1",
|
|
458
|
+
problem: "Users need authentication",
|
|
459
|
+
scope: ["Login", "Logout"],
|
|
460
|
+
out_of_scope: ["OAuth"],
|
|
461
|
+
success_criteria: ["Users can log in"],
|
|
462
|
+
constraints: [],
|
|
463
|
+
history: [],
|
|
464
|
+
status: "in_progress",
|
|
465
|
+
source: "user_added",
|
|
466
|
+
created: new Date().toISOString(),
|
|
467
|
+
updated: new Date().toISOString(),
|
|
468
|
+
};
|
|
469
|
+
await state.writeEpicDiscovery("E1", "auth", discovery);
|
|
470
|
+
const read = await state.readEpicDiscovery("E1", "auth");
|
|
471
|
+
expect(read).not.toBeNull();
|
|
472
|
+
expect(read?.element_id).toBe("E1");
|
|
473
|
+
expect(read?.problem).toBe("Users need authentication");
|
|
474
|
+
expect(read?.scope).toEqual(["Login", "Logout"]);
|
|
475
|
+
expect(read?.status).toBe("in_progress");
|
|
476
|
+
});
|
|
477
|
+
it("should update discovery timestamps on write", async () => {
|
|
478
|
+
const discovery = {
|
|
479
|
+
element_type: "epic",
|
|
480
|
+
element_id: "E1",
|
|
481
|
+
scope: [],
|
|
482
|
+
out_of_scope: [],
|
|
483
|
+
success_criteria: [],
|
|
484
|
+
constraints: [],
|
|
485
|
+
history: [],
|
|
486
|
+
status: "not_started",
|
|
487
|
+
source: "user_added",
|
|
488
|
+
created: "2024-01-01T00:00:00.000Z",
|
|
489
|
+
updated: "2024-01-01T00:00:00.000Z",
|
|
490
|
+
};
|
|
491
|
+
await state.writeEpicDiscovery("E1", "auth", discovery);
|
|
492
|
+
const read = await state.readEpicDiscovery("E1", "auth");
|
|
493
|
+
expect(read?.updated).not.toBe("2024-01-01T00:00:00.000Z");
|
|
494
|
+
});
|
|
495
|
+
it("should store discovery in epic directory", async () => {
|
|
496
|
+
const discovery = {
|
|
497
|
+
element_type: "epic",
|
|
498
|
+
element_id: "E1",
|
|
499
|
+
scope: [],
|
|
500
|
+
out_of_scope: [],
|
|
501
|
+
success_criteria: [],
|
|
502
|
+
constraints: [],
|
|
503
|
+
history: [],
|
|
504
|
+
status: "not_started",
|
|
505
|
+
source: "user_added",
|
|
506
|
+
created: new Date().toISOString(),
|
|
507
|
+
updated: new Date().toISOString(),
|
|
508
|
+
};
|
|
509
|
+
await state.writeEpicDiscovery("E1", "auth", discovery);
|
|
510
|
+
const exists = await state.exists("epics/E1-auth/discovery.json");
|
|
511
|
+
expect(exists).toBe(true);
|
|
512
|
+
});
|
|
513
|
+
});
|
|
514
|
+
describe("Milestone Discovery CRUD", () => {
|
|
515
|
+
beforeEach(async () => {
|
|
516
|
+
await state.initialize("Test Project");
|
|
517
|
+
});
|
|
518
|
+
it("should return null for milestone without discovery", async () => {
|
|
519
|
+
const discovery = await state.readMilestoneDiscovery("M1");
|
|
520
|
+
expect(discovery).toBeNull();
|
|
521
|
+
});
|
|
522
|
+
it("should write and read milestone discovery", async () => {
|
|
523
|
+
const discovery = {
|
|
524
|
+
element_type: "milestone",
|
|
525
|
+
element_id: "M1",
|
|
526
|
+
problem: "Need core infrastructure",
|
|
527
|
+
scope: [],
|
|
528
|
+
out_of_scope: [],
|
|
529
|
+
success_criteria: ["Foundation complete"],
|
|
530
|
+
constraints: [],
|
|
531
|
+
history: [],
|
|
532
|
+
status: "complete",
|
|
533
|
+
source: "user_added",
|
|
534
|
+
created: new Date().toISOString(),
|
|
535
|
+
updated: new Date().toISOString(),
|
|
536
|
+
};
|
|
537
|
+
await state.writeMilestoneDiscovery("M1", discovery);
|
|
538
|
+
const read = await state.readMilestoneDiscovery("M1");
|
|
539
|
+
expect(read).not.toBeNull();
|
|
540
|
+
expect(read?.element_id).toBe("M1");
|
|
541
|
+
expect(read?.success_criteria).toEqual(["Foundation complete"]);
|
|
542
|
+
expect(read?.status).toBe("complete");
|
|
543
|
+
});
|
|
544
|
+
it("should create milestone directory if not exists", async () => {
|
|
545
|
+
const discovery = {
|
|
546
|
+
element_type: "milestone",
|
|
547
|
+
element_id: "M1",
|
|
548
|
+
scope: [],
|
|
549
|
+
out_of_scope: [],
|
|
550
|
+
success_criteria: [],
|
|
551
|
+
constraints: [],
|
|
552
|
+
history: [],
|
|
553
|
+
status: "not_started",
|
|
554
|
+
source: "user_added",
|
|
555
|
+
created: new Date().toISOString(),
|
|
556
|
+
updated: new Date().toISOString(),
|
|
557
|
+
};
|
|
558
|
+
await state.writeMilestoneDiscovery("M1", discovery);
|
|
559
|
+
const exists = await state.exists("milestones/M1/discovery.json");
|
|
560
|
+
expect(exists).toBe(true);
|
|
561
|
+
});
|
|
562
|
+
it("should get correct milestone directory path", () => {
|
|
563
|
+
const dir = state.getMilestoneDir("M1");
|
|
564
|
+
expect(dir).toBe(path.join(testDir, RALPH_DIR, "milestones", "M1"));
|
|
565
|
+
});
|
|
566
|
+
});
|
|
567
|
+
describe("Element Discovery with History", () => {
|
|
568
|
+
beforeEach(async () => {
|
|
569
|
+
await state.initialize("Test Project");
|
|
570
|
+
await state.createEpicDir("E1", "auth");
|
|
571
|
+
});
|
|
572
|
+
it("should append to discovery history", async () => {
|
|
573
|
+
const discovery = {
|
|
574
|
+
element_type: "epic",
|
|
575
|
+
element_id: "E1",
|
|
576
|
+
scope: [],
|
|
577
|
+
out_of_scope: [],
|
|
578
|
+
success_criteria: [],
|
|
579
|
+
constraints: [],
|
|
580
|
+
history: [
|
|
581
|
+
{
|
|
582
|
+
timestamp: new Date().toISOString(),
|
|
583
|
+
question: "What problem does this solve?",
|
|
584
|
+
answer: "Authentication",
|
|
585
|
+
category: "problem",
|
|
586
|
+
},
|
|
587
|
+
],
|
|
588
|
+
status: "in_progress",
|
|
589
|
+
source: "user_added",
|
|
590
|
+
created: new Date().toISOString(),
|
|
591
|
+
updated: new Date().toISOString(),
|
|
592
|
+
};
|
|
593
|
+
await state.writeEpicDiscovery("E1", "auth", discovery);
|
|
594
|
+
// Read and add more history
|
|
595
|
+
const read = await state.readEpicDiscovery("E1", "auth");
|
|
596
|
+
read.history.push({
|
|
597
|
+
timestamp: new Date().toISOString(),
|
|
598
|
+
question: "What's in scope?",
|
|
599
|
+
answer: "Login and logout",
|
|
600
|
+
category: "other",
|
|
601
|
+
});
|
|
602
|
+
await state.writeEpicDiscovery("E1", "auth", read);
|
|
603
|
+
const updated = await state.readEpicDiscovery("E1", "auth");
|
|
604
|
+
expect(updated?.history).toHaveLength(2);
|
|
605
|
+
});
|
|
606
|
+
});
|
|
607
|
+
describe("deriveEpicStatus", () => {
|
|
608
|
+
beforeEach(async () => {
|
|
609
|
+
await state.initialize("Test Project");
|
|
610
|
+
await state.createEpicDir("E1", "auth");
|
|
611
|
+
});
|
|
612
|
+
it("should return 'deferred' when epic is deferred", async () => {
|
|
613
|
+
const epic = {
|
|
614
|
+
id: "E1",
|
|
615
|
+
slug: "auth",
|
|
616
|
+
name: "Auth",
|
|
617
|
+
description: "Auth epic",
|
|
618
|
+
milestone: "M1",
|
|
619
|
+
deferred: true,
|
|
620
|
+
created: new Date().toISOString(),
|
|
621
|
+
updated: new Date().toISOString(),
|
|
622
|
+
artifacts: {
|
|
623
|
+
prd: { status: "complete", version: 1 },
|
|
624
|
+
architecture: { status: "complete", version: 1 },
|
|
625
|
+
stories: { status: "complete", count: 3 },
|
|
626
|
+
},
|
|
627
|
+
dependencies: [],
|
|
628
|
+
stats: { requirements: 2, stories: 3, coverage: 100 },
|
|
629
|
+
};
|
|
630
|
+
const status = await state.deriveEpicStatus(epic, []);
|
|
631
|
+
expect(status).toBe("deferred");
|
|
632
|
+
});
|
|
633
|
+
it("should return 'planned' when no artifacts complete", async () => {
|
|
634
|
+
const epic = {
|
|
635
|
+
id: "E1",
|
|
636
|
+
slug: "auth",
|
|
637
|
+
name: "Auth",
|
|
638
|
+
description: "Auth epic",
|
|
639
|
+
milestone: "M1",
|
|
640
|
+
deferred: false,
|
|
641
|
+
created: new Date().toISOString(),
|
|
642
|
+
updated: new Date().toISOString(),
|
|
643
|
+
artifacts: {
|
|
644
|
+
prd: { status: "pending", version: 0 },
|
|
645
|
+
architecture: { status: "pending", version: 0 },
|
|
646
|
+
stories: { status: "pending", count: 0 },
|
|
647
|
+
},
|
|
648
|
+
dependencies: [],
|
|
649
|
+
stats: { requirements: 0, stories: 0, coverage: 0 },
|
|
650
|
+
};
|
|
651
|
+
const status = await state.deriveEpicStatus(epic, null);
|
|
652
|
+
expect(status).toBe("planned");
|
|
653
|
+
});
|
|
654
|
+
it("should return 'drafting' when PRD complete but no stories", async () => {
|
|
655
|
+
const epic = {
|
|
656
|
+
id: "E1",
|
|
657
|
+
slug: "auth",
|
|
658
|
+
name: "Auth",
|
|
659
|
+
description: "Auth epic",
|
|
660
|
+
milestone: "M1",
|
|
661
|
+
deferred: false,
|
|
662
|
+
created: new Date().toISOString(),
|
|
663
|
+
updated: new Date().toISOString(),
|
|
664
|
+
artifacts: {
|
|
665
|
+
prd: { status: "complete", version: 1 },
|
|
666
|
+
architecture: { status: "pending", version: 0 },
|
|
667
|
+
stories: { status: "pending", count: 0 },
|
|
668
|
+
},
|
|
669
|
+
dependencies: [],
|
|
670
|
+
stats: { requirements: 2, stories: 0, coverage: 0 },
|
|
671
|
+
};
|
|
672
|
+
const status = await state.deriveEpicStatus(epic, null);
|
|
673
|
+
expect(status).toBe("drafting");
|
|
674
|
+
});
|
|
675
|
+
it("should return 'ready' when stories exist but none started", async () => {
|
|
676
|
+
const epic = {
|
|
677
|
+
id: "E1",
|
|
678
|
+
slug: "auth",
|
|
679
|
+
name: "Auth",
|
|
680
|
+
description: "Auth epic",
|
|
681
|
+
milestone: "M1",
|
|
682
|
+
deferred: false,
|
|
683
|
+
created: new Date().toISOString(),
|
|
684
|
+
updated: new Date().toISOString(),
|
|
685
|
+
artifacts: {
|
|
686
|
+
prd: { status: "complete", version: 1 },
|
|
687
|
+
architecture: { status: "complete", version: 1 },
|
|
688
|
+
stories: { status: "complete", count: 2 },
|
|
689
|
+
},
|
|
690
|
+
dependencies: [],
|
|
691
|
+
stats: { requirements: 2, stories: 2, coverage: 100 },
|
|
692
|
+
};
|
|
693
|
+
const stories = [
|
|
694
|
+
{
|
|
695
|
+
id: "E1.S1",
|
|
696
|
+
title: "Story 1",
|
|
697
|
+
description: "Desc",
|
|
698
|
+
type: "feature",
|
|
699
|
+
requirements: ["E1.R1"],
|
|
700
|
+
acceptance_criteria: [],
|
|
701
|
+
dependencies: [],
|
|
702
|
+
status: "todo",
|
|
703
|
+
assignee: null,
|
|
704
|
+
},
|
|
705
|
+
{
|
|
706
|
+
id: "E1.S2",
|
|
707
|
+
title: "Story 2",
|
|
708
|
+
description: "Desc",
|
|
709
|
+
type: "feature",
|
|
710
|
+
requirements: ["E1.R2"],
|
|
711
|
+
acceptance_criteria: [],
|
|
712
|
+
dependencies: [],
|
|
713
|
+
status: "todo",
|
|
714
|
+
assignee: null,
|
|
715
|
+
},
|
|
716
|
+
];
|
|
717
|
+
const status = await state.deriveEpicStatus(epic, stories);
|
|
718
|
+
expect(status).toBe("ready");
|
|
719
|
+
});
|
|
720
|
+
it("should return 'in_progress' when any story is in progress", async () => {
|
|
721
|
+
const epic = {
|
|
722
|
+
id: "E1",
|
|
723
|
+
slug: "auth",
|
|
724
|
+
name: "Auth",
|
|
725
|
+
description: "Auth epic",
|
|
726
|
+
milestone: "M1",
|
|
727
|
+
deferred: false,
|
|
728
|
+
created: new Date().toISOString(),
|
|
729
|
+
updated: new Date().toISOString(),
|
|
730
|
+
artifacts: {
|
|
731
|
+
prd: { status: "complete", version: 1 },
|
|
732
|
+
architecture: { status: "complete", version: 1 },
|
|
733
|
+
stories: { status: "complete", count: 2 },
|
|
734
|
+
},
|
|
735
|
+
dependencies: [],
|
|
736
|
+
stats: { requirements: 2, stories: 2, coverage: 100 },
|
|
737
|
+
};
|
|
738
|
+
const stories = [
|
|
739
|
+
{
|
|
740
|
+
id: "E1.S1",
|
|
741
|
+
title: "Story 1",
|
|
742
|
+
description: "Desc",
|
|
743
|
+
type: "feature",
|
|
744
|
+
requirements: ["E1.R1"],
|
|
745
|
+
acceptance_criteria: [],
|
|
746
|
+
dependencies: [],
|
|
747
|
+
status: "in_progress",
|
|
748
|
+
assignee: "dev@example.com",
|
|
749
|
+
},
|
|
750
|
+
{
|
|
751
|
+
id: "E1.S2",
|
|
752
|
+
title: "Story 2",
|
|
753
|
+
description: "Desc",
|
|
754
|
+
type: "feature",
|
|
755
|
+
requirements: ["E1.R2"],
|
|
756
|
+
acceptance_criteria: [],
|
|
757
|
+
dependencies: [],
|
|
758
|
+
status: "todo",
|
|
759
|
+
assignee: null,
|
|
760
|
+
},
|
|
761
|
+
];
|
|
762
|
+
const status = await state.deriveEpicStatus(epic, stories);
|
|
763
|
+
expect(status).toBe("in_progress");
|
|
764
|
+
});
|
|
765
|
+
it("should return 'in_progress' when any story is in review", async () => {
|
|
766
|
+
const epic = {
|
|
767
|
+
id: "E1",
|
|
768
|
+
slug: "auth",
|
|
769
|
+
name: "Auth",
|
|
770
|
+
description: "Auth epic",
|
|
771
|
+
milestone: "M1",
|
|
772
|
+
deferred: false,
|
|
773
|
+
created: new Date().toISOString(),
|
|
774
|
+
updated: new Date().toISOString(),
|
|
775
|
+
artifacts: {
|
|
776
|
+
prd: { status: "complete", version: 1 },
|
|
777
|
+
architecture: { status: "complete", version: 1 },
|
|
778
|
+
stories: { status: "complete", count: 2 },
|
|
779
|
+
},
|
|
780
|
+
dependencies: [],
|
|
781
|
+
stats: { requirements: 2, stories: 2, coverage: 100 },
|
|
782
|
+
};
|
|
783
|
+
const stories = [
|
|
784
|
+
{
|
|
785
|
+
id: "E1.S1",
|
|
786
|
+
title: "Story 1",
|
|
787
|
+
description: "Desc",
|
|
788
|
+
type: "feature",
|
|
789
|
+
requirements: ["E1.R1"],
|
|
790
|
+
acceptance_criteria: [],
|
|
791
|
+
dependencies: [],
|
|
792
|
+
status: "review",
|
|
793
|
+
assignee: "dev@example.com",
|
|
794
|
+
},
|
|
795
|
+
{
|
|
796
|
+
id: "E1.S2",
|
|
797
|
+
title: "Story 2",
|
|
798
|
+
description: "Desc",
|
|
799
|
+
type: "feature",
|
|
800
|
+
requirements: ["E1.R2"],
|
|
801
|
+
acceptance_criteria: [],
|
|
802
|
+
dependencies: [],
|
|
803
|
+
status: "done",
|
|
804
|
+
assignee: null,
|
|
805
|
+
},
|
|
806
|
+
];
|
|
807
|
+
const status = await state.deriveEpicStatus(epic, stories);
|
|
808
|
+
expect(status).toBe("in_progress");
|
|
809
|
+
});
|
|
810
|
+
it("should return 'done' when all stories are done", async () => {
|
|
811
|
+
const epic = {
|
|
812
|
+
id: "E1",
|
|
813
|
+
slug: "auth",
|
|
814
|
+
name: "Auth",
|
|
815
|
+
description: "Auth epic",
|
|
816
|
+
milestone: "M1",
|
|
817
|
+
deferred: false,
|
|
818
|
+
created: new Date().toISOString(),
|
|
819
|
+
updated: new Date().toISOString(),
|
|
820
|
+
artifacts: {
|
|
821
|
+
prd: { status: "complete", version: 1 },
|
|
822
|
+
architecture: { status: "complete", version: 1 },
|
|
823
|
+
stories: { status: "complete", count: 2 },
|
|
824
|
+
},
|
|
825
|
+
dependencies: [],
|
|
826
|
+
stats: { requirements: 2, stories: 2, coverage: 100 },
|
|
827
|
+
};
|
|
828
|
+
const stories = [
|
|
829
|
+
{
|
|
830
|
+
id: "E1.S1",
|
|
831
|
+
title: "Story 1",
|
|
832
|
+
description: "Desc",
|
|
833
|
+
type: "feature",
|
|
834
|
+
requirements: ["E1.R1"],
|
|
835
|
+
acceptance_criteria: [],
|
|
836
|
+
dependencies: [],
|
|
837
|
+
status: "done",
|
|
838
|
+
assignee: null,
|
|
839
|
+
},
|
|
840
|
+
{
|
|
841
|
+
id: "E1.S2",
|
|
842
|
+
title: "Story 2",
|
|
843
|
+
description: "Desc",
|
|
844
|
+
type: "feature",
|
|
845
|
+
requirements: ["E1.R2"],
|
|
846
|
+
acceptance_criteria: [],
|
|
847
|
+
dependencies: [],
|
|
848
|
+
status: "done",
|
|
849
|
+
assignee: null,
|
|
850
|
+
},
|
|
851
|
+
];
|
|
852
|
+
const status = await state.deriveEpicStatus(epic, stories);
|
|
853
|
+
expect(status).toBe("done");
|
|
854
|
+
});
|
|
855
|
+
});
|
|
856
|
+
describe("deriveMilestoneStatus", () => {
|
|
857
|
+
beforeEach(async () => {
|
|
858
|
+
await state.initialize("Test Project");
|
|
859
|
+
});
|
|
860
|
+
it("should return 'planned' when milestone has no epics", async () => {
|
|
861
|
+
const milestone = {
|
|
862
|
+
id: "M1",
|
|
863
|
+
slug: "foundation",
|
|
864
|
+
name: "Foundation",
|
|
865
|
+
description: "Core infrastructure",
|
|
866
|
+
order: 1,
|
|
867
|
+
epics: [],
|
|
868
|
+
created: new Date().toISOString(),
|
|
869
|
+
updated: new Date().toISOString(),
|
|
870
|
+
};
|
|
871
|
+
const status = await state.deriveMilestoneStatus(milestone);
|
|
872
|
+
expect(status).toBe("planned");
|
|
873
|
+
});
|
|
874
|
+
it("should return 'in_progress' when any epic is in progress", async () => {
|
|
875
|
+
// Create two epics with different statuses
|
|
876
|
+
await state.createEpicDir("E1", "auth");
|
|
877
|
+
await state.createEpicDir("E2", "db");
|
|
878
|
+
const epic1 = {
|
|
879
|
+
id: "E1",
|
|
880
|
+
slug: "auth",
|
|
881
|
+
name: "Auth",
|
|
882
|
+
description: "Auth epic",
|
|
883
|
+
milestone: "M1",
|
|
884
|
+
deferred: false,
|
|
885
|
+
created: new Date().toISOString(),
|
|
886
|
+
updated: new Date().toISOString(),
|
|
887
|
+
artifacts: {
|
|
888
|
+
prd: { status: "complete", version: 1 },
|
|
889
|
+
architecture: { status: "complete", version: 1 },
|
|
890
|
+
stories: { status: "complete", count: 1 },
|
|
891
|
+
},
|
|
892
|
+
dependencies: [],
|
|
893
|
+
stats: { requirements: 1, stories: 1, coverage: 100 },
|
|
894
|
+
};
|
|
895
|
+
await state.writeEpic(epic1);
|
|
896
|
+
const stories1 = {
|
|
897
|
+
epic_id: "E1",
|
|
898
|
+
stories: [
|
|
899
|
+
{
|
|
900
|
+
id: "E1.S1",
|
|
901
|
+
title: "Story 1",
|
|
902
|
+
description: "Desc",
|
|
903
|
+
type: "feature",
|
|
904
|
+
requirements: ["E1.R1"],
|
|
905
|
+
acceptance_criteria: [],
|
|
906
|
+
dependencies: [],
|
|
907
|
+
status: "in_progress",
|
|
908
|
+
assignee: "dev@example.com",
|
|
909
|
+
},
|
|
910
|
+
],
|
|
911
|
+
coverage: {},
|
|
912
|
+
validation: { coverage_complete: true, all_links_valid: true },
|
|
913
|
+
};
|
|
914
|
+
await state.writeStories("E1", "auth", stories1);
|
|
915
|
+
const epic2 = {
|
|
916
|
+
id: "E2",
|
|
917
|
+
slug: "db",
|
|
918
|
+
name: "Database",
|
|
919
|
+
description: "Database epic",
|
|
920
|
+
milestone: "M1",
|
|
921
|
+
deferred: false,
|
|
922
|
+
created: new Date().toISOString(),
|
|
923
|
+
updated: new Date().toISOString(),
|
|
924
|
+
artifacts: {
|
|
925
|
+
prd: { status: "pending", version: 0 },
|
|
926
|
+
architecture: { status: "pending", version: 0 },
|
|
927
|
+
stories: { status: "pending", count: 0 },
|
|
928
|
+
},
|
|
929
|
+
dependencies: [],
|
|
930
|
+
stats: { requirements: 0, stories: 0, coverage: 0 },
|
|
931
|
+
};
|
|
932
|
+
await state.writeEpic(epic2);
|
|
933
|
+
const milestone = {
|
|
934
|
+
id: "M1",
|
|
935
|
+
slug: "foundation",
|
|
936
|
+
name: "Foundation",
|
|
937
|
+
description: "Core infrastructure",
|
|
938
|
+
order: 1,
|
|
939
|
+
epics: ["E1", "E2"],
|
|
940
|
+
created: new Date().toISOString(),
|
|
941
|
+
updated: new Date().toISOString(),
|
|
942
|
+
};
|
|
943
|
+
const status = await state.deriveMilestoneStatus(milestone);
|
|
944
|
+
expect(status).toBe("in_progress");
|
|
945
|
+
});
|
|
946
|
+
it("should return 'done' when all epics are done", async () => {
|
|
947
|
+
await state.createEpicDir("E1", "auth");
|
|
948
|
+
const epic1 = {
|
|
949
|
+
id: "E1",
|
|
950
|
+
slug: "auth",
|
|
951
|
+
name: "Auth",
|
|
952
|
+
description: "Auth epic",
|
|
953
|
+
milestone: "M1",
|
|
954
|
+
deferred: false,
|
|
955
|
+
created: new Date().toISOString(),
|
|
956
|
+
updated: new Date().toISOString(),
|
|
957
|
+
artifacts: {
|
|
958
|
+
prd: { status: "complete", version: 1 },
|
|
959
|
+
architecture: { status: "complete", version: 1 },
|
|
960
|
+
stories: { status: "complete", count: 1 },
|
|
961
|
+
},
|
|
962
|
+
dependencies: [],
|
|
963
|
+
stats: { requirements: 1, stories: 1, coverage: 100 },
|
|
964
|
+
};
|
|
965
|
+
await state.writeEpic(epic1);
|
|
966
|
+
const stories1 = {
|
|
967
|
+
epic_id: "E1",
|
|
968
|
+
stories: [
|
|
969
|
+
{
|
|
970
|
+
id: "E1.S1",
|
|
971
|
+
title: "Story 1",
|
|
972
|
+
description: "Desc",
|
|
973
|
+
type: "feature",
|
|
974
|
+
requirements: ["E1.R1"],
|
|
975
|
+
acceptance_criteria: [],
|
|
976
|
+
dependencies: [],
|
|
977
|
+
status: "done",
|
|
978
|
+
assignee: null,
|
|
979
|
+
},
|
|
980
|
+
],
|
|
981
|
+
coverage: {},
|
|
982
|
+
validation: { coverage_complete: true, all_links_valid: true },
|
|
983
|
+
};
|
|
984
|
+
await state.writeStories("E1", "auth", stories1);
|
|
985
|
+
const milestone = {
|
|
986
|
+
id: "M1",
|
|
987
|
+
slug: "foundation",
|
|
988
|
+
name: "Foundation",
|
|
989
|
+
description: "Core infrastructure",
|
|
990
|
+
order: 1,
|
|
991
|
+
epics: ["E1"],
|
|
992
|
+
created: new Date().toISOString(),
|
|
993
|
+
updated: new Date().toISOString(),
|
|
994
|
+
};
|
|
995
|
+
const status = await state.deriveMilestoneStatus(milestone);
|
|
996
|
+
expect(status).toBe("done");
|
|
997
|
+
});
|
|
998
|
+
});
|
|
999
|
+
describe("getEpicWithStatus", () => {
|
|
1000
|
+
beforeEach(async () => {
|
|
1001
|
+
await state.initialize("Test Project");
|
|
1002
|
+
await state.createEpicDir("E1", "auth");
|
|
1003
|
+
});
|
|
1004
|
+
it("should return epic with derived status", async () => {
|
|
1005
|
+
const epic = {
|
|
1006
|
+
id: "E1",
|
|
1007
|
+
slug: "auth",
|
|
1008
|
+
name: "Auth",
|
|
1009
|
+
description: "Auth epic",
|
|
1010
|
+
milestone: "M1",
|
|
1011
|
+
deferred: false,
|
|
1012
|
+
created: new Date().toISOString(),
|
|
1013
|
+
updated: new Date().toISOString(),
|
|
1014
|
+
artifacts: {
|
|
1015
|
+
prd: { status: "pending", version: 0 },
|
|
1016
|
+
architecture: { status: "pending", version: 0 },
|
|
1017
|
+
stories: { status: "pending", count: 0 },
|
|
1018
|
+
},
|
|
1019
|
+
dependencies: [],
|
|
1020
|
+
stats: { requirements: 0, stories: 0, coverage: 0 },
|
|
1021
|
+
};
|
|
1022
|
+
await state.writeEpic(epic);
|
|
1023
|
+
const result = await state.getEpicWithStatus("E1", "auth");
|
|
1024
|
+
expect(result).not.toBeNull();
|
|
1025
|
+
expect(result?.epic.id).toBe("E1");
|
|
1026
|
+
expect(result?.status).toBe("planned");
|
|
1027
|
+
});
|
|
1028
|
+
it("should return null for non-existent epic", async () => {
|
|
1029
|
+
const result = await state.getEpicWithStatus("E99", "nonexistent");
|
|
1030
|
+
expect(result).toBeNull();
|
|
1031
|
+
});
|
|
1032
|
+
});
|
|
1033
|
+
describe("getMilestoneWithStatus", () => {
|
|
1034
|
+
beforeEach(async () => {
|
|
1035
|
+
await state.initialize("Test Project");
|
|
1036
|
+
});
|
|
1037
|
+
it("should return milestone with derived status", async () => {
|
|
1038
|
+
const milestone = {
|
|
1039
|
+
id: "M1",
|
|
1040
|
+
slug: "foundation",
|
|
1041
|
+
name: "Foundation",
|
|
1042
|
+
description: "Core infrastructure",
|
|
1043
|
+
order: 1,
|
|
1044
|
+
epics: [],
|
|
1045
|
+
created: new Date().toISOString(),
|
|
1046
|
+
updated: new Date().toISOString(),
|
|
1047
|
+
};
|
|
1048
|
+
const result = await state.getMilestoneWithStatus(milestone);
|
|
1049
|
+
expect(result.milestone.id).toBe("M1");
|
|
1050
|
+
expect(result.status).toBe("planned");
|
|
1051
|
+
});
|
|
1052
|
+
it("should derive in_progress status from epics", async () => {
|
|
1053
|
+
await state.createEpicDir("E1", "auth");
|
|
1054
|
+
const epic = {
|
|
1055
|
+
id: "E1",
|
|
1056
|
+
slug: "auth",
|
|
1057
|
+
name: "Auth",
|
|
1058
|
+
description: "Auth epic",
|
|
1059
|
+
milestone: "M1",
|
|
1060
|
+
deferred: false,
|
|
1061
|
+
created: new Date().toISOString(),
|
|
1062
|
+
updated: new Date().toISOString(),
|
|
1063
|
+
artifacts: {
|
|
1064
|
+
prd: { status: "complete", version: 1 },
|
|
1065
|
+
architecture: { status: "complete", version: 1 },
|
|
1066
|
+
stories: { status: "complete", count: 1 },
|
|
1067
|
+
},
|
|
1068
|
+
dependencies: [],
|
|
1069
|
+
stats: { requirements: 1, stories: 1, coverage: 100 },
|
|
1070
|
+
};
|
|
1071
|
+
await state.writeEpic(epic);
|
|
1072
|
+
const stories = {
|
|
1073
|
+
epic_id: "E1",
|
|
1074
|
+
stories: [
|
|
1075
|
+
{
|
|
1076
|
+
id: "E1.S1",
|
|
1077
|
+
title: "Story 1",
|
|
1078
|
+
description: "Desc",
|
|
1079
|
+
type: "feature",
|
|
1080
|
+
requirements: ["E1.R1"],
|
|
1081
|
+
acceptance_criteria: [],
|
|
1082
|
+
dependencies: [],
|
|
1083
|
+
status: "in_progress",
|
|
1084
|
+
assignee: "dev@example.com",
|
|
1085
|
+
},
|
|
1086
|
+
],
|
|
1087
|
+
coverage: {},
|
|
1088
|
+
validation: { coverage_complete: true, all_links_valid: true },
|
|
1089
|
+
};
|
|
1090
|
+
await state.writeStories("E1", "auth", stories);
|
|
1091
|
+
const milestone = {
|
|
1092
|
+
id: "M1",
|
|
1093
|
+
slug: "foundation",
|
|
1094
|
+
name: "Foundation",
|
|
1095
|
+
description: "Core infrastructure",
|
|
1096
|
+
order: 1,
|
|
1097
|
+
epics: ["E1"],
|
|
1098
|
+
created: new Date().toISOString(),
|
|
1099
|
+
updated: new Date().toISOString(),
|
|
1100
|
+
};
|
|
1101
|
+
const result = await state.getMilestoneWithStatus(milestone);
|
|
1102
|
+
expect(result.milestone.id).toBe("M1");
|
|
1103
|
+
expect(result.status).toBe("in_progress");
|
|
1104
|
+
});
|
|
1105
|
+
});
|
|
1106
|
+
});
|
|
1107
|
+
//# sourceMappingURL=state.test.js.map
|