@yasserkhanorg/e2e-agents 1.7.5 → 1.7.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/agent/git.d.ts +2 -2
- package/dist/agent/git.d.ts.map +1 -1
- package/dist/agent/git.js +54 -3
- package/dist/api.js +3 -3
- package/dist/engine/ai_enrichment.d.ts.map +1 -1
- package/dist/engine/ai_enrichment.js +2 -1
- package/dist/engine/impact_engine.d.ts +15 -4
- package/dist/engine/impact_engine.d.ts.map +1 -1
- package/dist/engine/impact_engine.js +26 -15
- package/dist/engine/plan_builder.d.ts.map +1 -1
- package/dist/engine/plan_builder.js +84 -9
- package/dist/esm/agent/git.js +54 -3
- package/dist/esm/api.js +3 -3
- package/dist/esm/engine/ai_enrichment.js +2 -1
- package/dist/esm/engine/impact_engine.js +25 -15
- package/dist/esm/engine/plan_builder.js +85 -10
- package/dist/esm/index.js +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/package.json +1 -1
package/dist/agent/git.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export interface GitChangeResult {
|
|
2
2
|
files: string[];
|
|
3
|
-
/**
|
|
4
|
-
|
|
3
|
+
/** Test/spec files from the diff that were filtered by isRelevantFile(). Only includes files matching TEST_FILE_PATTERNS — not config, docs, or other non-test filtered files. */
|
|
4
|
+
filteredTestFiles: string[];
|
|
5
5
|
error?: string;
|
|
6
6
|
baseRef?: string;
|
|
7
7
|
baseStrategy?: 'merge-base' | 'direct';
|
package/dist/agent/git.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"git.d.ts","sourceRoot":"","sources":["../../src/agent/git.ts"],"names":[],"mappings":"AAkHA,MAAM,WAAW,eAAe;IAC5B,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,
|
|
1
|
+
{"version":3,"file":"git.d.ts","sourceRoot":"","sources":["../../src/agent/git.ts"],"names":[],"mappings":"AAkHA,MAAM,WAAW,eAAe;IAC5B,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,kLAAkL;IAClL,iBAAiB,EAAE,MAAM,EAAE,CAAC;IAC5B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,YAAY,GAAG,QAAQ,CAAC;CAC1C;AAED,MAAM,WAAW,gBAAgB;IAC7B,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAChC;AAiFD,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,gBAAgB,GAAG,eAAe,CA8D3G"}
|
package/dist/agent/git.js
CHANGED
|
@@ -146,6 +146,38 @@ function parseStatusLines(lines) {
|
|
|
146
146
|
}
|
|
147
147
|
return files;
|
|
148
148
|
}
|
|
149
|
+
// Comment-line patterns by file extension.
|
|
150
|
+
// A diff that ONLY touches these lines is a comment-only change (typo fix, doc update).
|
|
151
|
+
const COMMENT_PATTERNS = [
|
|
152
|
+
{ extensions: ['.go'], pattern: /^\s*(\/\/|\/\*|\*)/ },
|
|
153
|
+
{ extensions: ['.ts', '.tsx', '.js', '.jsx'], pattern: /^\s*(\/\/|\/\*|\*|\*\/)/ },
|
|
154
|
+
{ extensions: ['.py'], pattern: /^\s*#/ },
|
|
155
|
+
{ extensions: ['.css', '.scss'], pattern: /^\s*(\/\*|\*|\*\/)/ },
|
|
156
|
+
];
|
|
157
|
+
/**
|
|
158
|
+
* Check if a file's diff only changes comment lines (no code changes).
|
|
159
|
+
* Returns true if the diff is comment-only and can be safely excluded.
|
|
160
|
+
*/
|
|
161
|
+
function isCommentOnlyDiff(file, repoRoot, baseRef) {
|
|
162
|
+
const diff = runGitRaw(['diff', `${baseRef}..HEAD`, '-U0', '--', file], repoRoot);
|
|
163
|
+
if (!diff)
|
|
164
|
+
return false;
|
|
165
|
+
const ext = file.slice(file.lastIndexOf('.'));
|
|
166
|
+
const commentEntry = COMMENT_PATTERNS.find((cp) => cp.extensions.includes(ext));
|
|
167
|
+
if (!commentEntry)
|
|
168
|
+
return false;
|
|
169
|
+
// Extract only added/removed content lines (skip diff headers)
|
|
170
|
+
const contentLines = diff
|
|
171
|
+
.split('\n')
|
|
172
|
+
.filter((line) => (line.startsWith('+') || line.startsWith('-')) && !line.startsWith('+++') && !line.startsWith('---'));
|
|
173
|
+
if (contentLines.length === 0)
|
|
174
|
+
return false;
|
|
175
|
+
// Every changed line must be a comment line
|
|
176
|
+
return contentLines.every((line) => {
|
|
177
|
+
const content = line.slice(1).trim(); // Remove +/- prefix
|
|
178
|
+
return content === '' || commentEntry.pattern.test(content);
|
|
179
|
+
});
|
|
180
|
+
}
|
|
149
181
|
function getChangedFiles(appRoot, since, options) {
|
|
150
182
|
try {
|
|
151
183
|
const files = new Set();
|
|
@@ -167,7 +199,7 @@ function getChangedFiles(appRoot, since, options) {
|
|
|
167
199
|
const repoRoot = runGitRaw(['rev-parse', '--show-toplevel'], appRoot)?.trim() || appRoot;
|
|
168
200
|
const diffFiles = runGit(['diff', '--name-only', `${baseRef}..HEAD`], repoRoot);
|
|
169
201
|
if (!diffFiles) {
|
|
170
|
-
return { files: [], error: 'git diff failed' };
|
|
202
|
+
return { files: [], filteredTestFiles: [], error: 'git diff failed' };
|
|
171
203
|
}
|
|
172
204
|
diffFiles.forEach((file) => files.add(file));
|
|
173
205
|
if (options?.includeUncommitted) {
|
|
@@ -182,9 +214,28 @@ function getChangedFiles(appRoot, since, options) {
|
|
|
182
214
|
}
|
|
183
215
|
}
|
|
184
216
|
const allFiles = Array.from(files);
|
|
185
|
-
|
|
217
|
+
const relevant = [];
|
|
218
|
+
const filteredTestFiles = [];
|
|
219
|
+
for (const f of allFiles) {
|
|
220
|
+
if (isRelevantFile(f)) {
|
|
221
|
+
// Skip files where the diff only touches comments (typo fixes, doc updates)
|
|
222
|
+
if (isCommentOnlyDiff(f, repoRoot, baseRef)) {
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
relevant.push(f);
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
// Only capture files that were filtered because they match test patterns.
|
|
229
|
+
// Config, docs, workflow files etc. are not useful for PR-test detection.
|
|
230
|
+
const basename = f.split('/').pop() || f;
|
|
231
|
+
if (TEST_FILE_PATTERNS.some((p) => p.test(basename))) {
|
|
232
|
+
filteredTestFiles.push(f);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return { files: relevant, filteredTestFiles, baseRef, baseStrategy };
|
|
186
237
|
}
|
|
187
238
|
catch {
|
|
188
|
-
return { files: [], error: 'git diff failed' };
|
|
239
|
+
return { files: [], filteredTestFiles: [], error: 'git diff failed' };
|
|
189
240
|
}
|
|
190
241
|
}
|
package/dist/api.js
CHANGED
|
@@ -51,7 +51,7 @@ function analyzeImpactDeterministic(options = {}) {
|
|
|
51
51
|
return (0, impact_engine_js_1.analyzeImpact)(gitResult.files, {
|
|
52
52
|
testsRoot: reportRoot,
|
|
53
53
|
routeFamilies: config.routeFamilies,
|
|
54
|
-
|
|
54
|
+
filteredTestFiles: gitResult.filteredTestFiles,
|
|
55
55
|
});
|
|
56
56
|
}
|
|
57
57
|
function recommendTestsDeterministic(options = {}) {
|
|
@@ -61,7 +61,7 @@ function recommendTestsDeterministic(options = {}) {
|
|
|
61
61
|
const impact = (0, impact_engine_js_1.analyzeImpact)(gitResult.files, {
|
|
62
62
|
testsRoot: reportRoot,
|
|
63
63
|
routeFamilies: config.routeFamilies,
|
|
64
|
-
|
|
64
|
+
filteredTestFiles: gitResult.filteredTestFiles,
|
|
65
65
|
});
|
|
66
66
|
const adaptive = (0, feedback_js_1.getAdaptiveThresholds)(reportRoot);
|
|
67
67
|
const plan = (0, plan_builder_js_1.buildPlanFromImpact)(impact, config.policy, undefined, adaptive);
|
|
@@ -78,7 +78,7 @@ async function recommendTestsAI(options = {}) {
|
|
|
78
78
|
const impact = (0, impact_engine_js_1.analyzeImpact)(gitResult.files, {
|
|
79
79
|
testsRoot: reportRoot,
|
|
80
80
|
routeFamilies: config.routeFamilies,
|
|
81
|
-
|
|
81
|
+
filteredTestFiles: gitResult.filteredTestFiles,
|
|
82
82
|
});
|
|
83
83
|
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
84
84
|
let aiEnrichment;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ai_enrichment.d.ts","sourceRoot":"","sources":["../../src/engine/ai_enrichment.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,0BAA0B,CAAC;AAC1D,OAAO,KAAK,EAAC,YAAY,EAAmB,iBAAiB,EAAC,MAAM,oBAAoB,CAAC;AACzF,OAAO,KAAK,EAAC,eAAe,EAAC,MAAM,gCAAgC,CAAC;AAGpE,MAAM,WAAW,eAAe;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,eAAe,CAAC;IAC1B,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,cAAc,EAAE,MAAM,CAAC;IACvB,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,kBAAkB,EAAE,MAAM,EAAE,CAAC;IAC7B,WAAW,EAAE,MAAM,EAAE,CAAC;CACzB;AAED,MAAM,WAAW,kBAAkB;IAC/B,gBAAgB,EAAE,eAAe,EAAE,CAAC;IACpC,mBAAmB,EAAE,KAAK,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,aAAa,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAC,CAAC,CAAC;IAClF,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAC,CAAC;CAC/C;AAED,MAAM,WAAW,mBAAmB;IAChC,mBAAmB,EAAE,YAAY,CAAC;IAClC,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC3B,QAAQ,EAAE,WAAW,CAAC;IACtB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,WAAW,CAAC,EAAE,iBAAiB,EAAE,CAAC;IAClC,eAAe,CAAC,EAAE,MAAM,CAAC;CAC5B;
|
|
1
|
+
{"version":3,"file":"ai_enrichment.d.ts","sourceRoot":"","sources":["../../src/engine/ai_enrichment.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,0BAA0B,CAAC;AAC1D,OAAO,KAAK,EAAC,YAAY,EAAmB,iBAAiB,EAAC,MAAM,oBAAoB,CAAC;AACzF,OAAO,KAAK,EAAC,eAAe,EAAC,MAAM,gCAAgC,CAAC;AAGpE,MAAM,WAAW,eAAe;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,eAAe,CAAC;IAC1B,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,cAAc,EAAE,MAAM,CAAC;IACvB,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,kBAAkB,EAAE,MAAM,EAAE,CAAC;IAC7B,WAAW,EAAE,MAAM,EAAE,CAAC;CACzB;AAED,MAAM,WAAW,kBAAkB;IAC/B,gBAAgB,EAAE,eAAe,EAAE,CAAC;IACpC,mBAAmB,EAAE,KAAK,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,aAAa,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAC,CAAC,CAAC;IAClF,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAC,CAAC;CAC/C;AAED,MAAM,WAAW,mBAAmB;IAChC,mBAAmB,EAAE,YAAY,CAAC;IAClC,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC3B,QAAQ,EAAE,WAAW,CAAC;IACtB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,WAAW,CAAC,EAAE,iBAAiB,EAAE,CAAC;IAClC,eAAe,CAAC,EAAE,MAAM,CAAC;CAC5B;AAoLD;;;GAGG;AACH,wBAAsB,kBAAkB,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAkIlG"}
|
|
@@ -41,7 +41,8 @@ function buildPrompt(options) {
|
|
|
41
41
|
const specCount = feature.playwrightSpecs.length + feature.cypressSpecs.length;
|
|
42
42
|
const specList2 = [...feature.playwrightSpecs, ...feature.cypressSpecs];
|
|
43
43
|
const specsDisplay = specList2.length > 0 ? specList2.join(', ') : 'none';
|
|
44
|
-
|
|
44
|
+
const userFlowsDisplay = feature.userFlows.length > 0 ? ` userFlows=[${feature.userFlows.slice(0, 5).join(', ')}]` : '';
|
|
45
|
+
lines.push(`- familyId=${feature.familyId} ${featureIdPart} (${feature.priority}): ${specCount} specs, coverage=${feature.coverageStatus}, specs=[${specsDisplay}]${userFlowsDisplay}`);
|
|
45
46
|
}
|
|
46
47
|
lines.push('');
|
|
47
48
|
// Unbound files
|
|
@@ -36,8 +36,8 @@ export interface ImpactEngineOptions {
|
|
|
36
36
|
cypressRoot?: string;
|
|
37
37
|
routeFamilies?: RouteFamiliesConfig;
|
|
38
38
|
expandedFiles?: string[];
|
|
39
|
-
/**
|
|
40
|
-
|
|
39
|
+
/** Test files that were filtered by the caller (e.g. isRelevantFile in git.ts). Used to detect PR-included E2E specs. */
|
|
40
|
+
filteredTestFiles?: string[];
|
|
41
41
|
}
|
|
42
42
|
/**
|
|
43
43
|
* Extract describe/test/it titles from a spec file using regex.
|
|
@@ -45,13 +45,24 @@ export interface ImpactEngineOptions {
|
|
|
45
45
|
*/
|
|
46
46
|
export declare function extractScenarios(filePath: string, framework: 'playwright' | 'cypress'): string[];
|
|
47
47
|
export declare function analyzeImpact(changedFiles: string[], options: ImpactEngineOptions): ImpactResult;
|
|
48
|
+
export interface GapResult {
|
|
49
|
+
/** Active gaps that should be reported/enforced. */
|
|
50
|
+
gaps: ImpactedFeature[];
|
|
51
|
+
/** Family-level gaps suppressed because all their files are covered by specific feature matches. These should be promoted to advisory. */
|
|
52
|
+
suppressedGaps: ImpactedFeature[];
|
|
53
|
+
}
|
|
48
54
|
/**
|
|
49
55
|
* Get gaps: P0/P1 features with 'uncovered' status.
|
|
50
56
|
*
|
|
51
57
|
* Suppresses family-level (generic) gaps when ALL their changed files are
|
|
52
58
|
* already covered by feature-level (specific) matches in other families.
|
|
53
|
-
*
|
|
54
|
-
*
|
|
59
|
+
* Suppressed gaps are returned separately so the plan builder can promote
|
|
60
|
+
* them to advisory ("new behavior detected") on covered flows.
|
|
61
|
+
*/
|
|
62
|
+
export declare function getGapsWithSuppressed(result: ImpactResult): GapResult;
|
|
63
|
+
/**
|
|
64
|
+
* Get gaps: P0/P1 features with 'uncovered' status.
|
|
65
|
+
* Convenience wrapper that returns only active gaps (backward-compatible).
|
|
55
66
|
*/
|
|
56
67
|
export declare function getGaps(result: ImpactResult): ImpactedFeature[];
|
|
57
68
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"impact_engine.d.ts","sourceRoot":"","sources":["../../src/engine/impact_engine.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAGR,eAAe,EAClB,MAAM,gCAAgC,CAAC;AASxC,OAAO,KAAK,EAAC,mBAAmB,EAAC,MAAM,oBAAoB,CAAC;AAE5D,MAAM,MAAM,cAAc,GAAG,SAAS,GAAG,SAAS,GAAG,WAAW,CAAC;AAEjE,MAAM,WAAW,iBAAiB;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,EAAE,CAAC;CACvB;AAED,MAAM,WAAW,eAAe;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,eAAe,CAAC;IAC1B,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,qBAAqB,EAAE,iBAAiB,EAAE,CAAC;IAC3C,kBAAkB,EAAE,iBAAiB,EAAE,CAAC;IACxC,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,cAAc,EAAE,cAAc,CAAC;CAClC;AAED,MAAM,MAAM,cAAc,GAAG,YAAY,GAAG,SAAS,GAAG,MAAM,GAAG,UAAU,CAAC;AAE5E,MAAM,WAAW,UAAU;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,cAAc,CAAC;CACxB;AAED,MAAM,WAAW,YAAY;IACzB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,gBAAgB,EAAE,eAAe,EAAE,CAAC;IACpC,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,oFAAoF;IACpF,mBAAmB,EAAE,UAAU,EAAE,CAAC;CACrC;AAED,MAAM,WAAW,mBAAmB;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,mBAAmB,CAAC;IACpC,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,
|
|
1
|
+
{"version":3,"file":"impact_engine.d.ts","sourceRoot":"","sources":["../../src/engine/impact_engine.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAGR,eAAe,EAClB,MAAM,gCAAgC,CAAC;AASxC,OAAO,KAAK,EAAC,mBAAmB,EAAC,MAAM,oBAAoB,CAAC;AAE5D,MAAM,MAAM,cAAc,GAAG,SAAS,GAAG,SAAS,GAAG,WAAW,CAAC;AAEjE,MAAM,WAAW,iBAAiB;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,EAAE,CAAC;CACvB;AAED,MAAM,WAAW,eAAe;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,eAAe,CAAC;IAC1B,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,qBAAqB,EAAE,iBAAiB,EAAE,CAAC;IAC3C,kBAAkB,EAAE,iBAAiB,EAAE,CAAC;IACxC,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,cAAc,EAAE,cAAc,CAAC;CAClC;AAED,MAAM,MAAM,cAAc,GAAG,YAAY,GAAG,SAAS,GAAG,MAAM,GAAG,UAAU,CAAC;AAE5E,MAAM,WAAW,UAAU;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,cAAc,CAAC;CACxB;AAED,MAAM,WAAW,YAAY;IACzB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,gBAAgB,EAAE,eAAe,EAAE,CAAC;IACpC,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,oFAAoF;IACpF,mBAAmB,EAAE,UAAU,EAAE,CAAC;CACrC;AAED,MAAM,WAAW,mBAAmB;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,mBAAmB,CAAC;IACpC,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,yHAAyH;IACzH,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;CAChC;AA+CD;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,YAAY,GAAG,SAAS,GAAG,MAAM,EAAE,CAgBhG;AA0GD,wBAAgB,aAAa,CACzB,YAAY,EAAE,MAAM,EAAE,EACtB,OAAO,EAAE,mBAAmB,GAC7B,YAAY,CAuFd;AAYD,MAAM,WAAW,SAAS;IACtB,oDAAoD;IACpD,IAAI,EAAE,eAAe,EAAE,CAAC;IACxB,0IAA0I;IAC1I,cAAc,EAAE,eAAe,EAAE,CAAC;CACrC;AAED;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,YAAY,GAAG,SAAS,CA2BrE;AAED;;;GAGG;AACH,wBAAgB,OAAO,CAAC,MAAM,EAAE,YAAY,GAAG,eAAe,EAAE,CAE/D;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,YAAY,GAAG,eAAe,EAAE,CAItE"}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
5
|
exports.extractScenarios = extractScenarios;
|
|
6
6
|
exports.analyzeImpact = analyzeImpact;
|
|
7
|
+
exports.getGapsWithSuppressed = getGapsWithSuppressed;
|
|
7
8
|
exports.getGaps = getGaps;
|
|
8
9
|
exports.getPartialGaps = getPartialGaps;
|
|
9
10
|
const fs_1 = require("fs");
|
|
@@ -180,11 +181,10 @@ function analyzeImpact(changedFiles, options) {
|
|
|
180
181
|
const { testsRoot, routeFamilies } = options;
|
|
181
182
|
const warnings = [];
|
|
182
183
|
// Partition into source files and test files.
|
|
183
|
-
//
|
|
184
|
-
//
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
: [...changedFiles];
|
|
184
|
+
// Combine: (a) test files already in changedFiles that isTestFile catches, and
|
|
185
|
+
// (b) test files pre-filtered by the caller (filteredTestFiles from git.ts).
|
|
186
|
+
const preFilteredTests = options.filteredTestFiles ?? [];
|
|
187
|
+
const allOriginalFiles = [...new Set([...changedFiles, ...preFilteredTests])];
|
|
188
188
|
changedFiles = changedFiles.filter((f) => !isTestFile(f));
|
|
189
189
|
const prIncludedTestFiles = classifyPrTestFiles(allOriginalFiles, changedFiles);
|
|
190
190
|
// Load manifest
|
|
@@ -267,10 +267,10 @@ function inferCypressRoot(testsRoot) {
|
|
|
267
267
|
*
|
|
268
268
|
* Suppresses family-level (generic) gaps when ALL their changed files are
|
|
269
269
|
* already covered by feature-level (specific) matches in other families.
|
|
270
|
-
*
|
|
271
|
-
*
|
|
270
|
+
* Suppressed gaps are returned separately so the plan builder can promote
|
|
271
|
+
* them to advisory ("new behavior detected") on covered flows.
|
|
272
272
|
*/
|
|
273
|
-
function
|
|
273
|
+
function getGapsWithSuppressed(result) {
|
|
274
274
|
// Collect files that are covered via feature-level matches (more specific)
|
|
275
275
|
const filesCoveredByFeatures = new Set();
|
|
276
276
|
for (const f of result.impactedFeatures) {
|
|
@@ -280,18 +280,29 @@ function getGaps(result) {
|
|
|
280
280
|
}
|
|
281
281
|
}
|
|
282
282
|
}
|
|
283
|
-
|
|
283
|
+
const gaps = [];
|
|
284
|
+
const suppressedGaps = [];
|
|
285
|
+
for (const f of result.impactedFeatures) {
|
|
284
286
|
if (f.priority !== 'P0' && f.priority !== 'P1')
|
|
285
|
-
|
|
287
|
+
continue;
|
|
286
288
|
if (f.coverageStatus !== 'uncovered')
|
|
287
|
-
|
|
289
|
+
continue;
|
|
288
290
|
// Only suppress FAMILY-level gaps (no featureId = generic match).
|
|
289
|
-
// If it's a feature-level gap, keep it — it's specific and intentional.
|
|
290
291
|
if (!f.featureId && f.changedFiles.every((file) => filesCoveredByFeatures.has(file))) {
|
|
291
|
-
|
|
292
|
+
suppressedGaps.push(f);
|
|
292
293
|
}
|
|
293
|
-
|
|
294
|
-
|
|
294
|
+
else {
|
|
295
|
+
gaps.push(f);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return { gaps, suppressedGaps };
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Get gaps: P0/P1 features with 'uncovered' status.
|
|
302
|
+
* Convenience wrapper that returns only active gaps (backward-compatible).
|
|
303
|
+
*/
|
|
304
|
+
function getGaps(result) {
|
|
305
|
+
return getGapsWithSuppressed(result).gaps;
|
|
295
306
|
}
|
|
296
307
|
/**
|
|
297
308
|
* Get partial gaps: P0/P1 features with 'partial' status (advisory).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plan_builder.d.ts","sourceRoot":"","sources":["../../src/engine/plan_builder.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,oBAAoB,CAAC;AAErD,OAAO,KAAK,EAAC,YAAY,
|
|
1
|
+
{"version":3,"file":"plan_builder.d.ts","sourceRoot":"","sources":["../../src/engine/plan_builder.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,oBAAoB,CAAC;AAErD,OAAO,KAAK,EAAC,YAAY,EAA8B,MAAM,oBAAoB,CAAC;AAGlF,OAAO,KAAK,EAAC,kBAAkB,EAAC,MAAM,oBAAoB,CAAC;AAC3D,OAAO,KAAK,EAAC,kBAAkB,EAAC,MAAM,sBAAsB,CAAC;AAG7D,OAAO,KAAK,EACR,UAAU,EACV,SAAS,EACT,kBAAkB,EAIrB,MAAM,kBAAkB,CAAC;AAE1B,YAAY,EAAC,UAAU,EAAE,SAAS,EAAE,kBAAkB,EAAC,CAAC;AAsUxD,wBAAgB,mBAAmB,CAC/B,MAAM,EAAE,YAAY,EACpB,cAAc,CAAC,EAAE,OAAO,CAAC,YAAY,CAAC,EACtC,YAAY,CAAC,EAAE,kBAAkB,EACjC,kBAAkB,CAAC,EAAE,kBAAkB,GACxC,UAAU,CA6LZ;AAED,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,GAAG,MAAM,CAMzE;AAED,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM,CA4IhE;AAED,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,YAAY,SAAiC,GAAG,MAAM,CAMvH"}
|
|
@@ -11,6 +11,7 @@ const path_1 = require("path");
|
|
|
11
11
|
const minimatch_1 = require("minimatch");
|
|
12
12
|
const test_path_js_1 = require("../agent/test_path.js");
|
|
13
13
|
const impact_engine_js_1 = require("./impact_engine.js");
|
|
14
|
+
const route_families_js_1 = require("../knowledge/route_families.js");
|
|
14
15
|
const DEFAULT_POLICY = {
|
|
15
16
|
minConfidenceForTargeted: 60,
|
|
16
17
|
safeMergeMinConfidence: 85,
|
|
@@ -111,17 +112,73 @@ function pickRunSet(impact, confidence, policy) {
|
|
|
111
112
|
riskyFiles,
|
|
112
113
|
};
|
|
113
114
|
}
|
|
115
|
+
/**
|
|
116
|
+
* Check which gaps have matching PR-included E2E spec files by binding
|
|
117
|
+
* spec files to families via the manifest. Returns familyIds that are covered.
|
|
118
|
+
*/
|
|
119
|
+
function matchPrSpecsToGaps(prTestFiles, gaps, testsRoot) {
|
|
120
|
+
const coveredFamilies = new Set();
|
|
121
|
+
const prE2ESpecs = prTestFiles.filter((t) => t.type === 'playwright' || t.type === 'cypress');
|
|
122
|
+
if (prE2ESpecs.length === 0 || !testsRoot) {
|
|
123
|
+
return coveredFamilies;
|
|
124
|
+
}
|
|
125
|
+
// Try to bind PR spec files to families via the manifest
|
|
126
|
+
const manifest = (0, route_families_js_1.loadRouteFamilyManifest)(testsRoot);
|
|
127
|
+
if (manifest) {
|
|
128
|
+
const specBindings = (0, route_families_js_1.bindFilesToFamilies)(prE2ESpecs.map((s) => s.file), manifest);
|
|
129
|
+
for (const sb of specBindings) {
|
|
130
|
+
for (const binding of sb.bindings) {
|
|
131
|
+
coveredFamilies.add(binding.family);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
// Fallback heuristic: if manifest binding didn't match (common for Cypress specs
|
|
136
|
+
// in directories not mapped in the manifest), check path-based keyword overlap.
|
|
137
|
+
if (coveredFamilies.size === 0) {
|
|
138
|
+
const gapFamilyIds = new Set(gaps.map((g) => g.familyId));
|
|
139
|
+
for (const spec of prE2ESpecs) {
|
|
140
|
+
const specLower = spec.file.toLowerCase().replace(/[_\-/\\]/g, ' ');
|
|
141
|
+
for (const familyId of gapFamilyIds) {
|
|
142
|
+
// Check if the spec path contains the family name or related terms
|
|
143
|
+
if (specLower.includes(familyId.toLowerCase())) {
|
|
144
|
+
coveredFamilies.add(familyId);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return coveredFamilies;
|
|
150
|
+
}
|
|
114
151
|
function buildDecision(impact, runSet, confidence, policy) {
|
|
115
|
-
const gaps = (0, impact_engine_js_1.
|
|
152
|
+
const gaps = (0, impact_engine_js_1.getGapsWithSuppressed)(impact).gaps;
|
|
116
153
|
if (gaps.length > 0) {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
154
|
+
const prE2ESpecs = (impact.prIncludedTestFiles ?? [])
|
|
155
|
+
.filter((t) => t.type === 'playwright' || t.type === 'cypress');
|
|
156
|
+
if (prE2ESpecs.length > 0) {
|
|
157
|
+
// Bind PR specs to families — only soften gaps that have matching specs
|
|
158
|
+
const coveredFamilies = matchPrSpecsToGaps(impact.prIncludedTestFiles ?? [], gaps);
|
|
159
|
+
const uncoveredGaps = gaps.filter((g) => !coveredFamilies.has(g.familyId));
|
|
160
|
+
if (uncoveredGaps.length === 0) {
|
|
161
|
+
// ALL gaps have matching PR specs
|
|
162
|
+
return {
|
|
163
|
+
action: 'run-now',
|
|
164
|
+
title: 'Run now',
|
|
165
|
+
summary: `Detected ${gaps.length} coverage gap(s), but the PR includes ${prE2ESpecs.length} E2E test file(s) covering them. Verify the new tests cover impacted flows.`,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
if (uncoveredGaps.length < gaps.length) {
|
|
169
|
+
// SOME gaps covered by PR specs, others not
|
|
170
|
+
return {
|
|
171
|
+
action: 'must-add-tests',
|
|
172
|
+
title: 'Must add tests',
|
|
173
|
+
summary: `Detected ${gaps.length} coverage gap(s). PR includes E2E tests for ${gaps.length - uncoveredGaps.length}, but ${uncoveredGaps.length} flow(s) still need coverage.`,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
// No gaps matched by PR specs — but PR still has E2E files.
|
|
177
|
+
// Soften to run-now since the developer is actively writing tests.
|
|
121
178
|
return {
|
|
122
179
|
action: 'run-now',
|
|
123
180
|
title: 'Run now',
|
|
124
|
-
summary: `Detected ${gaps.length} coverage gap(s), but the PR includes ${
|
|
181
|
+
summary: `Detected ${gaps.length} coverage gap(s), but the PR includes ${prE2ESpecs.length} E2E test file(s). Verify the new tests cover impacted flows.`,
|
|
125
182
|
};
|
|
126
183
|
}
|
|
127
184
|
return {
|
|
@@ -239,7 +296,7 @@ function buildPlanFromImpact(impact, policyOverride, aiEnrichment, adaptiveThres
|
|
|
239
296
|
const runSetResult = pickRunSet(impact, confidence, policy);
|
|
240
297
|
const decision = buildDecision(impact, runSetResult.runSet, confidence, policy);
|
|
241
298
|
const enforcement = evaluateEnforcement(decision, policy);
|
|
242
|
-
const gaps = (0, impact_engine_js_1.
|
|
299
|
+
const { gaps, suppressedGaps } = (0, impact_engine_js_1.getGapsWithSuppressed)(impact);
|
|
243
300
|
const partialGaps = (0, impact_engine_js_1.getPartialGaps)(impact);
|
|
244
301
|
// Build two separate lookup maps from aiEnrichment: one by featureId, one by familyId.
|
|
245
302
|
// The familyId map stores only the FIRST feature encountered to avoid last-write-wins collisions.
|
|
@@ -324,9 +381,27 @@ function buildPlanFromImpact(impact, policyOverride, aiEnrichment, adaptiveThres
|
|
|
324
381
|
? (aiFeatureByFeatureId.get(f.featureId) ?? aiFeatureByFamilyId.get(f.familyId))
|
|
325
382
|
: aiFeatureByFamilyId.get(f.familyId);
|
|
326
383
|
// Only surface advisory scenarios when AI found new behavior in this diff
|
|
327
|
-
|
|
328
|
-
? aiFeature.aiMissingScenarios
|
|
384
|
+
let advisoryScenarios = aiFeature?.aiMissingScenarios?.length
|
|
385
|
+
? [...aiFeature.aiMissingScenarios]
|
|
329
386
|
: undefined;
|
|
387
|
+
// Promote suppressed gaps to advisory on covered flows that share changed files.
|
|
388
|
+
// When a family-level gap is suppressed (e.g. "post" because post.go is also in
|
|
389
|
+
// a covered feature like "channels/threads"), the behavioral change should appear
|
|
390
|
+
// here as "new behavior detected" instead of vanishing.
|
|
391
|
+
for (const sg of suppressedGaps) {
|
|
392
|
+
const sharedFiles = sg.changedFiles.filter((file) => f.changedFiles.includes(file));
|
|
393
|
+
if (sharedFiles.length > 0) {
|
|
394
|
+
const sgAi = sg.featureId
|
|
395
|
+
? (aiFeatureByFeatureId.get(sg.featureId) ?? aiFeatureByFamilyId.get(sg.familyId))
|
|
396
|
+
: aiFeatureByFamilyId.get(sg.familyId);
|
|
397
|
+
const sgScenarios = sgAi?.aiMissingScenarios?.length
|
|
398
|
+
? sgAi.aiMissingScenarios
|
|
399
|
+
: sg.userFlows.slice(0, 3);
|
|
400
|
+
if (sgScenarios.length > 0) {
|
|
401
|
+
advisoryScenarios = [...(advisoryScenarios || []), ...sgScenarios];
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
330
405
|
return {
|
|
331
406
|
id: featureLabel(f),
|
|
332
407
|
name: featureLabel(f),
|
package/dist/esm/agent/git.js
CHANGED
|
@@ -143,6 +143,38 @@ function parseStatusLines(lines) {
|
|
|
143
143
|
}
|
|
144
144
|
return files;
|
|
145
145
|
}
|
|
146
|
+
// Comment-line patterns by file extension.
|
|
147
|
+
// A diff that ONLY touches these lines is a comment-only change (typo fix, doc update).
|
|
148
|
+
const COMMENT_PATTERNS = [
|
|
149
|
+
{ extensions: ['.go'], pattern: /^\s*(\/\/|\/\*|\*)/ },
|
|
150
|
+
{ extensions: ['.ts', '.tsx', '.js', '.jsx'], pattern: /^\s*(\/\/|\/\*|\*|\*\/)/ },
|
|
151
|
+
{ extensions: ['.py'], pattern: /^\s*#/ },
|
|
152
|
+
{ extensions: ['.css', '.scss'], pattern: /^\s*(\/\*|\*|\*\/)/ },
|
|
153
|
+
];
|
|
154
|
+
/**
|
|
155
|
+
* Check if a file's diff only changes comment lines (no code changes).
|
|
156
|
+
* Returns true if the diff is comment-only and can be safely excluded.
|
|
157
|
+
*/
|
|
158
|
+
function isCommentOnlyDiff(file, repoRoot, baseRef) {
|
|
159
|
+
const diff = runGitRaw(['diff', `${baseRef}..HEAD`, '-U0', '--', file], repoRoot);
|
|
160
|
+
if (!diff)
|
|
161
|
+
return false;
|
|
162
|
+
const ext = file.slice(file.lastIndexOf('.'));
|
|
163
|
+
const commentEntry = COMMENT_PATTERNS.find((cp) => cp.extensions.includes(ext));
|
|
164
|
+
if (!commentEntry)
|
|
165
|
+
return false;
|
|
166
|
+
// Extract only added/removed content lines (skip diff headers)
|
|
167
|
+
const contentLines = diff
|
|
168
|
+
.split('\n')
|
|
169
|
+
.filter((line) => (line.startsWith('+') || line.startsWith('-')) && !line.startsWith('+++') && !line.startsWith('---'));
|
|
170
|
+
if (contentLines.length === 0)
|
|
171
|
+
return false;
|
|
172
|
+
// Every changed line must be a comment line
|
|
173
|
+
return contentLines.every((line) => {
|
|
174
|
+
const content = line.slice(1).trim(); // Remove +/- prefix
|
|
175
|
+
return content === '' || commentEntry.pattern.test(content);
|
|
176
|
+
});
|
|
177
|
+
}
|
|
146
178
|
export function getChangedFiles(appRoot, since, options) {
|
|
147
179
|
try {
|
|
148
180
|
const files = new Set();
|
|
@@ -164,7 +196,7 @@ export function getChangedFiles(appRoot, since, options) {
|
|
|
164
196
|
const repoRoot = runGitRaw(['rev-parse', '--show-toplevel'], appRoot)?.trim() || appRoot;
|
|
165
197
|
const diffFiles = runGit(['diff', '--name-only', `${baseRef}..HEAD`], repoRoot);
|
|
166
198
|
if (!diffFiles) {
|
|
167
|
-
return { files: [], error: 'git diff failed' };
|
|
199
|
+
return { files: [], filteredTestFiles: [], error: 'git diff failed' };
|
|
168
200
|
}
|
|
169
201
|
diffFiles.forEach((file) => files.add(file));
|
|
170
202
|
if (options?.includeUncommitted) {
|
|
@@ -179,9 +211,28 @@ export function getChangedFiles(appRoot, since, options) {
|
|
|
179
211
|
}
|
|
180
212
|
}
|
|
181
213
|
const allFiles = Array.from(files);
|
|
182
|
-
|
|
214
|
+
const relevant = [];
|
|
215
|
+
const filteredTestFiles = [];
|
|
216
|
+
for (const f of allFiles) {
|
|
217
|
+
if (isRelevantFile(f)) {
|
|
218
|
+
// Skip files where the diff only touches comments (typo fixes, doc updates)
|
|
219
|
+
if (isCommentOnlyDiff(f, repoRoot, baseRef)) {
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
relevant.push(f);
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
// Only capture files that were filtered because they match test patterns.
|
|
226
|
+
// Config, docs, workflow files etc. are not useful for PR-test detection.
|
|
227
|
+
const basename = f.split('/').pop() || f;
|
|
228
|
+
if (TEST_FILE_PATTERNS.some((p) => p.test(basename))) {
|
|
229
|
+
filteredTestFiles.push(f);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return { files: relevant, filteredTestFiles, baseRef, baseStrategy };
|
|
183
234
|
}
|
|
184
235
|
catch {
|
|
185
|
-
return { files: [], error: 'git diff failed' };
|
|
236
|
+
return { files: [], filteredTestFiles: [], error: 'git diff failed' };
|
|
186
237
|
}
|
|
187
238
|
}
|
package/dist/esm/api.js
CHANGED
|
@@ -43,7 +43,7 @@ export function analyzeImpactDeterministic(options = {}) {
|
|
|
43
43
|
return analyzeImpactV2(gitResult.files, {
|
|
44
44
|
testsRoot: reportRoot,
|
|
45
45
|
routeFamilies: config.routeFamilies,
|
|
46
|
-
|
|
46
|
+
filteredTestFiles: gitResult.filteredTestFiles,
|
|
47
47
|
});
|
|
48
48
|
}
|
|
49
49
|
export function recommendTestsDeterministic(options = {}) {
|
|
@@ -53,7 +53,7 @@ export function recommendTestsDeterministic(options = {}) {
|
|
|
53
53
|
const impact = analyzeImpactV2(gitResult.files, {
|
|
54
54
|
testsRoot: reportRoot,
|
|
55
55
|
routeFamilies: config.routeFamilies,
|
|
56
|
-
|
|
56
|
+
filteredTestFiles: gitResult.filteredTestFiles,
|
|
57
57
|
});
|
|
58
58
|
const adaptive = getAdaptiveThresholds(reportRoot);
|
|
59
59
|
const plan = buildPlanFromImpact(impact, config.policy, undefined, adaptive);
|
|
@@ -70,7 +70,7 @@ export async function recommendTestsAI(options = {}) {
|
|
|
70
70
|
const impact = analyzeImpactV2(gitResult.files, {
|
|
71
71
|
testsRoot: reportRoot,
|
|
72
72
|
routeFamilies: config.routeFamilies,
|
|
73
|
-
|
|
73
|
+
filteredTestFiles: gitResult.filteredTestFiles,
|
|
74
74
|
});
|
|
75
75
|
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
76
76
|
let aiEnrichment;
|
|
@@ -38,7 +38,8 @@ function buildPrompt(options) {
|
|
|
38
38
|
const specCount = feature.playwrightSpecs.length + feature.cypressSpecs.length;
|
|
39
39
|
const specList2 = [...feature.playwrightSpecs, ...feature.cypressSpecs];
|
|
40
40
|
const specsDisplay = specList2.length > 0 ? specList2.join(', ') : 'none';
|
|
41
|
-
|
|
41
|
+
const userFlowsDisplay = feature.userFlows.length > 0 ? ` userFlows=[${feature.userFlows.slice(0, 5).join(', ')}]` : '';
|
|
42
|
+
lines.push(`- familyId=${feature.familyId} ${featureIdPart} (${feature.priority}): ${specCount} specs, coverage=${feature.coverageStatus}, specs=[${specsDisplay}]${userFlowsDisplay}`);
|
|
42
43
|
}
|
|
43
44
|
lines.push('');
|
|
44
45
|
// Unbound files
|
|
@@ -174,11 +174,10 @@ export function analyzeImpact(changedFiles, options) {
|
|
|
174
174
|
const { testsRoot, routeFamilies } = options;
|
|
175
175
|
const warnings = [];
|
|
176
176
|
// Partition into source files and test files.
|
|
177
|
-
//
|
|
178
|
-
//
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
: [...changedFiles];
|
|
177
|
+
// Combine: (a) test files already in changedFiles that isTestFile catches, and
|
|
178
|
+
// (b) test files pre-filtered by the caller (filteredTestFiles from git.ts).
|
|
179
|
+
const preFilteredTests = options.filteredTestFiles ?? [];
|
|
180
|
+
const allOriginalFiles = [...new Set([...changedFiles, ...preFilteredTests])];
|
|
182
181
|
changedFiles = changedFiles.filter((f) => !isTestFile(f));
|
|
183
182
|
const prIncludedTestFiles = classifyPrTestFiles(allOriginalFiles, changedFiles);
|
|
184
183
|
// Load manifest
|
|
@@ -261,10 +260,10 @@ function inferCypressRoot(testsRoot) {
|
|
|
261
260
|
*
|
|
262
261
|
* Suppresses family-level (generic) gaps when ALL their changed files are
|
|
263
262
|
* already covered by feature-level (specific) matches in other families.
|
|
264
|
-
*
|
|
265
|
-
*
|
|
263
|
+
* Suppressed gaps are returned separately so the plan builder can promote
|
|
264
|
+
* them to advisory ("new behavior detected") on covered flows.
|
|
266
265
|
*/
|
|
267
|
-
export function
|
|
266
|
+
export function getGapsWithSuppressed(result) {
|
|
268
267
|
// Collect files that are covered via feature-level matches (more specific)
|
|
269
268
|
const filesCoveredByFeatures = new Set();
|
|
270
269
|
for (const f of result.impactedFeatures) {
|
|
@@ -274,18 +273,29 @@ export function getGaps(result) {
|
|
|
274
273
|
}
|
|
275
274
|
}
|
|
276
275
|
}
|
|
277
|
-
|
|
276
|
+
const gaps = [];
|
|
277
|
+
const suppressedGaps = [];
|
|
278
|
+
for (const f of result.impactedFeatures) {
|
|
278
279
|
if (f.priority !== 'P0' && f.priority !== 'P1')
|
|
279
|
-
|
|
280
|
+
continue;
|
|
280
281
|
if (f.coverageStatus !== 'uncovered')
|
|
281
|
-
|
|
282
|
+
continue;
|
|
282
283
|
// Only suppress FAMILY-level gaps (no featureId = generic match).
|
|
283
|
-
// If it's a feature-level gap, keep it — it's specific and intentional.
|
|
284
284
|
if (!f.featureId && f.changedFiles.every((file) => filesCoveredByFeatures.has(file))) {
|
|
285
|
-
|
|
285
|
+
suppressedGaps.push(f);
|
|
286
286
|
}
|
|
287
|
-
|
|
288
|
-
|
|
287
|
+
else {
|
|
288
|
+
gaps.push(f);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return { gaps, suppressedGaps };
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Get gaps: P0/P1 features with 'uncovered' status.
|
|
295
|
+
* Convenience wrapper that returns only active gaps (backward-compatible).
|
|
296
|
+
*/
|
|
297
|
+
export function getGaps(result) {
|
|
298
|
+
return getGapsWithSuppressed(result).gaps;
|
|
289
299
|
}
|
|
290
300
|
/**
|
|
291
301
|
* Get partial gaps: P0/P1 features with 'partial' status (advisory).
|
|
@@ -4,7 +4,8 @@ import { mkdirSync, writeFileSync } from 'fs';
|
|
|
4
4
|
import { dirname, join } from 'path';
|
|
5
5
|
import { minimatch } from 'minimatch';
|
|
6
6
|
import { inferSubsystemFromTestPath } from '../agent/test_path.js';
|
|
7
|
-
import { getGaps, getPartialGaps } from './impact_engine.js';
|
|
7
|
+
import { getGaps, getGapsWithSuppressed, getPartialGaps } from './impact_engine.js';
|
|
8
|
+
import { bindFilesToFamilies, loadRouteFamilyManifest } from '../knowledge/route_families.js';
|
|
8
9
|
const DEFAULT_POLICY = {
|
|
9
10
|
minConfidenceForTargeted: 60,
|
|
10
11
|
safeMergeMinConfidence: 85,
|
|
@@ -105,17 +106,73 @@ function pickRunSet(impact, confidence, policy) {
|
|
|
105
106
|
riskyFiles,
|
|
106
107
|
};
|
|
107
108
|
}
|
|
109
|
+
/**
|
|
110
|
+
* Check which gaps have matching PR-included E2E spec files by binding
|
|
111
|
+
* spec files to families via the manifest. Returns familyIds that are covered.
|
|
112
|
+
*/
|
|
113
|
+
function matchPrSpecsToGaps(prTestFiles, gaps, testsRoot) {
|
|
114
|
+
const coveredFamilies = new Set();
|
|
115
|
+
const prE2ESpecs = prTestFiles.filter((t) => t.type === 'playwright' || t.type === 'cypress');
|
|
116
|
+
if (prE2ESpecs.length === 0 || !testsRoot) {
|
|
117
|
+
return coveredFamilies;
|
|
118
|
+
}
|
|
119
|
+
// Try to bind PR spec files to families via the manifest
|
|
120
|
+
const manifest = loadRouteFamilyManifest(testsRoot);
|
|
121
|
+
if (manifest) {
|
|
122
|
+
const specBindings = bindFilesToFamilies(prE2ESpecs.map((s) => s.file), manifest);
|
|
123
|
+
for (const sb of specBindings) {
|
|
124
|
+
for (const binding of sb.bindings) {
|
|
125
|
+
coveredFamilies.add(binding.family);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// Fallback heuristic: if manifest binding didn't match (common for Cypress specs
|
|
130
|
+
// in directories not mapped in the manifest), check path-based keyword overlap.
|
|
131
|
+
if (coveredFamilies.size === 0) {
|
|
132
|
+
const gapFamilyIds = new Set(gaps.map((g) => g.familyId));
|
|
133
|
+
for (const spec of prE2ESpecs) {
|
|
134
|
+
const specLower = spec.file.toLowerCase().replace(/[_\-/\\]/g, ' ');
|
|
135
|
+
for (const familyId of gapFamilyIds) {
|
|
136
|
+
// Check if the spec path contains the family name or related terms
|
|
137
|
+
if (specLower.includes(familyId.toLowerCase())) {
|
|
138
|
+
coveredFamilies.add(familyId);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return coveredFamilies;
|
|
144
|
+
}
|
|
108
145
|
function buildDecision(impact, runSet, confidence, policy) {
|
|
109
|
-
const gaps =
|
|
146
|
+
const gaps = getGapsWithSuppressed(impact).gaps;
|
|
110
147
|
if (gaps.length > 0) {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
148
|
+
const prE2ESpecs = (impact.prIncludedTestFiles ?? [])
|
|
149
|
+
.filter((t) => t.type === 'playwright' || t.type === 'cypress');
|
|
150
|
+
if (prE2ESpecs.length > 0) {
|
|
151
|
+
// Bind PR specs to families — only soften gaps that have matching specs
|
|
152
|
+
const coveredFamilies = matchPrSpecsToGaps(impact.prIncludedTestFiles ?? [], gaps);
|
|
153
|
+
const uncoveredGaps = gaps.filter((g) => !coveredFamilies.has(g.familyId));
|
|
154
|
+
if (uncoveredGaps.length === 0) {
|
|
155
|
+
// ALL gaps have matching PR specs
|
|
156
|
+
return {
|
|
157
|
+
action: 'run-now',
|
|
158
|
+
title: 'Run now',
|
|
159
|
+
summary: `Detected ${gaps.length} coverage gap(s), but the PR includes ${prE2ESpecs.length} E2E test file(s) covering them. Verify the new tests cover impacted flows.`,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
if (uncoveredGaps.length < gaps.length) {
|
|
163
|
+
// SOME gaps covered by PR specs, others not
|
|
164
|
+
return {
|
|
165
|
+
action: 'must-add-tests',
|
|
166
|
+
title: 'Must add tests',
|
|
167
|
+
summary: `Detected ${gaps.length} coverage gap(s). PR includes E2E tests for ${gaps.length - uncoveredGaps.length}, but ${uncoveredGaps.length} flow(s) still need coverage.`,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
// No gaps matched by PR specs — but PR still has E2E files.
|
|
171
|
+
// Soften to run-now since the developer is actively writing tests.
|
|
115
172
|
return {
|
|
116
173
|
action: 'run-now',
|
|
117
174
|
title: 'Run now',
|
|
118
|
-
summary: `Detected ${gaps.length} coverage gap(s), but the PR includes ${
|
|
175
|
+
summary: `Detected ${gaps.length} coverage gap(s), but the PR includes ${prE2ESpecs.length} E2E test file(s). Verify the new tests cover impacted flows.`,
|
|
119
176
|
};
|
|
120
177
|
}
|
|
121
178
|
return {
|
|
@@ -233,7 +290,7 @@ export function buildPlanFromImpact(impact, policyOverride, aiEnrichment, adapti
|
|
|
233
290
|
const runSetResult = pickRunSet(impact, confidence, policy);
|
|
234
291
|
const decision = buildDecision(impact, runSetResult.runSet, confidence, policy);
|
|
235
292
|
const enforcement = evaluateEnforcement(decision, policy);
|
|
236
|
-
const gaps =
|
|
293
|
+
const { gaps, suppressedGaps } = getGapsWithSuppressed(impact);
|
|
237
294
|
const partialGaps = getPartialGaps(impact);
|
|
238
295
|
// Build two separate lookup maps from aiEnrichment: one by featureId, one by familyId.
|
|
239
296
|
// The familyId map stores only the FIRST feature encountered to avoid last-write-wins collisions.
|
|
@@ -318,9 +375,27 @@ export function buildPlanFromImpact(impact, policyOverride, aiEnrichment, adapti
|
|
|
318
375
|
? (aiFeatureByFeatureId.get(f.featureId) ?? aiFeatureByFamilyId.get(f.familyId))
|
|
319
376
|
: aiFeatureByFamilyId.get(f.familyId);
|
|
320
377
|
// Only surface advisory scenarios when AI found new behavior in this diff
|
|
321
|
-
|
|
322
|
-
? aiFeature.aiMissingScenarios
|
|
378
|
+
let advisoryScenarios = aiFeature?.aiMissingScenarios?.length
|
|
379
|
+
? [...aiFeature.aiMissingScenarios]
|
|
323
380
|
: undefined;
|
|
381
|
+
// Promote suppressed gaps to advisory on covered flows that share changed files.
|
|
382
|
+
// When a family-level gap is suppressed (e.g. "post" because post.go is also in
|
|
383
|
+
// a covered feature like "channels/threads"), the behavioral change should appear
|
|
384
|
+
// here as "new behavior detected" instead of vanishing.
|
|
385
|
+
for (const sg of suppressedGaps) {
|
|
386
|
+
const sharedFiles = sg.changedFiles.filter((file) => f.changedFiles.includes(file));
|
|
387
|
+
if (sharedFiles.length > 0) {
|
|
388
|
+
const sgAi = sg.featureId
|
|
389
|
+
? (aiFeatureByFeatureId.get(sg.featureId) ?? aiFeatureByFamilyId.get(sg.familyId))
|
|
390
|
+
: aiFeatureByFamilyId.get(sg.familyId);
|
|
391
|
+
const sgScenarios = sgAi?.aiMissingScenarios?.length
|
|
392
|
+
? sgAi.aiMissingScenarios
|
|
393
|
+
: sg.userFlows.slice(0, 3);
|
|
394
|
+
if (sgScenarios.length > 0) {
|
|
395
|
+
advisoryScenarios = [...(advisoryScenarios || []), ...sgScenarios];
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
324
399
|
return {
|
|
325
400
|
id: featureLabel(f),
|
|
326
401
|
name: featureLabel(f),
|
package/dist/esm/index.js
CHANGED
|
@@ -11,7 +11,7 @@ export { LLMProviderFactory, validateProviderSetup } from './provider_factory.js
|
|
|
11
11
|
// Agent API (deterministic impact + plan, traceability)
|
|
12
12
|
export { analyzeImpactDeterministic, recommendTestsDeterministic, handoffGeneratedTests, ingestTraceability, captureTraceability } from './api.js';
|
|
13
13
|
// V2 Engine (deterministic impact + plan)
|
|
14
|
-
export { analyzeImpact as analyzeImpactV2, getGaps, getPartialGaps } from './engine/impact_engine.js';
|
|
14
|
+
export { analyzeImpact as analyzeImpactV2, getGaps, getGapsWithSuppressed, getPartialGaps } from './engine/impact_engine.js';
|
|
15
15
|
export { extractScenarios } from './engine/impact_engine.js';
|
|
16
16
|
export { buildPlanFromImpact } from './engine/plan_builder.js';
|
|
17
17
|
export { appendFeedbackAndRecompute, readCalibration, readFlakyTests, getAdaptiveThresholds } from './agent/feedback.js';
|
package/dist/index.d.ts
CHANGED
|
@@ -20,8 +20,8 @@ export { LLMProviderFactory, validateProviderSetup } from './provider_factory.js
|
|
|
20
20
|
export type { HybridConfig } from './provider_factory.js';
|
|
21
21
|
export { analyzeImpactDeterministic, recommendTestsDeterministic, handoffGeneratedTests, ingestTraceability, captureTraceability } from './api.js';
|
|
22
22
|
export type { AgentApiOptions, RecommendTestsV2Result, TraceabilityIngestApiOptions, TraceabilityCaptureApiOptions, } from './api.js';
|
|
23
|
-
export { analyzeImpact as analyzeImpactV2, getGaps, getPartialGaps } from './engine/impact_engine.js';
|
|
24
|
-
export type { ImpactResult, ImpactedFeature, CoverageStatus, ImpactEngineOptions, SpecWithScenarios, PrTestFile, PrTestFileType } from './engine/impact_engine.js';
|
|
23
|
+
export { analyzeImpact as analyzeImpactV2, getGaps, getGapsWithSuppressed, getPartialGaps } from './engine/impact_engine.js';
|
|
24
|
+
export type { ImpactResult, ImpactedFeature, CoverageStatus, ImpactEngineOptions, SpecWithScenarios, PrTestFile, PrTestFileType, GapResult } from './engine/impact_engine.js';
|
|
25
25
|
export { extractScenarios } from './engine/impact_engine.js';
|
|
26
26
|
export { buildPlanFromImpact } from './engine/plan_builder.js';
|
|
27
27
|
export { appendFeedbackAndRecompute, readCalibration, readFlakyTests, getAdaptiveThresholds } from './agent/feedback.js';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;GAWG;AAGH,YAAY,EACR,WAAW,EACX,eAAe,EACf,UAAU,EACV,WAAW,EACX,UAAU,EACV,oBAAoB,EACpB,kBAAkB,EAClB,cAAc,EACd,eAAe,EACf,YAAY,EACZ,YAAY,EACZ,YAAY,GACf,MAAM,yBAAyB,CAAC;AAEjC,OAAO,EAAC,gBAAgB,EAAE,0BAA0B,EAAC,MAAM,yBAAyB,CAAC;AAGrF,OAAO,EAAC,iBAAiB,EAAE,mBAAmB,EAAC,MAAM,yBAAyB,CAAC;AAC/E,OAAO,EAAC,cAAc,EAAE,gBAAgB,EAAC,MAAM,sBAAsB,CAAC;AACtE,OAAO,EAAC,cAAc,EAAE,gBAAgB,EAAC,MAAM,sBAAsB,CAAC;AACtE,OAAO,EAAC,cAAc,EAAC,MAAM,sBAAsB,CAAC;AAGpD,OAAO,EAAC,kBAAkB,EAAE,qBAAqB,EAAC,MAAM,uBAAuB,CAAC;AAChF,YAAY,EAAC,YAAY,EAAC,MAAM,uBAAuB,CAAC;AAGxD,OAAO,EAAC,0BAA0B,EAAE,2BAA2B,EAAE,qBAAqB,EAAE,kBAAkB,EAAE,mBAAmB,EAAC,MAAM,UAAU,CAAC;AACjJ,YAAY,EACR,eAAe,EACf,sBAAsB,EACtB,4BAA4B,EAC5B,6BAA6B,GAChC,MAAM,UAAU,CAAC;AAGlB,OAAO,EAAC,aAAa,IAAI,eAAe,EAAE,OAAO,EAAE,cAAc,EAAC,MAAM,2BAA2B,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
3
3
|
// See LICENSE.txt for license information.
|
|
4
4
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
|
-
exports.
|
|
6
|
-
exports.formatValidationReport = exports.buildValidationReport = exports.validateCommit = exports.getCommitFiles = exports.enrichFamilies = exports.detectStaleFamilies = exports.mergeFamilies = void 0;
|
|
5
|
+
exports.runAgenticGeneration = exports.getSpecsForFamily = exports.buildSpecIndex = exports.loadOrBuildApiSurface = exports.buildApiSurface = exports.getUserFlowsForBinding = exports.getPriorityForBinding = exports.getCypressSpecDirsForBinding = exports.bindFilesToFamilies = exports.loadRouteFamilyManifest = exports.buildQualityFixPrompt = exports.buildHealPrompt = exports.renderHealMarkdown = exports.resolveHealTargets = exports.healFromReport = exports.runHealStage = exports.detectHallucinatedMethods = exports.parseGenerationResponse = exports.buildGenerationPrompt = exports.runGenerationStage = exports.runPipeline = exports.captureTraceabilityInput = exports.ingestTraceabilityInput = exports.finalizeGeneratedTests = exports.getAdaptiveThresholds = exports.readFlakyTests = exports.readCalibration = exports.appendFeedbackAndRecompute = exports.buildPlanFromImpact = exports.extractScenarios = exports.getPartialGaps = exports.getGapsWithSuppressed = exports.getGaps = exports.analyzeImpactV2 = exports.captureTraceability = exports.ingestTraceability = exports.handoffGeneratedTests = exports.recommendTestsDeterministic = exports.analyzeImpactDeterministic = exports.validateProviderSetup = exports.LLMProviderFactory = exports.CustomProvider = exports.checkOpenAISetup = exports.OpenAIProvider = exports.checkOllamaSetup = exports.OllamaProvider = exports.checkAnthropicSetup = exports.AnthropicProvider = exports.UnsupportedCapabilityError = exports.LLMProviderError = void 0;
|
|
6
|
+
exports.formatValidationReport = exports.buildValidationReport = exports.validateCommit = exports.getCommitFiles = exports.enrichFamilies = exports.detectStaleFamilies = exports.mergeFamilies = exports.scanProject = void 0;
|
|
7
7
|
var provider_interface_js_1 = require("./provider_interface.js");
|
|
8
8
|
Object.defineProperty(exports, "LLMProviderError", { enumerable: true, get: function () { return provider_interface_js_1.LLMProviderError; } });
|
|
9
9
|
Object.defineProperty(exports, "UnsupportedCapabilityError", { enumerable: true, get: function () { return provider_interface_js_1.UnsupportedCapabilityError; } });
|
|
@@ -34,6 +34,7 @@ Object.defineProperty(exports, "captureTraceability", { enumerable: true, get: f
|
|
|
34
34
|
var impact_engine_js_1 = require("./engine/impact_engine.js");
|
|
35
35
|
Object.defineProperty(exports, "analyzeImpactV2", { enumerable: true, get: function () { return impact_engine_js_1.analyzeImpact; } });
|
|
36
36
|
Object.defineProperty(exports, "getGaps", { enumerable: true, get: function () { return impact_engine_js_1.getGaps; } });
|
|
37
|
+
Object.defineProperty(exports, "getGapsWithSuppressed", { enumerable: true, get: function () { return impact_engine_js_1.getGapsWithSuppressed; } });
|
|
37
38
|
Object.defineProperty(exports, "getPartialGaps", { enumerable: true, get: function () { return impact_engine_js_1.getPartialGaps; } });
|
|
38
39
|
var impact_engine_js_2 = require("./engine/impact_engine.js");
|
|
39
40
|
Object.defineProperty(exports, "extractScenarios", { enumerable: true, get: function () { return impact_engine_js_2.extractScenarios; } });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yasserkhanorg/e2e-agents",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.7",
|
|
4
4
|
"description": "AI-powered E2E test impact analysis, generation, and healing. Analyzes code changes to identify affected Playwright tests, detects coverage gaps, and generates or repairs specs using pluggable LLM providers (Claude, OpenAI, Ollama). Includes MCP server, traceability, and CI/CD integration.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/esm/index.js",
|