@yasserkhanorg/e2e-agents 0.5.16 → 0.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.
Files changed (105) hide show
  1. package/dist/agent/pipeline.d.ts +1 -1
  2. package/dist/agent/pipeline.d.ts.map +1 -1
  3. package/dist/agent/plan.d.ts +0 -12
  4. package/dist/agent/plan.d.ts.map +1 -1
  5. package/dist/agent/plan.js +0 -365
  6. package/dist/agent/types.d.ts +42 -0
  7. package/dist/agent/types.d.ts.map +1 -0
  8. package/dist/agent/types.js +4 -0
  9. package/dist/api.d.ts +10 -14
  10. package/dist/api.d.ts.map +1 -1
  11. package/dist/api.js +29 -59
  12. package/dist/cli.js +41 -174
  13. package/dist/engine/impact_engine.d.ts +36 -0
  14. package/dist/engine/impact_engine.d.ts.map +1 -0
  15. package/dist/engine/impact_engine.js +196 -0
  16. package/dist/engine/plan_builder.d.ts +9 -0
  17. package/dist/engine/plan_builder.d.ts.map +1 -0
  18. package/dist/engine/plan_builder.js +329 -0
  19. package/dist/esm/agent/plan.js +1 -360
  20. package/dist/esm/agent/types.js +3 -0
  21. package/dist/esm/api.js +27 -56
  22. package/dist/esm/cli.js +40 -173
  23. package/dist/esm/engine/impact_engine.js +191 -0
  24. package/dist/esm/engine/plan_builder.js +323 -0
  25. package/dist/esm/index.js +6 -3
  26. package/dist/esm/knowledge/route_families.js +57 -0
  27. package/dist/index.d.ts +9 -4
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +14 -5
  30. package/dist/knowledge/route_families.d.ts +19 -0
  31. package/dist/knowledge/route_families.d.ts.map +1 -1
  32. package/dist/knowledge/route_families.js +60 -0
  33. package/package.json +1 -1
  34. package/dist/agent/ai_flow_analysis.d.ts +0 -13
  35. package/dist/agent/ai_flow_analysis.d.ts.map +0 -1
  36. package/dist/agent/ai_flow_analysis.js +0 -334
  37. package/dist/agent/ai_mapping.d.ts +0 -14
  38. package/dist/agent/ai_mapping.d.ts.map +0 -1
  39. package/dist/agent/ai_mapping.js +0 -560
  40. package/dist/agent/analysis.d.ts +0 -64
  41. package/dist/agent/analysis.d.ts.map +0 -1
  42. package/dist/agent/analysis.js +0 -292
  43. package/dist/agent/blast_radius.d.ts +0 -4
  44. package/dist/agent/blast_radius.d.ts.map +0 -1
  45. package/dist/agent/blast_radius.js +0 -37
  46. package/dist/agent/dependency_graph.d.ts +0 -14
  47. package/dist/agent/dependency_graph.d.ts.map +0 -1
  48. package/dist/agent/dependency_graph.js +0 -227
  49. package/dist/agent/flags.d.ts +0 -23
  50. package/dist/agent/flags.d.ts.map +0 -1
  51. package/dist/agent/flags.js +0 -171
  52. package/dist/agent/flow_catalog.d.ts +0 -25
  53. package/dist/agent/flow_catalog.d.ts.map +0 -1
  54. package/dist/agent/flow_catalog.js +0 -115
  55. package/dist/agent/flow_mapping.d.ts +0 -10
  56. package/dist/agent/flow_mapping.d.ts.map +0 -1
  57. package/dist/agent/flow_mapping.js +0 -84
  58. package/dist/agent/framework.d.ts +0 -13
  59. package/dist/agent/framework.d.ts.map +0 -1
  60. package/dist/agent/framework.js +0 -149
  61. package/dist/agent/gap_suggestions.d.ts +0 -14
  62. package/dist/agent/gap_suggestions.d.ts.map +0 -1
  63. package/dist/agent/gap_suggestions.js +0 -101
  64. package/dist/agent/generator.d.ts +0 -10
  65. package/dist/agent/generator.d.ts.map +0 -1
  66. package/dist/agent/generator.js +0 -115
  67. package/dist/agent/operational_insights.d.ts +0 -41
  68. package/dist/agent/operational_insights.d.ts.map +0 -1
  69. package/dist/agent/operational_insights.js +0 -127
  70. package/dist/agent/report.d.ts +0 -97
  71. package/dist/agent/report.d.ts.map +0 -1
  72. package/dist/agent/report.js +0 -159
  73. package/dist/agent/runner.d.ts +0 -7
  74. package/dist/agent/runner.d.ts.map +0 -1
  75. package/dist/agent/runner.js +0 -898
  76. package/dist/agent/selectors.d.ts +0 -10
  77. package/dist/agent/selectors.d.ts.map +0 -1
  78. package/dist/agent/selectors.js +0 -75
  79. package/dist/agent/subsystem_risk.d.ts +0 -23
  80. package/dist/agent/subsystem_risk.d.ts.map +0 -1
  81. package/dist/agent/subsystem_risk.js +0 -207
  82. package/dist/agent/tests.d.ts +0 -19
  83. package/dist/agent/tests.d.ts.map +0 -1
  84. package/dist/agent/tests.js +0 -116
  85. package/dist/agent/traceability.d.ts +0 -22
  86. package/dist/agent/traceability.d.ts.map +0 -1
  87. package/dist/agent/traceability.js +0 -183
  88. package/dist/esm/agent/ai_flow_analysis.js +0 -331
  89. package/dist/esm/agent/ai_mapping.js +0 -557
  90. package/dist/esm/agent/analysis.js +0 -287
  91. package/dist/esm/agent/blast_radius.js +0 -34
  92. package/dist/esm/agent/dependency_graph.js +0 -224
  93. package/dist/esm/agent/flags.js +0 -160
  94. package/dist/esm/agent/flow_catalog.js +0 -112
  95. package/dist/esm/agent/flow_mapping.js +0 -81
  96. package/dist/esm/agent/framework.js +0 -145
  97. package/dist/esm/agent/gap_suggestions.js +0 -98
  98. package/dist/esm/agent/generator.js +0 -112
  99. package/dist/esm/agent/operational_insights.js +0 -124
  100. package/dist/esm/agent/report.js +0 -156
  101. package/dist/esm/agent/runner.js +0 -894
  102. package/dist/esm/agent/selectors.js +0 -71
  103. package/dist/esm/agent/subsystem_risk.js +0 -204
  104. package/dist/esm/agent/tests.js +0 -111
  105. package/dist/esm/agent/traceability.js +0 -180
