@quinteroac/agents-coding-toolkit 0.1.0-preview → 0.2.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 +29 -15
- package/package.json +14 -4
- package/scaffold/.agents/flow/tmpl_it_000001_progress.example.json +20 -0
- package/scaffold/.agents/skills/execute-refactor-item/tmpl_SKILL.md +59 -0
- package/scaffold/.agents/skills/plan-refactor/tmpl_SKILL.md +89 -9
- package/scaffold/.agents/skills/refine-refactor-plan/tmpl_SKILL.md +30 -0
- package/scaffold/.agents/tmpl_state_rules.md +0 -1
- package/scaffold/schemas/tmpl_prototype-progress.ts +22 -0
- package/scaffold/schemas/tmpl_refactor-execution-progress.ts +16 -0
- package/scaffold/schemas/tmpl_refactor-prd.ts +14 -0
- package/scaffold/schemas/tmpl_state.ts +1 -0
- package/scaffold/schemas/tmpl_test-execution-progress.ts +17 -0
- package/schemas/issues.ts +19 -0
- package/schemas/prototype-progress.ts +22 -0
- package/schemas/refactor-execution-progress.ts +16 -0
- package/schemas/refactor-prd.ts +14 -0
- package/schemas/state.test.ts +58 -0
- package/schemas/state.ts +1 -0
- package/schemas/test-execution-progress.ts +17 -0
- package/schemas/test-plan.test.ts +1 -1
- package/schemas/validate-progress.ts +1 -1
- package/schemas/validate-state.ts +1 -1
- package/src/cli.test.ts +57 -0
- package/src/cli.ts +227 -58
- package/src/commands/approve-project-context.ts +13 -6
- package/src/commands/approve-prototype.test.ts +427 -0
- package/src/commands/approve-prototype.ts +185 -0
- package/src/commands/approve-refactor-plan.test.ts +254 -0
- package/src/commands/approve-refactor-plan.ts +200 -0
- package/src/commands/approve-requirement.test.ts +224 -0
- package/src/commands/approve-requirement.ts +75 -16
- package/src/commands/approve-test-plan.test.ts +2 -2
- package/src/commands/approve-test-plan.ts +21 -7
- package/src/commands/create-issue.test.ts +2 -2
- package/src/commands/create-project-context.ts +31 -25
- package/src/commands/create-prototype.test.ts +488 -18
- package/src/commands/create-prototype.ts +185 -63
- package/src/commands/create-test-plan.ts +8 -6
- package/src/commands/define-refactor-plan.test.ts +208 -0
- package/src/commands/define-refactor-plan.ts +96 -0
- package/src/commands/define-requirement.ts +15 -9
- package/src/commands/execute-automated-fix.test.ts +78 -33
- package/src/commands/execute-automated-fix.ts +34 -101
- package/src/commands/execute-refactor.test.ts +954 -0
- package/src/commands/execute-refactor.ts +332 -0
- package/src/commands/execute-test-plan.test.ts +24 -16
- package/src/commands/execute-test-plan.ts +29 -55
- package/src/commands/flow-config.ts +79 -0
- package/src/commands/flow.test.ts +755 -0
- package/src/commands/flow.ts +405 -0
- package/src/commands/refine-project-context.ts +9 -7
- package/src/commands/refine-refactor-plan.test.ts +210 -0
- package/src/commands/refine-refactor-plan.ts +95 -0
- package/src/commands/refine-requirement.ts +9 -6
- package/src/commands/refine-test-plan.test.ts +2 -2
- package/src/commands/refine-test-plan.ts +9 -6
- package/src/commands/start-iteration.test.ts +52 -0
- package/src/commands/start-iteration.ts +5 -0
- package/src/commands/write-json.ts +102 -97
- package/src/flow-cli.test.ts +18 -0
- package/src/force-flag.test.ts +144 -0
- package/src/guardrail.test.ts +411 -0
- package/src/guardrail.ts +82 -0
- package/src/install.test.ts +7 -5
- package/src/pack.test.ts +2 -1
- package/src/progress-utils.ts +34 -0
- package/src/readline.ts +23 -0
- package/src/write-json-artifact.ts +33 -0
- package/scaffold/.agents/flow/tmpl_README.md +0 -7
- package/scaffold/.agents/flow/tmpl_iteration_close_checklist.example.md +0 -11
- package/schemas/test-plan.ts +0 -20
|
@@ -0,0 +1,954 @@
|
|
|
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 { runExecuteRefactor, RefactorExecutionProgressSchema, buildRefactorExecutionReport } from "./execute-refactor";
|
|
9
|
+
|
|
10
|
+
async function createProjectRoot(): Promise<string> {
|
|
11
|
+
return mkdtemp(join(tmpdir(), "nvst-execute-refactor-"));
|
|
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(
|
|
25
|
+
projectRoot: string,
|
|
26
|
+
opts: {
|
|
27
|
+
phase?: "define" | "prototype" | "refactor";
|
|
28
|
+
refactorPlanStatus?: "pending" | "pending_approval" | "approved";
|
|
29
|
+
refactorExecutionStatus?: "pending" | "in_progress" | "completed";
|
|
30
|
+
iteration?: string;
|
|
31
|
+
} = {},
|
|
32
|
+
) {
|
|
33
|
+
const {
|
|
34
|
+
phase = "refactor",
|
|
35
|
+
refactorPlanStatus = "approved",
|
|
36
|
+
refactorExecutionStatus = "pending",
|
|
37
|
+
iteration = "000013",
|
|
38
|
+
} = opts;
|
|
39
|
+
|
|
40
|
+
await mkdir(join(projectRoot, ".agents", "flow"), { recursive: true });
|
|
41
|
+
await writeState(projectRoot, {
|
|
42
|
+
current_iteration: iteration,
|
|
43
|
+
current_phase: phase,
|
|
44
|
+
phases: {
|
|
45
|
+
define: {
|
|
46
|
+
requirement_definition: { status: "approved", file: `it_${iteration}_product-requirement-document.md` },
|
|
47
|
+
prd_generation: { status: "completed", file: `it_${iteration}_PRD.json` },
|
|
48
|
+
},
|
|
49
|
+
prototype: {
|
|
50
|
+
project_context: { status: "created", file: ".agents/PROJECT_CONTEXT.md" },
|
|
51
|
+
test_plan: { status: "created", file: `it_${iteration}_test-plan.md` },
|
|
52
|
+
tp_generation: { status: "created", file: `it_${iteration}_TP.json` },
|
|
53
|
+
prototype_build: { status: "created", file: `it_${iteration}_progress.json` },
|
|
54
|
+
test_execution: { status: "completed", file: `it_${iteration}_test-execution-report.json` },
|
|
55
|
+
prototype_approved: true,
|
|
56
|
+
},
|
|
57
|
+
refactor: {
|
|
58
|
+
evaluation_report: { status: "created", file: `it_${iteration}_evaluation-report.md` },
|
|
59
|
+
refactor_plan: { status: refactorPlanStatus, file: refactorPlanStatus === "approved" ? `it_${iteration}_refactor-plan.md` : null },
|
|
60
|
+
refactor_execution: { status: refactorExecutionStatus, file: null },
|
|
61
|
+
changelog: { status: "pending", file: null },
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
last_updated: "2026-02-26T00:00:00.000Z",
|
|
65
|
+
updated_by: "seed",
|
|
66
|
+
history: [],
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function writeRefactorPrd(
|
|
71
|
+
projectRoot: string,
|
|
72
|
+
iteration: string,
|
|
73
|
+
items: Array<{ id: string; title: string; description: string; rationale: string }>,
|
|
74
|
+
) {
|
|
75
|
+
const fileName = `it_${iteration}_refactor-prd.json`;
|
|
76
|
+
const filePath = join(projectRoot, ".agents", "flow", fileName);
|
|
77
|
+
await writeFile(
|
|
78
|
+
filePath,
|
|
79
|
+
`${JSON.stringify({ refactorItems: items }, null, 2)}\n`,
|
|
80
|
+
"utf8",
|
|
81
|
+
);
|
|
82
|
+
return fileName;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function makeAgentResult(exitCode: number): AgentResult {
|
|
86
|
+
return { exitCode, stdout: "", stderr: "" };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function makeSkillFn(content = "# Execute Refactor Item\nApply the refactor item.") {
|
|
90
|
+
return async (_projectRoot: string, _skillName: string) => content;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const createdRoots: string[] = [];
|
|
94
|
+
|
|
95
|
+
afterEach(async () => {
|
|
96
|
+
process.exitCode = 0;
|
|
97
|
+
await Promise.all(createdRoots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe("execute refactor command", () => {
|
|
101
|
+
test("registers execute refactor command in CLI dispatch", async () => {
|
|
102
|
+
const source = await readFile(join(process.cwd(), "src", "cli.ts"), "utf8");
|
|
103
|
+
|
|
104
|
+
expect(source).toContain('import { runExecuteRefactor } from "./commands/execute-refactor";');
|
|
105
|
+
expect(source).toContain('if (subcommand === "refactor") {');
|
|
106
|
+
expect(source).toContain("await runExecuteRefactor({ provider, force });");
|
|
107
|
+
expect(source).toContain("execute refactor --agent <provider>");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// AC02: Rejects if current_phase !== "refactor"
|
|
111
|
+
test("rejects with error when current_phase is not refactor", async () => {
|
|
112
|
+
const projectRoot = await createProjectRoot();
|
|
113
|
+
createdRoots.push(projectRoot);
|
|
114
|
+
await seedState(projectRoot, { phase: "prototype" });
|
|
115
|
+
|
|
116
|
+
await withCwd(projectRoot, async () => {
|
|
117
|
+
await expect(
|
|
118
|
+
runExecuteRefactor(
|
|
119
|
+
{ provider: "claude" },
|
|
120
|
+
{ loadSkillFn: makeSkillFn() },
|
|
121
|
+
),
|
|
122
|
+
).rejects.toThrow("Cannot execute refactor: current_phase must be 'refactor'. Current phase: 'prototype'.");
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// AC03: Rejects if refactor_plan.status !== "approved"
|
|
127
|
+
test("rejects with error when refactor_plan.status is not approved", async () => {
|
|
128
|
+
const projectRoot = await createProjectRoot();
|
|
129
|
+
createdRoots.push(projectRoot);
|
|
130
|
+
await seedState(projectRoot, { refactorPlanStatus: "pending" });
|
|
131
|
+
|
|
132
|
+
await withCwd(projectRoot, async () => {
|
|
133
|
+
await expect(
|
|
134
|
+
runExecuteRefactor(
|
|
135
|
+
{ provider: "claude" },
|
|
136
|
+
{ loadSkillFn: makeSkillFn() },
|
|
137
|
+
),
|
|
138
|
+
).rejects.toThrow("Cannot execute refactor: refactor_plan.status must be 'approved'. Current status: 'pending'.");
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// AC03: pending_approval variant
|
|
143
|
+
test("rejects with error when refactor_plan.status is pending_approval", async () => {
|
|
144
|
+
const projectRoot = await createProjectRoot();
|
|
145
|
+
createdRoots.push(projectRoot);
|
|
146
|
+
await seedState(projectRoot, { refactorPlanStatus: "pending_approval" });
|
|
147
|
+
|
|
148
|
+
await withCwd(projectRoot, async () => {
|
|
149
|
+
await expect(
|
|
150
|
+
runExecuteRefactor(
|
|
151
|
+
{ provider: "claude" },
|
|
152
|
+
{ loadSkillFn: makeSkillFn() },
|
|
153
|
+
),
|
|
154
|
+
).rejects.toThrow("Cannot execute refactor: refactor_plan.status must be 'approved'. Current status: 'pending_approval'.");
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// AC04: Rejects if refactor_execution.status is already "completed"
|
|
159
|
+
test("rejects with error when refactor_execution.status is already completed", async () => {
|
|
160
|
+
const projectRoot = await createProjectRoot();
|
|
161
|
+
createdRoots.push(projectRoot);
|
|
162
|
+
await seedState(projectRoot, { refactorExecutionStatus: "completed" });
|
|
163
|
+
|
|
164
|
+
await withCwd(projectRoot, async () => {
|
|
165
|
+
await expect(
|
|
166
|
+
runExecuteRefactor(
|
|
167
|
+
{ provider: "claude" },
|
|
168
|
+
{ loadSkillFn: makeSkillFn() },
|
|
169
|
+
),
|
|
170
|
+
).rejects.toThrow("Cannot execute refactor: refactor_execution.status is already 'completed'.");
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// AC05: Rejects if refactor-prd.json is missing
|
|
175
|
+
test("rejects with error when refactor-prd.json is missing", async () => {
|
|
176
|
+
const projectRoot = await createProjectRoot();
|
|
177
|
+
createdRoots.push(projectRoot);
|
|
178
|
+
await seedState(projectRoot);
|
|
179
|
+
|
|
180
|
+
await withCwd(projectRoot, async () => {
|
|
181
|
+
await expect(
|
|
182
|
+
runExecuteRefactor(
|
|
183
|
+
{ provider: "claude" },
|
|
184
|
+
{ loadSkillFn: makeSkillFn() },
|
|
185
|
+
),
|
|
186
|
+
).rejects.toThrow("Refactor PRD file missing: expected .agents/flow/it_000013_refactor-prd.json.");
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// AC05: Rejects on invalid JSON
|
|
191
|
+
test("rejects with error when refactor-prd.json contains invalid JSON", async () => {
|
|
192
|
+
const projectRoot = await createProjectRoot();
|
|
193
|
+
createdRoots.push(projectRoot);
|
|
194
|
+
await seedState(projectRoot);
|
|
195
|
+
const prdPath = join(projectRoot, ".agents", "flow", "it_000013_refactor-prd.json");
|
|
196
|
+
await writeFile(prdPath, "not-valid-json", "utf8");
|
|
197
|
+
|
|
198
|
+
await withCwd(projectRoot, async () => {
|
|
199
|
+
await expect(
|
|
200
|
+
runExecuteRefactor(
|
|
201
|
+
{ provider: "claude" },
|
|
202
|
+
{ loadSkillFn: makeSkillFn() },
|
|
203
|
+
),
|
|
204
|
+
).rejects.toThrow("Invalid refactor PRD JSON in .agents/flow/it_000013_refactor-prd.json.");
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
// AC05: Rejects on schema mismatch
|
|
209
|
+
test("rejects with error when refactor-prd.json fails schema validation", async () => {
|
|
210
|
+
const projectRoot = await createProjectRoot();
|
|
211
|
+
createdRoots.push(projectRoot);
|
|
212
|
+
await seedState(projectRoot);
|
|
213
|
+
const prdPath = join(projectRoot, ".agents", "flow", "it_000013_refactor-prd.json");
|
|
214
|
+
await writeFile(prdPath, JSON.stringify({ refactorItems: [] }), "utf8");
|
|
215
|
+
|
|
216
|
+
await withCwd(projectRoot, async () => {
|
|
217
|
+
await expect(
|
|
218
|
+
runExecuteRefactor(
|
|
219
|
+
{ provider: "claude" },
|
|
220
|
+
{ loadSkillFn: makeSkillFn() },
|
|
221
|
+
),
|
|
222
|
+
).rejects.toThrow("Refactor PRD schema mismatch in .agents/flow/it_000013_refactor-prd.json.");
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// AC06: Sets refactor_execution.status = "in_progress" before processing
|
|
227
|
+
test("sets refactor_execution.status to in_progress before invoking agent", async () => {
|
|
228
|
+
const projectRoot = await createProjectRoot();
|
|
229
|
+
createdRoots.push(projectRoot);
|
|
230
|
+
await seedState(projectRoot);
|
|
231
|
+
await writeRefactorPrd(projectRoot, "000013", [
|
|
232
|
+
{ id: "RI-001", title: "Refactor A", description: "Do A", rationale: "Because A" },
|
|
233
|
+
]);
|
|
234
|
+
|
|
235
|
+
let statusBeforeAgent: string | undefined;
|
|
236
|
+
|
|
237
|
+
await withCwd(projectRoot, async () => {
|
|
238
|
+
await runExecuteRefactor(
|
|
239
|
+
{ provider: "claude" },
|
|
240
|
+
{
|
|
241
|
+
loadSkillFn: makeSkillFn(),
|
|
242
|
+
invokeAgentFn: async () => {
|
|
243
|
+
const s = await readState(projectRoot);
|
|
244
|
+
statusBeforeAgent = s.phases.refactor.refactor_execution.status;
|
|
245
|
+
return makeAgentResult(0);
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
expect(statusBeforeAgent).toBe("in_progress");
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// AC07: Invokes agent with prompt built from skill and item fields
|
|
255
|
+
test("invokes agent with prompt containing refactor item fields", async () => {
|
|
256
|
+
const projectRoot = await createProjectRoot();
|
|
257
|
+
createdRoots.push(projectRoot);
|
|
258
|
+
await seedState(projectRoot);
|
|
259
|
+
await writeRefactorPrd(projectRoot, "000013", [
|
|
260
|
+
{ id: "RI-001", title: "My Title", description: "My Description", rationale: "My Rationale" },
|
|
261
|
+
]);
|
|
262
|
+
|
|
263
|
+
const capturedPrompts: string[] = [];
|
|
264
|
+
|
|
265
|
+
await withCwd(projectRoot, async () => {
|
|
266
|
+
await runExecuteRefactor(
|
|
267
|
+
{ provider: "claude" },
|
|
268
|
+
{
|
|
269
|
+
loadSkillFn: makeSkillFn("SKILL_BODY"),
|
|
270
|
+
invokeAgentFn: async (opts) => {
|
|
271
|
+
capturedPrompts.push(opts.prompt);
|
|
272
|
+
return makeAgentResult(0);
|
|
273
|
+
},
|
|
274
|
+
},
|
|
275
|
+
);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
expect(capturedPrompts).toHaveLength(1);
|
|
279
|
+
expect(capturedPrompts[0]).toContain("SKILL_BODY");
|
|
280
|
+
expect(capturedPrompts[0]).toContain("RI-001");
|
|
281
|
+
expect(capturedPrompts[0]).toContain("My Title");
|
|
282
|
+
expect(capturedPrompts[0]).toContain("My Description");
|
|
283
|
+
expect(capturedPrompts[0]).toContain("My Rationale");
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
// US-002-AC01: Agent invoked in non-interactive mode
|
|
287
|
+
test("invokes agent with interactive: false (non-interactive mode)", async () => {
|
|
288
|
+
const projectRoot = await createProjectRoot();
|
|
289
|
+
createdRoots.push(projectRoot);
|
|
290
|
+
await seedState(projectRoot);
|
|
291
|
+
await writeRefactorPrd(projectRoot, "000013", [
|
|
292
|
+
{ id: "RI-001", title: "T", description: "D", rationale: "R" },
|
|
293
|
+
]);
|
|
294
|
+
|
|
295
|
+
const capturedOptions: Array<{ interactive?: boolean; provider: string }> = [];
|
|
296
|
+
|
|
297
|
+
await withCwd(projectRoot, async () => {
|
|
298
|
+
await runExecuteRefactor(
|
|
299
|
+
{ provider: "codex" },
|
|
300
|
+
{
|
|
301
|
+
loadSkillFn: makeSkillFn(),
|
|
302
|
+
invokeAgentFn: async (opts) => {
|
|
303
|
+
capturedOptions.push({ interactive: opts.interactive, provider: opts.provider });
|
|
304
|
+
return makeAgentResult(0);
|
|
305
|
+
},
|
|
306
|
+
},
|
|
307
|
+
);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
expect(capturedOptions).toHaveLength(1);
|
|
311
|
+
expect(capturedOptions[0].interactive).toBe(false);
|
|
312
|
+
expect(capturedOptions[0].provider).toBe("codex");
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
// AC09 & AC10: Records result after each invocation, continues on failure
|
|
316
|
+
test("records completed status and continues after each successful item", async () => {
|
|
317
|
+
const projectRoot = await createProjectRoot();
|
|
318
|
+
createdRoots.push(projectRoot);
|
|
319
|
+
await seedState(projectRoot);
|
|
320
|
+
await writeRefactorPrd(projectRoot, "000013", [
|
|
321
|
+
{ id: "RI-001", title: "T1", description: "D1", rationale: "R1" },
|
|
322
|
+
{ id: "RI-002", title: "T2", description: "D2", rationale: "R2" },
|
|
323
|
+
]);
|
|
324
|
+
|
|
325
|
+
const agentCallOrder: string[] = [];
|
|
326
|
+
|
|
327
|
+
await withCwd(projectRoot, async () => {
|
|
328
|
+
await runExecuteRefactor(
|
|
329
|
+
{ provider: "claude" },
|
|
330
|
+
{
|
|
331
|
+
loadSkillFn: makeSkillFn(),
|
|
332
|
+
invokeAgentFn: async (opts) => {
|
|
333
|
+
agentCallOrder.push(opts.prompt.includes("RI-001") ? "RI-001" : "RI-002");
|
|
334
|
+
return makeAgentResult(0);
|
|
335
|
+
},
|
|
336
|
+
},
|
|
337
|
+
);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
expect(agentCallOrder).toEqual(["RI-001", "RI-002"]);
|
|
341
|
+
|
|
342
|
+
const progressPath = join(projectRoot, ".agents", "flow", "it_000013_refactor-execution-progress.json");
|
|
343
|
+
const progress = RefactorExecutionProgressSchema.parse(
|
|
344
|
+
JSON.parse(await readFile(progressPath, "utf8")),
|
|
345
|
+
);
|
|
346
|
+
expect(progress.entries[0].status).toBe("completed");
|
|
347
|
+
expect(progress.entries[1].status).toBe("completed");
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
// AC10: Non-zero exit code marks item as failed, continues to next
|
|
351
|
+
test("marks item as failed on non-zero exit code and continues to next item", async () => {
|
|
352
|
+
const projectRoot = await createProjectRoot();
|
|
353
|
+
createdRoots.push(projectRoot);
|
|
354
|
+
await seedState(projectRoot);
|
|
355
|
+
await writeRefactorPrd(projectRoot, "000013", [
|
|
356
|
+
{ id: "RI-001", title: "T1", description: "D1", rationale: "R1" },
|
|
357
|
+
{ id: "RI-002", title: "T2", description: "D2", rationale: "R2" },
|
|
358
|
+
]);
|
|
359
|
+
|
|
360
|
+
const agentCallOrder: string[] = [];
|
|
361
|
+
|
|
362
|
+
await withCwd(projectRoot, async () => {
|
|
363
|
+
await runExecuteRefactor(
|
|
364
|
+
{ provider: "claude" },
|
|
365
|
+
{
|
|
366
|
+
loadSkillFn: makeSkillFn(),
|
|
367
|
+
invokeAgentFn: async (opts) => {
|
|
368
|
+
const id = opts.prompt.includes("RI-001") ? "RI-001" : "RI-002";
|
|
369
|
+
agentCallOrder.push(id);
|
|
370
|
+
// RI-001 fails, RI-002 succeeds
|
|
371
|
+
return makeAgentResult(id === "RI-001" ? 1 : 0);
|
|
372
|
+
},
|
|
373
|
+
},
|
|
374
|
+
);
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
// Both items are attempted
|
|
378
|
+
expect(agentCallOrder).toEqual(["RI-001", "RI-002"]);
|
|
379
|
+
|
|
380
|
+
const progressPath = join(projectRoot, ".agents", "flow", "it_000013_refactor-execution-progress.json");
|
|
381
|
+
const progress = RefactorExecutionProgressSchema.parse(
|
|
382
|
+
JSON.parse(await readFile(progressPath, "utf8")),
|
|
383
|
+
);
|
|
384
|
+
expect(progress.entries[0].status).toBe("failed");
|
|
385
|
+
expect(progress.entries[0].last_agent_exit_code).toBe(1);
|
|
386
|
+
expect(progress.entries[1].status).toBe("completed");
|
|
387
|
+
expect(progress.entries[1].last_agent_exit_code).toBe(0);
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
// AC09: Progress file is written after each agent invocation (via write-json)
|
|
391
|
+
test("writes progress file after each agent invocation", async () => {
|
|
392
|
+
const projectRoot = await createProjectRoot();
|
|
393
|
+
createdRoots.push(projectRoot);
|
|
394
|
+
await seedState(projectRoot);
|
|
395
|
+
await writeRefactorPrd(projectRoot, "000013", [
|
|
396
|
+
{ id: "RI-001", title: "T1", description: "D1", rationale: "R1" },
|
|
397
|
+
{ id: "RI-002", title: "T2", description: "D2", rationale: "R2" },
|
|
398
|
+
]);
|
|
399
|
+
|
|
400
|
+
const progressSnapshots: Array<ReturnType<typeof RefactorExecutionProgressSchema.parse>> = [];
|
|
401
|
+
|
|
402
|
+
await withCwd(projectRoot, async () => {
|
|
403
|
+
await runExecuteRefactor(
|
|
404
|
+
{ provider: "claude" },
|
|
405
|
+
{
|
|
406
|
+
loadSkillFn: makeSkillFn(),
|
|
407
|
+
invokeAgentFn: async (opts) => {
|
|
408
|
+
const id = opts.prompt.includes("RI-001") ? "RI-001" : "RI-002";
|
|
409
|
+
return makeAgentResult(id === "RI-001" ? 1 : 0);
|
|
410
|
+
},
|
|
411
|
+
invokeWriteJsonFn: async (_root, schemaName, _outPath, data) => {
|
|
412
|
+
if (schemaName === "refactor-execution-progress") {
|
|
413
|
+
const parsed = JSON.parse(data) as unknown;
|
|
414
|
+
const validation = RefactorExecutionProgressSchema.safeParse(parsed);
|
|
415
|
+
if (validation.success) {
|
|
416
|
+
progressSnapshots.push(validation.data);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
return { exitCode: 0, stderr: "" };
|
|
420
|
+
},
|
|
421
|
+
},
|
|
422
|
+
);
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
// Two snapshots: one per item (after RI-001, after RI-002)
|
|
426
|
+
expect(progressSnapshots.length).toBeGreaterThanOrEqual(2);
|
|
427
|
+
// After RI-001 write: RI-001 failed
|
|
428
|
+
const afterRi001 = progressSnapshots.find((s) =>
|
|
429
|
+
s.entries.some((e) => e.id === "RI-001" && e.status === "failed"),
|
|
430
|
+
);
|
|
431
|
+
expect(afterRi001).toBeDefined();
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
// AC11: All items complete → refactor_execution.status = "completed"
|
|
435
|
+
test("sets refactor_execution.status to completed when all items succeed", async () => {
|
|
436
|
+
const projectRoot = await createProjectRoot();
|
|
437
|
+
createdRoots.push(projectRoot);
|
|
438
|
+
await seedState(projectRoot);
|
|
439
|
+
await writeRefactorPrd(projectRoot, "000013", [
|
|
440
|
+
{ id: "RI-001", title: "T1", description: "D1", rationale: "R1" },
|
|
441
|
+
{ id: "RI-002", title: "T2", description: "D2", rationale: "R2" },
|
|
442
|
+
]);
|
|
443
|
+
|
|
444
|
+
await withCwd(projectRoot, async () => {
|
|
445
|
+
await runExecuteRefactor(
|
|
446
|
+
{ provider: "claude" },
|
|
447
|
+
{
|
|
448
|
+
loadSkillFn: makeSkillFn(),
|
|
449
|
+
invokeAgentFn: async () => makeAgentResult(0),
|
|
450
|
+
},
|
|
451
|
+
);
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
const state = await readState(projectRoot);
|
|
455
|
+
expect(state.phases.refactor.refactor_execution.status).toBe("completed");
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
// AC12: Any failure → status remains "in_progress"
|
|
459
|
+
test("leaves refactor_execution.status as in_progress when any item fails", async () => {
|
|
460
|
+
const projectRoot = await createProjectRoot();
|
|
461
|
+
createdRoots.push(projectRoot);
|
|
462
|
+
await seedState(projectRoot);
|
|
463
|
+
await writeRefactorPrd(projectRoot, "000013", [
|
|
464
|
+
{ id: "RI-001", title: "T1", description: "D1", rationale: "R1" },
|
|
465
|
+
{ id: "RI-002", title: "T2", description: "D2", rationale: "R2" },
|
|
466
|
+
]);
|
|
467
|
+
|
|
468
|
+
await withCwd(projectRoot, async () => {
|
|
469
|
+
await runExecuteRefactor(
|
|
470
|
+
{ provider: "claude" },
|
|
471
|
+
{
|
|
472
|
+
loadSkillFn: makeSkillFn(),
|
|
473
|
+
invokeAgentFn: async (opts) => {
|
|
474
|
+
// RI-001 fails
|
|
475
|
+
return makeAgentResult(opts.prompt.includes("RI-001") ? 1 : 0);
|
|
476
|
+
},
|
|
477
|
+
},
|
|
478
|
+
);
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
const state = await readState(projectRoot);
|
|
482
|
+
expect(state.phases.refactor.refactor_execution.status).toBe("in_progress");
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
// AC13: refactor_execution.file is set to progress file name
|
|
486
|
+
test("sets refactor_execution.file to the progress file name", async () => {
|
|
487
|
+
const projectRoot = await createProjectRoot();
|
|
488
|
+
createdRoots.push(projectRoot);
|
|
489
|
+
await seedState(projectRoot);
|
|
490
|
+
await writeRefactorPrd(projectRoot, "000013", [
|
|
491
|
+
{ id: "RI-001", title: "T1", description: "D1", rationale: "R1" },
|
|
492
|
+
]);
|
|
493
|
+
|
|
494
|
+
await withCwd(projectRoot, async () => {
|
|
495
|
+
await runExecuteRefactor(
|
|
496
|
+
{ provider: "claude" },
|
|
497
|
+
{
|
|
498
|
+
loadSkillFn: makeSkillFn(),
|
|
499
|
+
invokeAgentFn: async () => makeAgentResult(0),
|
|
500
|
+
},
|
|
501
|
+
);
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
const state = await readState(projectRoot);
|
|
505
|
+
expect(state.phases.refactor.refactor_execution.file).toBe(
|
|
506
|
+
"it_000013_refactor-execution-progress.json",
|
|
507
|
+
);
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
// Full happy path: all items completed, state and progress correct
|
|
511
|
+
test("happy path: all items completed, progress and state updated correctly", async () => {
|
|
512
|
+
const projectRoot = await createProjectRoot();
|
|
513
|
+
createdRoots.push(projectRoot);
|
|
514
|
+
await seedState(projectRoot);
|
|
515
|
+
await writeRefactorPrd(projectRoot, "000013", [
|
|
516
|
+
{ id: "RI-001", title: "Refactor One", description: "First thing", rationale: "R1" },
|
|
517
|
+
{ id: "RI-002", title: "Refactor Two", description: "Second thing", rationale: "R2" },
|
|
518
|
+
]);
|
|
519
|
+
|
|
520
|
+
const logs: string[] = [];
|
|
521
|
+
|
|
522
|
+
await withCwd(projectRoot, async () => {
|
|
523
|
+
await runExecuteRefactor(
|
|
524
|
+
{ provider: "claude" },
|
|
525
|
+
{
|
|
526
|
+
loadSkillFn: makeSkillFn(),
|
|
527
|
+
invokeAgentFn: async () => makeAgentResult(0),
|
|
528
|
+
logFn: (msg) => logs.push(msg),
|
|
529
|
+
nowFn: () => new Date("2026-02-26T12:00:00.000Z"),
|
|
530
|
+
},
|
|
531
|
+
);
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
const state = await readState(projectRoot);
|
|
535
|
+
expect(state.phases.refactor.refactor_execution.status).toBe("completed");
|
|
536
|
+
expect(state.phases.refactor.refactor_execution.file).toBe(
|
|
537
|
+
"it_000013_refactor-execution-progress.json",
|
|
538
|
+
);
|
|
539
|
+
expect(state.updated_by).toBe("nvst:execute-refactor");
|
|
540
|
+
expect(state.last_updated).toBe("2026-02-26T12:00:00.000Z");
|
|
541
|
+
|
|
542
|
+
const progressPath = join(projectRoot, ".agents", "flow", "it_000013_refactor-execution-progress.json");
|
|
543
|
+
const progress = RefactorExecutionProgressSchema.parse(
|
|
544
|
+
JSON.parse(await readFile(progressPath, "utf8")),
|
|
545
|
+
);
|
|
546
|
+
expect(progress.entries).toHaveLength(2);
|
|
547
|
+
expect(progress.entries[0]).toMatchObject({ id: "RI-001", status: "completed", last_agent_exit_code: 0 });
|
|
548
|
+
expect(progress.entries[1]).toMatchObject({ id: "RI-002", status: "completed", last_agent_exit_code: 0 });
|
|
549
|
+
|
|
550
|
+
expect(logs).toContain("iteration=it_000013 item=RI-001 outcome=completed");
|
|
551
|
+
expect(logs).toContain("iteration=it_000013 item=RI-002 outcome=completed");
|
|
552
|
+
expect(logs).toContain("Refactor execution completed for all items.");
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
// AC02: Progress schema mismatch on resume rejects with clear error
|
|
556
|
+
test("rejects with error when progress file schema is invalid on resume", async () => {
|
|
557
|
+
const projectRoot = await createProjectRoot();
|
|
558
|
+
createdRoots.push(projectRoot);
|
|
559
|
+
await seedState(projectRoot, { refactorExecutionStatus: "in_progress" });
|
|
560
|
+
await writeRefactorPrd(projectRoot, "000013", [
|
|
561
|
+
{ id: "RI-001", title: "T1", description: "D1", rationale: "R1" },
|
|
562
|
+
]);
|
|
563
|
+
|
|
564
|
+
// Write a progress file with invalid schema (missing required fields)
|
|
565
|
+
const progressPath = join(projectRoot, ".agents", "flow", "it_000013_refactor-execution-progress.json");
|
|
566
|
+
await writeFile(
|
|
567
|
+
progressPath,
|
|
568
|
+
JSON.stringify({ entries: [{ id: "RI-001", bad_field: "value" }] }, null, 2) + "\n",
|
|
569
|
+
"utf8",
|
|
570
|
+
);
|
|
571
|
+
|
|
572
|
+
await withCwd(projectRoot, async () => {
|
|
573
|
+
await expect(
|
|
574
|
+
runExecuteRefactor(
|
|
575
|
+
{ provider: "claude" },
|
|
576
|
+
{ loadSkillFn: makeSkillFn() },
|
|
577
|
+
),
|
|
578
|
+
).rejects.toThrow("Progress schema mismatch in .agents/flow/it_000013_refactor-execution-progress.json.");
|
|
579
|
+
});
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
// AC04: Re-attempts items with "pending" status when resuming
|
|
583
|
+
test("re-attempts pending items when resuming execution", async () => {
|
|
584
|
+
const projectRoot = await createProjectRoot();
|
|
585
|
+
createdRoots.push(projectRoot);
|
|
586
|
+
await seedState(projectRoot, { refactorExecutionStatus: "in_progress" });
|
|
587
|
+
await writeRefactorPrd(projectRoot, "000013", [
|
|
588
|
+
{ id: "RI-001", title: "T1", description: "D1", rationale: "R1" },
|
|
589
|
+
{ id: "RI-002", title: "T2", description: "D2", rationale: "R2" },
|
|
590
|
+
]);
|
|
591
|
+
|
|
592
|
+
// Write a progress file with both items pending
|
|
593
|
+
const progressPath = join(projectRoot, ".agents", "flow", "it_000013_refactor-execution-progress.json");
|
|
594
|
+
await writeFile(
|
|
595
|
+
progressPath,
|
|
596
|
+
JSON.stringify({
|
|
597
|
+
entries: [
|
|
598
|
+
{ id: "RI-001", title: "T1", status: "pending", attempt_count: 0, last_agent_exit_code: null, updated_at: "2026-02-26T00:00:00.000Z" },
|
|
599
|
+
{ id: "RI-002", title: "T2", status: "pending", attempt_count: 0, last_agent_exit_code: null, updated_at: "2026-02-26T00:00:00.000Z" },
|
|
600
|
+
],
|
|
601
|
+
}, null, 2) + "\n",
|
|
602
|
+
"utf8",
|
|
603
|
+
);
|
|
604
|
+
|
|
605
|
+
const invokedItems: string[] = [];
|
|
606
|
+
|
|
607
|
+
await withCwd(projectRoot, async () => {
|
|
608
|
+
await runExecuteRefactor(
|
|
609
|
+
{ provider: "claude" },
|
|
610
|
+
{
|
|
611
|
+
loadSkillFn: makeSkillFn(),
|
|
612
|
+
invokeAgentFn: async (opts) => {
|
|
613
|
+
invokedItems.push(opts.prompt.includes("RI-001") ? "RI-001" : "RI-002");
|
|
614
|
+
return makeAgentResult(0);
|
|
615
|
+
},
|
|
616
|
+
},
|
|
617
|
+
);
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
expect(invokedItems).toEqual(["RI-001", "RI-002"]);
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
// AC04: Re-attempts items with "failed" status when resuming
|
|
624
|
+
test("re-attempts failed items when resuming execution", async () => {
|
|
625
|
+
const projectRoot = await createProjectRoot();
|
|
626
|
+
createdRoots.push(projectRoot);
|
|
627
|
+
await seedState(projectRoot, { refactorExecutionStatus: "in_progress" });
|
|
628
|
+
await writeRefactorPrd(projectRoot, "000013", [
|
|
629
|
+
{ id: "RI-001", title: "T1", description: "D1", rationale: "R1" },
|
|
630
|
+
{ id: "RI-002", title: "T2", description: "D2", rationale: "R2" },
|
|
631
|
+
]);
|
|
632
|
+
|
|
633
|
+
// Write a progress file with RI-001 completed, RI-002 failed
|
|
634
|
+
const progressPath = join(projectRoot, ".agents", "flow", "it_000013_refactor-execution-progress.json");
|
|
635
|
+
await writeFile(
|
|
636
|
+
progressPath,
|
|
637
|
+
JSON.stringify({
|
|
638
|
+
entries: [
|
|
639
|
+
{ id: "RI-001", title: "T1", status: "completed", attempt_count: 1, last_agent_exit_code: 0, updated_at: "2026-02-26T00:00:00.000Z" },
|
|
640
|
+
{ id: "RI-002", title: "T2", status: "failed", attempt_count: 1, last_agent_exit_code: 1, updated_at: "2026-02-26T00:00:00.000Z" },
|
|
641
|
+
],
|
|
642
|
+
}, null, 2) + "\n",
|
|
643
|
+
"utf8",
|
|
644
|
+
);
|
|
645
|
+
|
|
646
|
+
const invokedItems: string[] = [];
|
|
647
|
+
|
|
648
|
+
await withCwd(projectRoot, async () => {
|
|
649
|
+
await runExecuteRefactor(
|
|
650
|
+
{ provider: "claude" },
|
|
651
|
+
{
|
|
652
|
+
loadSkillFn: makeSkillFn(),
|
|
653
|
+
invokeAgentFn: async (opts) => {
|
|
654
|
+
invokedItems.push(opts.prompt.includes("RI-001") ? "RI-001" : "RI-002");
|
|
655
|
+
return makeAgentResult(0);
|
|
656
|
+
},
|
|
657
|
+
},
|
|
658
|
+
);
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
// Only RI-002 (failed) should be re-attempted; RI-001 (completed) is skipped
|
|
662
|
+
expect(invokedItems).toEqual(["RI-002"]);
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
// AC05: Rejects when progress item IDs do not match refactor PRD item IDs
|
|
666
|
+
test("rejects with error when progress item IDs do not match refactor PRD item IDs", async () => {
|
|
667
|
+
const projectRoot = await createProjectRoot();
|
|
668
|
+
createdRoots.push(projectRoot);
|
|
669
|
+
await seedState(projectRoot, { refactorExecutionStatus: "in_progress" });
|
|
670
|
+
await writeRefactorPrd(projectRoot, "000013", [
|
|
671
|
+
{ id: "RI-001", title: "T1", description: "D1", rationale: "R1" },
|
|
672
|
+
{ id: "RI-002", title: "T2", description: "D2", rationale: "R2" },
|
|
673
|
+
]);
|
|
674
|
+
|
|
675
|
+
// Write a progress file with different IDs (stale/mismatched)
|
|
676
|
+
const progressPath = join(projectRoot, ".agents", "flow", "it_000013_refactor-execution-progress.json");
|
|
677
|
+
await writeFile(
|
|
678
|
+
progressPath,
|
|
679
|
+
JSON.stringify({
|
|
680
|
+
entries: [
|
|
681
|
+
{ id: "RI-001", title: "T1", status: "completed", attempt_count: 1, last_agent_exit_code: 0, updated_at: "2026-02-26T00:00:00.000Z" },
|
|
682
|
+
{ id: "RI-999", title: "STALE", status: "pending", attempt_count: 0, last_agent_exit_code: null, updated_at: "2026-02-26T00:00:00.000Z" },
|
|
683
|
+
],
|
|
684
|
+
}, null, 2) + "\n",
|
|
685
|
+
"utf8",
|
|
686
|
+
);
|
|
687
|
+
|
|
688
|
+
await withCwd(projectRoot, async () => {
|
|
689
|
+
await expect(
|
|
690
|
+
runExecuteRefactor(
|
|
691
|
+
{ provider: "claude" },
|
|
692
|
+
{ loadSkillFn: makeSkillFn() },
|
|
693
|
+
),
|
|
694
|
+
).rejects.toThrow(
|
|
695
|
+
"Refactor execution progress file out of sync: entry ids do not match refactor PRD item ids.",
|
|
696
|
+
);
|
|
697
|
+
});
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
// AC05: Rejects when progress has different number of items than PRD
|
|
701
|
+
test("rejects with error when progress has different number of items than refactor PRD", async () => {
|
|
702
|
+
const projectRoot = await createProjectRoot();
|
|
703
|
+
createdRoots.push(projectRoot);
|
|
704
|
+
await seedState(projectRoot, { refactorExecutionStatus: "in_progress" });
|
|
705
|
+
await writeRefactorPrd(projectRoot, "000013", [
|
|
706
|
+
{ id: "RI-001", title: "T1", description: "D1", rationale: "R1" },
|
|
707
|
+
{ id: "RI-002", title: "T2", description: "D2", rationale: "R2" },
|
|
708
|
+
]);
|
|
709
|
+
|
|
710
|
+
// Write a progress file with only one entry (missing RI-002)
|
|
711
|
+
const progressPath = join(projectRoot, ".agents", "flow", "it_000013_refactor-execution-progress.json");
|
|
712
|
+
await writeFile(
|
|
713
|
+
progressPath,
|
|
714
|
+
JSON.stringify({
|
|
715
|
+
entries: [
|
|
716
|
+
{ id: "RI-001", title: "T1", status: "completed", attempt_count: 1, last_agent_exit_code: 0, updated_at: "2026-02-26T00:00:00.000Z" },
|
|
717
|
+
],
|
|
718
|
+
}, null, 2) + "\n",
|
|
719
|
+
"utf8",
|
|
720
|
+
);
|
|
721
|
+
|
|
722
|
+
await withCwd(projectRoot, async () => {
|
|
723
|
+
await expect(
|
|
724
|
+
runExecuteRefactor(
|
|
725
|
+
{ provider: "claude" },
|
|
726
|
+
{ loadSkillFn: makeSkillFn() },
|
|
727
|
+
),
|
|
728
|
+
).rejects.toThrow(
|
|
729
|
+
"Refactor execution progress file out of sync: entry ids do not match refactor PRD item ids.",
|
|
730
|
+
);
|
|
731
|
+
});
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
// TC-001-17: Progress file schema has entries with id, title, status, attempt_count, last_agent_exit_code, updated_at
|
|
735
|
+
test("TC-001-17: RefactorExecutionProgressSchema accepts valid payload with attempt_count and last_agent_exit_code", () => {
|
|
736
|
+
const validPayload = {
|
|
737
|
+
entries: [
|
|
738
|
+
{ id: "RI-001", title: "T1", status: "pending", attempt_count: 0, last_agent_exit_code: null, updated_at: "2026-01-01T00:00:00.000Z" },
|
|
739
|
+
{ id: "RI-002", title: "T2", status: "completed", attempt_count: 1, last_agent_exit_code: 0, updated_at: "2026-01-01T00:00:00.000Z" },
|
|
740
|
+
{ id: "RI-003", title: "T3", status: "failed", attempt_count: 2, last_agent_exit_code: 1, updated_at: "2026-01-01T00:00:00.000Z" },
|
|
741
|
+
],
|
|
742
|
+
};
|
|
743
|
+
|
|
744
|
+
const result = RefactorExecutionProgressSchema.safeParse(validPayload);
|
|
745
|
+
expect(result.success).toBe(true);
|
|
746
|
+
if (result.success) {
|
|
747
|
+
const entry = result.data.entries[0];
|
|
748
|
+
expect(entry).toHaveProperty("id");
|
|
749
|
+
expect(entry).toHaveProperty("title");
|
|
750
|
+
expect(entry).toHaveProperty("status");
|
|
751
|
+
expect(entry).toHaveProperty("attempt_count");
|
|
752
|
+
expect(entry).toHaveProperty("last_agent_exit_code");
|
|
753
|
+
expect(entry).toHaveProperty("updated_at");
|
|
754
|
+
expect(entry.attempt_count).toBe(0);
|
|
755
|
+
expect(result.data.entries[1].attempt_count).toBe(1);
|
|
756
|
+
expect(result.data.entries[1].last_agent_exit_code).toBe(0);
|
|
757
|
+
expect(result.data.entries[2].last_agent_exit_code).toBe(1);
|
|
758
|
+
}
|
|
759
|
+
});
|
|
760
|
+
|
|
761
|
+
test("TC-001-17 / FR-4: RefactorExecutionProgressSchema accepts status in_progress", () => {
|
|
762
|
+
const payloadWithInProgress = {
|
|
763
|
+
entries: [
|
|
764
|
+
{ id: "RI-001", title: "T1", status: "in_progress", attempt_count: 1, last_agent_exit_code: null, updated_at: "2026-01-01T00:00:00.000Z" },
|
|
765
|
+
],
|
|
766
|
+
};
|
|
767
|
+
const result = RefactorExecutionProgressSchema.safeParse(payloadWithInProgress);
|
|
768
|
+
expect(result.success).toBe(true);
|
|
769
|
+
if (result.success) {
|
|
770
|
+
expect(result.data.entries[0].status).toBe("in_progress");
|
|
771
|
+
}
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
test("TC-001-17: RefactorExecutionProgressSchema rejects payload missing attempt_count", () => {
|
|
775
|
+
const invalidPayload = {
|
|
776
|
+
entries: [
|
|
777
|
+
{ id: "RI-001", title: "T1", status: "pending", last_agent_exit_code: null, updated_at: "2026-01-01T00:00:00.000Z" },
|
|
778
|
+
],
|
|
779
|
+
};
|
|
780
|
+
const result = RefactorExecutionProgressSchema.safeParse(invalidPayload);
|
|
781
|
+
expect(result.success).toBe(false);
|
|
782
|
+
});
|
|
783
|
+
|
|
784
|
+
test("TC-001-17: RefactorExecutionProgressSchema rejects payload with agent_exit_code instead of last_agent_exit_code", () => {
|
|
785
|
+
const invalidPayload = {
|
|
786
|
+
entries: [
|
|
787
|
+
{ id: "RI-001", title: "T1", status: "pending", attempt_count: 0, agent_exit_code: null, updated_at: "2026-01-01T00:00:00.000Z" },
|
|
788
|
+
],
|
|
789
|
+
};
|
|
790
|
+
const result = RefactorExecutionProgressSchema.safeParse(invalidPayload);
|
|
791
|
+
expect(result.success).toBe(false);
|
|
792
|
+
});
|
|
793
|
+
|
|
794
|
+
// Skips already-completed entries on re-run
|
|
795
|
+
test("skips completed entries when resuming execution", async () => {
|
|
796
|
+
const projectRoot = await createProjectRoot();
|
|
797
|
+
createdRoots.push(projectRoot);
|
|
798
|
+
await seedState(projectRoot, { refactorExecutionStatus: "in_progress" });
|
|
799
|
+
await writeRefactorPrd(projectRoot, "000013", [
|
|
800
|
+
{ id: "RI-001", title: "T1", description: "D1", rationale: "R1" },
|
|
801
|
+
{ id: "RI-002", title: "T2", description: "D2", rationale: "R2" },
|
|
802
|
+
]);
|
|
803
|
+
|
|
804
|
+
// Write a pre-existing progress file with RI-001 already completed
|
|
805
|
+
const progressPath = join(projectRoot, ".agents", "flow", "it_000013_refactor-execution-progress.json");
|
|
806
|
+
await writeFile(
|
|
807
|
+
progressPath,
|
|
808
|
+
JSON.stringify({
|
|
809
|
+
entries: [
|
|
810
|
+
{ id: "RI-001", title: "T1", status: "completed", attempt_count: 1, last_agent_exit_code: 0, updated_at: "2026-02-26T00:00:00.000Z" },
|
|
811
|
+
{ id: "RI-002", title: "T2", status: "pending", attempt_count: 0, last_agent_exit_code: null, updated_at: "2026-02-26T00:00:00.000Z" },
|
|
812
|
+
],
|
|
813
|
+
}, null, 2) + "\n",
|
|
814
|
+
"utf8",
|
|
815
|
+
);
|
|
816
|
+
|
|
817
|
+
const invokedItems: string[] = [];
|
|
818
|
+
|
|
819
|
+
await withCwd(projectRoot, async () => {
|
|
820
|
+
await runExecuteRefactor(
|
|
821
|
+
{ provider: "claude" },
|
|
822
|
+
{
|
|
823
|
+
loadSkillFn: makeSkillFn(),
|
|
824
|
+
invokeAgentFn: async (opts) => {
|
|
825
|
+
invokedItems.push(opts.prompt.includes("RI-001") ? "RI-001" : "RI-002");
|
|
826
|
+
return makeAgentResult(0);
|
|
827
|
+
},
|
|
828
|
+
},
|
|
829
|
+
);
|
|
830
|
+
});
|
|
831
|
+
|
|
832
|
+
// Only RI-002 should be invoked (RI-001 was already completed)
|
|
833
|
+
expect(invokedItems).toEqual(["RI-002"]);
|
|
834
|
+
});
|
|
835
|
+
});
|
|
836
|
+
|
|
837
|
+
describe("US-003: generate refactor execution report", () => {
|
|
838
|
+
// AC01: Report file is written to .agents/flow/ after all items are processed
|
|
839
|
+
test("writes refactor-execution-report.md to .agents/flow/ after processing", async () => {
|
|
840
|
+
const projectRoot = await createProjectRoot();
|
|
841
|
+
createdRoots.push(projectRoot);
|
|
842
|
+
await seedState(projectRoot);
|
|
843
|
+
await writeRefactorPrd(projectRoot, "000013", [
|
|
844
|
+
{ id: "RI-001", title: "T1", description: "D1", rationale: "R1" },
|
|
845
|
+
]);
|
|
846
|
+
|
|
847
|
+
await withCwd(projectRoot, async () => {
|
|
848
|
+
await runExecuteRefactor(
|
|
849
|
+
{ provider: "claude" },
|
|
850
|
+
{
|
|
851
|
+
loadSkillFn: makeSkillFn(),
|
|
852
|
+
invokeAgentFn: async () => makeAgentResult(0),
|
|
853
|
+
},
|
|
854
|
+
);
|
|
855
|
+
});
|
|
856
|
+
|
|
857
|
+
const reportPath = join(projectRoot, ".agents", "flow", "it_000013_refactor-execution-report.md");
|
|
858
|
+
const content = await readFile(reportPath, "utf8");
|
|
859
|
+
expect(content).toBeTruthy();
|
|
860
|
+
});
|
|
861
|
+
|
|
862
|
+
// AC02: Report includes iteration, total, completed, failed, and table
|
|
863
|
+
test("report contains iteration number, totals, and table with required columns", async () => {
|
|
864
|
+
const projectRoot = await createProjectRoot();
|
|
865
|
+
createdRoots.push(projectRoot);
|
|
866
|
+
await seedState(projectRoot);
|
|
867
|
+
await writeRefactorPrd(projectRoot, "000013", [
|
|
868
|
+
{ id: "RI-001", title: "First Refactor", description: "D1", rationale: "R1" },
|
|
869
|
+
{ id: "RI-002", title: "Second Refactor", description: "D2", rationale: "R2" },
|
|
870
|
+
]);
|
|
871
|
+
|
|
872
|
+
await withCwd(projectRoot, async () => {
|
|
873
|
+
await runExecuteRefactor(
|
|
874
|
+
{ provider: "claude" },
|
|
875
|
+
{
|
|
876
|
+
loadSkillFn: makeSkillFn(),
|
|
877
|
+
invokeAgentFn: async (opts) =>
|
|
878
|
+
makeAgentResult(opts.prompt.includes("RI-001") ? 1 : 0),
|
|
879
|
+
},
|
|
880
|
+
);
|
|
881
|
+
});
|
|
882
|
+
|
|
883
|
+
const reportPath = join(projectRoot, ".agents", "flow", "it_000013_refactor-execution-report.md");
|
|
884
|
+
const content = await readFile(reportPath, "utf8");
|
|
885
|
+
|
|
886
|
+
// Iteration number
|
|
887
|
+
expect(content).toContain("it_000013");
|
|
888
|
+
// Total, completed, failed counts
|
|
889
|
+
expect(content).toContain("**Total:** 2");
|
|
890
|
+
expect(content).toContain("**Completed:** 1");
|
|
891
|
+
expect(content).toContain("**Failed:** 1");
|
|
892
|
+
// Table header columns
|
|
893
|
+
expect(content).toContain("RI ID");
|
|
894
|
+
expect(content).toContain("Title");
|
|
895
|
+
expect(content).toContain("Status");
|
|
896
|
+
expect(content).toContain("Agent Exit Code");
|
|
897
|
+
// Table rows with item data
|
|
898
|
+
expect(content).toContain("RI-001");
|
|
899
|
+
expect(content).toContain("First Refactor");
|
|
900
|
+
expect(content).toContain("failed");
|
|
901
|
+
expect(content).toContain("RI-002");
|
|
902
|
+
expect(content).toContain("Second Refactor");
|
|
903
|
+
expect(content).toContain("completed");
|
|
904
|
+
});
|
|
905
|
+
|
|
906
|
+
// AC03: Report is written even when items fail
|
|
907
|
+
test("writes report regardless of whether items failed", async () => {
|
|
908
|
+
const projectRoot = await createProjectRoot();
|
|
909
|
+
createdRoots.push(projectRoot);
|
|
910
|
+
await seedState(projectRoot);
|
|
911
|
+
await writeRefactorPrd(projectRoot, "000013", [
|
|
912
|
+
{ id: "RI-001", title: "T1", description: "D1", rationale: "R1" },
|
|
913
|
+
{ id: "RI-002", title: "T2", description: "D2", rationale: "R2" },
|
|
914
|
+
]);
|
|
915
|
+
|
|
916
|
+
await withCwd(projectRoot, async () => {
|
|
917
|
+
await runExecuteRefactor(
|
|
918
|
+
{ provider: "claude" },
|
|
919
|
+
{
|
|
920
|
+
loadSkillFn: makeSkillFn(),
|
|
921
|
+
invokeAgentFn: async () => makeAgentResult(1), // all items fail
|
|
922
|
+
},
|
|
923
|
+
);
|
|
924
|
+
});
|
|
925
|
+
|
|
926
|
+
const reportPath = join(projectRoot, ".agents", "flow", "it_000013_refactor-execution-report.md");
|
|
927
|
+
const content = await readFile(reportPath, "utf8");
|
|
928
|
+
expect(content).toContain("**Failed:** 2");
|
|
929
|
+
expect(content).toContain("**Completed:** 0");
|
|
930
|
+
});
|
|
931
|
+
|
|
932
|
+
// Unit: buildRefactorExecutionReport renders correctly
|
|
933
|
+
test("buildRefactorExecutionReport produces correct markdown for mixed results", () => {
|
|
934
|
+
const progress = {
|
|
935
|
+
entries: [
|
|
936
|
+
{ id: "RI-001", title: "Alpha", status: "completed" as const, attempt_count: 1, last_agent_exit_code: 0, updated_at: "2026-01-01T00:00:00.000Z" },
|
|
937
|
+
{ id: "RI-002", title: "Beta", status: "failed" as const, attempt_count: 1, last_agent_exit_code: 2, updated_at: "2026-01-01T00:00:00.000Z" },
|
|
938
|
+
{ id: "RI-003", title: "Gamma", status: "pending" as const, attempt_count: 0, last_agent_exit_code: null, updated_at: "2026-01-01T00:00:00.000Z" },
|
|
939
|
+
],
|
|
940
|
+
};
|
|
941
|
+
|
|
942
|
+
const report = buildRefactorExecutionReport("000014", progress);
|
|
943
|
+
|
|
944
|
+
expect(report).toContain("it_000014");
|
|
945
|
+
expect(report).toContain("**Total:** 3");
|
|
946
|
+
expect(report).toContain("**Completed:** 1");
|
|
947
|
+
expect(report).toContain("**Failed:** 1");
|
|
948
|
+
expect(report).toContain("| RI-001 | Alpha | completed | 0 |");
|
|
949
|
+
expect(report).toContain("| RI-002 | Beta | failed | 2 |");
|
|
950
|
+
expect(report).toContain("| RI-003 | Gamma | pending | N/A |");
|
|
951
|
+
// Table header
|
|
952
|
+
expect(report).toContain("| RI ID | Title | Status | Agent Exit Code |");
|
|
953
|
+
});
|
|
954
|
+
});
|