tdd-enforcer 0.3.3 → 0.3.5
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/README.md +169 -0
- package/adapters/pi/hooks.ts +1 -2
- package/adapters/pi/index.ts +7 -3
- package/adapters/pi/tools.test.ts +19 -0
- package/adapters/pi/tools.ts +59 -105
- package/engine/index.ts +6 -1
- package/engine/log.test.ts +171 -0
- package/engine/orchestrate.test.ts +445 -0
- package/engine/orchestrate.ts +159 -0
- package/engine/prompts.test.ts +90 -0
- package/{adapters/pi → engine}/prompts.ts +1 -1
- package/engine/state.test.ts +274 -2
- package/engine/state.ts +135 -1
- package/engine/transition.test.ts +46 -2
- package/engine/transition.ts +10 -0
- package/package.json +13 -1
- package/adapters/pi/helpers.test.ts +0 -275
- package/adapters/pi/helpers.ts +0 -143
- package/adapters/pi/log.test.ts +0 -95
- package/adapters/pi/prompts.test.ts +0 -35
- /package/{adapters/pi → engine}/log.ts +0 -0
|
@@ -1,275 +0,0 @@
|
|
|
1
|
-
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
-
import { loadTddState } from "./helpers.js";
|
|
3
|
-
|
|
4
|
-
const validRules = {
|
|
5
|
-
blockedInRed: ["tests/**/*.test.ts"],
|
|
6
|
-
blockedInGreen: ["src/**/*.ts"],
|
|
7
|
-
testCommands: ["npm test"],
|
|
8
|
-
timeoutSeconds: 30,
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
const validPhase = {
|
|
12
|
-
enabled: true,
|
|
13
|
-
current: "red",
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
describe("loadTddState", () => {
|
|
17
|
-
let mockExistsSync: ReturnType<typeof vi.fn>;
|
|
18
|
-
let mockLoadConfig: ReturnType<typeof vi.fn>;
|
|
19
|
-
let mockInitGit: ReturnType<typeof vi.fn>;
|
|
20
|
-
let mockLoadPhaseState: ReturnType<typeof vi.fn>;
|
|
21
|
-
let mockSavePhaseState: ReturnType<typeof vi.fn>;
|
|
22
|
-
let mockHeadMessage: ReturnType<typeof vi.fn>;
|
|
23
|
-
let mockNextPhase: ReturnType<typeof vi.fn>;
|
|
24
|
-
let mockStageFiles: ReturnType<typeof vi.fn>;
|
|
25
|
-
|
|
26
|
-
// Shared helper to simulate nextPhase behavior
|
|
27
|
-
const realNextPhase = (p: string) =>
|
|
28
|
-
p === "red"
|
|
29
|
-
? "green"
|
|
30
|
-
: p === "green"
|
|
31
|
-
? "refactor"
|
|
32
|
-
: p === "refactor"
|
|
33
|
-
? "red"
|
|
34
|
-
: null;
|
|
35
|
-
|
|
36
|
-
function makeDeps(overrides = {}) {
|
|
37
|
-
return {
|
|
38
|
-
existsSync: mockExistsSync,
|
|
39
|
-
loadConfig: mockLoadConfig,
|
|
40
|
-
initGit: mockInitGit,
|
|
41
|
-
loadPhaseState: mockLoadPhaseState,
|
|
42
|
-
savePhaseState: mockSavePhaseState,
|
|
43
|
-
headMessage: mockHeadMessage,
|
|
44
|
-
nextPhase: mockNextPhase,
|
|
45
|
-
stageFiles: mockStageFiles,
|
|
46
|
-
...overrides,
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
beforeEach(() => {
|
|
51
|
-
vi.clearAllMocks();
|
|
52
|
-
// Default: all setup valid
|
|
53
|
-
mockExistsSync = vi.fn().mockImplementation((path: string) => {
|
|
54
|
-
// Make .pi/tdd and rules.json exist by default
|
|
55
|
-
if (path.includes(".git")) return false;
|
|
56
|
-
if (path.includes(".pi/tdd")) return true;
|
|
57
|
-
return true;
|
|
58
|
-
});
|
|
59
|
-
mockLoadConfig = vi.fn().mockReturnValue(validRules);
|
|
60
|
-
mockInitGit = vi.fn();
|
|
61
|
-
mockLoadPhaseState = vi.fn().mockReturnValue(validPhase);
|
|
62
|
-
mockSavePhaseState = vi.fn();
|
|
63
|
-
mockHeadMessage = vi.fn().mockReturnValue("tdd: red");
|
|
64
|
-
mockNextPhase = vi.fn().mockImplementation(realNextPhase);
|
|
65
|
-
mockStageFiles = vi.fn();
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it("returns missing dir error when .pi/tdd does not exist", () => {
|
|
69
|
-
mockExistsSync.mockReturnValue(false);
|
|
70
|
-
const result = loadTddState("/test", makeDeps());
|
|
71
|
-
expect(result.ok).toBe(false);
|
|
72
|
-
expect(result.reason).toContain("Missing .pi/tdd/");
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it("returns missing rules.json error when only dir exists", () => {
|
|
76
|
-
mockExistsSync.mockImplementation((path: string) => {
|
|
77
|
-
if (path.includes("rules.json")) return false;
|
|
78
|
-
if (path.includes(".pi/tdd")) return true;
|
|
79
|
-
return false;
|
|
80
|
-
});
|
|
81
|
-
const result = loadTddState("/test", makeDeps());
|
|
82
|
-
expect(result.ok).toBe(false);
|
|
83
|
-
expect(result.reason).toContain("rules.json");
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it("auto-creates state.json when missing (default RED disabled)", () => {
|
|
87
|
-
mockExistsSync.mockImplementation((path: string) => {
|
|
88
|
-
if (path.includes("state.json")) return false;
|
|
89
|
-
if (path.includes(".git")) return false;
|
|
90
|
-
return true;
|
|
91
|
-
});
|
|
92
|
-
mockHeadMessage.mockReturnValue("tdd: init");
|
|
93
|
-
const result = loadTddState("/test", makeDeps());
|
|
94
|
-
expect(result.ok).toBe(true);
|
|
95
|
-
if (result.ok) {
|
|
96
|
-
expect(result.state.enabled).toBe(false);
|
|
97
|
-
expect(result.state.current).toBe("red");
|
|
98
|
-
}
|
|
99
|
-
expect(mockInitGit).toHaveBeenCalled();
|
|
100
|
-
expect(mockSavePhaseState).toHaveBeenCalledWith("/test", {
|
|
101
|
-
enabled: false,
|
|
102
|
-
current: "red",
|
|
103
|
-
});
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
it("auto-creates state.json when corrupted (recovers to default)", () => {
|
|
107
|
-
mockExistsSync.mockImplementation((path: string) => {
|
|
108
|
-
if (path.includes("state.json")) return true; // exists but corrupted
|
|
109
|
-
if (path.includes(".git")) return false;
|
|
110
|
-
return true;
|
|
111
|
-
});
|
|
112
|
-
mockLoadPhaseState.mockImplementation(() => {
|
|
113
|
-
throw new Error("corrupt");
|
|
114
|
-
});
|
|
115
|
-
mockHeadMessage.mockReturnValue("tdd: init");
|
|
116
|
-
const result = loadTddState("/test", makeDeps());
|
|
117
|
-
expect(result.ok).toBe(true);
|
|
118
|
-
if (result.ok) {
|
|
119
|
-
expect(result.state.enabled).toBe(false);
|
|
120
|
-
expect(result.state.current).toBe("red");
|
|
121
|
-
}
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
it("returns invalid rules.json error for malformed JSON", () => {
|
|
125
|
-
mockLoadConfig.mockImplementation(() => {
|
|
126
|
-
throw new Error("Unexpected token");
|
|
127
|
-
});
|
|
128
|
-
const result = loadTddState("/test", makeDeps());
|
|
129
|
-
expect(result.ok).toBe(false);
|
|
130
|
-
expect(result.reason).toContain("Invalid .pi/tdd/rules.json");
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
it("returns ok when state.json has enabled: false (callers check enabled)", () => {
|
|
134
|
-
mockLoadPhaseState.mockReturnValue({ enabled: false, current: "red" });
|
|
135
|
-
const result = loadTddState("/test", makeDeps());
|
|
136
|
-
expect(result.ok).toBe(true);
|
|
137
|
-
if (result.ok) {
|
|
138
|
-
expect(result.state.enabled).toBe(false);
|
|
139
|
-
expect(result.state.current).toBe("red");
|
|
140
|
-
}
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
it("returns ok with state and config when everything valid", () => {
|
|
144
|
-
mockLoadPhaseState.mockReturnValue(validPhase);
|
|
145
|
-
const result = loadTddState("/test", makeDeps());
|
|
146
|
-
expect(result.ok).toBe(true);
|
|
147
|
-
if (result.ok) {
|
|
148
|
-
expect(result.state.current).toBe("red");
|
|
149
|
-
expect(result.state.enabled).toBe(true);
|
|
150
|
-
expect(result.config.testCommands).toEqual(["npm test"]);
|
|
151
|
-
expect(result.config.blockedInRed).toEqual(["tests/**/*.test.ts"]);
|
|
152
|
-
}
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
it("auto-creates git repo when missing", () => {
|
|
156
|
-
let gitExists = false;
|
|
157
|
-
mockExistsSync.mockImplementation((path: string) => {
|
|
158
|
-
if (path.includes(".git")) return gitExists;
|
|
159
|
-
if (path.includes(".pi/tdd")) return true;
|
|
160
|
-
return true;
|
|
161
|
-
});
|
|
162
|
-
mockInitGit.mockImplementation(() => {
|
|
163
|
-
gitExists = true;
|
|
164
|
-
});
|
|
165
|
-
const result = loadTddState("/test", makeDeps());
|
|
166
|
-
expect(result.ok).toBe(true);
|
|
167
|
-
expect(mockInitGit).toHaveBeenCalled();
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
it("handles multiple calls without error", () => {
|
|
171
|
-
const r1 = loadTddState("/test", makeDeps());
|
|
172
|
-
expect(r1.ok).toBe(true);
|
|
173
|
-
const r2 = loadTddState("/test", makeDeps());
|
|
174
|
-
expect(r2.ok).toBe(true);
|
|
175
|
-
expect(mockLoadConfig).toHaveBeenCalledTimes(2);
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
it("recovers from invalid current phase in state.json (auto-creates default)", () => {
|
|
179
|
-
mockLoadPhaseState.mockImplementation(() => {
|
|
180
|
-
throw new Error("invalid phase");
|
|
181
|
-
});
|
|
182
|
-
mockHeadMessage.mockReturnValue("tdd: init");
|
|
183
|
-
const result = loadTddState("/test", makeDeps());
|
|
184
|
-
expect(result.ok).toBe(true);
|
|
185
|
-
if (result.ok) {
|
|
186
|
-
expect(result.state.enabled).toBe(false);
|
|
187
|
-
expect(result.state.current).toBe("red");
|
|
188
|
-
}
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
it("recoverState: HEAD tdd:red → enabled green", () => {
|
|
192
|
-
mockExistsSync.mockImplementation((path: string) => {
|
|
193
|
-
if (path.includes("state.json")) return false; // missing → triggers recover
|
|
194
|
-
return true;
|
|
195
|
-
});
|
|
196
|
-
mockHeadMessage.mockReturnValue("tdd: red");
|
|
197
|
-
mockNextPhase.mockImplementation(realNextPhase);
|
|
198
|
-
const result = loadTddState("/test", makeDeps());
|
|
199
|
-
expect(result.ok).toBe(true);
|
|
200
|
-
if (result.ok) {
|
|
201
|
-
expect(result.state.enabled).toBe(true);
|
|
202
|
-
expect(result.state.current).toBe("green");
|
|
203
|
-
}
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
it("recoverState: HEAD tdd:green → enabled refactor", () => {
|
|
207
|
-
mockExistsSync.mockImplementation((path: string) => {
|
|
208
|
-
if (path.includes("state.json")) return false;
|
|
209
|
-
return true;
|
|
210
|
-
});
|
|
211
|
-
mockHeadMessage.mockReturnValue("tdd: green");
|
|
212
|
-
const result = loadTddState("/test", makeDeps());
|
|
213
|
-
expect(result.ok).toBe(true);
|
|
214
|
-
if (result.ok) {
|
|
215
|
-
expect(result.state.enabled).toBe(true);
|
|
216
|
-
expect(result.state.current).toBe("refactor");
|
|
217
|
-
}
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
it("recoverState: HEAD tdd:refactor → enabled red", () => {
|
|
221
|
-
mockExistsSync.mockImplementation((path: string) => {
|
|
222
|
-
if (path.includes("state.json")) return false;
|
|
223
|
-
return true;
|
|
224
|
-
});
|
|
225
|
-
mockHeadMessage.mockReturnValue("tdd: refactor");
|
|
226
|
-
const result = loadTddState("/test", makeDeps());
|
|
227
|
-
expect(result.ok).toBe(true);
|
|
228
|
-
if (result.ok) {
|
|
229
|
-
expect(result.state.enabled).toBe(true);
|
|
230
|
-
expect(result.state.current).toBe("red");
|
|
231
|
-
}
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
it("force-adds state.json after creating it from recovery", () => {
|
|
235
|
-
mockExistsSync.mockImplementation((path: string) => {
|
|
236
|
-
if (path.includes("state.json")) return false; // missing → triggers create
|
|
237
|
-
if (path.includes(".git")) return false;
|
|
238
|
-
return true;
|
|
239
|
-
});
|
|
240
|
-
mockHeadMessage.mockReturnValue("tdd: init");
|
|
241
|
-
const result = loadTddState("/test", makeDeps());
|
|
242
|
-
expect(result.ok).toBe(true);
|
|
243
|
-
expect(mockStageFiles).toHaveBeenCalledWith("/test", [
|
|
244
|
-
".pi/tdd/state.json",
|
|
245
|
-
]);
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
it("does not force-add state.json when it already exists and is valid", () => {
|
|
249
|
-
mockExistsSync.mockImplementation((path: string) => {
|
|
250
|
-
if (path.includes("state.json")) return true;
|
|
251
|
-
if (path.includes(".git")) return true;
|
|
252
|
-
return true;
|
|
253
|
-
});
|
|
254
|
-
const result = loadTddState("/test", makeDeps());
|
|
255
|
-
expect(result.ok).toBe(true);
|
|
256
|
-
expect(mockStageFiles).not.toHaveBeenCalled();
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
it("force-adds state.json after recovering from corrupted state", () => {
|
|
260
|
-
mockExistsSync.mockImplementation((path: string) => {
|
|
261
|
-
if (path.includes("state.json")) return true; // exists but corrupted
|
|
262
|
-
if (path.includes(".git")) return true;
|
|
263
|
-
return true;
|
|
264
|
-
});
|
|
265
|
-
mockLoadPhaseState.mockImplementation(() => {
|
|
266
|
-
throw new Error("corrupt");
|
|
267
|
-
});
|
|
268
|
-
mockHeadMessage.mockReturnValue("tdd: init");
|
|
269
|
-
const result = loadTddState("/test", makeDeps());
|
|
270
|
-
expect(result.ok).toBe(true);
|
|
271
|
-
expect(mockStageFiles).toHaveBeenCalledWith("/test", [
|
|
272
|
-
".pi/tdd/state.json",
|
|
273
|
-
]);
|
|
274
|
-
});
|
|
275
|
-
});
|
package/adapters/pi/helpers.ts
DELETED
|
@@ -1,143 +0,0 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
import {
|
|
4
|
-
headMessage,
|
|
5
|
-
initGit,
|
|
6
|
-
loadConfig,
|
|
7
|
-
loadPhaseState,
|
|
8
|
-
nextPhase,
|
|
9
|
-
savePhaseState,
|
|
10
|
-
stageFiles,
|
|
11
|
-
} from "../../engine/index.js";
|
|
12
|
-
import type { Config, PhaseState } from "../../engine/types.js";
|
|
13
|
-
|
|
14
|
-
export type TddLoadResult =
|
|
15
|
-
| { ok: true; state: PhaseState; config: Config }
|
|
16
|
-
| { ok: false; reason: string };
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Recover state.json from private git HEAD, or create default.
|
|
20
|
-
* Returns the state (does not save to disk — caller does that).
|
|
21
|
-
*/
|
|
22
|
-
function recoverState(
|
|
23
|
-
root: string,
|
|
24
|
-
tddDir: string,
|
|
25
|
-
deps: {
|
|
26
|
-
existsSync: typeof existsSync;
|
|
27
|
-
headMessage: typeof headMessage;
|
|
28
|
-
nextPhase: typeof nextPhase;
|
|
29
|
-
} = { existsSync, headMessage, nextPhase },
|
|
30
|
-
): PhaseState {
|
|
31
|
-
const gitDir = join(tddDir, ".git");
|
|
32
|
-
if (deps.existsSync(gitDir)) {
|
|
33
|
-
try {
|
|
34
|
-
const msg = deps.headMessage(root);
|
|
35
|
-
const m = msg.match(/^tdd: (red|green|refactor|init)$/);
|
|
36
|
-
if (m) {
|
|
37
|
-
const label = m[1];
|
|
38
|
-
if (label === "init") {
|
|
39
|
-
return { enabled: false, current: "red" };
|
|
40
|
-
}
|
|
41
|
-
const next = deps.nextPhase(label);
|
|
42
|
-
return { enabled: true, current: next ?? "red" };
|
|
43
|
-
}
|
|
44
|
-
} catch {
|
|
45
|
-
// No commits or bad HEAD — fall through to default
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
return { enabled: false, current: "red" };
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Load TDD state + config in one go.
|
|
53
|
-
* Auto-creates state.json from private git HEAD when missing or corrupted.
|
|
54
|
-
* Returns ok:true with state and config when rules.json is valid.
|
|
55
|
-
* Returns ok:false with a specific reason string otherwise.
|
|
56
|
-
*
|
|
57
|
-
* Callers must check state.enabled themselves if they need active enforcement.
|
|
58
|
-
*/
|
|
59
|
-
export function loadTddState(
|
|
60
|
-
root: string,
|
|
61
|
-
deps: {
|
|
62
|
-
existsSync: typeof existsSync;
|
|
63
|
-
loadConfig: typeof loadConfig;
|
|
64
|
-
initGit: typeof initGit;
|
|
65
|
-
loadPhaseState: typeof loadPhaseState;
|
|
66
|
-
savePhaseState: typeof savePhaseState;
|
|
67
|
-
headMessage: typeof headMessage;
|
|
68
|
-
nextPhase: typeof nextPhase;
|
|
69
|
-
stageFiles: typeof stageFiles;
|
|
70
|
-
} = {
|
|
71
|
-
existsSync,
|
|
72
|
-
loadConfig,
|
|
73
|
-
initGit,
|
|
74
|
-
loadPhaseState,
|
|
75
|
-
savePhaseState,
|
|
76
|
-
headMessage,
|
|
77
|
-
nextPhase,
|
|
78
|
-
stageFiles,
|
|
79
|
-
},
|
|
80
|
-
): TddLoadResult {
|
|
81
|
-
const tddDir = join(root, ".pi", "tdd");
|
|
82
|
-
if (!deps.existsSync(tddDir)) {
|
|
83
|
-
return {
|
|
84
|
-
ok: false,
|
|
85
|
-
reason:
|
|
86
|
-
"Missing .pi/tdd/ directory. See the tdd-enforcer skill to learn how to set up TDD configs.",
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const rulesPath = join(tddDir, "rules.json");
|
|
91
|
-
if (!deps.existsSync(rulesPath)) {
|
|
92
|
-
return {
|
|
93
|
-
ok: false,
|
|
94
|
-
reason:
|
|
95
|
-
"Missing .pi/tdd/rules.json. See the tdd-enforcer skill to learn how to set up TDD configs.",
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
let config: Config;
|
|
100
|
-
try {
|
|
101
|
-
config = deps.loadConfig(root);
|
|
102
|
-
} catch (e) {
|
|
103
|
-
return {
|
|
104
|
-
ok: false,
|
|
105
|
-
reason: `Invalid .pi/tdd/rules.json: ${(e as Error).message}. See the tdd-enforcer skill.`,
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// Init git if missing — required for state recovery and all consumers
|
|
110
|
-
const gitDir = join(tddDir, ".git");
|
|
111
|
-
if (!deps.existsSync(gitDir)) {
|
|
112
|
-
try {
|
|
113
|
-
deps.initGit(root);
|
|
114
|
-
} catch (e) {
|
|
115
|
-
return {
|
|
116
|
-
ok: false,
|
|
117
|
-
reason: `Failed to initialise private git repo: ${(e as Error).message}`,
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// Auto-create state.json if missing or corrupted
|
|
123
|
-
const phasePath = join(tddDir, "state.json");
|
|
124
|
-
let state: PhaseState | undefined;
|
|
125
|
-
if (deps.existsSync(phasePath)) {
|
|
126
|
-
try {
|
|
127
|
-
state = deps.loadPhaseState(root);
|
|
128
|
-
} catch {
|
|
129
|
-
// Corrupted — recover below
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
if (!state) {
|
|
133
|
-
state = recoverState(root, tddDir, {
|
|
134
|
-
existsSync: deps.existsSync,
|
|
135
|
-
headMessage: deps.headMessage,
|
|
136
|
-
nextPhase: deps.nextPhase,
|
|
137
|
-
});
|
|
138
|
-
deps.savePhaseState(root, state);
|
|
139
|
-
deps.stageFiles(root, [".pi/tdd/state.json"]);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
return { ok: true, state, config };
|
|
143
|
-
}
|
package/adapters/pi/log.test.ts
DELETED
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
-
import { tddLog } from "./log.js";
|
|
3
|
-
|
|
4
|
-
describe("tddLog", () => {
|
|
5
|
-
let mockExistsSync: ReturnType<typeof vi.fn>;
|
|
6
|
-
let mockMkdirSync: ReturnType<typeof vi.fn>;
|
|
7
|
-
let mockAppendFileSync: ReturnType<typeof vi.fn>;
|
|
8
|
-
let mockWriteFileSync: ReturnType<typeof vi.fn>;
|
|
9
|
-
let mockReadFileSync: ReturnType<typeof vi.fn>;
|
|
10
|
-
let writtenContent: string;
|
|
11
|
-
|
|
12
|
-
function makeDeps() {
|
|
13
|
-
return {
|
|
14
|
-
existsSync: mockExistsSync,
|
|
15
|
-
mkdirSync: mockMkdirSync,
|
|
16
|
-
appendFileSync: mockAppendFileSync,
|
|
17
|
-
writeFileSync: mockWriteFileSync,
|
|
18
|
-
readFileSync: mockReadFileSync,
|
|
19
|
-
};
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
beforeEach(() => {
|
|
23
|
-
writtenContent = "";
|
|
24
|
-
mockExistsSync = vi.fn().mockReturnValue(true);
|
|
25
|
-
mockMkdirSync = vi.fn();
|
|
26
|
-
mockAppendFileSync = vi.fn((_path: string, content: string) => {
|
|
27
|
-
writtenContent += content;
|
|
28
|
-
});
|
|
29
|
-
mockWriteFileSync = vi.fn((_path: string, content: string) => {
|
|
30
|
-
writtenContent = content;
|
|
31
|
-
});
|
|
32
|
-
mockReadFileSync = vi.fn(() => writtenContent);
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it("creates the log file if it doesn't exist", () => {
|
|
36
|
-
// readFileSync throws → outer catch silently swallows it
|
|
37
|
-
mockReadFileSync = vi.fn(() => {
|
|
38
|
-
throw new Error("ENOENT: no such file or directory");
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
tddLog("/tdd", "INFO", "hello", undefined, makeDeps());
|
|
42
|
-
|
|
43
|
-
expect(mockAppendFileSync).toHaveBeenCalled();
|
|
44
|
-
expect(mockAppendFileSync.mock.calls[0][1]).toContain("hello");
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it("appends multiple lines", () => {
|
|
48
|
-
tddLog("/tdd", "INFO", "line one", undefined, makeDeps());
|
|
49
|
-
tddLog("/tdd", "DEBUG", "line two", undefined, makeDeps());
|
|
50
|
-
|
|
51
|
-
const lines = writtenContent.trim().split("\n");
|
|
52
|
-
expect(lines).toHaveLength(2);
|
|
53
|
-
expect(lines[0]).toContain("[INFO]");
|
|
54
|
-
expect(lines[0]).toContain("line one");
|
|
55
|
-
expect(lines[1]).toContain("[DEBUG]");
|
|
56
|
-
expect(lines[1]).toContain("line two");
|
|
57
|
-
expect(mockAppendFileSync).toHaveBeenCalledTimes(2);
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it("includes data as JSON when provided", () => {
|
|
61
|
-
tddLog("/tdd", "INFO", "with data", { key: "val", num: 42 }, makeDeps());
|
|
62
|
-
|
|
63
|
-
expect(writtenContent).toContain('{"key":"val","num":42}');
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it("trims to last 1000 lines when exceeded", () => {
|
|
67
|
-
for (let i = 0; i < 1005; i++) {
|
|
68
|
-
tddLog("/tdd", "DEBUG", `line ${i}`, undefined, makeDeps());
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const lines = writtenContent.trim().split("\n");
|
|
72
|
-
expect(lines).toHaveLength(1000);
|
|
73
|
-
expect(lines[0]).toContain("line 5");
|
|
74
|
-
expect(lines[999]).toContain("line 1004");
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it("does not throw on invalid tddDir", () => {
|
|
78
|
-
// Simulate appendFileSync failure
|
|
79
|
-
mockAppendFileSync = vi.fn(() => {
|
|
80
|
-
throw new Error("ENOENT: no such file or directory");
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
expect(() =>
|
|
84
|
-
tddLog("/nonexistent/path/tdd", "INFO", "fail", undefined, makeDeps()),
|
|
85
|
-
).not.toThrow();
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it("handles missing data field gracefully", () => {
|
|
89
|
-
tddLog("/tdd", "WARN", "no data", undefined, makeDeps());
|
|
90
|
-
|
|
91
|
-
expect(writtenContent).toContain("[WARN]");
|
|
92
|
-
expect(writtenContent).toContain("no data");
|
|
93
|
-
expect(writtenContent).not.toContain("{}");
|
|
94
|
-
});
|
|
95
|
-
});
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import type { Config } from "../../engine/types.js";
|
|
3
|
-
import { getNudgePrompt } from "./prompts.js";
|
|
4
|
-
|
|
5
|
-
const config: Config = {
|
|
6
|
-
blockedInRed: ["tests/**/*.test.ts"],
|
|
7
|
-
blockedInGreen: ["src/**/*.ts"],
|
|
8
|
-
testCommands: ["npm test"],
|
|
9
|
-
timeoutSeconds: 30,
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
describe("getNudgePrompt", () => {
|
|
13
|
-
it("returns RED prompt with blocked files", () => {
|
|
14
|
-
const result = getNudgePrompt("red", config);
|
|
15
|
-
expect(result).toContain("RED");
|
|
16
|
-
expect(result).toContain("Blocked files: tests/**/*.test.ts");
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
it("returns GREEN prompt with blocked files", () => {
|
|
20
|
-
const result = getNudgePrompt("green", config);
|
|
21
|
-
expect(result).toContain("GREEN");
|
|
22
|
-
expect(result).toContain("Blocked files: src/**/*.ts");
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
it("returns REFACTOR prompt", () => {
|
|
26
|
-
const result = getNudgePrompt("refactor", config);
|
|
27
|
-
expect(result).toContain("REFACTOR");
|
|
28
|
-
expect(result).toContain("free to modify");
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it("returns empty string for unknown phase", () => {
|
|
32
|
-
const result = getNudgePrompt("blurple" as any, config);
|
|
33
|
-
expect(result).toBe("");
|
|
34
|
-
});
|
|
35
|
-
});
|
|
File without changes
|