tdd-enforcer 0.2.6 → 0.2.8

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.
@@ -1,351 +1,407 @@
1
- import { describe, it, expect, beforeEach, vi } from "vitest";
2
- import { executeNextPhase, executePreviousPhase, executeTddStatus } from "./tools.js";
3
- import type { NextPhaseDeps, PreviousPhaseDeps, TddStatusDeps } from "./tools.js";
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+ import type {
3
+ NextPhaseDeps,
4
+ PreviousPhaseDeps,
5
+ TddStatusDeps,
6
+ } from "./tools.js";
7
+ import {
8
+ executeNextPhase,
9
+ executePreviousPhase,
10
+ executeTddStatus,
11
+ } from "./tools.js";
4
12
 
5
13
  // ── Helpers ─────────────────────────────────────────────────────────────────
6
14
 
7
15
  const CONFIG = {
8
- blockedInRed: ["tests/**/*.test.ts"],
9
- blockedInGreen: ["src/**/*.ts"],
10
- testCommands: ["npm test"],
11
- timeoutSeconds: 30,
16
+ blockedInRed: ["tests/**/*.test.ts"],
17
+ blockedInGreen: ["src/**/*.ts"],
18
+ testCommands: ["npm test"],
19
+ timeoutSeconds: 30,
12
20
  };
13
21
 
14
22
  // ── executeNextPhase ────────────────────────────────────────────────────────
15
23
 
