tdd-enforcer 0.2.6 → 0.2.7

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,411 @@
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("returns config error 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
+ const result = await executeNextPhase({ cwd: "/test" } as any, makeDeps());
82
+ expect(result.content[0].text).toContain("Missing .pi/tdd/");
83
+ });
84
+
85
+ it("returns disabled message when TDD disabled", async () => {
86
+ mockLoadTddState.mockReturnValue({
87
+ ok: true,
88
+ state: { enabled: false, current: "red" },
89
+ config: CONFIG,
90
+ });
91
+ const result = await executeNextPhase({ cwd: "/test" } as any, makeDeps());
92
+ expect(result.content[0].text).toContain("not enabled");
93
+ });
94
+
95
+ it("blocks when allowlist violations exist", async () => {
96
+ mockLoadTddState.mockReturnValue({
97
+ ok: true,
98
+ state: { enabled: true, current: "red" },
99
+ config: CONFIG,
100
+ });
101
+ mockGetDisallowedChanges.mockReturnValue(["src/violation.ts"]);
102
+ const result = await executeNextPhase({ cwd: "/test" } as any, makeDeps());
103
+ expect(result.content[0].text).toContain("BLOCKED");
104
+ expect(result.content[0].text).toContain("src/violation.ts");
105
+ });
106
+
107
+ it("blocks red→green when tests pass (need failing test)", async () => {
108
+ mockLoadTddState.mockReturnValue({
109
+ ok: true,
110
+ state: { enabled: true, current: "red" },
111
+ config: CONFIG,
112
+ });
113
+ mockCheckGate.mockResolvedValue({
114
+ passed: false,
115
+ message:
116
+ "Tests passed. Add a failing test before transitioning to GREEN.",
117
+ });
118
+ const result = await executeNextPhase({ cwd: "/test" } as any, makeDeps());
119
+ expect(result.content[0].text).toContain("Add a failing test");
120
+ expect(result.content[0].text).toContain("GREEN");
121
+ });
122
+
123
+ it("blocks green→refactor when tests fail", async () => {
124
+ mockLoadTddState.mockReturnValue({
125
+ ok: true,
126
+ state: { enabled: true, current: "green" },
127
+ config: CONFIG,
128
+ });
129
+ mockCheckGate.mockResolvedValue({
130
+ passed: false,
131
+ message: "Tests failed. Fix them before transitioning to REFACTOR.",
132
+ });
133
+ const result = await executeNextPhase({ cwd: "/test" } as any, makeDeps());
134
+ expect(result.content[0].text).toContain("failed");
135
+ expect(result.content[0].text).toContain("REFACTOR");
136
+ });
137
+
138
+ it("advances red→green when tests fail", async () => {
139
+ mockLoadTddState.mockReturnValue({
140
+ ok: true,
141
+ state: { enabled: true, current: "red" },
142
+ config: CONFIG,
143
+ });
144
+ mockCheckGate.mockResolvedValue({
145
+ passed: true,
146
+ message: "Tests fail — proceed to GREEN.",
147
+ });
148
+ mockGetNudgePrompt.mockReturnValue("You are now in **GREEN** phase.");
149
+ const result = await executeNextPhase({ cwd: "/test" } as any, makeDeps());
150
+ expect(result.content[0].text).toContain("GREEN");
151
+ expect(mockSnapshot).toHaveBeenCalledWith("/test", "red");
152
+ expect(mockSavePhaseState).toHaveBeenCalledWith("/test", {
153
+ enabled: true,
154
+ current: "green",
155
+ });
156
+ });
157
+
158
+ it("advances green→refactor when tests pass", async () => {
159
+ mockLoadTddState.mockReturnValue({
160
+ ok: true,
161
+ state: { enabled: true, current: "green" },
162
+ config: CONFIG,
163
+ });
164
+ mockCheckGate.mockResolvedValue({
165
+ passed: true,
166
+ message: "All tests pass proceeding.",
167
+ });
168
+ mockGetNudgePrompt.mockReturnValue("You are now in **REFACTOR** phase.");
169
+ const result = await executeNextPhase({ cwd: "/test" } as any, makeDeps());
170
+ expect(result.content[0].text).toContain("REFACTOR");
171
+ expect(mockSnapshot).toHaveBeenCalledWith("/test", "green");
172
+ expect(mockSavePhaseState).toHaveBeenCalledWith("/test", {
173
+ enabled: true,
174
+ current: "refactor",
175
+ });
176
+ });
177
+
178
+ it("blocks refactor→red when tests fail", async () => {
179
+ mockLoadTddState.mockReturnValue({
180
+ ok: true,
181
+ state: { enabled: true, current: "refactor" },
182
+ config: CONFIG,
183
+ });
184
+ mockCheckGate.mockResolvedValue({
185
+ passed: false,
186
+ message: "Tests failed. Fix them before transitioning to RED.",
187
+ });
188
+ const result = await executeNextPhase({ cwd: "/test" } as any, makeDeps());
189
+ expect(result.content[0].text).toContain("failed");
190
+ expect(result.content[0].text).toContain("RED");
191
+ expect(mockSavePhaseState).not.toHaveBeenCalled();
192
+ });
193
+
194
+ it("advances refactor→red when tests pass", async () => {
195
+ mockLoadTddState.mockReturnValue({
196
+ ok: true,
197
+ state: { enabled: true, current: "refactor" },
198
+ config: CONFIG,
199
+ });
200
+ mockCheckGate.mockResolvedValue({
201
+ passed: true,
202
+ message: "All tests pass — proceeding.",
203
+ });
204
+ mockGetNudgePrompt.mockReturnValue("You are now in **RED** phase.");
205
+ const result = await executeNextPhase({ cwd: "/test" } as any, makeDeps());
206
+ expect(result.content[0].text).toContain("RED");
207
+ expect(mockSnapshot).toHaveBeenCalledWith("/test", "refactor");
208
+ expect(mockSavePhaseState).toHaveBeenCalledWith("/test", {
209
+ enabled: true,
210
+ current: "red",
211
+ });
212
+ });
179
213
  });
