@aiready/testability 0.6.22 → 0.6.23
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 +24 -23
- package/.turbo/turbo-test.log +20 -18
- package/coverage/analyzer.ts.html +54 -165
- package/coverage/clover.xml +38 -53
- package/coverage/coverage-final.json +3 -3
- package/coverage/index.html +30 -30
- package/coverage/provider.ts.html +29 -59
- package/coverage/scoring.ts.html +24 -135
- package/dist/chunk-CE46EON2.mjs +240 -0
- package/dist/chunk-CISO2RDG.mjs +198 -0
- package/dist/chunk-KYBNMI4K.mjs +237 -0
- package/dist/cli.js +33 -76
- package/dist/cli.mjs +9 -9
- package/dist/index.js +23 -66
- package/dist/index.mjs +1 -1
- package/package.json +2 -2
- package/src/__tests__/scoring.test.ts +31 -0
- package/src/analyzer.ts +25 -78
- package/src/cli.ts +8 -8
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
// src/analyzer.ts
|
|
2
|
+
import {
|
|
3
|
+
scanFiles,
|
|
4
|
+
calculateTestabilityIndex,
|
|
5
|
+
Severity,
|
|
6
|
+
IssueType,
|
|
7
|
+
runBatchAnalysis,
|
|
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
|
+
await runBatchAnalysis(
|
|
126
|
+
sourceFiles,
|
|
127
|
+
"analyzing files",
|
|
128
|
+
"testability",
|
|
129
|
+
options.onProgress,
|
|
130
|
+
async (f) => ({ filePath: f, analysis: await analyzeFileTestability(f) }),
|
|
131
|
+
(result) => {
|
|
132
|
+
const a = result.analysis;
|
|
133
|
+
for (const key of Object.keys(aggregated)) {
|
|
134
|
+
aggregated[key] += a[key];
|
|
135
|
+
}
|
|
136
|
+
fileDetails.push({
|
|
137
|
+
filePath: result.filePath,
|
|
138
|
+
pureFunctions: a.pureFunctions,
|
|
139
|
+
totalFunctions: a.totalFunctions
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
);
|
|
143
|
+
const hasTestFramework = detectTestFramework(options.rootDir);
|
|
144
|
+
const indexResult = calculateTestabilityIndex({
|
|
145
|
+
testFiles: testFiles.length,
|
|
146
|
+
sourceFiles: sourceFiles.length,
|
|
147
|
+
pureFunctions: aggregated.pureFunctions,
|
|
148
|
+
totalFunctions: Math.max(1, aggregated.totalFunctions),
|
|
149
|
+
injectionPatterns: aggregated.injectionPatterns,
|
|
150
|
+
totalClasses: Math.max(1, aggregated.totalClasses),
|
|
151
|
+
bloatedInterfaces: aggregated.bloatedInterfaces,
|
|
152
|
+
totalInterfaces: Math.max(1, aggregated.totalInterfaces),
|
|
153
|
+
externalStateMutations: aggregated.externalStateMutations,
|
|
154
|
+
hasTestFramework,
|
|
155
|
+
fileDetails
|
|
156
|
+
});
|
|
157
|
+
const issues = [];
|
|
158
|
+
const minCoverage = options.minCoverageRatio ?? 0.3;
|
|
159
|
+
const actualRatio = sourceFiles.length > 0 ? testFiles.length / sourceFiles.length : 0;
|
|
160
|
+
if (!hasTestFramework) {
|
|
161
|
+
issues.push({
|
|
162
|
+
type: IssueType.LowTestability,
|
|
163
|
+
dimension: "framework",
|
|
164
|
+
severity: Severity.Critical,
|
|
165
|
+
message: "No major testing framework detected \u2014 AI changes cannot be safely verified.",
|
|
166
|
+
location: { file: options.rootDir, line: 0 },
|
|
167
|
+
suggestion: "Add a testing framework (e.g., Jest, Pytest, JUnit) to enable automated verification."
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
if (actualRatio < minCoverage) {
|
|
171
|
+
const needed = Math.ceil(sourceFiles.length * minCoverage) - testFiles.length;
|
|
172
|
+
issues.push({
|
|
173
|
+
type: IssueType.LowTestability,
|
|
174
|
+
dimension: "test-coverage",
|
|
175
|
+
severity: actualRatio === 0 ? Severity.Critical : Severity.Major,
|
|
176
|
+
message: `Test ratio is ${Math.round(actualRatio * 100)}% (${testFiles.length} test files for ${sourceFiles.length} source files). Need at least ${Math.round(minCoverage * 100)}%.`,
|
|
177
|
+
location: { file: options.rootDir, line: 0 },
|
|
178
|
+
suggestion: `Add ~${needed} test file(s) to reach the ${Math.round(minCoverage * 100)}% minimum for safe AI assistance.`
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
if (indexResult.dimensions.purityScore < 50) {
|
|
182
|
+
issues.push({
|
|
183
|
+
type: IssueType.LowTestability,
|
|
184
|
+
dimension: "purity",
|
|
185
|
+
severity: Severity.Major,
|
|
186
|
+
message: `Only ${indexResult.dimensions.purityScore}% of functions appear pure \u2014 side-effectful code is harder for AI to verify safely.`,
|
|
187
|
+
location: { file: options.rootDir, line: 0 },
|
|
188
|
+
suggestion: "Refactor complex side-effectful logic into pure functions where possible."
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
return {
|
|
192
|
+
summary: {
|
|
193
|
+
sourceFiles: sourceFiles.length,
|
|
194
|
+
testFiles: testFiles.length,
|
|
195
|
+
coverageRatio: Math.round(actualRatio * 100) / 100,
|
|
196
|
+
score: indexResult.score,
|
|
197
|
+
rating: indexResult.rating,
|
|
198
|
+
aiChangeSafetyRating: indexResult.aiChangeSafetyRating,
|
|
199
|
+
dimensions: indexResult.dimensions
|
|
200
|
+
},
|
|
201
|
+
issues,
|
|
202
|
+
rawData: {
|
|
203
|
+
sourceFiles: sourceFiles.length,
|
|
204
|
+
testFiles: testFiles.length,
|
|
205
|
+
...aggregated,
|
|
206
|
+
hasTestFramework
|
|
207
|
+
},
|
|
208
|
+
recommendations: indexResult.recommendations
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// src/scoring.ts
|
|
213
|
+
import { ToolName, buildStandardToolScore } from "@aiready/core";
|
|
214
|
+
function calculateTestabilityScore(report) {
|
|
215
|
+
const { summary, rawData, recommendations } = report;
|
|
216
|
+
return buildStandardToolScore({
|
|
217
|
+
toolName: ToolName.TestabilityIndex,
|
|
218
|
+
score: summary.score,
|
|
219
|
+
rawData,
|
|
220
|
+
dimensions: summary.dimensions,
|
|
221
|
+
dimensionNames: {
|
|
222
|
+
testCoverageRatio: "Test Coverage",
|
|
223
|
+
purityScore: "Function Purity",
|
|
224
|
+
dependencyInjectionScore: "Dependency Injection",
|
|
225
|
+
interfaceFocusScore: "Interface Focus",
|
|
226
|
+
observabilityScore: "Observability"
|
|
227
|
+
},
|
|
228
|
+
recommendations,
|
|
229
|
+
recommendationImpact: summary.aiChangeSafetyRating === "blind-risk" ? 15 : 8,
|
|
230
|
+
rating: summary.aiChangeSafetyRating || summary.rating
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export {
|
|
235
|
+
analyzeTestability,
|
|
236
|
+
calculateTestabilityScore
|
|
237
|
+
};
|
package/dist/cli.js
CHANGED
|
@@ -29,7 +29,6 @@ var import_commander = require("commander");
|
|
|
29
29
|
// src/analyzer.ts
|
|
30
30
|
var import_core = require("@aiready/core");
|
|
31
31
|
var import_fs = require("fs");
|
|
32
|
-
var import_path = require("path");
|
|
33
32
|
async function analyzeFileTestability(filePath) {
|
|
34
33
|
const result = {
|
|
35
34
|
pureFunctions: 0,
|
|
@@ -80,49 +79,6 @@ async function analyzeFileTestability(filePath) {
|
|
|
80
79
|
}
|
|
81
80
|
return result;
|
|
82
81
|
}
|
|
83
|
-
function detectTestFramework(rootDir) {
|
|
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
|
-
}
|
|
106
|
-
}
|
|
107
|
-
return false;
|
|
108
|
-
}
|
|
109
|
-
var TEST_PATTERNS = [
|
|
110
|
-
/\.(test|spec)\.(ts|tsx|js|jsx)$/,
|
|
111
|
-
/_test\.go$/,
|
|
112
|
-
/test_.*\.py$/,
|
|
113
|
-
/.*_test\.py$/,
|
|
114
|
-
/.*Test\.java$/,
|
|
115
|
-
/.*Tests\.cs$/,
|
|
116
|
-
/__tests__\//,
|
|
117
|
-
/\/tests?\//,
|
|
118
|
-
/\/e2e\//,
|
|
119
|
-
/\/fixtures\//
|
|
120
|
-
];
|
|
121
|
-
function isTestFile(filePath, extra) {
|
|
122
|
-
if (TEST_PATTERNS.some((p) => p.test(filePath))) return true;
|
|
123
|
-
if (extra) return extra.some((p) => filePath.includes(p));
|
|
124
|
-
return false;
|
|
125
|
-
}
|
|
126
82
|
async function analyzeTestability(options) {
|
|
127
83
|
const allFiles = await (0, import_core.scanFiles)({
|
|
128
84
|
...options,
|
|
@@ -130,9 +86,9 @@ async function analyzeTestability(options) {
|
|
|
130
86
|
includeTests: true
|
|
131
87
|
});
|
|
132
88
|
const sourceFiles = allFiles.filter(
|
|
133
|
-
(f) => !isTestFile(f, options.testPatterns)
|
|
89
|
+
(f) => !(0, import_core.isTestFile)(f, options.testPatterns)
|
|
134
90
|
);
|
|
135
|
-
const testFiles = allFiles.filter((f) => isTestFile(f, options.testPatterns));
|
|
91
|
+
const testFiles = allFiles.filter((f) => (0, import_core.isTestFile)(f, options.testPatterns));
|
|
136
92
|
const aggregated = {
|
|
137
93
|
pureFunctions: 0,
|
|
138
94
|
totalFunctions: 0,
|
|
@@ -143,27 +99,28 @@ async function analyzeTestability(options) {
|
|
|
143
99
|
externalStateMutations: 0
|
|
144
100
|
};
|
|
145
101
|
const fileDetails = [];
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
"testability",
|
|
153
|
-
"analyzing files",
|
|
154
|
-
options.onProgress
|
|
155
|
-
);
|
|
156
|
-
const a = await analyzeFileTestability(f);
|
|
157
|
-
for (const key of Object.keys(aggregated)) {
|
|
158
|
-
aggregated[key] += a[key];
|
|
159
|
-
}
|
|
160
|
-
fileDetails.push({
|
|
102
|
+
await (0, import_core.runBatchAnalysis)(
|
|
103
|
+
sourceFiles,
|
|
104
|
+
"analyzing files",
|
|
105
|
+
"testability",
|
|
106
|
+
options.onProgress,
|
|
107
|
+
async (f) => ({
|
|
161
108
|
filePath: f,
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
109
|
+
analysis: await analyzeFileTestability(f)
|
|
110
|
+
}),
|
|
111
|
+
(result) => {
|
|
112
|
+
const a = result.analysis;
|
|
113
|
+
for (const key of Object.keys(aggregated)) {
|
|
114
|
+
aggregated[key] += a[key];
|
|
115
|
+
}
|
|
116
|
+
fileDetails.push({
|
|
117
|
+
filePath: result.filePath,
|
|
118
|
+
pureFunctions: a.pureFunctions,
|
|
119
|
+
totalFunctions: a.totalFunctions
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
);
|
|
123
|
+
const hasTestFramework = (0, import_core.detectTestFramework)(options.rootDir);
|
|
167
124
|
const indexResult = (0, import_core.calculateTestabilityIndex)({
|
|
168
125
|
testFiles: testFiles.length,
|
|
169
126
|
sourceFiles: sourceFiles.length,
|
|
@@ -257,7 +214,7 @@ function calculateTestabilityScore(report) {
|
|
|
257
214
|
// src/cli.ts
|
|
258
215
|
var import_chalk = __toESM(require("chalk"));
|
|
259
216
|
var import_fs2 = require("fs");
|
|
260
|
-
var
|
|
217
|
+
var import_path = require("path");
|
|
261
218
|
var import_core3 = require("@aiready/core");
|
|
262
219
|
var program = new import_commander.Command();
|
|
263
220
|
var startTime = Date.now();
|
|
@@ -311,35 +268,35 @@ EXAMPLES:
|
|
|
311
268
|
`testability-report-${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}.json`,
|
|
312
269
|
directory
|
|
313
270
|
);
|
|
314
|
-
const dir = (0,
|
|
271
|
+
const dir = (0, import_path.dirname)(outputPath);
|
|
315
272
|
if (!(0, import_fs2.existsSync)(dir)) (0, import_fs2.mkdirSync)(dir, { recursive: true });
|
|
316
273
|
(0, import_fs2.writeFileSync)(outputPath, JSON.stringify(payload, null, 2));
|
|
317
274
|
console.log(import_chalk.default.green(`\u2713 Report saved to ${outputPath}`));
|
|
318
275
|
} else {
|
|
319
276
|
(0, import_core3.displayStandardConsoleReport)({
|
|
320
277
|
title: "\u{1F9EA} Testability Analysis",
|
|
321
|
-
score: scoring.
|
|
322
|
-
rating: scoring.summary.rating,
|
|
278
|
+
score: scoring.score,
|
|
279
|
+
rating: scoring.rating || report.summary.rating,
|
|
323
280
|
dimensions: [
|
|
324
281
|
{
|
|
325
282
|
name: "Test Coverage",
|
|
326
|
-
value:
|
|
283
|
+
value: report.summary.dimensions.testCoverageRatio
|
|
327
284
|
},
|
|
328
285
|
{
|
|
329
286
|
name: "Function Purity",
|
|
330
|
-
value:
|
|
287
|
+
value: report.summary.dimensions.purityScore
|
|
331
288
|
},
|
|
332
289
|
{
|
|
333
290
|
name: "Dependency Injection",
|
|
334
|
-
value:
|
|
291
|
+
value: report.summary.dimensions.dependencyInjectionScore
|
|
335
292
|
},
|
|
336
293
|
{
|
|
337
294
|
name: "Interface Focus",
|
|
338
|
-
value:
|
|
295
|
+
value: report.summary.dimensions.interfaceFocusScore
|
|
339
296
|
},
|
|
340
297
|
{
|
|
341
298
|
name: "Observability",
|
|
342
|
-
value:
|
|
299
|
+
value: report.summary.dimensions.observabilityScore
|
|
343
300
|
}
|
|
344
301
|
],
|
|
345
302
|
stats: [
|
|
@@ -347,7 +304,7 @@ EXAMPLES:
|
|
|
347
304
|
{ label: "Test Files", value: report.rawData.testFiles },
|
|
348
305
|
{
|
|
349
306
|
label: "Coverage Ratio",
|
|
350
|
-
value: Math.round(
|
|
307
|
+
value: Math.round(report.summary.coverageRatio * 100) + "%"
|
|
351
308
|
}
|
|
352
309
|
],
|
|
353
310
|
issues: report.issues,
|
package/dist/cli.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import {
|
|
3
3
|
analyzeTestability,
|
|
4
4
|
calculateTestabilityScore
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-CISO2RDG.mjs";
|
|
6
6
|
|
|
7
7
|
// src/cli.ts
|
|
8
8
|
import { Command } from "commander";
|
|
@@ -73,28 +73,28 @@ EXAMPLES:
|
|
|
73
73
|
} else {
|
|
74
74
|
displayStandardConsoleReport({
|
|
75
75
|
title: "\u{1F9EA} Testability Analysis",
|
|
76
|
-
score: scoring.
|
|
77
|
-
rating: scoring.summary.rating,
|
|
76
|
+
score: scoring.score,
|
|
77
|
+
rating: scoring.rating || report.summary.rating,
|
|
78
78
|
dimensions: [
|
|
79
79
|
{
|
|
80
80
|
name: "Test Coverage",
|
|
81
|
-
value:
|
|
81
|
+
value: report.summary.dimensions.testCoverageRatio
|
|
82
82
|
},
|
|
83
83
|
{
|
|
84
84
|
name: "Function Purity",
|
|
85
|
-
value:
|
|
85
|
+
value: report.summary.dimensions.purityScore
|
|
86
86
|
},
|
|
87
87
|
{
|
|
88
88
|
name: "Dependency Injection",
|
|
89
|
-
value:
|
|
89
|
+
value: report.summary.dimensions.dependencyInjectionScore
|
|
90
90
|
},
|
|
91
91
|
{
|
|
92
92
|
name: "Interface Focus",
|
|
93
|
-
value:
|
|
93
|
+
value: report.summary.dimensions.interfaceFocusScore
|
|
94
94
|
},
|
|
95
95
|
{
|
|
96
96
|
name: "Observability",
|
|
97
|
-
value:
|
|
97
|
+
value: report.summary.dimensions.observabilityScore
|
|
98
98
|
}
|
|
99
99
|
],
|
|
100
100
|
stats: [
|
|
@@ -102,7 +102,7 @@ EXAMPLES:
|
|
|
102
102
|
{ label: "Test Files", value: report.rawData.testFiles },
|
|
103
103
|
{
|
|
104
104
|
label: "Coverage Ratio",
|
|
105
|
-
value: Math.round(
|
|
105
|
+
value: Math.round(report.summary.coverageRatio * 100) + "%"
|
|
106
106
|
}
|
|
107
107
|
],
|
|
108
108
|
issues: report.issues,
|
package/dist/index.js
CHANGED
|
@@ -33,7 +33,6 @@ var import_core3 = require("@aiready/core");
|
|
|
33
33
|
// src/analyzer.ts
|
|
34
34
|
var import_core = require("@aiready/core");
|
|
35
35
|
var import_fs = require("fs");
|
|
36
|
-
var import_path = require("path");
|
|
37
36
|
async function analyzeFileTestability(filePath) {
|
|
38
37
|
const result = {
|
|
39
38
|
pureFunctions: 0,
|
|
@@ -84,49 +83,6 @@ async function analyzeFileTestability(filePath) {
|
|
|
84
83
|
}
|
|
85
84
|
return result;
|
|
86
85
|
}
|
|
87
|
-
function detectTestFramework(rootDir) {
|
|
88
|
-
const manifests = [
|
|
89
|
-
{
|
|
90
|
-
file: "package.json",
|
|
91
|
-
deps: ["jest", "vitest", "mocha", "mocha", "jasmine", "ava", "tap"]
|
|
92
|
-
},
|
|
93
|
-
{ file: "requirements.txt", deps: ["pytest", "unittest", "nose"] },
|
|
94
|
-
{ file: "pyproject.toml", deps: ["pytest"] },
|
|
95
|
-
{ file: "pom.xml", deps: ["junit", "testng"] },
|
|
96
|
-
{ file: "build.gradle", deps: ["junit", "testng"] },
|
|
97
|
-
{ file: "go.mod", deps: ["testing"] }
|
|
98
|
-
// go testing is built-in
|
|
99
|
-
];
|
|
100
|
-
for (const m of manifests) {
|
|
101
|
-
const p = (0, import_path.join)(rootDir, m.file);
|
|
102
|
-
if ((0, import_fs.existsSync)(p)) {
|
|
103
|
-
if (m.file === "go.mod") return true;
|
|
104
|
-
try {
|
|
105
|
-
const content = (0, import_fs.readFileSync)(p, "utf-8");
|
|
106
|
-
if (m.deps.some((d) => content.includes(d))) return true;
|
|
107
|
-
} catch {
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
return false;
|
|
112
|
-
}
|
|
113
|
-
var TEST_PATTERNS = [
|
|
114
|
-
/\.(test|spec)\.(ts|tsx|js|jsx)$/,
|
|
115
|
-
/_test\.go$/,
|
|
116
|
-
/test_.*\.py$/,
|
|
117
|
-
/.*_test\.py$/,
|
|
118
|
-
/.*Test\.java$/,
|
|
119
|
-
/.*Tests\.cs$/,
|
|
120
|
-
/__tests__\//,
|
|
121
|
-
/\/tests?\//,
|
|
122
|
-
/\/e2e\//,
|
|
123
|
-
/\/fixtures\//
|
|
124
|
-
];
|
|
125
|
-
function isTestFile(filePath, extra) {
|
|
126
|
-
if (TEST_PATTERNS.some((p) => p.test(filePath))) return true;
|
|
127
|
-
if (extra) return extra.some((p) => filePath.includes(p));
|
|
128
|
-
return false;
|
|
129
|
-
}
|
|
130
86
|
async function analyzeTestability(options) {
|
|
131
87
|
const allFiles = await (0, import_core.scanFiles)({
|
|
132
88
|
...options,
|
|
@@ -134,9 +90,9 @@ async function analyzeTestability(options) {
|
|
|
134
90
|
includeTests: true
|
|
135
91
|
});
|
|
136
92
|
const sourceFiles = allFiles.filter(
|
|
137
|
-
(f) => !isTestFile(f, options.testPatterns)
|
|
93
|
+
(f) => !(0, import_core.isTestFile)(f, options.testPatterns)
|
|
138
94
|
);
|
|
139
|
-
const testFiles = allFiles.filter((f) => isTestFile(f, options.testPatterns));
|
|
95
|
+
const testFiles = allFiles.filter((f) => (0, import_core.isTestFile)(f, options.testPatterns));
|
|
140
96
|
const aggregated = {
|
|
141
97
|
pureFunctions: 0,
|
|
142
98
|
totalFunctions: 0,
|
|
@@ -147,27 +103,28 @@ async function analyzeTestability(options) {
|
|
|
147
103
|
externalStateMutations: 0
|
|
148
104
|
};
|
|
149
105
|
const fileDetails = [];
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
"testability",
|
|
157
|
-
"analyzing files",
|
|
158
|
-
options.onProgress
|
|
159
|
-
);
|
|
160
|
-
const a = await analyzeFileTestability(f);
|
|
161
|
-
for (const key of Object.keys(aggregated)) {
|
|
162
|
-
aggregated[key] += a[key];
|
|
163
|
-
}
|
|
164
|
-
fileDetails.push({
|
|
106
|
+
await (0, import_core.runBatchAnalysis)(
|
|
107
|
+
sourceFiles,
|
|
108
|
+
"analyzing files",
|
|
109
|
+
"testability",
|
|
110
|
+
options.onProgress,
|
|
111
|
+
async (f) => ({
|
|
165
112
|
filePath: f,
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
113
|
+
analysis: await analyzeFileTestability(f)
|
|
114
|
+
}),
|
|
115
|
+
(result) => {
|
|
116
|
+
const a = result.analysis;
|
|
117
|
+
for (const key of Object.keys(aggregated)) {
|
|
118
|
+
aggregated[key] += a[key];
|
|
119
|
+
}
|
|
120
|
+
fileDetails.push({
|
|
121
|
+
filePath: result.filePath,
|
|
122
|
+
pureFunctions: a.pureFunctions,
|
|
123
|
+
totalFunctions: a.totalFunctions
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
);
|
|
127
|
+
const hasTestFramework = (0, import_core.detectTestFramework)(options.rootDir);
|
|
171
128
|
const indexResult = (0, import_core.calculateTestabilityIndex)({
|
|
172
129
|
testFiles: testFiles.length,
|
|
173
130
|
sourceFiles: sourceFiles.length,
|
package/dist/index.mjs
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aiready/testability",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.23",
|
|
4
4
|
"description": "Measures how safely and verifiably AI-generated changes can be made to your codebase",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"chalk": "^5.3.0",
|
|
41
41
|
"commander": "^14.0.0",
|
|
42
42
|
"glob": "^13.0.0",
|
|
43
|
-
"@aiready/core": "0.23.
|
|
43
|
+
"@aiready/core": "0.23.24"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"@types/node": "^24.0.0",
|
|
@@ -78,4 +78,35 @@ describe('Testability Scoring', () => {
|
|
|
78
78
|
expect(scoring.recommendations[0].estimatedImpact).toBe(15);
|
|
79
79
|
expect(scoring.recommendations[0].priority).toBe('high');
|
|
80
80
|
});
|
|
81
|
+
|
|
82
|
+
it('should return ToolScoringOutput with score at top level (not summary.score)', () => {
|
|
83
|
+
const scoring = calculateTestabilityScore(mockReport);
|
|
84
|
+
|
|
85
|
+
// The bug was that CLI tried to access scoring.summary.score
|
|
86
|
+
// but ToolScoringOutput has score at the top level
|
|
87
|
+
expect(scoring.score).toBeDefined();
|
|
88
|
+
expect(typeof scoring.score).toBe('number');
|
|
89
|
+
expect(scoring.score).toBe(75);
|
|
90
|
+
|
|
91
|
+
// Verify there is no .summary property (which would cause the CLI crash)
|
|
92
|
+
expect((scoring as any).summary).toBeUndefined();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should return rating at top level (not summary.rating)', () => {
|
|
96
|
+
const scoring = calculateTestabilityScore(mockReport);
|
|
97
|
+
|
|
98
|
+
// The CLI accesses scoring.rating || report.summary.rating
|
|
99
|
+
// ToolScoringOutput may not have rating, but it should not have summary
|
|
100
|
+
expect((scoring as any).summary).toBeUndefined();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('should have rawMetrics with testability data', () => {
|
|
104
|
+
const scoring = calculateTestabilityScore(mockReport);
|
|
105
|
+
|
|
106
|
+
expect(scoring.rawMetrics).toBeDefined();
|
|
107
|
+
expect(scoring.rawMetrics).toHaveProperty('sourceFiles');
|
|
108
|
+
expect(scoring.rawMetrics).toHaveProperty('testFiles');
|
|
109
|
+
expect(scoring.rawMetrics).toHaveProperty('pureFunctions');
|
|
110
|
+
expect(scoring.rawMetrics).toHaveProperty('totalFunctions');
|
|
111
|
+
});
|
|
81
112
|
});
|