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