@yasserkhanorg/e2e-agents 0.3.2

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 (221) hide show
  1. package/LICENSE +168 -0
  2. package/README.md +620 -0
  3. package/dist/agent/analysis.d.ts +62 -0
  4. package/dist/agent/analysis.d.ts.map +1 -0
  5. package/dist/agent/analysis.js +292 -0
  6. package/dist/agent/blast_radius.d.ts +4 -0
  7. package/dist/agent/blast_radius.d.ts.map +1 -0
  8. package/dist/agent/blast_radius.js +37 -0
  9. package/dist/agent/cache_utils.d.ts +38 -0
  10. package/dist/agent/cache_utils.d.ts.map +1 -0
  11. package/dist/agent/cache_utils.js +67 -0
  12. package/dist/agent/config.d.ts +148 -0
  13. package/dist/agent/config.d.ts.map +1 -0
  14. package/dist/agent/config.js +640 -0
  15. package/dist/agent/dependency_graph.d.ts +14 -0
  16. package/dist/agent/dependency_graph.d.ts.map +1 -0
  17. package/dist/agent/dependency_graph.js +227 -0
  18. package/dist/agent/feedback.d.ts +55 -0
  19. package/dist/agent/feedback.d.ts.map +1 -0
  20. package/dist/agent/feedback.js +257 -0
  21. package/dist/agent/flags.d.ts +23 -0
  22. package/dist/agent/flags.d.ts.map +1 -0
  23. package/dist/agent/flags.js +171 -0
  24. package/dist/agent/flow_catalog.d.ts +25 -0
  25. package/dist/agent/flow_catalog.d.ts.map +1 -0
  26. package/dist/agent/flow_catalog.js +106 -0
  27. package/dist/agent/flow_mapping.d.ts +10 -0
  28. package/dist/agent/flow_mapping.d.ts.map +1 -0
  29. package/dist/agent/flow_mapping.js +84 -0
  30. package/dist/agent/framework.d.ts +13 -0
  31. package/dist/agent/framework.d.ts.map +1 -0
  32. package/dist/agent/framework.js +149 -0
  33. package/dist/agent/gap_suggestions.d.ts +14 -0
  34. package/dist/agent/gap_suggestions.d.ts.map +1 -0
  35. package/dist/agent/gap_suggestions.js +101 -0
  36. package/dist/agent/generator.d.ts +10 -0
  37. package/dist/agent/generator.d.ts.map +1 -0
  38. package/dist/agent/generator.js +115 -0
  39. package/dist/agent/git.d.ts +11 -0
  40. package/dist/agent/git.d.ts.map +1 -0
  41. package/dist/agent/git.js +90 -0
  42. package/dist/agent/handoff.d.ts +22 -0
  43. package/dist/agent/handoff.d.ts.map +1 -0
  44. package/dist/agent/handoff.js +180 -0
  45. package/dist/agent/impact-analyzer.d.ts +114 -0
  46. package/dist/agent/impact-analyzer.d.ts.map +1 -0
  47. package/dist/agent/impact-analyzer.js +557 -0
  48. package/dist/agent/index.d.ts +21 -0
  49. package/dist/agent/index.d.ts.map +1 -0
  50. package/dist/agent/index.js +38 -0
  51. package/dist/agent/model-router.d.ts +57 -0
  52. package/dist/agent/model-router.d.ts.map +1 -0
  53. package/dist/agent/model-router.js +154 -0
  54. package/dist/agent/operational_insights.d.ts +41 -0
  55. package/dist/agent/operational_insights.d.ts.map +1 -0
  56. package/dist/agent/operational_insights.js +126 -0
  57. package/dist/agent/pipeline.d.ts +23 -0
  58. package/dist/agent/pipeline.d.ts.map +1 -0
  59. package/dist/agent/pipeline.js +609 -0
  60. package/dist/agent/plan.d.ts +91 -0
  61. package/dist/agent/plan.d.ts.map +1 -0
  62. package/dist/agent/plan.js +331 -0
  63. package/dist/agent/playwright_report.d.ts +8 -0
  64. package/dist/agent/playwright_report.d.ts.map +1 -0
  65. package/dist/agent/playwright_report.js +126 -0
  66. package/dist/agent/report-generator.d.ts +24 -0
  67. package/dist/agent/report-generator.d.ts.map +1 -0
  68. package/dist/agent/report-generator.js +250 -0
  69. package/dist/agent/report.d.ts +81 -0
  70. package/dist/agent/report.d.ts.map +1 -0
  71. package/dist/agent/report.js +147 -0
  72. package/dist/agent/runner.d.ts +7 -0
  73. package/dist/agent/runner.d.ts.map +1 -0
  74. package/dist/agent/runner.js +576 -0
  75. package/dist/agent/selectors.d.ts +10 -0
  76. package/dist/agent/selectors.d.ts.map +1 -0
  77. package/dist/agent/selectors.js +75 -0
  78. package/dist/agent/spec-bridge.d.ts +101 -0
  79. package/dist/agent/spec-bridge.d.ts.map +1 -0
  80. package/dist/agent/spec-bridge.js +273 -0
  81. package/dist/agent/spec-builder.d.ts +102 -0
  82. package/dist/agent/spec-builder.d.ts.map +1 -0
  83. package/dist/agent/spec-builder.js +273 -0
  84. package/dist/agent/subsystem_risk.d.ts +23 -0
  85. package/dist/agent/subsystem_risk.d.ts.map +1 -0
  86. package/dist/agent/subsystem_risk.js +207 -0
  87. package/dist/agent/telemetry.d.ts +84 -0
  88. package/dist/agent/telemetry.d.ts.map +1 -0
  89. package/dist/agent/telemetry.js +220 -0
  90. package/dist/agent/test_path.d.ts +2 -0
  91. package/dist/agent/test_path.d.ts.map +1 -0
  92. package/dist/agent/test_path.js +23 -0
  93. package/dist/agent/tests.d.ts +18 -0
  94. package/dist/agent/tests.d.ts.map +1 -0
  95. package/dist/agent/tests.js +106 -0
  96. package/dist/agent/traceability.d.ts +22 -0
  97. package/dist/agent/traceability.d.ts.map +1 -0
  98. package/dist/agent/traceability.js +183 -0
  99. package/dist/agent/traceability_capture.d.ts +18 -0
  100. package/dist/agent/traceability_capture.d.ts.map +1 -0
  101. package/dist/agent/traceability_capture.js +313 -0
  102. package/dist/agent/traceability_ingest.d.ts +21 -0
  103. package/dist/agent/traceability_ingest.d.ts.map +1 -0
  104. package/dist/agent/traceability_ingest.js +237 -0
  105. package/dist/agent/utils.d.ts +13 -0
  106. package/dist/agent/utils.d.ts.map +1 -0
  107. package/dist/agent/utils.js +152 -0
  108. package/dist/agent/validators/selector-validator.d.ts +74 -0
  109. package/dist/agent/validators/selector-validator.d.ts.map +1 -0
  110. package/dist/agent/validators/selector-validator.js +165 -0
  111. package/dist/anthropic_provider.d.ts +65 -0
  112. package/dist/anthropic_provider.d.ts.map +1 -0
  113. package/dist/anthropic_provider.js +332 -0
  114. package/dist/api.d.ts +48 -0
  115. package/dist/api.d.ts.map +1 -0
  116. package/dist/api.js +113 -0
  117. package/dist/base_provider.d.ts +53 -0
  118. package/dist/base_provider.d.ts.map +1 -0
  119. package/dist/base_provider.js +81 -0
  120. package/dist/cli.d.ts +3 -0
  121. package/dist/cli.d.ts.map +1 -0
  122. package/dist/cli.js +843 -0
  123. package/dist/custom_provider.d.ts +20 -0
  124. package/dist/custom_provider.d.ts.map +1 -0
  125. package/dist/custom_provider.js +276 -0
  126. package/dist/e2e-test-gen/index.d.ts +51 -0
  127. package/dist/e2e-test-gen/index.d.ts.map +1 -0
  128. package/dist/e2e-test-gen/index.js +57 -0
  129. package/dist/e2e-test-gen/spec_parser.d.ts +142 -0
  130. package/dist/e2e-test-gen/spec_parser.d.ts.map +1 -0
  131. package/dist/e2e-test-gen/spec_parser.js +786 -0
  132. package/dist/e2e-test-gen/types.d.ts +185 -0
  133. package/dist/e2e-test-gen/types.d.ts.map +1 -0
  134. package/dist/e2e-test-gen/types.js +4 -0
  135. package/dist/esm/agent/analysis.js +287 -0
  136. package/dist/esm/agent/blast_radius.js +34 -0
  137. package/dist/esm/agent/cache_utils.js +63 -0
  138. package/dist/esm/agent/config.js +637 -0
  139. package/dist/esm/agent/dependency_graph.js +224 -0
  140. package/dist/esm/agent/feedback.js +253 -0
  141. package/dist/esm/agent/flags.js +160 -0
  142. package/dist/esm/agent/flow_catalog.js +103 -0
  143. package/dist/esm/agent/flow_mapping.js +81 -0
  144. package/dist/esm/agent/framework.js +145 -0
  145. package/dist/esm/agent/gap_suggestions.js +98 -0
  146. package/dist/esm/agent/generator.js +112 -0
  147. package/dist/esm/agent/git.js +87 -0
  148. package/dist/esm/agent/handoff.js +177 -0
  149. package/dist/esm/agent/impact-analyzer.js +548 -0
  150. package/dist/esm/agent/index.js +22 -0
  151. package/dist/esm/agent/model-router.js +150 -0
  152. package/dist/esm/agent/operational_insights.js +123 -0
  153. package/dist/esm/agent/pipeline.js +605 -0
  154. package/dist/esm/agent/plan.js +324 -0
  155. package/dist/esm/agent/playwright_report.js +123 -0
  156. package/dist/esm/agent/report-generator.js +247 -0
  157. package/dist/esm/agent/report.js +144 -0
  158. package/dist/esm/agent/runner.js +572 -0
  159. package/dist/esm/agent/selectors.js +71 -0
  160. package/dist/esm/agent/spec-bridge.js +267 -0
  161. package/dist/esm/agent/spec-builder.js +267 -0
  162. package/dist/esm/agent/subsystem_risk.js +204 -0
  163. package/dist/esm/agent/telemetry.js +216 -0
  164. package/dist/esm/agent/test_path.js +20 -0
  165. package/dist/esm/agent/tests.js +101 -0
  166. package/dist/esm/agent/traceability.js +180 -0
  167. package/dist/esm/agent/traceability_capture.js +310 -0
  168. package/dist/esm/agent/traceability_ingest.js +234 -0
  169. package/dist/esm/agent/utils.js +138 -0
  170. package/dist/esm/agent/validators/selector-validator.js +160 -0
  171. package/dist/esm/anthropic_provider.js +324 -0
  172. package/dist/esm/api.js +105 -0
  173. package/dist/esm/base_provider.js +77 -0
  174. package/dist/esm/cli.js +841 -0
  175. package/dist/esm/custom_provider.js +272 -0
  176. package/dist/esm/e2e-test-gen/index.js +50 -0
  177. package/dist/esm/e2e-test-gen/spec_parser.js +782 -0
  178. package/dist/esm/e2e-test-gen/types.js +3 -0
  179. package/dist/esm/index.js +16 -0
  180. package/dist/esm/logger.js +89 -0
  181. package/dist/esm/mcp-server.js +465 -0
  182. package/dist/esm/ollama_provider.js +300 -0
  183. package/dist/esm/openai_provider.js +242 -0
  184. package/dist/esm/package.json +3 -0
  185. package/dist/esm/plan-and-test-constants.js +126 -0
  186. package/dist/esm/provider_factory.js +336 -0
  187. package/dist/esm/provider_interface.js +23 -0
  188. package/dist/esm/provider_utils.js +96 -0
  189. package/dist/index.d.ts +31 -0
  190. package/dist/index.d.ts.map +1 -0
  191. package/dist/index.js +41 -0
  192. package/dist/logger.d.ts +23 -0
  193. package/dist/logger.d.ts.map +1 -0
  194. package/dist/logger.js +93 -0
  195. package/dist/mcp-server.d.ts +35 -0
  196. package/dist/mcp-server.d.ts.map +1 -0
  197. package/dist/mcp-server.js +469 -0
  198. package/dist/ollama_provider.d.ts +65 -0
  199. package/dist/ollama_provider.d.ts.map +1 -0
  200. package/dist/ollama_provider.js +308 -0
  201. package/dist/openai_provider.d.ts +23 -0
  202. package/dist/openai_provider.d.ts.map +1 -0
  203. package/dist/openai_provider.js +250 -0
  204. package/dist/plan-and-test-constants.d.ts +110 -0
  205. package/dist/plan-and-test-constants.d.ts.map +1 -0
  206. package/dist/plan-and-test-constants.js +132 -0
  207. package/dist/provider_factory.d.ts +99 -0
  208. package/dist/provider_factory.d.ts.map +1 -0
  209. package/dist/provider_factory.js +341 -0
  210. package/dist/provider_interface.d.ts +358 -0
  211. package/dist/provider_interface.d.ts.map +1 -0
  212. package/dist/provider_interface.js +28 -0
  213. package/dist/provider_utils.d.ts +39 -0
  214. package/dist/provider_utils.d.ts.map +1 -0
  215. package/dist/provider_utils.js +103 -0
  216. package/package.json +101 -0
  217. package/schemas/gap.schema.json +18 -0
  218. package/schemas/impact.schema.json +418 -0
  219. package/schemas/plan.schema.json +285 -0
  220. package/schemas/subsystem-risk-map.schema.json +62 -0
  221. package/schemas/traceability-input.schema.json +122 -0