@@ -1,160 +0,0 @@
1
- // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
2
- // See LICENSE.txt for license information.
3
- const ROLE_ORDER = [
4
- 'system_admin',
5
- 'team_admin',
6
- 'channel_admin',
7
- 'member',
8
- 'guest',
9
- 'deactivated',
10
- ];
11
- const FEATURE_FLAG_REGEX = /\bFeatureFlags?\.(\w+)\b/g;
12
- const FEATURE_FLAG_STRING_REGEX = /\b(?:isFeatureEnabled|getFeatureFlag|featureFlag)\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
13
- const SERVICE_SETTINGS_REGEX = /\bServiceSettings\.(\w+)\b/g;
14
- const TEST_GATE_REGEX = /\bskipIfFeatureFlagNotSet\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
15
- const ROLE_ALIASES = {
16
- 'system admin': 'system_admin',
17
- 'system_admin': 'system_admin',
18
- sysadmin: 'system_admin',
19
- 'team admin': 'team_admin',
20
- 'team_admin': 'team_admin',
21
- 'channel admin': 'channel_admin',
22
- 'channel_admin': 'channel_admin',
23
- member: 'member',
24
- members: 'member',
25
- guest: 'guest',
26
- guests: 'guest',
27
- deactivated: 'deactivated',
28
- inactive: 'deactivated',
29
- disabled: 'deactivated',
30
- };
31
- export function normalizeRole(role) {
32
- const key = role.trim().toLowerCase();
33
- return ROLE_ALIASES[key] ?? null;
34
- }
35
- export function normalizeRoles(roles, fallback) {
36
- const normalized = roles
37
- .map((role) => normalizeRole(role))
38
- .filter((role) => Boolean(role));
39
- const combined = normalized.length > 0 ? normalized : fallback;
40
- const unique = new Set(combined);
41
- return ROLE_ORDER.filter((role) => unique.has(role));
42
- }
43
- export function normalizeFlagSource(source) {
44
- if (!source) {
45
- return 'featureFlag';
46
- }
47
- const value = source.trim().toLowerCase();
48
- if (['feature', 'featureflag', 'feature_flag', 'flag'].includes(value)) {
49
- return 'featureFlag';
50
- }
51
- if (['config', 'service', 'servicesettings', 'server'].includes(value)) {
52
- return 'configFlag';
53
- }
54
- if (['test', 'gate', 'testgate'].includes(value)) {
55
- return 'testGate';
56
- }
57
- return 'featureFlag';
58
- }
59
- export function normalizeFlagState(value, fallback) {
60
- if (!value) {
61
- return fallback;
62
- }
63
- const lowered = value.trim().toLowerCase();
64
- if (lowered === 'on' || lowered === 'off' || lowered === 'unknown') {
65
- return lowered;
66
- }
67
- return fallback;
68
- }
69
- export function mergeFlags(flags, defaultState) {
70
- const map = new Map();
71
- for (const flag of flags) {
72
- const key = `${flag.source}:${flag.name.toLowerCase()}`;
73
- if (!map.has(key)) {
74
- map.set(key, {
75
- ...flag,
76
- defaultState: flag.defaultState ?? defaultState,
77
- });
78
- }
79
- }
80
- return Array.from(map.values());
81
- }
82
- export function extractFlagHits(content, config) {
83
- if (!content) {
84
- return [];
85
- }
86
- const hits = [];
87
- const defaultState = config.flags.defaultState;
88
- for (const match of content.matchAll(FEATURE_FLAG_REGEX)) {
89
- if (match[1]) {
90
- hits.push({ name: match[1], source: 'featureFlag', defaultState });
91
- }
92
- }
93
- for (const match of content.matchAll(FEATURE_FLAG_STRING_REGEX)) {
94
- if (match[1]) {
95
- hits.push({ name: match[1], source: 'featureFlag', defaultState });
96
- }
97
- }
98
- for (const match of content.matchAll(SERVICE_SETTINGS_REGEX)) {
99
- if (match[1]) {
100
- hits.push({ name: match[1], source: 'configFlag', defaultState });
101
- }
102
- }
103
- for (const match of content.matchAll(TEST_GATE_REGEX)) {
104
- if (match[1]) {
105
- hits.push({ name: match[1], source: 'testGate', defaultState });
106
- }
107
- }
108
- return mergeFlags(hits, defaultState);
109
- }
110
- export function inferAudienceFromPath(relativePath, config) {
111
- const normalized = relativePath.toLowerCase();
112
- if (normalized.includes('admin_console') || normalized.includes('system_console')) {
113
- return normalizeRoles(['system_admin'], config.audience.defaultRoles);
114
- }
115
- if (normalized.includes('team') && normalized.includes('admin')) {
116
- return normalizeRoles(['team_admin'], config.audience.defaultRoles);
117
- }
118
- if (normalized.includes('channel') && normalized.includes('admin')) {
119
- return normalizeRoles(['channel_admin'], config.audience.defaultRoles);
120
- }
121
- return normalizeRoles(config.audience.defaultRoles, config.audience.defaultRoles);
122
- }
123
- export function formatFlags(flags) {
124
- if (flags.length === 0) {
125
- return 'none';
126
- }
127
- return flags.map((flag) => `${flag.name} (${flag.defaultState})`).join(', ');
128
- }
129
- export function computeBlastRadius(audience, flags, config) {
130
- const normalizedAudience = normalizeRoles(audience, config.audience.defaultRoles);
131
- const normalizedFlags = mergeFlags(flags, config.flags.defaultState);
132
- const hasMember = normalizedAudience.includes('member');
133
- const hasGuest = normalizedAudience.includes('guest');
134
- const hasAdmin = normalizedAudience.some((role) => role === 'system_admin' || role === 'team_admin' || role === 'channel_admin');
135
- const scope = hasMember || hasGuest ? 'broad' : hasAdmin ? 'admin-only' : 'unknown';
136
- const flagState = normalizedFlags.length === 0
137
- ? 'unflagged'
138
- : normalizedFlags.some((flag) => flag.defaultState === 'off')
139
- ? 'flagged-off'
140
- : 'flagged-on';
141
- let scoreDelta = 0;
142
- if (hasMember) {
143
- scoreDelta += config.blastRadius.memberBonus;
144
- }
145
- if (hasGuest) {
146
- scoreDelta += config.blastRadius.guestBonus;
147
- }
148
- if (!hasMember && !hasGuest) {
149
- scoreDelta += config.blastRadius.adminOnlyPenalty;
150
- }
151
- if (normalizedFlags.some((flag) => flag.defaultState === 'off')) {
152
- scoreDelta += config.blastRadius.flagOffPenalty;
153
- }
154
- return {
155
- audience: normalizedAudience,
156
- flags: normalizedFlags,
157
- summary: `${scope}; ${flagState}`,
158
- scoreDelta,
159
- };
160
- }
@@ -1,112 +0,0 @@
1
- // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
2
- // See LICENSE.txt for license information.
3
- import { existsSync, readFileSync, statSync } from 'fs';
4
- import { join } from 'path';
5
- import { normalizeFlagSource, normalizeFlagState, normalizeRoles } from './flags.js';
6
- import { normalizePath, titleCase } from './utils.js';
7
- const catalogCache = new Map();
8
- function normalizePriority(value) {
9
- const upper = value.toUpperCase();
10
- if (upper === 'P0' || upper === 'P1' || upper === 'P2') {
11
- return upper;
12
- }
13
- return null;
14
- }
15
- function normalizeEntry(entry, config) {
16
- if (!entry.id || !entry.priority) {
17
- return null;
18
- }
19
- const priority = normalizePriority(entry.priority);
20
- if (!priority) {
21
- return null;
22
- }
23
- const rawAudience = Array.isArray(entry.audience)
24
- ? entry.audience.filter((role) => typeof role === 'string')
25
- : [];
26
- const normalizedAudience = normalizeRoles(rawAudience, config.audience.defaultRoles);
27
- const rawFlags = Array.isArray(entry.flags) ? entry.flags : [];
28
- const normalizedFlags = [];
29
- for (const flag of rawFlags) {
30
- if (typeof flag === 'string') {
31
- normalizedFlags.push({
32
- name: flag,
33
- source: 'featureFlag',
34
- defaultState: config.flags.defaultState,
35
- });
36
- continue;
37
- }
38
- if (flag && typeof flag === 'object' && typeof flag.name === 'string') {
39
- normalizedFlags.push({
40
- name: flag.name,
41
- source: normalizeFlagSource(flag.source),
42
- defaultState: normalizeFlagState(flag.defaultState, config.flags.defaultState),
43
- });
44
- }
45
- }
46
- return {
47
- ...entry,
48
- id: normalizePath(entry.id),
49
- name: entry.name || titleCase(entry.id),
50
- priority,
51
- keywords: (entry.keywords || []).map((keyword) => keyword.toLowerCase()),
52
- paths: (entry.paths || []).map((path) => normalizePath(path)),
53
- tests: (entry.tests || []).map((path) => normalizePath(path)),
54
- audience: normalizedAudience,
55
- flags: normalizedFlags,
56
- };
57
- }
58
- function readCatalog(path, config) {
59
- try {
60
- if (!existsSync(path)) {
61
- return null;
62
- }
63
- const mtimeMs = statSync(path).mtimeMs;
64
- const cached = catalogCache.get(path);
65
- if (cached && cached.mtimeMs === mtimeMs) {
66
- return cached.catalog;
67
- }
68
- const raw = JSON.parse(readFileSync(path, 'utf-8'));
69
- if (!raw.flows || !Array.isArray(raw.flows)) {
70
- if (config.profile === 'mattermost') {
71
- throw new Error(`Mattermost profile requires a non-empty flow catalog schema at ${path}.`);
72
- }
73
- catalogCache.set(path, { mtimeMs, catalog: null });
74
- return null;
75
- }
76
- const flows = raw.flows
77
- .map((flow) => normalizeEntry(flow, config))
78
- .filter((flow) => Boolean(flow));
79
- if (flows.length === 0) {
80
- if (config.profile === 'mattermost') {
81
- throw new Error(`Mattermost profile requires at least one valid flow catalog entry at ${path}.`);
82
- }
83
- catalogCache.set(path, { mtimeMs, catalog: null });
84
- return null;
85
- }
86
- const catalog = { flows, source: path };
87
- catalogCache.set(path, { mtimeMs, catalog });
88
- return catalog;
89
- }
90
- catch (error) {
91
- if (config.profile === 'mattermost') {
92
- throw error;
93
- }
94
- return null;
95
- }
96
- }
97
- export function loadFlowCatalog(config) {
98
- const candidates = [];
99
- if (config.flowCatalogPath) {
100
- candidates.push(config.flowCatalogPath);
101
- }
102
- const testsRoot = config.testsRoot || config.path;
103
- candidates.push(join(testsRoot, '.e2e-ai-agents', 'flows.json'));
104
- candidates.push(join(config.path, '.e2e-ai-agents', 'flows.json'));
105
- for (const candidate of candidates) {
106
- const catalog = readCatalog(candidate, config);
107
- if (catalog) {
108
- return catalog;
109
- }
110
- }
111
- return null;
112
- }
@@ -1,81 +0,0 @@
1
- // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
2
- // See LICENSE.txt for license information.
3
- import { matchGlob, normalizePath, tokenize, uniqueTokens } from './utils.js';
4
- function pathMatches(patterns, filePath) {
5
- for (const pattern of patterns) {
6
- if (matchGlob(filePath, pattern)) {
7
- return pattern;
8
- }
9
- }
10
- return null;
11
- }
12
- function keywordMatches(keywords, filePath) {
13
- const tokens = tokenize(filePath);
14
- for (const keyword of keywords) {
15
- if (tokens.includes(keyword.toLowerCase())) {
16
- return keyword;
17
- }
18
- }
19
- return null;
20
- }
21
- export function mapChangesToCatalogFlows(catalog, changedFiles, mode, config) {
22
- const warnings = [];
23
- const flows = [];
24
- const testsByFlow = new Map();
25
- const normalizedChanges = Array.from(new Set(changedFiles.map((file) => normalizePath(file))));
26
- for (const flow of catalog.flows) {
27
- const reasons = [];
28
- const matchedFiles = new Set();
29
- let matched = false;
30
- if (flow.paths && flow.paths.length > 0) {
31
- for (const file of normalizedChanges) {
32
- const match = pathMatches(flow.paths, file);
33
- if (match) {
34
- matchedFiles.add(file);
35
- reasons.push(`Path match: ${match}`);
36
- matched = true;
37
- }
38
- }
39
- }
40
- if (!matched && flow.keywords && flow.keywords.length > 0) {
41
- for (const file of normalizedChanges) {
42
- const keyword = keywordMatches(flow.keywords, file);
43
- if (keyword) {
44
- matchedFiles.add(file);
45
- reasons.push(`Keyword match: ${keyword}`);
46
- matched = true;
47
- }
48
- }
49
- }
50
- if (mode === 'impact' && !matched) {
51
- continue;
52
- }
53
- if (mode === 'gap' && reasons.length === 0) {
54
- reasons.push('Catalog flow');
55
- }
56
- const priorityScore = config.catalogScoring?.priorityScores?.[flow.priority] ??
57
- (flow.priority === 'P0' ? 10 : flow.priority === 'P1' ? 6 : 3);
58
- const fileMatchWeight = config.catalogScoring?.fileMatchWeight ?? 1;
59
- const score = priorityScore + matchedFiles.size * fileMatchWeight;
60
- const matchedFilesList = Array.from(matchedFiles);
61
- flows.push({
62
- id: flow.id,
63
- name: flow.name || flow.id,
64
- kind: 'flow',
65
- score,
66
- priority: flow.priority,
67
- reasons: uniqueTokens(reasons),
68
- keywords: flow.keywords || [],
69
- files: uniqueTokens(matchedFilesList),
70
- audience: flow.audience,
71
- flags: flow.flags,
72
- });
73
- if (flow.tests && flow.tests.length > 0) {
74
- testsByFlow.set(flow.id, flow.tests.map((test) => normalizePath(test)));
75
- }
76
- }
77
- if (flows.length === 0 && mode === 'impact') {
78
- warnings.push('No flow catalog entries matched changed files.');
79
- }
80
- return { flows, testsByFlow, warnings };
81
- }
@@ -1,145 +0,0 @@
1
- // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
2
- // See LICENSE.txt for license information.
3
- import { existsSync, readFileSync } from 'fs';
4
- import { basename, join } from 'path';
5
- const PLAYWRIGHT_CONFIG_FILES = ['playwright.config.ts', 'playwright.config.js'];
6
- const CYPRESS_CONFIG_FILES = ['cypress.config.ts', 'cypress.config.js'];
7
- const SELENIUM_CONFIG_FILES = ['selenium.config.ts', 'selenium.config.js', 'wdio.conf.ts', 'wdio.conf.js'];
8
- function readPackageJson(appRoot) {
9
- const pkgPath = join(appRoot, 'package.json');
10
- if (!existsSync(pkgPath)) {
11
- return undefined;
12
- }
13
- try {
14
- return JSON.parse(readFileSync(pkgPath, 'utf-8'));
15
- }
16
- catch {
17
- return undefined;
18
- }
19
- }
20
- function hasDependency(pkg, dep) {
21
- if (!pkg)
22
- return false;
23
- const dependencies = pkg.dependencies || {};
24
- const devDependencies = pkg.devDependencies || {};
25
- return Boolean(dependencies[dep] || devDependencies[dep]);
26
- }
27
- function findConfigFile(appRoot, candidates) {
28
- for (const file of candidates) {
29
- const fullPath = join(appRoot, file);
30
- if (existsSync(fullPath)) {
31
- return fullPath;
32
- }
33
- }
34
- return undefined;
35
- }
36
- export function detectFramework(appRoot, explicitFramework) {
37
- if (explicitFramework && explicitFramework !== 'auto') {
38
- return {
39
- framework: explicitFramework,
40
- reason: 'explicit',
41
- };
42
- }
43
- const playwrightConfig = findConfigFile(appRoot, PLAYWRIGHT_CONFIG_FILES);
44
- if (playwrightConfig) {
45
- return { framework: 'playwright', configPath: playwrightConfig, reason: 'config' };
46
- }
47
- const cypressConfig = findConfigFile(appRoot, CYPRESS_CONFIG_FILES);
48
- if (cypressConfig) {
49
- return { framework: 'cypress', configPath: cypressConfig, reason: 'config' };
50
- }
51
- const seleniumConfig = findConfigFile(appRoot, SELENIUM_CONFIG_FILES);
52
- if (seleniumConfig) {
53
- return { framework: 'selenium', configPath: seleniumConfig, reason: 'config' };
54
- }
55
- const pkg = readPackageJson(appRoot);
56
- if (hasDependency(pkg, '@playwright/test') || hasDependency(pkg, 'playwright')) {
57
- return { framework: 'playwright', reason: 'package.json' };
58
- }
59
- if (hasDependency(pkg, 'cypress')) {
60
- return { framework: 'cypress', reason: 'package.json' };
61
- }
62
- if (hasDependency(pkg, 'selenium-webdriver') || hasDependency(pkg, 'webdriverio')) {
63
- return { framework: 'selenium', reason: 'package.json' };
64
- }
65
- return { framework: 'unknown', reason: 'unknown' };
66
- }
67
- function extractQuotedStrings(value) {
68
- const matches = value.match(/['"]([^'"]+)['"]/g);
69
- if (!matches) {
70
- return [];
71
- }
72
- return matches.map((match) => match.slice(1, -1)).filter(Boolean);
73
- }
74
- function parsePlaywrightPatterns(content) {
75
- const testDirMatch = content.match(/testDir\s*:\s*['"]([^'"]+)['"]/);
76
- if (testDirMatch) {
77
- const testDir = testDirMatch[1];
78
- return [
79
- join(testDir, '**/*.spec.{ts,tsx,js,jsx}'),
80
- join(testDir, '**/*.test.{ts,tsx,js,jsx}'),
81
- ];
82
- }
83
- const testMatchMatch = content.match(/testMatch\s*:\s*(\[[^\]]+\]|['"][^'"]+['"])/);
84
- if (testMatchMatch) {
85
- const patterns = extractQuotedStrings(testMatchMatch[1]);
86
- if (patterns.length > 0) {
87
- return patterns;
88
- }
89
- }
90
- return [];
91
- }
92
- function parseCypressPatterns(content) {
93
- const specPatternMatch = content.match(/specPattern\s*:\s*(\[[^\]]+\]|['"][^'"]+['"])/);
94
- if (specPatternMatch) {
95
- const patterns = extractQuotedStrings(specPatternMatch[1]);
96
- if (patterns.length > 0) {
97
- return patterns;
98
- }
99
- }
100
- return [];
101
- }
102
- export function resolveTestPatterns(appRoot, detection, explicitPatterns) {
103
- if (explicitPatterns && explicitPatterns.length > 0) {
104
- return { patterns: explicitPatterns, source: 'config' };
105
- }
106
- if (detection.configPath) {
107
- try {
108
- const configContent = readFileSync(detection.configPath, 'utf-8');
109
- if (detection.framework === 'playwright') {
110
- const parsed = parsePlaywrightPatterns(configContent);
111
- if (parsed.length > 0) {
112
- return { patterns: parsed, source: basename(detection.configPath) };
113
- }
114
- }
115
- if (detection.framework === 'cypress') {
116
- const parsed = parseCypressPatterns(configContent);
117
- if (parsed.length > 0) {
118
- return { patterns: parsed, source: basename(detection.configPath) };
119
- }
120
- }
121
- }
122
- catch {
123
- // Fall through to defaults
124
- }
125
- }
126
- if (detection.framework === 'playwright') {
127
- return {
128
- patterns: ['tests/**/*.{spec,test}.{ts,tsx,js,jsx}'],
129
- source: 'default-playwright',
130
- };
131
- }
132
- if (detection.framework === 'cypress') {
133
- return {
134
- patterns: ['cypress/e2e/**/*.cy.{js,jsx,ts,tsx}'],
135
- source: 'default-cypress',
136
- };
137
- }
138
- if (detection.framework === 'selenium') {
139
- return {
140
- patterns: ['tests/selenium/**/*.{spec,test}.{js,ts}'],
141
- source: 'default-selenium',
142
- };
143
- }
144
- return { patterns: [], source: 'none' };
145
- }
@@ -1,98 +0,0 @@
1
- // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
2
- // See LICENSE.txt for license information.
3
- import { resolve } from 'path';
4
- import { isPathWithinRoot } from './utils.js';
5
- function inferTestDir(patterns) {
6
- if (patterns.length === 0) {
7
- return 'tests';
8
- }
9
- const pattern = patterns[0];
10
- const wildcardIndex = pattern.search(/[*{]/);
11
- const base = wildcardIndex === -1 ? pattern : pattern.slice(0, wildcardIndex);
12
- const trimmed = base.replace(/\/+$/, '');
13
- return trimmed || 'tests';
14
- }
15
- function inferExtension(patterns) {
16
- const joined = patterns.join(' ');
17
- if (joined.includes('.ts') || joined.includes('.tsx')) {
18
- return 'ts';
19
- }
20
- return 'js';
21
- }
22
- function normalizeFramework(framework) {
23
- if (framework === 'cypress' || framework === 'selenium') {
24
- return framework;
25
- }
26
- return 'playwright';
27
- }
28
- function buildSkeleton(flow, sourceFiles, framework) {
29
- const linkedFiles = sourceFiles.length > 0 ? sourceFiles.join(', ') : 'N/A';
30
- if (framework === 'cypress') {
31
- return [
32
- `describe('Flow: ${flow.name}', () => {`,
33
- ` it('${flow.priority}: critical coverage for ${flow.id}', () => {`,
34
- " cy.visit('/');",
35
- ` // Linked code areas: ${linkedFiles}`,
36
- ' // TODO: implement critical user path assertions',
37
- ' });',
38
- '});',
39
- '',
40
- ].join('\n');
41
- }
42
- if (framework === 'selenium') {
43
- return [
44
- "const {Builder} = require('selenium-webdriver');",
45
- '',
46
- '(async () => {',
47
- " const driver = await new Builder().forBrowser('chrome').build();",
48
- ' try {',
49
- " await driver.get('http://localhost:3000');",
50
- ` // Linked code areas: ${linkedFiles}`,
51
- ' // TODO: implement critical user path assertions',
52
- ' } finally {',
53
- ' await driver.quit();',
54
- ' }',
55
- '})();',
56
- '',
57
- ].join('\n');
58
- }
59
- return [
60
- "import {test, expect} from '@mattermost/playwright-lib';",
61
- '',
62
- `test('${flow.priority}: ${flow.name} critical path', {tag: '@ai-assisted'}, async ({pw}) => {`,
63
- ' const {user, team} = await pw.initSetup();',
64
- ' const {channelsPage} = await pw.testBrowser.login(user);',
65
- " await channelsPage.goto(team.name);",
66
- ` // Linked code areas: ${linkedFiles}`,
67
- ' // TODO: implement critical user path assertions',
68
- ' await expect(channelsPage.page).toHaveURL(/.*/);',
69
- '});',
70
- '',
71
- ].join('\n');
72
- }
73
- export function buildGapTestSuggestions(testsRoot, flowsWithGaps, framework, testPatterns) {
74
- const testDir = inferTestDir(testPatterns);
75
- const ext = inferExtension(testPatterns);
76
- const resolvedFramework = normalizeFramework(framework);
77
- return flowsWithGaps
78
- .filter((flow) => flow.priority === 'P0' || flow.priority === 'P1')
79
- .map((flow) => {
80
- const fileName = resolvedFramework === 'cypress' ? `${flow.id}.cy.${ext}` : `${flow.id}.spec.${ext}`;
81
- const candidatePath = resolve(testsRoot, testDir, fileName);
82
- const suggestionPath = isPathWithinRoot(testsRoot, candidatePath)
83
- ? candidatePath
84
- : resolve(testsRoot, 'tests', fileName);
85
- const sourceFiles = (flow.files || []).slice(0, 6);
86
- const rationale = flow.reasons.length > 0 ? flow.reasons.join('; ') : 'High priority flow is currently uncovered';
87
- return {
88
- flowId: flow.id,
89
- flowName: flow.name,
90
- priority: flow.priority,
91
- rationale,
92
- sourceFiles,
93
- suggestedTestPath: suggestionPath,
94
- framework: resolvedFramework,
95
- skeleton: buildSkeleton(flow, sourceFiles, resolvedFramework),
96
- };
97
- });
98
- }