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.
package/engine/git.ts CHANGED
@@ -1,182 +1,308 @@
1
- import { execSync, type ExecSyncOptions } from "node:child_process";
2
- import { existsSync, mkdirSync, writeFileSync, unlinkSync, rmSync } from "node:fs";
1
+ import { type ExecSyncOptions, execSync } from "node:child_process";
2
+ import {
3
+ existsSync,
4
+ mkdirSync,
5
+ rmSync,
6
+ unlinkSync,
7
+ writeFileSync,
8
+ } from "node:fs";
3
9
  import { join } from "node:path";
4
10
 
5
11
  const TDD_DIR = ".pi/tdd";
6
12
 
7
13
  export type GitDeps = {
8
- execSync: (command: string, options?: ExecSyncOptions) => Buffer;
9
- existsSync: (path: string) => boolean;
10
- mkdirSync: (path: string, options?: { recursive?: boolean }) => void;
11
- writeFileSync: (path: string, data: string, encoding: BufferEncoding) => void;
12
- unlinkSync: (path: string) => void;
13
- rmSync: (path: string, options?: { recursive?: boolean; force?: boolean }) => void;
14
+ execSync: (command: string, options?: ExecSyncOptions) => Buffer;
15
+ existsSync: (path: string) => boolean;
16
+ mkdirSync: (path: string, options?: { recursive?: boolean }) => void;
17
+ writeFileSync: (path: string, data: string, encoding: BufferEncoding) => void;
18
+ unlinkSync: (path: string) => void;
19
+ rmSync: (
20
+ path: string,
21
+ options?: { recursive?: boolean; force?: boolean },
22
+ ) => void;
14
23
  };
15
24
 
16
25
  const defaultDeps: GitDeps = {
17
- execSync: execSync as (command: string, options?: ExecSyncOptions) => Buffer,
18
- existsSync,
19
- mkdirSync,
20
- writeFileSync: writeFileSync as (path: string, data: string, encoding: BufferEncoding) => void,
21
- unlinkSync,
22
- rmSync,
26
+ execSync: execSync as (command: string, options?: ExecSyncOptions) => Buffer,
27
+ existsSync,
28
+ mkdirSync,
29
+ writeFileSync: writeFileSync as (
30
+ path: string,
31
+ data: string,
32
+ encoding: BufferEncoding,
33
+ ) => void,
34
+ unlinkSync,
35
+ rmSync,
23
36
  };
24
37
 
25
38
  function gitEnv(projectRoot: string): NodeJS.ProcessEnv {
26
- const gitDir = join(projectRoot, TDD_DIR, ".git");
27
- return {
28
- GIT_DIR: gitDir,
29
- GIT_WORK_TREE: projectRoot,
30
- };
39
+ const gitDir = join(projectRoot, TDD_DIR, ".git");
40
+ return {
41
+ GIT_DIR: gitDir,
42
+ GIT_WORK_TREE: projectRoot,
43
+ };
31
44
  }
32
45
 