@@ -0,0 +1,103 @@
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
+ catalogCache.set(path, { mtimeMs, catalog: null });
71
+ return null;
72
+ }
73
+ const flows = raw.flows
74
+ .map((flow) => normalizeEntry(flow, config))
75
+ .filter((flow) => Boolean(flow));
76
+ if (flows.length === 0) {
77
+ catalogCache.set(path, { mtimeMs, catalog: null });
78
+ return null;
79
+ }
80
+ const catalog = { flows, source: path };
81
+ catalogCache.set(path, { mtimeMs, catalog });
82
+ return catalog;
83
+ }
84
+ catch {
85
+ return null;
86
+ }
87
+ }
88
+ export function loadFlowCatalog(config) {
89
+ const candidates = [];
90
+ if (config.flowCatalogPath) {
91
+ candidates.push(config.flowCatalogPath);
92
+ }
93
+ const testsRoot = config.testsRoot || config.path;
94
+ candidates.push(join(testsRoot, '.e2e-ai-agents', 'flows.json'));
95
+ candidates.push(join(config.path, '.e2e-ai-agents', 'flows.json'));
96
+ for (const candidate of candidates) {
97
+ const catalog = readCatalog(candidate, config);
98
+ if (catalog) {
99
+ return catalog;
100
+ }
101
+ }
102
+ return null;
103
+ }
@@ -0,0 +1,81 @@
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
+ }
@@ -0,0 +1,145 @@
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
+ }
@@ -0,0 +1,98 @@
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
+ }
@@ -0,0 +1,112 @@
1
+ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
2
+ // See LICENSE.txt for license information.
3
+ import { existsSync, mkdirSync, writeFileSync } from 'fs';
4
+ import { join, resolve } from 'path';
5
+ import { isPathWithinRoot } from './utils.js';
6
+ function inferTestDir(patterns) {
7
+ if (patterns.length === 0) {
8
+ return 'tests';
9
+ }
10
+ const pattern = patterns[0];
11
+ const wildcardIndex = pattern.search(/[*{]/);
12
+ const base = wildcardIndex === -1 ? pattern : pattern.slice(0, wildcardIndex);
13
+ const trimmed = base.replace(/\/+$/, '');
14
+ return trimmed || 'tests';
15
+ }
16
+ function inferTestExtension(patterns, framework) {
17
+ const joined = patterns.join(' ');
18
+ if (joined.includes('.ts') || joined.includes('.tsx')) {
19
+ return 'ts';
20
+ }
21
+ return 'js';
22
+ }
23
+ function createPlaywrightTest(flow, testIds) {
24
+ const idsComment = testIds.length > 0 ? `// Suggested data-testid: ${testIds.join(', ')}` : '// TODO: add data-testid selectors';
25
+ return [
26
+ "import {test, expect} from '@mattermost/playwright-lib';",
27
+ '',
28
+ '/**',
29
+ ` * @objective Validate ${flow.name} flow`,
30
+ ' */',
31
+ `test('${flow.priority}: ${flow.name} basic flow', {tag: '@ai-assisted'}, async ({pw}) => {`,
32
+ ' const {user, team} = await pw.initSetup();',
33
+ ' const {channelsPage} = await pw.testBrowser.login(user);',
34
+ " await channelsPage.goto(team.name);",
35
+ ` ${idsComment}`,
36
+ ' // # TODO: implement steps',
37
+ ' // * TODO: implement assertions',
38
+ ' await expect(channelsPage.page).toHaveURL(/.*/);',
39
+ '});',
40
+ '',
41
+ ].join('\n');
42
+ }
43
+ function createCypressTest(flow, testIds) {
44
+ const idsComment = testIds.length > 0 ? `// Suggested data-testid: ${testIds.join(', ')}` : '// TODO: add data-testid selectors';
45
+ return [
46
+ `describe('Flow: ${flow.name}', () => {`,
47
+ ` it('${flow.priority}: ${flow.name} basic flow', () => {`,
48
+ " cy.visit('/');",
49
+ ` ${idsComment}`,
50
+ ' // TODO: implement steps',
51
+ ' cy.url().should(\'match\', /.*/);',
52
+ ' });',
53
+ '});',
54
+ '',
55
+ ].join('\n');
56
+ }
57
+ function createSeleniumTest(flow, testIds) {
58
+ const idsComment = testIds.length > 0 ? `// Suggested data-testid: ${testIds.join(', ')}` : '// TODO: add data-testid selectors';
59
+ return [
60
+ "const {Builder, By, until} = require('selenium-webdriver');",
61
+ '',
62
+ `(async () => {`,
63
+ " const driver = await new Builder().forBrowser('chrome').build();",
64
+ ' try {',
65
+ " await driver.get('http://localhost:3000');",
66
+ ` ${idsComment}`,
67
+ ' // TODO: implement steps',
68
+ ' await driver.wait(until.titleIs(\'\'), 5000);',
69
+ ' } finally {',
70
+ ' await driver.quit();',
71
+ ' }',
72
+ '})();',
73
+ '',
74
+ ].join('\n');
75
+ }
76
+ export function generateTests(appRoot, flows, framework, testPatterns, testIdsByFlow) {
77
+ const inferredTestDir = inferTestDir(testPatterns);
78
+ const safeTestDir = isPathWithinRoot(appRoot, resolve(appRoot, inferredTestDir)) ? inferredTestDir : 'tests';
79
+ const testDir = safeTestDir;
80
+ const extension = inferTestExtension(testPatterns, framework);
81
+ const generated = [];
82
+ for (const flow of flows) {
83
+ if (flow.priority !== 'P0' && flow.priority !== 'P1') {
84
+ continue;
85
+ }
86
+ const testIds = testIdsByFlow.get(flow.id) || [];
87
+ const fileName = framework === 'cypress' ? `${flow.id}.cy.${extension}` : `${flow.id}.spec.${extension}`;
88
+ const fullPath = resolve(appRoot, testDir, fileName);
89
+ if (!isPathWithinRoot(appRoot, fullPath)) {
90
+ generated.push({ path: fullPath, flowId: flow.id, created: false, reason: 'outside-root' });
91
+ continue;
92
+ }
93
+ if (existsSync(fullPath)) {
94
+ generated.push({ path: fullPath, flowId: flow.id, created: false, reason: 'exists' });
95
+ continue;
96
+ }
97
+ mkdirSync(join(appRoot, testDir), { recursive: true });
98
+ let content = '';
99
+ if (framework === 'cypress') {
100
+ content = createCypressTest(flow, testIds);
101
+ }
102
+ else if (framework === 'selenium') {
103
+ content = createSeleniumTest(flow, testIds);
104
+ }
105
+ else {
106
+ content = createPlaywrightTest(flow, testIds);
107
+ }
108
+ writeFileSync(fullPath, content, 'utf-8');
109
+ generated.push({ path: fullPath, flowId: flow.id, created: true });
110
+ }
111
+ return generated;
112
+ }
@@ -0,0 +1,87 @@
1
+ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
2
+ // See LICENSE.txt for license information.
3
+ import { spawnSync } from 'child_process';
4
+ import { normalizePath } from './utils.js';
5
+ function runGitRaw(args, cwd) {
6
+ const result = spawnSync('git', args, {
7
+ cwd,
8
+ encoding: 'utf-8',
9
+ timeout: 30000,
10
+ });
11
+ if (result.error || result.status !== 0) {
12
+ return null;
13
+ }
14
+ return result.stdout;
15
+ }
16
+ function runGit(args, cwd) {
17
+ const output = runGitRaw(args, cwd);
18
+ if (output === null) {
19
+ return null;
20
+ }
21
+ return output
22
+ .split('\n')
23
+ .map((file) => file.trim())
24
+ .filter(Boolean)
25
+ .map((file) => normalizePath(file));
26
+ }
27
+ function parseStatusLines(lines) {
28
+ const files = [];
29
+ for (const line of lines) {
30
+ if (!line)
31
+ continue;
32
+ if (line.length < 4)
33
+ continue;
34
+ const pathPart = line.slice(3).trim();
35
+ if (!pathPart)
36
+ continue;
37
+ if (pathPart.includes('->')) {
38
+ const parts = pathPart.split('->').map((part) => part.trim());
39
+ const target = parts[parts.length - 1];
40
+ if (target) {
41
+ files.push(normalizePath(target));
42
+ }
43
+ }
44
+ else {
45
+ files.push(normalizePath(pathPart));
46
+ }
47
+ }
48
+ return files;
49
+ }
50
+ export function getChangedFiles(appRoot, since, options) {
51
+ try {
52
+ const files = new Set();
53
+ let baseRef = since;
54
+ let baseStrategy = 'direct';
55
+ const mergeBase = runGitRaw(['merge-base', since, 'HEAD'], appRoot);
56
+ if (mergeBase) {
57
+ const candidate = mergeBase
58
+ .split('\n')
59
+ .map((line) => line.trim())
60
+ .find(Boolean);
61
+ if (candidate) {
62
+ baseRef = candidate;
63
+ baseStrategy = 'merge-base';
64
+ }
65
+ }
66
+ const diffFiles = runGit(['diff', '--name-only', `${baseRef}..HEAD`, '--', '.'], appRoot);
67
+ if (!diffFiles) {
68
+ return { files: [], error: 'git diff failed' };
69
+ }
70
+ diffFiles.forEach((file) => files.add(file));
71
+ if (options?.includeUncommitted) {
72
+ const staged = runGit(['diff', '--name-only', '--cached', '--', '.'], appRoot) || [];
73
+ staged.forEach((file) => files.add(file));
74
+ const unstaged = runGit(['diff', '--name-only', '--', '.'], appRoot) || [];
75
+ unstaged.forEach((file) => files.add(file));
76
+ const statusOutput = runGitRaw(['status', '--porcelain', '--', '.'], appRoot);
77
+ if (statusOutput) {
78
+ const statusLines = statusOutput.split('\n').filter(Boolean);
79
+ parseStatusLines(statusLines).forEach((file) => files.add(file));
80
+ }
81
+ }
82
+ return { files: Array.from(files), baseRef, baseStrategy };
83
+ }
84
+ catch {
85
+ return { files: [], error: 'git diff failed' };
86
+ }
87
+ }