@quinteroac/agents-coding-toolkit 0.1.0-preview
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/AGENTS.md +7 -0
- package/README.md +127 -0
- package/package.json +34 -0
- package/scaffold/.agents/flow/archived/tmpl_.gitkeep +0 -0
- package/scaffold/.agents/flow/tmpl_README.md +7 -0
- package/scaffold/.agents/flow/tmpl_iteration_close_checklist.example.md +11 -0
- package/scaffold/.agents/skills/automated-fix/tmpl_SKILL.md +67 -0
- package/scaffold/.agents/skills/create-issue/tmpl_SKILL.md +68 -0
- package/scaffold/.agents/skills/create-pr-document/tmpl_SKILL.md +125 -0
- package/scaffold/.agents/skills/create-project-context/tmpl_SKILL.md +168 -0
- package/scaffold/.agents/skills/create-test-plan/tmpl_SKILL.md +86 -0
- package/scaffold/.agents/skills/debug/tmpl_SKILL.md +19 -0
- package/scaffold/.agents/skills/evaluate/tmpl_SKILL.md +19 -0
- package/scaffold/.agents/skills/execute-test-batch/tmpl_SKILL.md +49 -0
- package/scaffold/.agents/skills/execute-test-case/tmpl_SKILL.md +47 -0
- package/scaffold/.agents/skills/implement-user-story/tmpl_SKILL.md +68 -0
- package/scaffold/.agents/skills/plan-refactor/tmpl_SKILL.md +19 -0
- package/scaffold/.agents/skills/refactor-prd/tmpl_SKILL.md +19 -0
- package/scaffold/.agents/skills/refine-pr-document/tmpl_SKILL.md +108 -0
- package/scaffold/.agents/skills/refine-project-context/tmpl_SKILL.md +157 -0
- package/scaffold/.agents/skills/refine-test-plan/tmpl_SKILL.md +76 -0
- package/scaffold/.agents/tmpl_PROJECT_CONTEXT.md +3 -0
- package/scaffold/.agents/tmpl_state.example.json +26 -0
- package/scaffold/.agents/tmpl_state_rules.md +29 -0
- package/scaffold/docs/nvst-flow/templates/tmpl_CHANGELOG.md +18 -0
- package/scaffold/docs/nvst-flow/templates/tmpl_TECHNICAL_DEBT.md +11 -0
- package/scaffold/docs/nvst-flow/templates/tmpl_it_000001_evaluation-report.md +19 -0
- package/scaffold/docs/nvst-flow/templates/tmpl_it_000001_product-requirement-document.md +19 -0
- package/scaffold/docs/nvst-flow/templates/tmpl_it_000001_refactor_plan.md +19 -0
- package/scaffold/docs/nvst-flow/templates/tmpl_it_000001_test-plan.md +19 -0
- package/scaffold/docs/nvst-flow/tmpl_COMMANDS.md +0 -0
- package/scaffold/docs/nvst-flow/tmpl_QUICK_USE.md +0 -0
- package/scaffold/docs/tmpl_PLACEHOLDER.md +0 -0
- package/scaffold/schemas/node-shims.d.ts +15 -0
- package/scaffold/schemas/tmpl_issues.ts +19 -0
- package/scaffold/schemas/tmpl_prd.ts +26 -0
- package/scaffold/schemas/tmpl_progress.ts +39 -0
- package/scaffold/schemas/tmpl_state.ts +81 -0
- package/scaffold/schemas/tmpl_test-plan.ts +20 -0
- package/scaffold/schemas/tmpl_validate-progress.ts +13 -0
- package/scaffold/schemas/tmpl_validate-state.ts +13 -0
- package/scaffold/tmpl_AGENTS.md +7 -0
- package/schemas/prd.ts +26 -0
- package/schemas/progress.ts +39 -0
- package/schemas/state.ts +81 -0
- package/schemas/test-plan.test.ts +53 -0
- package/schemas/test-plan.ts +20 -0
- package/schemas/validate-progress.ts +13 -0
- package/schemas/validate-state.ts +13 -0
- package/src/agent.test.ts +37 -0
- package/src/agent.ts +225 -0
- package/src/cli-path.ts +4 -0
- package/src/cli.ts +578 -0
- package/src/commands/approve-project-context.ts +37 -0
- package/src/commands/approve-requirement.ts +217 -0
- package/src/commands/approve-test-plan.test.ts +193 -0
- package/src/commands/approve-test-plan.ts +202 -0
- package/src/commands/create-issue.test.ts +484 -0
- package/src/commands/create-issue.ts +371 -0
- package/src/commands/create-project-context.ts +96 -0
- package/src/commands/create-prototype.test.ts +153 -0
- package/src/commands/create-prototype.ts +425 -0
- package/src/commands/create-test-plan.test.ts +381 -0
- package/src/commands/create-test-plan.ts +248 -0
- package/src/commands/define-requirement.ts +47 -0
- package/src/commands/destroy.ts +113 -0
- package/src/commands/execute-automated-fix.test.ts +580 -0
- package/src/commands/execute-automated-fix.ts +363 -0
- package/src/commands/execute-manual-fix.test.ts +343 -0
- package/src/commands/execute-manual-fix.ts +203 -0
- package/src/commands/execute-test-plan.test.ts +1891 -0
- package/src/commands/execute-test-plan.ts +722 -0
- package/src/commands/init.ts +85 -0
- package/src/commands/refine-project-context.ts +74 -0
- package/src/commands/refine-requirement.ts +60 -0
- package/src/commands/refine-test-plan.test.ts +200 -0
- package/src/commands/refine-test-plan.ts +93 -0
- package/src/commands/start-iteration.test.ts +144 -0
- package/src/commands/start-iteration.ts +101 -0
- package/src/commands/write-json.ts +136 -0
- package/src/install.test.ts +124 -0
- package/src/pack.test.ts +103 -0
- package/src/state.test.ts +66 -0
- package/src/state.ts +52 -0
- package/tsconfig.json +15 -0
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
|
|
6
|
+
import type { AgentResult } from "../agent";
|
|
7
|
+
import { readState, writeState } from "../state";
|
|
8
|
+
import { parseTestPlanForValidation, runCreateTestPlan } from "./create-test-plan";
|
|
9
|
+
|
|
10
|
+
async function createProjectRoot(): Promise<string> {
|
|
11
|
+
return mkdtemp(join(tmpdir(), "nvst-create-test-plan-"));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function withCwd<T>(cwd: string, fn: () => Promise<T>): Promise<T> {
|
|
15
|
+
const previous = process.cwd();
|
|
16
|
+
process.chdir(cwd);
|
|
17
|
+
try {
|
|
18
|
+
return await fn();
|
|
19
|
+
} finally {
|
|
20
|
+
process.chdir(previous);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function seedState(projectRoot: string, projectContextStatus: "pending" | "pending_approval" | "created") {
|
|
25
|
+
await mkdir(join(projectRoot, ".agents", "flow"), { recursive: true });
|
|
26
|
+
|
|
27
|
+
await writeState(projectRoot, {
|
|
28
|
+
current_iteration: "000003",
|
|
29
|
+
current_phase: "prototype",
|
|
30
|
+
phases: {
|
|
31
|
+
define: {
|
|
32
|
+
requirement_definition: { status: "approved", file: "it_000003_product-requirement-document.md" },
|
|
33
|
+
prd_generation: { status: "completed", file: "it_000003_PRD.json" },
|
|
34
|
+
},
|
|
35
|
+
prototype: {
|
|
36
|
+
project_context: { status: projectContextStatus, file: ".agents/PROJECT_CONTEXT.md" },
|
|
37
|
+
test_plan: { status: "pending", file: null },
|
|
38
|
+
tp_generation: { status: "pending", file: null },
|
|
39
|
+
prototype_build: { status: "pending", file: null },
|
|
40
|
+
test_execution: { status: "pending", file: null },
|
|
41
|
+
prototype_approved: false,
|
|
42
|
+
},
|
|
43
|
+
refactor: {
|
|
44
|
+
evaluation_report: { status: "pending", file: null },
|
|
45
|
+
refactor_plan: { status: "pending", file: null },
|
|
46
|
+
refactor_execution: { status: "pending", file: null },
|
|
47
|
+
changelog: { status: "pending", file: null },
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
last_updated: "2026-02-20T00:00:00.000Z",
|
|
51
|
+
updated_by: "seed",
|
|
52
|
+
history: [],
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
await writeFile(join(projectRoot, ".agents", "PROJECT_CONTEXT.md"), "# Context\n", "utf8");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const createdRoots: string[] = [];
|
|
59
|
+
|
|
60
|
+
afterEach(async () => {
|
|
61
|
+
await Promise.all(createdRoots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe("create test-plan command", () => {
|
|
65
|
+
test("registers create test-plan command in CLI dispatch", async () => {
|
|
66
|
+
const source = await readFile(join(process.cwd(), "src", "cli.ts"), "utf8");
|
|
67
|
+
|
|
68
|
+
expect(source).toContain('import { runCreateTestPlan } from "./commands/create-test-plan";');
|
|
69
|
+
expect(source).toContain('if (subcommand === "test-plan") {');
|
|
70
|
+
expect(source).toContain("await runCreateTestPlan({ provider, force });");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("loads create-test-plan skill, invokes agent interactively with iteration context, writes state", async () => {
|
|
74
|
+
const projectRoot = await createProjectRoot();
|
|
75
|
+
createdRoots.push(projectRoot);
|
|
76
|
+
await seedState(projectRoot, "created");
|
|
77
|
+
|
|
78
|
+
let loadedSkill = "";
|
|
79
|
+
let invocation: { interactive: boolean | undefined; prompt: string } | undefined;
|
|
80
|
+
const outputPath = join(projectRoot, ".agents", "flow", "it_000003_test-plan.md");
|
|
81
|
+
|
|
82
|
+
await withCwd(projectRoot, async () => {
|
|
83
|
+
await runCreateTestPlan(
|
|
84
|
+
{ provider: "codex" },
|
|
85
|
+
{
|
|
86
|
+
loadSkillFn: async (_root, skillName) => {
|
|
87
|
+
loadedSkill = skillName;
|
|
88
|
+
return [
|
|
89
|
+
"# Create Test Plan",
|
|
90
|
+
"Every functional requirement (`FR-N`) must appear in at least one test case `Correlated Requirements` field.",
|
|
91
|
+
"Use this table column exactly: Correlated Requirements (US-XXX, FR-X).",
|
|
92
|
+
].join("\n");
|
|
93
|
+
},
|
|
94
|
+
invokeAgentFn: async (options): Promise<AgentResult> => {
|
|
95
|
+
invocation = {
|
|
96
|
+
interactive: options.interactive,
|
|
97
|
+
prompt: options.prompt,
|
|
98
|
+
};
|
|
99
|
+
await writeFile(
|
|
100
|
+
outputPath,
|
|
101
|
+
[
|
|
102
|
+
"# Test Plan - Iteration 000003",
|
|
103
|
+
"## User Story: US-001 - Example Story",
|
|
104
|
+
"| Test Case ID | Description | Type (unit/integration/e2e) | Mode (automated/manual) | Correlated Requirements (US-XXX, FR-X) | Expected Result |",
|
|
105
|
+
"|---|---|---|---|---|---|",
|
|
106
|
+
"| TC-US001-01 | Validate login success | integration | automated | US-001, FR-1 | Login succeeds with valid credentials |",
|
|
107
|
+
"## Scope",
|
|
108
|
+
"- Validate login and session behavior",
|
|
109
|
+
"## Environment and data",
|
|
110
|
+
"- Node 22 with seeded test user",
|
|
111
|
+
].join("\n"),
|
|
112
|
+
"utf8",
|
|
113
|
+
);
|
|
114
|
+
return { exitCode: 0, stdout: "", stderr: "" };
|
|
115
|
+
},
|
|
116
|
+
nowFn: () => new Date("2026-02-21T03:00:00.000Z"),
|
|
117
|
+
},
|
|
118
|
+
);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
expect(loadedSkill).toBe("create-test-plan");
|
|
122
|
+
if (invocation === undefined) {
|
|
123
|
+
throw new Error("Agent invocation was not captured");
|
|
124
|
+
}
|
|
125
|
+
expect(invocation.interactive).toBe(true);
|
|
126
|
+
expect(invocation.prompt).toContain("### iteration");
|
|
127
|
+
expect(invocation.prompt).toContain("000003");
|
|
128
|
+
expect(invocation.prompt).toContain("Correlated Requirements (US-XXX, FR-X)");
|
|
129
|
+
expect(invocation.prompt).toContain(
|
|
130
|
+
"Every functional requirement (`FR-N`) must appear in at least one test case `Correlated Requirements` field.",
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
const content = await readFile(outputPath, "utf8");
|
|
134
|
+
expect(content).toContain("# Test Plan");
|
|
135
|
+
|
|
136
|
+
const state = await readState(projectRoot);
|
|
137
|
+
expect(state.phases.prototype.test_plan.status).toBe("pending_approval");
|
|
138
|
+
expect(state.phases.prototype.test_plan.file).toBe("it_000003_test-plan.md");
|
|
139
|
+
expect(state.last_updated).toBe("2026-02-21T03:00:00.000Z");
|
|
140
|
+
expect(state.updated_by).toBe("nvst:create-test-plan");
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("supports --agent cursor and writes stdout output to the test-plan file", async () => {
|
|
144
|
+
const projectRoot = await createProjectRoot();
|
|
145
|
+
createdRoots.push(projectRoot);
|
|
146
|
+
await seedState(projectRoot, "created");
|
|
147
|
+
|
|
148
|
+
const outputPath = join(projectRoot, ".agents", "flow", "it_000003_test-plan.md");
|
|
149
|
+
let capturedPrompt = "";
|
|
150
|
+
let capturedProvider = "";
|
|
151
|
+
|
|
152
|
+
await withCwd(projectRoot, async () => {
|
|
153
|
+
await runCreateTestPlan(
|
|
154
|
+
{ provider: "cursor" },
|
|
155
|
+
{
|
|
156
|
+
loadSkillFn: async () => "Create a test plan using iteration and project context.",
|
|
157
|
+
invokeAgentFn: async (options): Promise<AgentResult> => {
|
|
158
|
+
capturedPrompt = options.prompt;
|
|
159
|
+
capturedProvider = options.provider;
|
|
160
|
+
return {
|
|
161
|
+
exitCode: 0,
|
|
162
|
+
stdout: [
|
|
163
|
+
"# Test Plan - Iteration 000003",
|
|
164
|
+
"## User Story: US-001 - Cursor flow",
|
|
165
|
+
"| Test Case ID | Description | Type (unit/integration/e2e) | Mode (automated/manual) | Correlated Requirements (US-XXX, FR-X) | Expected Result |",
|
|
166
|
+
"|---|---|---|---|---|---|",
|
|
167
|
+
"| TC-US001-01 | Validate cursor provider flow | integration | automated | US-001, FR-1 | Cursor provider generates plan output |",
|
|
168
|
+
"## Scope",
|
|
169
|
+
"- Cursor provider compatibility",
|
|
170
|
+
"## Environment and data",
|
|
171
|
+
"- Local dev shell",
|
|
172
|
+
].join("\n"),
|
|
173
|
+
stderr: "",
|
|
174
|
+
};
|
|
175
|
+
},
|
|
176
|
+
nowFn: () => new Date("2026-02-21T03:30:00.000Z"),
|
|
177
|
+
},
|
|
178
|
+
);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
expect(capturedProvider).toBe("cursor");
|
|
182
|
+
expect(capturedPrompt).toContain("### iteration");
|
|
183
|
+
expect(capturedPrompt).toContain("000003");
|
|
184
|
+
expect(capturedPrompt).toContain("### project_context");
|
|
185
|
+
expect(capturedPrompt).toContain("# Context");
|
|
186
|
+
|
|
187
|
+
const content = await readFile(outputPath, "utf8");
|
|
188
|
+
expect(content).toContain("TC-US001-01");
|
|
189
|
+
expect(content).toContain("Cursor provider compatibility");
|
|
190
|
+
|
|
191
|
+
const state = await readState(projectRoot);
|
|
192
|
+
expect(state.phases.prototype.test_plan.status).toBe("pending_approval");
|
|
193
|
+
expect(state.phases.prototype.test_plan.file).toBe("it_000003_test-plan.md");
|
|
194
|
+
expect(state.last_updated).toBe("2026-02-21T03:30:00.000Z");
|
|
195
|
+
expect(state.updated_by).toBe("nvst:create-test-plan");
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test("requires project_context.status to be created", async () => {
|
|
199
|
+
const projectRoot = await createProjectRoot();
|
|
200
|
+
createdRoots.push(projectRoot);
|
|
201
|
+
await seedState(projectRoot, "pending");
|
|
202
|
+
|
|
203
|
+
await withCwd(projectRoot, async () => {
|
|
204
|
+
await expect(
|
|
205
|
+
runCreateTestPlan(
|
|
206
|
+
{ provider: "codex" },
|
|
207
|
+
{
|
|
208
|
+
loadSkillFn: async () => "unused",
|
|
209
|
+
invokeAgentFn: async () => ({ exitCode: 0, stdout: "", stderr: "" }),
|
|
210
|
+
},
|
|
211
|
+
),
|
|
212
|
+
).rejects.toThrow("Cannot create test plan: prototype.project_context.status must be created");
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test("asks for confirmation before overwrite and cancels when denied", async () => {
|
|
217
|
+
const projectRoot = await createProjectRoot();
|
|
218
|
+
createdRoots.push(projectRoot);
|
|
219
|
+
await seedState(projectRoot, "created");
|
|
220
|
+
|
|
221
|
+
const outputPath = join(projectRoot, ".agents", "flow", "it_000003_test-plan.md");
|
|
222
|
+
await writeFile(outputPath, "old", "utf8");
|
|
223
|
+
|
|
224
|
+
let confirmCalls = 0;
|
|
225
|
+
let invokeCalls = 0;
|
|
226
|
+
|
|
227
|
+
await withCwd(projectRoot, async () => {
|
|
228
|
+
await runCreateTestPlan(
|
|
229
|
+
{ provider: "codex" },
|
|
230
|
+
{
|
|
231
|
+
confirmOverwriteFn: async () => {
|
|
232
|
+
confirmCalls += 1;
|
|
233
|
+
return false;
|
|
234
|
+
},
|
|
235
|
+
loadSkillFn: async () => "unused",
|
|
236
|
+
invokeAgentFn: async () => {
|
|
237
|
+
invokeCalls += 1;
|
|
238
|
+
return { exitCode: 0, stdout: "", stderr: "" };
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
expect(confirmCalls).toBe(1);
|
|
245
|
+
expect(invokeCalls).toBe(0);
|
|
246
|
+
|
|
247
|
+
const state = await readState(projectRoot);
|
|
248
|
+
expect(state.phases.prototype.test_plan.status).toBe("pending");
|
|
249
|
+
expect(state.phases.prototype.test_plan.file).toBeNull();
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
test("force overwrite bypasses confirmation", async () => {
|
|
253
|
+
const projectRoot = await createProjectRoot();
|
|
254
|
+
createdRoots.push(projectRoot);
|
|
255
|
+
await seedState(projectRoot, "created");
|
|
256
|
+
|
|
257
|
+
const outputPath = join(projectRoot, ".agents", "flow", "it_000003_test-plan.md");
|
|
258
|
+
await writeFile(outputPath, "old", "utf8");
|
|
259
|
+
|
|
260
|
+
let confirmCalled = false;
|
|
261
|
+
let invokeCalls = 0;
|
|
262
|
+
|
|
263
|
+
await withCwd(projectRoot, async () => {
|
|
264
|
+
await runCreateTestPlan(
|
|
265
|
+
{ provider: "codex", force: true },
|
|
266
|
+
{
|
|
267
|
+
confirmOverwriteFn: async () => {
|
|
268
|
+
confirmCalled = true;
|
|
269
|
+
return true;
|
|
270
|
+
},
|
|
271
|
+
loadSkillFn: async () => "skill",
|
|
272
|
+
invokeAgentFn: async () => {
|
|
273
|
+
invokeCalls += 1;
|
|
274
|
+
await writeFile(
|
|
275
|
+
outputPath,
|
|
276
|
+
[
|
|
277
|
+
"# Test Plan - Iteration 000003",
|
|
278
|
+
"## User Story: US-001 - Example Story",
|
|
279
|
+
"| Test Case ID | Description | Type (unit/integration/e2e) | Mode (automated/manual) | Correlated Requirements (US-XXX, FR-X) | Expected Result |",
|
|
280
|
+
"|---|---|---|---|---|---|",
|
|
281
|
+
"| TC-US001-01 | Validate login success | integration | automated | US-001, FR-1 | Login succeeds with valid credentials |",
|
|
282
|
+
].join("\n"),
|
|
283
|
+
"utf8",
|
|
284
|
+
);
|
|
285
|
+
return { exitCode: 0, stdout: "", stderr: "" };
|
|
286
|
+
},
|
|
287
|
+
},
|
|
288
|
+
);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
expect(confirmCalled).toBe(false);
|
|
292
|
+
expect(invokeCalls).toBe(1);
|
|
293
|
+
expect(await readFile(outputPath, "utf8")).toContain("Correlated Requirements");
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
test("fails when generated markdown table omits correlated requirement IDs", async () => {
|
|
297
|
+
const projectRoot = await createProjectRoot();
|
|
298
|
+
createdRoots.push(projectRoot);
|
|
299
|
+
await seedState(projectRoot, "created");
|
|
300
|
+
|
|
301
|
+
const outputPath = join(projectRoot, ".agents", "flow", "it_000003_test-plan.md");
|
|
302
|
+
|
|
303
|
+
await withCwd(projectRoot, async () => {
|
|
304
|
+
await expect(
|
|
305
|
+
runCreateTestPlan(
|
|
306
|
+
{ provider: "codex" },
|
|
307
|
+
{
|
|
308
|
+
loadSkillFn: async () => "skill",
|
|
309
|
+
invokeAgentFn: async () => {
|
|
310
|
+
await writeFile(
|
|
311
|
+
outputPath,
|
|
312
|
+
[
|
|
313
|
+
"# Test Plan - Iteration 000003",
|
|
314
|
+
"## User Story: US-001 - Example Story",
|
|
315
|
+
"| Test Case ID | Description | Type (unit/integration/e2e) | Mode (automated/manual) | Expected Result |",
|
|
316
|
+
"|---|---|---|---|---|",
|
|
317
|
+
"| TC-US001-01 | Validate login success | integration | automated | Login succeeds with valid credentials |",
|
|
318
|
+
].join("\n"),
|
|
319
|
+
"utf8",
|
|
320
|
+
);
|
|
321
|
+
return { exitCode: 0, stdout: "", stderr: "" };
|
|
322
|
+
},
|
|
323
|
+
},
|
|
324
|
+
),
|
|
325
|
+
).rejects.toThrow(
|
|
326
|
+
"Generated test plan does not satisfy traceability requirements for the test-plan schema.",
|
|
327
|
+
);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
const state = await readState(projectRoot);
|
|
331
|
+
expect(state.phases.prototype.test_plan.status).toBe("pending");
|
|
332
|
+
expect(state.phases.prototype.test_plan.file).toBeNull();
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
describe("create-test-plan skill definition", () => {
|
|
337
|
+
test("has YAML frontmatter and required automation-first instructions", async () => {
|
|
338
|
+
const skillPath = join(
|
|
339
|
+
process.cwd(),
|
|
340
|
+
".agents",
|
|
341
|
+
"skills",
|
|
342
|
+
"create-test-plan",
|
|
343
|
+
"SKILL.md",
|
|
344
|
+
);
|
|
345
|
+
const source = await readFile(skillPath, "utf8");
|
|
346
|
+
|
|
347
|
+
expect(source.startsWith("---\n")).toBe(true);
|
|
348
|
+
expect(source).toContain("name: create-test-plan");
|
|
349
|
+
expect(source).toContain("description:");
|
|
350
|
+
expect(source).toContain("user-invocable: true");
|
|
351
|
+
expect(source).toContain("Read these first to understand what must be tested:");
|
|
352
|
+
expect(source).toContain("`it_{iteration}_PRD.json`");
|
|
353
|
+
expect(source).toContain("`.agents/PROJECT_CONTEXT.md`");
|
|
354
|
+
expect(source).toContain("structured by user story");
|
|
355
|
+
expect(source).toContain("| Test Case ID | Description | Type (unit/integration/e2e) | Mode (automated/manual) | Correlated Requirements (US-XXX, FR-X) | Expected Result |");
|
|
356
|
+
expect(source).toContain("Every functional requirement (`FR-N`) must have automated coverage.");
|
|
357
|
+
expect(source).toContain("Every functional requirement (`FR-N`) must appear in at least one test case `Correlated Requirements` field.");
|
|
358
|
+
expect(source).toContain("`Correlated Requirements` with at least one requirement ID (`US-XXX`, `FR-X`)");
|
|
359
|
+
expect(source).toContain(
|
|
360
|
+
"Manual tests are allowed only for UI/UX nuances that cannot be reliably validated through DOM/state assertions",
|
|
361
|
+
);
|
|
362
|
+
expect(source).toContain("`.agents/flow/it_{iteration}_test-plan.md`");
|
|
363
|
+
});
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
describe("parseTestPlanForValidation", () => {
|
|
367
|
+
test("maps requirement traceability into schema-compatible test items", () => {
|
|
368
|
+
const parsed = parseTestPlanForValidation(
|
|
369
|
+
[
|
|
370
|
+
"# Test Plan - Iteration 000003",
|
|
371
|
+
"| Test Case ID | Description | Type (unit/integration/e2e) | Mode (automated/manual) | Correlated Requirements (US-XXX, FR-X) | Expected Result |",
|
|
372
|
+
"|---|---|---|---|---|---|",
|
|
373
|
+
"| TC-US001-01 | Validate login success | integration | automated | US-001, FR-1 | Login succeeds with valid credentials |",
|
|
374
|
+
].join("\n"),
|
|
375
|
+
);
|
|
376
|
+
|
|
377
|
+
expect(parsed.overallStatus).toBe("pending");
|
|
378
|
+
expect(parsed.automatedTests).toHaveLength(1);
|
|
379
|
+
expect(parsed.automatedTests[0]?.correlatedRequirements).toEqual(["US-001", "FR-1"]);
|
|
380
|
+
});
|
|
381
|
+
});
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { mkdir, readFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { createInterface } from "node:readline/promises";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
buildPrompt,
|
|
7
|
+
invokeAgent,
|
|
8
|
+
loadSkill,
|
|
9
|
+
type AgentInvokeOptions,
|
|
10
|
+
type AgentProvider,
|
|
11
|
+
type AgentResult,
|
|
12
|
+
} from "../agent";
|
|
13
|
+
import { exists, FLOW_REL_DIR, readState, writeState } from "../state";
|
|
14
|
+
import { TestPlanSchema, type TestPlan } from "../../schemas/test-plan";
|
|
15
|
+
|
|
16
|
+
export interface CreateTestPlanOptions {
|
|
17
|
+
provider: AgentProvider;
|
|
18
|
+
force?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface CreateTestPlanDeps {
|
|
22
|
+
confirmOverwriteFn: (question: string) => Promise<boolean>;
|
|
23
|
+
existsFn: (path: string) => Promise<boolean>;
|
|
24
|
+
invokeAgentFn: (options: AgentInvokeOptions) => Promise<AgentResult>;
|
|
25
|
+
loadSkillFn: (projectRoot: string, skillName: string) => Promise<string>;
|
|
26
|
+
mkdirFn: typeof mkdir;
|
|
27
|
+
nowFn: () => Date;
|
|
28
|
+
readFileFn: typeof readFile;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const defaultDeps: CreateTestPlanDeps = {
|
|
32
|
+
confirmOverwriteFn: promptForConfirmation,
|
|
33
|
+
existsFn: exists,
|
|
34
|
+
invokeAgentFn: invokeAgent,
|
|
35
|
+
loadSkillFn: loadSkill,
|
|
36
|
+
mkdirFn: mkdir,
|
|
37
|
+
nowFn: () => new Date(),
|
|
38
|
+
readFileFn: readFile,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
function parseRequirements(cell: string): string[] {
|
|
42
|
+
return cell
|
|
43
|
+
.split(",")
|
|
44
|
+
.map((entry) => entry.trim())
|
|
45
|
+
.filter((entry) => /^(US-\d{3}|FR-\d+)$/i.test(entry))
|
|
46
|
+
.map((entry) => entry.toUpperCase());
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function parseTestPlanForValidation(markdown: string): TestPlan {
|
|
50
|
+
const scope: string[] = [];
|
|
51
|
+
const environmentData: string[] = [];
|
|
52
|
+
const automatedTests: TestPlan["automatedTests"] = [];
|
|
53
|
+
const exploratoryManualTests: TestPlan["exploratoryManualTests"] = [];
|
|
54
|
+
|
|
55
|
+
type Section = "scope" | "environmentData" | null;
|
|
56
|
+
let currentSection: Section = null;
|
|
57
|
+
let inTable = false;
|
|
58
|
+
|
|
59
|
+
for (const line of markdown.split("\n")) {
|
|
60
|
+
const trimmed = line.trim();
|
|
61
|
+
|
|
62
|
+
if (/^##\s+Scope$/i.test(trimmed)) {
|
|
63
|
+
currentSection = "scope";
|
|
64
|
+
inTable = false;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (/^##\s+Environment\s*(?:and|&)\s*data$/i.test(trimmed)) {
|
|
68
|
+
currentSection = "environmentData";
|
|
69
|
+
inTable = false;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (
|
|
74
|
+
trimmed.startsWith("|")
|
|
75
|
+
&& trimmed.includes("Test Case ID")
|
|
76
|
+
&& trimmed.includes("Correlated Requirements")
|
|
77
|
+
) {
|
|
78
|
+
inTable = true;
|
|
79
|
+
currentSection = null;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (inTable && trimmed.startsWith("|")) {
|
|
84
|
+
if (trimmed.includes("---|")) continue;
|
|
85
|
+
|
|
86
|
+
const cells = trimmed
|
|
87
|
+
.split("|")
|
|
88
|
+
.map((cell) => cell.trim())
|
|
89
|
+
.filter((cell, index, all) => index > 0 && index < all.length - 1);
|
|
90
|
+
|
|
91
|
+
if (cells.length >= 6) {
|
|
92
|
+
const [id, description, , mode, correlatedRequirementsCell] = cells;
|
|
93
|
+
if (id === "Test Case ID") continue;
|
|
94
|
+
|
|
95
|
+
const item = {
|
|
96
|
+
id,
|
|
97
|
+
description,
|
|
98
|
+
status: "pending" as const,
|
|
99
|
+
correlatedRequirements: parseRequirements(correlatedRequirementsCell),
|
|
100
|
+
};
|
|
101
|
+
if (mode.toLowerCase().includes("automated")) {
|
|
102
|
+
automatedTests.push(item);
|
|
103
|
+
} else {
|
|
104
|
+
exploratoryManualTests.push(item);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (inTable && trimmed.length === 0) {
|
|
111
|
+
inTable = false;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (!currentSection || trimmed.length === 0 || /^<!--/.test(trimmed)) {
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const bulletMatch = trimmed.match(/^[-*]\s+(.+)$/);
|
|
120
|
+
const value = bulletMatch ? bulletMatch[1].trim() : trimmed;
|
|
121
|
+
if (!value) continue;
|
|
122
|
+
|
|
123
|
+
if (currentSection === "scope") scope.push(value);
|
|
124
|
+
if (currentSection === "environmentData") environmentData.push(value);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
overallStatus: "pending",
|
|
129
|
+
scope,
|
|
130
|
+
environmentData,
|
|
131
|
+
automatedTests,
|
|
132
|
+
exploratoryManualTests,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export async function promptForConfirmation(question: string): Promise<boolean> {
|
|
137
|
+
const readline = createInterface({
|
|
138
|
+
input: process.stdin,
|
|
139
|
+
output: process.stdout,
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
const answer = (await readline.question(question)).trim();
|
|
144
|
+
return /^y(?:es)?$/i.test(answer);
|
|
145
|
+
} finally {
|
|
146
|
+
readline.close();
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export async function runCreateTestPlan(
|
|
151
|
+
opts: CreateTestPlanOptions,
|
|
152
|
+
deps: Partial<CreateTestPlanDeps> = {},
|
|
153
|
+
): Promise<void> {
|
|
154
|
+
const projectRoot = process.cwd();
|
|
155
|
+
const state = await readState(projectRoot);
|
|
156
|
+
const mergedDeps: CreateTestPlanDeps = { ...defaultDeps, ...deps };
|
|
157
|
+
|
|
158
|
+
if (state.phases.prototype.project_context.status !== "created") {
|
|
159
|
+
throw new Error(
|
|
160
|
+
"Cannot create test plan: prototype.project_context.status must be created. Run `bun nvst approve project-context` first.",
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const iteration = state.current_iteration;
|
|
165
|
+
const fileName = `it_${iteration}_test-plan.md`;
|
|
166
|
+
const flowDir = join(projectRoot, FLOW_REL_DIR);
|
|
167
|
+
const outputPath = join(flowDir, fileName);
|
|
168
|
+
|
|
169
|
+
await mergedDeps.mkdirFn(flowDir, { recursive: true });
|
|
170
|
+
|
|
171
|
+
if ((await mergedDeps.existsFn(outputPath)) && !opts.force) {
|
|
172
|
+
const shouldOverwrite = await mergedDeps.confirmOverwriteFn(
|
|
173
|
+
`Test plan file already exists at ${join(FLOW_REL_DIR, fileName)}. Overwrite? [y/N] `,
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
if (!shouldOverwrite) {
|
|
177
|
+
console.log("Test plan creation cancelled.");
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
let skillBody: string;
|
|
183
|
+
try {
|
|
184
|
+
skillBody = await mergedDeps.loadSkillFn(projectRoot, "create-test-plan");
|
|
185
|
+
} catch {
|
|
186
|
+
throw new Error(
|
|
187
|
+
"Required skill missing: expected .agents/skills/create-test-plan/SKILL.md.",
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const projectContextPath = join(projectRoot, ".agents", "PROJECT_CONTEXT.md");
|
|
192
|
+
if (!(await mergedDeps.existsFn(projectContextPath))) {
|
|
193
|
+
throw new Error("Project context missing: expected .agents/PROJECT_CONTEXT.md.");
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const projectContextContent = await mergedDeps.readFileFn(projectContextPath, "utf8");
|
|
197
|
+
const prompt = buildPrompt(skillBody, {
|
|
198
|
+
iteration,
|
|
199
|
+
project_context: projectContextContent,
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
const result = await mergedDeps.invokeAgentFn({
|
|
203
|
+
provider: opts.provider,
|
|
204
|
+
prompt,
|
|
205
|
+
cwd: projectRoot,
|
|
206
|
+
interactive: true,
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
if (result.exitCode !== 0) {
|
|
210
|
+
throw new Error(`Agent invocation failed with exit code ${result.exitCode}.`);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (!(await mergedDeps.existsFn(outputPath)) && result.stdout.trim().length > 0) {
|
|
214
|
+
const content = result.stdout.endsWith("\n") ? result.stdout : `${result.stdout}\n`;
|
|
215
|
+
await Bun.write(outputPath, content);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (!(await mergedDeps.existsFn(outputPath))) {
|
|
219
|
+
throw new Error(
|
|
220
|
+
`Test plan generation did not produce ${join(FLOW_REL_DIR, fileName)}.`,
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const generatedMarkdown = await mergedDeps.readFileFn(outputPath, "utf8");
|
|
225
|
+
const parsed = parseTestPlanForValidation(generatedMarkdown);
|
|
226
|
+
const totalTestCases = parsed.automatedTests.length + parsed.exploratoryManualTests.length;
|
|
227
|
+
if (totalTestCases === 0) {
|
|
228
|
+
throw new Error(
|
|
229
|
+
"Generated test plan does not satisfy traceability requirements for the test-plan schema.",
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
const validation = TestPlanSchema.safeParse(parsed);
|
|
233
|
+
if (!validation.success) {
|
|
234
|
+
throw new Error(
|
|
235
|
+
"Generated test plan does not satisfy traceability requirements for the test-plan schema.",
|
|
236
|
+
{ cause: validation.error },
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
state.phases.prototype.test_plan.status = "pending_approval";
|
|
241
|
+
state.phases.prototype.test_plan.file = fileName;
|
|
242
|
+
state.last_updated = mergedDeps.nowFn().toISOString();
|
|
243
|
+
state.updated_by = "nvst:create-test-plan";
|
|
244
|
+
|
|
245
|
+
await writeState(projectRoot, state);
|
|
246
|
+
|
|
247
|
+
console.log("Test plan generated and marked as pending approval.");
|
|
248
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { buildPrompt, invokeAgent, loadSkill, type AgentProvider } from "../agent";
|
|
2
|
+
import { readState, writeState } from "../state";
|
|
3
|
+
|
|
4
|
+
export interface DefineRequirementOptions {
|
|
5
|
+
provider: AgentProvider;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export async function runDefineRequirement(opts: DefineRequirementOptions): Promise<void> {
|
|
9
|
+
const { provider } = opts;
|
|
10
|
+
const projectRoot = process.cwd();
|
|
11
|
+
const state = await readState(projectRoot);
|
|
12
|
+
|
|
13
|
+
if (state.current_phase !== "define") {
|
|
14
|
+
throw new Error("Cannot define requirement: current_phase must be 'define'.");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const requirementDefinition = state.phases.define.requirement_definition;
|
|
18
|
+
if (requirementDefinition.status !== "pending") {
|
|
19
|
+
throw new Error(
|
|
20
|
+
`Cannot define requirement from status '${requirementDefinition.status}'. Expected pending.`,
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const skillBody = await loadSkill(projectRoot, "create-pr-document");
|
|
25
|
+
const prompt = buildPrompt(skillBody, {
|
|
26
|
+
current_iteration: state.current_iteration,
|
|
27
|
+
});
|
|
28
|
+
const result = await invokeAgent({
|
|
29
|
+
provider,
|
|
30
|
+
prompt,
|
|
31
|
+
cwd: projectRoot,
|
|
32
|
+
interactive: true,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
if (result.exitCode !== 0) {
|
|
36
|
+
throw new Error(`Agent invocation failed with exit code ${result.exitCode}.`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
requirementDefinition.status = "in_progress";
|
|
40
|
+
requirementDefinition.file = `it_${state.current_iteration}_product-requirement-document.md`;
|
|
41
|
+
state.last_updated = new Date().toISOString();
|
|
42
|
+
state.updated_by = "nvst:define-requirement";
|
|
43
|
+
|
|
44
|
+
await writeState(projectRoot, state);
|
|
45
|
+
|
|
46
|
+
console.log("Requirement definition started and marked as in progress.");
|
|
47
|
+
}
|