@yasserkhanorg/e2e-agents 1.9.5 → 1.10.0
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/esm/knowledge/api_surface.js +265 -34
- package/dist/esm/knowledge/failure_history.js +121 -0
- package/dist/esm/knowledge/route_families.js +31 -1
- package/dist/esm/pipeline/stage1_impact.js +19 -3
- package/dist/esm/pipeline/stage2_coverage.js +28 -7
- package/dist/esm/pipeline/stage3_generation.js +20 -1
- package/dist/esm/prompts/coverage.js +10 -0
- package/dist/esm/prompts/generation.js +41 -7
- package/dist/esm/validation/guardrails.js +5 -0
- package/dist/knowledge/api_surface.d.ts +12 -0
- package/dist/knowledge/api_surface.d.ts.map +1 -1
- package/dist/knowledge/api_surface.js +268 -34
- package/dist/knowledge/failure_history.d.ts +39 -0
- package/dist/knowledge/failure_history.d.ts.map +1 -0
- package/dist/knowledge/failure_history.js +128 -0
- package/dist/knowledge/route_families.d.ts +11 -0
- package/dist/knowledge/route_families.d.ts.map +1 -1
- package/dist/knowledge/route_families.js +32 -1
- package/dist/pipeline/stage1_impact.d.ts +1 -1
- package/dist/pipeline/stage1_impact.d.ts.map +1 -1
- package/dist/pipeline/stage1_impact.js +18 -2
- package/dist/pipeline/stage2_coverage.d.ts.map +1 -1
- package/dist/pipeline/stage2_coverage.js +28 -7
- package/dist/pipeline/stage3_generation.d.ts.map +1 -1
- package/dist/pipeline/stage3_generation.js +20 -1
- package/dist/prompts/coverage.d.ts.map +1 -1
- package/dist/prompts/coverage.js +10 -0
- package/dist/prompts/generation.d.ts +1 -1
- package/dist/prompts/generation.d.ts.map +1 -1
- package/dist/prompts/generation.js +41 -7
- package/dist/validation/guardrails.d.ts +2 -0
- package/dist/validation/guardrails.d.ts.map +1 -1
- package/dist/validation/guardrails.js +5 -0
- package/dist/validation/output_schema.d.ts +3 -0
- package/dist/validation/output_schema.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -101,10 +101,29 @@ export async function runGenerationStage(decisions, apiSurface, testsRoot, confi
|
|
|
101
101
|
skipped.push(`${decision.flowId}: invalid code returned`);
|
|
102
102
|
continue;
|
|
103
103
|
}
|
|
104
|
-
// Hallucination detection
|
|
104
|
+
// Hallucination detection — block specs with hallucinated methods
|
|
105
105
|
const hallucinationWarnings = detectHallucinatedMethods(parsed.code, apiSurface);
|
|
106
106
|
if (hallucinationWarnings.length > 0) {
|
|
107
107
|
warnings.push(`Flow ${decision.flowId}: suspected hallucinated methods: ${hallucinationWarnings.join(', ')}`);
|
|
108
|
+
if (!config.warnOnHallucinations) {
|
|
109
|
+
// Block: move to needs-review instead of writing to specs dir
|
|
110
|
+
if (!dryRun) {
|
|
111
|
+
const reviewDir = join(testsRoot, 'generated-needs-review');
|
|
112
|
+
mkdirSync(reviewDir, { recursive: true });
|
|
113
|
+
const safeName = decision.flowId.replace(/[^a-zA-Z0-9_-]/g, '_').toLowerCase();
|
|
114
|
+
const reviewPath = join(reviewDir, `${safeName}-${Date.now().toString(36)}.spec.ts`);
|
|
115
|
+
writeFileSync(reviewPath, `${parsed.code}\n`, 'utf-8');
|
|
116
|
+
warnings.push(`Flow ${decision.flowId}: blocked — moved to ${reviewPath}`);
|
|
117
|
+
}
|
|
118
|
+
generated.push({
|
|
119
|
+
flowId: decision.flowId,
|
|
120
|
+
specPath,
|
|
121
|
+
mode,
|
|
122
|
+
written: false,
|
|
123
|
+
hallucinationWarnings,
|
|
124
|
+
});
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
108
127
|
}
|
|
109
128
|
let written = false;
|
|
110
129
|
if (!dryRun) {
|
|
@@ -40,6 +40,16 @@ export function buildCoveragePrompt(ctx) {
|
|
|
40
40
|
'- For add_scenarios, specify which existing spec file to extend in targetSpec.',
|
|
41
41
|
`- For create_spec, suggest a path following ${ctx.profile?.projectName || 'Mattermost'} conventions.`,
|
|
42
42
|
'- Prefer adding scenarios to existing specs over creating new spec files.',
|
|
43
|
+
'',
|
|
44
|
+
'SEMANTIC MATCHING RULES (critical for accuracy):',
|
|
45
|
+
'- A happy-path test does NOT cover the negative/error path of the same feature.',
|
|
46
|
+
' "user can edit post" does NOT cover "user without permission cannot edit post".',
|
|
47
|
+
'- A test for one user role does NOT cover a different role.',
|
|
48
|
+
' "admin can delete channel" does NOT cover "member cannot delete channel".',
|
|
49
|
+
'- A test for creation does NOT cover editing or deletion of the same entity.',
|
|
50
|
+
'- "partial" means: same feature area but different specific scenario.',
|
|
51
|
+
'- "full" means: the exact user action sequence and outcome is tested.',
|
|
52
|
+
'- When in doubt between "full" and "partial", choose "partial".',
|
|
43
53
|
].join('\n');
|
|
44
54
|
}
|
|
45
55
|
export function parseCoverageResponse(text) {
|
|
@@ -22,6 +22,18 @@ function resolveRelevantPageObjects(apiSurface, decision) {
|
|
|
22
22
|
}
|
|
23
23
|
return [...new Set(relevant)].slice(0, 10);
|
|
24
24
|
}
|
|
25
|
+
function buildAssertionPatternsBlock(patterns) {
|
|
26
|
+
if (!patterns || patterns.length === 0) {
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
return [
|
|
30
|
+
'REQUIRED ASSERTION PATTERNS:',
|
|
31
|
+
'Your tests MUST include assertions that verify each of these behaviors.',
|
|
32
|
+
'Do NOT just check element visibility — verify the actual business outcome.',
|
|
33
|
+
...patterns.map((p) => ` - [${p.type}] ${p.pattern}`),
|
|
34
|
+
'',
|
|
35
|
+
];
|
|
36
|
+
}
|
|
25
37
|
export function buildGenerationPrompt(ctx) {
|
|
26
38
|
const profile = ctx.profile;
|
|
27
39
|
const isMM = profile ? isMattermostProfile(profile) : true;
|
|
@@ -122,6 +134,7 @@ export function buildGenerationPrompt(ctx) {
|
|
|
122
134
|
'USER ACTIONS:',
|
|
123
135
|
ctx.decision.userActions.map((a) => ` - ${sanitizeForPrompt(a)}`).join('\n') || ' (none specified)',
|
|
124
136
|
'',
|
|
137
|
+
...buildAssertionPatternsBlock(ctx.decision.assertionPatterns),
|
|
125
138
|
'AVAILABLE PAGE OBJECTS AND METHODS:',
|
|
126
139
|
apiBlock,
|
|
127
140
|
existingBlock,
|
|
@@ -157,6 +170,8 @@ function buildApiTestPrompt(ctx, profile, scenariosBlock, routeFamilyTag) {
|
|
|
157
170
|
'',
|
|
158
171
|
'USER ACTIONS:',
|
|
159
172
|
ctx.decision.userActions.map((a) => ` - ${sanitizeForPrompt(a)}`).join('\n') || ' (none specified)',
|
|
173
|
+
'',
|
|
174
|
+
...buildAssertionPatternsBlock(ctx.decision.assertionPatterns),
|
|
160
175
|
existingBlock,
|
|
161
176
|
'',
|
|
162
177
|
'MANDATORY RULES:',
|
|
@@ -246,18 +261,37 @@ const BUILT_IN_METHODS = new Set([
|
|
|
246
261
|
]);
|
|
247
262
|
/**
|
|
248
263
|
* Returns method names that appear in generated code but do not exist in the API surface.
|
|
249
|
-
*
|
|
264
|
+
* Detects all call patterns: await X.Y(), X.Y(), const z = X.Y(), chained calls.
|
|
250
265
|
*/
|
|
251
266
|
export function detectHallucinatedMethods(code, apiSurface) {
|
|
252
267
|
const allMethods = new Set(apiSurface.pageObjects.flatMap((po) => po.methods.map((m) => m.name)));
|
|
253
|
-
const suspected =
|
|
254
|
-
const
|
|
268
|
+
const suspected = new Set();
|
|
269
|
+
const callPatterns = [
|
|
270
|
+
/\bawait\s+\w+\.([a-zA-Z_]\w*)\s*\(/g,
|
|
271
|
+
/\b(?:const|let|var)\s+\w+\s*=\s*\w+\.([a-zA-Z_]\w*)\s*\(/g,
|
|
272
|
+
/\b\w+Page\.([a-zA-Z_]\w*)\s*\(/g, // any *Page object (channelsPage, settingsPage, etc.)
|
|
273
|
+
/\b(?:pw|page|this)\.\w*\.?([a-zA-Z_]\w*)\s*\(/g,
|
|
274
|
+
];
|
|
275
|
+
const generalCallRe = /\b[a-zA-Z_]\w*\.([a-zA-Z_]\w*)\s*\(/g;
|
|
276
|
+
for (const re of callPatterns) {
|
|
277
|
+
let match;
|
|
278
|
+
while ((match = re.exec(code)) !== null) {
|
|
279
|
+
const methodName = match[1];
|
|
280
|
+
if (!BUILT_IN_METHODS.has(methodName) && !allMethods.has(methodName) && methodName.length > 3) {
|
|
281
|
+
suspected.add(methodName);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
255
285
|
let match;
|
|
256
|
-
while ((match =
|
|
286
|
+
while ((match = generalCallRe.exec(code)) !== null) {
|
|
257
287
|
const methodName = match[1];
|
|
258
|
-
if (!BUILT_IN_METHODS.has(methodName) &&
|
|
259
|
-
|
|
288
|
+
if (!BUILT_IN_METHODS.has(methodName) &&
|
|
289
|
+
!allMethods.has(methodName) &&
|
|
290
|
+
methodName.length > 3 &&
|
|
291
|
+
!methodName.startsWith('to') &&
|
|
292
|
+
!methodName.startsWith('get')) {
|
|
293
|
+
suspected.add(methodName);
|
|
260
294
|
}
|
|
261
295
|
}
|
|
262
|
-
return [...
|
|
296
|
+
return [...suspected];
|
|
263
297
|
}
|
|
@@ -23,6 +23,11 @@ export function computeConfidence(check) {
|
|
|
23
23
|
if (check.hasExistingSpecCited) {
|
|
24
24
|
score += 15;
|
|
25
25
|
}
|
|
26
|
+
// Historical failure correlation: if this file historically causes test failures,
|
|
27
|
+
// we're more confident it needs testing now
|
|
28
|
+
if (check.historyBoost) {
|
|
29
|
+
score += check.historyBoost;
|
|
30
|
+
}
|
|
26
31
|
return Math.min(100, score);
|
|
27
32
|
}
|
|
28
33
|
export function classifyConfidence(confidence) {
|
|
@@ -1,11 +1,21 @@
|
|
|
1
|
+
export interface MethodParam {
|
|
2
|
+
name: string;
|
|
3
|
+
type?: string;
|
|
4
|
+
optional?: boolean;
|
|
5
|
+
}
|
|
1
6
|
export interface MethodSignature {
|
|
2
7
|
name: string;
|
|
3
8
|
kind: 'method' | 'property' | 'getter';
|
|
9
|
+
params?: MethodParam[];
|
|
10
|
+
returnType?: string;
|
|
11
|
+
async?: boolean;
|
|
4
12
|
}
|
|
5
13
|
export interface PageObjectSurface {
|
|
6
14
|
className: string;
|
|
7
15
|
file: string;
|
|
8
16
|
methods: MethodSignature[];
|
|
17
|
+
/** Base class name if the class extends another */
|
|
18
|
+
extends?: string;
|
|
9
19
|
}
|
|
10
20
|
export interface ApiSurfaceCatalog {
|
|
11
21
|
pageObjects: PageObjectSurface[];
|
|
@@ -16,6 +26,8 @@ export interface ApiSurfaceConfig {
|
|
|
16
26
|
pageObjectsDir?: string;
|
|
17
27
|
componentsDir?: string;
|
|
18
28
|
cachePath?: string;
|
|
29
|
+
/** Use fast regex extraction instead of TypeScript AST (fallback mode) */
|
|
30
|
+
useRegexFallback?: boolean;
|
|
19
31
|
}
|
|
20
32
|
export declare function buildApiSurface(testsRoot: string, config?: ApiSurfaceConfig): ApiSurfaceCatalog;
|
|
21
33
|
export declare function loadOrBuildApiSurface(testsRoot: string, config?: ApiSurfaceConfig): ApiSurfaceCatalog;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api_surface.d.ts","sourceRoot":"","sources":["../../src/knowledge/api_surface.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"api_surface.d.ts","sourceRoot":"","sources":["../../src/knowledge/api_surface.ts"],"names":[],"mappings":"AAOA,MAAM,WAAW,WAAW;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,MAAM,WAAW,eAAe;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,QAAQ,GAAG,UAAU,GAAG,QAAQ,CAAC;IACvC,MAAM,CAAC,EAAE,WAAW,EAAE,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,iBAAiB;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,eAAe,EAAE,CAAC;IAC3B,mDAAmD;IACnD,OAAO,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,iBAAiB;IAC9B,WAAW,EAAE,iBAAiB,EAAE,CAAC;IACjC,WAAW,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,gBAAgB;IAC7B,OAAO,EAAE,OAAO,CAAC;IACjB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,0EAA0E;IAC1E,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC9B;AAgVD,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,gBAAgB,GAAG,iBAAiB,CAqC/F;AAED,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,gBAAgB,GAAG,iBAAiB,CAgCrG;AAED,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,iBAAiB,EAAE,SAAS,EAAE,MAAM,GAAG,eAAe,EAAE,CAGxG;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,iBAAiB,EAAE,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAG7G;AAED,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,iBAAiB,EAAE,UAAU,EAAE,MAAM,EAAE,GAAG,MAAM,CA0BlG"}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
3
3
|
// See LICENSE.txt for license information.
|
|
4
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
5
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
6
|
+
};
|
|
4
7
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
8
|
exports.buildApiSurface = buildApiSurface;
|
|
6
9
|
exports.loadOrBuildApiSurface = loadOrBuildApiSurface;
|
|
@@ -9,39 +12,233 @@ exports.validateMethodCall = validateMethodCall;
|
|
|
9
12
|
exports.formatApiSurfaceForPrompt = formatApiSurfaceForPrompt;
|
|
10
13
|
const fs_1 = require("fs");
|
|
11
14
|
const path_1 = require("path");
|
|
15
|
+
const typescript_1 = __importDefault(require("typescript"));
|
|
16
|
+
// ── TypeScript AST-based extraction ────────────────────────
|
|
17
|
+
function extractMethodsFromAST(sourceFile, checker) {
|
|
18
|
+
const surfaces = [];
|
|
19
|
+
typescript_1.default.forEachChild(sourceFile, (node) => {
|
|
20
|
+
if (!typescript_1.default.isClassDeclaration(node) || !node.name) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const className = node.name.text;
|
|
24
|
+
const methods = [];
|
|
25
|
+
const seen = new Set();
|
|
26
|
+
// Get base class name if extends
|
|
27
|
+
let extendsName;
|
|
28
|
+
if (node.heritageClauses) {
|
|
29
|
+
for (const clause of node.heritageClauses) {
|
|
30
|
+
if (clause.token === typescript_1.default.SyntaxKind.ExtendsKeyword && clause.types.length > 0) {
|
|
31
|
+
const expr = clause.types[0].expression;
|
|
32
|
+
if (typescript_1.default.isIdentifier(expr)) {
|
|
33
|
+
extendsName = expr.text;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
for (const member of node.members) {
|
|
39
|
+
// Skip constructor
|
|
40
|
+
if (typescript_1.default.isConstructorDeclaration(member)) {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
// Skip private/protected members
|
|
44
|
+
const modifiers = typescript_1.default.canHaveModifiers(member) ? typescript_1.default.getModifiers(member) : undefined;
|
|
45
|
+
if (modifiers?.some((m) => m.kind === typescript_1.default.SyntaxKind.PrivateKeyword || m.kind === typescript_1.default.SyntaxKind.ProtectedKeyword)) {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
const name = member.name && typescript_1.default.isIdentifier(member.name) ? member.name.text : null;
|
|
49
|
+
if (!name || name.startsWith('_') || seen.has(name)) {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
seen.add(name);
|
|
53
|
+
if (typescript_1.default.isMethodDeclaration(member)) {
|
|
54
|
+
const isAsync = modifiers?.some((m) => m.kind === typescript_1.default.SyntaxKind.AsyncKeyword) ?? false;
|
|
55
|
+
const params = extractParams(member.parameters, checker);
|
|
56
|
+
const returnType = extractReturnType(member, checker);
|
|
57
|
+
methods.push({
|
|
58
|
+
name,
|
|
59
|
+
kind: 'method',
|
|
60
|
+
async: isAsync ? true : undefined,
|
|
61
|
+
params: params.length > 0 ? params : undefined,
|
|
62
|
+
returnType: returnType || undefined,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
else if (typescript_1.default.isGetAccessorDeclaration(member)) {
|
|
66
|
+
methods.push({ name, kind: 'getter' });
|
|
67
|
+
}
|
|
68
|
+
else if (typescript_1.default.isPropertyDeclaration(member)) {
|
|
69
|
+
// Check if it's an arrow function property (e.g., name = async () => {})
|
|
70
|
+
if (member.initializer && (typescript_1.default.isArrowFunction(member.initializer) || typescript_1.default.isFunctionExpression(member.initializer))) {
|
|
71
|
+
const fn = member.initializer;
|
|
72
|
+
const fnModifiers = typescript_1.default.canHaveModifiers(fn) ? typescript_1.default.getModifiers(fn) : undefined;
|
|
73
|
+
const isAsync = fnModifiers?.some((m) => m.kind === typescript_1.default.SyntaxKind.AsyncKeyword) ?? false;
|
|
74
|
+
const params = extractParams(fn.parameters, checker);
|
|
75
|
+
methods.push({
|
|
76
|
+
name,
|
|
77
|
+
kind: 'method',
|
|
78
|
+
async: isAsync ? true : undefined,
|
|
79
|
+
params: params.length > 0 ? params : undefined,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
methods.push({ name, kind: 'property' });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (methods.length > 0) {
|
|
88
|
+
surfaces.push({
|
|
89
|
+
className,
|
|
90
|
+
file: sourceFile.fileName,
|
|
91
|
+
methods,
|
|
92
|
+
extends: extendsName,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
return surfaces;
|
|
97
|
+
}
|
|
98
|
+
function extractParams(params, checker) {
|
|
99
|
+
return params.map((p) => {
|
|
100
|
+
const name = typescript_1.default.isIdentifier(p.name) ? p.name.text : p.name.getText();
|
|
101
|
+
const optional = p.questionToken !== undefined || p.initializer !== undefined;
|
|
102
|
+
let type;
|
|
103
|
+
if (p.type) {
|
|
104
|
+
type = p.type.getText();
|
|
105
|
+
}
|
|
106
|
+
else if (checker) {
|
|
107
|
+
try {
|
|
108
|
+
const symbol = checker.getSymbolAtLocation(p.name);
|
|
109
|
+
if (symbol) {
|
|
110
|
+
const t = checker.getTypeOfSymbolAtLocation(symbol, p);
|
|
111
|
+
type = checker.typeToString(t);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
// Type inference failure is non-fatal
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return { name, type, optional: optional || undefined };
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
function extractReturnType(method, checker) {
|
|
122
|
+
if (method.type) {
|
|
123
|
+
return method.type.getText();
|
|
124
|
+
}
|
|
125
|
+
if (checker) {
|
|
126
|
+
try {
|
|
127
|
+
const signature = checker.getSignatureFromDeclaration(method);
|
|
128
|
+
if (signature) {
|
|
129
|
+
const returnType = checker.getReturnTypeOfSignature(signature);
|
|
130
|
+
const typeStr = checker.typeToString(returnType);
|
|
131
|
+
// Skip overly verbose inferred types
|
|
132
|
+
if (typeStr.length < 100) {
|
|
133
|
+
return typeStr;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
// Type inference failure is non-fatal
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Extract page objects using the TypeScript Compiler API.
|
|
145
|
+
* Falls back to regex if compilation fails.
|
|
146
|
+
*/
|
|
147
|
+
function extractWithAST(files) {
|
|
148
|
+
if (files.length === 0) {
|
|
149
|
+
return [];
|
|
150
|
+
}
|
|
151
|
+
// Find the nearest tsconfig.json
|
|
152
|
+
const firstDir = (0, path_1.join)(files[0], '..');
|
|
153
|
+
const tsconfigPath = typescript_1.default.findConfigFile(firstDir, typescript_1.default.sys.fileExists, 'tsconfig.json');
|
|
154
|
+
let program;
|
|
155
|
+
if (tsconfigPath) {
|
|
156
|
+
const configFile = typescript_1.default.readConfigFile(tsconfigPath, typescript_1.default.sys.readFile);
|
|
157
|
+
const parsedConfig = typescript_1.default.parseJsonConfigFileContent(configFile.config, typescript_1.default.sys, (0, path_1.join)(tsconfigPath, '..'));
|
|
158
|
+
// Only compile the files we care about, using the config's compiler options
|
|
159
|
+
program = typescript_1.default.createProgram(files, parsedConfig.options);
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
program = typescript_1.default.createProgram(files, {
|
|
163
|
+
target: typescript_1.default.ScriptTarget.ES2022,
|
|
164
|
+
module: typescript_1.default.ModuleKind.ESNext,
|
|
165
|
+
moduleResolution: typescript_1.default.ModuleResolutionKind.Bundler,
|
|
166
|
+
strict: true,
|
|
167
|
+
allowJs: false,
|
|
168
|
+
noEmit: true,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
const checker = program.getTypeChecker();
|
|
172
|
+
const surfaces = [];
|
|
173
|
+
for (const filePath of files) {
|
|
174
|
+
const sourceFile = program.getSourceFile(filePath);
|
|
175
|
+
if (!sourceFile) {
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
surfaces.push(...extractMethodsFromAST(sourceFile, checker));
|
|
179
|
+
}
|
|
180
|
+
// Resolve inherited methods: if class extends another class in the catalog,
|
|
181
|
+
// merge parent methods that the child doesn't override
|
|
182
|
+
resolveInheritance(surfaces);
|
|
183
|
+
return surfaces;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Merge parent class methods into child classes that extend them.
|
|
187
|
+
*/
|
|
188
|
+
function resolveInheritance(surfaces) {
|
|
189
|
+
const byName = new Map(surfaces.map((s) => [s.className, s]));
|
|
190
|
+
for (const surface of surfaces) {
|
|
191
|
+
if (!surface.extends) {
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
const parent = byName.get(surface.extends);
|
|
195
|
+
if (!parent) {
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
const childMethodNames = new Set(surface.methods.map((m) => m.name));
|
|
199
|
+
for (const parentMethod of parent.methods) {
|
|
200
|
+
if (!childMethodNames.has(parentMethod.name)) {
|
|
201
|
+
surface.methods.push({ ...parentMethod });
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
// ── Regex-based extraction (fallback) ──────────────────────
|
|
12
207
|
const RESERVED_WORDS = new Set([
|
|
13
208
|
'private', 'protected', 'static', 'abstract', 'override',
|
|
14
209
|
'if', 'for', 'while', 'switch', 'return',
|
|
15
210
|
'const', 'let', 'var', 'import', 'export',
|
|
16
211
|
'class', 'type', 'interface', 'constructor',
|
|
17
212
|
]);
|
|
18
|
-
function
|
|
213
|
+
function extractMethodsFromRegex(content) {
|
|
19
214
|
const methods = [];
|
|
20
215
|
const seen = new Set();
|
|
21
|
-
// Match async method declarations: async methodName(
|
|
22
216
|
const asyncMethodRe = /(?:async\s+)([a-zA-Z_]\w*)\s*\(/g;
|
|
23
217
|
let match;
|
|
24
218
|
while ((match = asyncMethodRe.exec(content)) !== null) {
|
|
25
219
|
const name = match[1];
|
|
26
|
-
if (RESERVED_WORDS.has(name)) {
|
|
27
|
-
continue;
|
|
28
|
-
}
|
|
29
|
-
if (!seen.has(name)) {
|
|
220
|
+
if (!RESERVED_WORDS.has(name) && !seen.has(name)) {
|
|
30
221
|
seen.add(name);
|
|
31
|
-
methods.push({ name, kind: 'method' });
|
|
222
|
+
methods.push({ name, kind: 'method', async: true });
|
|
32
223
|
}
|
|
33
224
|
}
|
|
34
|
-
// Match non-async public method patterns
|
|
35
225
|
const methodRe = /^\s+(?:readonly\s+)?([a-zA-Z_]\w*)\s*(?:\(|=\s*(?:async\s*)?\()/gm;
|
|
36
226
|
while ((match = methodRe.exec(content)) !== null) {
|
|
37
227
|
const name = match[1];
|
|
38
|
-
if (RESERVED_WORDS.has(name)
|
|
39
|
-
|
|
228
|
+
if (!RESERVED_WORDS.has(name) && !seen.has(name)) {
|
|
229
|
+
seen.add(name);
|
|
230
|
+
const isAsync = match[0].includes('async');
|
|
231
|
+
methods.push({ name, kind: 'method', async: isAsync ? true : undefined });
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
const arrowRe = /^\s+([a-zA-Z_]\w*)\s*=\s*(async\s+)?(?:\([^)]*\)|[a-zA-Z_]\w*)\s*=>/gm;
|
|
235
|
+
while ((match = arrowRe.exec(content)) !== null) {
|
|
236
|
+
const name = match[1];
|
|
237
|
+
if (!RESERVED_WORDS.has(name) && !seen.has(name)) {
|
|
238
|
+
seen.add(name);
|
|
239
|
+
methods.push({ name, kind: 'method', async: match[2] ? true : undefined });
|
|
40
240
|
}
|
|
41
|
-
seen.add(name);
|
|
42
|
-
methods.push({ name, kind: 'method' });
|
|
43
241
|
}
|
|
44
|
-
// Match getter patterns: get propertyName()
|
|
45
242
|
const getterRe = /\bget\s+([a-zA-Z_]\w*)\s*\(\)/g;
|
|
46
243
|
while ((match = getterRe.exec(content)) !== null) {
|
|
47
244
|
const name = match[1];
|
|
@@ -50,15 +247,13 @@ function extractMethodsFromSource(content) {
|
|
|
50
247
|
methods.push({ name, kind: 'getter' });
|
|
51
248
|
}
|
|
52
249
|
}
|
|
53
|
-
// Match readonly property declarations
|
|
54
250
|
const propRe = /^\s+(?:readonly\s+)?([a-zA-Z_]\w*)\s*[:=]/gm;
|
|
55
251
|
while ((match = propRe.exec(content)) !== null) {
|
|
56
252
|
const name = match[1];
|
|
57
|
-
if (RESERVED_WORDS.has(name)
|
|
58
|
-
|
|
253
|
+
if (!RESERVED_WORDS.has(name) && !seen.has(name)) {
|
|
254
|
+
seen.add(name);
|
|
255
|
+
methods.push({ name, kind: 'property' });
|
|
59
256
|
}
|
|
60
|
-
seen.add(name);
|
|
61
|
-
methods.push({ name, kind: 'property' });
|
|
62
257
|
}
|
|
63
258
|
return methods;
|
|
64
259
|
}
|
|
@@ -66,7 +261,27 @@ function extractClassName(content) {
|
|
|
66
261
|
const match = content.match(/(?:export\s+)?class\s+(\w+)/);
|
|
67
262
|
return match ? match[1] : null;
|
|
68
263
|
}
|
|
69
|
-
|
|
264
|
+
// ── Directory scanning ─────────────────────────────────────
|
|
265
|
+
function collectTypeScriptFiles(dir) {
|
|
266
|
+
const files = [];
|
|
267
|
+
if (!(0, fs_1.existsSync)(dir)) {
|
|
268
|
+
return files;
|
|
269
|
+
}
|
|
270
|
+
const entries = (0, fs_1.readdirSync)(dir, { withFileTypes: true });
|
|
271
|
+
for (const entry of entries) {
|
|
272
|
+
const fullPath = (0, path_1.join)(dir, entry.name);
|
|
273
|
+
if (entry.isDirectory()) {
|
|
274
|
+
files.push(...collectTypeScriptFiles(fullPath));
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
const ext = (0, path_1.extname)(entry.name);
|
|
278
|
+
if (ext === '.ts' || ext === '.tsx') {
|
|
279
|
+
files.push(fullPath);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return files;
|
|
283
|
+
}
|
|
284
|
+
function scanDirectoryWithRegex(dir) {
|
|
70
285
|
const surfaces = [];
|
|
71
286
|
if (!(0, fs_1.existsSync)(dir)) {
|
|
72
287
|
return surfaces;
|
|
@@ -75,29 +290,22 @@ function scanDirectory(dir) {
|
|
|
75
290
|
for (const entry of entries) {
|
|
76
291
|
const fullPath = (0, path_1.join)(dir, entry.name);
|
|
77
292
|
if (entry.isDirectory()) {
|
|
78
|
-
surfaces.push(...
|
|
293
|
+
surfaces.push(...scanDirectoryWithRegex(fullPath));
|
|
79
294
|
continue;
|
|
80
295
|
}
|
|
81
296
|
const ext = (0, path_1.extname)(entry.name);
|
|
82
297
|
if (ext !== '.ts' && ext !== '.tsx') {
|
|
83
298
|
continue;
|
|
84
299
|
}
|
|
85
|
-
if (entry.name === 'index.ts' || entry.name === 'index.tsx') {
|
|
86
|
-
continue;
|
|
87
|
-
}
|
|
88
300
|
try {
|
|
89
301
|
const content = (0, fs_1.readFileSync)(fullPath, 'utf-8');
|
|
90
302
|
const className = extractClassName(content);
|
|
91
303
|
if (!className) {
|
|
92
304
|
continue;
|
|
93
305
|
}
|
|
94
|
-
const extractedMethods =
|
|
306
|
+
const extractedMethods = extractMethodsFromRegex(content);
|
|
95
307
|
if (extractedMethods.length > 0) {
|
|
96
|
-
surfaces.push({
|
|
97
|
-
className,
|
|
98
|
-
file: fullPath,
|
|
99
|
-
methods: extractedMethods,
|
|
100
|
-
});
|
|
308
|
+
surfaces.push({ className, file: fullPath, methods: extractedMethods });
|
|
101
309
|
}
|
|
102
310
|
}
|
|
103
311
|
catch {
|
|
@@ -106,6 +314,7 @@ function scanDirectory(dir) {
|
|
|
106
314
|
}
|
|
107
315
|
return surfaces;
|
|
108
316
|
}
|
|
317
|
+
// ── Public API ──────────────────────────────────────────────
|
|
109
318
|
function buildApiSurface(testsRoot, config) {
|
|
110
319
|
const pageObjectsDir = config?.pageObjectsDir
|
|
111
320
|
? (0, path_1.join)(testsRoot, config.pageObjectsDir)
|
|
@@ -113,10 +322,30 @@ function buildApiSurface(testsRoot, config) {
|
|
|
113
322
|
const componentsDir = config?.componentsDir
|
|
114
323
|
? (0, path_1.join)(testsRoot, config.componentsDir)
|
|
115
324
|
: (0, path_1.join)(testsRoot, 'lib', 'src', 'ui', 'components');
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
325
|
+
let pageObjects;
|
|
326
|
+
if (config?.useRegexFallback) {
|
|
327
|
+
pageObjects = [
|
|
328
|
+
...scanDirectoryWithRegex(pageObjectsDir),
|
|
329
|
+
...scanDirectoryWithRegex(componentsDir),
|
|
330
|
+
];
|
|
331
|
+
}
|
|
332
|
+
else {
|
|
333
|
+
// Use TypeScript AST — full type info, inheritance, params
|
|
334
|
+
const allFiles = [
|
|
335
|
+
...collectTypeScriptFiles(pageObjectsDir),
|
|
336
|
+
...collectTypeScriptFiles(componentsDir),
|
|
337
|
+
];
|
|
338
|
+
try {
|
|
339
|
+
pageObjects = extractWithAST(allFiles);
|
|
340
|
+
}
|
|
341
|
+
catch {
|
|
342
|
+
// Fall back to regex if AST extraction fails
|
|
343
|
+
pageObjects = [
|
|
344
|
+
...scanDirectoryWithRegex(pageObjectsDir),
|
|
345
|
+
...scanDirectoryWithRegex(componentsDir),
|
|
346
|
+
];
|
|
347
|
+
}
|
|
348
|
+
}
|
|
120
349
|
return {
|
|
121
350
|
pageObjects,
|
|
122
351
|
generatedAt: new Date().toISOString(),
|
|
@@ -175,7 +404,12 @@ function formatApiSurfaceForPrompt(catalog, classNames) {
|
|
|
175
404
|
if (m.kind === 'getter') {
|
|
176
405
|
return ` get ${m.name}()`;
|
|
177
406
|
}
|
|
178
|
-
|
|
407
|
+
const prefix = m.async ? 'async ' : '';
|
|
408
|
+
const paramStr = m.params
|
|
409
|
+
? m.params.map((p) => `${p.name}${p.optional ? '?' : ''}${p.type ? `: ${p.type}` : ''}`).join(', ')
|
|
410
|
+
: '';
|
|
411
|
+
const retStr = m.returnType ? `: ${m.returnType}` : '';
|
|
412
|
+
return ` ${prefix}${m.name}(${paramStr})${retStr}`;
|
|
179
413
|
})
|
|
180
414
|
.join('\n');
|
|
181
415
|
sections.push(`${name}:\n${methodList}`);
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export interface FailureCorrelation {
|
|
2
|
+
/** Source file that was changed */
|
|
3
|
+
changedFile: string;
|
|
4
|
+
/** Spec file that failed */
|
|
5
|
+
specFile: string;
|
|
6
|
+
/** Number of times this correlation has been observed */
|
|
7
|
+
count: number;
|
|
8
|
+
/** ISO timestamp of last observation */
|
|
9
|
+
lastSeen: string;
|
|
10
|
+
}
|
|
11
|
+
export interface FailureHistory {
|
|
12
|
+
correlations: FailureCorrelation[];
|
|
13
|
+
/** Total runs recorded */
|
|
14
|
+
totalRuns: number;
|
|
15
|
+
/** ISO timestamp of last update */
|
|
16
|
+
updatedAt: string;
|
|
17
|
+
}
|
|
18
|
+
export declare function loadFailureHistory(testsRoot: string): FailureHistory;
|
|
19
|
+
export declare function saveFailureHistory(testsRoot: string, history: FailureHistory): void;
|
|
20
|
+
/**
|
|
21
|
+
* Record that a set of changed files caused a set of spec failures.
|
|
22
|
+
* Call this after a test run where failures were observed.
|
|
23
|
+
*/
|
|
24
|
+
export declare function recordFailures(history: FailureHistory, changedFiles: string[], failedSpecs: string[]): FailureHistory;
|
|
25
|
+
/**
|
|
26
|
+
* Get a confidence boost (0-20) for a file based on historical failure patterns.
|
|
27
|
+
* A file that historically causes test failures gets a higher confidence boost
|
|
28
|
+
* when detected as impacted, meaning the system is more confident it needs testing.
|
|
29
|
+
*/
|
|
30
|
+
export declare function getConfidenceBoost(history: FailureHistory, changedFile: string): number;
|
|
31
|
+
/**
|
|
32
|
+
* Get the most likely failing specs for a set of changed files, based on history.
|
|
33
|
+
* Returns specs sorted by correlation strength (count * recency).
|
|
34
|
+
*/
|
|
35
|
+
export declare function getPredictedFailures(history: FailureHistory, changedFiles: string[], limit?: number): Array<{
|
|
36
|
+
specFile: string;
|
|
37
|
+
score: number;
|
|
38
|
+
}>;
|
|
39
|
+
//# sourceMappingURL=failure_history.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"failure_history.d.ts","sourceRoot":"","sources":["../../src/knowledge/failure_history.ts"],"names":[],"mappings":"AAcA,MAAM,WAAW,kBAAkB;IAC/B,mCAAmC;IACnC,WAAW,EAAE,MAAM,CAAC;IACpB,4BAA4B;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,yDAAyD;IACzD,KAAK,EAAE,MAAM,CAAC;IACd,wCAAwC;IACxC,QAAQ,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,cAAc;IAC3B,YAAY,EAAE,kBAAkB,EAAE,CAAC;IACnC,0BAA0B;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,mCAAmC;IACnC,SAAS,EAAE,MAAM,CAAC;CACrB;AAQD,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,cAAc,CAcpE;AAED,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,cAAc,GAAG,IAAI,CAYnF;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAC1B,OAAO,EAAE,cAAc,EACvB,YAAY,EAAE,MAAM,EAAE,EACtB,WAAW,EAAE,MAAM,EAAE,GACtB,cAAc,CA8BhB;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,CAevF;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAChC,OAAO,EAAE,cAAc,EACvB,YAAY,EAAE,MAAM,EAAE,EACtB,KAAK,SAAK,GACX,KAAK,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAC,CAAC,CAoB1C"}
|