@lisa.ai/agent 2.1.1 → 2.1.4
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/dist/commands/coverage.js +13 -4
- package/dist/services/discovery.service.js +48 -0
- package/dist/utils/parser.js +30 -11
- package/package.json +1 -1
|
@@ -130,12 +130,21 @@ async function coverageCommand(command, modelProvider, attempt = 1, maxRetries =
|
|
|
130
130
|
try {
|
|
131
131
|
// Find JSON summary expecting standard output paths
|
|
132
132
|
const coveragePath = path.resolve(process.cwd(), 'coverage/coverage-summary.json');
|
|
133
|
+
let uncoveredFiles = [];
|
|
133
134
|
if (!fs.existsSync(coveragePath)) {
|
|
134
|
-
console.
|
|
135
|
-
|
|
135
|
+
console.log(`\n[Lisa.ai Coverage] No coverage-summary.json found. Initiating Cold-Start Project Crawler...`);
|
|
136
|
+
// [Lisa.ai Phase 8] Cold-Start Discovery
|
|
137
|
+
uncoveredFiles = discovery_service_1.AutoDiscoveryService.findUntestedFiles(process.cwd());
|
|
138
|
+
if (uncoveredFiles.length === 0) {
|
|
139
|
+
console.log(`✅ [Lisa.ai Coverage] Zero-Test scan complete. No untested source files discovered.`);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
console.log(`[Lisa.ai Coverage] Discovered ${uncoveredFiles.length} untested file(s). Seeding first test suite...`);
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
console.log(`[Lisa.ai Coverage] Evaluating summary...`);
|
|
146
|
+
uncoveredFiles = (0, coverage_parser_1.parseCoverageSummary)('coverage/coverage-summary.json');
|
|
136
147
|
}
|
|
137
|
-
console.log(`[Lisa.ai Coverage] Evaluating summary...`);
|
|
138
|
-
const uncoveredFiles = (0, coverage_parser_1.parseCoverageSummary)('coverage/coverage-summary.json');
|
|
139
148
|
if (uncoveredFiles.length === 0) {
|
|
140
149
|
console.log(`✅ [Lisa.ai Coverage] 100% Logic Coverage Verified! No un-covered files remaining.`);
|
|
141
150
|
return; // We exit the loop
|
|
@@ -89,5 +89,53 @@ class AutoDiscoveryService {
|
|
|
89
89
|
}
|
|
90
90
|
return result;
|
|
91
91
|
}
|
|
92
|
+
/**
|
|
93
|
+
* Recursively crawls the project to find source files that lack any corresponding test/spec files.
|
|
94
|
+
*/
|
|
95
|
+
static findUntestedFiles(dir, skipList = []) {
|
|
96
|
+
const untested = [];
|
|
97
|
+
if (!fs.existsSync(dir))
|
|
98
|
+
return untested;
|
|
99
|
+
const files = fs.readdirSync(dir);
|
|
100
|
+
const ignoreDirs = ['node_modules', 'dist', 'build', '.git', '.angular', 'coverage', 'public', 'assets'];
|
|
101
|
+
const ignoreFiles = ['main.ts', 'index.ts', 'app.config.ts', 'app.routes.ts', 'styles.css', 'styles.scss', 'tailwind.config.js', 'eslint.config.js'];
|
|
102
|
+
for (const file of files) {
|
|
103
|
+
const fullPath = path.join(dir, file);
|
|
104
|
+
if (ignoreDirs.includes(file))
|
|
105
|
+
continue;
|
|
106
|
+
let stat;
|
|
107
|
+
try {
|
|
108
|
+
stat = fs.statSync(fullPath);
|
|
109
|
+
}
|
|
110
|
+
catch (e) {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (stat.isDirectory()) {
|
|
114
|
+
untested.push(...this.findUntestedFiles(fullPath, skipList));
|
|
115
|
+
}
|
|
116
|
+
else if (file.match(/\.(ts|tsx|js|jsx|vue)$/) && !file.includes('.spec.') && !file.includes('.test.')) {
|
|
117
|
+
if (ignoreFiles.includes(file))
|
|
118
|
+
continue;
|
|
119
|
+
const ext = path.extname(file);
|
|
120
|
+
const base = file.slice(0, -ext.length);
|
|
121
|
+
const possibleSpecs = [
|
|
122
|
+
path.join(dir, `${base}.spec${ext}`),
|
|
123
|
+
path.join(dir, `${base}.test${ext}`),
|
|
124
|
+
path.join(dir, `${base}.spec.js`),
|
|
125
|
+
path.join(dir, `${base}.test.js`),
|
|
126
|
+
path.join(dir, `${base}.spec.ts`),
|
|
127
|
+
path.join(dir, `${base}.test.ts`)
|
|
128
|
+
];
|
|
129
|
+
const hasTest = possibleSpecs.some(p => fs.existsSync(p));
|
|
130
|
+
if (!hasTest) {
|
|
131
|
+
const relativePath = path.relative(process.cwd(), fullPath);
|
|
132
|
+
if (!skipList.includes(relativePath)) {
|
|
133
|
+
untested.push(relativePath);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return untested;
|
|
139
|
+
}
|
|
92
140
|
}
|
|
93
141
|
exports.AutoDiscoveryService = AutoDiscoveryService;
|
package/dist/utils/parser.js
CHANGED
|
@@ -55,7 +55,18 @@ function extractFilePath(errorLog, skipFiles = [], searchDir = process.cwd()) {
|
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
|
+
// 1.5 Special Pass for Jest Headers (FAIL src/path/to/test.js)
|
|
59
|
+
const jestFailRegex = /FAIL\s+([a-zA-Z0-9_.\-\/\\]+\.(?:ts|tsx|js|jsx|vue))/gi;
|
|
60
|
+
let jestMatch;
|
|
61
|
+
while ((jestMatch = jestFailRegex.exec(cleanLog)) !== null) {
|
|
62
|
+
const foundPath = jestMatch[1];
|
|
63
|
+
const absoluteFoundPath = path.isAbsolute(foundPath) ? foundPath : path.resolve(searchDir, foundPath);
|
|
64
|
+
if (!normalizedSkips.includes(absoluteFoundPath) && fs.existsSync(absoluteFoundPath)) {
|
|
65
|
+
return foundPath;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
58
68
|
// 2. Second Pass (Fallback): Find anything that looks like a source file
|
|
69
|
+
// Loosened regex: Doesn't require trailing colon/paren
|
|
59
70
|
const fallbackRegex = /([a-zA-Z]:[a-zA-Z0-9_.\-\/\\]+\.(?:ts|tsx|js|jsx|vue)|[a-zA-Z0-9_.\-\/\\]+\.(?:ts|tsx|js|jsx|vue))/g;
|
|
60
71
|
let fallbackMatch;
|
|
61
72
|
while ((fallbackMatch = fallbackRegex.exec(cleanLog)) !== null) {
|
|
@@ -73,13 +84,14 @@ function extractFilePath(errorLog, skipFiles = [], searchDir = process.cwd()) {
|
|
|
73
84
|
let symbolMatch;
|
|
74
85
|
const searchedSymbols = new Set();
|
|
75
86
|
// Filter out common JS/Browser built-ins and framework generics that are not user files
|
|
76
|
-
const ignoreList = ['Error', 'TypeError', 'SyntaxError', 'NullInjectorError', 'Object', 'Boolean', 'String', 'Number', 'Array', 'Chrome', 'Windows', 'Linux', 'Macintosh', 'UserContext', 'TestBed', 'Module', 'Unexpected', 'Expected', 'ChromeHeadless', 'Users', 'AppData', 'Local', 'Temp', 'Process', 'Component'];
|
|
87
|
+
const ignoreList = ['Error', 'TypeError', 'SyntaxError', 'ReferenceError', 'RangeError', 'NullInjectorError', 'Object', 'Boolean', 'String', 'Number', 'Array', 'Chrome', 'Windows', 'Linux', 'Macintosh', 'UserContext', 'TestBed', 'Module', 'Unexpected', 'Expected', 'ChromeHeadless', 'Users', 'AppData', 'Local', 'Temp', 'Process', 'Component', 'Validation', 'Directory', 'Configuration', 'Documentation'];
|
|
77
88
|
while ((symbolMatch = symbolRegex.exec(cleanLog)) !== null) {
|
|
78
89
|
const symbolName = symbolMatch[1];
|
|
79
90
|
if (symbolName && !ignoreList.includes(symbolName) && !searchedSymbols.has(symbolName)) {
|
|
80
91
|
searchedSymbols.add(symbolName);
|
|
81
|
-
console.log(`[Lisa.ai Parser] Discovered abstract symbol failure: ${symbolName}. Scanning
|
|
82
|
-
|
|
92
|
+
console.log(`[Lisa.ai Parser] Discovered abstract symbol failure: ${symbolName}. Scanning project tree...`);
|
|
93
|
+
// SEARCH ENTIRE ROOT instead of just 'src/' to support diverse project structures
|
|
94
|
+
const matchedFile = findFileBySymbolName(symbolName, searchDir, normalizedSkips);
|
|
83
95
|
if (matchedFile) {
|
|
84
96
|
// Return path relative to project root
|
|
85
97
|
return path.relative(searchDir, matchedFile);
|
|
@@ -100,14 +112,19 @@ function findFileBySymbolName(symbolName, dir, skipLedgerAbs) {
|
|
|
100
112
|
const files = fs.readdirSync(dir);
|
|
101
113
|
for (const file of files) {
|
|
102
114
|
const fullPath = path.join(dir, file);
|
|
103
|
-
|
|
115
|
+
if (file === 'node_modules' || file === 'dist' || file === 'build' || file === '.git' || file === '.angular')
|
|
116
|
+
continue;
|
|
117
|
+
let stat;
|
|
118
|
+
try {
|
|
119
|
+
stat = fs.statSync(fullPath);
|
|
120
|
+
}
|
|
121
|
+
catch (e) {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
104
124
|
if (stat.isDirectory()) {
|
|
105
|
-
|
|
106
|
-
if (
|
|
107
|
-
|
|
108
|
-
if (found)
|
|
109
|
-
return found;
|
|
110
|
-
}
|
|
125
|
+
const found = findFileBySymbolName(symbolName, fullPath, skipLedgerAbs);
|
|
126
|
+
if (found)
|
|
127
|
+
return found;
|
|
111
128
|
}
|
|
112
129
|
else if (file.match(/\.(ts|tsx|js|jsx|vue)$/)) {
|
|
113
130
|
// We ONLY care if the file natively exports/declares the symbol.
|
|
@@ -116,7 +133,9 @@ function findFileBySymbolName(symbolName, dir, skipLedgerAbs) {
|
|
|
116
133
|
if (content.includes(`class ${symbolName}`) ||
|
|
117
134
|
content.includes(`function ${symbolName}`) ||
|
|
118
135
|
content.includes(`const ${symbolName}`) ||
|
|
119
|
-
content.includes(`let ${symbolName}`)
|
|
136
|
+
content.includes(`let ${symbolName}`) ||
|
|
137
|
+
content.includes(`exports.${symbolName}`) ||
|
|
138
|
+
content.includes(`module.exports.${symbolName}`)) {
|
|
120
139
|
let targetPath = fullPath;
|
|
121
140
|
// Find standard test extensions dynamically based on ecosystem
|
|
122
141
|
const ext = path.extname(fullPath);
|