180
214
 
181
215
  // ── executePreviousPhase ────────────────────────────────────────────────────
182
216
 
183
217
  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
- });
218
+ let mockLoadTddState: ReturnType<typeof vi.fn>;
219
+ let mockHasParent: ReturnType<typeof vi.fn>;
220
+ let mockHeadMessage: ReturnType<typeof vi.fn>;
221
+ let mockResetHard: ReturnType<typeof vi.fn>;
222
+ let mockUndoLastCommit: ReturnType<typeof vi.fn>;
223
+ let mockSavePhaseState: ReturnType<typeof vi.fn>;
224
+ let mockTddLog: ReturnType<typeof vi.fn>;
225
+
226
+ function makeDeps(
227
+ overrides: Partial<PreviousPhaseDeps> = {},
228
+ ): PreviousPhaseDeps {
229
+ return {
230
+ loadTddState: mockLoadTddState,
231
+ hasParent: mockHasParent,
232
+ headMessage: mockHeadMessage,
233
+ resetHard: mockResetHard,
234
+ undoLastCommit: mockUndoLastCommit,
235
+ savePhaseState: mockSavePhaseState,
236
+ tddLog: mockTddLog,
237
+ ...overrides,
238
+ };
239
+ }
240
+
241
+ beforeEach(() => {
242
+ vi.clearAllMocks();
243
+ mockLoadTddState = vi.fn();
244
+ mockHasParent = vi.fn();
245
+ mockHeadMessage = vi.fn();
246
+ mockResetHard = vi.fn();
247
+ mockUndoLastCommit = vi.fn();
248
+ mockSavePhaseState = vi.fn();
249
+ mockTddLog = vi.fn();
250
+
251
+ mockHasParent.mockReturnValue(true);
252
+ });
253
+
254
+ it("returns config error when TDD not setup", async () => {
255
+ mockLoadTddState.mockReturnValue({
256
+ ok: false,
257
+ reason: "Missing .pi/tdd/ directory.",
258
+ });
259
+ const result = await executePreviousPhase(
260
+ { cwd: "/test" } as any,
261
+ makeDeps(),
262
+ );
263
+ expect(result.content[0].text).toContain("Missing .pi/tdd/");
264
+ });
265
+
266
+ it("returns disabled message when TDD disabled", async () => {
267
+ mockLoadTddState.mockReturnValue({
268
+ ok: true,
269
+ state: { enabled: false, current: "red" },
270
+ config: CONFIG,
271
+ });
272
+ const result = await executePreviousPhase(
273
+ { cwd: "/test" } as any,
274
+ makeDeps(),
275
+ );
276
+ expect(result.content[0].text).toContain("not enabled");
277
+ });
278
+
279
+ it("returns no-parent message when only init commit exists", async () => {
280
+ mockLoadTddState.mockReturnValue({
281
+ ok: true,
282
+ state: { enabled: true, current: "red" },
283
+ config: CONFIG,
284
+ });
285
+ mockHasParent.mockReturnValue(false);
286
+ const result = await executePreviousPhase(
287
+ { cwd: "/test" } as any,
288
+ makeDeps(),
289
+ );
290
+ expect(result.content[0].text).toContain("No previous phase");
291
+ });
292
+
293
+ it("returns error when HEAD message is not a TDD snapshot", async () => {
294
+ mockLoadTddState.mockReturnValue({
295
+ ok: true,
296
+ state: { enabled: true, current: "red" },
297
+ config: CONFIG,
298
+ });
299
+ mockHeadMessage.mockReturnValue("garbage");
300
+ const result = await executePreviousPhase(
301
+ { cwd: "/test" } as any,
302
+ makeDeps(),
303
+ );
304
+ expect(result.content[0].text).toContain("not a TDD snapshot");
305
+ });
306
+
307
+ it("reverts to previous phase on success", async () => {
308
+ mockLoadTddState.mockReturnValue({
309
+ ok: true,
310
+ state: { enabled: true, current: "green" },
311
+ config: CONFIG,
312
+ });
313
+ mockHeadMessage.mockReturnValue("tdd: red");
314
+ const result = await executePreviousPhase(
315
+ { cwd: "/test" } as any,
316
+ makeDeps(),
317
+ );
318
+ expect(result.content[0].text).toContain("RED");
319
+ expect(mockResetHard).toHaveBeenCalledWith("/test");
320
+ expect(mockUndoLastCommit).toHaveBeenCalledWith("/test");
321
+ expect(mockSavePhaseState).toHaveBeenCalledWith("/test", {
322
+ enabled: true,
323
+ current: "red",
324
+ });
325
+ });
326
+
327
+ it("reverts to correct phase from green head label", async () => {
328
+ mockLoadTddState.mockReturnValue({
329
+ ok: true,
330
+ state: { enabled: true, current: "refactor" },
331
+ config: CONFIG,
332
+ });
333
+ mockHeadMessage.mockReturnValue("tdd: green");
334
+ const result = await executePreviousPhase(
335
+ { cwd: "/test" } as any,
336
+ makeDeps(),
337
+ );
338
+ expect(result.content[0].text).toContain("GREEN");
339
+ expect(mockSavePhaseState).toHaveBeenCalledWith("/test", {
340
+ enabled: true,
341
+ current: "green",
342
+ });
343
+ });
284
344
  });
