@lifeaitools/rdc-skills 0.34.0 → 0.35.0

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.
Files changed (60) hide show
  1. package/.claude-plugin/plugin.json +284 -1
  2. package/VALIDATOR-ARCHITECTURE.md +534 -0
  3. package/commands/analyze-tests.md +11 -0
  4. package/commands/check-clean-code.md +11 -0
  5. package/commands/check-packages.md +10 -0
  6. package/commands/compare-compliance.md +14 -0
  7. package/commands/full-analysis.md +50 -0
  8. package/commands/get-refactoring-plan.md +13 -0
  9. package/commands/quick-check.md +13 -0
  10. package/commands/recover.md +149 -0
  11. package/commands/review-arch.md +12 -0
  12. package/commands/review.md +12 -113
  13. package/commands/suggest-patterns.md +11 -0
  14. package/commands/validate-solid.md +11 -0
  15. package/package.json +14 -2
  16. package/scripts/architecture-score.mjs +157 -0
  17. package/scripts/clean-code-score.mjs +177 -0
  18. package/scripts/duplication-score.mjs +66 -0
  19. package/scripts/lib/architecture-scoring.mjs +695 -0
  20. package/scripts/lib/clean-code-scoring.mjs +258 -0
  21. package/scripts/lib/duplication-scoring.mjs +238 -0
  22. package/scripts/lib/language-plugin.mjs +82 -0
  23. package/scripts/lib/package-metrics.mjs +439 -0
  24. package/scripts/lib/pattern-scoring.mjs +351 -0
  25. package/scripts/lib/plugins/treesitter.mjs +1182 -0
  26. package/scripts/lib/plugins/typescript.mjs +672 -0
  27. package/scripts/lib/refactoring-scoring.mjs +307 -0
  28. package/scripts/lib/solid-scoring.mjs +101 -0
  29. package/scripts/lib/test-smell-scoring.mjs +581 -0
  30. package/scripts/lib/vendor/codeflow-parser/.source-commit +1 -0
  31. package/scripts/lib/vendor/codeflow-parser/grammars.d.ts +23 -0
  32. package/scripts/lib/vendor/codeflow-parser/grammars.js +57 -0
  33. package/scripts/lib/vendor/codeflow-parser/memberFacts.d.ts +274 -0
  34. package/scripts/lib/vendor/codeflow-parser/memberFacts.js +1117 -0
  35. package/scripts/lib/vendor/codeflow-parser/nativeParser.d.ts +115 -0
  36. package/scripts/lib/vendor/codeflow-parser/nativeParser.js +759 -0
  37. package/scripts/lib/vendor/codeflow-parser/package.json +3 -0
  38. package/scripts/lib/vendor/codeflow-parser/xmlParser.d.ts +77 -0
  39. package/scripts/lib/vendor/codeflow-parser/xmlParser.js +400 -0
  40. package/scripts/package-metrics-cli.mjs +112 -0
  41. package/scripts/pattern-score.mjs +143 -0
  42. package/scripts/refactoring-score.mjs +253 -0
  43. package/scripts/solid-score.mjs +337 -0
  44. package/skills/architecture-reviewer/SKILL.md +287 -0
  45. package/skills/clean-code-analyzer/SKILL.md +147 -0
  46. package/skills/package-design/SKILL.md +118 -0
  47. package/skills/pattern-advisor/SKILL.md +237 -0
  48. package/skills/pattern-refactoring-guide/SKILL.md +262 -0
  49. package/skills/review/SKILL.md +29 -0
  50. package/skills/solid-validator/SKILL.md +92 -0
  51. package/skills/testing-strategy/SKILL.md +132 -0
  52. package/tests/lib/architecture-scoring.test.mjs +335 -0
  53. package/tests/lib/clean-code-scoring.test.mjs +241 -0
  54. package/tests/lib/duplication-scoring.test.mjs +144 -0
  55. package/tests/lib/fixtures.mjs +58 -0
  56. package/tests/lib/package-metrics.test.mjs +241 -0
  57. package/tests/lib/pattern-scoring.test.mjs +251 -0
  58. package/tests/lib/refactoring-scoring.test.mjs +264 -0
  59. package/tests/lib/solid-scoring.test.mjs +291 -0
  60. package/tests/lib/test-smell-scoring.test.mjs +281 -0
