@aiready/testability 0.4.16 → 0.4.18
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 +10 -10
- package/.turbo/turbo-test.log +4 -4
- package/README.md +6 -0
- package/dist/chunk-P53QYYIW.mjs +258 -0
- package/dist/chunk-QOIBI5E7.mjs +262 -0
- package/dist/chunk-RQRAKTO6.mjs +251 -0
- package/dist/chunk-U2VGBMYR.mjs +259 -0
- package/dist/chunk-UT24GZ66.mjs +254 -0
- package/dist/chunk-VDC6AKUD.mjs +257 -0
- package/dist/chunk-VNU3SQOC.mjs +265 -0
- package/dist/cli.js +63 -144
- package/dist/cli.mjs +1 -1
- package/dist/index.d.mts +0 -12
- package/dist/index.d.ts +0 -12
- package/dist/index.js +63 -144
- package/dist/index.mjs +1 -1
- package/package.json +2 -2
- package/src/analyzer.ts +70 -216
|
@@ -0,0 +1,265 @@
|
|
|
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.name === "BloatedService") {
|
|
35
|
+
console.log(`[DEBUG] Found BloatedService: type=${exp.type}, methods=${exp.methodCount}, props=${exp.propertyCount}`);
|
|
36
|
+
}
|
|
37
|
+
if (exp.type === "function") {
|
|
38
|
+
result.totalFunctions++;
|
|
39
|
+
if (exp.isPure) result.pureFunctions++;
|
|
40
|
+
if (exp.hasSideEffects) result.externalStateMutations++;
|
|
41
|
+
}
|
|
42
|
+
if (exp.type === "class") {
|
|
43
|
+
result.totalClasses++;
|
|
44
|
+
if (exp.parameters && exp.parameters.length > 0) {
|
|
45
|
+
result.injectionPatterns++;
|
|
46
|
+
}
|
|
47
|
+
const total = (exp.methodCount || 0) + (exp.propertyCount || 0);
|
|
48
|
+
if (total > 5) {
|
|
49
|
+
result.bloatedInterfaces++;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (exp.type === "interface") {
|
|
53
|
+
result.totalInterfaces++;
|
|
54
|
+
const total = (exp.methodCount || 0) + (exp.propertyCount || 0);
|
|
55
|
+
if (total > 5) {
|
|
56
|
+
result.bloatedInterfaces++;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
} catch (error) {
|
|
61
|
+
console.warn(`Testability: Failed to parse ${filePath}: ${error}`);
|
|
62
|
+
}
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
function detectTestFramework(rootDir) {
|
|
66
|
+
const manifests = [
|
|
67
|
+
{
|
|
68
|
+
file: "package.json",
|
|
69
|
+
deps: ["jest", "vitest", "mocha", "mocha", "jasmine", "ava", "tap"]
|
|
70
|
+
},
|
|
71
|
+
{ file: "requirements.txt", deps: ["pytest", "unittest", "nose"] },
|
|
72
|
+
{ file: "pyproject.toml", deps: ["pytest"] },
|
|
73
|
+
{ file: "pom.xml", deps: ["junit", "testng"] },
|
|
74
|
+
{ file: "build.gradle", deps: ["junit", "testng"] },
|
|
75
|
+
{ file: "go.mod", deps: ["testing"] }
|
|
76
|
+
// go testing is built-in
|
|
77
|
+
];
|
|
78
|
+
for (const m of manifests) {
|
|
79
|
+
const p = join(rootDir, m.file);
|
|
80
|
+
if (existsSync(p)) {
|
|
81
|
+
if (m.file === "go.mod") return true;
|
|
82
|
+
try {
|
|
83
|
+
const content = readFileSync(p, "utf-8");
|
|
84
|
+
if (m.deps.some((d) => content.includes(d))) return true;
|
|
85
|
+
} catch {
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
var TEST_PATTERNS = [
|
|
92
|
+
/\.(test|spec)\.(ts|tsx|js|jsx)$/,
|
|
93
|
+
/_test\.go$/,
|
|
94
|
+
/test_.*\.py$/,
|
|
95
|
+
/.*_test\.py$/,
|
|
96
|
+
/.*Test\.java$/,
|
|
97
|
+
/.*Tests\.cs$/,
|
|
98
|
+
/__tests__\//,
|
|
99
|
+
/\/tests?\//,
|
|
100
|
+
/\/e2e\//,
|
|
101
|
+
/\/fixtures\//
|
|
102
|
+
];
|
|
103
|
+
function isTestFile(filePath, extra) {
|
|
104
|
+
if (TEST_PATTERNS.some((p) => p.test(filePath))) return true;
|
|
105
|
+
if (extra) return extra.some((p) => filePath.includes(p));
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
async function analyzeTestability(options) {
|
|
109
|
+
const allFiles = await scanFiles({
|
|
110
|
+
...options,
|
|
111
|
+
include: options.include || ["**/*.{ts,tsx,js,jsx,py,java,cs,go}"],
|
|
112
|
+
includeTests: true
|
|
113
|
+
});
|
|
114
|
+
const sourceFiles = allFiles.filter(
|
|
115
|
+
(f) => !isTestFile(f, options.testPatterns)
|
|
116
|
+
);
|
|
117
|
+
const testFiles = allFiles.filter((f) => isTestFile(f, options.testPatterns));
|
|
118
|
+
const aggregated = {
|
|
119
|
+
pureFunctions: 0,
|
|
120
|
+
totalFunctions: 0,
|
|
121
|
+
injectionPatterns: 0,
|
|
122
|
+
totalClasses: 0,
|
|
123
|
+
bloatedInterfaces: 0,
|
|
124
|
+
totalInterfaces: 0,
|
|
125
|
+
externalStateMutations: 0
|
|
126
|
+
};
|
|
127
|
+
let processed = 0;
|
|
128
|
+
for (const f of sourceFiles) {
|
|
129
|
+
processed++;
|
|
130
|
+
emitProgress(
|
|
131
|
+
processed,
|
|
132
|
+
sourceFiles.length,
|
|
133
|
+
"testability",
|
|
134
|
+
"analyzing files",
|
|
135
|
+
options.onProgress
|
|
136
|
+
);
|
|
137
|
+
const a = await analyzeFileTestability(f);
|
|
138
|
+
for (const key of Object.keys(aggregated)) {
|
|
139
|
+
aggregated[key] += a[key];
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
const hasTestFramework = detectTestFramework(options.rootDir);
|
|
143
|
+
const indexResult = calculateTestabilityIndex({
|
|
144
|
+
testFiles: testFiles.length,
|
|
145
|
+
sourceFiles: sourceFiles.length,
|
|
146
|
+
pureFunctions: aggregated.pureFunctions,
|
|
147
|
+
totalFunctions: Math.max(1, aggregated.totalFunctions),
|
|
148
|
+
injectionPatterns: aggregated.injectionPatterns,
|
|
149
|
+
totalClasses: Math.max(1, aggregated.totalClasses),
|
|
150
|
+
bloatedInterfaces: aggregated.bloatedInterfaces,
|
|
151
|
+
totalInterfaces: Math.max(1, aggregated.totalInterfaces),
|
|
152
|
+
externalStateMutations: aggregated.externalStateMutations,
|
|
153
|
+
hasTestFramework
|
|
154
|
+
});
|
|
155
|
+
const issues = [];
|
|
156
|
+
const minCoverage = options.minCoverageRatio ?? 0.3;
|
|
157
|
+
const actualRatio = sourceFiles.length > 0 ? testFiles.length / sourceFiles.length : 0;
|
|
158
|
+
if (!hasTestFramework) {
|
|
159
|
+
issues.push({
|
|
160
|
+
type: IssueType.LowTestability,
|
|
161
|
+
dimension: "framework",
|
|
162
|
+
severity: Severity.Critical,
|
|
163
|
+
message: "No major testing framework detected \u2014 AI changes cannot be safely verified.",
|
|
164
|
+
location: { file: options.rootDir, line: 0 },
|
|
165
|
+
suggestion: "Add a testing framework (e.g., Jest, Pytest, JUnit) to enable automated verification."
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
if (actualRatio < minCoverage) {
|
|
169
|
+
const needed = Math.ceil(sourceFiles.length * minCoverage) - testFiles.length;
|
|
170
|
+
issues.push({
|
|
171
|
+
type: IssueType.LowTestability,
|
|
172
|
+
dimension: "test-coverage",
|
|
173
|
+
severity: actualRatio === 0 ? Severity.Critical : Severity.Major,
|
|
174
|
+
message: `Test ratio is ${Math.round(actualRatio * 100)}% (${testFiles.length} test files for ${sourceFiles.length} source files). Need at least ${Math.round(minCoverage * 100)}%.`,
|
|
175
|
+
location: { file: options.rootDir, line: 0 },
|
|
176
|
+
suggestion: `Add ~${needed} test file(s) to reach the ${Math.round(minCoverage * 100)}% minimum for safe AI assistance.`
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
if (indexResult.dimensions.purityScore < 50) {
|
|
180
|
+
issues.push({
|
|
181
|
+
type: IssueType.LowTestability,
|
|
182
|
+
dimension: "purity",
|
|
183
|
+
severity: Severity.Major,
|
|
184
|
+
message: `Only ${indexResult.dimensions.purityScore}% of functions appear pure \u2014 side-effectful code is harder for AI to verify safely.`,
|
|
185
|
+
location: { file: options.rootDir, line: 0 },
|
|
186
|
+
suggestion: "Refactor complex side-effectful logic into pure functions where possible."
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
return {
|
|
190
|
+
summary: {
|
|
191
|
+
sourceFiles: sourceFiles.length,
|
|
192
|
+
testFiles: testFiles.length,
|
|
193
|
+
coverageRatio: Math.round(actualRatio * 100) / 100,
|
|
194
|
+
score: indexResult.score,
|
|
195
|
+
rating: indexResult.rating,
|
|
196
|
+
aiChangeSafetyRating: indexResult.aiChangeSafetyRating,
|
|
197
|
+
dimensions: indexResult.dimensions
|
|
198
|
+
},
|
|
199
|
+
issues,
|
|
200
|
+
rawData: {
|
|
201
|
+
sourceFiles: sourceFiles.length,
|
|
202
|
+
testFiles: testFiles.length,
|
|
203
|
+
...aggregated,
|
|
204
|
+
hasTestFramework
|
|
205
|
+
},
|
|
206
|
+
recommendations: indexResult.recommendations
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// src/scoring.ts
|
|
211
|
+
import { ToolName } from "@aiready/core";
|
|
212
|
+
function calculateTestabilityScore(report) {
|
|
213
|
+
const { summary, rawData, recommendations } = report;
|
|
214
|
+
const factors = [
|
|
215
|
+
{
|
|
216
|
+
name: "Test Coverage",
|
|
217
|
+
impact: Math.round(summary.dimensions.testCoverageRatio - 50),
|
|
218
|
+
description: `${rawData.testFiles} test files / ${rawData.sourceFiles} source files (${Math.round(summary.coverageRatio * 100)}%)`
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
name: "Function Purity",
|
|
222
|
+
impact: Math.round(summary.dimensions.purityScore - 50),
|
|
223
|
+
description: `${rawData.pureFunctions}/${rawData.totalFunctions} functions are pure`
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
name: "Dependency Injection",
|
|
227
|
+
impact: Math.round(summary.dimensions.dependencyInjectionScore - 50),
|
|
228
|
+
description: `${rawData.injectionPatterns}/${rawData.totalClasses} classes use DI`
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
name: "Interface Focus",
|
|
232
|
+
impact: Math.round(summary.dimensions.interfaceFocusScore - 50),
|
|
233
|
+
description: `${rawData.bloatedInterfaces} interfaces have >10 methods`
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
name: "Observability",
|
|
237
|
+
impact: Math.round(summary.dimensions.observabilityScore - 50),
|
|
238
|
+
description: `${rawData.externalStateMutations} functions mutate external state`
|
|
239
|
+
}
|
|
240
|
+
];
|
|
241
|
+
const recs = recommendations.map(
|
|
242
|
+
(action) => ({
|
|
243
|
+
action,
|
|
244
|
+
estimatedImpact: summary.aiChangeSafetyRating === "blind-risk" ? 15 : 8,
|
|
245
|
+
priority: summary.aiChangeSafetyRating === "blind-risk" || summary.aiChangeSafetyRating === "high-risk" ? "high" : "medium"
|
|
246
|
+
})
|
|
247
|
+
);
|
|
248
|
+
return {
|
|
249
|
+
toolName: ToolName.TestabilityIndex,
|
|
250
|
+
score: summary.score,
|
|
251
|
+
rawMetrics: {
|
|
252
|
+
...rawData,
|
|
253
|
+
rating: summary.rating,
|
|
254
|
+
aiChangeSafetyRating: summary.aiChangeSafetyRating,
|
|
255
|
+
coverageRatio: summary.coverageRatio
|
|
256
|
+
},
|
|
257
|
+
factors,
|
|
258
|
+
recommendations: recs
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export {
|
|
263
|
+
analyzeTestability,
|
|
264
|
+
calculateTestabilityScore
|
|
265
|
+
};
|
package/dist/cli.js
CHANGED
|
@@ -30,82 +30,7 @@ var import_commander = require("commander");
|
|
|
30
30
|
var import_core = require("@aiready/core");
|
|
31
31
|
var import_fs = require("fs");
|
|
32
32
|
var import_path = require("path");
|
|
33
|
-
|
|
34
|
-
function countMethodsInInterface(node) {
|
|
35
|
-
if (node.type === "TSInterfaceDeclaration") {
|
|
36
|
-
return node.body.body.filter(
|
|
37
|
-
(m) => m.type === "TSMethodSignature" || m.type === "TSPropertySignature"
|
|
38
|
-
).length;
|
|
39
|
-
}
|
|
40
|
-
if (node.type === "TSTypeAliasDeclaration" && node.typeAnnotation.type === "TSTypeLiteral") {
|
|
41
|
-
return node.typeAnnotation.members.length;
|
|
42
|
-
}
|
|
43
|
-
return 0;
|
|
44
|
-
}
|
|
45
|
-
function hasDependencyInjection(node) {
|
|
46
|
-
for (const member of node.body.body) {
|
|
47
|
-
if (member.type === "MethodDefinition" && member.key.type === "Identifier" && member.key.name === "constructor") {
|
|
48
|
-
const fn = member.value;
|
|
49
|
-
if (fn.params && fn.params.length > 0) {
|
|
50
|
-
const typedParams = fn.params.filter((p) => {
|
|
51
|
-
const param = p;
|
|
52
|
-
return param.typeAnnotation != null || param.parameter?.typeAnnotation != null;
|
|
53
|
-
});
|
|
54
|
-
if (typedParams.length > 0) return true;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
return false;
|
|
59
|
-
}
|
|
60
|
-
function isPureFunction(fn) {
|
|
61
|
-
let hasReturn = false;
|
|
62
|
-
let hasSideEffect = false;
|
|
63
|
-
function walk(node) {
|
|
64
|
-
if (node.type === "ReturnStatement" && node.argument) hasReturn = true;
|
|
65
|
-
if (node.type === "AssignmentExpression" && node.left.type === "MemberExpression")
|
|
66
|
-
hasSideEffect = true;
|
|
67
|
-
if (node.type === "CallExpression" && node.callee.type === "MemberExpression" && node.callee.object.type === "Identifier" && ["console", "process", "window", "document", "fs"].includes(
|
|
68
|
-
node.callee.object.name
|
|
69
|
-
))
|
|
70
|
-
hasSideEffect = true;
|
|
71
|
-
for (const key of Object.keys(node)) {
|
|
72
|
-
if (key === "parent") continue;
|
|
73
|
-
const child = node[key];
|
|
74
|
-
if (child && typeof child === "object") {
|
|
75
|
-
if (Array.isArray(child)) {
|
|
76
|
-
child.forEach((c) => c?.type && walk(c));
|
|
77
|
-
} else if (child.type) {
|
|
78
|
-
walk(child);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
if (fn.body?.type === "BlockStatement") {
|
|
84
|
-
fn.body.body.forEach((s) => walk(s));
|
|
85
|
-
} else if (fn.body) {
|
|
86
|
-
hasReturn = true;
|
|
87
|
-
}
|
|
88
|
-
return hasReturn && !hasSideEffect;
|
|
89
|
-
}
|
|
90
|
-
function hasExternalStateMutation(fn) {
|
|
91
|
-
let found = false;
|
|
92
|
-
function walk(node) {
|
|
93
|
-
if (found) return;
|
|
94
|
-
if (node.type === "AssignmentExpression" && node.left.type === "MemberExpression")
|
|
95
|
-
found = true;
|
|
96
|
-
for (const key of Object.keys(node)) {
|
|
97
|
-
if (key === "parent") continue;
|
|
98
|
-
const child = node[key];
|
|
99
|
-
if (child && typeof child === "object") {
|
|
100
|
-
if (Array.isArray(child)) child.forEach((c) => c?.type && walk(c));
|
|
101
|
-
else if (child.type) walk(child);
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
if (fn.body?.type === "BlockStatement") fn.body.body.forEach((s) => walk(s));
|
|
106
|
-
return found;
|
|
107
|
-
}
|
|
108
|
-
function analyzeFileTestability(filePath) {
|
|
33
|
+
async function analyzeFileTestability(filePath) {
|
|
109
34
|
const result = {
|
|
110
35
|
pureFunctions: 0,
|
|
111
36
|
totalFunctions: 0,
|
|
@@ -115,75 +40,79 @@ function analyzeFileTestability(filePath) {
|
|
|
115
40
|
totalInterfaces: 0,
|
|
116
41
|
externalStateMutations: 0
|
|
117
42
|
};
|
|
43
|
+
const parser = (0, import_core.getParser)(filePath);
|
|
44
|
+
if (!parser) return result;
|
|
118
45
|
let code;
|
|
119
46
|
try {
|
|
120
47
|
code = (0, import_fs.readFileSync)(filePath, "utf-8");
|
|
121
48
|
} catch {
|
|
122
49
|
return result;
|
|
123
50
|
}
|
|
124
|
-
let ast;
|
|
125
51
|
try {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
if (key === "parent") continue;
|
|
151
|
-
const child = node[key];
|
|
152
|
-
if (child && typeof child === "object") {
|
|
153
|
-
if (Array.isArray(child)) child.forEach((c) => c?.type && visit(c));
|
|
154
|
-
else if (child.type) visit(child);
|
|
52
|
+
await parser.initialize();
|
|
53
|
+
const parseResult = parser.parse(code, filePath);
|
|
54
|
+
for (const exp of parseResult.exports) {
|
|
55
|
+
if (exp.type === "function") {
|
|
56
|
+
result.totalFunctions++;
|
|
57
|
+
if (exp.isPure) result.pureFunctions++;
|
|
58
|
+
if (exp.hasSideEffects) result.externalStateMutations++;
|
|
59
|
+
}
|
|
60
|
+
if (exp.type === "class") {
|
|
61
|
+
result.totalClasses++;
|
|
62
|
+
if (exp.parameters && exp.parameters.length > 0) {
|
|
63
|
+
result.injectionPatterns++;
|
|
64
|
+
}
|
|
65
|
+
const total = (exp.methodCount || 0) + (exp.propertyCount || 0);
|
|
66
|
+
if (total > 5) {
|
|
67
|
+
result.bloatedInterfaces++;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (exp.type === "interface") {
|
|
71
|
+
result.totalInterfaces++;
|
|
72
|
+
const total = (exp.methodCount || 0) + (exp.propertyCount || 0);
|
|
73
|
+
if (total > 5) {
|
|
74
|
+
result.bloatedInterfaces++;
|
|
75
|
+
}
|
|
155
76
|
}
|
|
156
77
|
}
|
|
78
|
+
} catch (error) {
|
|
79
|
+
console.warn(`Testability: Failed to parse ${filePath}: ${error}`);
|
|
157
80
|
}
|
|
158
|
-
ast.body.forEach(visit);
|
|
159
81
|
return result;
|
|
160
82
|
}
|
|
161
83
|
function detectTestFramework(rootDir) {
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
"
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
84
|
+
const manifests = [
|
|
85
|
+
{
|
|
86
|
+
file: "package.json",
|
|
87
|
+
deps: ["jest", "vitest", "mocha", "mocha", "jasmine", "ava", "tap"]
|
|
88
|
+
},
|
|
89
|
+
{ file: "requirements.txt", deps: ["pytest", "unittest", "nose"] },
|
|
90
|
+
{ file: "pyproject.toml", deps: ["pytest"] },
|
|
91
|
+
{ file: "pom.xml", deps: ["junit", "testng"] },
|
|
92
|
+
{ file: "build.gradle", deps: ["junit", "testng"] },
|
|
93
|
+
{ file: "go.mod", deps: ["testing"] }
|
|
94
|
+
// go testing is built-in
|
|
95
|
+
];
|
|
96
|
+
for (const m of manifests) {
|
|
97
|
+
const p = (0, import_path.join)(rootDir, m.file);
|
|
98
|
+
if ((0, import_fs.existsSync)(p)) {
|
|
99
|
+
if (m.file === "go.mod") return true;
|
|
100
|
+
try {
|
|
101
|
+
const content = (0, import_fs.readFileSync)(p, "utf-8");
|
|
102
|
+
if (m.deps.some((d) => content.includes(d))) return true;
|
|
103
|
+
} catch {
|
|
104
|
+
}
|
|
105
|
+
}
|
|
183
106
|
}
|
|
107
|
+
return false;
|
|
184
108
|
}
|
|
185
109
|
var TEST_PATTERNS = [
|
|
186
110
|
/\.(test|spec)\.(ts|tsx|js|jsx)$/,
|
|
111
|
+
/_test\.go$/,
|
|
112
|
+
/test_.*\.py$/,
|
|
113
|
+
/.*_test\.py$/,
|
|
114
|
+
/.*Test\.java$/,
|
|
115
|
+
/.*Tests\.cs$/,
|
|
187
116
|
/__tests__\//,
|
|
188
117
|
/\/tests?\//,
|
|
189
118
|
/\/e2e\//,
|
|
@@ -197,7 +126,7 @@ function isTestFile(filePath, extra) {
|
|
|
197
126
|
async function analyzeTestability(options) {
|
|
198
127
|
const allFiles = await (0, import_core.scanFiles)({
|
|
199
128
|
...options,
|
|
200
|
-
include: options.include || ["**/*.{ts,tsx,js,jsx}"],
|
|
129
|
+
include: options.include || ["**/*.{ts,tsx,js,jsx,py,java,cs,go}"],
|
|
201
130
|
includeTests: true
|
|
202
131
|
});
|
|
203
132
|
const sourceFiles = allFiles.filter(
|
|
@@ -223,7 +152,7 @@ async function analyzeTestability(options) {
|
|
|
223
152
|
"analyzing files",
|
|
224
153
|
options.onProgress
|
|
225
154
|
);
|
|
226
|
-
const a = analyzeFileTestability(f);
|
|
155
|
+
const a = await analyzeFileTestability(f);
|
|
227
156
|
for (const key of Object.keys(aggregated)) {
|
|
228
157
|
aggregated[key] += a[key];
|
|
229
158
|
}
|
|
@@ -249,9 +178,9 @@ async function analyzeTestability(options) {
|
|
|
249
178
|
type: import_core.IssueType.LowTestability,
|
|
250
179
|
dimension: "framework",
|
|
251
180
|
severity: import_core.Severity.Critical,
|
|
252
|
-
message: "No testing framework detected
|
|
181
|
+
message: "No major testing framework detected \u2014 AI changes cannot be safely verified.",
|
|
253
182
|
location: { file: options.rootDir, line: 0 },
|
|
254
|
-
suggestion: "Add Jest,
|
|
183
|
+
suggestion: "Add a testing framework (e.g., Jest, Pytest, JUnit) to enable automated verification."
|
|
255
184
|
});
|
|
256
185
|
}
|
|
257
186
|
if (actualRatio < minCoverage) {
|
|
@@ -270,19 +199,9 @@ async function analyzeTestability(options) {
|
|
|
270
199
|
type: import_core.IssueType.LowTestability,
|
|
271
200
|
dimension: "purity",
|
|
272
201
|
severity: import_core.Severity.Major,
|
|
273
|
-
message: `Only ${indexResult.dimensions.purityScore}% of functions
|
|
274
|
-
location: { file: options.rootDir, line: 0 },
|
|
275
|
-
suggestion: "Extract pure transformation logic from I/O and mutation code."
|
|
276
|
-
});
|
|
277
|
-
}
|
|
278
|
-
if (indexResult.dimensions.observabilityScore < 50) {
|
|
279
|
-
issues.push({
|
|
280
|
-
type: import_core.IssueType.LowTestability,
|
|
281
|
-
dimension: "observability",
|
|
282
|
-
severity: import_core.Severity.Major,
|
|
283
|
-
message: `Many functions mutate external state directly \u2014 outputs are invisible to unit tests.`,
|
|
202
|
+
message: `Only ${indexResult.dimensions.purityScore}% of functions appear pure \u2014 side-effectful code is harder for AI to verify safely.`,
|
|
284
203
|
location: { file: options.rootDir, line: 0 },
|
|
285
|
-
suggestion: "
|
|
204
|
+
suggestion: "Refactor complex side-effectful logic into pure functions where possible."
|
|
286
205
|
});
|
|
287
206
|
}
|
|
288
207
|
return {
|
package/dist/cli.mjs
CHANGED
package/dist/index.d.mts
CHANGED
|
@@ -59,18 +59,6 @@ interface TestabilityReport {
|
|
|
59
59
|
recommendations: string[];
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
/**
|
|
63
|
-
* Testability analyzer.
|
|
64
|
-
*
|
|
65
|
-
* Walks the codebase and measures 5 structural dimensions that determine
|
|
66
|
-
* whether AI-generated changes can be safely verified:
|
|
67
|
-
* 1. Test file coverage ratio
|
|
68
|
-
* 2. Pure function prevalence
|
|
69
|
-
* 3. Dependency injection patterns
|
|
70
|
-
* 4. Interface focus (bloated interface detection)
|
|
71
|
-
* 5. Observability (return values vs. external state mutations)
|
|
72
|
-
*/
|
|
73
|
-
|
|
74
62
|
declare function analyzeTestability(options: TestabilityOptions): Promise<TestabilityReport>;
|
|
75
63
|
|
|
76
64
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -59,18 +59,6 @@ interface TestabilityReport {
|
|
|
59
59
|
recommendations: string[];
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
/**
|
|
63
|
-
* Testability analyzer.
|
|
64
|
-
*
|
|
65
|
-
* Walks the codebase and measures 5 structural dimensions that determine
|
|
66
|
-
* whether AI-generated changes can be safely verified:
|
|
67
|
-
* 1. Test file coverage ratio
|
|
68
|
-
* 2. Pure function prevalence
|
|
69
|
-
* 3. Dependency injection patterns
|
|
70
|
-
* 4. Interface focus (bloated interface detection)
|
|
71
|
-
* 5. Observability (return values vs. external state mutations)
|
|
72
|
-
*/
|
|
73
|
-
|
|
74
62
|
declare function analyzeTestability(options: TestabilityOptions): Promise<TestabilityReport>;
|
|
75
63
|
|
|
76
64
|
/**
|