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,302 +1,418 @@
1
- import { describe, it, expect, beforeEach, vi } from "vitest";
2
- import { handleTddOn, handleTddOff, handleTddStatus, handleTddReset } from "./index.js";
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+ import {
3
+ handleTddJump,
4
+ handleTddOff,
5
+ handleTddOn,
6
+ handleTddReset,
7
+ handleTddStatus,
8
+ } from "./index.js";
3
9
 
4
10
  const config = {
5
- blockedInRed: ["tests/**/*.test.ts"],
6
- blockedInGreen: ["src/**/*.ts"],
7
- testCommands: ["npm test"],
8
- timeoutSeconds: 30,
11
+ blockedInRed: ["tests/**/*.test.ts"],
12
+ blockedInGreen: ["src/**/*.ts"],
13
+ testCommands: ["npm test"],
14
+ timeoutSeconds: 30,
9
15
  };
10
16
 
11
17
  // ── Helpers ─────────────────────────────────────────────────────────────────
12
18
 
13
19
  function makeCtx(dir = "/test") {
14
- const notifications: Array<{ message: string; type: string }> = [];
15
- return {
16
- cwd: dir,
17
- ui: { notify: (message: string, type: string) => notifications.push({ message, type }) },
18
- notifications, // stored alongside for easy assertion
19
- } as any;
20
+ const notifications: Array<{ message: string; type: string }> = [];
21
+ return {
22
+ cwd: dir,
23
+ ui: {
24
+ notify: (message: string, type: string) =>
25
+ notifications.push({ message, type }),
26
+ },
27
+ notifications, // stored alongside for easy assertion
28
+ } as any;
20
29
  }
21
30
 