@@ -0,0 +1,291 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+
4
+ import { srp, ocp, lsp, isp, dip, scoreUnit } from '../../scripts/lib/solid-scoring.mjs';
5
+ import { makeMember, makeUnit } from './fixtures.mjs';
6
+
7
+ // ── empty / trivial input ───────────────────────────────────────────────
8
+
9
+ test('srp: zero members returns unmeasured, not a fake clean 100', () => {
10
+ const r = srp(makeUnit({ members: [] }));
11
+ assert.equal(r.score, 100);
12
+ assert.equal(r.confidence, 'none');
13
+ });
14
+
15
+ test('ocp: zero members returns unmeasured', () => {
16
+ const r = ocp(makeUnit({ members: [] }));
17
+ assert.equal(r.score, 100);
18
+ assert.equal(r.confidence, 'none');
19
+ });
20
+
21
+ test('isp: zero members returns unmeasured', () => {
22
+ const r = isp(makeUnit({ members: [] }));
23
+ assert.equal(r.score, 100);
24
+ assert.equal(r.confidence, 'none');
25
+ });
26
+
27
+ test('lsp: no base class scores 100 at low-medium confidence (nothing to violate)', () => {
28
+ const r = lsp(makeUnit({ hasBaseClass: false }));
29
+ assert.equal(r.score, 100);
30
+ assert.equal(r.confidence, 'low-medium');
31
+ assert.match(r.detail, /no base class/);
32
+ });
33
+
34
+ test('dip: zero total dependencies scores 100 (nothing to be concrete about)', () => {
35
+ const r = dip(makeUnit({ totalDependencies: 0 }));
36
+ assert.equal(r.score, 100);
37
+ assert.equal(r.confidence, 'high');
38
+ });
39
+
40
+ test('srp: exactly one member scores 100 at high confidence', () => {
41
+ const r = srp(makeUnit({ members: [makeMember()] }));
42
+ assert.equal(r.score, 100);
43
+ assert.equal(r.confidence, 'high');
44
+ });
45
+
46
+ // ── SRP — connected components ──────────────────────────────────────────
47
+
48
+ test('srp: two members with no shared field and no cross-calls are 2 components — score 70', () => {
49
+ const unit = makeUnit({
50
+ members: [
51
+ makeMember({ name: 'a', fieldAccess: ['x'] }),
52
+ makeMember({ name: 'b', fieldAccess: ['y'] }),
53
+ ],
54
+ });
55
+ const r = srp(unit);
56
+ assert.match(r.detail, /2 connected component/);
57
+ assert.equal(r.score, 70);
58
+ });
59
+
60
+ test('srp: two members sharing a field collapse to 1 component — score 100 (clean)', () => {
61
+ const unit = makeUnit({
62
+ members: [
63
+ makeMember({ name: 'a', fieldAccess: ['shared'] }),
64
+ makeMember({ name: 'b', fieldAccess: ['shared'] }),
65
+ ],
66
+ });
67
+ const r = srp(unit);
68
+ assert.match(r.detail, /1 connected component/);
69
+ assert.equal(r.score, 100);
70
+ });
71
+
72
+ test('srp: two members where one calls the other also collapse to 1 component', () => {
73
+ const unit = makeUnit({
74
+ members: [
75
+ makeMember({ name: 'a', calls: ['b'] }),
76
+ makeMember({ name: 'b' }),
77
+ ],
78
+ });
79
+ const r = srp(unit);
80
+ assert.equal(r.score, 100);
81
+ });
82
+
83
+ test('srp: three fully isolated members are 3 components — score 40', () => {
84
+ const unit = makeUnit({
85
+ members: [
86
+ makeMember({ name: 'a', fieldAccess: ['x'] }),
87
+ makeMember({ name: 'b', fieldAccess: ['y'] }),
88
+ makeMember({ name: 'c', fieldAccess: ['z'] }),
89
+ ],
90
+ });
91
+ assert.equal(srp(unit).score, 40);
92
+ });
93
+
94
+ test('srp: four or more fully isolated members floor at score 10', () => {
95
+ const unit = makeUnit({
96
+ members: [
97
+ makeMember({ name: 'a', fieldAccess: ['w'] }),
98
+ makeMember({ name: 'b', fieldAccess: ['x'] }),
99
+ makeMember({ name: 'c', fieldAccess: ['y'] }),
100
+ makeMember({ name: 'd', fieldAccess: ['z'] }),
101
+ ],
102
+ });
103
+ assert.equal(srp(unit).score, 10);
104
+ });
105
+
106
+ // ── OCP — branch-hit density ────────────────────────────────────────────
107
+
108
+ test('ocp: known violation — high branch-hit density scores near/at 0', () => {
109
+ const unit = makeUnit({ members: [makeMember({ branchHits: 8 })] });
110
+ const r = ocp(unit);
111
+ assert.equal(r.score, 0);
112
+ assert.equal(r.confidence, 'low');
113
+ });
114
+
115
+ test('ocp: known-clean — zero branch hits scores 100', () => {
116
+ const unit = makeUnit({ members: [makeMember({ branchHits: 0 })] });
117
+ assert.equal(ocp(unit).score, 100);
118
+ });
119
+
120
+ test('ocp: density formula boundary — density=4 clips exactly to score 0', () => {
121
+ const unit = makeUnit({ members: [makeMember({ branchHits: 4 })] });
122
+ assert.equal(ocp(unit).score, 0);
123
+ });
124
+
125
+ test('ocp: density formula just below the clip — density=3 scores 25', () => {
126
+ const unit = makeUnit({ members: [makeMember({ branchHits: 3 })] });
127
+ assert.equal(ocp(unit).score, 25);
128
+ });
129
+
130
+ // ── LSP — override drift ────────────────────────────────────────────────
131
+
132
+ test('lsp: hasBaseClass true but no overridden members scores 100', () => {
133
+ const unit = makeUnit({ hasBaseClass: true, members: [makeMember({ override: null })] });
134
+ const r = lsp(unit);
135
+ assert.equal(r.score, 100);
136
+ assert.match(r.detail, /no overridden methods/);
137
+ });
138
+
139
+ test('lsp: known violation — full drift (param mismatch, no super call, return-type mismatch) scores 0', () => {
140
+ const unit = makeUnit({
141
+ hasBaseClass: true,
142
+ members: [makeMember({
143
+ paramCount: 3,
144
+ override: { baseParamCount: 2, callsSuper: false, returnType: 'string', baseReturnType: 'number' },
145
+ })],
146
+ });
147
+ assert.equal(lsp(unit).score, 0);
148
+ });
149
+
150
+ test('lsp: known-clean — override matches base exactly scores 100', () => {
151
+ const unit = makeUnit({
152
+ hasBaseClass: true,
153
+ members: [makeMember({
154
+ paramCount: 2,
155
+ override: { baseParamCount: 2, callsSuper: true, returnType: 'number', baseReturnType: 'number' },
156
+ })],
157
+ });
158
+ assert.equal(lsp(unit).score, 100);
159
+ });
160
+
161
+ test('lsp: boundary — exactly one of three drift signals scores 67', () => {
162
+ const unit = makeUnit({
163
+ hasBaseClass: true,
164
+ members: [makeMember({
165
+ paramCount: 2, // matches base -> no drift here
166
+ override: { baseParamCount: 2, callsSuper: false, returnType: 'number', baseReturnType: 'number' },
167
+ })],
168
+ });
169
+ assert.equal(lsp(unit).score, 67);
170
+ });
171
+
172
+ test('lsp: boundary — exactly two of three drift signals scores 33', () => {
173
+ const unit = makeUnit({
174
+ hasBaseClass: true,
175
+ members: [makeMember({
176
+ paramCount: 3, // mismatch -> drift
177
+ override: { baseParamCount: 2, callsSuper: false, returnType: 'number', baseReturnType: 'number' },
178
+ })],
179
+ });
180
+ assert.equal(lsp(unit).score, 33);
181
+ });
182
+
183
+ // ── ISP — public member count + avg params ──────────────────────────────
184
+
185
+ test('isp: known-clean — no public members scores 100 at medium-high confidence', () => {
186
+ const unit = makeUnit({ members: [makeMember({ isPublic: false })] });
187
+ const r = isp(unit);
188
+ assert.equal(r.score, 100);
189
+ assert.equal(r.confidence, 'medium-high');
190
+ });
191
+
192
+ test('isp: known violation — many public members with high avg params scores low', () => {
193
+ const members = Array.from({ length: 21 }, (_, i) => makeMember({ name: `m${i}`, paramCount: 5 }));
194
+ const r = isp(makeUnit({ members }));
195
+ assert.equal(r.score, 28); // countScore 15 (len>20), paramScore 40 (avg5>4) -> round((15+40)/2)
196
+ });
197
+
198
+ test('isp: boundary — publicMembers.length=5 (<=5) scores countScore 100', () => {
199
+ const members = Array.from({ length: 5 }, (_, i) => makeMember({ name: `m${i}`, paramCount: 0 }));
200
+ assert.equal(isp(makeUnit({ members })).score, 100);
201
+ });
202
+
203
+ test('isp: boundary — publicMembers.length=6 (>5) drops countScore to 75', () => {
204
+ const members = Array.from({ length: 6 }, (_, i) => makeMember({ name: `m${i}`, paramCount: 0 }));
205
+ assert.equal(isp(makeUnit({ members })).score, 88); // round((75+100)/2)
206
+ });
207
+
208
+ test('isp: boundary — publicMembers.length=10 (<=10) still countScore 75', () => {
209
+ const members = Array.from({ length: 10 }, (_, i) => makeMember({ name: `m${i}`, paramCount: 0 }));
210
+ assert.equal(isp(makeUnit({ members })).score, 88);
211
+ });
212
+
213
+ test('isp: boundary — publicMembers.length=11 (>10) drops countScore to 45', () => {
214
+ const members = Array.from({ length: 11 }, (_, i) => makeMember({ name: `m${i}`, paramCount: 0 }));
215
+ assert.equal(isp(makeUnit({ members })).score, 73); // round((45+100)/2)
216
+ });
217
+
218
+ test('isp: boundary — publicMembers.length=20 (<=20) still countScore 45', () => {
219
+ const members = Array.from({ length: 20 }, (_, i) => makeMember({ name: `m${i}`, paramCount: 0 }));
220
+ assert.equal(isp(makeUnit({ members })).score, 73);
221
+ });
222
+
223
+ test('isp: boundary — publicMembers.length=21 (>20) drops countScore to 15', () => {
224
+ const members = Array.from({ length: 21 }, (_, i) => makeMember({ name: `m${i}`, paramCount: 0 }));
225
+ assert.equal(isp(makeUnit({ members })).score, 58); // round((15+100)/2)
226
+ });
227
+
228
+ test('isp: boundary — avgParams=2 (<=2) scores paramScore 100', () => {
229
+ const unit = makeUnit({ members: [makeMember({ paramCount: 2 })] });
230
+ assert.equal(isp(unit).score, 100); // countScore 100, paramScore 100
231
+ });
232
+
233
+ test('isp: boundary — avgParams=3 (>2) drops paramScore to 75', () => {
234
+ const unit = makeUnit({ members: [makeMember({ paramCount: 3 })] });
235
+ assert.equal(isp(unit).score, 88); // round((100+75)/2)
236
+ });
237
+
238
+ test('isp: boundary — avgParams=4 (<=4) still paramScore 75', () => {
239
+ const unit = makeUnit({ members: [makeMember({ paramCount: 4 })] });
240
+ assert.equal(isp(unit).score, 88);
241
+ });
242
+
243
+ test('isp: boundary — avgParams=5 (>4) drops paramScore to 40', () => {
244
+ const unit = makeUnit({ members: [makeMember({ paramCount: 5 })] });
245
+ assert.equal(isp(unit).score, 70); // round((100+40)/2)
246
+ });
247
+
248
+ // ── DIP — concrete-instantiation ratio ──────────────────────────────────
249
+
250
+ test('dip: known violation — every dependency is a concrete instantiation scores 0', () => {
251
+ const unit = makeUnit({ concreteInstantiations: 5, totalDependencies: 5 });
252
+ assert.equal(dip(unit).score, 0);
253
+ });
254
+
255
+ test('dip: known-clean — zero concrete instantiations of nonzero deps scores 100', () => {
256
+ const unit = makeUnit({ concreteInstantiations: 0, totalDependencies: 5 });
257
+ assert.equal(dip(unit).score, 100);
258
+ });
259
+
260
+ test('dip: boundary — half the dependencies concrete scores exactly 50', () => {
261
+ const unit = makeUnit({ concreteInstantiations: 1, totalDependencies: 2 });
262
+ assert.equal(dip(unit).score, 50);
263
+ });
264
+
265
+ // ── scoreUnit — weighted aggregate + unmeasured exclusion ──────────────
266
+
267
+ test('scoreUnit: unmeasured criteria (confidence "none") are excluded and renormalized, not zeroed', () => {
268
+ const unit = makeUnit({ members: [] }); // srp/ocp/isp all 'none' on empty unit
269
+ const r = scoreUnit(unit, { srp: 1, ocp: 1, lsp: 1, isp: 1, dip: 1 });
270
+ assert.deepEqual(new Set(r.unmeasured), new Set(['srp', 'ocp', 'isp']));
271
+ assert.equal(r.total, 100); // only lsp(100) + dip(100) measured
272
+ });
273
+
274
+ test('scoreUnit: total is null, not 0, when every weighted criterion is unmeasured', () => {
275
+ const unit = makeUnit({ members: [] });
276
+ const r = scoreUnit(unit, { srp: 1, ocp: 1, isp: 1 }); // omit lsp/dip, the only always-measured ones
277
+ assert.equal(r.total, null);
278
+ assert.deepEqual(new Set(r.unmeasured), new Set(['srp', 'ocp', 'isp']));
279
+ });
280
+
281
+ test('scoreUnit: a populated unit produces a real weighted average across all five criteria', () => {
282
+ const unit = makeUnit({
283
+ hasBaseClass: false,
284
+ concreteInstantiations: 0,
285
+ totalDependencies: 1,
286
+ members: [makeMember({ branchHits: 0, paramCount: 1 })],
287
+ });
288
+ const r = scoreUnit(unit, { srp: 1, ocp: 1, lsp: 1, isp: 1, dip: 1 });
289
+ assert.deepEqual(r.unmeasured, []);
290
+ assert.equal(r.total, 100); // single member, everything clean
291
+ });
@@ -0,0 +1,281 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+
4
+ import {
5
+ findTestBlocks, findSetupBlocks, checkInsufficientTests, checkIgnoredTests,
6
+ checkExhaustiveTesting, checkLongTests, checkSlowTests, checkFragileTests,
7
+ checkDuplicatedSetupAcrossFiles, checkIndependentSharedState,
8
+ countExportedUnits, guessSourceFilePath, scoreTestFile,
9
+ } from '../../scripts/lib/test-smell-scoring.mjs';
10
+ import { makeMember, makeUnit } from './fixtures.mjs';
11
+
12
+ // ── empty / trivial input ───────────────────────────────────────────────
13
+
14
+ test('scoreTestFile: empty text produces zero test blocks and zero findings, never throws', () => {
15
+ const r = scoreTestFile('');
16
+ assert.equal(r.testBlockCount, 0);
17
+ assert.deepEqual(r.findings, []);
18
+ });
19
+
20
+ test('scoreTestFile: a single-line file with no test() calls produces zero findings', () => {
21
+ const r = scoreTestFile('const x = 1;');
22
+ assert.equal(r.testBlockCount, 0);
23
+ assert.deepEqual(r.findings, []);
24
+ });
25
+
26
+ test('findTestBlocks: a minimal single passing test is found with correct title/kind', () => {
27
+ const blocks = findTestBlocks(`test('does a thing', () => { assert.ok(true); });`);
28
+ assert.equal(blocks.length, 1);
29
+ assert.equal(blocks[0].kind, 'test');
30
+ assert.equal(blocks[0].title, 'does a thing');
31
+ assert.equal(blocks[0].skipped, false);
32
+ });
33
+
34
+ // ── T1 — Insufficient Tests ────────────────────────────────────────────
35
+
36
+ test('T1: fewer test() blocks than exported units fires', () => {
37
+ const blocks = findTestBlocks(`test('a', () => {});`);
38
+ const r = checkInsufficientTests(blocks, 3);
39
+ assert.ok(r);
40
+ assert.equal(r.ruleId, 'T1');
41
+ });
42
+
43
+ test('T1: exportedUnitCount null is unmeasured, returns null (not a finding, not silence-as-clean)', () => {
44
+ const blocks = findTestBlocks(`test('a', () => {});`);
45
+ assert.equal(checkInsufficientTests(blocks, null), null);
46
+ });
47
+
48
+ test('T1: exportedUnitCount 0 (nothing exported) does not fire', () => {
49
+ const blocks = findTestBlocks(`test('a', () => {});`);
50
+ assert.equal(checkInsufficientTests(blocks, 0), null);
51
+ });
52
+
53
+ test('T1: boundary — test count equal to exported count does not fire', () => {
54
+ const blocks = findTestBlocks(`test('a', () => {}); test('b', () => {});`);
55
+ assert.equal(checkInsufficientTests(blocks, 2), null);
56
+ });
57
+
58
+ test('T1: boundary — one fewer test than exported count fires', () => {
59
+ const blocks = findTestBlocks(`test('a', () => {}); test('b', () => {});`);
60
+ assert.ok(checkInsufficientTests(blocks, 3));
61
+ });
62
+
63
+ test('countExportedUnits: sums isPublic members across units, null for empty/absent', () => {
64
+ assert.equal(countExportedUnits([]), null);
65
+ assert.equal(countExportedUnits(null), null);
66
+ const units = [makeUnit({ members: [makeMember({ isPublic: true }), makeMember({ isPublic: false })] })];
67
+ assert.equal(countExportedUnits(units), 1);
68
+ });
69
+
70
+ test('guessSourceFilePath: maps a .test.mjs path back to its source path', () => {
71
+ assert.equal(guessSourceFilePath('foo.test.mjs'), 'foo.mjs');
72
+ // the test-dir swap only fires when the segment is bracketed by separators
73
+ // on both sides (`/test/`), not merely a leading path segment
74
+ assert.equal(guessSourceFilePath('project/test/x.test.ts'), 'project/src/x.ts');
75
+ });
76
+
77
+ // ── T2 — Ignored Tests ─────────────────────────────────────────────────
78
+
79
+ test('T2: it.skip fires', () => {
80
+ const text = `it.skip('broken', () => {});`;
81
+ const blocks = findTestBlocks(text);
82
+ const findings = checkIgnoredTests(text, blocks);
83
+ assert.equal(findings.length, 1);
84
+ assert.equal(findings[0].ruleId, 'T2');
85
+ });
86
+
87
+ test('T2: test.skip fires', () => {
88
+ const text = `test.skip('broken', () => {});`;
89
+ const blocks = findTestBlocks(text);
90
+ assert.equal(checkIgnoredTests(text, blocks).length, 1);
91
+ });
92
+
93
+ test('T2: xdescribe fires (whole-suite disable)', () => {
94
+ const text = `xdescribe('a suite', () => {});`;
95
+ assert.equal(checkIgnoredTests(text, []).length, 1);
96
+ });
97
+
98
+ test('T2: an un-skipped test does not fire', () => {
99
+ const text = `it('works', () => { assert.ok(true); });`;
100
+ const blocks = findTestBlocks(text);
101
+ assert.deepEqual(checkIgnoredTests(text, blocks), []);
102
+ });
103
+
104
+ // ── T5 — Exhaustive Testing (>10 assertion calls per block) ──────────────
105
+
106
+ test('T5: 11 assertion calls in one test fires', () => {
107
+ const asserts = Array.from({ length: 11 }, (_, i) => `expect(${i}).toBe(${i});`).join('\n');
108
+ const text = `test('exhaustive', () => {\n${asserts}\n});`;
109
+ const blocks = findTestBlocks(text);
110
+ assert.equal(checkExhaustiveTesting(blocks).length, 1);
111
+ });
112
+
113
+ test('T5: boundary — exactly 10 assertion calls does not fire', () => {
114
+ const asserts = Array.from({ length: 10 }, (_, i) => `expect(${i}).toBe(${i});`).join('\n');
115
+ const text = `test('ok', () => {\n${asserts}\n});`;
116
+ const blocks = findTestBlocks(text);
117
+ assert.deepEqual(checkExhaustiveTesting(blocks), []);
118
+ });
119
+
120
+ test('T5: an expression-bodied test (no block, body: null) is not measured, not flagged', () => {
121
+ const text = `it('x', () => expect(f()).toBe(1));`;
122
+ const blocks = findTestBlocks(text);
123
+ assert.equal(blocks[0].body, null);
124
+ assert.deepEqual(checkExhaustiveTesting(blocks), []);
125
+ });
126
+
127
+ // ── T6 — Long Tests (>30 lines) ────────────────────────────────────────
128
+
129
+ test('T6: boundary — 29 content lines produces exactly 31 total lines and fires', () => {
130
+ const body = Array.from({ length: 29 }, (_, i) => `line${i}();`).join('\n');
131
+ const text = `test('long', () => {\n${body}\n});`;
132
+ const blocks = findTestBlocks(text);
133
+ assert.equal(blocks[0].body.text.split('\n').length, 31);
134
+ assert.equal(checkLongTests(blocks).length, 1);
135
+ });
136
+
137
+ test('T6: boundary — exactly 30 lines does not fire', () => {
138
+ // extractCallbackBody's body.text spans from just after the opening '{' to
139
+ // just before the closing '}', i.e. `\n<content>\n` — splitting on '\n'
140
+ // yields 2 extra (empty) lines beyond the content lines, so 28 content
141
+ // lines here produces exactly 30 total lines, the documented threshold.
142
+ const body = Array.from({ length: 28 }, (_, i) => `line${i}();`).join('\n');
143
+ const text = `test('ok', () => {\n${body}\n});`;
144
+ const blocks = findTestBlocks(text);
145
+ assert.equal(blocks[0].body.text.split('\n').length, 30);
146
+ assert.deepEqual(checkLongTests(blocks), []);
147
+ });
148
+
149
+ // ── T7 — Slow Tests (literal timer calls) ─────────────────────────────
150
+
151
+ test('T7: a literal setTimeout inside a test fires', () => {
152
+ const text = `test('slow', () => { setTimeout(() => {}, 100); });`;
153
+ const blocks = findTestBlocks(text);
154
+ const findings = checkSlowTests(blocks);
155
+ assert.equal(findings.length, 1);
156
+ assert.equal(findings[0].ruleId, 'T7');
157
+ });
158
+
159
+ test('T7: sleep()/delay() calls also fire', () => {
160
+ const text = `test('slow', () => { sleep(10); });`;
161
+ const blocks = findTestBlocks(text);
162
+ assert.equal(checkSlowTests(blocks).length, 1);
163
+ });
164
+
165
+ test('T7: a test with no timer calls does not fire', () => {
166
+ const text = `test('fast', () => { assert.ok(true); });`;
167
+ const blocks = findTestBlocks(text);
168
+ assert.deepEqual(checkSlowTests(blocks), []);
169
+ });
170
+
171
+ // ── T8 — Fragile Tests (non-deterministic references) ─────────────────
172
+
173
+ test('T8: Date.now() fires', () => {
174
+ const text = `test('fragile', () => { const t = Date.now(); assert.ok(t); });`;
175
+ const blocks = findTestBlocks(text);
176
+ assert.equal(checkFragileTests(blocks).length, 1);
177
+ });
178
+
179
+ test('T8: Math.random() fires', () => {
180
+ const text = `test('fragile', () => { const r = Math.random(); assert.ok(r); });`;
181
+ const blocks = findTestBlocks(text);
182
+ assert.equal(checkFragileTests(blocks).length, 1);
183
+ });
184
+
185
+ test('T8: process.env.* fires', () => {
186
+ const text = `test('fragile', () => { assert.ok(process.env.FOO); });`;
187
+ const blocks = findTestBlocks(text);
188
+ assert.equal(checkFragileTests(blocks).length, 1);
189
+ });
190
+
191
+ test('T8: a deterministic test (fixed clock injected) does not fire', () => {
192
+ const text = `test('deterministic', () => { const t = fixedClock.now(); assert.ok(t); });`;
193
+ const blocks = findTestBlocks(text);
194
+ assert.deepEqual(checkFragileTests(blocks), []);
195
+ });
196
+
197
+ // ── T9 — Duplicated Setup (cross-file, structural similarity) ────────────
198
+
199
+ test('T9: two near-identical beforeEach blocks in DIFFERENT files fire above the similarity threshold', () => {
200
+ const setupA = `beforeEach(() => { const harness = new Harness('/tmp/a'); harness.start(); harness.seed(42); });`;
201
+ const setupB = `beforeEach(() => { const harness = new Harness('/tmp/b'); harness.start(); harness.seed(99); });`;
202
+ const files = [
203
+ { file: 'a.test.mjs', setupBlocks: findSetupBlocks(setupA) },
204
+ { file: 'b.test.mjs', setupBlocks: findSetupBlocks(setupB) },
205
+ ];
206
+ const findings = checkDuplicatedSetupAcrossFiles(files);
207
+ assert.equal(findings.length, 1);
208
+ assert.equal(findings[0].ruleId, 'T9');
209
+ });
210
+
211
+ test('T9: identical setup blocks in the SAME file do not fire (T9 is cross-file only)', () => {
212
+ const text = `
213
+ beforeEach(() => { const harness = new Harness('/tmp/a'); harness.start(); harness.seed(1); });
214
+ beforeEach(() => { const harness = new Harness('/tmp/a'); harness.start(); harness.seed(1); });
215
+ `;
216
+ const files = [{ file: 'a.test.mjs', setupBlocks: findSetupBlocks(text) }];
217
+ assert.deepEqual(checkDuplicatedSetupAcrossFiles(files), []);
218
+ });
219
+
220
+ test('T9: structurally unrelated setup blocks across files do not fire', () => {
221
+ const files = [
222
+ { file: 'a.test.mjs', setupBlocks: findSetupBlocks(`beforeEach(() => { db.connect(); db.migrate(); db.seedUsers(); });`) },
223
+ { file: 'b.test.mjs', setupBlocks: findSetupBlocks(`beforeEach(() => { server.listen(); server.mockAuth(); server.warmCache(); });`) },
224
+ ];
225
+ assert.deepEqual(checkDuplicatedSetupAcrossFiles(files), []);
226
+ });
227
+
228
+ // ── FIRST-Independent — mutated shared top-level state ────────────────
229
+
230
+ test('FIRST-Independent: a top-level let mutated inside 2+ test() blocks fires', () => {
231
+ const text = `
232
+ let counter;
233
+ test('a', () => { counter = 1; });
234
+ test('b', () => { counter = 2; });
235
+ `;
236
+ const blocks = findTestBlocks(text);
237
+ const findings = checkIndependentSharedState(text, blocks);
238
+ assert.equal(findings.length, 1);
239
+ assert.equal(findings[0].ruleId, 'FIRST-Independent');
240
+ });
241
+
242
+ test('FIRST-Independent: a top-level let read (never mutated) by multiple tests does not fire', () => {
243
+ const text = `
244
+ let config = { a: 1 };
245
+ test('a', () => { assert.ok(config.a); });
246
+ test('b', () => { assert.ok(config.a); });
247
+ `;
248
+ const blocks = findTestBlocks(text);
249
+ assert.deepEqual(checkIndependentSharedState(text, blocks), []);
250
+ });
251
+
252
+ test('FIRST-Independent: a let mutated inside only ONE test does not fire', () => {
253
+ const text = `
254
+ let counter;
255
+ test('a', () => { counter = 1; });
256
+ `;
257
+ const blocks = findTestBlocks(text);
258
+ assert.deepEqual(checkIndependentSharedState(text, blocks), []);
259
+ });
260
+
261
+ test('FIRST-Independent: a let declared and mutated entirely INSIDE a single test is local, not shared state', () => {
262
+ const text = `
263
+ test('a', () => { let counter = 1; counter = 2; });
264
+ `;
265
+ const blocks = findTestBlocks(text);
266
+ assert.deepEqual(checkIndependentSharedState(text, blocks), []);
267
+ });
268
+
269
+ // ── scoreTestFile — full per-file aggregate ────────────────────────────
270
+
271
+ test('scoreTestFile: a file tripping several single-file smells sums findings correctly', () => {
272
+ const text = `
273
+ it.skip('broken', () => {});
274
+ test('fragile', () => { const t = Date.now(); assert.ok(t); });
275
+ `;
276
+ const r = scoreTestFile(text, { exportedUnitCount: null });
277
+ assert.equal(r.testBlockCount, 2);
278
+ assert.equal(r.skippedCount, 1);
279
+ const ruleIds = r.findings.map((f) => f.ruleId).sort();
280
+ assert.deepEqual(ruleIds, ['T2', 'T8']);
281
+ });