16
24
  describe("executeNextPhase", () => {
17
- let mockLoadTddState: ReturnType<typeof vi.fn>;
18
- let mockNextPhase: ReturnType<typeof vi.fn>;
19
- let mockGetDisallowedChanges: ReturnType<typeof vi.fn>;
20
- let mockCheckGate: ReturnType<typeof vi.fn>;
21
- let mockSnapshot: ReturnType<typeof vi.fn>;
22
- let mockSavePhaseState: ReturnType<typeof vi.fn>;
23
- let mockGetNudgePrompt: ReturnType<typeof vi.fn>;
24
- let mockAsyncExec: ReturnType<typeof vi.fn>;
25
- let mockTddLog: ReturnType<typeof vi.fn>;
26
-
27
- function makeDeps(overrides: Partial<NextPhaseDeps> = {}): NextPhaseDeps {
28
- return {
29
- loadTddState: mockLoadTddState,
30
- nextPhase: mockNextPhase,
31
- getDisallowedChanges: mockGetDisallowedChanges,
32
- checkGate: mockCheckGate,
33
- snapshot: mockSnapshot,
34
- savePhaseState: mockSavePhaseState,
35
- getNudgePrompt: mockGetNudgePrompt,
36
- asyncExec: mockAsyncExec,
37
- tddLog: mockTddLog,
38
- ...overrides,
39
- };
40
- }
41
-
42
- beforeEach(() => {
43
- vi.clearAllMocks();
44
- mockLoadTddState = vi.fn();
45
- mockNextPhase = vi.fn();
46
- mockGetDisallowedChanges = vi.fn().mockReturnValue([]);
47
- mockCheckGate = vi.fn();
48
- mockSnapshot = vi.fn().mockReturnValue("hash123");
49
- mockSavePhaseState = vi.fn();
50
- mockGetNudgePrompt = vi.fn().mockReturnValue("");
51
- mockAsyncExec = vi.fn();
52
- mockTddLog = vi.fn();
53
-
54
- mockNextPhase.mockImplementation((p: string) =>
55
- p === "red" ? "green" : p === "green" ? "refactor" : p === "refactor" ? "red" : null,
56
- );
57
- mockCheckGate.mockResolvedValue({ passed: true, message: "ok" });
58
- mockAsyncExec.mockResolvedValue({ stdout: "", stderr: "" });
59
- });
60
-
61
- it("returns config error when TDD not setup", async () => {
62
- mockLoadTddState.mockReturnValue({
63
- ok: false,
64
- reason: "Missing .pi/tdd/ directory. See the tdd-enforcer skill to learn how to set up TDD configs.",
65
- });
66
- const result = await executeNextPhase({ cwd: "/test" } as any, makeDeps());
67
- expect(result.content[0].text).toContain("Missing .pi/tdd/");
68
- });
69
-
70
- it("returns disabled message when TDD disabled", async () => {
71
- mockLoadTddState.mockReturnValue({
72
- ok: true,
73
- state: { enabled: false, current: "red" },
74
- config: CONFIG,
75
- });
76
- const result = await executeNextPhase({ cwd: "/test" } as any, makeDeps());
77
- expect(result.content[0].text).toContain("not enabled");
78
- });
79
-
80
- it("blocks when allowlist violations exist", async () => {
81
- mockLoadTddState.mockReturnValue({
82
- ok: true,
83
- state: { enabled: true, current: "red" },
84
- config: CONFIG,
85
- });
86
- mockGetDisallowedChanges.mockReturnValue(["src/violation.ts"]);
87
- const result = await executeNextPhase({ cwd: "/test" } as any, makeDeps());
88
- expect(result.content[0].text).toContain("BLOCKED");
89
- expect(result.content[0].text).toContain("src/violation.ts");
90
- });
91
-
92
- it("blocks red→green when tests pass (need failing test)", async () => {
93
- mockLoadTddState.mockReturnValue({
94
- ok: true,
95
- state: { enabled: true, current: "red" },
96
- config: CONFIG,
97
- });
98
- mockCheckGate.mockResolvedValue({
99
- passed: false,
100
- message: "Tests passed. Add a failing test before transitioning to GREEN.",
101
- });
102
- const result = await executeNextPhase({ cwd: "/test" } as any, makeDeps());
103
- expect(result.content[0].text).toContain("Add a failing test");
104
- expect(result.content[0].text).toContain("GREEN");
105
- });
106
-
107
- it("blocks green→refactor when tests fail", async () => {
108
- mockLoadTddState.mockReturnValue({
109
- ok: true,
110
- state: { enabled: true, current: "green" },
111
- config: CONFIG,
112
- });
113
- mockCheckGate.mockResolvedValue({
114
- passed: false,
115
- message: "Tests failed. Fix them before transitioning to REFACTOR.",
116
- });
117
- const result = await executeNextPhase({ cwd: "/test" } as any, makeDeps());
118
- expect(result.content[0].text).toContain("failed");
119
- expect(result.content[0].text).toContain("REFACTOR");
120
- });
121
-
122
- it("advances red→green when tests fail", async () => {
123
- mockLoadTddState.mockReturnValue({
124
- ok: true,
125
- state: { enabled: true, current: "red" },
126
- config: CONFIG,
127
- });
128
- mockCheckGate.mockResolvedValue({ passed: true, message: "Tests fail proceed to GREEN." });
129
- mockGetNudgePrompt.mockReturnValue("You are now in **GREEN** phase.");
130
- const result = await executeNextPhase({ cwd: "/test" } as any, makeDeps());
131
- expect(result.content[0].text).toContain("GREEN");
132
- expect(mockSnapshot).toHaveBeenCalledWith("/test", "red");
133
- expect(mockSavePhaseState).toHaveBeenCalledWith("/test", { enabled: true, current: "green" });
134
- });
135
-
136
- it("advances green→refactor when tests pass", async () => {
137
- mockLoadTddState.mockReturnValue({
138
- ok: true,
139
- state: { enabled: true, current: "green" },
140
- config: CONFIG,
141
- });
142
- mockCheckGate.mockResolvedValue({ passed: true, message: "All tests pass — proceeding." });
143
- mockGetNudgePrompt.mockReturnValue("You are now in **REFACTOR** phase.");
144
- const result = await executeNextPhase({ cwd: "/test" } as any, makeDeps());
145
- expect(result.content[0].text).toContain("REFACTOR");
146
- expect(mockSnapshot).toHaveBeenCalledWith("/test", "green");
147
- expect(mockSavePhaseState).toHaveBeenCalledWith("/test", { enabled: true, current: "refactor" });
148
- });
149
-
150
- it("blocks refactor→red when tests fail", async () => {
151
- mockLoadTddState.mockReturnValue({
152
- ok: true,
153
- state: { enabled: true, current: "refactor" },
154
- config: CONFIG,
155
- });
156
- mockCheckGate.mockResolvedValue({
157
- passed: false,
158
- message: "Tests failed. Fix them before transitioning to RED.",
159
- });
160
- const result = await executeNextPhase({ cwd: "/test" } as any, makeDeps());
161
- expect(result.content[0].text).toContain("failed");
162
- expect(result.content[0].text).toContain("RED");
163
- expect(mockSavePhaseState).not.toHaveBeenCalled();
164
- });
165
-
166
- it("advances refactor→red when tests pass", async () => {
167
- mockLoadTddState.mockReturnValue({
168
- ok: true,
169
- state: { enabled: true, current: "refactor" },
170
- config: CONFIG,
171
- });
172
- mockCheckGate.mockResolvedValue({ passed: true, message: "All tests pass proceeding." });
173
- mockGetNudgePrompt.mockReturnValue("You are now in **RED** phase.");
174
- const result = await executeNextPhase({ cwd: "/test" } as any, makeDeps());
175
- expect(result.content[0].text).toContain("RED");
176
- expect(mockSnapshot).toHaveBeenCalledWith("/test", "refactor");
177
- expect(mockSavePhaseState).toHaveBeenCalledWith("/test", { enabled: true, current: "red" });
178
- });
25
+ let mockLoadTddState: ReturnType<typeof vi.fn>;
26
+ let mockNextPhase: ReturnType<typeof vi.fn>;
27
+ let mockGetDisallowedChanges: ReturnType<typeof vi.fn>;
28
+ let mockCheckGate: ReturnType<typeof vi.fn>;
29
+ let mockSnapshot: ReturnType<typeof vi.fn>;
30
+ let mockSavePhaseState: ReturnType<typeof vi.fn>;
31
+ let mockGetNudgePrompt: ReturnType<typeof vi.fn>;
32
+ let mockAsyncExec: ReturnType<typeof vi.fn>;
33
+ let mockTddLog: ReturnType<typeof vi.fn>;
34
+
35
+ function makeDeps(overrides: Partial<NextPhaseDeps> = {}): NextPhaseDeps {
36
+ return {
37
+ loadTddState: mockLoadTddState,
38
+ nextPhase: mockNextPhase,
39
+ getDisallowedChanges: mockGetDisallowedChanges,
40
+ checkGate: mockCheckGate,
41
+ snapshot: mockSnapshot,
42
+ savePhaseState: mockSavePhaseState,
43
+ getNudgePrompt: mockGetNudgePrompt,
44
+ asyncExec: mockAsyncExec,
45
+ tddLog: mockTddLog,
46
+ ...overrides,
47
+ };
48
+ }
49
+
50
+ beforeEach(() => {
51
+ vi.clearAllMocks();
52
+ mockLoadTddState = vi.fn();
53
+ mockNextPhase = vi.fn();
54
+ mockGetDisallowedChanges = vi.fn().mockReturnValue([]);
55
+ mockCheckGate = vi.fn();
56
+ mockSnapshot = vi.fn().mockReturnValue("hash123");
57
+ mockSavePhaseState = vi.fn();
58
+ mockGetNudgePrompt = vi.fn().mockReturnValue("");
59
+ mockAsyncExec = vi.fn();
60
+ mockTddLog = vi.fn();
61
+
62
+ mockNextPhase.mockImplementation((p: string) =>
63
+ p === "red"
64
+ ? "green"
65
+ : p === "green"
66
+ ? "refactor"
67
+ : p === "refactor"
68
+ ? "red"
69
+ : null,
70
+ );
71
+ mockCheckGate.mockResolvedValue({ passed: true, message: "ok" });
72
+ mockAsyncExec.mockResolvedValue({ stdout: "", stderr: "" });
73
+ });
74
+
75
+ it("throws when TDD not setup", async () => {
76
+ mockLoadTddState.mockReturnValue({
77
+ ok: false,
78
+ reason:
79
+ "Missing .pi/tdd/ directory. See the tdd-enforcer skill to learn how to set up TDD configs.",
80
+ });
81
+ await expect(
82
+ executeNextPhase({ cwd: "/test" } as any, makeDeps()),
83
+ ).rejects.toThrow("Missing .pi/tdd/");
84
+ });
85
+
86
+ it("throws when TDD disabled", async () => {
87
+ mockLoadTddState.mockReturnValue({
88
+ ok: true,
89
+ state: { enabled: false, current: "red" },
90
+ config: CONFIG,
91
+ });
92
+ await expect(
93
+ executeNextPhase({ cwd: "/test" } as any, makeDeps()),
94
+ ).rejects.toThrow("not enabled");
95
+ });
96
+
97
+ it("blocks when allowlist violations exist", async () => {
98
+ mockLoadTddState.mockReturnValue({
99
+ ok: true,
100
+ state: { enabled: true, current: "red" },
101
+ config: CONFIG,
102
+ });
103
+ mockGetDisallowedChanges.mockReturnValue(["src/violation.ts"]);
104
+ await expect(
105
+ executeNextPhase({ cwd: "/test" } as any, makeDeps()),
106
+ ).rejects.toThrow("BLOCKED");
107
+ });
108
+
109
+ it("blocks red→green when tests pass (need failing test)", async () => {
110
+ mockLoadTddState.mockReturnValue({
111
+ ok: true,
112
+ state: { enabled: true, current: "red" },
113
+ config: CONFIG,
114
+ });
115
+ mockCheckGate.mockResolvedValue({
116
+ passed: false,
117
+ message:
118
+ "Tests passed. Add a failing test before transitioning to GREEN.",
119
+ });
120
+ await expect(
121
+ executeNextPhase({ cwd: "/test" } as any, makeDeps()),
122
+ ).rejects.toThrow("Add a failing test");
123
+ });
124
+
125
+ it("blocks green→refactor when tests fail", async () => {
126
+ mockLoadTddState.mockReturnValue({
127
+ ok: true,
128
+ state: { enabled: true, current: "green" },
129
+ config: CONFIG,
130
+ });
131
+ mockCheckGate.mockResolvedValue({
132
+ passed: false,
133
+ message: "Tests failed. Fix them before transitioning to REFACTOR.",
134
+ });
135
+ await expect(
136
+ executeNextPhase({ cwd: "/test" } as any, makeDeps()),
137
+ ).rejects.toThrow("failed");
138
+ });
139
+
140
+ it("advances red→green when tests fail", async () => {
141
+ mockLoadTddState.mockReturnValue({
142
+ ok: true,
143
+ state: { enabled: true, current: "red" },
144
+ config: CONFIG,
145
+ });
146
+ mockCheckGate.mockResolvedValue({
147
+ passed: true,
148
+ message: "Tests fail — proceed to GREEN.",
149
+ });
150
+ mockGetNudgePrompt.mockReturnValue("You are now in **GREEN** phase.");
151
+ const result = await executeNextPhase({ cwd: "/test" } as any, makeDeps());
152
+ expect(result.content[0].text).toContain("GREEN");
153
+ expect(mockSnapshot).toHaveBeenCalledWith("/test", "red");
154
+ expect(mockSavePhaseState).toHaveBeenCalledWith("/test", {
155
+ enabled: true,
156
+ current: "green",
157
+ });
158
+ });
159
+
160
+ it("advances green→refactor when tests pass", async () => {
161
+ mockLoadTddState.mockReturnValue({
162
+ ok: true,
163
+ state: { enabled: true, current: "green" },
164
+ config: CONFIG,
165
+ });
166
+ mockCheckGate.mockResolvedValue({
167
+ passed: true,
168
+ message: "All tests pass proceeding.",
169
+ });
170
+ mockGetNudgePrompt.mockReturnValue("You are now in **REFACTOR** phase.");
171
+ const result = await executeNextPhase({ cwd: "/test" } as any, makeDeps());
172
+ expect(result.content[0].text).toContain("REFACTOR");
173
+ expect(mockSnapshot).toHaveBeenCalledWith("/test", "green");
174
+ expect(mockSavePhaseState).toHaveBeenCalledWith("/test", {
175
+ enabled: true,
176
+ current: "refactor",
177
+ });
178
+ });
179
+
180
+ it("blocks refactor→red when tests fail", async () => {
181
+ mockLoadTddState.mockReturnValue({
182
+ ok: true,
183
+ state: { enabled: true, current: "refactor" },
184
+ config: CONFIG,
185
+ });
186
+ mockCheckGate.mockResolvedValue({
187
+ passed: false,
188
+ message: "Tests failed. Fix them before transitioning to RED.",
189
+ });
190
+ await expect(
191
+ executeNextPhase({ cwd: "/test" } as any, makeDeps()),
192
+ ).rejects.toThrow("failed");
193
+ expect(mockSavePhaseState).not.toHaveBeenCalled();
194
+ });
195
+
196
+ it("advances refactor→red when tests pass", async () => {
197
+ mockLoadTddState.mockReturnValue({
198
+ ok: true,
199
+ state: { enabled: true, current: "refactor" },
200
+ config: CONFIG,
201
+ });
202
+ mockCheckGate.mockResolvedValue({
203
+ passed: true,
204
+ message: "All tests pass — proceeding.",
205
+ });
206
+ mockGetNudgePrompt.mockReturnValue("You are now in **RED** phase.");
207
+ const result = await executeNextPhase({ cwd: "/test" } as any, makeDeps());
208
+ expect(result.content[0].text).toContain("RED");
209
+ expect(mockSnapshot).toHaveBeenCalledWith("/test", "refactor");
210
+ expect(mockSavePhaseState).toHaveBeenCalledWith("/test", {
211
+ enabled: true,
212
+ current: "red",
213
+ });
214
+ });
179
215
  });
