@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.
@@ -4,6 +4,7 @@
4
4
  Object.defineProperty(exports, "__esModule", { value: true });
5
5
  exports.mergeFamilies = mergeFamilies;
6
6
  exports.detectStaleFamilies = detectStaleFamilies;
7
+ const child_process_1 = require("child_process");
7
8
  const fs_1 = require("fs");
8
9
  const path_1 = require("path");
9
10
  const types_js_1 = require("./types.js");
@@ -71,6 +72,21 @@ function scannedToRouteFamily(scanned) {
71
72
  }
72
73
  return family;
73
74
  }
75
+ /**
76
+ * Try to find a matching family ID with singular/plural normalization.
77
+ * "team" matches "teams", "emoji" matches "emoji", etc.
78
+ */
79
+ function findFuzzyMatch(id, idMap) {
80
+ if (idMap.has(id))
81
+ return id;
82
+ // Try adding 's'
83
+ if (!id.endsWith('s') && idMap.has(id + 's'))
84
+ return id + 's';
85
+ // Try removing 's'
86
+ if (id.endsWith('s') && idMap.has(id.slice(0, -1)))
87
+ return id.slice(0, -1);
88
+ return undefined;
89
+ }
74
90
  function mergeFamilies(existing, scanned) {
75
91
  const existingFamilies = existing?.families || [];
76
92
  const existingMap = new Map(existingFamilies.map((f) => [f.id, f]));
@@ -78,9 +94,15 @@ function mergeFamilies(existing, scanned) {
78
94
  const newFamilies = [];
79
95
  const updatedFamilies = [];
80
96
  const mergedFamilies = [];
81
- // Process existing families
97
+ // Process existing families — match scanned by exact or fuzzy ID
82
98
  for (const ef of existingFamilies) {
83
- const sf = scannedMap.get(ef.id);
99
+ let sf = scannedMap.get(ef.id);
100
+ // Try singular/plural match if exact match failed
101
+ if (!sf) {
102
+ const fuzzyId = findFuzzyMatch(ef.id, scannedMap);
103
+ if (fuzzyId)
104
+ sf = scannedMap.get(fuzzyId);
105
+ }
84
106
  if (sf) {
85
107
  mergedFamilies.push(mergeFamily(ef, sf));
86
108
  updatedFamilies.push(ef.id);
@@ -90,9 +112,10 @@ function mergeFamilies(existing, scanned) {
90
112
  mergedFamilies.push({ ...ef });
91
113
  }
92
114
  }
93
- // Add new families from scanner
115
+ // Add new families from scanner (if no existing family matched)
94
116
  for (const sf of scanned) {
95
- if (!existingMap.has(sf.id)) {
117
+ const matchedExisting = findFuzzyMatch(sf.id, existingMap);
118
+ if (!matchedExisting) {
96
119
  mergedFamilies.push(scannedToRouteFamily(sf));
97
120
  newFamilies.push(sf.id);
98
121
  }
@@ -112,8 +135,33 @@ function mergeFamilies(existing, scanned) {
112
135
  summary: parts.join(', '),
113
136
  };
114
137
  }
115
- function detectStaleFamilies(manifest, projectRoot) {
116
- const resolved = (0, path_1.resolve)(projectRoot);
138
+ /**
139
+ * Detect families whose paths no longer exist on disk.
140
+ *
141
+ * Paths in the manifest may be relative to different roots:
142
+ * - webappPaths / serverPaths are typically relative to the repo root
143
+ * - specDirs may be relative to the tests root
144
+ *
145
+ * We try each pattern against all provided roots (and the git repo root
146
+ * if discoverable) to avoid false positives from path-prefix mismatches.
147
+ */
148
+ function detectStaleFamilies(manifest, projectRoot, testsRoot) {
149
+ const roots = new Set([(0, path_1.resolve)(projectRoot)]);
150
+ if (testsRoot)
151
+ roots.add((0, path_1.resolve)(testsRoot));
152
+ // Also try to discover the git repo root — manifest paths may be repo-relative
153
+ try {
154
+ const gitRoot = (0, child_process_1.execFileSync)('git', ['rev-parse', '--show-toplevel'], {
155
+ cwd: projectRoot,
156
+ encoding: 'utf-8',
157
+ stdio: ['pipe', 'pipe', 'pipe'],
158
+ }).trim();
159
+ if (gitRoot)
160
+ roots.add((0, path_1.resolve)(gitRoot));
161
+ }
162
+ catch {
163
+ // Not a git repo or git not available — that's fine
164
+ }
117
165
  const stale = [];
118
166
  for (const family of manifest.families) {
119
167
  const allPatterns = [
@@ -123,15 +171,34 @@ function detectStaleFamilies(manifest, projectRoot) {
123
171
  ];
124
172
  if (allPatterns.length === 0)
125
173
  continue;
126
- // Check if any pattern resolves to existing files/dirs
174
+ // Check if any pattern resolves to existing files/dirs in any root
127
175
  let hasAny = false;
128
176
  for (const pattern of allPatterns) {
129
177
  // Strip trailing glob (* or **) to get the directory
130
178
  const dirPart = pattern.replace(/\/?\*.*$/, '');
131
- if (dirPart && (0, fs_1.existsSync)((0, path_1.join)(resolved, dirPart))) {
132
- hasAny = true;
133
- break;
179
+ if (!dirPart)
180
+ continue;
181
+ // For file-level patterns like "server/channels/api4/draft*.go",
182
+ // dirPart is "server/channels/api4/draft" — check the parent dir instead
183
+ const isFileGlob = /\.\w+$/.test(pattern);
184
+ const pathsToCheck = [dirPart];
185
+ if (isFileGlob) {
186
+ const parentDir = dirPart.split('/').slice(0, -1).join('/');
187
+ if (parentDir)
188
+ pathsToCheck.push(parentDir);
189
+ }
190
+ for (const checkPath of pathsToCheck) {
191
+ for (const root of roots) {
192
+ if ((0, fs_1.existsSync)((0, path_1.join)(root, checkPath))) {
193
+ hasAny = true;
194
+ break;
195
+ }
196
+ }
197
+ if (hasAny)
198
+ break;
134
199
  }
200
+ if (hasAny)
201
+ break;
135
202
  }
136
203
  if (!hasAny) {
137
204
  stale.push(family.id);
@@ -1,5 +1,18 @@
1
- import type { DiscoveredDir, ScanResult } from './types.js';
1
+ import type { DiscoveredDir, ScannedFamily, ScanResult } from './types.js';
2
2
  export declare function discoverSourceDirs(projectRoot: string): DiscoveredDir[];
3
3
  export declare function discoverTestDirs(projectRoot: string): DiscoveredDir[];
4
- export declare function scanProject(projectRoot: string): ScanResult;
4
+ /**
5
+ * Discover families by scanning server Go source files.
6
+ *
7
+ * The backend follows a three-tier pattern:
8
+ * api4/draft.go + app/draft.go + store/sqlstore/draft_store.go
9
+ *
10
+ * Related files are grouped under parent domains:
11
+ * channel.go, channel_bookmark.go, channel_category.go → "channel" family
12
+ *
13
+ * Each domain becomes a candidate family with precise serverPaths.
14
+ */
15
+ export declare function discoverServerDerivedFamilies(serverRoot: string): ScannedFamily[];
16
+ export declare function discoverTestDerivedFamilies(testsRoot: string): ScannedFamily[];
17
+ export declare function scanProject(projectRoot: string, testsRoot?: string, serverRoot?: string): ScanResult;
5
18
  //# sourceMappingURL=scanner.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"scanner.d.ts","sourceRoot":"","sources":["../../src/training/scanner.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAC,aAAa,EAAiC,UAAU,EAAC,MAAM,YAAY,CAAC;AAgGzF,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,aAAa,EAAE,CA+BvE;AAED,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,MAAM,GAAG,aAAa,EAAE,CA6DrE;AA6ID,wBAAgB,WAAW,CAAC,WAAW,EAAE,MAAM,GAAG,UAAU,CA+E3D"}
1
+ {"version":3,"file":"scanner.d.ts","sourceRoot":"","sources":["../../src/training/scanner.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAC,aAAa,EAAE,aAAa,EAAkB,UAAU,EAAC,MAAM,YAAY,CAAC;AAgJzF,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,aAAa,EAAE,CA+BvE;AAED,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,MAAM,GAAG,aAAa,EAAE,CA6DrE;AAuLD;;;;;;;;;;GAUG;AACH,wBAAgB,6BAA6B,CAAC,UAAU,EAAE,MAAM,GAAG,aAAa,EAAE,CA0HjF;AAED,wBAAgB,2BAA2B,CAAC,SAAS,EAAE,MAAM,GAAG,aAAa,EAAE,CAiG9E;AAED,wBAAgB,WAAW,CAAC,WAAW,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,UAAU,CA6IpG"}
@@ -4,6 +4,8 @@
4
4
  Object.defineProperty(exports, "__esModule", { value: true });
5
5
  exports.discoverSourceDirs = discoverSourceDirs;
6
6
  exports.discoverTestDirs = discoverTestDirs;
7
+ exports.discoverServerDerivedFamilies = discoverServerDerivedFamilies;
8
+ exports.discoverTestDerivedFamilies = discoverTestDerivedFamilies;
7
9
  exports.scanProject = scanProject;
8
10
  const fs_1 = require("fs");
9
11
  const path_1 = require("path");
@@ -20,6 +22,50 @@ const SKIP_DIRS = new Set([
20
22
  ]);
21
23
  const TEST_EXTENSIONS = ['.spec.ts', '.test.ts', '.spec.js', '.test.js', '.spec.tsx', '.test.tsx'];
22
24
  const GO_TEST_SUFFIX = '_test.go';
25
+ /**
26
+ * Test category directories that organize tests but aren't feature families.
27
+ * Test-only families matching these names are excluded.
28
+ */
29
+ const TEST_CATEGORY_DIRS = new Set([
30
+ 'specs', 'spec', 'accessibility', 'visual', 'smoke', 'regression',
31
+ 'integration', 'functional', 'unit', 'e2e', 'performance', 'load',
32
+ ]);
33
+ /**
34
+ * Structural directories that are code-organization concerns, not feature families.
35
+ * Discovered source dirs matching these names are excluded from family creation.
36
+ */
37
+ const STRUCTURAL_DIRS = new Set([
38
+ 'actions', 'client', 'components', 'hooks', 'i18n', 'packages',
39
+ 'reducers', 'selectors', 'store', 'stores', 'tests', 'types',
40
+ 'utils', 'helpers', 'lib', 'common', 'shared', 'constants',
41
+ 'config', 'styles', 'sass', 'css', 'assets', 'images', 'fonts',
42
+ 'middleware', 'contexts', 'providers', 'layouts', 'templates',
43
+ ]);
44
+ /**
45
+ * Server Go files that are infrastructure / cross-cutting concerns,
46
+ * not feature-specific domains. Matched after stripping _local/_store suffixes.
47
+ */
48
+ const SERVER_INFRA_FILES = new Set([
49
+ 'api', 'apitestlib', 'context', 'helpers', 'params', 'swagger',
50
+ 'app', 'server', 'enterprise', 'product_service', 'security_update_check',
51
+ 'store', 'adapters', 'errors', 'integrity', 'migrate', 'doc',
52
+ 'main', 'init', 'cluster_discovery', 'web_conn', 'web_broadcast_hooks',
53
+ 'manualtesting', 'testlib', 'router', 'handler', 'opentracing',
54
+ 'platform', 'focalboard', 'playbooks', 'client4', 'model',
55
+ 'manifest', 'permission', 'log', 'utils',
56
+ ]);
57
+ /**
58
+ * Server tier directories to scan for Go domain files.
59
+ * Each tier represents a layer of the backend architecture.
60
+ */
61
+ const SERVER_TIERS = [
62
+ 'channels/api4',
63
+ 'channels/app',
64
+ 'channels/store/sqlstore',
65
+ 'channels/web',
66
+ 'channels/wsapi',
67
+ 'public/model',
68
+ ];
23
69
  /** Type-safe includes check for readonly arrays */
24
70
  const includes = (arr, v) => arr.includes(v);
25
71
  function isSkipped(name) {
@@ -319,10 +365,273 @@ function detectFeatures(familyId, group, projectRoot) {
319
365
  }
320
366
  return features;
321
367
  }
322
- function scanProject(projectRoot) {
368
+ /**
369
+ * Discover families by walking the test directory tree at depth ≥ 2.
370
+ *
371
+ * This is the primary family discovery mechanism for projects where source
372
+ * code is organized by code type (components/, actions/) but tests are
373
+ * organized by feature (channels/drafts/, channels/search/).
374
+ *
375
+ * Each leaf test directory (containing spec files) at meaningful depth ≥ 2
376
+ * becomes a candidate family. Top-level feature dirs (depth 1) are already
377
+ * discovered by the standard `discoverTestDirs` + `groupByFamily` pipeline.
378
+ */
379
+ /**
380
+ * Normalize a Go filename into a family domain identifier.
381
+ * Strips _local, _store, trailing 's' (plurals), and normalizes casing.
382
+ */
383
+ function normalizeServerDomain(baseName) {
384
+ let name = baseName;
385
+ // Strip common suffixes
386
+ name = name.replace(/_local$/, '');
387
+ name = name.replace(/_store$/, '');
388
+ // Skip very short names (e.g., single-letter files)
389
+ if (name.length < 3)
390
+ return null;
391
+ return normalizeId(name);
392
+ }
393
+ /**
394
+ * Given a domain name like "channel_bookmark", find its parent domain
395
+ * if a shorter prefix exists in the set (e.g., "channel").
396
+ * This groups related server files under a single family.
397
+ */
398
+ function findParentDomain(name, allDomains) {
399
+ const parts = name.split('_');
400
+ // Try progressively shorter prefixes
401
+ for (let i = parts.length - 1; i >= 1; i--) {
402
+ const candidate = parts.slice(0, i).join('_');
403
+ if (allDomains.has(candidate) && candidate !== name) {
404
+ return candidate;
405
+ }
406
+ }
407
+ return name;
408
+ }
409
+ /**
410
+ * Discover families by scanning server Go source files.
411
+ *
412
+ * The backend follows a three-tier pattern:
413
+ * api4/draft.go + app/draft.go + store/sqlstore/draft_store.go
414
+ *
415
+ * Related files are grouped under parent domains:
416
+ * channel.go, channel_bookmark.go, channel_category.go → "channel" family
417
+ *
418
+ * Each domain becomes a candidate family with precise serverPaths.
419
+ */
420
+ function discoverServerDerivedFamilies(serverRoot) {
421
+ const resolved = (0, path_1.resolve)(serverRoot);
422
+ // First pass: collect all raw domain names across tiers
423
+ const allRawDomains = new Set();
424
+ // domain → tier → Set<file basenames>
425
+ const domainTierFiles = new Map();
426
+ function collectGoFile(entry, tierRelPath) {
427
+ if (!entry.endsWith('.go') || entry.endsWith('_test.go') || entry.startsWith('.'))
428
+ return;
429
+ const baseName = entry.replace('.go', '');
430
+ const domain = normalizeServerDomain(baseName);
431
+ if (!domain || SERVER_INFRA_FILES.has(domain))
432
+ return;
433
+ allRawDomains.add(domain);
434
+ if (!domainTierFiles.has(domain))
435
+ domainTierFiles.set(domain, new Map());
436
+ const tierMap = domainTierFiles.get(domain);
437
+ if (!tierMap.has(tierRelPath))
438
+ tierMap.set(tierRelPath, new Set());
439
+ tierMap.get(tierRelPath).add(baseName);
440
+ }
441
+ for (const tier of SERVER_TIERS) {
442
+ const tierPath = (0, path_1.join)(resolved, tier);
443
+ if (!(0, fs_1.existsSync)(tierPath))
444
+ continue;
445
+ let entries;
446
+ try {
447
+ entries = (0, fs_1.readdirSync)(tierPath);
448
+ }
449
+ catch {
450
+ continue;
451
+ }
452
+ for (const entry of entries) {
453
+ collectGoFile(entry, tier);
454
+ // Also check subdirectories (e.g., app/slashcommands/, app/users/)
455
+ const subPath = (0, path_1.join)(tierPath, entry);
456
+ try {
457
+ const stat = (0, fs_1.lstatSync)(subPath);
458
+ if (stat.isDirectory() && !isSkipped(entry)) {
459
+ const subEntries = (0, fs_1.readdirSync)(subPath);
460
+ for (const subEntry of subEntries) {
461
+ collectGoFile(subEntry, `${tier}/${entry}`);
462
+ }
463
+ }
464
+ }
465
+ catch { /* skip */ }
466
+ }
467
+ }
468
+ // Scan job directories — each subdirectory is a job type
469
+ const jobsPath = (0, path_1.join)(resolved, 'channels/jobs');
470
+ if ((0, fs_1.existsSync)(jobsPath)) {
471
+ try {
472
+ for (const entry of (0, fs_1.readdirSync)(jobsPath)) {
473
+ const jobPath = (0, path_1.join)(jobsPath, entry);
474
+ try {
475
+ if (!(0, fs_1.lstatSync)(jobPath).isDirectory() || isSkipped(entry))
476
+ continue;
477
+ const domain = normalizeId(entry);
478
+ if (SERVER_INFRA_FILES.has(domain))
479
+ continue;
480
+ allRawDomains.add(domain);
481
+ const jobFiles = (0, fs_1.readdirSync)(jobPath);
482
+ for (const jf of jobFiles) {
483
+ if (jf.endsWith('.go') && !jf.endsWith('_test.go')) {
484
+ if (!domainTierFiles.has(domain))
485
+ domainTierFiles.set(domain, new Map());
486
+ const tierMap = domainTierFiles.get(domain);
487
+ const tierKey = `channels/jobs/${entry}`;
488
+ if (!tierMap.has(tierKey))
489
+ tierMap.set(tierKey, new Set());
490
+ tierMap.get(tierKey).add(jf.replace('.go', ''));
491
+ }
492
+ }
493
+ }
494
+ catch { /* skip */ }
495
+ }
496
+ }
497
+ catch { /* skip */ }
498
+ }
499
+ // Second pass: group child domains under parents
500
+ // e.g., channel_bookmark → channel, post_priority → post
501
+ // Track which top-level tiers each family touches for significance filtering.
502
+ const familyPaths = new Map();
503
+ const familyTiers = new Map();
504
+ for (const [domain, tierMap] of domainTierFiles) {
505
+ const parentDomain = findParentDomain(domain, allRawDomains);
506
+ if (!familyPaths.has(parentDomain))
507
+ familyPaths.set(parentDomain, new Set());
508
+ if (!familyTiers.has(parentDomain))
509
+ familyTiers.set(parentDomain, new Set());
510
+ const paths = familyPaths.get(parentDomain);
511
+ const tiers = familyTiers.get(parentDomain);
512
+ for (const [tierRelPath, fileNames] of tierMap) {
513
+ // Track the top-level tier (e.g., "channels/api4" from "channels/api4/slashcommands")
514
+ const topTier = tierRelPath.split('/').slice(0, 2).join('/');
515
+ tiers.add(topTier);
516
+ for (const baseName of fileNames) {
517
+ // Use directory-level glob to capture the file and related variants
518
+ paths.add(`server/${tierRelPath}/${baseName}*.go`);
519
+ }
520
+ }
521
+ }
522
+ // Build families from grouped domains.
523
+ // Only include server-only families that span ≥ 2 tiers (architecturally significant).
524
+ const families = [];
525
+ for (const [domain, paths] of familyPaths) {
526
+ if (paths.size === 0)
527
+ continue;
528
+ const tierCount = familyTiers.get(domain)?.size ?? 0;
529
+ if (tierCount < 2)
530
+ continue; // Skip single-tier domains (likely infrastructure)
531
+ families.push({
532
+ id: domain,
533
+ routes: [`/${domain.replace(/_/g, '-')}`],
534
+ webappPaths: [],
535
+ serverPaths: Array.from(paths),
536
+ specDirs: [],
537
+ cypressSpecDirs: [],
538
+ tags: [],
539
+ features: [],
540
+ routesGuessed: true,
541
+ });
542
+ }
543
+ return families;
544
+ }
545
+ function discoverTestDerivedFamilies(testsRoot) {
546
+ const resolved = (0, path_1.resolve)(testsRoot);
547
+ const candidates = [];
548
+ function walk(dir, depth) {
549
+ if (depth > 8)
550
+ return;
551
+ let entries;
552
+ try {
553
+ entries = (0, fs_1.readdirSync)(dir);
554
+ }
555
+ catch {
556
+ return;
557
+ }
558
+ const hasSpecs = entries.some((e) => TEST_EXTENSIONS.some((ext) => e.endsWith(ext)) || e.endsWith(GO_TEST_SUFFIX));
559
+ const subdirs = entries.filter((e) => {
560
+ if (isSkipped(e))
561
+ return false;
562
+ try {
563
+ const stat = (0, fs_1.lstatSync)((0, path_1.join)(dir, e));
564
+ return !stat.isSymbolicLink() && stat.isDirectory();
565
+ }
566
+ catch {
567
+ return false;
568
+ }
569
+ });
570
+ const relPath = (0, path_1.relative)(resolved, dir).replace(/\\/g, '/');
571
+ const parts = relPath.split('/').filter(Boolean);
572
+ const meaningful = parts.filter((p) => !TEST_CATEGORY_DIRS.has(normalizeId(p)) && !isSkipped(p));
573
+ // Depth-2+ meaningful dirs with spec files → candidate families
574
+ if (meaningful.length >= 2 && hasSpecs) {
575
+ const leafId = normalizeId(meaningful[meaningful.length - 1]);
576
+ const parentId = normalizeId(meaningful[meaningful.length - 2]);
577
+ if (!STRUCTURAL_DIRS.has(leafId) && !TEST_CATEGORY_DIRS.has(leafId)) {
578
+ candidates.push({ dir, relPath, leafId, parentId });
579
+ }
580
+ }
581
+ for (const sub of subdirs) {
582
+ walk((0, path_1.join)(dir, sub), depth + 1);
583
+ }
584
+ }
585
+ // Walk from standard test roots
586
+ const testRoots = ['tests', 'test', 'e2e-tests', 'e2e', 'specs', 'spec'];
587
+ for (const root of testRoots) {
588
+ const rootPath = (0, path_1.join)(resolved, root);
589
+ if ((0, fs_1.existsSync)(rootPath)) {
590
+ walk(rootPath, 0);
591
+ }
592
+ }
593
+ // Detect leaf-name collisions across parents
594
+ const idCount = new Map();
595
+ for (const c of candidates) {
596
+ idCount.set(c.leafId, (idCount.get(c.leafId) || 0) + 1);
597
+ }
598
+ // Build families — prefix with parent when names collide
599
+ const familyMap = new Map();
600
+ for (const c of candidates) {
601
+ let familyId = c.leafId;
602
+ if ((idCount.get(c.leafId) || 0) > 1 && c.parentId) {
603
+ familyId = `${c.parentId}_${c.leafId}`;
604
+ }
605
+ if (!familyMap.has(familyId)) {
606
+ const specFiles = getSpecFiles(c.dir);
607
+ familyMap.set(familyId, {
608
+ id: familyId,
609
+ routes: [`/${familyId.replace(/_/g, '-')}`],
610
+ webappPaths: [],
611
+ serverPaths: [],
612
+ specDirs: [c.relPath + '/'],
613
+ cypressSpecDirs: [],
614
+ tags: extractTags(specFiles),
615
+ features: [],
616
+ routesGuessed: true,
617
+ });
618
+ }
619
+ else {
620
+ const existing = familyMap.get(familyId);
621
+ const specDir = c.relPath + '/';
622
+ if (!existing.specDirs.includes(specDir)) {
623
+ existing.specDirs.push(specDir);
624
+ existing.tags = [...new Set([...existing.tags, ...extractTags(getSpecFiles(c.dir))])];
625
+ }
626
+ }
627
+ }
628
+ return Array.from(familyMap.values());
629
+ }
630
+ function scanProject(projectRoot, testsRoot, serverRoot) {
323
631
  const resolved = (0, path_1.resolve)(projectRoot);
632
+ const resolvedTestsRoot = testsRoot ? (0, path_1.resolve)(testsRoot) : resolved;
324
633
  const sourceDirs = discoverSourceDirs(resolved);
325
- const testDirs = discoverTestDirs(resolved);
634
+ const testDirs = discoverTestDirs(resolvedTestsRoot);
326
635
  const allDirs = [...sourceDirs, ...testDirs];
327
636
  const groups = groupByFamily(allDirs);
328
637
  const families = [];
@@ -331,6 +640,13 @@ function scanProject(projectRoot) {
331
640
  const hasTests = group.test.length > 0 || group.cypress.length > 0;
332
641
  if (!hasSrc && !hasTests)
333
642
  continue;
643
+ // Skip structural directories that are code-organization, not features.
644
+ // Only skip if they have source dirs but no corresponding test dirs.
645
+ if (STRUCTURAL_DIRS.has(familyId) && !hasTests)
646
+ continue;
647
+ // Skip test-only families that match broad test categories (not feature families).
648
+ if (!hasSrc && hasTests && TEST_CATEGORY_DIRS.has(familyId))
649
+ continue;
334
650
  const allSpecFiles = [];
335
651
  for (const td of [...group.test, ...group.cypress]) {
336
652
  allSpecFiles.push(...getSpecFiles(td.path));
@@ -348,6 +664,58 @@ function scanProject(projectRoot) {
348
664
  routesGuessed: true,
349
665
  });
350
666
  }
667
+ // When a separate testsRoot is provided, discover families from test
668
+ // directory structure. Projects with feature-organized tests but
669
+ // code-type-organized source benefit from this.
670
+ if (testsRoot) {
671
+ const testFamilies = discoverTestDerivedFamilies(resolvedTestsRoot);
672
+ const existingIds = new Set(families.map((f) => f.id));
673
+ for (const tf of testFamilies) {
674
+ if (existingIds.has(tf.id)) {
675
+ // Merge specDirs into existing family
676
+ const existing = families.find((f) => f.id === tf.id);
677
+ for (const sd of tf.specDirs) {
678
+ if (!existing.specDirs.includes(sd)) {
679
+ existing.specDirs.push(sd);
680
+ }
681
+ }
682
+ existing.tags = [...new Set([...existing.tags, ...tf.tags])];
683
+ }
684
+ else {
685
+ families.push(tf);
686
+ existingIds.add(tf.id);
687
+ }
688
+ }
689
+ }
690
+ // When a separate serverRoot is provided, discover families from Go source
691
+ // filenames across the three-tier backend (api4, app, store).
692
+ if (serverRoot) {
693
+ const serverFamilies = discoverServerDerivedFamilies((0, path_1.resolve)(serverRoot));
694
+ const existingIds = new Set(families.map((f) => f.id));
695
+ for (const sf of serverFamilies) {
696
+ // Try exact match, then singular/plural variants
697
+ let target = families.find((f) => f.id === sf.id);
698
+ if (!target && !sf.id.endsWith('s')) {
699
+ target = families.find((f) => f.id === sf.id + 's');
700
+ }
701
+ if (!target && sf.id.endsWith('s')) {
702
+ target = families.find((f) => f.id === sf.id.slice(0, -1));
703
+ }
704
+ if (target) {
705
+ // Merge serverPaths into existing family
706
+ for (const sp of sf.serverPaths) {
707
+ if (!target.serverPaths.includes(sp)) {
708
+ target.serverPaths.push(sp);
709
+ }
710
+ }
711
+ }
712
+ else {
713
+ // New server-only family
714
+ families.push(sf);
715
+ existingIds.add(sf.id);
716
+ }
717
+ }
718
+ }
351
719
  const familyIds = new Set(families.map((f) => f.id));
352
720
  const unmatchedSourceDirs = sourceDirs.filter((d) => !familyIds.has(normalizeId(d.familyHint)));
353
721
  const unmatchedTestDirs = testDirs.filter((d) => !familyIds.has(normalizeId(d.familyHint)));
@@ -87,6 +87,10 @@ export interface TrainOptions {
87
87
  appPath: string;
88
88
  /** Path to tests root (may differ from appPath) */
89
89
  testsRoot: string;
90
+ /** Path to server/backend root (may differ from appPath) */
91
+ serverRoot?: string;
92
+ /** Git repo root for monorepo-aware validation */
93
+ gitRepoRoot?: string;
90
94
  /** Enable LLM enrichment (default: true) */
91
95
  enrich: boolean;
92
96
  /** Run validation against git history */
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/training/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/training/types.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yasserkhanorg/e2e-agents",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
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",