tdd-enforcer 0.2.2 → 0.2.3
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/adapters/pi/helpers.test.ts +218 -160
- package/adapters/pi/helpers.ts +50 -17
- package/adapters/pi/hooks.test.ts +472 -0
- package/adapters/pi/hooks.ts +215 -157
- package/adapters/pi/index.test.ts +302 -0
- package/adapters/pi/index.ts +192 -135
- package/adapters/pi/log.test.ts +63 -41
- package/adapters/pi/log.ts +13 -4
- package/adapters/pi/prompts.test.ts +43 -0
- package/adapters/pi/tools.test.ts +351 -0
- package/adapters/pi/tools.ts +321 -218
- package/behaviour.md +26 -11
- package/engine/git.test.ts +432 -173
- package/engine/git.ts +80 -49
- package/engine/index.ts +1 -1
- package/engine/transition.test.ts +62 -57
- package/engine/transition.ts +9 -2
- package/package.json +4 -1
- package/tsconfig.json +12 -0
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from "vitest";
|
|
2
|
+
import { handleTddOn, handleTddOff, handleTddStatus, handleTddReset } from "./index.js";
|
|
3
|
+
|
|
4
|
+
const config = {
|
|
5
|
+
allowedRedPhaseFiles: ["tests/**/*.test.ts"],
|
|
6
|
+
allowedGreenPhaseFiles: ["src/**/*.ts"],
|
|
7
|
+
testCommands: ["npm test"],
|
|
8
|
+
timeoutSeconds: 30,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
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
|
+
}
|
|
21
|
+
|
|
22
|
+
function captureNotifications(ctx: any): Array<{ message: string; type: string }> {
|
|
23
|
+
return ctx.notifications;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ── handleTddOn ─────────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
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
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// ── handleTddOff ────────────────────────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
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
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// ── handleTddStatus ─────────────────────────────────────────────────────────
|
|
175
|
+
|
|
176
|
+
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
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
// ── handleTddReset ──────────────────────────────────────────────────────────
|
|
241
|
+
|
|
242
|
+
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
|
+
});
|
|
302
|
+
});
|
package/adapters/pi/index.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { existsSync } from "node:fs";
|
|
3
2
|
import { join } from "node:path";
|
|
4
3
|
import { savePhaseState, resetGit, snapshot } from "../../engine/index.js";
|
|
5
4
|
import { registerTools } from "./tools.js";
|
|
@@ -7,157 +6,215 @@ import { registerHooks } from "./hooks.js";
|
|
|
7
6
|
import { loadTddState } from "./helpers.js";
|
|
8
7
|
import { tddLog } from "./log.js";
|
|
9
8
|
|
|
9
|
+
export async function handleTddOn(
|
|
10
|
+
ctx: ExtensionContext,
|
|
11
|
+
deps: {
|
|
12
|
+
loadTddState: typeof loadTddState;
|
|
13
|
+
snapshot: typeof snapshot;
|
|
14
|
+
savePhaseState: typeof savePhaseState;
|
|
15
|
+
tddLog: typeof tddLog;
|
|
16
|
+
} = {
|
|
17
|
+
loadTddState,
|
|
18
|
+
snapshot,
|
|
19
|
+
savePhaseState,
|
|
20
|
+
tddLog,
|
|
21
|
+
},
|
|
22
|
+
): Promise<void> {
|
|
23
|
+
const root = ctx.cwd;
|
|
24
|
+
const tddDir = join(root, ".pi", "tdd");
|
|
25
|
+
|
|
26
|
+
deps.tddLog(tddDir, "INFO", "tdd:on: starting");
|
|
27
|
+
|
|
28
|
+
const setup = deps.loadTddState(root);
|
|
29
|
+
if (!setup.ok) {
|
|
30
|
+
deps.tddLog(tddDir, "WARN", "tdd:on: setup invalid", { reason: setup.reason });
|
|
31
|
+
ctx.ui.notify(setup.reason, "error");
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const { state } = setup;
|
|
36
|
+
|
|
37
|
+
if (state.enabled) {
|
|
38
|
+
deps.tddLog(tddDir, "INFO", "tdd:on: already enabled", {
|
|
39
|
+
phase: state.current,
|
|
40
|
+
});
|
|
41
|
+
ctx.ui.notify(`TDD already enabled — ${state.current.toUpperCase()} phase`, "info");
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Snapshot working tree so stale baseline doesn't nuke user changes
|
|
46
|
+
deps.snapshot(root, state.current);
|
|
47
|
+
deps.tddLog(tddDir, "INFO", "tdd:on: snapshot taken", {
|
|
48
|
+
phase: state.current,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
state.enabled = true;
|
|
52
|
+
deps.savePhaseState(root, state);
|
|
53
|
+
deps.tddLog(tddDir, "INFO", "tdd:on: enabled", {
|
|
54
|
+
phase: state.current,
|
|
55
|
+
});
|
|
56
|
+
ctx.ui.notify(`TDD enabled — ${state.current.toUpperCase()} phase`, "info");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function handleTddOff(
|
|
60
|
+
ctx: ExtensionContext,
|
|
61
|
+
deps: {
|
|
62
|
+
loadTddState: typeof loadTddState;
|
|
63
|
+
savePhaseState: typeof savePhaseState;
|
|
64
|
+
tddLog: typeof tddLog;
|
|
65
|
+
} = {
|
|
66
|
+
loadTddState,
|
|
67
|
+
savePhaseState,
|
|
68
|
+
tddLog,
|
|
69
|
+
},
|
|
70
|
+
): Promise<void> {
|
|
71
|
+
const root = ctx.cwd;
|
|
72
|
+
const tddDir = join(root, ".pi", "tdd");
|
|
73
|
+
|
|
74
|
+
const setup = deps.loadTddState(root);
|
|
75
|
+
if (!setup.ok) {
|
|
76
|
+
deps.tddLog(tddDir, "WARN", "tdd:off: setup invalid", { reason: setup.reason });
|
|
77
|
+
ctx.ui.notify(setup.reason, "error");
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const { state } = setup;
|
|
82
|
+
|
|
83
|
+
if (!state.enabled) {
|
|
84
|
+
deps.tddLog(tddDir, "INFO", "tdd:off: already disabled");
|
|
85
|
+
ctx.ui.notify("TDD already disabled", "info");
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
state.enabled = false;
|
|
90
|
+
deps.savePhaseState(root, state);
|
|
91
|
+
deps.tddLog(tddDir, "INFO", "tdd:off: disabled", {
|
|
92
|
+
was: state.current,
|
|
93
|
+
});
|
|
94
|
+
ctx.ui.notify("TDD disabled", "info");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export async function handleTddStatus(
|
|
98
|
+
ctx: ExtensionContext,
|
|
99
|
+
deps: {
|
|
100
|
+
loadTddState: typeof loadTddState;
|
|
101
|
+
tddLog: typeof tddLog;
|
|
102
|
+
} = {
|
|
103
|
+
loadTddState,
|
|
104
|
+
tddLog,
|
|
105
|
+
},
|
|
106
|
+
): Promise<void> {
|
|
107
|
+
const root = ctx.cwd;
|
|
108
|
+
const tddDir = join(root, ".pi", "tdd");
|
|
109
|
+
const result = deps.loadTddState(root);
|
|
110
|
+
|
|
111
|
+
if (!result.ok) {
|
|
112
|
+
deps.tddLog(tddDir, "WARN", "tdd:status: setup invalid", {
|
|
113
|
+
reason: result.reason,
|
|
114
|
+
});
|
|
115
|
+
ctx.ui.notify(`TDD: ${result.reason}`, "error");
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const { state, config } = result;
|
|
120
|
+
const enabledStr = state.enabled ? "enabled" : "disabled";
|
|
121
|
+
const phaseStr = state.current.toUpperCase();
|
|
122
|
+
const redGlobs = config.allowedRedPhaseFiles.join(", ") || "(none)";
|
|
123
|
+
const greenGlobs = config.allowedGreenPhaseFiles.join(", ") || "(none)";
|
|
124
|
+
const commands = config.testCommands.join(", ") || "(none)";
|
|
125
|
+
|
|
126
|
+
deps.tddLog(tddDir, "INFO", "tdd:status: queried", {
|
|
127
|
+
enabled: state.enabled,
|
|
128
|
+
phase: state.current,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
ctx.ui.notify(
|
|
132
|
+
`TDD enforcer ${enabledStr}\n` +
|
|
133
|
+
`Current phase: ${phaseStr}\n` +
|
|
134
|
+
`Test files: ${redGlobs}\n` +
|
|
135
|
+
`Impl files: ${greenGlobs}\n` +
|
|
136
|
+
`Test commands: ${commands}`,
|
|
137
|
+
"info",
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export async function handleTddReset(
|
|
142
|
+
ctx: ExtensionContext,
|
|
143
|
+
deps: {
|
|
144
|
+
loadTddState: typeof loadTddState;
|
|
145
|
+
resetGit: typeof resetGit;
|
|
146
|
+
snapshot: typeof snapshot;
|
|
147
|
+
savePhaseState: typeof savePhaseState;
|
|
148
|
+
tddLog: typeof tddLog;
|
|
149
|
+
} = {
|
|
150
|
+
loadTddState,
|
|
151
|
+
resetGit,
|
|
152
|
+
snapshot,
|
|
153
|
+
savePhaseState,
|
|
154
|
+
tddLog,
|
|
155
|
+
},
|
|
156
|
+
): Promise<void> {
|
|
157
|
+
const root = ctx.cwd;
|
|
158
|
+
const tddDir = join(root, ".pi", "tdd");
|
|
159
|
+
|
|
160
|
+
deps.tddLog(tddDir, "INFO", "tdd:reset: starting");
|
|
161
|
+
|
|
162
|
+
const setup = deps.loadTddState(root);
|
|
163
|
+
if (!setup.ok) {
|
|
164
|
+
deps.tddLog(tddDir, "WARN", "tdd:reset: setup invalid", { reason: setup.reason });
|
|
165
|
+
ctx.ui.notify(setup.reason, "error");
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Nuke git history and re-init
|
|
170
|
+
try {
|
|
171
|
+
deps.resetGit(root);
|
|
172
|
+
deps.tddLog(tddDir, "INFO", "tdd:reset: git reset and re-initialised");
|
|
173
|
+
} catch (e) {
|
|
174
|
+
deps.tddLog(tddDir, "ERROR", "tdd:reset: git reset failed", {
|
|
175
|
+
error: (e as Error).message,
|
|
176
|
+
});
|
|
177
|
+
ctx.ui.notify("Failed to reset private git repo.", "error");
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Snapshot current working tree
|
|
182
|
+
deps.snapshot(root, "red");
|
|
183
|
+
deps.tddLog(tddDir, "INFO", "tdd:reset: snapshot taken");
|
|
184
|
+
|
|
185
|
+
// Reset state to RED (disabled, user must run /tdd:on)
|
|
186
|
+
deps.savePhaseState(root, { enabled: false, current: "red" });
|
|
187
|
+
deps.tddLog(tddDir, "INFO", "tdd:reset: complete");
|
|
188
|
+
|
|
189
|
+
ctx.ui.notify(
|
|
190
|
+
"TDD snapshot history reset. Run /tdd:on to re-enable enforcement.",
|
|
191
|
+
"warning",
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
10
195
|
export default function (pi: ExtensionAPI) {
|
|
196
|
+
const defaultDeps = { loadTddState, snapshot, savePhaseState, tddLog, resetGit };
|
|
197
|
+
|
|
11
198
|
pi.registerCommand("tdd:on", {
|
|
12
199
|
description: "Enable TDD enforcement",
|
|
13
|
-
handler:
|
|
14
|
-
const root = ctx.cwd;
|
|
15
|
-
const tddDir = join(root, ".pi", "tdd");
|
|
16
|
-
|
|
17
|
-
tddLog(tddDir, "INFO", "tdd:on: starting");
|
|
18
|
-
|
|
19
|
-
const setup = loadTddState(root);
|
|
20
|
-
if (!setup.ok) {
|
|
21
|
-
tddLog(tddDir, "WARN", "tdd:on: setup invalid", { reason: setup.reason });
|
|
22
|
-
ctx.ui.notify(setup.reason, "error");
|
|
23
|
-
return;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const { state } = setup;
|
|
27
|
-
|
|
28
|
-
if (state.enabled) {
|
|
29
|
-
tddLog(tddDir, "INFO", "tdd:on: already enabled", {
|
|
30
|
-
phase: state.current,
|
|
31
|
-
});
|
|
32
|
-
ctx.ui.notify(`TDD already enabled — ${state.current.toUpperCase()} phase`, "info");
|
|
33
|
-
return;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// Snapshot working tree so stale baseline doesn't nuke user changes
|
|
37
|
-
snapshot(root, state.current);
|
|
38
|
-
tddLog(tddDir, "INFO", "tdd:on: snapshot taken", {
|
|
39
|
-
phase: state.current,
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
state.enabled = true;
|
|
43
|
-
savePhaseState(root, state);
|
|
44
|
-
tddLog(tddDir, "INFO", "tdd:on: enabled", {
|
|
45
|
-
phase: state.current,
|
|
46
|
-
});
|
|
47
|
-
ctx.ui.notify(`TDD enabled — ${state.current.toUpperCase()} phase`, "info");
|
|
48
|
-
},
|
|
200
|
+
handler: (_args: string, ctx: ExtensionContext) => handleTddOn(ctx, defaultDeps),
|
|
49
201
|
});
|
|
50
202
|
|
|
51
203
|
pi.registerCommand("tdd:off", {
|
|
52
204
|
description: "Disable TDD enforcement",
|
|
53
|
-
handler:
|
|
54
|
-
const root = ctx.cwd;
|
|
55
|
-
const tddDir = join(root, ".pi", "tdd");
|
|
56
|
-
|
|
57
|
-
const setup = loadTddState(root);
|
|
58
|
-
if (!setup.ok) {
|
|
59
|
-
tddLog(tddDir, "WARN", "tdd:off: setup invalid", { reason: setup.reason });
|
|
60
|
-
ctx.ui.notify(setup.reason, "error");
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const { state } = setup;
|
|
65
|
-
|
|
66
|
-
if (!state.enabled) {
|
|
67
|
-
tddLog(tddDir, "INFO", "tdd:off: already disabled");
|
|
68
|
-
ctx.ui.notify("TDD already disabled", "info");
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
state.enabled = false;
|
|
73
|
-
savePhaseState(root, state);
|
|
74
|
-
tddLog(tddDir, "INFO", "tdd:off: disabled", {
|
|
75
|
-
was: state.current,
|
|
76
|
-
});
|
|
77
|
-
ctx.ui.notify("TDD disabled", "info");
|
|
78
|
-
},
|
|
205
|
+
handler: (_args: string, ctx: ExtensionContext) => handleTddOff(ctx, defaultDeps),
|
|
79
206
|
});
|
|
80
207
|
|
|
81
208
|
pi.registerCommand("tdd:status", {
|
|
82
209
|
description: "Show TDD enforcement status",
|
|
83
|
-
handler:
|
|
84
|
-
const root = ctx.cwd;
|
|
85
|
-
const tddDir = join(root, ".pi", "tdd");
|
|
86
|
-
const result = loadTddState(root);
|
|
87
|
-
|
|
88
|
-
if (!result.ok) {
|
|
89
|
-
tddLog(tddDir, "WARN", "tdd:status: setup invalid", {
|
|
90
|
-
reason: result.reason,
|
|
91
|
-
});
|
|
92
|
-
ctx.ui.notify(`TDD: ${result.reason}`, "error");
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const { state, config } = result;
|
|
97
|
-
const enabledStr = state.enabled ? "enabled" : "disabled";
|
|
98
|
-
const phaseStr = state.current.toUpperCase();
|
|
99
|
-
const redGlobs = config.allowedRedPhaseFiles.join(", ") || "(none)";
|
|
100
|
-
const greenGlobs = config.allowedGreenPhaseFiles.join(", ") || "(none)";
|
|
101
|
-
const commands = config.testCommands.join(", ") || "(none)";
|
|
102
|
-
|
|
103
|
-
tddLog(tddDir, "INFO", "tdd:status: queried", {
|
|
104
|
-
enabled: state.enabled,
|
|
105
|
-
phase: state.current,
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
ctx.ui.notify(
|
|
109
|
-
`TDD enforcer ${enabledStr}\n` +
|
|
110
|
-
`Current phase: ${phaseStr}\n` +
|
|
111
|
-
`Test files: ${redGlobs}\n` +
|
|
112
|
-
`Impl files: ${greenGlobs}\n` +
|
|
113
|
-
`Test commands: ${commands}`,
|
|
114
|
-
"info"
|
|
115
|
-
);
|
|
116
|
-
},
|
|
210
|
+
handler: (_args: string, ctx: ExtensionContext) => handleTddStatus(ctx, defaultDeps),
|
|
117
211
|
});
|
|
118
212
|
|
|
119
213
|
pi.registerCommand("tdd:reset", {
|
|
120
214
|
description:
|
|
121
215
|
"WARNING: Destroys ALL TDD snapshot history and resets to RED phase. " +
|
|
122
216
|
"Working tree is preserved. Run /tdd:on to re-enable after reset.",
|
|
123
|
-
handler:
|
|
124
|
-
const root = ctx.cwd;
|
|
125
|
-
const tddDir = join(root, ".pi", "tdd");
|
|
126
|
-
|
|
127
|
-
tddLog(tddDir, "INFO", "tdd:reset: starting");
|
|
128
|
-
|
|
129
|
-
const setup = loadTddState(root);
|
|
130
|
-
if (!setup.ok) {
|
|
131
|
-
tddLog(tddDir, "WARN", "tdd:reset: setup invalid", { reason: setup.reason });
|
|
132
|
-
ctx.ui.notify(setup.reason, "error");
|
|
133
|
-
return;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// Nuke git history and re-init
|
|
137
|
-
try {
|
|
138
|
-
resetGit(root);
|
|
139
|
-
tddLog(tddDir, "INFO", "tdd:reset: git reset and re-initialised");
|
|
140
|
-
} catch (e) {
|
|
141
|
-
tddLog(tddDir, "ERROR", "tdd:reset: git reset failed", {
|
|
142
|
-
error: (e as Error).message,
|
|
143
|
-
});
|
|
144
|
-
ctx.ui.notify("Failed to reset private git repo.", "error");
|
|
145
|
-
return;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// Snapshot current working tree
|
|
149
|
-
snapshot(root, "red");
|
|
150
|
-
tddLog(tddDir, "INFO", "tdd:reset: snapshot taken");
|
|
151
|
-
|
|
152
|
-
// Reset state to RED (disabled, user must run /tdd:on)
|
|
153
|
-
savePhaseState(root, { enabled: false, current: "red" });
|
|
154
|
-
tddLog(tddDir, "INFO", "tdd:reset: complete");
|
|
155
|
-
|
|
156
|
-
ctx.ui.notify(
|
|
157
|
-
"TDD snapshot history reset. Run /tdd:on to re-enable enforcement.",
|
|
158
|
-
"warning",
|
|
159
|
-
);
|
|
160
|
-
},
|
|
217
|
+
handler: (_args: string, ctx: ExtensionContext) => handleTddReset(ctx, { ...defaultDeps, resetGit }),
|
|
161
218
|
});
|
|
162
219
|
|
|
163
220
|
registerTools(pi);
|