tdd-enforcer 0.2.5 → 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.
@@ -1,222 +1,244 @@
1
- import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
1
  import { join } from "node:path";
3
- import { savePhaseState, resetGit, snapshot } from "../../engine/index.js";
4
- import { registerTools } from "./tools.js";
5
- import { registerHooks } from "./hooks.js";
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
- 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
- },
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
- 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");
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
- ctx: ExtensionContext,
61
- deps: {
62
- loadTddState: typeof loadTddState;
63
- savePhaseState: typeof savePhaseState;
64
- tddLog: typeof tddLog;
65
- } = {
66
- loadTddState,
67
- savePhaseState,
68
- tddLog,
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
- 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");
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
- ctx: ExtensionContext,
99
- deps: {
100
- loadTddState: typeof loadTddState;
101
- tddLog: typeof tddLog;
102
- } = {
103
- loadTddState,
104
- tddLog,
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
- 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 redBlk = config.blockedInRed.join(", ") || "(none)";
123
- const greenBlk = config.blockedInGreen.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
- `Blocked in RED: ${redBlk}\n` +
135
- `Blocked in GREEN: ${greenBlk}\n` +
136
- `Test commands: ${commands}`,
137
- "info",
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
- 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
- },
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
- 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
- );
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
- const defaultDeps = { loadTddState, snapshot, savePhaseState, tddLog, resetGit };
197
-
198
- pi.registerCommand("tdd:on", {
199
- description: "Enable TDD enforcement",
200
- handler: (_args: string, ctx: ExtensionContext) => handleTddOn(ctx, defaultDeps),
201
- });
202
-
203
- pi.registerCommand("tdd:off", {
204
- description: "Disable TDD enforcement",
205
- handler: (_args: string, ctx: ExtensionContext) => handleTddOff(ctx, defaultDeps),
206
- });
207
-
208
- pi.registerCommand("tdd:status", {
209
- description: "Show TDD enforcement status",
210
- handler: (_args: string, ctx: ExtensionContext) => handleTddStatus(ctx, defaultDeps),
211
- });
212
-
213
- pi.registerCommand("tdd:reset", {
214
- description:
215
- "WARNING: Destroys ALL TDD snapshot history and resets to RED phase. " +
216
- "Working tree is preserved. Run /tdd:on to re-enable after reset.",
217
- handler: (_args: string, ctx: ExtensionContext) => handleTddReset(ctx, { ...defaultDeps, resetGit }),
218
- });
219
-
220
- registerTools(pi);
221
- registerHooks(pi);
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
  }
@@ -1,95 +1,95 @@
1
- import { describe, it, expect, beforeEach, vi } from "vitest";
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
2
  import { tddLog } from "./log.js";
3
3
 
4
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
- });
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
  });