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.
- 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/skills/tdd-enforcer/SKILL.md +2 -2
- package/tsconfig.json +10 -10
package/engine/git.ts
CHANGED
|
@@ -1,182 +1,308 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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(
|
|
34
|
-
|
|
35
|
-
|
|
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(
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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(
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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(
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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(
|
|
81
|
-
|
|
82
|
-
|
|
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(
|
|
86
|
-
|
|
87
|
-
|
|
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(
|
|
91
|
-
|
|
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(
|
|
95
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
105
|
-
|
|
191
|
+
const trackedFiles = files.filter((f) => trackedSet.has(f));
|
|
192
|
+
const untrackedFilesList = files.filter((f) => !trackedSet.has(f));
|
|
106
193
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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(
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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(
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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(
|
|
144
|
-
|
|
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(
|
|
149
|
-
|
|
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(
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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(
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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(
|
|
175
|
-
|
|
176
|
-
|
|
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(
|
|
181
|
-
|
|
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 {
|
|
6
|
-
export type {
|
|
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";
|
package/engine/state.test.ts
CHANGED
|
@@ -1,84 +1,84 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
18
|
+
it("throws when no file exists", () => {
|
|
19
|
+
withTempDir((dir) => {
|
|
20
|
+
expect(() => loadPhaseState(dir)).toThrow();
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
});
|