33
- function gitExec(args: string, projectRoot: string, deps: GitDeps, options?: ExecSyncOptions): string {
34
- const env = { ...process.env, ...gitEnv(projectRoot) };
35
- return deps.execSync(`git ${args}`, { ...options, env, encoding: "utf-8" } as ExecSyncOptions).toString();
46
+ function gitExec(
47
+ args: string,
48
+ projectRoot: string,
49
+ deps: GitDeps,
50
+ options?: ExecSyncOptions,
51
+ ): string {
52
+ const env = { ...process.env, ...gitEnv(projectRoot) };
53
+ return deps
54
+ .execSync(`git ${args}`, {
55
+ ...options,
56
+ env,
57
+ encoding: "utf-8",
58
+ } as ExecSyncOptions)
59
+ .toString();
36
60
  }
37
61
 
38
- export function initGit(projectRoot: string, deps: GitDeps = defaultDeps): void {
39
- const tddPath = join(projectRoot, TDD_DIR);
40
- const gitDir = join(tddPath, ".git");
41
- if (deps.existsSync(gitDir)) return;
62
+ export function initGit(
63
+ projectRoot: string,
64
+ deps: GitDeps = defaultDeps,
65
+ ): void {
66
+ const tddPath = join(projectRoot, TDD_DIR);
67
+ const gitDir = join(tddPath, ".git");
68
+ if (deps.existsSync(gitDir)) return;
42
69
 
43
- deps.mkdirSync(tddPath, { recursive: true });
44
- gitExec(`init "${tddPath}"`, projectRoot, deps, { stdio: "pipe" as const });
45
- gitExec(`config core.worktree "${projectRoot}"`, projectRoot, deps, { stdio: "pipe" as const });
46
- gitExec(`config core.excludesFile "${join(tddPath, ".gitignore")}"`, projectRoot, deps, { stdio: "pipe" as const });
70
+ deps.mkdirSync(tddPath, { recursive: true });
71
+ gitExec(`init "${tddPath}"`, projectRoot, deps, { stdio: "pipe" as const });
72
+ gitExec(`config core.worktree "${projectRoot}"`, projectRoot, deps, {
73
+ stdio: "pipe" as const,
74
+ });
75
+ gitExec(
76
+ `config core.excludesFile "${join(tddPath, ".gitignore")}"`,
77
+ projectRoot,
78
+ deps,
79
+ { stdio: "pipe" as const },
80
+ );
47
81
 
48
- const gitignorePath = join(tddPath, ".gitignore");
49
- if (!deps.existsSync(gitignorePath)) {
50
- deps.writeFileSync(
51
- gitignorePath,
52
- ["node_modules/", ".pnpm-store/", ".next/", "dist/", "build/", ".cache/", "*.log", ".DS_Store", "Thumbs.db", ""].join("\n"),
53
- "utf-8",
54
- );
55
- }
82
+ const gitignorePath = join(tddPath, ".gitignore");
83
+ if (!deps.existsSync(gitignorePath)) {
84
+ deps.writeFileSync(
85
+ gitignorePath,
86
+ [
87
+ "node_modules/",
88
+ ".pnpm-store/",
89
+ ".next/",
90
+ "dist/",
91
+ "build/",
92
+ ".cache/",
93
+ "*.log",
94
+ ".DS_Store",
95
+ "Thumbs.db",
96
+ "",
97
+ ].join("\n"),
98
+ "utf-8",
99
+ );
100
+ }
56
101
 
57
- gitExec("add -A", projectRoot, deps, { stdio: "pipe" as const });
58
- stageFiles(projectRoot, [".pi/tdd/state.json", ".pi/tdd/rules.json", ".pi/tdd/.gitignore"], deps);
59
- gitExec('commit --allow-empty -m "tdd: init"', projectRoot, deps, { stdio: "pipe" as const });
102
+ gitExec("add -A", projectRoot, deps, { stdio: "pipe" as const });
103
+ stageFiles(
104
+ projectRoot,
105
+ [".pi/tdd/state.json", ".pi/tdd/rules.json", ".pi/tdd/.gitignore"],
106
+ deps,
107
+ );
108
+ gitExec('commit --allow-empty -m "tdd: init"', projectRoot, deps, {
109
+ stdio: "pipe" as const,
110
+ });
60
111
  }
61
112
 
62
113
  /** Destroy the private git repo and re-init from scratch. */
63
- export function resetGit(projectRoot: string, deps: GitDeps = defaultDeps): void {
64
- const tddPath = join(projectRoot, TDD_DIR);
65
- const gitDir = join(tddPath, ".git");
66
- if (deps.existsSync(gitDir)) {
67
- deps.rmSync(gitDir, { recursive: true, force: true });
68
- }
69
- initGit(projectRoot, deps);
114
+ export function resetGit(
115
+ projectRoot: string,
116
+ deps: GitDeps = defaultDeps,
117
+ ): void {
118
+ const tddPath = join(projectRoot, TDD_DIR);
119
+ const gitDir = join(tddPath, ".git");
120
+ if (deps.existsSync(gitDir)) {
121
+ deps.rmSync(gitDir, { recursive: true, force: true });
122
+ }
123
+ initGit(projectRoot, deps);
70
124
  }
71
125
 
72
126
  /** Stage all + commit with --allow-empty so every phase transition has a labeled commit. */
73
- export function snapshot(projectRoot: string, phase: string, deps: GitDeps = defaultDeps): string {
74
- gitExec("add -A", projectRoot, deps, { stdio: "pipe" as const });
75
- stageFiles(projectRoot, [".pi/tdd/state.json", ".pi/tdd/rules.json", ".pi/tdd/.gitignore"], deps);
76
- gitExec(`commit --allow-empty -m "tdd: ${phase}"`, projectRoot, deps, { stdio: "pipe" as const });
77
- return gitExec("rev-parse HEAD", projectRoot, deps).trim();
127
+ export function snapshot(
128
+ projectRoot: string,
129
+ phase: string,
130
+ deps: GitDeps = defaultDeps,
131
+ ): string {
132
+ gitExec("add -A", projectRoot, deps, { stdio: "pipe" as const });
133
+ stageFiles(
134
+ projectRoot,
135
+ [".pi/tdd/state.json", ".pi/tdd/rules.json", ".pi/tdd/.gitignore"],
136
+ deps,
137
+ );
138
+ gitExec(`commit --allow-empty -m "tdd: ${phase}"`, projectRoot, deps, {
139
+ stdio: "pipe" as const,
140
+ });
141
+ return gitExec("rev-parse HEAD", projectRoot, deps).trim();
78
142
  }
79
143
 
80
- export function modifiedFiles(projectRoot: string, deps: GitDeps = defaultDeps): string[] {
81
- const out = gitExec("diff --name-only HEAD", projectRoot, deps).trim();
82
- return out ? out.split("\n") : [];
144
+ export function modifiedFiles(
145
+ projectRoot: string,
146
+ deps: GitDeps = defaultDeps,
147
+ ): string[] {
148
+ const out = gitExec("diff --name-only HEAD", projectRoot, deps).trim();
149
+ return out ? out.split("\n") : [];
83
150
  }
84
151
 
85
- export function untrackedFiles(projectRoot: string, deps: GitDeps = defaultDeps): string[] {
86
- const out = gitExec("ls-files --others --exclude-standard", projectRoot, deps).trim();
87
- return out ? out.split("\n") : [];
152
+ export function untrackedFiles(
153
+ projectRoot: string,
154
+ deps: GitDeps = defaultDeps,
155
+ ): string[] {
156
+ const out = gitExec(
157
+ "ls-files --others --exclude-standard",
158
+ projectRoot,
159
+ deps,
160
+ ).trim();
161
+ return out ? out.split("\n") : [];
88
162
  }
89
163
 
90
- export function changesSinceSnapshot(projectRoot: string, deps: GitDeps = defaultDeps): string[] {
91
- return [...new Set([...modifiedFiles(projectRoot, deps), ...untrackedFiles(projectRoot, deps)])];
164
+ export function changesSinceSnapshot(
165
+ projectRoot: string,
166
+ deps: GitDeps = defaultDeps,
167
+ ): string[] {
168
+ return [
169
+ ...new Set([
170
+ ...modifiedFiles(projectRoot, deps),
171
+ ...untrackedFiles(projectRoot, deps),
172
+ ]),
173
+ ];
92
174
  }
93
175
 
94
- export function restoreFilesTo(projectRoot: string, files: string[], source?: string, deps: GitDeps = defaultDeps): void {
95
- if (files.length === 0) return;
176
+ export function restoreFilesTo(
177
+ projectRoot: string,
178
+ files: string[],
179
+ source?: string,
180
+ deps: GitDeps = defaultDeps,
181
+ ): void {
182
+ if (files.length === 0) return;
96
183
 
97
- // Separate tracked (git restore) from untracked (delete)
98
- const tracked = gitExec("ls-files", projectRoot, deps)
99
- .trim()
100
- .split("\n")
101
- .filter(Boolean);
102
- const trackedSet = new Set(tracked);
184
+ // Separate tracked (git restore) from untracked (delete)
185
+ const tracked = gitExec("ls-files", projectRoot, deps)
186
+ .trim()
187
+ .split("\n")
188
+ .filter(Boolean);
189
+ const trackedSet = new Set(tracked);
103
190
 
104
- const trackedFiles = files.filter((f) => trackedSet.has(f));
105
- const untrackedFilesList = files.filter((f) => !trackedSet.has(f));
191
+ const trackedFiles = files.filter((f) => trackedSet.has(f));
192
+ const untrackedFilesList = files.filter((f) => !trackedSet.has(f));
106
193
 
107
- if (trackedFiles.length > 0) {
108
- const escaped = trackedFiles.map((f) => `"${f}"`).join(" ");
109
- const sourceFlag = source ? `--source=${source} ` : "";
110
- gitExec(`restore ${sourceFlag}--worktree -- ${escaped}`, projectRoot, deps, { stdio: "pipe" as const });
111
- }
194
+ if (trackedFiles.length > 0) {
195
+ const escaped = trackedFiles.map((f) => `"${f}"`).join(" ");
196
+ const sourceFlag = source ? `--source=${source} ` : "";
197
+ gitExec(
198
+ `restore ${sourceFlag}--worktree -- ${escaped}`,
199
+ projectRoot,
200
+ deps,
201
+ { stdio: "pipe" as const },
202
+ );
203
+ }
112
204
 
113
- for (const f of untrackedFilesList) {
114
- try {
115
- deps.unlinkSync(f);
116
- } catch {
117
- // File may already be gone, ignore
118
- }
119
- }
205
+ for (const f of untrackedFilesList) {
206
+ try {
207
+ deps.unlinkSync(f);
208
+ } catch {
209
+ // File may already be gone, ignore
210
+ }
211
+ }
120
212
  }
121
213
 
122
214
  /**
123
215
  * Force-stage files in the private git repo, bypassing worktree .gitignore.
124
216
  * Does not commit — call snapshot() or commit separately to persist.
125
217
  */
126
- export function stageFiles(projectRoot: string, files: string[], deps: GitDeps = defaultDeps): void {
127
- const existing = files.filter((f) => deps.existsSync(join(projectRoot, f)));
128
- if (existing.length === 0) return;
129
- const escaped = existing.map((f) => `"${f}"`).join(" ");
130
- gitExec(`add -f ${escaped}`, projectRoot, deps, { stdio: "pipe" as const });
218
+ export function stageFiles(
219
+ projectRoot: string,
220
+ files: string[],
221
+ deps: GitDeps = defaultDeps,
222
+ ): void {
223
+ const existing = files.filter((f) => deps.existsSync(join(projectRoot, f)));
224
+ if (existing.length === 0) return;
225
+ const escaped = existing.map((f) => `"${f}"`).join(" ");
226
+ gitExec(`add -f ${escaped}`, projectRoot, deps, { stdio: "pipe" as const });
131
227
  }
132
228
 
133
229
  /**
134
230
  * Create a lightweight commit of the current working tree without touching the stash ref.
135
231
  * Returns the commit hash. Used as a pre-bash baseline for per-command diff.
136
232
  */
137
- export function gitStashCreate(projectRoot: string, deps: GitDeps = defaultDeps): string {
138
- const hash = gitExec("stash create --include-untracked", projectRoot, deps).trim();
139
- if (!hash) return "HEAD";
140
- return hash;
233
+ export function gitStashCreate(
234
+ projectRoot: string,
235
+ deps: GitDeps = defaultDeps,
236
+ ): string {
237
+ const hash = gitExec(
238
+ "stash create --include-untracked",
239
+ projectRoot,
240
+ deps,
241
+ ).trim();
242
+ if (!hash) return "HEAD";
243
+ return hash;
141
244
  }
142
245
 
143
- export function headHash(projectRoot: string, deps: GitDeps = defaultDeps): string {
144
- return gitExec("rev-parse HEAD", projectRoot, deps).trim();
246
+ export function headHash(
247
+ projectRoot: string,
248
+ deps: GitDeps = defaultDeps,
249
+ ): string {
250
+ return gitExec("rev-parse HEAD", projectRoot, deps).trim();
145
251
  }
146
252
 
147
253
  /** Get the commit message of HEAD. */
148
- export function headMessage(projectRoot: string, deps: GitDeps = defaultDeps): string {
149
- return gitExec("log -1 --format=%s HEAD", projectRoot, deps).trim();
254
+ export function headMessage(
255
+ projectRoot: string,
256
+ deps: GitDeps = defaultDeps,
257
+ ): string {
258
+ return gitExec("log -1 --format=%s HEAD", projectRoot, deps).trim();
150
259
  }
151
260
 
152
261
  /** Check if HEAD has a parent commit (i.e. can go back one). */
153
- export function hasParent(projectRoot: string, deps: GitDeps = defaultDeps): boolean {
154
- try {
155
- gitExec("rev-parse HEAD~1", projectRoot, deps);
156
- return true;
157
- } catch {
158
- return false;
159
- }
262
+ export function hasParent(
263
+ projectRoot: string,
264
+ deps: GitDeps = defaultDeps,
265
+ ): boolean {
266
+ try {
267
+ gitExec("rev-parse HEAD~1", projectRoot, deps);
268
+ return true;
269
+ } catch {
270
+ return false;
271
+ }
160
272
  }
161
273
 
162
274
  /**
163
275
  * Get files changed since a specific commit (instead of HEAD).
164
276
  */
165
- export function changesSince(projectRoot: string, commitHash: string, deps: GitDeps = defaultDeps): string[] {
166
- const out = gitExec(`diff --name-only ${commitHash} -- .`, projectRoot, deps).trim();
167
- const files = out ? out.split("\n") : [];
168
- // Also include untracked files
169
- const untracked = untrackedFiles(projectRoot, deps);
170
- return [...new Set([...files, ...untracked])];
277
+ export function changesSince(
278
+ projectRoot: string,
279
+ commitHash: string,
280
+ deps: GitDeps = defaultDeps,
281
+ ): string[] {
282
+ const out = gitExec(
283
+ `diff --name-only ${commitHash} -- .`,
284
+ projectRoot,
285
+ deps,
286
+ ).trim();
287
+ const files = out ? out.split("\n") : [];
288
+ // Also include untracked files
289
+ const untracked = untrackedFiles(projectRoot, deps);
290
+ return [...new Set([...files, ...untracked])];
171
291
  }
172
292
 
173
293
  /** Hard reset — discard all uncommitted changes (tracked and untracked), keep HEAD. */
174
- export function resetHard(projectRoot: string, deps: GitDeps = defaultDeps): void {
175
- gitExec("reset --hard", projectRoot, deps, { stdio: "pipe" as const });
176
- gitExec("clean -fd", projectRoot, deps, { stdio: "pipe" as const });
294
+ export function resetHard(
295
+ projectRoot: string,
296
+ deps: GitDeps = defaultDeps,
297
+ ): void {
298
+ gitExec("reset --hard", projectRoot, deps, { stdio: "pipe" as const });
299
+ gitExec("clean -fd", projectRoot, deps, { stdio: "pipe" as const });
177
300
  }
178
301
 
179
302
  /** Soft reset — remove last commit, keep working tree content as unstaged. */
180
- export function undoLastCommit(projectRoot: string, deps: GitDeps = defaultDeps): void {
181
- gitExec("reset --soft HEAD~1", projectRoot, deps, { stdio: "pipe" as const });
303
+ export function undoLastCommit(
304
+ projectRoot: string,
305
+ deps: GitDeps = defaultDeps,
306
+ ): void {
307
+ gitExec("reset --soft HEAD~1", projectRoot, deps, { stdio: "pipe" as const });
182
308
  }
package/engine/index.ts CHANGED
@@ -1,6 +1,29 @@
1
- export { isAllowed, disallowedFiles } from "./enforce.js";
2
- export { initGit, resetGit, snapshot, changesSinceSnapshot, changesSince, modifiedFiles, untrackedFiles, restoreFilesTo, gitStashCreate, stageFiles, headHash, headMessage, hasParent, resetHard, undoLastCommit } from "./git.js";
3
1
  export { loadConfig } from "./config.js";
2
+ export { disallowedFiles, isAllowed } from "./enforce.js";
3
+ export {
4
+ changesSince,
5
+ changesSinceSnapshot,
6
+ gitStashCreate,
7
+ hasParent,
8
+ headHash,
9
+ headMessage,
10
+ initGit,
11
+ modifiedFiles,
12
+ resetGit,
13
+ resetHard,
14
+ restoreFilesTo,
15
+ snapshot,
16
+ stageFiles,
17
+ undoLastCommit,
18
+ untrackedFiles,
19
+ } from "./git.js";
4
20
  export { loadPhaseState, savePhaseState } from "./state.js";
5
- export { nextPhase, checkGate, getDisallowedChanges } from "./transition.js";
6
- export type { Phase, PhaseState, Config, Transition, GateResult, TestRunner } from "./types.js";
21
+ export { checkGate, getDisallowedChanges, nextPhase } from "./transition.js";
22
+ export type {
23
+ Config,
24
+ GateResult,
25
+ Phase,
26
+ PhaseState,
27
+ TestRunner,
28
+ Transition,
29
+ } from "./types.js";
@@ -1,84 +1,84 @@
1
- import { describe, it, expect } from "vitest";
2
- import { mkdirSync, writeFileSync, rmSync } from "node:fs";
3
- import { join } from "node:path";
1
+ import { mkdirSync, rmSync, writeFileSync } from "node:fs";
4
2
  import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { describe, expect, it } from "vitest";
5
5
  import { loadPhaseState, savePhaseState } from "./state.js";
6
6
 
7
7
  function withTempDir(fn: (dir: string) => void) {
8
- const dir = join(tmpdir(), `tdd-state-test-${Date.now()}`);
9
- mkdirSync(dir, { recursive: true });
10
- try {
11
- fn(dir);
12
- } finally {
13
- rmSync(dir, { recursive: true, force: true });
14
- }
8
+ const dir = join(tmpdir(), `tdd-state-test-${Date.now()}`);
9
+ mkdirSync(dir, { recursive: true });
10
+ try {
11
+ fn(dir);
12
+ } finally {
13
+ rmSync(dir, { recursive: true, force: true });
14
+ }
15
15
  }
16
16
 
17
17
  describe("loadPhaseState", () => {
18
- it("throws when no file exists", () => {
19
- withTempDir((dir) => {
20
- expect(() => loadPhaseState(dir)).toThrow();
21
- });
22
- });
18
+ it("throws when no file exists", () => {
19
+ withTempDir((dir) => {
20
+ expect(() => loadPhaseState(dir)).toThrow();
21
+ });
22
+ });
23
23
 
24
- it("returns parsed state from state.json", () => {
25
- withTempDir((dir) => {
26
- const tddDir = join(dir, ".pi", "tdd");
27
- mkdirSync(tddDir, { recursive: true });
28
- writeFileSync(
29
- join(tddDir, "state.json"),
30
- JSON.stringify({ enabled: true, current: "green" }),
31
- "utf-8",
32
- );
33
- const state = loadPhaseState(dir);
34
- expect(state.enabled).toBe(true);
35
- expect(state.current).toBe("green");
36
- });
37
- });
24
+ it("returns parsed state from state.json", () => {
25
+ withTempDir((dir) => {
26
+ const tddDir = join(dir, ".pi", "tdd");
27
+ mkdirSync(tddDir, { recursive: true });
28
+ writeFileSync(
29
+ join(tddDir, "state.json"),
30
+ JSON.stringify({ enabled: true, current: "green" }),
31
+ "utf-8",
32
+ );
33
+ const state = loadPhaseState(dir);
34
+ expect(state.enabled).toBe(true);
35
+ expect(state.current).toBe("green");
36
+ });
37
+ });
38
38
 
39
- it("throws when current is an invalid phase", () => {
40
- withTempDir((dir) => {
41
- const tddDir = join(dir, ".pi", "tdd");
42
- mkdirSync(tddDir, { recursive: true });
43
- writeFileSync(
44
- join(tddDir, "state.json"),
45
- JSON.stringify({ enabled: true, current: "blurple" }),
46
- "utf-8",
47
- );
48
- expect(() => loadPhaseState(dir)).toThrow();
49
- });
50
- });
39
+ it("throws when current is an invalid phase", () => {
40
+ withTempDir((dir) => {
41
+ const tddDir = join(dir, ".pi", "tdd");
42
+ mkdirSync(tddDir, { recursive: true });
43
+ writeFileSync(
44
+ join(tddDir, "state.json"),
45
+ JSON.stringify({ enabled: true, current: "blurple" }),
46
+ "utf-8",
47
+ );
48
+ expect(() => loadPhaseState(dir)).toThrow();
49
+ });
50
+ });
51
51
 
52
- it("throws when current is the old 'off' value", () => {
53
- withTempDir((dir) => {
54
- const tddDir = join(dir, ".pi", "tdd");
55
- mkdirSync(tddDir, { recursive: true });
56
- writeFileSync(
57
- join(tddDir, "state.json"),
58
- JSON.stringify({ enabled: false, current: "off" }),
59
- "utf-8",
60
- );
61
- expect(() => loadPhaseState(dir)).toThrow();
62
- });
63
- });
52
+ it("throws when current is the old 'off' value", () => {
53
+ withTempDir((dir) => {
54
+ const tddDir = join(dir, ".pi", "tdd");
55
+ mkdirSync(tddDir, { recursive: true });
56
+ writeFileSync(
57
+ join(tddDir, "state.json"),
58
+ JSON.stringify({ enabled: false, current: "off" }),
59
+ "utf-8",
60
+ );
61
+ expect(() => loadPhaseState(dir)).toThrow();
62
+ });
63
+ });
64
64
 
65
- it("throws on malformed JSON", () => {
66
- withTempDir((dir) => {
67
- const tddDir = join(dir, ".pi", "tdd");
68
- mkdirSync(tddDir, { recursive: true });
69
- writeFileSync(join(tddDir, "state.json"), "not json{{{", "utf-8");
70
- expect(() => loadPhaseState(dir)).toThrow();
71
- });
72
- });
65
+ it("throws on malformed JSON", () => {
66
+ withTempDir((dir) => {
67
+ const tddDir = join(dir, ".pi", "tdd");
68
+ mkdirSync(tddDir, { recursive: true });
69
+ writeFileSync(join(tddDir, "state.json"), "not json{{{", "utf-8");
70
+ expect(() => loadPhaseState(dir)).toThrow();
71
+ });
72
+ });
73
73
  });
74
74
 
75
75
  describe("savePhaseState", () => {
76
- it("writes state.json and can be read back", () => {
77
- withTempDir((dir) => {
78
- savePhaseState(dir, { enabled: true, current: "refactor" });
79
- const state = loadPhaseState(dir);
80
- expect(state.enabled).toBe(true);
81
- expect(state.current).toBe("refactor");
82
- });
83
- });
76
+ it("writes state.json and can be read back", () => {
77
+ withTempDir((dir) => {
78
+ savePhaseState(dir, { enabled: true, current: "refactor" });
79
+ const state = loadPhaseState(dir);
80
+ expect(state.enabled).toBe(true);
81
+ expect(state.current).toBe("refactor");
82
+ });
83
+ });
84
84
  });