@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
|
@@ -15,6 +15,50 @@ const SKIP_DIRS = new Set([
|
|
|
15
15
|
]);
|
|
16
16
|
const TEST_EXTENSIONS = ['.spec.ts', '.test.ts', '.spec.js', '.test.js', '.spec.tsx', '.test.tsx'];
|
|
17
17
|
const GO_TEST_SUFFIX = '_test.go';
|
|
18
|
+
/**
|
|
19
|
+
* Test category directories that organize tests but aren't feature families.
|
|
20
|
+
* Test-only families matching these names are excluded.
|
|
21
|
+
*/
|
|
22
|
+
const TEST_CATEGORY_DIRS = new Set([
|
|
23
|
+
'specs', 'spec', 'accessibility', 'visual', 'smoke', 'regression',
|
|
24
|
+
'integration', 'functional', 'unit', 'e2e', 'performance', 'load',
|
|
25
|
+
]);
|
|
26
|
+
/**
|
|
27
|
+
* Structural directories that are code-organization concerns, not feature families.
|
|
28
|
+
* Discovered source dirs matching these names are excluded from family creation.
|
|
29
|
+
*/
|
|
30
|
+
const STRUCTURAL_DIRS = new Set([
|
|
31
|
+
'actions', 'client', 'components', 'hooks', 'i18n', 'packages',
|
|
32
|
+
'reducers', 'selectors', 'store', 'stores', 'tests', 'types',
|
|
33
|
+
'utils', 'helpers', 'lib', 'common', 'shared', 'constants',
|
|
34
|
+
'config', 'styles', 'sass', 'css', 'assets', 'images', 'fonts',
|
|
35
|
+
'middleware', 'contexts', 'providers', 'layouts', 'templates',
|
|
36
|
+
]);
|
|
37
|
+
/**
|
|
38
|
+
* Server Go files that are infrastructure / cross-cutting concerns,
|
|
39
|
+
* not feature-specific domains. Matched after stripping _local/_store suffixes.
|
|
40
|
+
*/
|
|
41
|
+
const SERVER_INFRA_FILES = new Set([
|
|
42
|
+
'api', 'apitestlib', 'context', 'helpers', 'params', 'swagger',
|
|
43
|
+
'app', 'server', 'enterprise', 'product_service', 'security_update_check',
|
|
44
|
+
'store', 'adapters', 'errors', 'integrity', 'migrate', 'doc',
|
|
45
|
+
'main', 'init', 'cluster_discovery', 'web_conn', 'web_broadcast_hooks',
|
|
46
|
+
'manualtesting', 'testlib', 'router', 'handler', 'opentracing',
|
|
47
|
+
'platform', 'focalboard', 'playbooks', 'client4', 'model',
|
|
48
|
+
'manifest', 'permission', 'log', 'utils',
|
|
49
|
+
]);
|
|
50
|
+
/**
|
|
51
|
+
* Server tier directories to scan for Go domain files.
|
|
52
|
+
* Each tier represents a layer of the backend architecture.
|
|
53
|
+
*/
|
|
54
|
+
const SERVER_TIERS = [
|
|
55
|
+
'channels/api4',
|
|
56
|
+
'channels/app',
|
|
57
|
+
'channels/store/sqlstore',
|
|
58
|
+
'channels/web',
|
|
59
|
+
'channels/wsapi',
|
|
60
|
+
'public/model',
|
|
61
|
+
];
|
|
18
62
|
/** Type-safe includes check for readonly arrays */
|
|
19
63
|
const includes = (arr, v) => arr.includes(v);
|
|
20
64
|
function isSkipped(name) {
|
|
@@ -314,10 +358,273 @@ function detectFeatures(familyId, group, projectRoot) {
|
|
|
314
358
|
}
|
|
315
359
|
return features;
|
|
316
360
|
}
|
|
317
|
-
|
|
361
|
+
/**
|
|
362
|
+
* Discover families by walking the test directory tree at depth ≥ 2.
|
|
363
|
+
*
|
|
364
|
+
* This is the primary family discovery mechanism for projects where source
|
|
365
|
+
* code is organized by code type (components/, actions/) but tests are
|
|
366
|
+
* organized by feature (channels/drafts/, channels/search/).
|
|
367
|
+
*
|
|
368
|
+
* Each leaf test directory (containing spec files) at meaningful depth ≥ 2
|
|
369
|
+
* becomes a candidate family. Top-level feature dirs (depth 1) are already
|
|
370
|
+
* discovered by the standard `discoverTestDirs` + `groupByFamily` pipeline.
|
|
371
|
+
*/
|
|
372
|
+
/**
|
|
373
|
+
* Normalize a Go filename into a family domain identifier.
|
|
374
|
+
* Strips _local, _store, trailing 's' (plurals), and normalizes casing.
|
|
375
|
+
*/
|
|
376
|
+
function normalizeServerDomain(baseName) {
|
|
377
|
+
let name = baseName;
|
|
378
|
+
// Strip common suffixes
|
|
379
|
+
name = name.replace(/_local$/, '');
|
|
380
|
+
name = name.replace(/_store$/, '');
|
|
381
|
+
// Skip very short names (e.g., single-letter files)
|
|
382
|
+
if (name.length < 3)
|
|
383
|
+
return null;
|
|
384
|
+
return normalizeId(name);
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Given a domain name like "channel_bookmark", find its parent domain
|
|
388
|
+
* if a shorter prefix exists in the set (e.g., "channel").
|
|
389
|
+
* This groups related server files under a single family.
|
|
390
|
+
*/
|
|
391
|
+
function findParentDomain(name, allDomains) {
|
|
392
|
+
const parts = name.split('_');
|
|
393
|
+
// Try progressively shorter prefixes
|
|
394
|
+
for (let i = parts.length - 1; i >= 1; i--) {
|
|
395
|
+
const candidate = parts.slice(0, i).join('_');
|
|
396
|
+
if (allDomains.has(candidate) && candidate !== name) {
|
|
397
|
+
return candidate;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
return name;
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Discover families by scanning server Go source files.
|
|
404
|
+
*
|
|
405
|
+
* The backend follows a three-tier pattern:
|
|
406
|
+
* api4/draft.go + app/draft.go + store/sqlstore/draft_store.go
|
|
407
|
+
*
|
|
408
|
+
* Related files are grouped under parent domains:
|
|
409
|
+
* channel.go, channel_bookmark.go, channel_category.go → "channel" family
|
|
410
|
+
*
|
|
411
|
+
* Each domain becomes a candidate family with precise serverPaths.
|
|
412
|
+
*/
|
|
413
|
+
export function discoverServerDerivedFamilies(serverRoot) {
|
|
414
|
+
const resolved = resolve(serverRoot);
|
|
415
|
+
// First pass: collect all raw domain names across tiers
|
|
416
|
+
const allRawDomains = new Set();
|
|
417
|
+
// domain → tier → Set<file basenames>
|
|
418
|
+
const domainTierFiles = new Map();
|
|
419
|
+
function collectGoFile(entry, tierRelPath) {
|
|
420
|
+
if (!entry.endsWith('.go') || entry.endsWith('_test.go') || entry.startsWith('.'))
|
|
421
|
+
return;
|
|
422
|
+
const baseName = entry.replace('.go', '');
|
|
423
|
+
const domain = normalizeServerDomain(baseName);
|
|
424
|
+
if (!domain || SERVER_INFRA_FILES.has(domain))
|
|
425
|
+
return;
|
|
426
|
+
allRawDomains.add(domain);
|
|
427
|
+
if (!domainTierFiles.has(domain))
|
|
428
|
+
domainTierFiles.set(domain, new Map());
|
|
429
|
+
const tierMap = domainTierFiles.get(domain);
|
|
430
|
+
if (!tierMap.has(tierRelPath))
|
|
431
|
+
tierMap.set(tierRelPath, new Set());
|
|
432
|
+
tierMap.get(tierRelPath).add(baseName);
|
|
433
|
+
}
|
|
434
|
+
for (const tier of SERVER_TIERS) {
|
|
435
|
+
const tierPath = join(resolved, tier);
|
|
436
|
+
if (!existsSync(tierPath))
|
|
437
|
+
continue;
|
|
438
|
+
let entries;
|
|
439
|
+
try {
|
|
440
|
+
entries = readdirSync(tierPath);
|
|
441
|
+
}
|
|
442
|
+
catch {
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
for (const entry of entries) {
|
|
446
|
+
collectGoFile(entry, tier);
|
|
447
|
+
// Also check subdirectories (e.g., app/slashcommands/, app/users/)
|
|
448
|
+
const subPath = join(tierPath, entry);
|
|
449
|
+
try {
|
|
450
|
+
const stat = lstatSync(subPath);
|
|
451
|
+
if (stat.isDirectory() && !isSkipped(entry)) {
|
|
452
|
+
const subEntries = readdirSync(subPath);
|
|
453
|
+
for (const subEntry of subEntries) {
|
|
454
|
+
collectGoFile(subEntry, `${tier}/${entry}`);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
catch { /* skip */ }
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
// Scan job directories — each subdirectory is a job type
|
|
462
|
+
const jobsPath = join(resolved, 'channels/jobs');
|
|
463
|
+
if (existsSync(jobsPath)) {
|
|
464
|
+
try {
|
|
465
|
+
for (const entry of readdirSync(jobsPath)) {
|
|
466
|
+
const jobPath = join(jobsPath, entry);
|
|
467
|
+
try {
|
|
468
|
+
if (!lstatSync(jobPath).isDirectory() || isSkipped(entry))
|
|
469
|
+
continue;
|
|
470
|
+
const domain = normalizeId(entry);
|
|
471
|
+
if (SERVER_INFRA_FILES.has(domain))
|
|
472
|
+
continue;
|
|
473
|
+
allRawDomains.add(domain);
|
|
474
|
+
const jobFiles = readdirSync(jobPath);
|
|
475
|
+
for (const jf of jobFiles) {
|
|
476
|
+
if (jf.endsWith('.go') && !jf.endsWith('_test.go')) {
|
|
477
|
+
if (!domainTierFiles.has(domain))
|
|
478
|
+
domainTierFiles.set(domain, new Map());
|
|
479
|
+
const tierMap = domainTierFiles.get(domain);
|
|
480
|
+
const tierKey = `channels/jobs/${entry}`;
|
|
481
|
+
if (!tierMap.has(tierKey))
|
|
482
|
+
tierMap.set(tierKey, new Set());
|
|
483
|
+
tierMap.get(tierKey).add(jf.replace('.go', ''));
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
catch { /* skip */ }
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
catch { /* skip */ }
|
|
491
|
+
}
|
|
492
|
+
// Second pass: group child domains under parents
|
|
493
|
+
// e.g., channel_bookmark → channel, post_priority → post
|
|
494
|
+
// Track which top-level tiers each family touches for significance filtering.
|
|
495
|
+
const familyPaths = new Map();
|
|
496
|
+
const familyTiers = new Map();
|
|
497
|
+
for (const [domain, tierMap] of domainTierFiles) {
|
|
498
|
+
const parentDomain = findParentDomain(domain, allRawDomains);
|
|
499
|
+
if (!familyPaths.has(parentDomain))
|
|
500
|
+
familyPaths.set(parentDomain, new Set());
|
|
501
|
+
if (!familyTiers.has(parentDomain))
|
|
502
|
+
familyTiers.set(parentDomain, new Set());
|
|
503
|
+
const paths = familyPaths.get(parentDomain);
|
|
504
|
+
const tiers = familyTiers.get(parentDomain);
|
|
505
|
+
for (const [tierRelPath, fileNames] of tierMap) {
|
|
506
|
+
// Track the top-level tier (e.g., "channels/api4" from "channels/api4/slashcommands")
|
|
507
|
+
const topTier = tierRelPath.split('/').slice(0, 2).join('/');
|
|
508
|
+
tiers.add(topTier);
|
|
509
|
+
for (const baseName of fileNames) {
|
|
510
|
+
// Use directory-level glob to capture the file and related variants
|
|
511
|
+
paths.add(`server/${tierRelPath}/${baseName}*.go`);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
// Build families from grouped domains.
|
|
516
|
+
// Only include server-only families that span ≥ 2 tiers (architecturally significant).
|
|
517
|
+
const families = [];
|
|
518
|
+
for (const [domain, paths] of familyPaths) {
|
|
519
|
+
if (paths.size === 0)
|
|
520
|
+
continue;
|
|
521
|
+
const tierCount = familyTiers.get(domain)?.size ?? 0;
|
|
522
|
+
if (tierCount < 2)
|
|
523
|
+
continue; // Skip single-tier domains (likely infrastructure)
|
|
524
|
+
families.push({
|
|
525
|
+
id: domain,
|
|
526
|
+
routes: [`/${domain.replace(/_/g, '-')}`],
|
|
527
|
+
webappPaths: [],
|
|
528
|
+
serverPaths: Array.from(paths),
|
|
529
|
+
specDirs: [],
|
|
530
|
+
cypressSpecDirs: [],
|
|
531
|
+
tags: [],
|
|
532
|
+
features: [],
|
|
533
|
+
routesGuessed: true,
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
return families;
|
|
537
|
+
}
|
|
538
|
+
export function discoverTestDerivedFamilies(testsRoot) {
|
|
539
|
+
const resolved = resolve(testsRoot);
|
|
540
|
+
const candidates = [];
|
|
541
|
+
function walk(dir, depth) {
|
|
542
|
+
if (depth > 8)
|
|
543
|
+
return;
|
|
544
|
+
let entries;
|
|
545
|
+
try {
|
|
546
|
+
entries = readdirSync(dir);
|
|
547
|
+
}
|
|
548
|
+
catch {
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
const hasSpecs = entries.some((e) => TEST_EXTENSIONS.some((ext) => e.endsWith(ext)) || e.endsWith(GO_TEST_SUFFIX));
|
|
552
|
+
const subdirs = entries.filter((e) => {
|
|
553
|
+
if (isSkipped(e))
|
|
554
|
+
return false;
|
|
555
|
+
try {
|
|
556
|
+
const stat = lstatSync(join(dir, e));
|
|
557
|
+
return !stat.isSymbolicLink() && stat.isDirectory();
|
|
558
|
+
}
|
|
559
|
+
catch {
|
|
560
|
+
return false;
|
|
561
|
+
}
|
|
562
|
+
});
|
|
563
|
+
const relPath = relative(resolved, dir).replace(/\\/g, '/');
|
|
564
|
+
const parts = relPath.split('/').filter(Boolean);
|
|
565
|
+
const meaningful = parts.filter((p) => !TEST_CATEGORY_DIRS.has(normalizeId(p)) && !isSkipped(p));
|
|
566
|
+
// Depth-2+ meaningful dirs with spec files → candidate families
|
|
567
|
+
if (meaningful.length >= 2 && hasSpecs) {
|
|
568
|
+
const leafId = normalizeId(meaningful[meaningful.length - 1]);
|
|
569
|
+
const parentId = normalizeId(meaningful[meaningful.length - 2]);
|
|
570
|
+
if (!STRUCTURAL_DIRS.has(leafId) && !TEST_CATEGORY_DIRS.has(leafId)) {
|
|
571
|
+
candidates.push({ dir, relPath, leafId, parentId });
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
for (const sub of subdirs) {
|
|
575
|
+
walk(join(dir, sub), depth + 1);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
// Walk from standard test roots
|
|
579
|
+
const testRoots = ['tests', 'test', 'e2e-tests', 'e2e', 'specs', 'spec'];
|
|
580
|
+
for (const root of testRoots) {
|
|
581
|
+
const rootPath = join(resolved, root);
|
|
582
|
+
if (existsSync(rootPath)) {
|
|
583
|
+
walk(rootPath, 0);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
// Detect leaf-name collisions across parents
|
|
587
|
+
const idCount = new Map();
|
|
588
|
+
for (const c of candidates) {
|
|
589
|
+
idCount.set(c.leafId, (idCount.get(c.leafId) || 0) + 1);
|
|
590
|
+
}
|
|
591
|
+
// Build families — prefix with parent when names collide
|
|
592
|
+
const familyMap = new Map();
|
|
593
|
+
for (const c of candidates) {
|
|
594
|
+
let familyId = c.leafId;
|
|
595
|
+
if ((idCount.get(c.leafId) || 0) > 1 && c.parentId) {
|
|
596
|
+
familyId = `${c.parentId}_${c.leafId}`;
|
|
597
|
+
}
|
|
598
|
+
if (!familyMap.has(familyId)) {
|
|
599
|
+
const specFiles = getSpecFiles(c.dir);
|
|
600
|
+
familyMap.set(familyId, {
|
|
601
|
+
id: familyId,
|
|
602
|
+
routes: [`/${familyId.replace(/_/g, '-')}`],
|
|
603
|
+
webappPaths: [],
|
|
604
|
+
serverPaths: [],
|
|
605
|
+
specDirs: [c.relPath + '/'],
|
|
606
|
+
cypressSpecDirs: [],
|
|
607
|
+
tags: extractTags(specFiles),
|
|
608
|
+
features: [],
|
|
609
|
+
routesGuessed: true,
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
else {
|
|
613
|
+
const existing = familyMap.get(familyId);
|
|
614
|
+
const specDir = c.relPath + '/';
|
|
615
|
+
if (!existing.specDirs.includes(specDir)) {
|
|
616
|
+
existing.specDirs.push(specDir);
|
|
617
|
+
existing.tags = [...new Set([...existing.tags, ...extractTags(getSpecFiles(c.dir))])];
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
return Array.from(familyMap.values());
|
|
622
|
+
}
|
|
623
|
+
export function scanProject(projectRoot, testsRoot, serverRoot) {
|
|
318
624
|
const resolved = resolve(projectRoot);
|
|
625
|
+
const resolvedTestsRoot = testsRoot ? resolve(testsRoot) : resolved;
|
|
319
626
|
const sourceDirs = discoverSourceDirs(resolved);
|
|
320
|
-
const testDirs = discoverTestDirs(
|
|
627
|
+
const testDirs = discoverTestDirs(resolvedTestsRoot);
|
|
321
628
|
const allDirs = [...sourceDirs, ...testDirs];
|
|
322
629
|
const groups = groupByFamily(allDirs);
|
|
323
630
|
const families = [];
|
|
@@ -326,6 +633,13 @@ export function scanProject(projectRoot) {
|
|
|
326
633
|
const hasTests = group.test.length > 0 || group.cypress.length > 0;
|
|
327
634
|
if (!hasSrc && !hasTests)
|
|
328
635
|
continue;
|
|
636
|
+
// Skip structural directories that are code-organization, not features.
|
|
637
|
+
// Only skip if they have source dirs but no corresponding test dirs.
|
|
638
|
+
if (STRUCTURAL_DIRS.has(familyId) && !hasTests)
|
|
639
|
+
continue;
|
|
640
|
+
// Skip test-only families that match broad test categories (not feature families).
|
|
641
|
+
if (!hasSrc && hasTests && TEST_CATEGORY_DIRS.has(familyId))
|
|
642
|
+
continue;
|
|
329
643
|
const allSpecFiles = [];
|
|
330
644
|
for (const td of [...group.test, ...group.cypress]) {
|
|
331
645
|
allSpecFiles.push(...getSpecFiles(td.path));
|
|
@@ -343,6 +657,58 @@ export function scanProject(projectRoot) {
|
|
|
343
657
|
routesGuessed: true,
|
|
344
658
|
});
|
|
345
659
|
}
|
|
660
|
+
// When a separate testsRoot is provided, discover families from test
|
|
661
|
+
// directory structure. Projects with feature-organized tests but
|
|
662
|
+
// code-type-organized source benefit from this.
|
|
663
|
+
if (testsRoot) {
|
|
664
|
+
const testFamilies = discoverTestDerivedFamilies(resolvedTestsRoot);
|
|
665
|
+
const existingIds = new Set(families.map((f) => f.id));
|
|
666
|
+
for (const tf of testFamilies) {
|
|
667
|
+
if (existingIds.has(tf.id)) {
|
|
668
|
+
// Merge specDirs into existing family
|
|
669
|
+
const existing = families.find((f) => f.id === tf.id);
|
|
670
|
+
for (const sd of tf.specDirs) {
|
|
671
|
+
if (!existing.specDirs.includes(sd)) {
|
|
672
|
+
existing.specDirs.push(sd);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
existing.tags = [...new Set([...existing.tags, ...tf.tags])];
|
|
676
|
+
}
|
|
677
|
+
else {
|
|
678
|
+
families.push(tf);
|
|
679
|
+
existingIds.add(tf.id);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
// When a separate serverRoot is provided, discover families from Go source
|
|
684
|
+
// filenames across the three-tier backend (api4, app, store).
|
|
685
|
+
if (serverRoot) {
|
|
686
|
+
const serverFamilies = discoverServerDerivedFamilies(resolve(serverRoot));
|
|
687
|
+
const existingIds = new Set(families.map((f) => f.id));
|
|
688
|
+
for (const sf of serverFamilies) {
|
|
689
|
+
// Try exact match, then singular/plural variants
|
|
690
|
+
let target = families.find((f) => f.id === sf.id);
|
|
691
|
+
if (!target && !sf.id.endsWith('s')) {
|
|
692
|
+
target = families.find((f) => f.id === sf.id + 's');
|
|
693
|
+
}
|
|
694
|
+
if (!target && sf.id.endsWith('s')) {
|
|
695
|
+
target = families.find((f) => f.id === sf.id.slice(0, -1));
|
|
696
|
+
}
|
|
697
|
+
if (target) {
|
|
698
|
+
// Merge serverPaths into existing family
|
|
699
|
+
for (const sp of sf.serverPaths) {
|
|
700
|
+
if (!target.serverPaths.includes(sp)) {
|
|
701
|
+
target.serverPaths.push(sp);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
else {
|
|
706
|
+
// New server-only family
|
|
707
|
+
families.push(sf);
|
|
708
|
+
existingIds.add(sf.id);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
}
|
|
346
712
|
const familyIds = new Set(families.map((f) => f.id));
|
|
347
713
|
const unmatchedSourceDirs = sourceDirs.filter((d) => !familyIds.has(normalizeId(d.familyHint)));
|
|
348
714
|
const unmatchedTestDirs = testDirs.filter((d) => !familyIds.has(normalizeId(d.familyHint)));
|
|
@@ -8,8 +8,10 @@ export interface EnrichedEntry {
|
|
|
8
8
|
routes?: string[];
|
|
9
9
|
pageObjects?: string[];
|
|
10
10
|
components?: string[];
|
|
11
|
+
webappPaths?: string[];
|
|
12
|
+
serverPaths?: string[];
|
|
11
13
|
}
|
|
12
14
|
export declare function validateEntries(parsed: unknown[]): EnrichedEntry[];
|
|
13
15
|
export declare function parseEnrichResponse(response: string): EnrichedEntry[];
|
|
14
|
-
export declare function enrichFamilies(families: RouteFamily[], scanned: ScannedFamily[], projectRoot: string, provider: LLMProvider, budgetUSD: number): Promise<EnrichmentResult>;
|
|
16
|
+
export declare function enrichFamilies(families: RouteFamily[], scanned: ScannedFamily[], projectRoot: string, provider: LLMProvider, budgetUSD: number, testsRoot?: string): Promise<EnrichmentResult>;
|
|
15
17
|
//# sourceMappingURL=enricher.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"enricher.d.ts","sourceRoot":"","sources":["../../src/training/enricher.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,0BAA0B,CAAC;AAC1D,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,gCAAgC,CAAC;AAGhE,OAAO,KAAK,EAAC,gBAAgB,EAAE,aAAa,EAAC,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"enricher.d.ts","sourceRoot":"","sources":["../../src/training/enricher.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,0BAA0B,CAAC;AAC1D,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,gCAAgC,CAAC;AAGhE,OAAO,KAAK,EAAC,gBAAgB,EAAE,aAAa,EAAC,MAAM,YAAY,CAAC;AAkLhE,MAAM,WAAW,aAAa;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,CAAC,EAAE,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;IAC9B,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;CAC1B;AAED,wBAAgB,eAAe,CAAC,MAAM,EAAE,OAAO,EAAE,GAAG,aAAa,EAAE,CAmBlE;AAED,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,aAAa,EAAE,CAwBrE;AAkCD,wBAAsB,cAAc,CAChC,QAAQ,EAAE,WAAW,EAAE,EACvB,OAAO,EAAE,aAAa,EAAE,EACxB,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,WAAW,EACrB,SAAS,EAAE,MAAM,EACjB,SAAS,CAAC,EAAE,MAAM,GACnB,OAAO,CAAC,gBAAgB,CAAC,CAgF3B"}
|
|
@@ -64,9 +64,47 @@ function sampleFiles(dir, maxFiles) {
|
|
|
64
64
|
walk(dir);
|
|
65
65
|
return files;
|
|
66
66
|
}
|
|
67
|
-
|
|
67
|
+
/**
|
|
68
|
+
* Build a shallow directory listing of the source tree (depth 2-3) so the LLM
|
|
69
|
+
* can suggest accurate webappPaths / serverPaths for test-derived families.
|
|
70
|
+
*/
|
|
71
|
+
function getSourceTreeListing(projectRoot, maxDepth = 3) {
|
|
72
|
+
const lines = [];
|
|
73
|
+
function walk(dir, depth, prefix) {
|
|
74
|
+
if (depth > maxDepth || lines.length > 200)
|
|
75
|
+
return;
|
|
76
|
+
let entries;
|
|
77
|
+
try {
|
|
78
|
+
entries = (0, fs_1.readdirSync)(dir).sort();
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const dirs = entries.filter((e) => {
|
|
84
|
+
if (e.startsWith('.') || SKIP_DIRS.has(e))
|
|
85
|
+
return false;
|
|
86
|
+
try {
|
|
87
|
+
const stat = (0, fs_1.lstatSync)((0, path_1.join)(dir, e));
|
|
88
|
+
return !stat.isSymbolicLink() && stat.isDirectory();
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
for (const d of dirs) {
|
|
95
|
+
lines.push(`${prefix}${d}/`);
|
|
96
|
+
walk((0, path_1.join)(dir, d), depth + 1, prefix + ' ');
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
walk((0, path_1.resolve)(projectRoot), 0, '');
|
|
100
|
+
return lines.join('\n');
|
|
101
|
+
}
|
|
102
|
+
function buildEnrichPrompt(families, projectRoot, testsRoot) {
|
|
68
103
|
const sections = [];
|
|
104
|
+
const hasTestOnlyFamilies = families.some((f) => f.webappPaths.length === 0 && f.serverPaths.length === 0);
|
|
105
|
+
const resolvedTestsRoot = testsRoot ? (0, path_1.resolve)(testsRoot) : (0, path_1.resolve)(projectRoot);
|
|
69
106
|
for (const family of families) {
|
|
107
|
+
const isTestOnly = family.webappPaths.length === 0 && family.serverPaths.length === 0;
|
|
70
108
|
const allDirs = [
|
|
71
109
|
...family.webappPaths.map((p) => p.replace(/\/?\*.*$/, '')),
|
|
72
110
|
...family.serverPaths.map((p) => p.replace(/\/?\*.*$/, '')),
|
|
@@ -80,10 +118,19 @@ function buildEnrichPrompt(families, projectRoot) {
|
|
|
80
118
|
if (samples.length >= MAX_FILES_PER_FAMILY)
|
|
81
119
|
break;
|
|
82
120
|
}
|
|
121
|
+
// For test-only families, sample the test files themselves for richer context
|
|
122
|
+
if (isTestOnly) {
|
|
123
|
+
for (const specDir of family.specDirs) {
|
|
124
|
+
if (samples.length >= MAX_FILES_PER_FAMILY)
|
|
125
|
+
break;
|
|
126
|
+
const fullDir = (0, path_1.join)(resolvedTestsRoot, specDir);
|
|
127
|
+
samples.push(...sampleFiles(fullDir, MAX_FILES_PER_FAMILY - samples.length));
|
|
128
|
+
}
|
|
129
|
+
}
|
|
83
130
|
// Sample spec descriptions
|
|
84
131
|
const specSamples = [];
|
|
85
132
|
for (const specDir of family.specDirs) {
|
|
86
|
-
const fullDir = (0, path_1.join)(
|
|
133
|
+
const fullDir = (0, path_1.join)(resolvedTestsRoot, specDir);
|
|
87
134
|
const specFiles = sampleFiles(fullDir, 5);
|
|
88
135
|
for (const sf of specFiles) {
|
|
89
136
|
const matches = sf.content.match(/(?:test|it|describe)\s*\(\s*['"`]([^'"`]+)/g);
|
|
@@ -92,7 +139,7 @@ function buildEnrichPrompt(families, projectRoot) {
|
|
|
92
139
|
}
|
|
93
140
|
}
|
|
94
141
|
}
|
|
95
|
-
sections.push(`## Family: ${family.id}
|
|
142
|
+
sections.push(`## Family: ${family.id}${isTestOnly ? ' [TEST-ONLY — needs webappPaths/serverPaths]' : ''}
|
|
96
143
|
Routes (guessed): ${JSON.stringify(family.routes)}
|
|
97
144
|
Webapp paths: ${JSON.stringify(family.webappPaths)}
|
|
98
145
|
Server paths: ${JSON.stringify(family.serverPaths)}
|
|
@@ -107,6 +154,10 @@ Test descriptions:
|
|
|
107
154
|
${specSamples.length > 0 ? specSamples.map((d) => `- ${d}`).join('\n') : '(none found)'}
|
|
108
155
|
`);
|
|
109
156
|
}
|
|
157
|
+
// Include source tree listing when we have test-only families
|
|
158
|
+
const sourceTreeSection = hasTestOnlyFamilies
|
|
159
|
+
? `\n## Source Directory Structure\nUse this to suggest accurate webappPaths and serverPaths for test-only families:\n\`\`\`\n${getSourceTreeListing(projectRoot)}\n\`\`\`\n`
|
|
160
|
+
: '';
|
|
110
161
|
return `You are analyzing a codebase to enrich route-family definitions for an E2E test impact analysis tool.
|
|
111
162
|
|
|
112
163
|
For each family below, provide:
|
|
@@ -115,6 +166,8 @@ For each family below, provide:
|
|
|
115
166
|
3. **routes**: Improved URL patterns (e.g., "/{team}/channels/{channel}" instead of "/channels")
|
|
116
167
|
4. **pageObjects**: Array of page object class names found in the code
|
|
117
168
|
5. **components**: Array of UI component names relevant to this family
|
|
169
|
+
6. **webappPaths**: Array of glob patterns for frontend source directories (e.g., "src/components/drafts/**"). REQUIRED for families marked [TEST-ONLY].
|
|
170
|
+
7. **serverPaths**: Array of glob patterns for backend source directories. REQUIRED for families marked [TEST-ONLY].
|
|
118
171
|
|
|
119
172
|
Respond in JSON format:
|
|
120
173
|
\`\`\`json
|
|
@@ -125,11 +178,13 @@ Respond in JSON format:
|
|
|
125
178
|
"userFlows": ["Flow name 1", "Flow name 2"],
|
|
126
179
|
"routes": ["/improved/route/{param}"],
|
|
127
180
|
"pageObjects": ["PageName"],
|
|
128
|
-
"components": ["ComponentName"]
|
|
181
|
+
"components": ["ComponentName"],
|
|
182
|
+
"webappPaths": ["src/components/feature_name/**"],
|
|
183
|
+
"serverPaths": ["server/channels/api4/feature.go"]
|
|
129
184
|
}
|
|
130
185
|
]
|
|
131
186
|
\`\`\`
|
|
132
|
-
|
|
187
|
+
${sourceTreeSection}
|
|
133
188
|
${sections.join('\n---\n')}`;
|
|
134
189
|
}
|
|
135
190
|
function validateEntries(parsed) {
|
|
@@ -148,6 +203,8 @@ function validateEntries(parsed) {
|
|
|
148
203
|
userFlows: filterStrings(entry.userFlows, 500),
|
|
149
204
|
pageObjects: filterStrings(entry.pageObjects, 200),
|
|
150
205
|
components: filterStrings(entry.components, 200),
|
|
206
|
+
webappPaths: filterStrings(entry.webappPaths, 300),
|
|
207
|
+
serverPaths: filterStrings(entry.serverPaths, 300),
|
|
151
208
|
}));
|
|
152
209
|
}
|
|
153
210
|
function parseEnrichResponse(response) {
|
|
@@ -197,9 +254,16 @@ function applyEnrichment(family, enriched) {
|
|
|
197
254
|
if (enriched.components && (!family.components || family.components.length === 0)) {
|
|
198
255
|
result.components = enriched.components;
|
|
199
256
|
}
|
|
257
|
+
// Only fill source paths when the family has none (test-derived families)
|
|
258
|
+
if (enriched.webappPaths && (!family.webappPaths || family.webappPaths.length === 0)) {
|
|
259
|
+
result.webappPaths = enriched.webappPaths;
|
|
260
|
+
}
|
|
261
|
+
if (enriched.serverPaths && (!family.serverPaths || family.serverPaths.length === 0)) {
|
|
262
|
+
result.serverPaths = enriched.serverPaths;
|
|
263
|
+
}
|
|
200
264
|
return result;
|
|
201
265
|
}
|
|
202
|
-
async function enrichFamilies(families, scanned, projectRoot, provider, budgetUSD) {
|
|
266
|
+
async function enrichFamilies(families, scanned, projectRoot, provider, budgetUSD, testsRoot) {
|
|
203
267
|
const scannedMap = new Map(scanned.map((s) => [s.id, s]));
|
|
204
268
|
const enriched = [];
|
|
205
269
|
let totalTokens = 0;
|
|
@@ -223,7 +287,7 @@ async function enrichFamilies(families, scanned, projectRoot, provider, budgetUS
|
|
|
223
287
|
enriched.push(...chunk);
|
|
224
288
|
continue;
|
|
225
289
|
}
|
|
226
|
-
let prompt = buildEnrichPrompt(scannedChunk, projectRoot);
|
|
290
|
+
let prompt = buildEnrichPrompt(scannedChunk, projectRoot, testsRoot);
|
|
227
291
|
if (prompt.length > MAX_PROMPT_CHARS) {
|
|
228
292
|
// Truncate at the last complete section boundary to avoid malformed input
|
|
229
293
|
const lastSectionEnd = prompt.lastIndexOf('\n---\n', MAX_PROMPT_CHARS);
|
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
import type { RouteFamilyManifest } from '../knowledge/route_families.js';
|
|
2
2
|
import type { MergeResult, ScannedFamily } from './types.js';
|
|
3
3
|
export declare function mergeFamilies(existing: RouteFamilyManifest | null, scanned: ScannedFamily[]): MergeResult;
|
|
4
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Detect families whose paths no longer exist on disk.
|
|
6
|
+
*
|
|
7
|
+
* Paths in the manifest may be relative to different roots:
|
|
8
|
+
* - webappPaths / serverPaths are typically relative to the repo root
|
|
9
|
+
* - specDirs may be relative to the tests root
|
|
10
|
+
*
|
|
11
|
+
* We try each pattern against all provided roots (and the git repo root
|
|
12
|
+
* if discoverable) to avoid false positives from path-prefix mismatches.
|
|
13
|
+
*/
|
|
14
|
+
export declare function detectStaleFamilies(manifest: RouteFamilyManifest, projectRoot: string, testsRoot?: string): string[];
|
|
5
15
|
//# sourceMappingURL=merger.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"merger.d.ts","sourceRoot":"","sources":["../../src/training/merger.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"merger.d.ts","sourceRoot":"","sources":["../../src/training/merger.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAc,mBAAmB,EAAC,MAAM,gCAAgC,CAAC;AAGrF,OAAO,KAAK,EAAC,WAAW,EAAE,aAAa,EAAC,MAAM,YAAY,CAAC;AAkF3D,wBAAgB,aAAa,CACzB,QAAQ,EAAE,mBAAmB,GAAG,IAAI,EACpC,OAAO,EAAE,aAAa,EAAE,GACzB,WAAW,CA+Cb;AAED;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CAC/B,QAAQ,EAAE,mBAAmB,EAC7B,WAAW,EAAE,MAAM,EACnB,SAAS,CAAC,EAAE,MAAM,GACnB,MAAM,EAAE,CA6DV"}
|