285
345
 
286
346
  // ── executeTddStatus ────────────────────────────────────────────────────────
287
347
 
288
348
  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
- });
349
+ let mockLoadTddState: ReturnType<typeof vi.fn>;
350
+ let mockTddLog: ReturnType<typeof vi.fn>;
351
+
352
+ function makeDeps(overrides: Partial<TddStatusDeps> = {}): TddStatusDeps {
353
+ return {
354
+ loadTddState: mockLoadTddState,
355
+ tddLog: mockTddLog,
356
+ ...overrides,
357
+ };
358
+ }
359
+
360
+ beforeEach(() => {
361
+ vi.clearAllMocks();
362
+ mockLoadTddState = vi.fn();
363
+ mockTddLog = vi.fn();
364
+ });
365
+
366
+ it("returns config error when TDD not setup", async () => {
367
+ mockLoadTddState.mockReturnValue({
368
+ ok: false,
369
+ reason: "Missing .pi/tdd/ directory.",
370
+ });
371
+ const result = await executeTddStatus({ cwd: "/test" } as any, makeDeps());
372
+ expect(result.content[0].text).toContain("Missing .pi/tdd/");
373
+ });
374
+
375
+ it("returns disabled message when TDD disabled", async () => {
376
+ mockLoadTddState.mockReturnValue({
377
+ ok: true,
378
+ state: { enabled: false, current: "red" },
379
+ config: CONFIG,
380
+ });
381
+ const result = await executeTddStatus({ cwd: "/test" } as any, makeDeps());
382
+ expect(result.content[0].text).toContain("not enabled");
383
+ });
384
+
385
+ it("returns status details when TDD enabled", async () => {
386
+ mockLoadTddState.mockReturnValue({
387
+ ok: true,
388
+ state: { enabled: true, current: "red" },
389
+ config: CONFIG,
390
+ });
391
+ const result = await executeTddStatus({ cwd: "/test" } as any, makeDeps());
392
+ expect(result.content[0].text).toContain("enabled");
393
+ expect(result.content[0].text).toContain("RED");
394
+ expect(result.content[0].text).toContain("tests/**/*.test.ts");
395
+ expect(result.content[0].text).toContain("src/**/*.ts");
396
+ expect(result.details).toBeDefined();
397
+ expect(result.details?.enabled).toBe(true);
398
+ expect(result.details?.phase).toBe("red");
399
+ });
400
+
401
+ it("returns status with current phase", async () => {
402
+ mockLoadTddState.mockReturnValue({
403
+ ok: true,
404
+ state: { enabled: true, current: "green" },
405
+ config: CONFIG,
406
+ });
407
+ const result = await executeTddStatus({ cwd: "/test" } as any, makeDeps());
408
+ expect(result.content[0].text).toContain("GREEN");
409
+ expect(result.details?.phase).toBe("green");
410
+ });
351
411
  });