22
- function captureNotifications(ctx: any): Array<{ message: string; type: string }> {
23
- return ctx.notifications;
31
+ function _captureNotifications(
32
+ ctx: any,
33
+ ): Array<{ message: string; type: string }> {
34
+ return ctx.notifications;
24
35
  }
25
36
 
26
37
  // ── handleTddOn ─────────────────────────────────────────────────────────────
27
38
 
28
39
  describe("handleTddOn", () => {
29
- let mockLoadTddState: ReturnType<typeof vi.fn>;
30
- let mockSnapshot: ReturnType<typeof vi.fn>;
31
- let mockSavePhaseState: ReturnType<typeof vi.fn>;
32
- let mockTddLog: ReturnType<typeof vi.fn>;
33
-
34
- function makeDeps(overrides = {}) {
35
- return {
36
- loadTddState: mockLoadTddState,
37
- snapshot: mockSnapshot,
38
- savePhaseState: mockSavePhaseState,
39
- tddLog: mockTddLog,
40
- ...overrides,
41
- };
42
- }
43
-
44
- beforeEach(() => {
45
- mockLoadTddState = vi.fn();
46
- mockSnapshot = vi.fn().mockReturnValue("hash123");
47
- mockSavePhaseState = vi.fn();
48
- mockTddLog = vi.fn();
49
- });
50
-
51
- it("enables TDD, takes snapshot, notifies user", async () => {
52
- mockLoadTddState.mockReturnValue({
53
- ok: true,
54
- state: { enabled: false, current: "red" },
55
- config,
56
- });
57
-
58
- const ctx = makeCtx();
59
- await handleTddOn(ctx, makeDeps());
60
-
61
- expect(mockSnapshot).toHaveBeenCalledWith("/test", "red");
62
- expect(mockSavePhaseState).toHaveBeenCalledWith("/test", {
63
- enabled: true,
64
- current: "red",
65
- });
66
- expect(ctx.notifications).toHaveLength(1);
67
- expect(ctx.notifications[0].message).toContain("TDD enabled");
68
- expect(ctx.notifications[0].type).toBe("info");
69
- });
70
-
71
- it("shows already enabled when TDD already on", async () => {
72
- mockLoadTddState.mockReturnValue({
73
- ok: true,
74
- state: { enabled: true, current: "red" },
75
- config,
76
- });
77
-
78
- const ctx = makeCtx();
79
- await handleTddOn(ctx, makeDeps());
80
-
81
- expect(ctx.notifications).toHaveLength(1);
82
- expect(ctx.notifications[0].message).toContain("already enabled");
83
- expect(ctx.notifications[0].type).toBe("info");
84
- expect(mockSnapshot).not.toHaveBeenCalled();
85
- });
86
-
87
- it("shows error when setup invalid", async () => {
88
- mockLoadTddState.mockReturnValue({
89
- ok: false,
90
- reason: "Missing .pi/tdd/",
91
- });
92
-
93
- const ctx = makeCtx();
94
- await handleTddOn(ctx, makeDeps());
95
-
96
- expect(ctx.notifications).toHaveLength(1);
97
- expect(ctx.notifications[0].message).toContain("Missing .pi/tdd/");
98
- expect(ctx.notifications[0].type).toBe("error");
99
- });
40
+ let mockLoadTddState: ReturnType<typeof vi.fn>;
41
+ let mockSnapshot: ReturnType<typeof vi.fn>;
42
+ let mockSavePhaseState: ReturnType<typeof vi.fn>;
43
+ let mockTddLog: ReturnType<typeof vi.fn>;
44
+
45
+ function makeDeps(overrides = {}) {
46
+ return {
47
+ loadTddState: mockLoadTddState,
48
+ snapshot: mockSnapshot,
49
+ savePhaseState: mockSavePhaseState,
50
+ tddLog: mockTddLog,
51
+ ...overrides,
52
+ };
53
+ }
54
+
55
+ beforeEach(() => {
56
+ mockLoadTddState = vi.fn();
57
+ mockSnapshot = vi.fn().mockReturnValue("hash123");
58
+ mockSavePhaseState = vi.fn();
59
+ mockTddLog = vi.fn();
60
+ });
61
+
62
+ it("enables TDD, takes snapshot, notifies user", async () => {
63
+ mockLoadTddState.mockReturnValue({
64
+ ok: true,
65
+ state: { enabled: false, current: "red" },
66
+ config,
67
+ });
68
+
69
+ const ctx = makeCtx();
70
+ await handleTddOn(ctx, makeDeps());
71
+
72
+ expect(mockSnapshot).toHaveBeenCalledWith("/test", "red");
73
+ expect(mockSavePhaseState).toHaveBeenCalledWith("/test", {
74
+ enabled: true,
75
+ current: "red",
76
+ });
77
+ expect(ctx.notifications).toHaveLength(1);
78
+ expect(ctx.notifications[0].message).toContain("TDD enabled");
79
+ expect(ctx.notifications[0].type).toBe("info");
80
+ });
81
+
82
+ it("shows already enabled when TDD already on", async () => {
83
+ mockLoadTddState.mockReturnValue({
84
+ ok: true,
85
+ state: { enabled: true, current: "red" },
86
+ config,
87
+ });
88
+
89
+ const ctx = makeCtx();
90
+ await handleTddOn(ctx, makeDeps());
91
+
92
+ expect(ctx.notifications).toHaveLength(1);
93
+ expect(ctx.notifications[0].message).toContain("already enabled");
94
+ expect(ctx.notifications[0].type).toBe("info");
95
+ expect(mockSnapshot).not.toHaveBeenCalled();
96
+ });
97
+
98
+ it("shows error when setup invalid", async () => {
99
+ mockLoadTddState.mockReturnValue({
100
+ ok: false,
101
+ reason: "Missing .pi/tdd/",
102
+ });
103
+
104
+ const ctx = makeCtx();
105
+ await handleTddOn(ctx, makeDeps());
106
+
107
+ expect(ctx.notifications).toHaveLength(1);
108
+ expect(ctx.notifications[0].message).toContain("Missing .pi/tdd/");
109
+ expect(ctx.notifications[0].type).toBe("error");
110
+ });
100
111
  });
101
112
 
102
113
  // ── handleTddOff ────────────────────────────────────────────────────────────
103
114
 
104
115
  describe("handleTddOff", () => {
105
- let mockLoadTddState: ReturnType<typeof vi.fn>;
106
- let mockSavePhaseState: ReturnType<typeof vi.fn>;
107
- let mockTddLog: ReturnType<typeof vi.fn>;
108
-
109
- function makeDeps(overrides = {}) {
110
- return {
111
- loadTddState: mockLoadTddState,
112
- savePhaseState: mockSavePhaseState,
113
- tddLog: mockTddLog,
114
- ...overrides,
115
- };
116
- }
117
-
118
- beforeEach(() => {
119
- mockLoadTddState = vi.fn();
120
- mockSavePhaseState = vi.fn();
121
- mockTddLog = vi.fn();
122
- });
123
-
124
- it("disables TDD, notifies user", async () => {
125
- mockLoadTddState.mockReturnValue({
126
- ok: true,
127
- state: { enabled: true, current: "red" },
128
- config,
129
- });
130
-
131
- const ctx = makeCtx();
132
- await handleTddOff(ctx, makeDeps());
133
-
134
- expect(mockSavePhaseState).toHaveBeenCalledWith("/test", {
135
- enabled: false,
136
- current: "red",
137
- });
138
- expect(ctx.notifications).toHaveLength(1);
139
- expect(ctx.notifications[0].message).toContain("disabled");
140
- expect(ctx.notifications[0].type).toBe("info");
141
- });
142
-
143
- it("shows already disabled when TDD already off", async () => {
144
- mockLoadTddState.mockReturnValue({
145
- ok: true,
146
- state: { enabled: false, current: "red" },
147
- config,
148
- });
149
-
150
- const ctx = makeCtx();
151
- await handleTddOff(ctx, makeDeps());
152
-
153
- expect(ctx.notifications).toHaveLength(1);
154
- expect(ctx.notifications[0].message).toContain("already disabled");
155
- expect(ctx.notifications[0].type).toBe("info");
156
- expect(mockSavePhaseState).not.toHaveBeenCalled();
157
- });
158
-
159
- it("shows error when setup invalid", async () => {
160
- mockLoadTddState.mockReturnValue({
161
- ok: false,
162
- reason: "Missing .pi/tdd/",
163
- });
164
-
165
- const ctx = makeCtx();
166
- await handleTddOff(ctx, makeDeps());
167
-
168
- expect(ctx.notifications).toHaveLength(1);
169
- expect(ctx.notifications[0].message).toContain("Missing .pi/tdd/");
170
- expect(ctx.notifications[0].type).toBe("error");
171
- });
116
+ let mockLoadTddState: ReturnType<typeof vi.fn>;
117
+ let mockSavePhaseState: ReturnType<typeof vi.fn>;
118
+ let mockTddLog: ReturnType<typeof vi.fn>;
119
+
120
+ function makeDeps(overrides = {}) {
121
+ return {
122
+ loadTddState: mockLoadTddState,
123
+ savePhaseState: mockSavePhaseState,
124
+ tddLog: mockTddLog,
125
+ ...overrides,
126
+ };
127
+ }
128
+
129
+ beforeEach(() => {
130
+ mockLoadTddState = vi.fn();
131
+ mockSavePhaseState = vi.fn();
132
+ mockTddLog = vi.fn();
133
+ });
134
+
135
+ it("disables TDD, notifies user", async () => {
136
+ mockLoadTddState.mockReturnValue({
137
+ ok: true,
138
+ state: { enabled: true, current: "red" },
139
+ config,
140
+ });
141
+
142
+ const ctx = makeCtx();
143
+ await handleTddOff(ctx, makeDeps());
144
+
145
+ expect(mockSavePhaseState).toHaveBeenCalledWith("/test", {
146
+ enabled: false,
147
+ current: "red",
148
+ });
149
+ expect(ctx.notifications).toHaveLength(1);
150
+ expect(ctx.notifications[0].message).toContain("disabled");
151
+ expect(ctx.notifications[0].type).toBe("info");
152
+ });
153
+
154
+ it("shows already disabled when TDD already off", async () => {
155
+ mockLoadTddState.mockReturnValue({
156
+ ok: true,
157
+ state: { enabled: false, current: "red" },
158
+ config,
159
+ });
160
+
161
+ const ctx = makeCtx();
162
+ await handleTddOff(ctx, makeDeps());
163
+
164
+ expect(ctx.notifications).toHaveLength(1);
165
+ expect(ctx.notifications[0].message).toContain("already disabled");
166
+ expect(ctx.notifications[0].type).toBe("info");
167
+ expect(mockSavePhaseState).not.toHaveBeenCalled();
168
+ });
169
+
170
+ it("shows error when setup invalid", async () => {
171
+ mockLoadTddState.mockReturnValue({
172
+ ok: false,
173
+ reason: "Missing .pi/tdd/",
174
+ });
175
+
176
+ const ctx = makeCtx();
177
+ await handleTddOff(ctx, makeDeps());
178
+
179
+ expect(ctx.notifications).toHaveLength(1);
180
+ expect(ctx.notifications[0].message).toContain("Missing .pi/tdd/");
181
+ expect(ctx.notifications[0].type).toBe("error");
182
+ });
172
183
  });
173
184
 
174
185
  // ── handleTddStatus ─────────────────────────────────────────────────────────
175
186
 
176
187
  describe("handleTddStatus", () => {
177
- let mockLoadTddState: ReturnType<typeof vi.fn>;
178
- let mockTddLog: ReturnType<typeof vi.fn>;
179
-
180
- function makeDeps(overrides = {}) {
181
- return {
182
- loadTddState: mockLoadTddState,
183
- tddLog: mockTddLog,
184
- ...overrides,
185
- };
186
- }
187
-
188
- beforeEach(() => {
189
- mockLoadTddState = vi.fn();
190
- mockTddLog = vi.fn();
191
- });
192
-
193
- it("shows status when TDD enabled", async () => {
194
- mockLoadTddState.mockReturnValue({
195
- ok: true,
196
- state: { enabled: true, current: "green" },
197
- config,
198
- });
199
-
200
- const ctx = makeCtx();
201
- await handleTddStatus(ctx, makeDeps());
202
-
203
- expect(ctx.notifications).toHaveLength(1);
204
- expect(ctx.notifications[0].message).toContain("enabled");
205
- expect(ctx.notifications[0].message).toContain("GREEN");
206
- expect(ctx.notifications[0].type).toBe("info");
207
- });
208
-
209
- it("shows status when TDD disabled", async () => {
210
- mockLoadTddState.mockReturnValue({
211
- ok: true,
212
- state: { enabled: false, current: "red" },
213
- config,
214
- });
215
-
216
- const ctx = makeCtx();
217
- await handleTddStatus(ctx, makeDeps());
218
-
219
- expect(ctx.notifications).toHaveLength(1);
220
- expect(ctx.notifications[0].message).toContain("disabled");
221
- expect(ctx.notifications[0].message).toContain("RED");
222
- expect(ctx.notifications[0].type).toBe("info");
223
- });
224
-
225
- it("shows error when setup invalid", async () => {
226
- mockLoadTddState.mockReturnValue({
227
- ok: false,
228
- reason: "Missing .pi/tdd/",
229
- });
230
-
231
- const ctx = makeCtx();
232
- await handleTddStatus(ctx, makeDeps());
233
-
234
- expect(ctx.notifications).toHaveLength(1);
235
- expect(ctx.notifications[0].message).toContain("Missing .pi/tdd/");
236
- expect(ctx.notifications[0].type).toBe("error");
237
- });
188
+ let mockLoadTddState: ReturnType<typeof vi.fn>;
189
+ let mockTddLog: ReturnType<typeof vi.fn>;
190
+
191
+ function makeDeps(overrides = {}) {
192
+ return {
193
+ loadTddState: mockLoadTddState,
194
+ tddLog: mockTddLog,
195
+ ...overrides,
196
+ };
197
+ }
198
+
199
+ beforeEach(() => {
200
+ mockLoadTddState = vi.fn();
201
+ mockTddLog = vi.fn();
202
+ });
203
+
204
+ it("shows status when TDD enabled", async () => {
205
+ mockLoadTddState.mockReturnValue({
206
+ ok: true,
207
+ state: { enabled: true, current: "green" },
208
+ config,
209
+ });
210
+
211
+ const ctx = makeCtx();
212
+ await handleTddStatus(ctx, makeDeps());
213
+
214
+ expect(ctx.notifications).toHaveLength(1);
215
+ expect(ctx.notifications[0].message).toContain("enabled");
216
+ expect(ctx.notifications[0].message).toContain("GREEN");
217
+ expect(ctx.notifications[0].type).toBe("info");
218
+ });
219
+
220
+ it("shows status when TDD disabled", async () => {
221
+ mockLoadTddState.mockReturnValue({
222
+ ok: true,
223
+ state: { enabled: false, current: "red" },
224
+ config,
225
+ });
226
+
227
+ const ctx = makeCtx();
228
+ await handleTddStatus(ctx, makeDeps());
229
+
230
+ expect(ctx.notifications).toHaveLength(1);
231
+ expect(ctx.notifications[0].message).toContain("disabled");
232
+ expect(ctx.notifications[0].message).toContain("RED");
233
+ expect(ctx.notifications[0].type).toBe("info");
234
+ });
235
+
236
+ it("shows error when setup invalid", async () => {
237
+ mockLoadTddState.mockReturnValue({
238
+ ok: false,
239
+ reason: "Missing .pi/tdd/",
240
+ });
241
+
242
+ const ctx = makeCtx();
243
+ await handleTddStatus(ctx, makeDeps());
244
+
245
+ expect(ctx.notifications).toHaveLength(1);
246
+ expect(ctx.notifications[0].message).toContain("Missing .pi/tdd/");
247
+ expect(ctx.notifications[0].type).toBe("error");
248
+ });
238
249
  });
239
250
 
240
251
  // ── handleTddReset ──────────────────────────────────────────────────────────
241
252
 
242
253
  describe("handleTddReset", () => {
243
- let mockLoadTddState: ReturnType<typeof vi.fn>;
244
- let mockResetGit: ReturnType<typeof vi.fn>;
245
- let mockSnapshot: ReturnType<typeof vi.fn>;
246
- let mockSavePhaseState: ReturnType<typeof vi.fn>;
247
- let mockTddLog: ReturnType<typeof vi.fn>;
248
-
249
- function makeDeps(overrides = {}) {
250
- return {
251
- loadTddState: mockLoadTddState,
252
- resetGit: mockResetGit,
253
- snapshot: mockSnapshot,
254
- savePhaseState: mockSavePhaseState,
255
- tddLog: mockTddLog,
256
- ...overrides,
257
- };
258
- }
259
-
260
- beforeEach(() => {
261
- mockLoadTddState = vi.fn();
262
- mockResetGit = vi.fn();
263
- mockSnapshot = vi.fn().mockReturnValue("hash123");
264
- mockSavePhaseState = vi.fn();
265
- mockTddLog = vi.fn();
266
- });
267
-
268
- it("nukes git, re-inits, snapshots, resets state to RED disabled", async () => {
269
- mockLoadTddState.mockReturnValue({
270
- ok: true,
271
- state: { enabled: true, current: "green" },
272
- config,
273
- });
274
-
275
- const ctx = makeCtx();
276
- await handleTddReset(ctx, makeDeps());
277
-
278
- expect(mockResetGit).toHaveBeenCalledWith("/test");
279
- expect(mockSnapshot).toHaveBeenCalledWith("/test", "red");
280
- expect(mockSavePhaseState).toHaveBeenCalledWith("/test", {
281
- enabled: false,
282
- current: "red",
283
- });
284
- expect(ctx.notifications).toHaveLength(1);
285
- expect(ctx.notifications[0].message).toContain("reset");
286
- expect(ctx.notifications[0].type).toBe("warning");
287
- });
288
-
289
- it("shows error when setup invalid", async () => {
290
- mockLoadTddState.mockReturnValue({
291
- ok: false,
292
- reason: "Missing .pi/tdd/",
293
- });
294
-
295
- const ctx = makeCtx();
296
- await handleTddReset(ctx, makeDeps());
297
-
298
- expect(ctx.notifications).toHaveLength(1);
299
- expect(ctx.notifications[0].message).toContain("Missing .pi/tdd/");
300
- expect(ctx.notifications[0].type).toBe("error");
301
- });
254
+ let mockLoadTddState: ReturnType<typeof vi.fn>;
255
+ let mockResetGit: ReturnType<typeof vi.fn>;
256
+ let mockSnapshot: ReturnType<typeof vi.fn>;
257
+ let mockSavePhaseState: ReturnType<typeof vi.fn>;
258
+ let mockTddLog: ReturnType<typeof vi.fn>;
259
+
260
+ function makeDeps(overrides = {}) {
261
+ return {
262
+ loadTddState: mockLoadTddState,
263
+ resetGit: mockResetGit,
264
+ snapshot: mockSnapshot,
265
+ savePhaseState: mockSavePhaseState,
266
+ tddLog: mockTddLog,
267
+ ...overrides,
268
+ };
269
+ }
270
+
271
+ beforeEach(() => {
272
+ mockLoadTddState = vi.fn();
273
+ mockResetGit = vi.fn();
274
+ mockSnapshot = vi.fn().mockReturnValue("hash123");
275
+ mockSavePhaseState = vi.fn();
276
+ mockTddLog = vi.fn();
277
+ });
278
+
279
+ it("nukes git, re-inits, snapshots, resets state to RED disabled", async () => {
280
+ mockLoadTddState.mockReturnValue({
281
+ ok: true,
282
+ state: { enabled: true, current: "green" },
283
+ config,
284
+ });
285
+
286
+ const ctx = makeCtx();
287
+ await handleTddReset(ctx, makeDeps());
288
+
289
+ expect(mockResetGit).toHaveBeenCalledWith("/test");
290
+ expect(mockSnapshot).toHaveBeenCalledWith("/test", "red");
291
+ expect(mockSavePhaseState).toHaveBeenCalledWith("/test", {
292
+ enabled: false,
293
+ current: "red",
294
+ });
295
+ expect(ctx.notifications).toHaveLength(1);
296
+ expect(ctx.notifications[0].message).toContain("reset");
297
+ expect(ctx.notifications[0].type).toBe("warning");
298
+ });
299
+
300
+ it("shows error when setup invalid", async () => {
301
+ mockLoadTddState.mockReturnValue({
302
+ ok: false,
303
+ reason: "Missing .pi/tdd/",
304
+ });
305
+
306
+ const ctx = makeCtx();
307
+ await handleTddReset(ctx, makeDeps());
308
+
309
+ expect(ctx.notifications).toHaveLength(1);
310
+ expect(ctx.notifications[0].message).toContain("Missing .pi/tdd/");
311
+ expect(ctx.notifications[0].type).toBe("error");
312
+ });
313
+ });
314
+
315
+ // ── handleTddJump ───────────────────────────────────────────────────────────
316
+
317
+ describe("handleTddJump", () => {
318
+ let mockLoadTddState: ReturnType<typeof vi.fn>;
319
+ let mockSnapshot: ReturnType<typeof vi.fn>;
320
+ let mockSavePhaseState: ReturnType<typeof vi.fn>;
321
+ let mockTddLog: ReturnType<typeof vi.fn>;
322
+
323
+ function makeDeps(overrides = {}) {
324
+ return {
325
+ loadTddState: mockLoadTddState,
326
+ snapshot: mockSnapshot,
327
+ savePhaseState: mockSavePhaseState,
328
+ tddLog: mockTddLog,
329
+ ...overrides,
330
+ };
331
+ }
332
+
333
+ beforeEach(() => {
334
+ vi.clearAllMocks();
335
+ mockLoadTddState = vi.fn();
336
+ mockSnapshot = vi.fn().mockReturnValue("snap123");
337
+ mockSavePhaseState = vi.fn();
338
+ mockTddLog = vi.fn();
339
+ });
340
+
341
+ function tddOk(overrides?: { current?: string; enabled?: boolean }) {
342
+ return {
343
+ ok: true as const,
344
+ state: {
345
+ enabled: overrides?.enabled ?? true,
346
+ current: overrides?.current ?? "red",
347
+ },
348
+ config,
349
+ };
350
+ }
351
+
352
+ it("shows error when TDD not setup", async () => {
353
+ mockLoadTddState.mockReturnValue({ ok: false, reason: "Missing .pi/tdd/" });
354
+ const ctx = makeCtx();
355
+ await handleTddJump("green", ctx, makeDeps());
356
+ expect(ctx.notifications[0].message).toContain("Missing");
357
+ expect(ctx.notifications[0].type).toBe("error");
358
+ expect(mockSavePhaseState).not.toHaveBeenCalled();
359
+ });
360
+
361
+ it("notifies no-op when already in target phase", async () => {
362
+ mockLoadTddState.mockReturnValue(tddOk({ current: "green" }));
363
+ const ctx = makeCtx();
364
+ await handleTddJump("green", ctx, makeDeps());
365
+ expect(ctx.notifications[0].message).toContain("already in GREEN");
366
+ expect(ctx.notifications[0].type).toBe("info");
367
+ expect(mockSnapshot).not.toHaveBeenCalled();
368
+ expect(mockSavePhaseState).not.toHaveBeenCalled();
369
+ });
370
+
371
+ it("snapshots, auto-enables, jumps phase, notifies", async () => {
372
+ mockLoadTddState.mockReturnValue(tddOk({ current: "red", enabled: true }));
373
+ const ctx = makeCtx();
374
+ await handleTddJump("green", ctx, makeDeps());
375
+
376
+ expect(mockSnapshot).toHaveBeenCalledWith("/test", "red");
377
+ expect(mockSavePhaseState).toHaveBeenCalledWith("/test", {
378
+ enabled: true,
379
+ current: "green",
380
+ });
381
+ expect(ctx.notifications[0].message).toContain("Skipped to GREEN phase");
382
+ expect(ctx.notifications[0].type).toBe("info");
383
+ });
384
+
385
+ it("auto-enables when TDD disabled", async () => {
386
+ mockLoadTddState.mockReturnValue(tddOk({ current: "red", enabled: false }));
387
+ const ctx = makeCtx();
388
+ await handleTddJump("green", ctx, makeDeps());
389
+
390
+ expect(mockSavePhaseState).toHaveBeenCalledWith("/test", {
391
+ enabled: true,
392
+ current: "green",
393
+ });
394
+ expect(ctx.notifications[0].message).toContain("Skipped to GREEN phase");
395
+ });
396
+
397
+ it("works for refactor from green", async () => {
398
+ mockLoadTddState.mockReturnValue(tddOk({ current: "green" }));
399
+ const ctx = makeCtx();
400
+ await handleTddJump("refactor", ctx, makeDeps());
401
+ expect(mockSavePhaseState).toHaveBeenCalledWith("/test", {
402
+ enabled: true,
403
+ current: "refactor",
404
+ });
405
+ expect(ctx.notifications[0].message).toContain("Skipped to REFACTOR phase");
406
+ });
407
+
408
+ it("works for red from green", async () => {
409
+ mockLoadTddState.mockReturnValue(tddOk({ current: "green" }));
410
+ const ctx = makeCtx();
411
+ await handleTddJump("red", ctx, makeDeps());
412
+ expect(mockSavePhaseState).toHaveBeenCalledWith("/test", {
413
+ enabled: true,
414
+ current: "red",
415
+ });
416
+ expect(ctx.notifications[0].message).toContain("Skipped to RED phase");
417
+ });
302
418
  });