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,155 +1,165 @@
1
- import { describe, it, expect } from "vitest";
2
- import { isAllowed, disallowedFiles } from "./enforce.js";
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
- blockedInRed: ["src/**/*.ts", "lib/**/*.ts"],
7
- blockedInGreen: ["tests/**/*.test.ts", "specs/**/*.spec.ts"],
8
- testCommands: ["npm test"],
9
- timeoutSeconds: 30,
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
- 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(false);
89
- });
90
-
91
- it("supports ! negation in blockedInRed as well", () => {
92
- const redExcl: Config = {
93
- blockedInRed: ["src/**/*.ts", "!src/**/*.test.ts"],
94
- blockedInGreen: [],
95
- testCommands: [],
96
- timeoutSeconds: 30,
97
- };
98
- // test file excluded from block → allowed in red
99
- expect(isAllowed("src/main.test.ts", "red", redExcl)).toBe(true);
100
- // impl file still blocked
101
- expect(isAllowed("src/main.ts", "red", redExcl)).toBe(false);
102
- // free file unaffected
103
- expect(isAllowed("README.md", "red", redExcl)).toBe(true);
104
- });
105
-
106
- it("free file with exclusions in config remains free", () => {
107
- expect(isAllowed("README.md", "green", exclConfig)).toBe(true);
108
- });
109
-
110
- it("file matching !pattern alone (no positive match) is not blocked", () => {
111
- // File matches the negation pattern but NOT the positive pattern.
112
- // MatchPatterns returns false (no positive match), so it's not blocked.
113
- expect(isAllowed("other/foo.test.ts", "green", exclConfig)).toBe(true);
114
- });
115
-
116
- it("exclusion-only patterns block nothing", () => {
117
- const exclOnly: Config = {
118
- blockedInRed: [],
119
- blockedInGreen: ["!**/*.test.ts"],
120
- testCommands: [],
121
- timeoutSeconds: 30,
122
- };
123
- expect(isAllowed("any/file.ts", "green", exclOnly)).toBe(true);
124
- expect(isAllowed("README.md", "green", exclOnly)).toBe(true);
125
- expect(isAllowed("tests/foo.test.ts", "green", exclOnly)).toBe(true);
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
- it("returns empty for refactor phase", () => {
132
- expect(disallowedFiles(["src/main.ts", "tests/foo.test.ts"], "refactor", testConfig)).toEqual([]);
133
- });
134
-
135
- it("returns empty when input list is empty", () => {
136
- expect(disallowedFiles([], "red", testConfig)).toEqual([]);
137
- expect(disallowedFiles([], "green", testConfig)).toEqual([]);
138
- });
139
-
140
- it("filters out blockedInRed files in red phase", () => {
141
- const files = ["src/main.ts", "README.md", "tests/foo.test.ts"];
142
- expect(disallowedFiles(files, "red", testConfig)).toEqual(["src/main.ts"]);
143
- });
144
-
145
- it("filters out blockedInGreen files in green phase", () => {
146
- const files = ["tests/foo.test.ts", "README.md", "src/main.ts"];
147
- expect(disallowedFiles(files, "green", testConfig)).toEqual(["tests/foo.test.ts"]);
148
- });
149
-
150
- it("allows free files in both phases", () => {
151
- const free = ["README.md", "package.json", "docs/guide.md"];
152
- expect(disallowedFiles(free, "red", testConfig)).toEqual([]);
153
- expect(disallowedFiles(free, "green", testConfig)).toEqual([]);
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 { Phase, Config } from "./types.js";
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
- const positive: string[] = [];
14
- const negative: string[] = [];
13
+ const positive: string[] = [];
14
+ const negative: string[] = [];
15
15
 
16
- for (const p of patterns) {
17
- if (p.startsWith("!")) {
18
- negative.push(p.slice(1));
19
- } else {
20
- positive.push(p);
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
- if (positive.length === 0) return false;
24
+ if (positive.length === 0) return false;
25
25
 
26
- const matchesPositive = positive.some((p) => picomatch(p)(filePath));
27
- if (!matchesPositive) return false;
26
+ const matchesPositive = positive.some((p) => picomatch(p)(filePath));
27
+ if (!matchesPositive) return false;
28
28
 
29
- const matchesNegative = negative.some((p) => picomatch(p)(filePath));
30
- return !matchesNegative;
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(filePath: string, phase: Phase, config: Config): boolean {
43
- if (phase === "refactor") return true;
44
-
45
- const blocked = phase === "red" ? config.blockedInRed : config.blockedInGreen;
46
- return !matchPatterns(blocked, filePath);
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(files: string[], phase: Phase, config: Config): string[] {
53
- if (phase === "refactor") return [];
54
- return files.filter((f) => !isAllowed(f, phase, config));
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
  }