@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,234 @@
1
+ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
2
+ // See LICENSE.txt for license information.
3
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
4
+ import { dirname, isAbsolute, join } from 'path';
5
+ import { normalizePath } from './utils.js';
6
+ const DEFAULT_OPTIONS = {
7
+ minHits: 1,
8
+ maxFilesPerTest: 200,
9
+ maxAgeDays: 120,
10
+ };
11
+ function resolvePath(root, value) {
12
+ if (isAbsolute(value)) {
13
+ return value;
14
+ }
15
+ return join(root, value);
16
+ }
17
+ function parseDate(value) {
18
+ const parsed = Date.parse(value);
19
+ if (Number.isNaN(parsed)) {
20
+ return null;
21
+ }
22
+ return parsed;
23
+ }
24
+ function safeReadJson(path) {
25
+ if (!existsSync(path)) {
26
+ return null;
27
+ }
28
+ try {
29
+ return JSON.parse(readFileSync(path, 'utf-8'));
30
+ }
31
+ catch {
32
+ return null;
33
+ }
34
+ }
35
+ function normalizeFiles(value) {
36
+ if (!Array.isArray(value)) {
37
+ return [];
38
+ }
39
+ return Array.from(new Set(value
40
+ .filter((entry) => typeof entry === 'string')
41
+ .map((entry) => normalizePath(entry))));
42
+ }
43
+ function normalizeTest(value) {
44
+ if (typeof value !== 'string') {
45
+ return null;
46
+ }
47
+ const normalized = normalizePath(value);
48
+ return normalized ? normalized : null;
49
+ }
50
+ function buildEntriesFromInput(payload) {
51
+ const warnings = [];
52
+ const entries = [];
53
+ const pushEntry = (testValue, filesValue, timestampValue) => {
54
+ const test = normalizeTest(testValue);
55
+ const files = normalizeFiles(filesValue);
56
+ if (!test || files.length === 0) {
57
+ return;
58
+ }
59
+ entries.push({
60
+ test,
61
+ touchedFiles: files,
62
+ timestamp: typeof timestampValue === 'string' ? timestampValue : undefined,
63
+ });
64
+ };
65
+ if (Array.isArray(payload)) {
66
+ for (const item of payload) {
67
+ if (!item || typeof item !== 'object') {
68
+ continue;
69
+ }
70
+ const entry = item;
71
+ pushEntry(entry.test, entry.touchedFiles, entry.timestamp);
72
+ }
73
+ if (entries.length === 0) {
74
+ warnings.push('Traceability input array had no valid entries.');
75
+ }
76
+ return { entries, warnings };
77
+ }
78
+ if (!payload || typeof payload !== 'object') {
79
+ warnings.push('Traceability input must be an object or array.');
80
+ return { entries, warnings };
81
+ }
82
+ const input = payload;
83
+ if (Array.isArray(input.tests)) {
84
+ for (const item of input.tests) {
85
+ pushEntry(item?.test, item?.touchedFiles, item?.timestamp);
86
+ }
87
+ }
88
+ if (Array.isArray(input.runs)) {
89
+ for (const item of input.runs) {
90
+ const files = Array.isArray(item?.touchedFiles) ? item?.touchedFiles : (Array.isArray(item?.coveredFiles) ? item?.coveredFiles : item?.files);
91
+ pushEntry(item?.test, files, item?.timestamp);
92
+ }
93
+ }
94
+ if (input.fileToTests && typeof input.fileToTests === 'object') {
95
+ for (const [file, tests] of Object.entries(input.fileToTests)) {
96
+ if (!Array.isArray(tests)) {
97
+ continue;
98
+ }
99
+ const normalizedFile = normalizePath(file);
100
+ for (const test of tests) {
101
+ pushEntry(test, [normalizedFile]);
102
+ }
103
+ }
104
+ }
105
+ if (Array.isArray(input.mappings)) {
106
+ for (const mapping of input.mappings) {
107
+ const file = typeof mapping?.file === 'string' ? normalizePath(mapping.file) : null;
108
+ if (!file || !Array.isArray(mapping.tests)) {
109
+ continue;
110
+ }
111
+ for (const test of mapping.tests) {
112
+ pushEntry(test, [file]);
113
+ }
114
+ }
115
+ }
116
+ if (entries.length === 0) {
117
+ warnings.push('Traceability input had no valid test<->file entries.');
118
+ }
119
+ return { entries, warnings };
120
+ }
121
+ function defaultState() {
122
+ return {
123
+ schemaVersion: '1.0.0',
124
+ updatedAt: new Date().toISOString(),
125
+ tests: {},
126
+ };
127
+ }
128
+ function loadState(path) {
129
+ const existing = safeReadJson(path);
130
+ if (!existing || typeof existing !== 'object' || !existing.tests) {
131
+ return defaultState();
132
+ }
133
+ return {
134
+ schemaVersion: '1.0.0',
135
+ updatedAt: existing.updatedAt || new Date().toISOString(),
136
+ tests: existing.tests,
137
+ };
138
+ }
139
+ function pruneByAge(state, maxAgeDays) {
140
+ const cutoff = Date.now() - maxAgeDays * 24 * 60 * 60 * 1000;
141
+ for (const [test, entry] of Object.entries(state.tests)) {
142
+ const lastSeen = parseDate(entry.lastSeen);
143
+ if (lastSeen === null) {
144
+ continue;
145
+ }
146
+ if (lastSeen < cutoff) {
147
+ delete state.tests[test];
148
+ }
149
+ }
150
+ }
151
+ function buildManifest(state, minHits, maxFilesPerTest) {
152
+ const tests = Object.entries(state.tests)
153
+ .map(([test, entry]) => {
154
+ const touchedFiles = Object.entries(entry.files)
155
+ .filter(([, hits]) => hits >= minHits)
156
+ .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
157
+ .slice(0, maxFilesPerTest)
158
+ .map(([file]) => file);
159
+ const signalCount = Object.values(entry.files).reduce((acc, value) => acc + value, 0);
160
+ return {
161
+ test,
162
+ touchedFiles,
163
+ signalCount,
164
+ lastSeen: entry.lastSeen,
165
+ };
166
+ })
167
+ .filter((entry) => entry.touchedFiles.length > 0)
168
+ .sort((a, b) => a.test.localeCompare(b.test));
169
+ return {
170
+ schemaVersion: '1.0.0',
171
+ generatedAt: new Date().toISOString(),
172
+ tests,
173
+ };
174
+ }
175
+ function ensureParent(path) {
176
+ mkdirSync(dirname(path), { recursive: true });
177
+ }
178
+ export function ingestTraceabilityInput(rootPath, traceabilityConfig, inputPayload, options) {
179
+ const resolvedOptions = {
180
+ minHits: options?.minHits ?? DEFAULT_OPTIONS.minHits,
181
+ maxFilesPerTest: options?.maxFilesPerTest ?? DEFAULT_OPTIONS.maxFilesPerTest,
182
+ maxAgeDays: options?.maxAgeDays ?? DEFAULT_OPTIONS.maxAgeDays,
183
+ };
184
+ const warnings = [];
185
+ const manifestPath = resolvePath(rootPath, traceabilityConfig.manifestPath);
186
+ const statePath = join(dirname(manifestPath), 'traceability-state.json');
187
+ if (!traceabilityConfig.enabled) {
188
+ warnings.push('Traceability is disabled in config. Input was not ingested.');
189
+ return {
190
+ manifestPath,
191
+ statePath,
192
+ entriesIngested: 0,
193
+ testsTracked: 0,
194
+ edgesTracked: 0,
195
+ warnings,
196
+ };
197
+ }
198
+ const parsed = buildEntriesFromInput(inputPayload);
199
+ warnings.push(...parsed.warnings);
200
+ const state = loadState(statePath);
201
+ const now = new Date().toISOString();
202
+ for (const entry of parsed.entries) {
203
+ const bucket = state.tests[entry.test] || {
204
+ files: {},
205
+ seenCount: 0,
206
+ lastSeen: now,
207
+ };
208
+ bucket.seenCount += 1;
209
+ bucket.lastSeen = entry.timestamp || now;
210
+ for (const file of entry.touchedFiles) {
211
+ bucket.files[file] = (bucket.files[file] || 0) + 1;
212
+ }
213
+ state.tests[entry.test] = bucket;
214
+ }
215
+ pruneByAge(state, Math.max(1, resolvedOptions.maxAgeDays));
216
+ state.updatedAt = now;
217
+ const manifest = buildManifest(state, Math.max(1, resolvedOptions.minHits), Math.max(1, resolvedOptions.maxFilesPerTest));
218
+ let edgesTracked = 0;
219
+ for (const entry of manifest.tests) {
220
+ edgesTracked += entry.touchedFiles.length;
221
+ }
222
+ ensureParent(statePath);
223
+ ensureParent(manifestPath);
224
+ writeFileSync(statePath, JSON.stringify(state, null, 2), 'utf-8');
225
+ writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), 'utf-8');
226
+ return {
227
+ manifestPath,
228
+ statePath,
229
+ entriesIngested: parsed.entries.length,
230
+ testsTracked: manifest.tests.length,
231
+ edgesTracked,
232
+ warnings,
233
+ };
234
+ }
@@ -0,0 +1,138 @@
1
+ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
2
+ // See LICENSE.txt for license information.
3
+ import { readFileSync, statSync } from 'fs';
4
+ import { basename, extname, posix, relative, resolve } from 'path';
5
+ const MAX_READ_BYTES = 1024 * 1024; // 1MB
6
+ const STOP_WORDS = new Set([
7
+ 'index',
8
+ 'component',
9
+ 'components',
10
+ 'page',
11
+ 'pages',
12
+ 'screen',
13
+ 'screens',
14
+ 'view',
15
+ 'views',
16
+ 'route',
17
+ 'routes',
18
+ 'feature',
19
+ 'features',
20
+ 'module',
21
+ 'modules',
22
+ 'flow',
23
+ 'flows',
24
+ 'test',
25
+ 'tests',
26
+ 'spec',
27
+ 'specs',
28
+ 'hooks',
29
+ 'hook',
30
+ 'context',
31
+ 'state',
32
+ 'store',
33
+ ]);
34
+ const GLOB_CHARS = /[*?[\]{}()!]/;
35
+ export function hasGlobChars(value) {
36
+ return GLOB_CHARS.test(value);
37
+ }
38
+ export function globToRegExp(pattern) {
39
+ const normalized = normalizePath(pattern);
40
+ let regex = '^';
41
+ let i = 0;
42
+ while (i < normalized.length) {
43
+ const char = normalized[i];
44
+ if (char === '*') {
45
+ const next = normalized[i + 1];
46
+ if (next === '*') {
47
+ regex += '.*';
48
+ i += 2;
49
+ continue;
50
+ }
51
+ regex += '[^/]*';
52
+ i += 1;
53
+ continue;
54
+ }
55
+ if (char === '?') {
56
+ regex += '[^/]';
57
+ i += 1;
58
+ continue;
59
+ }
60
+ if ('\\.[]{}()+-^$|'.includes(char)) {
61
+ regex += `\\${char}`;
62
+ }
63
+ else {
64
+ regex += char;
65
+ }
66
+ i += 1;
67
+ }
68
+ regex += '$';
69
+ return new RegExp(regex);
70
+ }
71
+ export function matchGlob(pathValue, pattern) {
72
+ const normalizedPath = normalizePath(pathValue);
73
+ const normalizedPattern = normalizePath(pattern);
74
+ if (!hasGlobChars(normalizedPattern)) {
75
+ if (normalizedPattern.endsWith('/')) {
76
+ return normalizedPath.startsWith(normalizedPattern);
77
+ }
78
+ return normalizedPath === normalizedPattern || normalizedPath.startsWith(`${normalizedPattern}/`);
79
+ }
80
+ const regex = globToRegExp(normalizedPattern);
81
+ return regex.test(normalizedPath);
82
+ }
83
+ export function safeReadTextFile(path) {
84
+ try {
85
+ const stats = statSync(path);
86
+ if (stats.size > MAX_READ_BYTES) {
87
+ return null;
88
+ }
89
+ return readFileSync(path, 'utf-8');
90
+ }
91
+ catch {
92
+ return null;
93
+ }
94
+ }
95
+ export function normalizePath(pathValue) {
96
+ return pathValue.split('\\').join('/');
97
+ }
98
+ export function toRelativePosix(root, filePath) {
99
+ const relative = posix.relative(normalizePath(root), normalizePath(filePath));
100
+ return relative.startsWith('../') ? normalizePath(filePath) : relative;
101
+ }
102
+ export function isPathWithinRoot(root, target) {
103
+ const rootAbs = resolve(root);
104
+ const targetAbs = resolve(target);
105
+ const rel = relative(rootAbs, targetAbs);
106
+ return rel === '' || (!rel.startsWith('..') && !rel.includes(`..${posix.sep}`) && !rel.includes('..\\'));
107
+ }
108
+ export function fileExtension(pathValue) {
109
+ return extname(pathValue).replace('.', '').toLowerCase();
110
+ }
111
+ export function baseNameWithoutExt(pathValue) {
112
+ const base = basename(pathValue);
113
+ const ext = extname(base);
114
+ return ext ? base.slice(0, -ext.length) : base;
115
+ }
116
+ function splitCamelCase(value) {
117
+ return value.replace(/([a-z])([A-Z])/g, '$1 $2');
118
+ }
119
+ export function tokenize(value) {
120
+ const normalized = splitCamelCase(value)
121
+ .replace(/[_\-.]/g, ' ')
122
+ .replace(/[^a-zA-Z0-9\s]/g, ' ')
123
+ .toLowerCase();
124
+ return normalized
125
+ .split(/\s+/)
126
+ .map((token) => token.trim())
127
+ .filter((token) => token.length > 2 && !STOP_WORDS.has(token));
128
+ }
129
+ export function uniqueTokens(tokens) {
130
+ return Array.from(new Set(tokens.filter(Boolean)));
131
+ }
132
+ export function titleCase(value) {
133
+ return value
134
+ .split(/[\s_-]+/)
135
+ .filter(Boolean)
136
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
137
+ .join(' ');
138
+ }
@@ -0,0 +1,160 @@
1
+ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
2
+ // See LICENSE.txt for license information.
3
+ /**
4
+ * SelectorValidator: Enforces whitelist matching on generated selectors
5
+ * Comments out unobserved selectors instead of letting tests fail randomly
6
+ */
7
+ export class SelectorValidator {
8
+ constructor(globalSelectors, minConfidence = 50) {
9
+ this.whitelist = new Set();
10
+ this.semanticWhitelist = new Map();
11
+ this.minConfidence = minConfidence;
12
+ // Build flat whitelist from semantic map
13
+ for (const [semantic, elements] of Object.entries(globalSelectors)) {
14
+ for (const elem of elements) {
15
+ if (elem.confidence >= minConfidence) {
16
+ this.whitelist.add(elem.selector);
17
+ }
18
+ }
19
+ this.semanticWhitelist.set(semantic, elements.map((e) => e.selector));
20
+ }
21
+ }
22
+ /**
23
+ * Validate a selector against the whitelist
24
+ */
25
+ validateSelector(selector) {
26
+ if (this.whitelist.has(selector)) {
27
+ return {
28
+ isValid: true,
29
+ confidence: 100,
30
+ reason: 'Found in whitelist',
31
+ };
32
+ }
33
+ // Check for similar selectors (lenient matching)
34
+ const normalized = this.normalizeSelector(selector);
35
+ for (const whitelisted of this.whitelist) {
36
+ if (this.normalizeSelector(whitelisted).includes(normalized)) {
37
+ return {
38
+ isValid: true,
39
+ confidence: 75,
40
+ reason: 'Found similar whitelisted selector',
41
+ };
42
+ }
43
+ }
44
+ return {
45
+ isValid: false,
46
+ confidence: 0,
47
+ reason: 'Not found in whitelist',
48
+ suggestedComment: `// UNOBSERVED SELECTOR - Not found in UI map. Use test.fixme() if needed.`,
49
+ };
50
+ }
51
+ /**
52
+ * Validate and comment out invalid selectors in generated test code
53
+ */
54
+ validateTestCode(code) {
55
+ const results = [];
56
+ const selectorRegex = /page\.(getByTestId|getByLabel|getByRole|locator)\(['"`]([^'"`]+)['"`]\)/g;
57
+ let match;
58
+ while ((match = selectorRegex.exec(code)) !== null) {
59
+ const [fullMatch, , selector] = match;
60
+ const validation = this.validateSelector(selector);
61
+ results.push({
62
+ selector,
63
+ isWhitelisted: validation.isValid,
64
+ confidence: validation.confidence,
65
+ originalCode: fullMatch,
66
+ validatedCode: validation.isValid
67
+ ? fullMatch
68
+ : `// ${validation.suggestedComment}\n // ${fullMatch}`,
69
+ });
70
+ }
71
+ return results;
72
+ }
73
+ /**
74
+ * Apply validation to test code, commenting out unwhitelisted selectors
75
+ */
76
+ applyValidation(code) {
77
+ let validated = code;
78
+ const results = this.validateTestCode(code);
79
+ // Apply in reverse order to preserve indices
80
+ for (const result of results.reverse()) {
81
+ if (!result.isWhitelisted) {
82
+ validated = validated.replace(result.originalCode, result.validatedCode);
83
+ }
84
+ }
85
+ return validated;
86
+ }
87
+ /**
88
+ * Get validation summary
89
+ */
90
+ getSummary(code) {
91
+ const results = this.validateTestCode(code);
92
+ const unobserved = results.filter((r) => !r.isWhitelisted).map((r) => r.selector);
93
+ return {
94
+ total: results.length,
95
+ whitelisted: results.filter((r) => r.isWhitelisted).length,
96
+ coverage: results.length > 0
97
+ ? Math.round((results.filter((r) => r.isWhitelisted).length / results.length) * 100)
98
+ : 100,
99
+ unobserved,
100
+ };
101
+ }
102
+ normalizeSelector(selector) {
103
+ // Normalize selector for lenient matching
104
+ return selector.toLowerCase().replace(/[^a-z0-9-_]/g, '');
105
+ }
106
+ }
107
+ /**
108
+ * APIFallbackResolver: Provides fallback strategies when UI selectors fail
109
+ * Converts test methods to API calls when UI elements aren't available
110
+ */
111
+ export class APIFallbackResolver {
112
+ constructor() {
113
+ this.apiMapping = new Map([
114
+ // UI action -> API endpoint mapping
115
+ ['click.*button.*submit', 'POST /api/v4/posts'],
116
+ ['fill.*search', 'GET /api/v4/users'],
117
+ ['click.*profile', 'GET /api/v4/users/me'],
118
+ ['navigate.*channel', 'GET /api/v4/channels'],
119
+ ['click.*settings', 'PATCH /api/v4/users/me'],
120
+ ]);
121
+ }
122
+ /**
123
+ * Check if a test should fall back to API testing
124
+ */
125
+ shouldFallback(selector, confidence) {
126
+ return confidence < 50;
127
+ }
128
+ /**
129
+ * Generate API-based fallback for unobserved selector
130
+ */
131
+ generateAPIFallback(selector, action) {
132
+ // Find matching API endpoint
133
+ let endpoint = 'GET /api/v4/';
134
+ for (const [pattern, api] of this.apiMapping) {
135
+ if (new RegExp(pattern, 'i').test(action)) {
136
+ endpoint = api;
137
+ break;
138
+ }
139
+ }
140
+ return `
141
+ // UI selector not found - falling back to API
142
+ const response = await fetch(\`\${baseUrl}${endpoint.split(' ')[1]}\`, {
143
+ method: '${endpoint.split(' ')[0]}',
144
+ headers: {'Authorization': \`Bearer \${token}\`},
145
+ });
146
+ expect(response.ok).toBe(true);`;
147
+ }
148
+ /**
149
+ * Wrap unobserved test in try-catch with API fallback
150
+ */
151
+ wrapWithFallback(testCode) {
152
+ return `
153
+ try {
154
+ ${testCode}
155
+ } catch (error) {
156
+ // UI element not found - using API fallback
157
+ ${this.generateAPIFallback('unknown', testCode)}
158
+ }`;
159
+ }
160
+ }