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/.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 +388 -272
- package/adapters/pi/index.ts +284 -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 +29 -30
- package/adapters/pi/tools.test.ts +387 -331
- package/adapters/pi/tools.ts +321 -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 +30 -23
- package/tsconfig.json +10 -10
package/engine/enforce.test.ts
CHANGED
|
@@ -1,155 +1,165 @@
|
|
|
1
|
-
import { describe,
|
|
2
|
-
import {
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { disallowedFiles, isAllowed } from "./enforce.js";
|
|
3
3
|
import type { Config } from "./types.js";
|
|
4
4
|
|
|
5
5
|
const testConfig: Config = {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
blockedInRed: ["src/**/*.ts", "lib/**/*.ts"],
|
|
7
|
+
blockedInGreen: ["tests/**/*.test.ts", "specs/**/*.spec.ts"],
|
|
8
|
+
testCommands: ["npm test"],
|
|
9
|
+
timeoutSeconds: 30,
|
|
10
10
|
};
|
|
11
11
|
|
|
12
12
|
describe("isAllowed", () => {
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
13
|
+
it("allows everything in refactor phase", () => {
|
|
14
|
+
expect(isAllowed("any/file.ts", "refactor", testConfig)).toBe(true);
|
|
15
|
+
expect(isAllowed("tests/foo.test.ts", "refactor", testConfig)).toBe(true);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe("red phase", () => {
|
|
19
|
+
it("allows free files", () => {
|
|
20
|
+
expect(isAllowed("README.md", "red", testConfig)).toBe(true);
|
|
21
|
+
expect(isAllowed("package.json", "red", testConfig)).toBe(true);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("blocks files in blockedInRed", () => {
|
|
25
|
+
expect(isAllowed("src/main.ts", "red", testConfig)).toBe(false);
|
|
26
|
+
expect(isAllowed("lib/helper.ts", "red", testConfig)).toBe(false);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("allows test files in red phase", () => {
|
|
30
|
+
expect(isAllowed("tests/foo.test.ts", "red", testConfig)).toBe(true);
|
|
31
|
+
expect(isAllowed("specs/api.spec.ts", "red", testConfig)).toBe(true);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe("green phase", () => {
|
|
36
|
+
it("allows free files", () => {
|
|
37
|
+
expect(isAllowed("README.md", "green", testConfig)).toBe(true);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("allows implementation files in green phase", () => {
|
|
41
|
+
expect(isAllowed("src/main.ts", "green", testConfig)).toBe(true);
|
|
42
|
+
expect(isAllowed("lib/helper.ts", "green", testConfig)).toBe(true);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("blocks files in blockedInGreen", () => {
|
|
46
|
+
expect(isAllowed("tests/foo.test.ts", "green", testConfig)).toBe(false);
|
|
47
|
+
expect(isAllowed("specs/api.spec.ts", "green", testConfig)).toBe(false);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe("negation patterns (!)", () => {
|
|
52
|
+
const exclConfig: Config = {
|
|
53
|
+
blockedInRed: ["src/**/*.ts"],
|
|
54
|
+
blockedInGreen: ["src/**/*.ts", "!**/*.test.ts"],
|
|
55
|
+
testCommands: ["npm test"],
|
|
56
|
+
timeoutSeconds: 30,
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
it("excludes files matching !pattern from the block", () => {
|
|
60
|
+
expect(isAllowed("src/main.test.ts", "green", exclConfig)).toBe(true);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("still blocks files not matching the exclusion", () => {
|
|
64
|
+
expect(isAllowed("src/main.ts", "green", exclConfig)).toBe(false);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("excludes spec files with a second !pattern", () => {
|
|
68
|
+
const multiExcl: Config = {
|
|
69
|
+
blockedInRed: [],
|
|
70
|
+
blockedInGreen: ["src/**/*.ts", "!**/*.test.ts", "!**/*.spec.ts"],
|
|
71
|
+
testCommands: [],
|
|
72
|
+
timeoutSeconds: 30,
|
|
73
|
+
};
|
|
74
|
+
expect(isAllowed("src/main.spec.ts", "green", multiExcl)).toBe(true);
|
|
75
|
+
expect(isAllowed("src/main.test.ts", "green", multiExcl)).toBe(true);
|
|
76
|
+
expect(isAllowed("src/main.ts", "green", multiExcl)).toBe(false);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("excludes via complex glob in negation pattern", () => {
|
|
80
|
+
const complexExcl: Config = {
|
|
81
|
+
blockedInRed: [],
|
|
82
|
+
blockedInGreen: ["src/**", "!src/vendor/**"],
|
|
83
|
+
testCommands: [],
|
|
84
|
+
timeoutSeconds: 30,
|
|
85
|
+
};
|
|
86
|
+
expect(isAllowed("src/vendor/lib.js", "green", complexExcl)).toBe(true);
|
|
87
|
+
expect(isAllowed("src/app.ts", "green", complexExcl)).toBe(false);
|
|
88
|
+
expect(isAllowed("src/utils/helper.ts", "green", complexExcl)).toBe(
|
|
89
|
+
false,
|
|
90
|
+
);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("supports ! negation in blockedInRed as well", () => {
|
|
94
|
+
const redExcl: Config = {
|
|
95
|
+
blockedInRed: ["src/**/*.ts", "!src/**/*.test.ts"],
|
|
96
|
+
blockedInGreen: [],
|
|
97
|
+
testCommands: [],
|
|
98
|
+
timeoutSeconds: 30,
|
|
99
|
+
};
|
|
100
|
+
// test file excluded from block → allowed in red
|
|
101
|
+
expect(isAllowed("src/main.test.ts", "red", redExcl)).toBe(true);
|
|
102
|
+
// impl file still blocked
|
|
103
|
+
expect(isAllowed("src/main.ts", "red", redExcl)).toBe(false);
|
|
104
|
+
// free file unaffected
|
|
105
|
+
expect(isAllowed("README.md", "red", redExcl)).toBe(true);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("free file with exclusions in config remains free", () => {
|
|
109
|
+
expect(isAllowed("README.md", "green", exclConfig)).toBe(true);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("file matching !pattern alone (no positive match) is not blocked", () => {
|
|
113
|
+
// File matches the negation pattern but NOT the positive pattern.
|
|
114
|
+
// MatchPatterns returns false (no positive match), so it's not blocked.
|
|
115
|
+
expect(isAllowed("other/foo.test.ts", "green", exclConfig)).toBe(true);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("exclusion-only patterns block nothing", () => {
|
|
119
|
+
const exclOnly: Config = {
|
|
120
|
+
blockedInRed: [],
|
|
121
|
+
blockedInGreen: ["!**/*.test.ts"],
|
|
122
|
+
testCommands: [],
|
|
123
|
+
timeoutSeconds: 30,
|
|
124
|
+
};
|
|
125
|
+
expect(isAllowed("any/file.ts", "green", exclOnly)).toBe(true);
|
|
126
|
+
expect(isAllowed("README.md", "green", exclOnly)).toBe(true);
|
|
127
|
+
expect(isAllowed("tests/foo.test.ts", "green", exclOnly)).toBe(true);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
128
130
|
});
|
|
129
131
|
|
|
130
132
|
describe("disallowedFiles", () => {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
133
|
+
it("returns empty for refactor phase", () => {
|
|
134
|
+
expect(
|
|
135
|
+
disallowedFiles(
|
|
136
|
+
["src/main.ts", "tests/foo.test.ts"],
|
|
137
|
+
"refactor",
|
|
138
|
+
testConfig,
|
|
139
|
+
),
|
|
140
|
+
).toEqual([]);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("returns empty when input list is empty", () => {
|
|
144
|
+
expect(disallowedFiles([], "red", testConfig)).toEqual([]);
|
|
145
|
+
expect(disallowedFiles([], "green", testConfig)).toEqual([]);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("filters out blockedInRed files in red phase", () => {
|
|
149
|
+
const files = ["src/main.ts", "README.md", "tests/foo.test.ts"];
|
|
150
|
+
expect(disallowedFiles(files, "red", testConfig)).toEqual(["src/main.ts"]);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("filters out blockedInGreen files in green phase", () => {
|
|
154
|
+
const files = ["tests/foo.test.ts", "README.md", "src/main.ts"];
|
|
155
|
+
expect(disallowedFiles(files, "green", testConfig)).toEqual([
|
|
156
|
+
"tests/foo.test.ts",
|
|
157
|
+
]);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("allows free files in both phases", () => {
|
|
161
|
+
const free = ["README.md", "package.json", "docs/guide.md"];
|
|
162
|
+
expect(disallowedFiles(free, "red", testConfig)).toEqual([]);
|
|
163
|
+
expect(disallowedFiles(free, "green", testConfig)).toEqual([]);
|
|
164
|
+
});
|
|
155
165
|
});
|
package/engine/enforce.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import picomatch from "picomatch";
|
|
2
|
-
import type {
|
|
2
|
+
import type { Config, Phase } from "./types.js";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Match a file path against a list of glob patterns with !exclusion support.
|
|
@@ -10,24 +10,24 @@ import type { Phase, Config } from "./types.js";
|
|
|
10
10
|
* Empty pattern list = no match.
|
|
11
11
|
*/
|
|
12
12
|
function matchPatterns(patterns: string[], filePath: string): boolean {
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
const positive: string[] = [];
|
|
14
|
+
const negative: string[] = [];
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
16
|
+
for (const p of patterns) {
|
|
17
|
+
if (p.startsWith("!")) {
|
|
18
|
+
negative.push(p.slice(1));
|
|
19
|
+
} else {
|
|
20
|
+
positive.push(p);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
if (positive.length === 0) return false;
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
const matchesPositive = positive.some((p) => picomatch(p)(filePath));
|
|
27
|
+
if (!matchesPositive) return false;
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
const matchesNegative = negative.some((p) => picomatch(p)(filePath));
|
|
30
|
+
return !matchesNegative;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
/**
|
|
@@ -39,17 +39,25 @@ function matchPatterns(patterns: string[], filePath: string): boolean {
|
|
|
39
39
|
* - GREEN: files in blockedInGreen are blocked, everything else is free
|
|
40
40
|
* - ! negation patterns exclude subsets from a block list
|
|
41
41
|
*/
|
|
42
|
-
export function isAllowed(
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
42
|
+
export function isAllowed(
|
|
43
|
+
filePath: string,
|
|
44
|
+
phase: Phase,
|
|
45
|
+
config: Config,
|
|
46
|
+
): boolean {
|
|
47
|
+
if (phase === "refactor") return true;
|
|
48
|
+
|
|
49
|
+
const blocked = phase === "red" ? config.blockedInRed : config.blockedInGreen;
|
|
50
|
+
return !matchPatterns(blocked, filePath);
|
|
47
51
|
}
|
|
48
52
|
|
|
49
53
|
/**
|
|
50
54
|
* Filter a list of file paths to those that are disallowed in the current phase.
|
|
51
55
|
*/
|
|
52
|
-
export function disallowedFiles(
|
|
53
|
-
|
|
54
|
-
|
|
56
|
+
export function disallowedFiles(
|
|
57
|
+
files: string[],
|
|
58
|
+
phase: Phase,
|
|
59
|
+
config: Config,
|
|
60
|
+
): string[] {
|
|
61
|
+
if (phase === "refactor") return [];
|
|
62
|
+
return files.filter((f) => !isAllowed(f, phase, config));
|
|
55
63
|
}
|