@lisa.ai/agent 2.2.0 → 2.2.2
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.
|
@@ -44,6 +44,7 @@ const telemetry_service_1 = require("../services/telemetry.service");
|
|
|
44
44
|
const discovery_service_1 = require("../services/discovery.service");
|
|
45
45
|
const installer_service_1 = require("../services/installer.service");
|
|
46
46
|
const generator_service_1 = require("../services/generator.service");
|
|
47
|
+
const parser_1 = require("../utils/parser");
|
|
47
48
|
async function coverageCommand(command, modelProvider, attempt = 1, maxRetries = 3, projectId = 'local', apiKey) {
|
|
48
49
|
// Bug 8 fix: Always run scanRepository() so we have a framework fingerprint regardless of
|
|
49
50
|
// whether the user passed --command explicitly. scanRepository() is cheap (reads package.json)
|
|
@@ -60,6 +61,17 @@ async function coverageCommand(command, modelProvider, attempt = 1, maxRetries =
|
|
|
60
61
|
}
|
|
61
62
|
if (mutableFingerprint.suggestedTestCommand) {
|
|
62
63
|
command = mutableFingerprint.suggestedTestCommand;
|
|
64
|
+
// Ensure coverage reporting is enabled. Without --coverage the test runner never
|
|
65
|
+
// writes coverage-summary.json, so the agent stays permanently stuck in cold-start
|
|
66
|
+
// discovery mode no matter how many tests it generates.
|
|
67
|
+
// Karma instruments coverage via karma.conf.js — don't touch it.
|
|
68
|
+
const fw = mutableFingerprint.testingFramework;
|
|
69
|
+
if ((fw === 'jest' || fw === 'vitest') && !command.includes('--coverage')) {
|
|
70
|
+
command = command.includes('npm run')
|
|
71
|
+
? `${command} -- --coverage`
|
|
72
|
+
: `${command} --coverage`;
|
|
73
|
+
console.log(`[Lisa.ai Coverage] Coverage flag appended: ${command}`);
|
|
74
|
+
}
|
|
63
75
|
console.log(`[Lisa.ai Auto-Discovery] Bootstrapping execution with natively discovered command: ${command}`);
|
|
64
76
|
}
|
|
65
77
|
else {
|
|
@@ -120,14 +132,24 @@ async function coverageCommand(command, modelProvider, attempt = 1, maxRetries =
|
|
|
120
132
|
console.log(`\n✅ [Lisa.ai Coverage] Tests passed successfully on attempt ${attempt}.`);
|
|
121
133
|
}
|
|
122
134
|
catch (error) {
|
|
123
|
-
|
|
124
|
-
//
|
|
125
|
-
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
135
|
+
const errorLog = (error.stderr || '') + '\n' + (error.stdout || '') + '\n' + (error.message || '');
|
|
136
|
+
// Check whether the error output contains an actual broken source file.
|
|
137
|
+
// If it does → real compilation/runtime failure → delegate to healCommand.
|
|
138
|
+
// If it does NOT (e.g. Jest's "No tests found, exiting with code 1" on a cold-start
|
|
139
|
+
// project, or a jest config error with no file reference) → delegating to healCommand
|
|
140
|
+
// would crash with "Could not parse a valid failing file path". Instead, fall through
|
|
141
|
+
// to the cold-start discovery block below which will generate the first test files.
|
|
142
|
+
const hasHealableFile = (0, parser_1.extractFilePath)(errorLog, [], process.cwd()) !== null;
|
|
143
|
+
if (hasHealableFile) {
|
|
144
|
+
console.log(`\n❌ [Lisa.ai Coverage] Tests failed. Delegating to Auto-Heal...`);
|
|
145
|
+
await (0, heal_1.healCommand)(command, modelProvider, 1, null, maxRetries, projectId, undefined, apiKey);
|
|
146
|
+
console.log(`\n🔄 [Lisa.ai Coverage] Auto-Heal successful. Restarting coverage analysis...`);
|
|
147
|
+
await coverageCommand(command, modelProvider, attempt + 1, maxRetries, projectId, apiKey);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
// No file path found in the error — most likely cold-start (no test files yet).
|
|
151
|
+
// Fall through to the coverage evaluation block which will trigger cold-start discovery.
|
|
152
|
+
console.log(`\n[Lisa.ai Coverage] No failing spec file detected in error output. Tests may not exist yet — initiating Cold-Start Discovery...`);
|
|
131
153
|
}
|
|
132
154
|
// 2. Tests passed — evaluate coverage.
|
|
133
155
|
// Bug 5 fix: removed dead `executionPassed` variable. It was set to `true` on line 42
|
|
@@ -184,15 +206,26 @@ async function coverageCommand(command, modelProvider, attempt = 1, maxRetries =
|
|
|
184
206
|
}
|
|
185
207
|
let testCode = '';
|
|
186
208
|
const fileContent = fs.readFileSync(absoluteTarget, 'utf-8');
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
209
|
+
// Bug 9 fix: wrap the LLM call in its own try-catch so that a context-overflow
|
|
210
|
+
// or garbage response for ONE file doesn't crash the whole coverage loop.
|
|
211
|
+
// On failure we write a minimal placeholder spec so:
|
|
212
|
+
// (a) Jest doesn't error on a missing/corrupt file
|
|
213
|
+
// (b) The coverage report records this file so it won't be re-attempted
|
|
214
|
+
// (c) The loop continues straight to the next uncovered file.
|
|
215
|
+
try {
|
|
216
|
+
if (existingSpecContent) {
|
|
217
|
+
console.log(`[Lisa.ai Coverage] Existing test suite discovered for ${targetFilePath}. Requesting single-pass logic coverage append...`);
|
|
218
|
+
testCode = await (0, llm_service_1.updateTestForFile)(targetFilePath, fileContent, path.relative(process.cwd(), targetSpecPath), existingSpecContent, modelProvider, apiKey, detectedFramework);
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
console.log(`[Lisa.ai Coverage] Requesting newly generated test suite for ${targetFilePath}...`);
|
|
222
|
+
testCode = await (0, llm_service_1.generateTestForFile)(targetFilePath, fileContent, modelProvider, apiKey, detectedFramework);
|
|
223
|
+
}
|
|
191
224
|
}
|
|
192
|
-
|
|
193
|
-
console.
|
|
194
|
-
//
|
|
195
|
-
testCode =
|
|
225
|
+
catch (llmError) {
|
|
226
|
+
console.warn(`[Lisa.ai Coverage] Skipping ${targetFilePath} — LLM call failed: ${llmError.message}`);
|
|
227
|
+
// Placeholder keeps Jest happy and marks the file as "attempted"
|
|
228
|
+
testCode = `// Lisa.ai: Auto-skipped — ${llmError.message}\ntest('placeholder', () => { expect(true).toBe(true); });\n`;
|
|
196
229
|
}
|
|
197
230
|
fs.writeFileSync(targetSpecPath, testCode, 'utf-8');
|
|
198
231
|
console.log(`[Lisa.ai Coverage] Wrote Spec File to ${targetSpecPath}`);
|
|
@@ -83,8 +83,19 @@ class AutoDiscoveryService {
|
|
|
83
83
|
result.suggestedTestCommand = 'npm run test';
|
|
84
84
|
}
|
|
85
85
|
else {
|
|
86
|
-
// They have the library but no script;
|
|
87
|
-
|
|
86
|
+
// They have the library but no script; execute the binary directly.
|
|
87
|
+
// Include --coverage for jest/vitest so coverage-summary.json is produced.
|
|
88
|
+
// Without it the coverage command has no report to evaluate and gets permanently
|
|
89
|
+
// stuck in cold-start discovery mode.
|
|
90
|
+
if (result.testingFramework === 'karma') {
|
|
91
|
+
result.suggestedTestCommand = 'npx karma start';
|
|
92
|
+
}
|
|
93
|
+
else if (result.testingFramework === 'jest') {
|
|
94
|
+
result.suggestedTestCommand = 'npx jest --coverage';
|
|
95
|
+
}
|
|
96
|
+
else if (result.testingFramework === 'vitest') {
|
|
97
|
+
result.suggestedTestCommand = 'npx vitest run --coverage';
|
|
98
|
+
}
|
|
88
99
|
}
|
|
89
100
|
}
|
|
90
101
|
return result;
|
|
@@ -98,7 +109,33 @@ class AutoDiscoveryService {
|
|
|
98
109
|
return untested;
|
|
99
110
|
const files = fs.readdirSync(dir);
|
|
100
111
|
const ignoreDirs = ['node_modules', 'dist', 'build', '.git', '.angular', 'coverage', 'public', 'assets'];
|
|
101
|
-
const ignoreFiles = [
|
|
112
|
+
const ignoreFiles = [
|
|
113
|
+
// Angular / framework entry points
|
|
114
|
+
'main.ts', 'index.ts', 'app.config.ts', 'app.routes.ts',
|
|
115
|
+
'styles.css', 'styles.scss',
|
|
116
|
+
// Common config files (exact names)
|
|
117
|
+
'tailwind.config.js', 'tailwind.config.ts',
|
|
118
|
+
'eslint.config.js', 'eslint.config.ts',
|
|
119
|
+
'jest.config.js', 'jest.config.ts', 'jest.config.mjs',
|
|
120
|
+
'jest.setup.js', 'jest.setup.ts',
|
|
121
|
+
'webpack.config.js', 'webpack.config.ts',
|
|
122
|
+
'babel.config.js', 'babel.config.ts',
|
|
123
|
+
'rollup.config.js', 'rollup.config.ts',
|
|
124
|
+
'vite.config.js', 'vite.config.ts',
|
|
125
|
+
'vitest.config.js', 'vitest.config.ts',
|
|
126
|
+
'karma.conf.js', 'karma.config.js',
|
|
127
|
+
'prettier.config.js', 'prettier.config.ts',
|
|
128
|
+
'postcss.config.js', 'postcss.config.ts',
|
|
129
|
+
'gulpfile.js', 'Gulpfile.js',
|
|
130
|
+
// Dotfile configs often present in Node projects
|
|
131
|
+
'.eslintrc.js', '.eslintrc.ts', '.babelrc.js',
|
|
132
|
+
];
|
|
133
|
+
// Pattern-based exclusions: dotfiles and *.config.* files not in the list above
|
|
134
|
+
const ignorePatterns = [
|
|
135
|
+
/^\./, // any dotfile (e.g. .eslintrc.js, .prettierrc.js)
|
|
136
|
+
/\.config\.(js|ts|mjs|cjs)$/, // foo.config.js / foo.config.ts
|
|
137
|
+
/\.conf\.(js|ts|mjs|cjs)$/, // foo.conf.js
|
|
138
|
+
];
|
|
102
139
|
for (const file of files) {
|
|
103
140
|
const fullPath = path.join(dir, file);
|
|
104
141
|
if (ignoreDirs.includes(file))
|
|
@@ -116,6 +153,8 @@ class AutoDiscoveryService {
|
|
|
116
153
|
else if (file.match(/\.(ts|tsx|js|jsx|vue)$/) && !file.includes('.spec.') && !file.includes('.test.')) {
|
|
117
154
|
if (ignoreFiles.includes(file))
|
|
118
155
|
continue;
|
|
156
|
+
if (ignorePatterns.some(p => p.test(file)))
|
|
157
|
+
continue;
|
|
119
158
|
const ext = path.extname(file);
|
|
120
159
|
const base = file.slice(0, -ext.length);
|
|
121
160
|
const possibleSpecs = [
|
|
@@ -44,6 +44,26 @@ const google_1 = require("@ai-sdk/google");
|
|
|
44
44
|
const path = __importStar(require("path"));
|
|
45
45
|
const dotenv = __importStar(require("dotenv"));
|
|
46
46
|
dotenv.config({ path: path.resolve(__dirname, '../../.env'), quiet: true });
|
|
47
|
+
// Context-window guard: large files cause the LLM to hit its input limit and return
|
|
48
|
+
// its internal context-compression prompt instead of code, corrupting the spec file.
|
|
49
|
+
// 15 000 chars ≈ ~4 000 tokens — safe for all supported models.
|
|
50
|
+
const MAX_SOURCE_CHARS = 15000;
|
|
51
|
+
function truncateSource(content, label) {
|
|
52
|
+
if (content.length <= MAX_SOURCE_CHARS)
|
|
53
|
+
return content;
|
|
54
|
+
console.warn(`[Lisa.ai LLM] ${label} is ${content.length} chars — truncating to ${MAX_SOURCE_CHARS} to stay within context window.`);
|
|
55
|
+
return content.slice(0, MAX_SOURCE_CHARS) + '\n\n// ... (truncated)';
|
|
56
|
+
}
|
|
57
|
+
// Sanity-check the LLM response before it gets written to disk.
|
|
58
|
+
// If the response contains none of the standard test primitives it is almost certainly
|
|
59
|
+
// a meta-prompt leak or hallucination — throw so the caller can write a placeholder
|
|
60
|
+
// instead of permanently corrupting the spec file.
|
|
61
|
+
function assertLooksLikeTestCode(text, filePath) {
|
|
62
|
+
const testKeywords = /\b(describe|it\(|test\(|expect\(|beforeEach|afterEach|suite|assert\.|cy\.)\b/;
|
|
63
|
+
if (!testKeywords.test(text)) {
|
|
64
|
+
throw new Error(`LLM returned a non-code response for ${filePath} (possible context overflow). Skipping to prevent file corruption.`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
47
67
|
function getProvider(provider, apiKey) {
|
|
48
68
|
if (provider === 'claude') {
|
|
49
69
|
const key = apiKey || process.env.ANTHROPIC_API_KEY;
|
|
@@ -125,7 +145,7 @@ async function generateTestForFile(sourceFilePath, sourceFileContent, modelProvi
|
|
|
125
145
|
: "3. Include all necessary imports assuming a standard testing framework (Jest/Karma/Vitest) is available.\n";
|
|
126
146
|
const prompt = "You are Lisa.ai, an autonomous CI/CD expert platform.\n" +
|
|
127
147
|
"A source file lacks 100% test coverage. Your task is to generate a comprehensive testing suite covering all branches, lines, and functions.\n\n" +
|
|
128
|
-
"--- Target File Content (" + sourceFilePath + ") ---\n" + sourceFileContent + "\n\n" +
|
|
148
|
+
"--- Target File Content (" + sourceFilePath + ") ---\n" + truncateSource(sourceFileContent, sourceFilePath) + "\n\n" +
|
|
129
149
|
"--- Constraints ---\n" +
|
|
130
150
|
"1. Return the generated test code wrapped in a markdown code block (```typescript ... ```).\n" +
|
|
131
151
|
"2. Do not include any explanation or intro text.\n" +
|
|
@@ -136,7 +156,9 @@ async function generateTestForFile(sourceFilePath, sourceFileContent, modelProvi
|
|
|
136
156
|
prompt,
|
|
137
157
|
});
|
|
138
158
|
const match = text.match(/```(?:typescript|ts|javascript|js)?\n([\s\S]*?)```/);
|
|
139
|
-
|
|
159
|
+
const result = match ? match[1].trim() : text.trim();
|
|
160
|
+
assertLooksLikeTestCode(result, sourceFilePath);
|
|
161
|
+
return result;
|
|
140
162
|
}
|
|
141
163
|
async function updateTestForFile(sourceFilePath, sourceFileContent, testFilePath, existingTestContent, modelProvider, apiKey, framework) {
|
|
142
164
|
console.log(`[Lisa.ai Coverage] Requesting test update from ${modelProvider} for ${sourceFilePath}...`);
|
|
@@ -148,7 +170,7 @@ async function updateTestForFile(sourceFilePath, sourceFileContent, testFilePath
|
|
|
148
170
|
: "3. Append missing tests to the existing suite. Do not delete existing passing tests unless they are fundamentally broken.\n";
|
|
149
171
|
const prompt = "You are Lisa.ai, an autonomous CI/CD expert platform.\n" +
|
|
150
172
|
"A source file lacks 100% test coverage. You must update its existing test suite to achieve full coverage.\n\n" +
|
|
151
|
-
"--- Target File Content (" + sourceFilePath + ") ---\n" + sourceFileContent + "\n\n" +
|
|
173
|
+
"--- Target File Content (" + sourceFilePath + ") ---\n" + truncateSource(sourceFileContent, sourceFilePath) + "\n\n" +
|
|
152
174
|
"--- Existing Test Suite (" + testFilePath + ") ---\n" + existingTestContent + "\n\n" +
|
|
153
175
|
"--- Constraints ---\n" +
|
|
154
176
|
"1. Return the updated complete test code wrapped in a markdown code block (```typescript ... ```).\n" +
|
|
@@ -160,7 +182,9 @@ async function updateTestForFile(sourceFilePath, sourceFileContent, testFilePath
|
|
|
160
182
|
prompt,
|
|
161
183
|
});
|
|
162
184
|
const match = text.match(/```(?:typescript|ts|javascript|js)?\n([\s\S]*?)```/);
|
|
163
|
-
|
|
185
|
+
const result = match ? match[1].trim() : text.trim();
|
|
186
|
+
assertLooksLikeTestCode(result, sourceFilePath);
|
|
187
|
+
return result;
|
|
164
188
|
}
|
|
165
189
|
async function askLisaGeneral(promptStr, modelProvider, apiKey) {
|
|
166
190
|
const model = getProvider(modelProvider, apiKey);
|