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.
- package/.github/workflows/ci.yml +28 -0
- package/adapters/pi/helpers.test.ts +246 -244
- package/adapters/pi/helpers.ts +117 -91
- package/adapters/pi/hooks.test.ts +528 -425
- package/adapters/pi/hooks.ts +297 -230
- package/adapters/pi/index.test.ts +388 -272
- package/adapters/pi/index.ts +284 -200
- package/adapters/pi/log.test.ts +91 -91
- package/adapters/pi/log.ts +39 -27
- package/adapters/pi/prompts.test.ts +25 -25
- package/adapters/pi/prompts.ts +29 -30
- package/adapters/pi/tools.test.ts +387 -331
- package/adapters/pi/tools.ts +321 -325
- package/behaviour.md +15 -1
- package/biome.json +37 -0
- package/engine/config.test.ts +157 -157
- package/engine/config.ts +22 -19
- package/engine/enforce.test.ts +155 -145
- package/engine/enforce.ts +31 -23
- package/engine/git.test.ts +529 -507
- package/engine/git.ts +240 -114
- package/engine/index.ts +27 -4
- package/engine/state.test.ts +69 -69
- package/engine/state.ts +22 -20
- package/engine/transition.test.ts +229 -177
- package/engine/transition.ts +55 -52
- package/engine/types.ts +9 -9
- package/package.json +30 -23
- package/tsconfig.json +10 -10
|
@@ -1,351 +1,407 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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
|
});
|