@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,605 @@
1
+ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
2
+ // See LICENSE.txt for license information.
3
+ import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'fs';
4
+ import { basename, dirname, join, relative, resolve } from 'path';
5
+ import { spawnSync } from 'child_process';
6
+ import { baseNameWithoutExt, isPathWithinRoot, normalizePath, titleCase, tokenize, uniqueTokens } from './utils.js';
7
+ function hasE2eTestGenCLI(testsRoot) {
8
+ const cliPath = join(testsRoot, 'e2e-test-gen-cli.ts');
9
+ return existsSync(cliPath) ? cliPath : null;
10
+ }
11
+ function toSafeSlug(value) {
12
+ return value.replace(/[^a-zA-Z0-9._-]+/g, '-').replace(/^-+|-+$/g, '') || 'flow';
13
+ }
14
+ function stripSpecSuffix(value) {
15
+ return value.replace(/\.(spec|test)\.[^.]+$/i, '').replace(/\.[^.]+$/, '');
16
+ }
17
+ function buildSyntheticFlowFromSpecTarget(relativeSpecPath, target) {
18
+ const normalizedSpecPath = normalizePath(relativeSpecPath);
19
+ const noSuffix = stripSpecSuffix(normalizedSpecPath);
20
+ const flowId = toSafeSlug(noSuffix.replace(/\//g, '.'));
21
+ const base = baseNameWithoutExt(stripSpecSuffix(basename(normalizedSpecPath)));
22
+ const flowName = titleCase(base.replace(/[._-]+/g, ' ')) || 'Recovered Spec';
23
+ const keywords = uniqueTokens(tokenize(noSuffix.replace(/[/.]/g, ' ')));
24
+ const reasons = [
25
+ `Playwright report marked this spec as ${target.status || 'unstable'}.`,
26
+ target.reason || `Auto-heal target: ${normalizedSpecPath}`,
27
+ ];
28
+ return {
29
+ id: flowId,
30
+ name: flowName,
31
+ kind: 'flow',
32
+ score: target.status === 'failed' ? 12 : 9,
33
+ priority: target.status === 'failed' ? 'P0' : 'P1',
34
+ reasons,
35
+ keywords,
36
+ files: [normalizedSpecPath],
37
+ };
38
+ }
39
+ function firstFlowFiles(flow) {
40
+ return (flow.files || []).filter(Boolean).slice(0, 5);
41
+ }
42
+ function buildNativeStrategyOrder(flow) {
43
+ const haystack = [
44
+ flow.id,
45
+ flow.name,
46
+ ...(flow.files || []),
47
+ ...(flow.reasons || []),
48
+ ...(flow.keywords || []),
49
+ ].join(' ').toLowerCase();
50
+ const strategies = [];
51
+ if (/(thread|reply|rhs|sidebar[_-]?right)/.test(haystack)) {
52
+ strategies.push('thread-reply');
53
+ }
54
+ if (/(message|post|realtime|websocket|chat)/.test(haystack)) {
55
+ strategies.push('message-post');
56
+ }
57
+ if (/(channel|navigation|sidebar|switch)/.test(haystack)) {
58
+ strategies.push('channel-baseline');
59
+ }
60
+ if (/(search|find|spotlight)/.test(haystack)) {
61
+ strategies.push('search-baseline');
62
+ }
63
+ strategies.push('generic-baseline');
64
+ return Array.from(new Set(strategies));
65
+ }
66
+ function validateGeneratedSpecContent(content) {
67
+ const issues = [];
68
+ if (/\btest\.describe\s*\(/.test(content)) {
69
+ issues.push({
70
+ code: 'disallowed-describe',
71
+ message: 'Generated tests must not use test.describe.',
72
+ });
73
+ }
74
+ if (/\btest\.only\s*\(/.test(content)) {
75
+ issues.push({
76
+ code: 'disallowed-only',
77
+ message: 'Generated tests must not use test.only.',
78
+ });
79
+ }
80
+ if (!/\btest\s*\(/.test(content)) {
81
+ issues.push({
82
+ code: 'missing-test',
83
+ message: 'Generated file does not include a test() declaration.',
84
+ });
85
+ }
86
+ if (/\btag\s*:\s*\[/.test(content)) {
87
+ issues.push({
88
+ code: 'tag-array-disallowed',
89
+ message: 'Generated tests must use a single tag string, not a tag array.',
90
+ });
91
+ }
92
+ const hasTagString = /\btag\s*:\s*['"][^'"]+['"]/.test(content);
93
+ if (!hasTagString || !/@ai-assisted/.test(content)) {
94
+ issues.push({
95
+ code: 'missing-tag',
96
+ message: "Generated tests must include a single '@ai-assisted' tag.",
97
+ });
98
+ }
99
+ return issues;
100
+ }
101
+ function createNativePlaywrightSpec(flow, slug, strategy) {
102
+ const linkedFiles = firstFlowFiles(flow).join(', ') || 'N/A';
103
+ const header = [
104
+ "import {test, expect} from '@mattermost/playwright-lib';",
105
+ '',
106
+ '/**',
107
+ ` * Auto-generated by @yasserkhanorg/e2e-agents`,
108
+ ` * Flow: ${flow.id} (${flow.name})`,
109
+ ` * Strategy: ${strategy}`,
110
+ ` * Linked files: ${linkedFiles}`,
111
+ ' */',
112
+ ];
113
+ const start = [
114
+ `test('${flow.priority}: ${flow.name} generated coverage', {tag: '@ai-assisted'}, async ({pw}) => {`,
115
+ ' const {user, team} = await pw.initSetup();',
116
+ ' const {channelsPage} = await pw.testBrowser.login(user);',
117
+ ' await channelsPage.goto(team.name);',
118
+ ];
119
+ const end = [
120
+ '});',
121
+ '',
122
+ ];
123
+ if (strategy === 'thread-reply') {
124
+ return [
125
+ ...header,
126
+ ...start,
127
+ ` const parentMessage = \`ai-${slug}-parent-\${Date.now()}\`;`,
128
+ ' await channelsPage.postMessage(parentMessage);',
129
+ ' await channelsPage.openAThread(parentMessage);',
130
+ ` const replyMessage = \`ai-${slug}-reply-\${Date.now()}\`;`,
131
+ ' await channelsPage.sidebarRight.postMessage(replyMessage);',
132
+ ' await expect(channelsPage.sidebarRight.getLastPost()).toContainText(replyMessage);',
133
+ ...end,
134
+ ].join('\n');
135
+ }
136
+ if (strategy === 'message-post') {
137
+ return [
138
+ ...header,
139
+ ...start,
140
+ ` const message = \`ai-${slug}-message-\${Date.now()}\`;`,
141
+ ' await channelsPage.postMessage(message);',
142
+ ' await expect(channelsPage.getLastPost()).toContainText(message);',
143
+ ...end,
144
+ ].join('\n');
145
+ }
146
+ if (strategy === 'channel-baseline') {
147
+ return [
148
+ ...header,
149
+ ...start,
150
+ " await expect(channelsPage.page.locator('#channelHeaderTitle')).toBeVisible();",
151
+ " await expect(channelsPage.page.locator('#SidebarContainer')).toBeVisible();",
152
+ ...end,
153
+ ].join('\n');
154
+ }
155
+ if (strategy === 'search-baseline') {
156
+ return [
157
+ ...header,
158
+ ...start,
159
+ ` const searchTerm = '${slug}'.slice(0, 20);`,
160
+ " await channelsPage.page.keyboard.press('ControlOrMeta+K');",
161
+ ' await channelsPage.page.keyboard.type(searchTerm);',
162
+ " await channelsPage.page.keyboard.press('Escape');",
163
+ ' await expect(channelsPage.page).toHaveURL(/\\/channels\\//);',
164
+ ...end,
165
+ ].join('\n');
166
+ }
167
+ return [
168
+ ...header,
169
+ ...start,
170
+ ' await expect(channelsPage.page).toHaveURL(/\\/channels\\//);',
171
+ " await expect(channelsPage.page.locator('#channelHeaderTitle')).toBeVisible();",
172
+ ...end,
173
+ ].join('\n');
174
+ }
175
+ function resolvePlaywrightBinary(testsRoot) {
176
+ const unixPath = join(testsRoot, 'node_modules', '.bin', 'playwright');
177
+ const windowsPath = join(testsRoot, 'node_modules', '.bin', 'playwright.cmd');
178
+ if (existsSync(unixPath)) {
179
+ return unixPath;
180
+ }
181
+ if (existsSync(windowsPath)) {
182
+ return windowsPath;
183
+ }
184
+ return null;
185
+ }
186
+ function summarizeCommandOutput(stdout, stderr) {
187
+ const combined = [stdout, stderr].filter(Boolean).join('\n').trim();
188
+ if (!combined) {
189
+ return '';
190
+ }
191
+ const lines = combined.split('\n').slice(-20);
192
+ return lines.join('\n').slice(0, 2000);
193
+ }
194
+ function runCommand(command, args, cwd) {
195
+ const result = spawnSync(command, args, {
196
+ cwd,
197
+ encoding: 'utf-8',
198
+ timeout: 60 * 60 * 1000,
199
+ stdio: 'pipe',
200
+ });
201
+ return {
202
+ status: result.status ?? 1,
203
+ stdout: result.stdout || '',
204
+ stderr: result.stderr || '',
205
+ error: result.error ? result.error.message : undefined,
206
+ };
207
+ }
208
+ function runPlaywrightListValidation(testsRoot, testFile, pipeline, playwrightBinary) {
209
+ if (!playwrightBinary) {
210
+ return {
211
+ status: 'skipped',
212
+ detail: 'Playwright binary not found under testsRoot/node_modules/.bin; runtime compile validation skipped.',
213
+ };
214
+ }
215
+ const relativeSpecPath = normalizePath(relative(testsRoot, testFile));
216
+ if (relativeSpecPath.startsWith('../') || relativeSpecPath.startsWith('..\\')) {
217
+ return {
218
+ status: 'failed',
219
+ detail: 'Generated spec path resolved outside testsRoot during validation.',
220
+ };
221
+ }
222
+ const args = ['test', '--list', relativeSpecPath];
223
+ if (pipeline.project) {
224
+ args.push('--project', pipeline.project);
225
+ }
226
+ const commandResult = runCommand(playwrightBinary, args, testsRoot);
227
+ if (commandResult.error && /ENOENT/.test(commandResult.error)) {
228
+ return {
229
+ status: 'skipped',
230
+ detail: 'Playwright binary was not executable; runtime compile validation skipped.',
231
+ };
232
+ }
233
+ if (commandResult.status === 0) {
234
+ return { status: 'passed' };
235
+ }
236
+ const summary = summarizeCommandOutput(commandResult.stdout, commandResult.stderr);
237
+ return {
238
+ status: 'failed',
239
+ detail: summary || commandResult.error || `playwright --list failed with status ${commandResult.status}`,
240
+ };
241
+ }
242
+ function runPackageNativeFlow(testsRoot, flow, pipeline, outputDir, testFile, playwrightBinary) {
243
+ const flowId = flow.id;
244
+ const flowName = flow.name;
245
+ const existingFile = existsSync(testFile);
246
+ const originalContent = existingFile ? readFileSync(testFile, 'utf-8') : null;
247
+ if (existingFile && !pipeline.heal) {
248
+ return {
249
+ flowId,
250
+ flowName,
251
+ generatedDir: outputDir,
252
+ generateStatus: 'skipped',
253
+ };
254
+ }
255
+ const slug = toSafeSlug(flow.id);
256
+ const strategies = buildNativeStrategyOrder(flow);
257
+ const attempts = [];
258
+ const candidates = [];
259
+ if (pipeline.heal && originalContent !== null) {
260
+ candidates.push({
261
+ label: 'existing',
262
+ content: originalContent,
263
+ write: false,
264
+ });
265
+ }
266
+ for (const strategy of strategies) {
267
+ candidates.push({
268
+ label: strategy,
269
+ strategy,
270
+ content: createNativePlaywrightSpec(flow, slug, strategy),
271
+ write: true,
272
+ });
273
+ }
274
+ mkdirSync(outputDir, { recursive: true });
275
+ let wroteNewFile = false;
276
+ for (let i = 0; i < candidates.length; i += 1) {
277
+ const candidate = candidates[i];
278
+ if (candidate.write) {
279
+ writeFileSync(testFile, candidate.content, 'utf-8');
280
+ wroteNewFile = true;
281
+ }
282
+ const currentContent = candidate.write ? candidate.content : (originalContent || '');
283
+ const qualityIssues = validateGeneratedSpecContent(currentContent);
284
+ if (qualityIssues.length > 0) {
285
+ attempts.push(`${candidate.label}: ${qualityIssues.map((issue) => issue.message).join(' ')}`);
286
+ if (pipeline.heal && i < candidates.length - 1) {
287
+ continue;
288
+ }
289
+ if (originalContent !== null) {
290
+ writeFileSync(testFile, originalContent, 'utf-8');
291
+ }
292
+ else if (wroteNewFile && existsSync(testFile)) {
293
+ rmSync(testFile, { force: true });
294
+ }
295
+ return {
296
+ flowId,
297
+ flowName,
298
+ generatedDir: outputDir,
299
+ generateStatus: 'failed',
300
+ healStatus: pipeline.heal ? 'failed' : undefined,
301
+ error: `Quality checks failed. Attempts: ${attempts.join(' | ')}`,
302
+ };
303
+ }
304
+ if (pipeline.heal) {
305
+ const validation = runPlaywrightListValidation(testsRoot, testFile, pipeline, playwrightBinary);
306
+ if (validation.status === 'failed') {
307
+ attempts.push(`${candidate.label}: ${validation.detail || 'playwright validation failed'}`);
308
+ if (i < candidates.length - 1) {
309
+ continue;
310
+ }
311
+ if (originalContent !== null) {
312
+ writeFileSync(testFile, originalContent, 'utf-8');
313
+ }
314
+ else if (wroteNewFile && existsSync(testFile)) {
315
+ rmSync(testFile, { force: true });
316
+ }
317
+ return {
318
+ flowId,
319
+ flowName,
320
+ generatedDir: outputDir,
321
+ generateStatus: 'failed',
322
+ healStatus: 'failed',
323
+ error: `Heal validation failed. Attempts: ${attempts.join(' | ')}`,
324
+ };
325
+ }
326
+ }
327
+ return {
328
+ flowId,
329
+ flowName,
330
+ generatedDir: outputDir,
331
+ generateStatus: candidate.write ? 'success' : 'skipped',
332
+ healStatus: pipeline.heal ? 'success' : undefined,
333
+ };
334
+ }
335
+ if (originalContent !== null) {
336
+ writeFileSync(testFile, originalContent, 'utf-8');
337
+ }
338
+ else if (wroteNewFile && existsSync(testFile)) {
339
+ rmSync(testFile, { force: true });
340
+ }
341
+ return {
342
+ flowId,
343
+ flowName,
344
+ generatedDir: outputDir,
345
+ generateStatus: 'failed',
346
+ healStatus: pipeline.heal ? 'failed' : undefined,
347
+ error: attempts.length > 0 ? attempts.join(' | ') : 'No generation candidates were available.',
348
+ };
349
+ }
350
+ function runPackageNativePipeline(testsRoot, flows, pipeline, baseWarnings = []) {
351
+ const warningSet = new Set(baseWarnings);
352
+ if (pipeline.mcp) {
353
+ warningSet.add('Package-native pipeline does not run Playwright MCP directly. Use follow-up heal workflows if MCP is required.');
354
+ }
355
+ const playwrightBinary = pipeline.heal ? resolvePlaywrightBinary(testsRoot) : null;
356
+ if (pipeline.heal && !playwrightBinary) {
357
+ warningSet.add('Playwright binary was not found. Heal uses static quality checks without runtime compile validation.');
358
+ }
359
+ const results = [];
360
+ const outputBase = resolve(testsRoot, pipeline.outputDir || 'specs/functional/ai-assisted');
361
+ if (!isPathWithinRoot(testsRoot, outputBase)) {
362
+ warningSet.add(`Pipeline outputDir resolves outside testsRoot and was blocked: ${pipeline.outputDir}`);
363
+ return { runner: 'unknown', results, warnings: Array.from(warningSet) };
364
+ }
365
+ for (const flow of flows) {
366
+ if (flow.priority !== 'P0' && flow.priority !== 'P1') {
367
+ continue;
368
+ }
369
+ const slug = toSafeSlug(flow.id);
370
+ const outputDir = normalizePath(join(outputBase, slug));
371
+ if (!isPathWithinRoot(testsRoot, outputDir)) {
372
+ results.push({
373
+ flowId: flow.id,
374
+ flowName: flow.name,
375
+ generatedDir: outputDir,
376
+ generateStatus: 'failed',
377
+ error: 'output directory resolves outside testsRoot',
378
+ });
379
+ continue;
380
+ }
381
+ if (pipeline.dryRun) {
382
+ results.push({
383
+ flowId: flow.id,
384
+ flowName: flow.name,
385
+ generatedDir: outputDir,
386
+ generateStatus: 'skipped',
387
+ healStatus: pipeline.heal ? 'skipped' : undefined,
388
+ });
389
+ continue;
390
+ }
391
+ const testFile = normalizePath(join(outputDir, `${slug}.spec.ts`));
392
+ if (!isPathWithinRoot(testsRoot, testFile)) {
393
+ results.push({
394
+ flowId: flow.id,
395
+ flowName: flow.name,
396
+ generatedDir: outputDir,
397
+ generateStatus: 'failed',
398
+ error: 'generated test path resolves outside testsRoot',
399
+ });
400
+ continue;
401
+ }
402
+ results.push(runPackageNativeFlow(testsRoot, flow, pipeline, outputDir, testFile, playwrightBinary));
403
+ }
404
+ return { runner: 'package-native', results, warnings: Array.from(warningSet) };
405
+ }
406
+ export function runTargetedSpecHeal(testsRoot, targets, pipeline) {
407
+ const warnings = new Set();
408
+ const results = [];
409
+ if (targets.length === 0) {
410
+ warnings.add('No targeted specs provided for heal.');
411
+ return {
412
+ runner: 'package-native',
413
+ results,
414
+ warnings: Array.from(warnings),
415
+ };
416
+ }
417
+ const playwrightBinary = pipeline.heal ? resolvePlaywrightBinary(testsRoot) : null;
418
+ if (pipeline.heal && !playwrightBinary) {
419
+ warnings.add('Playwright binary was not found. Targeted heal uses static quality checks without runtime compile validation.');
420
+ }
421
+ for (const target of targets) {
422
+ const inputPath = target.specPath || '';
423
+ const absoluteSpecPath = normalizePath(resolve(testsRoot, inputPath));
424
+ if (!isPathWithinRoot(testsRoot, absoluteSpecPath)) {
425
+ results.push({
426
+ flowId: inputPath || 'unknown',
427
+ flowName: inputPath || 'Unknown Spec',
428
+ generatedDir: normalizePath(dirname(absoluteSpecPath)),
429
+ generateStatus: 'failed',
430
+ healStatus: pipeline.heal ? 'failed' : undefined,
431
+ error: `Targeted spec resolves outside testsRoot: ${inputPath}`,
432
+ });
433
+ continue;
434
+ }
435
+ if (!existsSync(absoluteSpecPath)) {
436
+ results.push({
437
+ flowId: inputPath || 'unknown',
438
+ flowName: inputPath || 'Unknown Spec',
439
+ generatedDir: normalizePath(dirname(absoluteSpecPath)),
440
+ generateStatus: 'failed',
441
+ healStatus: pipeline.heal ? 'failed' : undefined,
442
+ error: `Targeted spec does not exist: ${inputPath}`,
443
+ });
444
+ continue;
445
+ }
446
+ const relativeSpecPath = normalizePath(relative(testsRoot, absoluteSpecPath));
447
+ if (!/\.(spec|test)\.[tj]sx?$/.test(relativeSpecPath)) {
448
+ warnings.add(`Skipping non-spec target path: ${relativeSpecPath}`);
449
+ results.push({
450
+ flowId: relativeSpecPath,
451
+ flowName: relativeSpecPath,
452
+ generatedDir: normalizePath(dirname(absoluteSpecPath)),
453
+ generateStatus: 'skipped',
454
+ healStatus: pipeline.heal ? 'skipped' : undefined,
455
+ });
456
+ continue;
457
+ }
458
+ if (pipeline.dryRun) {
459
+ results.push({
460
+ flowId: relativeSpecPath,
461
+ flowName: relativeSpecPath,
462
+ generatedDir: normalizePath(dirname(absoluteSpecPath)),
463
+ generateStatus: 'skipped',
464
+ healStatus: pipeline.heal ? 'skipped' : undefined,
465
+ });
466
+ continue;
467
+ }
468
+ const syntheticFlow = buildSyntheticFlowFromSpecTarget(relativeSpecPath, target);
469
+ results.push(runPackageNativeFlow(testsRoot, syntheticFlow, pipeline, normalizePath(dirname(absoluteSpecPath)), absoluteSpecPath, playwrightBinary));
470
+ }
471
+ return {
472
+ runner: 'package-native',
473
+ results,
474
+ warnings: Array.from(warnings),
475
+ };
476
+ }
477
+ function findSpecFiles(root) {
478
+ if (!existsSync(root)) {
479
+ return [];
480
+ }
481
+ const entries = readdirSync(root, { withFileTypes: true });
482
+ const files = [];
483
+ for (const entry of entries) {
484
+ const fullPath = join(root, entry.name);
485
+ if (entry.isDirectory()) {
486
+ files.push(...findSpecFiles(fullPath));
487
+ }
488
+ else if (entry.isFile() && entry.name.endsWith('.spec.ts')) {
489
+ files.push(fullPath);
490
+ }
491
+ }
492
+ return files;
493
+ }
494
+ function findDisallowedDescribeFiles(root) {
495
+ const files = findSpecFiles(root);
496
+ return files.filter((file) => /\btest\.describe\s*\(/.test(readFileSync(file, 'utf-8')));
497
+ }
498
+ export function runPlaywrightPipeline(testsRoot, flows, pipeline) {
499
+ const cliPath = hasE2eTestGenCLI(testsRoot);
500
+ if (!cliPath) {
501
+ return runPackageNativePipeline(testsRoot, flows, pipeline, ['e2e-test-gen-cli.ts not found; using package-native pipeline fallback.']);
502
+ }
503
+ const warnings = [];
504
+ const results = [];
505
+ const outputBase = resolve(testsRoot, pipeline.outputDir || 'specs/functional/ai-assisted');
506
+ if (!isPathWithinRoot(testsRoot, outputBase)) {
507
+ warnings.push(`Pipeline outputDir resolves outside testsRoot and was blocked: ${pipeline.outputDir}`);
508
+ return { runner: 'unknown', results, warnings };
509
+ }
510
+ for (const flow of flows) {
511
+ if (flow.priority !== 'P0' && flow.priority !== 'P1') {
512
+ continue;
513
+ }
514
+ const slug = flow.id.replace(/[^a-zA-Z0-9._-]+/g, '-');
515
+ const outputDir = normalizePath(join(outputBase, slug));
516
+ if (!isPathWithinRoot(testsRoot, outputDir)) {
517
+ results.push({
518
+ flowId: flow.id,
519
+ flowName: flow.name,
520
+ generatedDir: outputDir,
521
+ generateStatus: 'failed',
522
+ error: 'output directory resolves outside testsRoot',
523
+ });
524
+ continue;
525
+ }
526
+ if (pipeline.dryRun) {
527
+ results.push({
528
+ flowId: flow.id,
529
+ flowName: flow.name,
530
+ generatedDir: outputDir,
531
+ generateStatus: 'skipped',
532
+ healStatus: 'skipped',
533
+ });
534
+ continue;
535
+ }
536
+ const generateArgs = ['tsx', cliPath, 'generate', flow.name, '--output', outputDir, '--scenarios', `${pipeline.scenarios}`];
537
+ if (pipeline.baseUrl) {
538
+ generateArgs.push('--base-url', pipeline.baseUrl);
539
+ }
540
+ if (pipeline.headless) {
541
+ generateArgs.push('--headless');
542
+ }
543
+ if (pipeline.browser) {
544
+ generateArgs.push('--browser', pipeline.browser);
545
+ }
546
+ if (pipeline.project) {
547
+ generateArgs.push('--project', pipeline.project);
548
+ }
549
+ if (pipeline.parallel) {
550
+ generateArgs.push('--parallel');
551
+ }
552
+ if (pipeline.mcp) {
553
+ generateArgs.push('--mcp');
554
+ }
555
+ const generateResult = runCommand('npx', generateArgs, testsRoot);
556
+ if (generateResult.status !== 0) {
557
+ results.push({
558
+ flowId: flow.id,
559
+ flowName: flow.name,
560
+ generatedDir: outputDir,
561
+ generateStatus: 'failed',
562
+ error: summarizeCommandOutput(generateResult.stdout, generateResult.stderr) || generateResult.error || 'generate failed',
563
+ });
564
+ continue;
565
+ }
566
+ let healStatus = 'skipped';
567
+ if (pipeline.heal) {
568
+ const healArgs = ['tsx', cliPath, 'heal', outputDir];
569
+ if (pipeline.browser) {
570
+ healArgs.push('--browser', pipeline.browser);
571
+ }
572
+ if (pipeline.project) {
573
+ healArgs.push('--project', pipeline.project);
574
+ }
575
+ if (pipeline.parallel) {
576
+ healArgs.push('--parallel');
577
+ }
578
+ if (pipeline.mcp) {
579
+ healArgs.push('--mcp');
580
+ }
581
+ const healResult = runCommand('npx', healArgs, testsRoot);
582
+ healStatus = healResult.status === 0 ? 'success' : 'failed';
583
+ }
584
+ const disallowedDescribeFiles = findDisallowedDescribeFiles(outputDir);
585
+ if (disallowedDescribeFiles.length > 0) {
586
+ results.push({
587
+ flowId: flow.id,
588
+ flowName: flow.name,
589
+ generatedDir: outputDir,
590
+ generateStatus: 'failed',
591
+ healStatus,
592
+ error: `Generated tests contain test.describe (disallowed): ${disallowedDescribeFiles.join(', ')}`,
593
+ });
594
+ continue;
595
+ }
596
+ results.push({
597
+ flowId: flow.id,
598
+ flowName: flow.name,
599
+ generatedDir: outputDir,
600
+ generateStatus: 'success',
601
+ healStatus,
602
+ });
603
+ }
604
+ return { runner: 'e2e-test-gen', results, warnings };
605
+ }