180
216
 
181
217
  // ── executePreviousPhase ────────────────────────────────────────────────────
182
218
 
183
219
  describe("executePreviousPhase", () => {
184
- let mockLoadTddState: ReturnType<typeof vi.fn>;
185
- let mockHasParent: ReturnType<typeof vi.fn>;
186
- let mockHeadMessage: ReturnType<typeof vi.fn>;
187
- let mockResetHard: ReturnType<typeof vi.fn>;
188
- let mockUndoLastCommit: ReturnType<typeof vi.fn>;
189
- let mockSavePhaseState: ReturnType<typeof vi.fn>;
190
- let mockTddLog: ReturnType<typeof vi.fn>;
191
-
192
- function makeDeps(overrides: Partial<PreviousPhaseDeps> = {}): PreviousPhaseDeps {
193
- return {
194
- loadTddState: mockLoadTddState,
195
- hasParent: mockHasParent,
196
- headMessage: mockHeadMessage,
197
- resetHard: mockResetHard,
198
- undoLastCommit: mockUndoLastCommit,
199
- savePhaseState: mockSavePhaseState,
200
- tddLog: mockTddLog,
201
- ...overrides,
202
- };
203
- }
204
-
205
- beforeEach(() => {
206
- vi.clearAllMocks();
207
- mockLoadTddState = vi.fn();
208
- mockHasParent = vi.fn();
209
- mockHeadMessage = vi.fn();
210
- mockResetHard = vi.fn();
211
- mockUndoLastCommit = vi.fn();
212
- mockSavePhaseState = vi.fn();
213
- mockTddLog = vi.fn();
214
-
215
- mockHasParent.mockReturnValue(true);
216
- });
217
-
218
- it("returns config error when TDD not setup", async () => {
219
- mockLoadTddState.mockReturnValue({
220
- ok: false,
221
- reason: "Missing .pi/tdd/ directory.",
222
- });
223
- const result = await executePreviousPhase({ cwd: "/test" } as any, makeDeps());
224
- expect(result.content[0].text).toContain("Missing .pi/tdd/");
225
- });
226
-
227
- it("returns disabled message when TDD disabled", async () => {
228
- mockLoadTddState.mockReturnValue({
229
- ok: true,
230
- state: { enabled: false, current: "red" },
231
- config: CONFIG,
232
- });
233
- const result = await executePreviousPhase({ cwd: "/test" } as any, makeDeps());
234
- expect(result.content[0].text).toContain("not enabled");
235
- });
236
-
237
- it("returns no-parent message when only init commit exists", async () => {
238
- mockLoadTddState.mockReturnValue({
239
- ok: true,
240
- state: { enabled: true, current: "red" },
241
- config: CONFIG,
242
- });
243
- mockHasParent.mockReturnValue(false);
244
- const result = await executePreviousPhase({ cwd: "/test" } as any, makeDeps());
245
- expect(result.content[0].text).toContain("No previous phase");
246
- });
247
-
248
- it("returns error when HEAD message is not a TDD snapshot", async () => {
249
- mockLoadTddState.mockReturnValue({
250
- ok: true,
251
- state: { enabled: true, current: "red" },
252
- config: CONFIG,
253
- });
254
- mockHeadMessage.mockReturnValue("garbage");
255
- const result = await executePreviousPhase({ cwd: "/test" } as any, makeDeps());
256
- expect(result.content[0].text).toContain("not a TDD snapshot");
257
- });
258
-
259
- it("reverts to previous phase on success", async () => {
260
- mockLoadTddState.mockReturnValue({
261
- ok: true,
262
- state: { enabled: true, current: "green" },
263
- config: CONFIG,
264
- });
265
- mockHeadMessage.mockReturnValue("tdd: red");
266
- const result = await executePreviousPhase({ cwd: "/test" } as any, makeDeps());
267
- expect(result.content[0].text).toContain("RED");
268
- expect(mockResetHard).toHaveBeenCalledWith("/test");
269
- expect(mockUndoLastCommit).toHaveBeenCalledWith("/test");
270
- expect(mockSavePhaseState).toHaveBeenCalledWith("/test", { enabled: true, current: "red" });
271
- });
272
-
273
- it("reverts to correct phase from green head label", async () => {
274
- mockLoadTddState.mockReturnValue({
275
- ok: true,
276
- state: { enabled: true, current: "refactor" },
277
- config: CONFIG,
278
- });
279
- mockHeadMessage.mockReturnValue("tdd: green");
280
- const result = await executePreviousPhase({ cwd: "/test" } as any, makeDeps());
281
- expect(result.content[0].text).toContain("GREEN");
282
- expect(mockSavePhaseState).toHaveBeenCalledWith("/test", { enabled: true, current: "green" });
283
- });
220
+ let mockLoadTddState: ReturnType<typeof vi.fn>;
221
+ let mockHasParent: ReturnType<typeof vi.fn>;
222
+ let mockHeadMessage: ReturnType<typeof vi.fn>;
223
+ let mockResetHard: ReturnType<typeof vi.fn>;
224
+ let mockUndoLastCommit: ReturnType<typeof vi.fn>;
225
+ let mockSavePhaseState: ReturnType<typeof vi.fn>;
226
+ let mockTddLog: ReturnType<typeof vi.fn>;
227
+
228
+ function makeDeps(
229
+ overrides: Partial<PreviousPhaseDeps> = {},
230
+ ): PreviousPhaseDeps {
231
+ return {
232
+ loadTddState: mockLoadTddState,
233
+ hasParent: mockHasParent,
234
+ headMessage: mockHeadMessage,
235
+ resetHard: mockResetHard,
236
+ undoLastCommit: mockUndoLastCommit,
237
+ savePhaseState: mockSavePhaseState,
238
+ tddLog: mockTddLog,
239
+ ...overrides,
240
+ };
241
+ }
242
+
243
+ beforeEach(() => {
244
+ vi.clearAllMocks();
245
+ mockLoadTddState = vi.fn();
246
+ mockHasParent = vi.fn();
247
+ mockHeadMessage = vi.fn();
248
+ mockResetHard = vi.fn();
249
+ mockUndoLastCommit = vi.fn();
250
+ mockSavePhaseState = vi.fn();
251
+ mockTddLog = vi.fn();
252
+
253
+ mockHasParent.mockReturnValue(true);
254
+ });
255
+
256
+ it("throws when TDD not setup", async () => {
257
+ mockLoadTddState.mockReturnValue({
258
+ ok: false,
259
+ reason: "Missing .pi/tdd/ directory.",
260
+ });
261
+ await expect(
262
+ executePreviousPhase({ cwd: "/test" } as any, makeDeps()),
263
+ ).rejects.toThrow("Missing .pi/tdd/");
264
+ });
265
+
266
+ it("throws when TDD disabled", async () => {
267
+ mockLoadTddState.mockReturnValue({
268
+ ok: true,
269
+ state: { enabled: false, current: "red" },
270
+ config: CONFIG,
271
+ });
272
+ await expect(
273
+ executePreviousPhase({ cwd: "/test" } as any, makeDeps()),
274
+ ).rejects.toThrow("not enabled");
275
+ });
276
+
277
+ it("throws when no parent commit", async () => {
278
+ mockLoadTddState.mockReturnValue({
279
+ ok: true,
280
+ state: { enabled: true, current: "red" },
281
+ config: CONFIG,
282
+ });
283
+ mockHasParent.mockReturnValue(false);
284
+ await expect(
285
+ executePreviousPhase({ cwd: "/test" } as any, makeDeps()),
286
+ ).rejects.toThrow("No previous phase");
287
+ });
288
+
289
+ it("throws when HEAD message is not a TDD snapshot", async () => {
290
+ mockLoadTddState.mockReturnValue({
291
+ ok: true,
292
+ state: { enabled: true, current: "red" },
293
+ config: CONFIG,
294
+ });
295
+ mockHeadMessage.mockReturnValue("garbage");
296
+ await expect(
297
+ executePreviousPhase({ cwd: "/test" } as any, makeDeps()),
298
+ ).rejects.toThrow("not a TDD snapshot");
299
+ });
300
+
301
+ it("reverts to previous phase on success", async () => {
302
+ mockLoadTddState.mockReturnValue({
303
+ ok: true,
304
+ state: { enabled: true, current: "green" },
305
+ config: CONFIG,
306
+ });
307
+ mockHeadMessage.mockReturnValue("tdd: red");
308
+ const result = await executePreviousPhase(
309
+ { cwd: "/test" } as any,
310
+ makeDeps(),
311
+ );
312
+ expect(result.content[0].text).toContain("RED");
313
+ expect(mockResetHard).toHaveBeenCalledWith("/test");
314
+ expect(mockUndoLastCommit).toHaveBeenCalledWith("/test");
315
+ expect(mockSavePhaseState).toHaveBeenCalledWith("/test", {
316
+ enabled: true,
317
+ current: "red",
318
+ });
319
+ });
320
+
321
+ it("reverts to correct phase from green head label", async () => {
322
+ mockLoadTddState.mockReturnValue({
323
+ ok: true,
324
+ state: { enabled: true, current: "refactor" },
325
+ config: CONFIG,
326
+ });
327
+ mockHeadMessage.mockReturnValue("tdd: green");
328
+ const result = await executePreviousPhase(
329
+ { cwd: "/test" } as any,
330
+ makeDeps(),
331
+ );
332
+ expect(result.content[0].text).toContain("GREEN");
333
+ expect(mockSavePhaseState).toHaveBeenCalledWith("/test", {
334
+ enabled: true,
335
+ current: "green",
336
+ });
337
+ });
284
338
  });
