@yasserkhanorg/e2e-agents 1.5.0 → 1.6.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/cli/commands/train.d.ts.map +1 -1
- package/dist/cli/commands/train.js +31 -4
- package/dist/cli/parse_args.d.ts.map +1 -1
- package/dist/cli/parse_args.js +1 -0
- package/dist/cli/types.d.ts +1 -0
- package/dist/cli/types.d.ts.map +1 -1
- package/dist/esm/cli/commands/train.js +31 -4
- package/dist/esm/cli/parse_args.js +1 -0
- package/dist/esm/training/enricher.js +71 -7
- package/dist/esm/training/merger.js +77 -10
- package/dist/esm/training/scanner.js +368 -2
- package/dist/training/enricher.d.ts +3 -1
- package/dist/training/enricher.d.ts.map +1 -1
- package/dist/training/enricher.js +71 -7
- package/dist/training/merger.d.ts +11 -1
- package/dist/training/merger.d.ts.map +1 -1
- package/dist/training/merger.js +77 -10
- package/dist/training/scanner.d.ts +15 -2
- package/dist/training/scanner.d.ts.map +1 -1
- package/dist/training/scanner.js +370 -2
- package/dist/training/types.d.ts +4 -0
- package/dist/training/types.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"train.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/train.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"train.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/train.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,aAAa,CAAC;AAqJ5C,wBAAsB,eAAe,CAAC,IAAI,EAAE,UAAU,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAqL1F"}
|
|
@@ -36,6 +36,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
36
36
|
})();
|
|
37
37
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
38
38
|
exports.runTrainCommand = runTrainCommand;
|
|
39
|
+
const child_process_1 = require("child_process");
|
|
39
40
|
const fs_1 = require("fs");
|
|
40
41
|
const path_1 = require("path");
|
|
41
42
|
const readline = __importStar(require("readline"));
|
|
@@ -111,9 +112,32 @@ function resolveTrainOptions(args, autoConfig) {
|
|
|
111
112
|
if (!inApp && !inTests) {
|
|
112
113
|
throw new TrainError(`Output path must be within the project root or tests root: ${resolvedOutputPath}`);
|
|
113
114
|
}
|
|
115
|
+
// Discover git repo root for monorepo-aware scanning and validation
|
|
116
|
+
let gitRepoRoot;
|
|
117
|
+
try {
|
|
118
|
+
gitRepoRoot = (0, child_process_1.execFileSync)('git', ['rev-parse', '--show-toplevel'], {
|
|
119
|
+
cwd: resolvedAppPath,
|
|
120
|
+
encoding: 'utf-8',
|
|
121
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
122
|
+
}).trim();
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
// Not a git repo or git not available
|
|
126
|
+
}
|
|
127
|
+
// Resolve serverRoot: explicit flag, or auto-detect from git repo root
|
|
128
|
+
let serverRoot = args.serverPath;
|
|
129
|
+
if (!serverRoot && gitRepoRoot) {
|
|
130
|
+
const serverDir = (0, path_1.join)(gitRepoRoot, 'server');
|
|
131
|
+
if ((0, fs_1.existsSync)(serverDir)) {
|
|
132
|
+
serverRoot = serverDir;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const resolvedServerRoot = serverRoot ? (0, path_1.resolve)(serverRoot) : undefined;
|
|
114
136
|
return {
|
|
115
137
|
appPath: resolvedAppPath,
|
|
116
138
|
testsRoot: resolvedTestsRoot,
|
|
139
|
+
serverRoot: resolvedServerRoot,
|
|
140
|
+
gitRepoRoot: gitRepoRoot ? (0, path_1.resolve)(gitRepoRoot) : undefined,
|
|
117
141
|
enrich: args.trainEnrich !== false,
|
|
118
142
|
validate: args.trainValidate || false,
|
|
119
143
|
since,
|
|
@@ -158,7 +182,10 @@ async function runTrainCommand(args, autoConfig) {
|
|
|
158
182
|
console.log('');
|
|
159
183
|
// ---------- Phase 1: Deterministic scan ----------
|
|
160
184
|
console.log(' Scanning project structure...');
|
|
161
|
-
|
|
185
|
+
if (opts.serverRoot) {
|
|
186
|
+
console.log(` Server root: ${opts.serverRoot}`);
|
|
187
|
+
}
|
|
188
|
+
const scanResult = (0, scanner_js_1.scanProject)(opts.appPath, opts.testsRoot !== opts.appPath ? opts.testsRoot : undefined, opts.serverRoot);
|
|
162
189
|
console.log(` Found ${scanResult.stats.totalSourceFiles} source files, ${scanResult.stats.totalTestFiles} test files`);
|
|
163
190
|
console.log(` Discovered ${scanResult.families.length} candidate families`);
|
|
164
191
|
if (scanResult.families.length === 0) {
|
|
@@ -177,7 +204,7 @@ async function runTrainCommand(args, autoConfig) {
|
|
|
177
204
|
console.log(` Merge: ${mergeResult.summary}`);
|
|
178
205
|
// ---------- Phase 3: Stale detection ----------
|
|
179
206
|
if (mergeResult.manifest.families.length > 0) {
|
|
180
|
-
const stale = (0, merger_js_1.detectStaleFamilies)(mergeResult.manifest, opts.appPath);
|
|
207
|
+
const stale = (0, merger_js_1.detectStaleFamilies)(mergeResult.manifest, opts.appPath, opts.testsRoot);
|
|
181
208
|
if (stale.length > 0) {
|
|
182
209
|
console.log('');
|
|
183
210
|
console.log(` Stale families detected (${stale.length}):`);
|
|
@@ -207,7 +234,7 @@ async function runTrainCommand(args, autoConfig) {
|
|
|
207
234
|
console.log(' Enriching with LLM...');
|
|
208
235
|
try {
|
|
209
236
|
const provider = await provider_factory_js_1.LLMProviderFactory.createFromEnv();
|
|
210
|
-
const enrichResult = await (0, enricher_js_1.enrichFamilies)(mergeResult.manifest.families, scanResult.families, opts.appPath, provider, opts.budgetUSD);
|
|
237
|
+
const enrichResult = await (0, enricher_js_1.enrichFamilies)(mergeResult.manifest.families, scanResult.families, opts.appPath, provider, opts.budgetUSD, opts.testsRoot !== opts.appPath ? opts.testsRoot : undefined);
|
|
211
238
|
mergeResult.manifest.families = enrichResult.enrichedFamilies;
|
|
212
239
|
console.log(` Enriched ${enrichResult.enrichedFamilies.length} families (${enrichResult.tokensUsed} tokens, ~$${enrichResult.costUSD})`);
|
|
213
240
|
if (enrichResult.skippedFamilies.length > 0) {
|
|
@@ -291,7 +318,7 @@ async function runTrainCommand(args, autoConfig) {
|
|
|
291
318
|
else {
|
|
292
319
|
console.log('');
|
|
293
320
|
console.log(` Validating against git history (${opts.since})...`);
|
|
294
|
-
const commits = (0, validator_js_1.getCommitFiles)(opts.appPath, opts.since);
|
|
321
|
+
const commits = (0, validator_js_1.getCommitFiles)(opts.gitRepoRoot || opts.appPath, opts.since);
|
|
295
322
|
if (commits.length === 0) {
|
|
296
323
|
console.log(' No commits found in range.');
|
|
297
324
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"parse_args.d.ts","sourceRoot":"","sources":["../../src/cli/parse_args.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAU,UAAU,EAAC,MAAM,YAAY,CAAC;AAEpD,eAAO,MAAM,iBAAiB,UAA8D,CAAC;AAE7F,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAmBlF;AAED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM,GAAG,SAAS,CAmBtE;
|
|
1
|
+
{"version":3,"file":"parse_args.d.ts","sourceRoot":"","sources":["../../src/cli/parse_args.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAU,UAAU,EAAC,MAAM,YAAY,CAAC;AAEpD,eAAO,MAAM,iBAAiB,UAA8D,CAAC;AAE7F,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAmBlF;AAED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM,GAAG,SAAS,CAmBtE;AA4ID,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,UAAU,CA4EpD"}
|
package/dist/cli/parse_args.js
CHANGED
|
@@ -101,6 +101,7 @@ const FLAGS = {
|
|
|
101
101
|
'--heal-report': { key: 'analyzeHealReport', type: 'string' },
|
|
102
102
|
'--flow-catalog': { key: 'flowCatalogPath', type: 'string' },
|
|
103
103
|
'--output': { key: 'trainOutput', type: 'string' },
|
|
104
|
+
'--server-path': { key: 'serverPath', type: 'string' },
|
|
104
105
|
// -- number flags (with isFinite guard) --
|
|
105
106
|
'--pipeline-scenarios': { key: 'pipelineScenarios', type: 'number' },
|
|
106
107
|
'--time': { key: 'timeLimitMinutes', type: 'number' },
|
package/dist/cli/types.d.ts
CHANGED
package/dist/cli/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/cli/types.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAC,eAAe,EAAE,aAAa,EAAC,MAAM,oBAAoB,CAAC;AAEvE,MAAM,MAAM,OAAO,GACf,MAAM,GACJ,QAAQ,GACR,MAAM,GACN,MAAM,GACN,SAAS,GACT,UAAU,GACV,0BAA0B,GAC1B,UAAU,GACV,sBAAsB,GACtB,qBAAqB,GACrB,SAAS,GACT,YAAY,GACZ,OAAO,CAAC;AAEd,MAAM,WAAW,UAAU;IACvB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,eAAe,CAAC;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,aAAa,CAAC;IAC1B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,eAAe,CAAC,EAAE,QAAQ,GAAG,UAAU,GAAG,SAAS,GAAG,QAAQ,CAAC;IAC/D,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,wBAAwB,CAAC,EAAE,OAAO,CAAC;IACnC,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,yBAAyB,CAAC,EAAE,MAAM,CAAC;IACnC,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,mBAAmB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC/B,qBAAqB,CAAC,EAAE,UAAU,GAAG,MAAM,GAAG,OAAO,CAAC;IACtD,kBAAkB,CAAC,EAAE,KAAK,CAAC,SAAS,GAAG,gBAAgB,GAAG,eAAe,CAAC,CAAC;IAC3E,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,6BAA6B,CAAC,EAAE,MAAM,CAAC;IACvC,2BAA2B,CAAC,EAAE,MAAM,CAAC;IACrC,4BAA4B,CAAC,EAAE,MAAM,CAAC;IACtC,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,2BAA2B,CAAC,EAAE,MAAM,CAAC;IACrC,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,OAAO,CAAC;IACf,IAAI,EAAE,OAAO,CAAC;IACd,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,wBAAwB,CAAC,EAAE,MAAM,CAAC;IAClC,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAG3B,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/cli/types.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
2
2
|
// See LICENSE.txt for license information.
|
|
3
|
+
import { execFileSync } from 'child_process';
|
|
3
4
|
import { existsSync, mkdirSync, renameSync, writeFileSync } from 'fs';
|
|
4
5
|
import { dirname, join, resolve } from 'path';
|
|
5
6
|
import * as readline from 'readline';
|
|
@@ -75,9 +76,32 @@ function resolveTrainOptions(args, autoConfig) {
|
|
|
75
76
|
if (!inApp && !inTests) {
|
|
76
77
|
throw new TrainError(`Output path must be within the project root or tests root: ${resolvedOutputPath}`);
|
|
77
78
|
}
|
|
79
|
+
// Discover git repo root for monorepo-aware scanning and validation
|
|
80
|
+
let gitRepoRoot;
|
|
81
|
+
try {
|
|
82
|
+
gitRepoRoot = execFileSync('git', ['rev-parse', '--show-toplevel'], {
|
|
83
|
+
cwd: resolvedAppPath,
|
|
84
|
+
encoding: 'utf-8',
|
|
85
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
86
|
+
}).trim();
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
// Not a git repo or git not available
|
|
90
|
+
}
|
|
91
|
+
// Resolve serverRoot: explicit flag, or auto-detect from git repo root
|
|
92
|
+
let serverRoot = args.serverPath;
|
|
93
|
+
if (!serverRoot && gitRepoRoot) {
|
|
94
|
+
const serverDir = join(gitRepoRoot, 'server');
|
|
95
|
+
if (existsSync(serverDir)) {
|
|
96
|
+
serverRoot = serverDir;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const resolvedServerRoot = serverRoot ? resolve(serverRoot) : undefined;
|
|
78
100
|
return {
|
|
79
101
|
appPath: resolvedAppPath,
|
|
80
102
|
testsRoot: resolvedTestsRoot,
|
|
103
|
+
serverRoot: resolvedServerRoot,
|
|
104
|
+
gitRepoRoot: gitRepoRoot ? resolve(gitRepoRoot) : undefined,
|
|
81
105
|
enrich: args.trainEnrich !== false,
|
|
82
106
|
validate: args.trainValidate || false,
|
|
83
107
|
since,
|
|
@@ -122,7 +146,10 @@ export async function runTrainCommand(args, autoConfig) {
|
|
|
122
146
|
console.log('');
|
|
123
147
|
// ---------- Phase 1: Deterministic scan ----------
|
|
124
148
|
console.log(' Scanning project structure...');
|
|
125
|
-
|
|
149
|
+
if (opts.serverRoot) {
|
|
150
|
+
console.log(` Server root: ${opts.serverRoot}`);
|
|
151
|
+
}
|
|
152
|
+
const scanResult = scanProject(opts.appPath, opts.testsRoot !== opts.appPath ? opts.testsRoot : undefined, opts.serverRoot);
|
|
126
153
|
console.log(` Found ${scanResult.stats.totalSourceFiles} source files, ${scanResult.stats.totalTestFiles} test files`);
|
|
127
154
|
console.log(` Discovered ${scanResult.families.length} candidate families`);
|
|
128
155
|
if (scanResult.families.length === 0) {
|
|
@@ -141,7 +168,7 @@ export async function runTrainCommand(args, autoConfig) {
|
|
|
141
168
|
console.log(` Merge: ${mergeResult.summary}`);
|
|
142
169
|
// ---------- Phase 3: Stale detection ----------
|
|
143
170
|
if (mergeResult.manifest.families.length > 0) {
|
|
144
|
-
const stale = detectStaleFamilies(mergeResult.manifest, opts.appPath);
|
|
171
|
+
const stale = detectStaleFamilies(mergeResult.manifest, opts.appPath, opts.testsRoot);
|
|
145
172
|
if (stale.length > 0) {
|
|
146
173
|
console.log('');
|
|
147
174
|
console.log(` Stale families detected (${stale.length}):`);
|
|
@@ -171,7 +198,7 @@ export async function runTrainCommand(args, autoConfig) {
|
|
|
171
198
|
console.log(' Enriching with LLM...');
|
|
172
199
|
try {
|
|
173
200
|
const provider = await LLMProviderFactory.createFromEnv();
|
|
174
|
-
const enrichResult = await enrichFamilies(mergeResult.manifest.families, scanResult.families, opts.appPath, provider, opts.budgetUSD);
|
|
201
|
+
const enrichResult = await enrichFamilies(mergeResult.manifest.families, scanResult.families, opts.appPath, provider, opts.budgetUSD, opts.testsRoot !== opts.appPath ? opts.testsRoot : undefined);
|
|
175
202
|
mergeResult.manifest.families = enrichResult.enrichedFamilies;
|
|
176
203
|
console.log(` Enriched ${enrichResult.enrichedFamilies.length} families (${enrichResult.tokensUsed} tokens, ~$${enrichResult.costUSD})`);
|
|
177
204
|
if (enrichResult.skippedFamilies.length > 0) {
|
|
@@ -255,7 +282,7 @@ export async function runTrainCommand(args, autoConfig) {
|
|
|
255
282
|
else {
|
|
256
283
|
console.log('');
|
|
257
284
|
console.log(` Validating against git history (${opts.since})...`);
|
|
258
|
-
const commits = getCommitFiles(opts.appPath, opts.since);
|
|
285
|
+
const commits = getCommitFiles(opts.gitRepoRoot || opts.appPath, opts.since);
|
|
259
286
|
if (commits.length === 0) {
|
|
260
287
|
console.log(' No commits found in range.');
|
|
261
288
|
}
|
|
@@ -95,6 +95,7 @@ const FLAGS = {
|
|
|
95
95
|
'--heal-report': { key: 'analyzeHealReport', type: 'string' },
|
|
96
96
|
'--flow-catalog': { key: 'flowCatalogPath', type: 'string' },
|
|
97
97
|
'--output': { key: 'trainOutput', type: 'string' },
|
|
98
|
+
'--server-path': { key: 'serverPath', type: 'string' },
|
|
98
99
|
// -- number flags (with isFinite guard) --
|
|
99
100
|
'--pipeline-scenarios': { key: 'pipelineScenarios', type: 'number' },
|
|
100
101
|
'--time': { key: 'timeLimitMinutes', type: 'number' },
|
|
@@ -59,9 +59,47 @@ function sampleFiles(dir, maxFiles) {
|
|
|
59
59
|
walk(dir);
|
|
60
60
|
return files;
|
|
61
61
|
}
|
|
62
|
-
|
|
62
|
+
/**
|
|
63
|
+
* Build a shallow directory listing of the source tree (depth 2-3) so the LLM
|
|
64
|
+
* can suggest accurate webappPaths / serverPaths for test-derived families.
|
|
65
|
+
*/
|
|
66
|
+
function getSourceTreeListing(projectRoot, maxDepth = 3) {
|
|
67
|
+
const lines = [];
|
|
68
|
+
function walk(dir, depth, prefix) {
|
|
69
|
+
if (depth > maxDepth || lines.length > 200)
|
|
70
|
+
return;
|
|
71
|
+
let entries;
|
|
72
|
+
try {
|
|
73
|
+
entries = readdirSync(dir).sort();
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const dirs = entries.filter((e) => {
|
|
79
|
+
if (e.startsWith('.') || SKIP_DIRS.has(e))
|
|
80
|
+
return false;
|
|
81
|
+
try {
|
|
82
|
+
const stat = lstatSync(join(dir, e));
|
|
83
|
+
return !stat.isSymbolicLink() && stat.isDirectory();
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
for (const d of dirs) {
|
|
90
|
+
lines.push(`${prefix}${d}/`);
|
|
91
|
+
walk(join(dir, d), depth + 1, prefix + ' ');
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
walk(resolve(projectRoot), 0, '');
|
|
95
|
+
return lines.join('\n');
|
|
96
|
+
}
|
|
97
|
+
function buildEnrichPrompt(families, projectRoot, testsRoot) {
|
|
63
98
|
const sections = [];
|
|
99
|
+
const hasTestOnlyFamilies = families.some((f) => f.webappPaths.length === 0 && f.serverPaths.length === 0);
|
|
100
|
+
const resolvedTestsRoot = testsRoot ? resolve(testsRoot) : resolve(projectRoot);
|
|
64
101
|
for (const family of families) {
|
|
102
|
+
const isTestOnly = family.webappPaths.length === 0 && family.serverPaths.length === 0;
|
|
65
103
|
const allDirs = [
|
|
66
104
|
...family.webappPaths.map((p) => p.replace(/\/?\*.*$/, '')),
|
|
67
105
|
...family.serverPaths.map((p) => p.replace(/\/?\*.*$/, '')),
|
|
@@ -75,10 +113,19 @@ function buildEnrichPrompt(families, projectRoot) {
|
|
|
75
113
|
if (samples.length >= MAX_FILES_PER_FAMILY)
|
|
76
114
|
break;
|
|
77
115
|
}
|
|
116
|
+
// For test-only families, sample the test files themselves for richer context
|
|
117
|
+
if (isTestOnly) {
|
|
118
|
+
for (const specDir of family.specDirs) {
|
|
119
|
+
if (samples.length >= MAX_FILES_PER_FAMILY)
|
|
120
|
+
break;
|
|
121
|
+
const fullDir = join(resolvedTestsRoot, specDir);
|
|
122
|
+
samples.push(...sampleFiles(fullDir, MAX_FILES_PER_FAMILY - samples.length));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
78
125
|
// Sample spec descriptions
|
|
79
126
|
const specSamples = [];
|
|
80
127
|
for (const specDir of family.specDirs) {
|
|
81
|
-
const fullDir = join(
|
|
128
|
+
const fullDir = join(resolvedTestsRoot, specDir);
|
|
82
129
|
const specFiles = sampleFiles(fullDir, 5);
|
|
83
130
|
for (const sf of specFiles) {
|
|
84
131
|
const matches = sf.content.match(/(?:test|it|describe)\s*\(\s*['"`]([^'"`]+)/g);
|
|
@@ -87,7 +134,7 @@ function buildEnrichPrompt(families, projectRoot) {
|
|
|
87
134
|
}
|
|
88
135
|
}
|
|
89
136
|
}
|
|
90
|
-
sections.push(`## Family: ${family.id}
|
|
137
|
+
sections.push(`## Family: ${family.id}${isTestOnly ? ' [TEST-ONLY — needs webappPaths/serverPaths]' : ''}
|
|
91
138
|
Routes (guessed): ${JSON.stringify(family.routes)}
|
|
92
139
|
Webapp paths: ${JSON.stringify(family.webappPaths)}
|
|
93
140
|
Server paths: ${JSON.stringify(family.serverPaths)}
|
|
@@ -102,6 +149,10 @@ Test descriptions:
|
|
|
102
149
|
${specSamples.length > 0 ? specSamples.map((d) => `- ${d}`).join('\n') : '(none found)'}
|
|
103
150
|
`);
|
|
104
151
|
}
|
|
152
|
+
// Include source tree listing when we have test-only families
|
|
153
|
+
const sourceTreeSection = hasTestOnlyFamilies
|
|
154
|
+
? `\n## Source Directory Structure\nUse this to suggest accurate webappPaths and serverPaths for test-only families:\n\`\`\`\n${getSourceTreeListing(projectRoot)}\n\`\`\`\n`
|
|
155
|
+
: '';
|
|
105
156
|
return `You are analyzing a codebase to enrich route-family definitions for an E2E test impact analysis tool.
|
|
106
157
|
|
|
107
158
|
For each family below, provide:
|
|
@@ -110,6 +161,8 @@ For each family below, provide:
|
|
|
110
161
|
3. **routes**: Improved URL patterns (e.g., "/{team}/channels/{channel}" instead of "/channels")
|
|
111
162
|
4. **pageObjects**: Array of page object class names found in the code
|
|
112
163
|
5. **components**: Array of UI component names relevant to this family
|
|
164
|
+
6. **webappPaths**: Array of glob patterns for frontend source directories (e.g., "src/components/drafts/**"). REQUIRED for families marked [TEST-ONLY].
|
|
165
|
+
7. **serverPaths**: Array of glob patterns for backend source directories. REQUIRED for families marked [TEST-ONLY].
|
|
113
166
|
|
|
114
167
|
Respond in JSON format:
|
|
115
168
|
\`\`\`json
|
|
@@ -120,11 +173,13 @@ Respond in JSON format:
|
|
|
120
173
|
"userFlows": ["Flow name 1", "Flow name 2"],
|
|
121
174
|
"routes": ["/improved/route/{param}"],
|
|
122
175
|
"pageObjects": ["PageName"],
|
|
123
|
-
"components": ["ComponentName"]
|
|
176
|
+
"components": ["ComponentName"],
|
|
177
|
+
"webappPaths": ["src/components/feature_name/**"],
|
|
178
|
+
"serverPaths": ["server/channels/api4/feature.go"]
|
|
124
179
|
}
|
|
125
180
|
]
|
|
126
181
|
\`\`\`
|
|
127
|
-
|
|
182
|
+
${sourceTreeSection}
|
|
128
183
|
${sections.join('\n---\n')}`;
|
|
129
184
|
}
|
|
130
185
|
export function validateEntries(parsed) {
|
|
@@ -143,6 +198,8 @@ export function validateEntries(parsed) {
|
|
|
143
198
|
userFlows: filterStrings(entry.userFlows, 500),
|
|
144
199
|
pageObjects: filterStrings(entry.pageObjects, 200),
|
|
145
200
|
components: filterStrings(entry.components, 200),
|
|
201
|
+
webappPaths: filterStrings(entry.webappPaths, 300),
|
|
202
|
+
serverPaths: filterStrings(entry.serverPaths, 300),
|
|
146
203
|
}));
|
|
147
204
|
}
|
|
148
205
|
export function parseEnrichResponse(response) {
|
|
@@ -192,9 +249,16 @@ function applyEnrichment(family, enriched) {
|
|
|
192
249
|
if (enriched.components && (!family.components || family.components.length === 0)) {
|
|
193
250
|
result.components = enriched.components;
|
|
194
251
|
}
|
|
252
|
+
// Only fill source paths when the family has none (test-derived families)
|
|
253
|
+
if (enriched.webappPaths && (!family.webappPaths || family.webappPaths.length === 0)) {
|
|
254
|
+
result.webappPaths = enriched.webappPaths;
|
|
255
|
+
}
|
|
256
|
+
if (enriched.serverPaths && (!family.serverPaths || family.serverPaths.length === 0)) {
|
|
257
|
+
result.serverPaths = enriched.serverPaths;
|
|
258
|
+
}
|
|
195
259
|
return result;
|
|
196
260
|
}
|
|
197
|
-
export async function enrichFamilies(families, scanned, projectRoot, provider, budgetUSD) {
|
|
261
|
+
export async function enrichFamilies(families, scanned, projectRoot, provider, budgetUSD, testsRoot) {
|
|
198
262
|
const scannedMap = new Map(scanned.map((s) => [s.id, s]));
|
|
199
263
|
const enriched = [];
|
|
200
264
|
let totalTokens = 0;
|
|
@@ -218,7 +282,7 @@ export async function enrichFamilies(families, scanned, projectRoot, provider, b
|
|
|
218
282
|
enriched.push(...chunk);
|
|
219
283
|
continue;
|
|
220
284
|
}
|
|
221
|
-
let prompt = buildEnrichPrompt(scannedChunk, projectRoot);
|
|
285
|
+
let prompt = buildEnrichPrompt(scannedChunk, projectRoot, testsRoot);
|
|
222
286
|
if (prompt.length > MAX_PROMPT_CHARS) {
|
|
223
287
|
// Truncate at the last complete section boundary to avoid malformed input
|
|
224
288
|
const lastSectionEnd = prompt.lastIndexOf('\n---\n', MAX_PROMPT_CHARS);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
2
2
|
// See LICENSE.txt for license information.
|
|
3
|
+
import { execFileSync } from 'child_process';
|
|
3
4
|
import { existsSync } from 'fs';
|
|
4
5
|
import { join, resolve } from 'path';
|
|
5
6
|
import { isGuessedRoute } from './types.js';
|
|
@@ -67,6 +68,21 @@ function scannedToRouteFamily(scanned) {
|
|
|
67
68
|
}
|
|
68
69
|
return family;
|
|
69
70
|
}
|
|
71
|
+
/**
|
|
72
|
+
* Try to find a matching family ID with singular/plural normalization.
|
|
73
|
+
* "team" matches "teams", "emoji" matches "emoji", etc.
|
|
74
|
+
*/
|
|
75
|
+
function findFuzzyMatch(id, idMap) {
|
|
76
|
+
if (idMap.has(id))
|
|
77
|
+
return id;
|
|
78
|
+
// Try adding 's'
|
|
79
|
+
if (!id.endsWith('s') && idMap.has(id + 's'))
|
|
80
|
+
return id + 's';
|
|
81
|
+
// Try removing 's'
|
|
82
|
+
if (id.endsWith('s') && idMap.has(id.slice(0, -1)))
|
|
83
|
+
return id.slice(0, -1);
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
70
86
|
export function mergeFamilies(existing, scanned) {
|
|
71
87
|
const existingFamilies = existing?.families || [];
|
|
72
88
|
const existingMap = new Map(existingFamilies.map((f) => [f.id, f]));
|
|
@@ -74,9 +90,15 @@ export function mergeFamilies(existing, scanned) {
|
|
|
74
90
|
const newFamilies = [];
|
|
75
91
|
const updatedFamilies = [];
|
|
76
92
|
const mergedFamilies = [];
|
|
77
|
-
// Process existing families
|
|
93
|
+
// Process existing families — match scanned by exact or fuzzy ID
|
|
78
94
|
for (const ef of existingFamilies) {
|
|
79
|
-
|
|
95
|
+
let sf = scannedMap.get(ef.id);
|
|
96
|
+
// Try singular/plural match if exact match failed
|
|
97
|
+
if (!sf) {
|
|
98
|
+
const fuzzyId = findFuzzyMatch(ef.id, scannedMap);
|
|
99
|
+
if (fuzzyId)
|
|
100
|
+
sf = scannedMap.get(fuzzyId);
|
|
101
|
+
}
|
|
80
102
|
if (sf) {
|
|
81
103
|
mergedFamilies.push(mergeFamily(ef, sf));
|
|
82
104
|
updatedFamilies.push(ef.id);
|
|
@@ -86,9 +108,10 @@ export function mergeFamilies(existing, scanned) {
|
|
|
86
108
|
mergedFamilies.push({ ...ef });
|
|
87
109
|
}
|
|
88
110
|
}
|
|
89
|
-
// Add new families from scanner
|
|
111
|
+
// Add new families from scanner (if no existing family matched)
|
|
90
112
|
for (const sf of scanned) {
|
|
91
|
-
|
|
113
|
+
const matchedExisting = findFuzzyMatch(sf.id, existingMap);
|
|
114
|
+
if (!matchedExisting) {
|
|
92
115
|
mergedFamilies.push(scannedToRouteFamily(sf));
|
|
93
116
|
newFamilies.push(sf.id);
|
|
94
117
|
}
|
|
@@ -108,8 +131,33 @@ export function mergeFamilies(existing, scanned) {
|
|
|
108
131
|
summary: parts.join(', '),
|
|
109
132
|
};
|
|
110
133
|
}
|
|
111
|
-
|
|
112
|
-
|
|
134
|
+
/**
|
|
135
|
+
* Detect families whose paths no longer exist on disk.
|
|
136
|
+
*
|
|
137
|
+
* Paths in the manifest may be relative to different roots:
|
|
138
|
+
* - webappPaths / serverPaths are typically relative to the repo root
|
|
139
|
+
* - specDirs may be relative to the tests root
|
|
140
|
+
*
|
|
141
|
+
* We try each pattern against all provided roots (and the git repo root
|
|
142
|
+
* if discoverable) to avoid false positives from path-prefix mismatches.
|
|
143
|
+
*/
|
|
144
|
+
export function detectStaleFamilies(manifest, projectRoot, testsRoot) {
|
|
145
|
+
const roots = new Set([resolve(projectRoot)]);
|
|
146
|
+
if (testsRoot)
|
|
147
|
+
roots.add(resolve(testsRoot));
|
|
148
|
+
// Also try to discover the git repo root — manifest paths may be repo-relative
|
|
149
|
+
try {
|
|
150
|
+
const gitRoot = execFileSync('git', ['rev-parse', '--show-toplevel'], {
|
|
151
|
+
cwd: projectRoot,
|
|
152
|
+
encoding: 'utf-8',
|
|
153
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
154
|
+
}).trim();
|
|
155
|
+
if (gitRoot)
|
|
156
|
+
roots.add(resolve(gitRoot));
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
// Not a git repo or git not available — that's fine
|
|
160
|
+
}
|
|
113
161
|
const stale = [];
|
|
114
162
|
for (const family of manifest.families) {
|
|
115
163
|
const allPatterns = [
|
|
@@ -119,15 +167,34 @@ export function detectStaleFamilies(manifest, projectRoot) {
|
|
|
119
167
|
];
|
|
120
168
|
if (allPatterns.length === 0)
|
|
121
169
|
continue;
|
|
122
|
-
// Check if any pattern resolves to existing files/dirs
|
|
170
|
+
// Check if any pattern resolves to existing files/dirs in any root
|
|
123
171
|
let hasAny = false;
|
|
124
172
|
for (const pattern of allPatterns) {
|
|
125
173
|
// Strip trailing glob (* or **) to get the directory
|
|
126
174
|
const dirPart = pattern.replace(/\/?\*.*$/, '');
|
|
127
|
-
if (dirPart
|
|
128
|
-
|
|
129
|
-
|
|
175
|
+
if (!dirPart)
|
|
176
|
+
continue;
|
|
177
|
+
// For file-level patterns like "server/channels/api4/draft*.go",
|
|
178
|
+
// dirPart is "server/channels/api4/draft" — check the parent dir instead
|
|
179
|
+
const isFileGlob = /\.\w+$/.test(pattern);
|
|
180
|
+
const pathsToCheck = [dirPart];
|
|
181
|
+
if (isFileGlob) {
|
|
182
|
+
const parentDir = dirPart.split('/').slice(0, -1).join('/');
|
|
183
|
+
if (parentDir)
|
|
184
|
+
pathsToCheck.push(parentDir);
|
|
185
|
+
}
|
|
186
|
+
for (const checkPath of pathsToCheck) {
|
|
187
|
+
for (const root of roots) {
|
|
188
|
+
if (existsSync(join(root, checkPath))) {
|
|
189
|
+
hasAny = true;
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
if (hasAny)
|
|
194
|
+
break;
|
|
130
195
|
}
|
|
196
|
+
if (hasAny)
|
|
197
|
+
break;
|
|
131
198
|
}
|
|
132
199
|
if (!hasAny) {
|
|
133
200
|
stale.push(family.id);
|