@yasserkhanorg/e2e-agents 0.5.16 → 0.7.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 (113) 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 +2 -13
  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 +14 -14
  10. package/dist/api.d.ts.map +1 -1
  11. package/dist/api.js +67 -59
  12. package/dist/cli.js +86 -176
  13. package/dist/engine/ai_enrichment.d.ts +43 -0
  14. package/dist/engine/ai_enrichment.d.ts.map +1 -0
  15. package/dist/engine/ai_enrichment.js +235 -0
  16. package/dist/engine/diff_loader.d.ts +11 -0
  17. package/dist/engine/diff_loader.d.ts.map +1 -0
  18. package/dist/engine/diff_loader.js +74 -0
  19. package/dist/engine/impact_engine.d.ts +36 -0
  20. package/dist/engine/impact_engine.d.ts.map +1 -0
  21. package/dist/engine/impact_engine.js +196 -0
  22. package/dist/engine/plan_builder.d.ts +10 -0
  23. package/dist/engine/plan_builder.d.ts.map +1 -0
  24. package/dist/engine/plan_builder.js +374 -0
  25. package/dist/esm/agent/plan.js +1 -360
  26. package/dist/esm/agent/types.js +3 -0
  27. package/dist/esm/api.js +62 -54
  28. package/dist/esm/cli.js +87 -177
  29. package/dist/esm/engine/ai_enrichment.js +232 -0
  30. package/dist/esm/engine/diff_loader.js +70 -0
  31. package/dist/esm/engine/impact_engine.js +191 -0
  32. package/dist/esm/engine/plan_builder.js +368 -0
  33. package/dist/esm/index.js +6 -3
  34. package/dist/esm/knowledge/route_families.js +59 -1
  35. package/dist/index.d.ts +9 -4
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +14 -5
  38. package/dist/knowledge/route_families.d.ts +19 -0
  39. package/dist/knowledge/route_families.d.ts.map +1 -1
  40. package/dist/knowledge/route_families.js +62 -1
  41. package/package.json +1 -1
  42. package/dist/agent/ai_flow_analysis.d.ts +0 -13
  43. package/dist/agent/ai_flow_analysis.d.ts.map +0 -1
  44. package/dist/agent/ai_flow_analysis.js +0 -334
  45. package/dist/agent/ai_mapping.d.ts +0 -14
  46. package/dist/agent/ai_mapping.d.ts.map +0 -1
  47. package/dist/agent/ai_mapping.js +0 -560
  48. package/dist/agent/analysis.d.ts +0 -64
  49. package/dist/agent/analysis.d.ts.map +0 -1
  50. package/dist/agent/analysis.js +0 -292
  51. package/dist/agent/blast_radius.d.ts +0 -4
  52. package/dist/agent/blast_radius.d.ts.map +0 -1
  53. package/dist/agent/blast_radius.js +0 -37
  54. package/dist/agent/dependency_graph.d.ts +0 -14
  55. package/dist/agent/dependency_graph.d.ts.map +0 -1
  56. package/dist/agent/dependency_graph.js +0 -227
  57. package/dist/agent/flags.d.ts +0 -23
  58. package/dist/agent/flags.d.ts.map +0 -1
  59. package/dist/agent/flags.js +0 -171
  60. package/dist/agent/flow_catalog.d.ts +0 -25
  61. package/dist/agent/flow_catalog.d.ts.map +0 -1
  62. package/dist/agent/flow_catalog.js +0 -115
  63. package/dist/agent/flow_mapping.d.ts +0 -10
  64. package/dist/agent/flow_mapping.d.ts.map +0 -1
  65. package/dist/agent/flow_mapping.js +0 -84
  66. package/dist/agent/framework.d.ts +0 -13
  67. package/dist/agent/framework.d.ts.map +0 -1
  68. package/dist/agent/framework.js +0 -149
  69. package/dist/agent/gap_suggestions.d.ts +0 -14
  70. package/dist/agent/gap_suggestions.d.ts.map +0 -1
  71. package/dist/agent/gap_suggestions.js +0 -101
  72. package/dist/agent/generator.d.ts +0 -10
  73. package/dist/agent/generator.d.ts.map +0 -1
  74. package/dist/agent/generator.js +0 -115
  75. package/dist/agent/operational_insights.d.ts +0 -41
  76. package/dist/agent/operational_insights.d.ts.map +0 -1
  77. package/dist/agent/operational_insights.js +0 -127
  78. package/dist/agent/report.d.ts +0 -97
  79. package/dist/agent/report.d.ts.map +0 -1
  80. package/dist/agent/report.js +0 -159
  81. package/dist/agent/runner.d.ts +0 -7
  82. package/dist/agent/runner.d.ts.map +0 -1
  83. package/dist/agent/runner.js +0 -898
  84. package/dist/agent/selectors.d.ts +0 -10
  85. package/dist/agent/selectors.d.ts.map +0 -1
  86. package/dist/agent/selectors.js +0 -75
  87. package/dist/agent/subsystem_risk.d.ts +0 -23
  88. package/dist/agent/subsystem_risk.d.ts.map +0 -1
  89. package/dist/agent/subsystem_risk.js +0 -207
  90. package/dist/agent/tests.d.ts +0 -19
  91. package/dist/agent/tests.d.ts.map +0 -1
  92. package/dist/agent/tests.js +0 -116
  93. package/dist/agent/traceability.d.ts +0 -22
  94. package/dist/agent/traceability.d.ts.map +0 -1
  95. package/dist/agent/traceability.js +0 -183
  96. package/dist/esm/agent/ai_flow_analysis.js +0 -331
  97. package/dist/esm/agent/ai_mapping.js +0 -557
  98. package/dist/esm/agent/analysis.js +0 -287
  99. package/dist/esm/agent/blast_radius.js +0 -34
  100. package/dist/esm/agent/dependency_graph.js +0 -224
  101. package/dist/esm/agent/flags.js +0 -160
  102. package/dist/esm/agent/flow_catalog.js +0 -112
  103. package/dist/esm/agent/flow_mapping.js +0 -81
  104. package/dist/esm/agent/framework.js +0 -145
  105. package/dist/esm/agent/gap_suggestions.js +0 -98
  106. package/dist/esm/agent/generator.js +0 -112
  107. package/dist/esm/agent/operational_insights.js +0 -124
  108. package/dist/esm/agent/report.js +0 -156
  109. package/dist/esm/agent/runner.js +0 -894
  110. package/dist/esm/agent/selectors.js +0 -71
  111. package/dist/esm/agent/subsystem_risk.js +0 -204
  112. package/dist/esm/agent/tests.js +0 -111
  113. 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
- }