285
339
 
286
340
  // ── executeTddStatus ────────────────────────────────────────────────────────
287
341
 
288
342
  describe("executeTddStatus", () => {
289
- let mockLoadTddState: ReturnType<typeof vi.fn>;
290
- let mockTddLog: ReturnType<typeof vi.fn>;
291
-
292
- function makeDeps(overrides: Partial<TddStatusDeps> = {}): TddStatusDeps {
293
- return {
294
- loadTddState: mockLoadTddState,
295
- tddLog: mockTddLog,
296
- ...overrides,
297
- };
298
- }
299
-
300
- beforeEach(() => {
301
- vi.clearAllMocks();
302
- mockLoadTddState = vi.fn();
303
- mockTddLog = vi.fn();
304
- });
305
-
306
- it("returns config error when TDD not setup", async () => {
307
- mockLoadTddState.mockReturnValue({
308
- ok: false,
309
- reason: "Missing .pi/tdd/ directory.",
310
- });
311
- const result = await executeTddStatus({ cwd: "/test" } as any, makeDeps());
312
- expect(result.content[0].text).toContain("Missing .pi/tdd/");
313
- });
314
-
315
- it("returns disabled message when TDD disabled", async () => {
316
- mockLoadTddState.mockReturnValue({
317
- ok: true,
318
- state: { enabled: false, current: "red" },
319
- config: CONFIG,
320
- });
321
- const result = await executeTddStatus({ cwd: "/test" } as any, makeDeps());
322
- expect(result.content[0].text).toContain("not enabled");
323
- });
324
-
325
- it("returns status details when TDD enabled", async () => {
326
- mockLoadTddState.mockReturnValue({
327
- ok: true,
328
- state: { enabled: true, current: "red" },
329
- config: CONFIG,
330
- });
331
- const result = await executeTddStatus({ cwd: "/test" } as any, makeDeps());
332
- expect(result.content[0].text).toContain("enabled");
333
- expect(result.content[0].text).toContain("RED");
334
- expect(result.content[0].text).toContain("tests/**/*.test.ts");
335
- expect(result.content[0].text).toContain("src/**/*.ts");
336
- expect(result.details).toBeDefined();
337
- expect(result.details!.enabled).toBe(true);
338
- expect(result.details!.phase).toBe("red");
339
- });
340
-
341
- it("returns status with current phase", async () => {
342
- mockLoadTddState.mockReturnValue({
343
- ok: true,
344
- state: { enabled: true, current: "green" },
345
- config: CONFIG,
346
- });
347
- const result = await executeTddStatus({ cwd: "/test" } as any, makeDeps());
348
- expect(result.content[0].text).toContain("GREEN");
349
- expect(result.details!.phase).toBe("green");
350
- });
343
+ let mockLoadTddState: ReturnType<typeof vi.fn>;
344
+ let mockTddLog: ReturnType<typeof vi.fn>;
345
+
346
+ function makeDeps(overrides: Partial<TddStatusDeps> = {}): TddStatusDeps {
347
+ return {
348
+ loadTddState: mockLoadTddState,
349
+ tddLog: mockTddLog,
350
+ ...overrides,
351
+ };
352
+ }
353
+
354
+ beforeEach(() => {
355
+ vi.clearAllMocks();
356
+ mockLoadTddState = vi.fn();
357
+ mockTddLog = vi.fn();
358
+ });
359
+
360
+ it("throws when TDD not setup", async () => {
361
+ mockLoadTddState.mockReturnValue({
362
+ ok: false,
363
+ reason: "Missing .pi/tdd/ directory.",
364
+ });
365
+ await expect(
366
+ executeTddStatus({ cwd: "/test" } as any, makeDeps()),
367
+ ).rejects.toThrow("Missing .pi/tdd/");
368
+ });
369
+
370
+ it("throws when TDD disabled", async () => {
371
+ mockLoadTddState.mockReturnValue({
372
+ ok: true,
373
+ state: { enabled: false, current: "red" },
374
+ config: CONFIG,
375
+ });
376
+ await expect(
377
+ executeTddStatus({ cwd: "/test" } as any, makeDeps()),
378
+ ).rejects.toThrow("not enabled");
379
+ });
380
+
381
+ it("returns status details when TDD enabled", async () => {
382
+ mockLoadTddState.mockReturnValue({
383
+ ok: true,
384
+ state: { enabled: true, current: "red" },
385
+ config: CONFIG,
386
+ });
387
+ const result = await executeTddStatus({ cwd: "/test" } as any, makeDeps());
388
+ expect(result.content[0].text).toContain("enabled");
389
+ expect(result.content[0].text).toContain("RED");
390
+ expect(result.content[0].text).toContain("tests/**/*.test.ts");
391
+ expect(result.content[0].text).toContain("src/**/*.ts");
392
+ expect(result.details).toBeDefined();
393
+ expect(result.details?.enabled).toBe(true);
394
+ expect(result.details?.phase).toBe("red");
395
+ });
396
+
397
+ it("returns status with current phase", async () => {
398
+ mockLoadTddState.mockReturnValue({
399
+ ok: true,
400
+ state: { enabled: true, current: "green" },
401
+ config: CONFIG,
402
+ });
403
+ const result = await executeTddStatus({ cwd: "/test" } as any, makeDeps());
404
+ expect(result.content[0].text).toContain("GREEN");
405
+ expect(result.details?.phase).toBe("green");
406
+ });
351
407
  });