@lisa.ai/agent 1.1.6 â 1.1.7
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 +35 -13
- package/dist/commands/heal.js +68 -52
- package/dist/services/llm.service.js +30 -5
- package/dist/utils/parser.js +49 -37
- package/package.json +1 -1
|
@@ -76,29 +76,51 @@ async function coverageCommand(command, modelProvider, attempt = 1, maxRetries =
|
|
|
76
76
|
console.log(`â
[Lisa.ai Coverage] 100% Logic Coverage Verified! No un-covered files remaining.`);
|
|
77
77
|
return; // We exit the loop
|
|
78
78
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
79
|
+
// [Lisa.ai V2 Infinite Loop]
|
|
80
|
+
// We consciously remove the global retry cutoff. Lisa natively runs infinitely in the background
|
|
81
|
+
// resolving testing issues continuously until the pipeline hits 100% green code organically.
|
|
83
82
|
console.log(`[Lisa.ai Coverage] Found ${uncoveredFiles.length} file(s) below 100% threshold:`, uncoveredFiles);
|
|
84
83
|
// 3. Address the first uncovered file
|
|
85
84
|
const targetFilePath = uncoveredFiles[0];
|
|
86
85
|
const absoluteTarget = path.resolve(process.cwd(), targetFilePath);
|
|
87
|
-
console.log(`[Lisa.ai Coverage] Requesting generated test suite for ${targetFilePath}...`);
|
|
88
|
-
const fileContent = fs.readFileSync(absoluteTarget, 'utf-8');
|
|
89
|
-
const testCode = await (0, llm_service_1.generateTestForFile)(targetFilePath, fileContent, modelProvider, apiKey);
|
|
90
|
-
// Derive spec name from original file name (e.g. app.component.ts -> app.component.spec.ts)
|
|
91
86
|
const parsedPath = path.parse(absoluteTarget);
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
87
|
+
// Dynamically detect existing Testing specs irrespective of framework
|
|
88
|
+
const possibleSpecs = [
|
|
89
|
+
path.join(parsedPath.dir, `${parsedPath.name}.spec${parsedPath.ext}`),
|
|
90
|
+
path.join(parsedPath.dir, `${parsedPath.name}.test${parsedPath.ext}`),
|
|
91
|
+
path.join(parsedPath.dir, `${parsedPath.name}.spec.js`),
|
|
92
|
+
path.join(parsedPath.dir, `${parsedPath.name}.test.js`),
|
|
93
|
+
path.join(parsedPath.dir, `${parsedPath.name}.spec.ts`),
|
|
94
|
+
path.join(parsedPath.dir, `${parsedPath.name}.test.ts`)
|
|
95
|
+
];
|
|
96
|
+
let existingSpecContent = null;
|
|
97
|
+
let targetSpecPath = path.join(parsedPath.dir, `${parsedPath.name}.spec${parsedPath.ext}`); // Default
|
|
98
|
+
for (const p of possibleSpecs) {
|
|
99
|
+
if (fs.existsSync(p)) {
|
|
100
|
+
targetSpecPath = p;
|
|
101
|
+
existingSpecContent = fs.readFileSync(p, 'utf-8');
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
let testCode = '';
|
|
106
|
+
const fileContent = fs.readFileSync(absoluteTarget, 'utf-8');
|
|
107
|
+
if (existingSpecContent) {
|
|
108
|
+
console.log(`[Lisa.ai Coverage] Existing test suite discovered for ${targetFilePath}. Requesting single-pass logic coverage append...`);
|
|
109
|
+
testCode = await (0, llm_service_1.updateTestForFile)(targetFilePath, fileContent, path.relative(process.cwd(), targetSpecPath), existingSpecContent, modelProvider, apiKey);
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
console.log(`[Lisa.ai Coverage] Requesting newly generated test suite for ${targetFilePath}...`);
|
|
113
|
+
testCode = await (0, llm_service_1.generateTestForFile)(targetFilePath, fileContent, modelProvider, apiKey);
|
|
114
|
+
}
|
|
115
|
+
fs.writeFileSync(targetSpecPath, testCode, 'utf-8');
|
|
116
|
+
console.log(`[Lisa.ai Coverage] Wrote Spec File to ${targetSpecPath}`);
|
|
95
117
|
await (0, telemetry_service_1.reportTelemetry)({
|
|
96
118
|
projectId,
|
|
97
119
|
type: 'coverage',
|
|
98
|
-
filePath: targetFilePath,
|
|
120
|
+
filePath: targetFilePath,
|
|
99
121
|
modelUsed: modelProvider,
|
|
100
122
|
status: 'success',
|
|
101
|
-
details: `###
|
|
123
|
+
details: `### Logic Coverage Action Detected\n**Auto-Generated Specification Append (${modelProvider}):**\n\`\`\`typescript\n${testCode}\n\`\`\``
|
|
102
124
|
});
|
|
103
125
|
// 4. Recursive iteration to verify newly written test and hunt for next gap
|
|
104
126
|
await coverageCommand(command, modelProvider, attempt + 1, maxRetries, projectId, apiKey);
|
package/dist/commands/heal.js
CHANGED
|
@@ -41,30 +41,37 @@ const parser_1 = require("../utils/parser");
|
|
|
41
41
|
const llm_service_1 = require("../services/llm.service");
|
|
42
42
|
const git_service_1 = require("../services/git.service");
|
|
43
43
|
const telemetry_service_1 = require("../services/telemetry.service");
|
|
44
|
+
function isolateTestCommand(globalCommand, targetFile) {
|
|
45
|
+
const cmd = globalCommand.toLowerCase();
|
|
46
|
+
// Angular/Karma
|
|
47
|
+
if (cmd.includes('ng test') || cmd.includes('karma')) {
|
|
48
|
+
return `${globalCommand} --include ${targetFile}`;
|
|
49
|
+
}
|
|
50
|
+
// Jest/Vitest/Playwright
|
|
51
|
+
if (cmd.includes('jest') || cmd.includes('vitest') || cmd.includes('playwright')) {
|
|
52
|
+
return `${globalCommand} ${targetFile}`;
|
|
53
|
+
}
|
|
54
|
+
// Cypress
|
|
55
|
+
if (cmd.includes('cypress')) {
|
|
56
|
+
return `${globalCommand} --spec ${targetFile}`;
|
|
57
|
+
}
|
|
58
|
+
return globalCommand;
|
|
59
|
+
}
|
|
44
60
|
async function healCommand(command, modelProvider, attempt = 1, healedFilePath = null, maxRetries = 3, projectId = 'local', lastFixDetails, apiKey, skipLedger = [], consecutiveFails = 0, failTracker = {}) {
|
|
45
|
-
console.log(`\n[Lisa.ai Executing] ${command} (
|
|
61
|
+
console.log(`\n[Lisa.ai Executing] ${command} (Global Engine) Using Model: ${modelProvider}`);
|
|
46
62
|
try {
|
|
47
|
-
// Execute command synchronously
|
|
48
|
-
// stdio: 'pipe' captures stdout/stderr so they are thrown in error object on fail.
|
|
49
|
-
// However, to show stream we could use inherit, but we need the error log.
|
|
50
|
-
// For now, we capture and print it.
|
|
51
63
|
const output = (0, child_process_1.execSync)(command, { encoding: 'utf-8', stdio: 'pipe' });
|
|
52
64
|
console.log(output);
|
|
53
|
-
console.log(`\nâ
[Lisa.ai Success] Command executed successfully
|
|
54
|
-
// If we successfully executed and had previously healed a file, PR it
|
|
65
|
+
console.log(`\nâ
[Lisa.ai Success] Global Command executed successfully.`);
|
|
55
66
|
if (healedFilePath) {
|
|
56
67
|
await (0, git_service_1.createPullRequestForHeal)(healedFilePath);
|
|
57
68
|
}
|
|
69
|
+
return; // Successfully finished without throwing
|
|
58
70
|
}
|
|
59
71
|
catch (error) {
|
|
60
|
-
console.log(`\nâ [Lisa.ai Failure] Command failed.`);
|
|
61
|
-
// Combine logs
|
|
72
|
+
console.log(`\nâ [Lisa.ai Failure] Global Command failed.`);
|
|
62
73
|
const errorLog = (error.stderr || '') + '\n' + (error.stdout || '') + '\n' + (error.message || '');
|
|
63
74
|
console.error(errorLog);
|
|
64
|
-
if (attempt >= maxRetries) {
|
|
65
|
-
console.error(`\nđ¨ [Lisa.ai Error] Max retries (${maxRetries}) reached. Auto-heal failed.`);
|
|
66
|
-
process.exit(1);
|
|
67
|
-
}
|
|
68
75
|
console.log(`\n[Lisa.ai Auto-Heal] Analyzing errors...`);
|
|
69
76
|
const filePath = (0, parser_1.extractFilePath)(errorLog, skipLedger, process.cwd());
|
|
70
77
|
if (!filePath) {
|
|
@@ -76,51 +83,60 @@ async function healCommand(command, modelProvider, attempt = 1, healedFilePath =
|
|
|
76
83
|
console.error(`\nđ¨ [Lisa.ai Error] Extracted file path does not exist: ${absolutePath}`);
|
|
77
84
|
process.exit(1);
|
|
78
85
|
}
|
|
79
|
-
console.log(`[Lisa.ai Auto-Heal] Identified
|
|
80
|
-
//
|
|
81
|
-
|
|
82
|
-
//
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
86
|
+
console.log(`[Lisa.ai Auto-Heal] Identified bleeding file: ${filePath}`);
|
|
87
|
+
// [Lisa.ai V2 Micro-Targeted Healing Loop]
|
|
88
|
+
// Instead of restarting the global CI engine (e.g. `npm run test`) and waiting for all 400 specs
|
|
89
|
+
// to run just to fetch the error for this one file, we trap it locally with its native CLI frame isolation arguments!
|
|
90
|
+
let localAttempt = 1;
|
|
91
|
+
let isFixed = false;
|
|
92
|
+
let currentErrorLog = errorLog;
|
|
93
|
+
let previousContext = undefined;
|
|
94
|
+
let generatedDetails = '';
|
|
95
|
+
const isolatedCommand = isolateTestCommand(command, filePath);
|
|
96
|
+
while (localAttempt <= maxRetries && !isFixed) {
|
|
97
|
+
console.log(`\n[Lisa.ai Micro-Heal] Isolating ${filePath} (Attempt ${localAttempt}/${maxRetries})`);
|
|
98
|
+
const fileContent = fs.readFileSync(absolutePath, 'utf-8');
|
|
99
|
+
const fixedCode = await (0, llm_service_1.askLisaForFix)(filePath, fileContent, currentErrorLog, modelProvider, apiKey, previousContext);
|
|
100
|
+
fs.writeFileSync(absolutePath, fixedCode, 'utf-8');
|
|
101
|
+
console.log(`[Lisa.ai Micro-Heal] Applied isolated patch to ${filePath}`);
|
|
102
|
+
generatedDetails = `### Auto-Heal Analysis Triggered\n**Caught Error:**\n\`\`\`bash\n${currentErrorLog}\n\`\`\`\n\n**Applied Fix (${modelProvider}):**\n\`\`\`typescript\n${fixedCode}\n\`\`\``;
|
|
103
|
+
// Dispatch telemetry instantly so the dashboard updates exactly when the file is patched natively
|
|
104
|
+
await (0, telemetry_service_1.reportTelemetry)({
|
|
105
|
+
projectId,
|
|
106
|
+
type: 'heal',
|
|
107
|
+
filePath: filePath,
|
|
108
|
+
modelUsed: modelProvider,
|
|
109
|
+
status: 'success',
|
|
110
|
+
details: generatedDetails
|
|
111
|
+
});
|
|
112
|
+
try {
|
|
113
|
+
// Verify the isolated file silently without exploding the Global terminal with 400 other spec errors
|
|
114
|
+
console.log(`[Lisa.ai Micro-Heal] Verifying fix with isolated command: ${isolatedCommand}`);
|
|
115
|
+
(0, child_process_1.execSync)(isolatedCommand, { encoding: 'utf-8', stdio: 'pipe' });
|
|
116
|
+
console.log(`â
[Lisa.ai Micro-Heal] Isolated verification passed for ${filePath}!`);
|
|
117
|
+
isFixed = true;
|
|
118
|
+
}
|
|
119
|
+
catch (isolatedError) {
|
|
120
|
+
console.log(`â [Lisa.ai Micro-Heal] Isolated verification failed.`);
|
|
121
|
+
currentErrorLog = (isolatedError.stderr || '') + '\n' + (isolatedError.stdout || '') + '\n' + (isolatedError.message || '');
|
|
122
|
+
previousContext = `### Attempt ${localAttempt} Failed\n\`\`\`typescript\n${fixedCode}\n\`\`\`\n\n**New Error:**\n${currentErrorLog}`;
|
|
123
|
+
localAttempt++;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (!isFixed) {
|
|
127
|
+
console.warn(`\n[Lisa.ai Auto-Heal] â ď¸ Agent failed to locally heal ${filePath} after ${maxRetries} isolated attempts. Marking as Skipped to avoid infinite Loop.`);
|
|
87
128
|
skipLedger.push(filePath);
|
|
88
|
-
//
|
|
89
|
-
|
|
90
|
-
// otherwise Karma will STILL execute the broken spec, exit with Code 1, and eventually trap the Agent
|
|
91
|
-
// when the parser exhausts all files. We rename it to .broken to physically bypass the test runner.
|
|
92
|
-
if (filePath.endsWith('.spec.ts')) {
|
|
129
|
+
// Advanced Path Neutralization for generic JS tests across the V2 Ecosystem Architecture
|
|
130
|
+
if (filePath.match(/\.(spec|test)\.(ts|js|tsx|jsx|vue)$/)) {
|
|
93
131
|
const brokenPath = absolutePath + '.broken';
|
|
94
132
|
try {
|
|
95
133
|
fs.renameSync(absolutePath, brokenPath);
|
|
96
|
-
console.log(`[Lisa.ai Auto-Heal] đ¨ Renamed unhealable spec to ${filePath}.broken to force the test runner to
|
|
97
|
-
}
|
|
98
|
-
catch (renameErr) {
|
|
99
|
-
console.error(`[Lisa.ai Warning] Could not rename ${filePath}: ${renameErr.message}`);
|
|
134
|
+
console.log(`[Lisa.ai Auto-Heal] đ¨ Renamed unhealable spec to ${filePath}.broken to force the test runner to bypass it!`);
|
|
100
135
|
}
|
|
136
|
+
catch (e) { }
|
|
101
137
|
}
|
|
102
|
-
// Immediately retry the loop but force the parser to skip this broken file
|
|
103
|
-
await healCommand(command, modelProvider, attempt + 1, healedFilePath, maxRetries, projectId, lastFixDetails, apiKey, skipLedger, 0, failTracker);
|
|
104
|
-
return;
|
|
105
138
|
}
|
|
106
|
-
//
|
|
107
|
-
|
|
108
|
-
// Call LLM for a fix
|
|
109
|
-
const fixedCode = await (0, llm_service_1.askLisaForFix)(filePath, fileContent, errorLog, modelProvider, apiKey, previousContext);
|
|
110
|
-
// Write the fix to the file
|
|
111
|
-
fs.writeFileSync(absolutePath, fixedCode, 'utf-8');
|
|
112
|
-
console.log(`[Lisa.ai Auto-Heal] Applied patch to ${filePath}`);
|
|
113
|
-
const generatedDetails = `### Auto-Heal Analysis Triggered\n**Caught Error:**\n\`\`\`bash\n${errorLog}\n\`\`\`\n\n**Applied Fix (${modelProvider}):**\n\`\`\`typescript\n${fixedCode}\n\`\`\``;
|
|
114
|
-
// Dispatch telemetry instantly so the dashboard updates in real-time exactly when the file is patched
|
|
115
|
-
await (0, telemetry_service_1.reportTelemetry)({
|
|
116
|
-
projectId,
|
|
117
|
-
type: 'heal',
|
|
118
|
-
filePath: filePath,
|
|
119
|
-
modelUsed: modelProvider,
|
|
120
|
-
status: 'success',
|
|
121
|
-
details: generatedDetails
|
|
122
|
-
});
|
|
123
|
-
// Recursive retry, passing the healed file path state and updated limits
|
|
124
|
-
await healCommand(command, modelProvider, attempt + 1, filePath, maxRetries, projectId, generatedDetails, apiKey, skipLedger, 0, failTracker);
|
|
139
|
+
// Return to the Global Macro CI Engine to find the next error implicitly or gracefully green-light
|
|
140
|
+
await healCommand(command, modelProvider, 1, isFixed ? filePath : null, maxRetries, projectId, undefined, apiKey, skipLedger, 0, failTracker);
|
|
125
141
|
}
|
|
126
142
|
}
|
|
@@ -35,6 +35,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.askLisaForFix = askLisaForFix;
|
|
37
37
|
exports.generateTestForFile = generateTestForFile;
|
|
38
|
+
exports.updateTestForFile = updateTestForFile;
|
|
38
39
|
const ai_1 = require("ai");
|
|
39
40
|
const openai_1 = require("@ai-sdk/openai");
|
|
40
41
|
const anthropic_1 = require("@ai-sdk/anthropic");
|
|
@@ -79,7 +80,7 @@ ${fileContent}
|
|
|
79
80
|
2. Do not suppress TypeScript errors with @ts-ignore or any type assertions unless absolutely unavoidable.
|
|
80
81
|
3. Fix the underlying type or logic issue.
|
|
81
82
|
4. CRITICAL AGILE RULE: If the error is 'Cannot find module' or 'Property DOES NOT EXIST' and you do NOT know the correct import path or cannot resolve it from the context, you MUST silently comment out the failing import and completely comment out/stub the code that relies on it. Your ultimate goal is to make the file compile successfully so the Agent can proceed to the next file.
|
|
82
|
-
5.
|
|
83
|
+
5. FRAMEWORK TEST RULE: If fixing a testing file (\`.spec\` or \`.test\`) and an Angular error happens like 'NullInjectorError: No provider for X', you MUST add X to the 'providers' array in 'TestBed.configureTestingModule' or import the required testing module. If a React testing library error arises, fix the render context.
|
|
83
84
|
6. Return the code wrapped in a markdown code block (\`\`\`typescript ... \`\`\`). Do not include any explanation or intro text.`;
|
|
84
85
|
if (previousFixAttempt) {
|
|
85
86
|
console.log(`[Lisa.ai Auto-Heal] Warning! Agent is looping on ${filePath}. Injecting previous failed context...`);
|
|
@@ -100,20 +101,44 @@ async function generateTestForFile(sourceFilePath, sourceFileContent, modelProvi
|
|
|
100
101
|
console.log(`[Lisa.ai Coverage] Requesting test generation from ${modelProvider} for ${sourceFilePath}...`);
|
|
101
102
|
const model = getProvider(modelProvider, apiKey);
|
|
102
103
|
const prompt = `You are Lisa.ai, an autonomous CI/CD expert platform.
|
|
103
|
-
A
|
|
104
|
+
A source file lacks 100% test coverage. Your task is to generate a comprehensive testing suite covering all branches, lines, and functions.
|
|
104
105
|
|
|
105
106
|
--- Target File Content (${sourceFilePath}) ---
|
|
106
107
|
${sourceFileContent}
|
|
107
108
|
|
|
108
109
|
--- Constraints ---
|
|
109
|
-
1. Return the generated
|
|
110
|
+
1. Return the generated test code wrapped in a markdown code block (\`\`\`typescript ... \`\`\`).
|
|
110
111
|
2. Do not include any explanation or intro text.
|
|
111
|
-
3. Include all necessary imports assuming Jest is available.
|
|
112
|
+
3. Include all necessary imports assuming a standard testing framework (Jest/Karma/Vitest) is available.
|
|
112
113
|
4. Aim for 100% logic coverage.`;
|
|
113
114
|
const { text } = await (0, ai_1.generateText)({
|
|
114
115
|
model,
|
|
115
116
|
prompt,
|
|
116
117
|
});
|
|
117
|
-
const match = text.match(/```(?:typescript|ts)?\n([\s\S]*?)```/);
|
|
118
|
+
const match = text.match(/```(?:typescript|ts|javascript|js)?\n([\s\S]*?)```/);
|
|
119
|
+
return match ? match[1].trim() : text.trim();
|
|
120
|
+
}
|
|
121
|
+
async function updateTestForFile(sourceFilePath, sourceFileContent, testFilePath, existingTestContent, modelProvider, apiKey) {
|
|
122
|
+
console.log(`[Lisa.ai Coverage] Requesting test update from ${modelProvider} for ${sourceFilePath}...`);
|
|
123
|
+
const model = getProvider(modelProvider, apiKey);
|
|
124
|
+
const prompt = `You are Lisa.ai, an autonomous CI/CD expert platform.
|
|
125
|
+
A source file lacks 100% test coverage. You must update its existing test suite to achieve full coverage.
|
|
126
|
+
|
|
127
|
+
--- Target File Content (${sourceFilePath}) ---
|
|
128
|
+
${sourceFileContent}
|
|
129
|
+
|
|
130
|
+
--- Existing Test Suite (${testFilePath}) ---
|
|
131
|
+
${existingTestContent}
|
|
132
|
+
|
|
133
|
+
--- Constraints ---
|
|
134
|
+
1. Return the updated complete test code wrapped in a markdown code block (\`\`\`typescript ... \`\`\`).
|
|
135
|
+
2. Do not include any explanation or intro text.
|
|
136
|
+
3. Append missing tests to the existing suite. Do not delete existing passing tests unless they are fundamentally broken.
|
|
137
|
+
4. Aim for 100% logic coverage across branches, lines, and functions.`;
|
|
138
|
+
const { text } = await (0, ai_1.generateText)({
|
|
139
|
+
model,
|
|
140
|
+
prompt,
|
|
141
|
+
});
|
|
142
|
+
const match = text.match(/```(?:typescript|ts|javascript|js)?\n([\s\S]*?)```/);
|
|
118
143
|
return match ? match[1].trim() : text.trim();
|
|
119
144
|
}
|
package/dist/utils/parser.js
CHANGED
|
@@ -41,21 +41,18 @@ function extractFilePath(errorLog, skipFiles = [], searchDir = process.cwd()) {
|
|
|
41
41
|
const cleanLog = errorLog.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, '');
|
|
42
42
|
// Normalize skip files for reliable absolute path comparison
|
|
43
43
|
const normalizedSkips = skipFiles.map(p => path.resolve(p));
|
|
44
|
-
// 1. First Pass: Try to match typical TS/
|
|
45
|
-
// e.g., "src/
|
|
46
|
-
const
|
|
44
|
+
// 1. First Pass: Try to match typical JS/TS/Vue error patterns with line numbers
|
|
45
|
+
// e.g., "src/components/Button.tsx:12:3 - error"
|
|
46
|
+
const exactErrorRegex = /([a-zA-Z0-9_.\-\/\\]+\.(?:ts|tsx|js|jsx|vue))(?:\s*[:(])/g;
|
|
47
47
|
let match;
|
|
48
|
-
|
|
49
|
-
while ((match = tsErrorRegex.exec(cleanLog)) !== null) {
|
|
48
|
+
while ((match = exactErrorRegex.exec(cleanLog)) !== null) {
|
|
50
49
|
const foundPath = match[1];
|
|
51
|
-
// If this path is NOT in our skip ledger, we return it as the target
|
|
52
50
|
if (foundPath && !normalizedSkips.includes(path.resolve(foundPath))) {
|
|
53
51
|
return foundPath;
|
|
54
52
|
}
|
|
55
53
|
}
|
|
56
|
-
// 2. Second Pass (Fallback): Find anything that looks like a
|
|
57
|
-
|
|
58
|
-
const fallbackRegex = /([a-zA-Z0-9_.\-\/\\]+\.ts)/g;
|
|
54
|
+
// 2. Second Pass (Fallback): Find anything that looks like a source file
|
|
55
|
+
const fallbackRegex = /([a-zA-Z0-9_.\-\/\\]+\.(?:ts|tsx|js|jsx|vue))/g;
|
|
59
56
|
let fallbackMatch;
|
|
60
57
|
while ((fallbackMatch = fallbackRegex.exec(cleanLog)) !== null) {
|
|
61
58
|
const foundPath = fallbackMatch[1];
|
|
@@ -63,16 +60,19 @@ function extractFilePath(errorLog, skipFiles = [], searchDir = process.cwd()) {
|
|
|
63
60
|
return foundPath;
|
|
64
61
|
}
|
|
65
62
|
}
|
|
66
|
-
// 3. Third Pass (
|
|
67
|
-
//
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
63
|
+
// 3. Third Pass (Abstract Testing Fallback): Match exported PascalCase symbols
|
|
64
|
+
// Framework-agnostic. Often testing tools say "Unexpected 'MyButton'" or "Chrome 120 MyService FAILED"
|
|
65
|
+
const symbolRegex = /\b([A-Z][a-zA-Z0-9]{3,})\b/g;
|
|
66
|
+
let symbolMatch;
|
|
67
|
+
const searchedSymbols = new Set();
|
|
68
|
+
// Filter out common JS/Browser built-ins and framework generics that are not user files
|
|
69
|
+
const ignoreList = ['Error', 'TypeError', 'SyntaxError', 'NullInjectorError', 'Object', 'Boolean', 'String', 'Number', 'Array', 'Chrome', 'Windows', 'Linux', 'Macintosh', 'UserContext', 'TestBed', 'Module'];
|
|
70
|
+
while ((symbolMatch = symbolRegex.exec(cleanLog)) !== null) {
|
|
71
|
+
const symbolName = symbolMatch[1];
|
|
72
|
+
if (symbolName && !ignoreList.includes(symbolName) && !searchedSymbols.has(symbolName)) {
|
|
73
|
+
searchedSymbols.add(symbolName);
|
|
74
|
+
console.log(`[Lisa.ai Parser] Discovered abstract symbol failure: ${symbolName}. Scanning 'src/' tree...`);
|
|
75
|
+
const matchedFile = findFileBySymbolName(symbolName, path.join(searchDir, 'src'), normalizedSkips);
|
|
76
76
|
if (matchedFile) {
|
|
77
77
|
// Return path relative to project root
|
|
78
78
|
return path.relative(searchDir, matchedFile);
|
|
@@ -83,11 +83,11 @@ function extractFilePath(errorLog, skipFiles = [], searchDir = process.cwd()) {
|
|
|
83
83
|
return null;
|
|
84
84
|
}
|
|
85
85
|
/**
|
|
86
|
-
* Recursively walks a directory looking for a
|
|
87
|
-
* Designed to bypass
|
|
88
|
-
* We pass normalizedSkips to ensure we do not return a file that the Agent has explicitly blacklisted
|
|
86
|
+
* Recursively walks a directory looking for a source file that strictly exports the target symbol.
|
|
87
|
+
* Designed to bypass test runners omitting absolute script paths in test exceptions.
|
|
88
|
+
* We pass normalizedSkips to ensure we do not return a file that the Agent has explicitly blacklisted.
|
|
89
89
|
*/
|
|
90
|
-
function
|
|
90
|
+
function findFileBySymbolName(symbolName, dir, skipLedgerAbs) {
|
|
91
91
|
if (!fs.existsSync(dir))
|
|
92
92
|
return null;
|
|
93
93
|
const files = fs.readdirSync(dir);
|
|
@@ -95,29 +95,41 @@ function findFileByClassName(className, dir, skipLedgerAbs) {
|
|
|
95
95
|
const fullPath = path.join(dir, file);
|
|
96
96
|
const stat = fs.statSync(fullPath);
|
|
97
97
|
if (stat.isDirectory()) {
|
|
98
|
-
// Recurse into subdirectories
|
|
99
|
-
if (file !== 'node_modules') {
|
|
100
|
-
const found =
|
|
98
|
+
// Recurse into subdirectories
|
|
99
|
+
if (file !== 'node_modules' && file !== 'dist' && file !== 'build') {
|
|
100
|
+
const found = findFileBySymbolName(symbolName, fullPath, skipLedgerAbs);
|
|
101
101
|
if (found)
|
|
102
102
|
return found;
|
|
103
103
|
}
|
|
104
104
|
}
|
|
105
|
-
else if (file.
|
|
106
|
-
// We ONLY care if the file natively exports the
|
|
107
|
-
// We MUST NOT match files that merely `import` the
|
|
105
|
+
else if (file.match(/\.(ts|tsx|js|jsx|vue)$/)) {
|
|
106
|
+
// We ONLY care if the file natively exports/declares the symbol.
|
|
107
|
+
// We MUST NOT match files that merely `import` the symbol.
|
|
108
108
|
const content = fs.readFileSync(fullPath, 'utf8');
|
|
109
|
-
if (content.includes(`class ${
|
|
110
|
-
|
|
111
|
-
|
|
109
|
+
if (content.includes(`class ${symbolName}`) ||
|
|
110
|
+
content.includes(`function ${symbolName}`) ||
|
|
111
|
+
content.includes(`const ${symbolName}`) ||
|
|
112
|
+
content.includes(`let ${symbolName}`)) {
|
|
112
113
|
let targetPath = fullPath;
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
114
|
+
// Find standard test extensions dynamically based on ecosystem
|
|
115
|
+
const ext = path.extname(fullPath);
|
|
116
|
+
const base = fullPath.slice(0, -ext.length);
|
|
117
|
+
// Check if a spec/test file already exists for this source code natively
|
|
118
|
+
if (!file.includes('.spec.') && !file.includes('.test.')) {
|
|
119
|
+
const possibleSpecs = [
|
|
120
|
+
`${base}.spec${ext}`,
|
|
121
|
+
`${base}.test${ext}`,
|
|
122
|
+
`${base}.spec.js`,
|
|
123
|
+
`${base}.test.js`
|
|
124
|
+
];
|
|
125
|
+
for (const p of possibleSpecs) {
|
|
126
|
+
if (fs.existsSync(p)) {
|
|
127
|
+
targetPath = p;
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
117
130
|
}
|
|
118
131
|
}
|
|
119
132
|
// If the decided file is NOT in the skip ledger, return it.
|
|
120
|
-
// Otherwise, safely ignore it and keep looping for other matches in the directory tree.
|
|
121
133
|
if (!skipLedgerAbs.includes(path.resolve(targetPath))) {
|
|
122
134
|
return targetPath;
|
|
123
135
|
}
|