@aiready/testability 0.6.19 → 0.6.21
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/.turbo/turbo-build.log +23 -24
- package/.turbo/turbo-lint.log +4 -5
- package/.turbo/turbo-test.log +18 -20
- package/dist/chunk-QMDUZA7H.mjs +239 -0
- package/dist/chunk-RBPS3OGD.mjs +238 -0
- package/dist/chunk-YT5DTEQ4.mjs +238 -0
- package/dist/cli.js +57 -118
- package/dist/cli.mjs +46 -80
- package/dist/index.d.mts +2 -5
- package/dist/index.d.ts +2 -5
- package/dist/index.js +14 -44
- package/dist/index.mjs +1 -1
- package/package.json +2 -2
- package/src/__tests__/analyzer.test.ts +15 -26
- package/src/__tests__/scoring.test.ts +1 -1
- package/src/analyzer.ts +1 -1
- package/src/cli.ts +44 -89
- package/src/scoring.ts +16 -56
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,24 +1,23 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
>
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
DTS Build
|
|
20
|
-
DTS
|
|
21
|
-
DTS dist/
|
|
22
|
-
DTS dist/
|
|
23
|
-
DTS dist/
|
|
24
|
-
DTS dist/index.d.mts 2.62 KB
|
|
1
|
+
|
|
2
|
+
> @aiready/testability@0.6.21 build /Users/pengcao/projects/aiready/packages/testability
|
|
3
|
+
> tsup src/index.ts src/cli.ts --format cjs,esm --dts
|
|
4
|
+
|
|
5
|
+
CLI Building entry: src/cli.ts, src/index.ts
|
|
6
|
+
CLI Using tsconfig: tsconfig.json
|
|
7
|
+
CLI tsup v8.5.1
|
|
8
|
+
CLI Target: es2020
|
|
9
|
+
CJS Build start
|
|
10
|
+
ESM Build start
|
|
11
|
+
CJS dist/index.js 9.74 KB
|
|
12
|
+
CJS dist/cli.js 13.00 KB
|
|
13
|
+
CJS ⚡️ Build success in 84ms
|
|
14
|
+
ESM dist/index.mjs 1.17 KB
|
|
15
|
+
ESM dist/cli.mjs 4.33 KB
|
|
16
|
+
ESM dist/chunk-QMDUZA7H.mjs 7.44 KB
|
|
17
|
+
ESM ⚡️ Build success in 84ms
|
|
18
|
+
DTS Build start
|
|
19
|
+
DTS ⚡️ Build success in 9253ms
|
|
20
|
+
DTS dist/cli.d.ts 20.00 B
|
|
21
|
+
DTS dist/index.d.ts 2.42 KB
|
|
22
|
+
DTS dist/cli.d.mts 20.00 B
|
|
23
|
+
DTS dist/index.d.mts 2.42 KB
|
package/.turbo/turbo-lint.log
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
>
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
|
|
2
|
+
> @aiready/testability@0.6.20 lint /Users/pengcao/projects/aiready/packages/testability
|
|
3
|
+
> eslint src
|
|
4
|
+
|
package/.turbo/turbo-test.log
CHANGED
|
@@ -1,20 +1,18 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
>
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
[
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
[32m✓[39m src/__tests__/
|
|
10
|
-
[32m✓[39m src/__tests__/scoring.test.ts [2m([22m[2m3 tests[22m[2m)[22m[32m
|
|
11
|
-
[32m✓[39m src/__tests__/
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
[2m
|
|
16
|
-
[2m
|
|
17
|
-
[2m
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
[?25h
|
|
1
|
+
|
|
2
|
+
> @aiready/testability@0.6.20 test /Users/pengcao/projects/aiready/packages/testability
|
|
3
|
+
> vitest run
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
[1m[46m RUN [49m[22m [36mv4.0.18 [39m[90m/Users/pengcao/projects/aiready/packages/testability[39m
|
|
7
|
+
|
|
8
|
+
[32m✓[39m src/__tests__/types.test.ts [2m([22m[2m4 tests[22m[2m)[22m[32m 11[2mms[22m[39m
|
|
9
|
+
[32m✓[39m src/__tests__/provider.test.ts [2m([22m[2m2 tests[22m[2m)[22m[32m 16[2mms[22m[39m
|
|
10
|
+
[32m✓[39m src/__tests__/scoring.test.ts [2m([22m[2m3 tests[22m[2m)[22m[32m 2[2mms[22m[39m
|
|
11
|
+
[32m✓[39m src/__tests__/analyzer.test.ts [2m([22m[2m5 tests[22m[2m)[22m[33m 2737[2mms[22m[39m
|
|
12
|
+
[33m[2m✓[22m[39m detects test frameworks in multiple languages [33m 2687[2mms[22m[39m
|
|
13
|
+
|
|
14
|
+
[2m Test Files [22m [1m[32m4 passed[39m[22m[90m (4)[39m
|
|
15
|
+
[2m Tests [22m [1m[32m14 passed[39m[22m[90m (14)[39m
|
|
16
|
+
[2m Start at [22m 19:03:34
|
|
17
|
+
[2m Duration [22m 4.00s[2m (transform 923ms, setup 0ms, import 4.03s, tests 2.77s, environment 0ms)[22m
|
|
18
|
+
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
// src/analyzer.ts
|
|
2
|
+
import {
|
|
3
|
+
scanFiles,
|
|
4
|
+
calculateTestabilityIndex,
|
|
5
|
+
Severity,
|
|
6
|
+
IssueType,
|
|
7
|
+
emitProgress,
|
|
8
|
+
getParser
|
|
9
|
+
} from "@aiready/core";
|
|
10
|
+
import { readFileSync, existsSync } from "fs";
|
|
11
|
+
import { join } from "path";
|
|
12
|
+
async function analyzeFileTestability(filePath) {
|
|
13
|
+
const result = {
|
|
14
|
+
pureFunctions: 0,
|
|
15
|
+
totalFunctions: 0,
|
|
16
|
+
injectionPatterns: 0,
|
|
17
|
+
totalClasses: 0,
|
|
18
|
+
bloatedInterfaces: 0,
|
|
19
|
+
totalInterfaces: 0,
|
|
20
|
+
externalStateMutations: 0
|
|
21
|
+
};
|
|
22
|
+
const parser = await getParser(filePath);
|
|
23
|
+
if (!parser) return result;
|
|
24
|
+
let code;
|
|
25
|
+
try {
|
|
26
|
+
code = readFileSync(filePath, "utf-8");
|
|
27
|
+
} catch {
|
|
28
|
+
return result;
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
await parser.initialize();
|
|
32
|
+
const parseResult = parser.parse(code, filePath);
|
|
33
|
+
for (const exp of parseResult.exports) {
|
|
34
|
+
if (exp.type === "function") {
|
|
35
|
+
result.totalFunctions++;
|
|
36
|
+
if (exp.isPure) result.pureFunctions++;
|
|
37
|
+
if (exp.hasSideEffects) result.externalStateMutations++;
|
|
38
|
+
}
|
|
39
|
+
if (exp.type === "class") {
|
|
40
|
+
result.totalClasses++;
|
|
41
|
+
if (exp.parameters && exp.parameters.length > 0) {
|
|
42
|
+
result.injectionPatterns++;
|
|
43
|
+
}
|
|
44
|
+
const total = (exp.methodCount || 0) + (exp.propertyCount || 0);
|
|
45
|
+
if (total > 10) {
|
|
46
|
+
result.bloatedInterfaces++;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (exp.type === "interface") {
|
|
50
|
+
result.totalInterfaces++;
|
|
51
|
+
const total = (exp.methodCount || 0) + (exp.propertyCount || 0);
|
|
52
|
+
if (total > 10) {
|
|
53
|
+
result.bloatedInterfaces++;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
} catch (error) {
|
|
58
|
+
console.warn(`Testability: Failed to parse ${filePath}: ${error}`);
|
|
59
|
+
}
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
function detectTestFramework(rootDir) {
|
|
63
|
+
const manifests = [
|
|
64
|
+
{
|
|
65
|
+
file: "package.json",
|
|
66
|
+
deps: ["jest", "vitest", "mocha", "mocha", "jasmine", "ava", "tap"]
|
|
67
|
+
},
|
|
68
|
+
{ file: "requirements.txt", deps: ["pytest", "unittest", "nose"] },
|
|
69
|
+
{ file: "pyproject.toml", deps: ["pytest"] },
|
|
70
|
+
{ file: "pom.xml", deps: ["junit", "testng"] },
|
|
71
|
+
{ file: "build.gradle", deps: ["junit", "testng"] },
|
|
72
|
+
{ file: "go.mod", deps: ["testing"] }
|
|
73
|
+
// go testing is built-in
|
|
74
|
+
];
|
|
75
|
+
for (const m of manifests) {
|
|
76
|
+
const p = join(rootDir, m.file);
|
|
77
|
+
if (existsSync(p)) {
|
|
78
|
+
if (m.file === "go.mod") return true;
|
|
79
|
+
try {
|
|
80
|
+
const content = readFileSync(p, "utf-8");
|
|
81
|
+
if (m.deps.some((d) => content.includes(d))) return true;
|
|
82
|
+
} catch {
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
var TEST_PATTERNS = [
|
|
89
|
+
/\.(test|spec)\.(ts|tsx|js|jsx)$/,
|
|
90
|
+
/_test\.go$/,
|
|
91
|
+
/test_.*\.py$/,
|
|
92
|
+
/.*_test\.py$/,
|
|
93
|
+
/.*Test\.java$/,
|
|
94
|
+
/.*Tests\.cs$/,
|
|
95
|
+
/__tests__\//,
|
|
96
|
+
/\/tests?\//,
|
|
97
|
+
/\/e2e\//,
|
|
98
|
+
/\/fixtures\//
|
|
99
|
+
];
|
|
100
|
+
function isTestFile(filePath, extra) {
|
|
101
|
+
if (TEST_PATTERNS.some((p) => p.test(filePath))) return true;
|
|
102
|
+
if (extra) return extra.some((p) => filePath.includes(p));
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
async function analyzeTestability(options) {
|
|
106
|
+
const allFiles = await scanFiles({
|
|
107
|
+
...options,
|
|
108
|
+
include: options.include || ["**/*.{ts,tsx,js,jsx,py,java,cs,go}"],
|
|
109
|
+
includeTests: true
|
|
110
|
+
});
|
|
111
|
+
const sourceFiles = allFiles.filter(
|
|
112
|
+
(f) => !isTestFile(f, options.testPatterns)
|
|
113
|
+
);
|
|
114
|
+
const testFiles = allFiles.filter((f) => isTestFile(f, options.testPatterns));
|
|
115
|
+
const aggregated = {
|
|
116
|
+
pureFunctions: 0,
|
|
117
|
+
totalFunctions: 0,
|
|
118
|
+
injectionPatterns: 0,
|
|
119
|
+
totalClasses: 0,
|
|
120
|
+
bloatedInterfaces: 0,
|
|
121
|
+
totalInterfaces: 0,
|
|
122
|
+
externalStateMutations: 0
|
|
123
|
+
};
|
|
124
|
+
const fileDetails = [];
|
|
125
|
+
let processed = 0;
|
|
126
|
+
for (const f of sourceFiles) {
|
|
127
|
+
processed++;
|
|
128
|
+
emitProgress(
|
|
129
|
+
processed,
|
|
130
|
+
sourceFiles.length,
|
|
131
|
+
"testability",
|
|
132
|
+
"analyzing files",
|
|
133
|
+
options.onProgress
|
|
134
|
+
);
|
|
135
|
+
const a = await analyzeFileTestability(f);
|
|
136
|
+
for (const key of Object.keys(aggregated)) {
|
|
137
|
+
aggregated[key] += a[key];
|
|
138
|
+
}
|
|
139
|
+
fileDetails.push({
|
|
140
|
+
filePath: f,
|
|
141
|
+
pureFunctions: a.pureFunctions,
|
|
142
|
+
totalFunctions: a.totalFunctions
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
const hasTestFramework = detectTestFramework(options.rootDir);
|
|
146
|
+
const indexResult = calculateTestabilityIndex({
|
|
147
|
+
testFiles: testFiles.length,
|
|
148
|
+
sourceFiles: sourceFiles.length,
|
|
149
|
+
pureFunctions: aggregated.pureFunctions,
|
|
150
|
+
totalFunctions: Math.max(1, aggregated.totalFunctions),
|
|
151
|
+
injectionPatterns: aggregated.injectionPatterns,
|
|
152
|
+
totalClasses: Math.max(1, aggregated.totalClasses),
|
|
153
|
+
bloatedInterfaces: aggregated.bloatedInterfaces,
|
|
154
|
+
totalInterfaces: Math.max(1, aggregated.totalInterfaces),
|
|
155
|
+
externalStateMutations: aggregated.externalStateMutations,
|
|
156
|
+
hasTestFramework,
|
|
157
|
+
fileDetails
|
|
158
|
+
});
|
|
159
|
+
const issues = [];
|
|
160
|
+
const minCoverage = options.minCoverageRatio ?? 0.3;
|
|
161
|
+
const actualRatio = sourceFiles.length > 0 ? testFiles.length / sourceFiles.length : 0;
|
|
162
|
+
if (!hasTestFramework) {
|
|
163
|
+
issues.push({
|
|
164
|
+
type: IssueType.LowTestability,
|
|
165
|
+
dimension: "framework",
|
|
166
|
+
severity: Severity.Critical,
|
|
167
|
+
message: "No major testing framework detected \u2014 AI changes cannot be safely verified.",
|
|
168
|
+
location: { file: options.rootDir, line: 0 },
|
|
169
|
+
suggestion: "Add a testing framework (e.g., Jest, Pytest, JUnit) to enable automated verification."
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
if (actualRatio < minCoverage) {
|
|
173
|
+
const needed = Math.ceil(sourceFiles.length * minCoverage) - testFiles.length;
|
|
174
|
+
issues.push({
|
|
175
|
+
type: IssueType.LowTestability,
|
|
176
|
+
dimension: "test-coverage",
|
|
177
|
+
severity: actualRatio === 0 ? Severity.Critical : Severity.Major,
|
|
178
|
+
message: `Test ratio is ${Math.round(actualRatio * 100)}% (${testFiles.length} test files for ${sourceFiles.length} source files). Need at least ${Math.round(minCoverage * 100)}%.`,
|
|
179
|
+
location: { file: options.rootDir, line: 0 },
|
|
180
|
+
suggestion: `Add ~${needed} test file(s) to reach the ${Math.round(minCoverage * 100)}% minimum for safe AI assistance.`
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
if (indexResult.dimensions.purityScore < 50) {
|
|
184
|
+
issues.push({
|
|
185
|
+
type: IssueType.LowTestability,
|
|
186
|
+
dimension: "purity",
|
|
187
|
+
severity: Severity.Major,
|
|
188
|
+
message: `Only ${indexResult.dimensions.purityScore}% of functions appear pure \u2014 side-effectful code is harder for AI to verify safely.`,
|
|
189
|
+
location: { file: options.rootDir, line: 0 },
|
|
190
|
+
suggestion: "Refactor complex side-effectful logic into pure functions where possible."
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
return {
|
|
194
|
+
summary: {
|
|
195
|
+
sourceFiles: sourceFiles.length,
|
|
196
|
+
testFiles: testFiles.length,
|
|
197
|
+
coverageRatio: Math.round(actualRatio * 100) / 100,
|
|
198
|
+
score: indexResult.score,
|
|
199
|
+
rating: indexResult.rating,
|
|
200
|
+
aiChangeSafetyRating: indexResult.aiChangeSafetyRating,
|
|
201
|
+
dimensions: indexResult.dimensions
|
|
202
|
+
},
|
|
203
|
+
issues,
|
|
204
|
+
rawData: {
|
|
205
|
+
sourceFiles: sourceFiles.length,
|
|
206
|
+
testFiles: testFiles.length,
|
|
207
|
+
...aggregated,
|
|
208
|
+
hasTestFramework
|
|
209
|
+
},
|
|
210
|
+
recommendations: indexResult.recommendations
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// src/scoring.ts
|
|
215
|
+
import { ToolName, buildStandardToolScore } from "@aiready/core";
|
|
216
|
+
function calculateTestabilityScore(report) {
|
|
217
|
+
const { summary, rawData, recommendations } = report;
|
|
218
|
+
return buildStandardToolScore({
|
|
219
|
+
toolName: ToolName.TestabilityIndex,
|
|
220
|
+
score: summary.score,
|
|
221
|
+
rawData,
|
|
222
|
+
dimensions: summary.dimensions,
|
|
223
|
+
dimensionNames: {
|
|
224
|
+
testCoverageRatio: "Test Coverage",
|
|
225
|
+
purityScore: "Function Purity",
|
|
226
|
+
dependencyInjectionScore: "Dependency Injection",
|
|
227
|
+
interfaceFocusScore: "Interface Focus",
|
|
228
|
+
observabilityScore: "Observability"
|
|
229
|
+
},
|
|
230
|
+
recommendations,
|
|
231
|
+
recommendationImpact: summary.aiChangeSafetyRating === "blind-risk" ? 15 : 8,
|
|
232
|
+
rating: summary.aiChangeSafetyRating || summary.rating
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export {
|
|
237
|
+
analyzeTestability,
|
|
238
|
+
calculateTestabilityScore
|
|
239
|
+
};
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
// src/analyzer.ts
|
|
2
|
+
import {
|
|
3
|
+
scanFiles,
|
|
4
|
+
calculateTestabilityIndex,
|
|
5
|
+
Severity,
|
|
6
|
+
IssueType,
|
|
7
|
+
emitProgress,
|
|
8
|
+
getParser
|
|
9
|
+
} from "@aiready/core";
|
|
10
|
+
import { readFileSync, existsSync } from "fs";
|
|
11
|
+
import { join } from "path";
|
|
12
|
+
async function analyzeFileTestability(filePath) {
|
|
13
|
+
const result = {
|
|
14
|
+
pureFunctions: 0,
|
|
15
|
+
totalFunctions: 0,
|
|
16
|
+
injectionPatterns: 0,
|
|
17
|
+
totalClasses: 0,
|
|
18
|
+
bloatedInterfaces: 0,
|
|
19
|
+
totalInterfaces: 0,
|
|
20
|
+
externalStateMutations: 0
|
|
21
|
+
};
|
|
22
|
+
const parser = getParser(filePath);
|
|
23
|
+
if (!parser) return result;
|
|
24
|
+
let code;
|
|
25
|
+
try {
|
|
26
|
+
code = readFileSync(filePath, "utf-8");
|
|
27
|
+
} catch {
|
|
28
|
+
return result;
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
await parser.initialize();
|
|
32
|
+
const parseResult = parser.parse(code, filePath);
|
|
33
|
+
for (const exp of parseResult.exports) {
|
|
34
|
+
if (exp.type === "function") {
|
|
35
|
+
result.totalFunctions++;
|
|
36
|
+
if (exp.isPure) result.pureFunctions++;
|
|
37
|
+
if (exp.hasSideEffects) result.externalStateMutations++;
|
|
38
|
+
}
|
|
39
|
+
if (exp.type === "class") {
|
|
40
|
+
result.totalClasses++;
|
|
41
|
+
if (exp.parameters && exp.parameters.length > 0) {
|
|
42
|
+
result.injectionPatterns++;
|
|
43
|
+
}
|
|
44
|
+
const total = (exp.methodCount || 0) + (exp.propertyCount || 0);
|
|
45
|
+
if (total > 10) {
|
|
46
|
+
result.bloatedInterfaces++;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (exp.type === "interface") {
|
|
50
|
+
result.totalInterfaces++;
|
|
51
|
+
const total = (exp.methodCount || 0) + (exp.propertyCount || 0);
|
|
52
|
+
if (total > 10) {
|
|
53
|
+
result.bloatedInterfaces++;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
} catch (error) {
|
|
58
|
+
console.warn(`Testability: Failed to parse ${filePath}: ${error}`);
|
|
59
|
+
}
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
function detectTestFramework(rootDir) {
|
|
63
|
+
const manifests = [
|
|
64
|
+
{
|
|
65
|
+
file: "package.json",
|
|
66
|
+
deps: ["jest", "vitest", "mocha", "mocha", "jasmine", "ava", "tap"]
|
|
67
|
+
},
|
|
68
|
+
{ file: "requirements.txt", deps: ["pytest", "unittest", "nose"] },
|
|
69
|
+
{ file: "pyproject.toml", deps: ["pytest"] },
|
|
70
|
+
{ file: "pom.xml", deps: ["junit", "testng"] },
|
|
71
|
+
{ file: "build.gradle", deps: ["junit", "testng"] },
|
|
72
|
+
{ file: "go.mod", deps: ["testing"] }
|
|
73
|
+
// go testing is built-in
|
|
74
|
+
];
|
|
75
|
+
for (const m of manifests) {
|
|
76
|
+
const p = join(rootDir, m.file);
|
|
77
|
+
if (existsSync(p)) {
|
|
78
|
+
if (m.file === "go.mod") return true;
|
|
79
|
+
try {
|
|
80
|
+
const content = readFileSync(p, "utf-8");
|
|
81
|
+
if (m.deps.some((d) => content.includes(d))) return true;
|
|
82
|
+
} catch {
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
var TEST_PATTERNS = [
|
|
89
|
+
/\.(test|spec)\.(ts|tsx|js|jsx)$/,
|
|
90
|
+
/_test\.go$/,
|
|
91
|
+
/test_.*\.py$/,
|
|
92
|
+
/.*_test\.py$/,
|
|
93
|
+
/.*Test\.java$/,
|
|
94
|
+
/.*Tests\.cs$/,
|
|
95
|
+
/__tests__\//,
|
|
96
|
+
/\/tests?\//,
|
|
97
|
+
/\/e2e\//,
|
|
98
|
+
/\/fixtures\//
|
|
99
|
+
];
|
|
100
|
+
function isTestFile(filePath, extra) {
|
|
101
|
+
if (TEST_PATTERNS.some((p) => p.test(filePath))) return true;
|
|
102
|
+
if (extra) return extra.some((p) => filePath.includes(p));
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
async function analyzeTestability(options) {
|
|
106
|
+
const allFiles = await scanFiles({
|
|
107
|
+
...options,
|
|
108
|
+
include: options.include || ["**/*.{ts,tsx,js,jsx,py,java,cs,go}"],
|
|
109
|
+
includeTests: true
|
|
110
|
+
});
|
|
111
|
+
const sourceFiles = allFiles.filter(
|
|
112
|
+
(f) => !isTestFile(f, options.testPatterns)
|
|
113
|
+
);
|
|
114
|
+
const testFiles = allFiles.filter((f) => isTestFile(f, options.testPatterns));
|
|
115
|
+
const aggregated = {
|
|
116
|
+
pureFunctions: 0,
|
|
117
|
+
totalFunctions: 0,
|
|
118
|
+
injectionPatterns: 0,
|
|
119
|
+
totalClasses: 0,
|
|
120
|
+
bloatedInterfaces: 0,
|
|
121
|
+
totalInterfaces: 0,
|
|
122
|
+
externalStateMutations: 0
|
|
123
|
+
};
|
|
124
|
+
const fileDetails = [];
|
|
125
|
+
let processed = 0;
|
|
126
|
+
for (const f of sourceFiles) {
|
|
127
|
+
processed++;
|
|
128
|
+
emitProgress(
|
|
129
|
+
processed,
|
|
130
|
+
sourceFiles.length,
|
|
131
|
+
"testability",
|
|
132
|
+
"analyzing files",
|
|
133
|
+
options.onProgress
|
|
134
|
+
);
|
|
135
|
+
const a = await analyzeFileTestability(f);
|
|
136
|
+
for (const key of Object.keys(aggregated)) {
|
|
137
|
+
aggregated[key] += a[key];
|
|
138
|
+
}
|
|
139
|
+
fileDetails.push({
|
|
140
|
+
filePath: f,
|
|
141
|
+
pureFunctions: a.pureFunctions,
|
|
142
|
+
totalFunctions: a.totalFunctions
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
const hasTestFramework = detectTestFramework(options.rootDir);
|
|
146
|
+
const indexResult = calculateTestabilityIndex({
|
|
147
|
+
testFiles: testFiles.length,
|
|
148
|
+
sourceFiles: sourceFiles.length,
|
|
149
|
+
pureFunctions: aggregated.pureFunctions,
|
|
150
|
+
totalFunctions: Math.max(1, aggregated.totalFunctions),
|
|
151
|
+
injectionPatterns: aggregated.injectionPatterns,
|
|
152
|
+
totalClasses: Math.max(1, aggregated.totalClasses),
|
|
153
|
+
bloatedInterfaces: aggregated.bloatedInterfaces,
|
|
154
|
+
totalInterfaces: Math.max(1, aggregated.totalInterfaces),
|
|
155
|
+
externalStateMutations: aggregated.externalStateMutations,
|
|
156
|
+
hasTestFramework,
|
|
157
|
+
fileDetails
|
|
158
|
+
});
|
|
159
|
+
const issues = [];
|
|
160
|
+
const minCoverage = options.minCoverageRatio ?? 0.3;
|
|
161
|
+
const actualRatio = sourceFiles.length > 0 ? testFiles.length / sourceFiles.length : 0;
|
|
162
|
+
if (!hasTestFramework) {
|
|
163
|
+
issues.push({
|
|
164
|
+
type: IssueType.LowTestability,
|
|
165
|
+
dimension: "framework",
|
|
166
|
+
severity: Severity.Critical,
|
|
167
|
+
message: "No major testing framework detected \u2014 AI changes cannot be safely verified.",
|
|
168
|
+
location: { file: options.rootDir, line: 0 },
|
|
169
|
+
suggestion: "Add a testing framework (e.g., Jest, Pytest, JUnit) to enable automated verification."
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
if (actualRatio < minCoverage) {
|
|
173
|
+
const needed = Math.ceil(sourceFiles.length * minCoverage) - testFiles.length;
|
|
174
|
+
issues.push({
|
|
175
|
+
type: IssueType.LowTestability,
|
|
176
|
+
dimension: "test-coverage",
|
|
177
|
+
severity: actualRatio === 0 ? Severity.Critical : Severity.Major,
|
|
178
|
+
message: `Test ratio is ${Math.round(actualRatio * 100)}% (${testFiles.length} test files for ${sourceFiles.length} source files). Need at least ${Math.round(minCoverage * 100)}%.`,
|
|
179
|
+
location: { file: options.rootDir, line: 0 },
|
|
180
|
+
suggestion: `Add ~${needed} test file(s) to reach the ${Math.round(minCoverage * 100)}% minimum for safe AI assistance.`
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
if (indexResult.dimensions.purityScore < 50) {
|
|
184
|
+
issues.push({
|
|
185
|
+
type: IssueType.LowTestability,
|
|
186
|
+
dimension: "purity",
|
|
187
|
+
severity: Severity.Major,
|
|
188
|
+
message: `Only ${indexResult.dimensions.purityScore}% of functions appear pure \u2014 side-effectful code is harder for AI to verify safely.`,
|
|
189
|
+
location: { file: options.rootDir, line: 0 },
|
|
190
|
+
suggestion: "Refactor complex side-effectful logic into pure functions where possible."
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
return {
|
|
194
|
+
summary: {
|
|
195
|
+
sourceFiles: sourceFiles.length,
|
|
196
|
+
testFiles: testFiles.length,
|
|
197
|
+
coverageRatio: Math.round(actualRatio * 100) / 100,
|
|
198
|
+
score: indexResult.score,
|
|
199
|
+
rating: indexResult.rating,
|
|
200
|
+
aiChangeSafetyRating: indexResult.aiChangeSafetyRating,
|
|
201
|
+
dimensions: indexResult.dimensions
|
|
202
|
+
},
|
|
203
|
+
issues,
|
|
204
|
+
rawData: {
|
|
205
|
+
sourceFiles: sourceFiles.length,
|
|
206
|
+
testFiles: testFiles.length,
|
|
207
|
+
...aggregated,
|
|
208
|
+
hasTestFramework
|
|
209
|
+
},
|
|
210
|
+
recommendations: indexResult.recommendations
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// src/scoring.ts
|
|
215
|
+
import { ToolName, buildStandardToolScore } from "@aiready/core";
|
|
216
|
+
function calculateTestabilityScore(report) {
|
|
217
|
+
const { summary, rawData, recommendations } = report;
|
|
218
|
+
return buildStandardToolScore({
|
|
219
|
+
toolName: ToolName.TestabilityIndex,
|
|
220
|
+
score: summary.score,
|
|
221
|
+
rawData,
|
|
222
|
+
dimensions: summary.dimensions,
|
|
223
|
+
dimensionNames: {
|
|
224
|
+
testCoverageRatio: "Test Coverage",
|
|
225
|
+
purityScore: "Function Purity",
|
|
226
|
+
dependencyInjectionScore: "Dependency Injection",
|
|
227
|
+
interfaceFocusScore: "Interface Focus",
|
|
228
|
+
observabilityScore: "Observability"
|
|
229
|
+
},
|
|
230
|
+
recommendations,
|
|
231
|
+
rating: summary.rating
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export {
|
|
236
|
+
analyzeTestability,
|
|
237
|
+
calculateTestabilityScore
|
|
238
|
+
};
|