@yasserkhanorg/e2e-agents 0.3.2 → 0.3.4

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 (87) hide show
  1. package/README.md +29 -20
  2. package/dist/agent/config.d.ts +3 -0
  3. package/dist/agent/config.d.ts.map +1 -1
  4. package/dist/agent/config.js +38 -0
  5. package/dist/agent/operational_insights.d.ts +1 -1
  6. package/dist/agent/operational_insights.d.ts.map +1 -1
  7. package/dist/agent/operational_insights.js +2 -1
  8. package/dist/agent/pipeline.d.ts +8 -1
  9. package/dist/agent/pipeline.d.ts.map +1 -1
  10. package/dist/agent/pipeline.js +844 -33
  11. package/dist/agent/plan.d.ts +39 -0
  12. package/dist/agent/plan.d.ts.map +1 -1
  13. package/dist/agent/plan.js +146 -0
  14. package/dist/agent/report.d.ts +16 -0
  15. package/dist/agent/report.d.ts.map +1 -1
  16. package/dist/agent/report.js +12 -0
  17. package/dist/agent/runner.d.ts.map +1 -1
  18. package/dist/agent/runner.js +66 -11
  19. package/dist/agent/tests.d.ts.map +1 -1
  20. package/dist/agent/tests.js +12 -2
  21. package/dist/api.d.ts.map +1 -1
  22. package/dist/api.js +1 -0
  23. package/dist/cli.js +111 -7
  24. package/dist/esm/agent/config.js +38 -0
  25. package/dist/esm/agent/operational_insights.js +2 -1
  26. package/dist/esm/agent/pipeline.js +844 -33
  27. package/dist/esm/agent/plan.js +145 -1
  28. package/dist/esm/agent/report.js +12 -0
  29. package/dist/esm/agent/runner.js +66 -11
  30. package/dist/esm/agent/tests.js +12 -2
  31. package/dist/esm/api.js +2 -1
  32. package/dist/esm/cli.js +112 -8
  33. package/package.json +1 -1
  34. package/schemas/impact.schema.json +37 -0
  35. package/schemas/plan.schema.json +48 -0
  36. package/dist/agent/cache_utils.d.ts +0 -38
  37. package/dist/agent/cache_utils.d.ts.map +0 -1
  38. package/dist/agent/cache_utils.js +0 -67
  39. package/dist/agent/impact-analyzer.d.ts +0 -114
  40. package/dist/agent/impact-analyzer.d.ts.map +0 -1
  41. package/dist/agent/impact-analyzer.js +0 -557
  42. package/dist/agent/index.d.ts +0 -21
  43. package/dist/agent/index.d.ts.map +0 -1
  44. package/dist/agent/index.js +0 -38
  45. package/dist/agent/model-router.d.ts +0 -57
  46. package/dist/agent/model-router.d.ts.map +0 -1
  47. package/dist/agent/model-router.js +0 -154
  48. package/dist/agent/report-generator.d.ts +0 -24
  49. package/dist/agent/report-generator.d.ts.map +0 -1
  50. package/dist/agent/report-generator.js +0 -250
  51. package/dist/agent/spec-bridge.d.ts +0 -101
  52. package/dist/agent/spec-bridge.d.ts.map +0 -1
  53. package/dist/agent/spec-bridge.js +0 -273
  54. package/dist/agent/spec-builder.d.ts +0 -102
  55. package/dist/agent/spec-builder.d.ts.map +0 -1
  56. package/dist/agent/spec-builder.js +0 -273
  57. package/dist/agent/telemetry.d.ts +0 -84
  58. package/dist/agent/telemetry.d.ts.map +0 -1
  59. package/dist/agent/telemetry.js +0 -220
  60. package/dist/agent/validators/selector-validator.d.ts +0 -74
  61. package/dist/agent/validators/selector-validator.d.ts.map +0 -1
  62. package/dist/agent/validators/selector-validator.js +0 -165
  63. package/dist/e2e-test-gen/index.d.ts +0 -51
  64. package/dist/e2e-test-gen/index.d.ts.map +0 -1
  65. package/dist/e2e-test-gen/index.js +0 -57
  66. package/dist/e2e-test-gen/spec_parser.d.ts +0 -142
  67. package/dist/e2e-test-gen/spec_parser.d.ts.map +0 -1
  68. package/dist/e2e-test-gen/spec_parser.js +0 -786
  69. package/dist/e2e-test-gen/types.d.ts +0 -185
  70. package/dist/e2e-test-gen/types.d.ts.map +0 -1
  71. package/dist/e2e-test-gen/types.js +0 -4
  72. package/dist/esm/agent/cache_utils.js +0 -63
  73. package/dist/esm/agent/impact-analyzer.js +0 -548
  74. package/dist/esm/agent/index.js +0 -22
  75. package/dist/esm/agent/model-router.js +0 -150
  76. package/dist/esm/agent/report-generator.js +0 -247
  77. package/dist/esm/agent/spec-bridge.js +0 -267
  78. package/dist/esm/agent/spec-builder.js +0 -267
  79. package/dist/esm/agent/telemetry.js +0 -216
  80. package/dist/esm/agent/validators/selector-validator.js +0 -160
  81. package/dist/esm/e2e-test-gen/index.js +0 -50
  82. package/dist/esm/e2e-test-gen/spec_parser.js +0 -782
  83. package/dist/esm/e2e-test-gen/types.js +0 -3
  84. package/dist/esm/plan-and-test-constants.js +0 -126
  85. package/dist/plan-and-test-constants.d.ts +0 -110
  86. package/dist/plan-and-test-constants.d.ts.map +0 -1
  87. package/dist/plan-and-test-constants.js +0 -132
@@ -8,6 +8,47 @@ const fs_1 = require("fs");
8
8
  const path_1 = require("path");
9
9
  const child_process_1 = require("child_process");
10
10
  const utils_js_1 = require("./utils.js");
11
+ function createMcpStatus(backend, requested) {
12
+ return {
13
+ requested,
14
+ active: requested && (backend === 'e2e-test-gen' || backend === 'playwright-agents'),
15
+ backend,
16
+ };
17
+ }
18
+ function classifyPipelineFailure(result) {
19
+ if (result.failureCategory || result.failureCode) {
20
+ return result;
21
+ }
22
+ if (!result.error) {
23
+ return result;
24
+ }
25
+ const errorText = result.error.toLowerCase();
26
+ if (errorText.includes('outside testsroot')) {
27
+ return { ...result, failureCategory: 'path-safety', failureCode: 'path_outside_tests_root' };
28
+ }
29
+ if (errorText.includes('playwright binary') || errorText.includes('not found')) {
30
+ return { ...result, failureCategory: 'environment', failureCode: 'dependency_missing' };
31
+ }
32
+ if (errorText.includes('compile validation')) {
33
+ return { ...result, failureCategory: 'validation', failureCode: 'compile_validation_failed' };
34
+ }
35
+ if (errorText.includes('runtime validation') || errorText.includes('playwright test failed')) {
36
+ return { ...result, failureCategory: 'runtime', failureCode: 'runtime_validation_failed' };
37
+ }
38
+ if (errorText.includes('quality checks failed') || errorText.includes('invalid test content')) {
39
+ return { ...result, failureCategory: 'quality', failureCode: 'quality_guard_failed' };
40
+ }
41
+ if (errorText.includes('generate failed') || errorText.includes('did not produce expected test file')) {
42
+ return { ...result, failureCategory: 'generation', failureCode: 'generation_failed' };
43
+ }
44
+ return { ...result, failureCategory: 'unknown', failureCode: 'unknown' };
45
+ }
46
+ function finalizePipelineSummary(summary) {
47
+ return {
48
+ ...summary,
49
+ results: summary.results.map(classifyPipelineFailure),
50
+ };
51
+ }
11
52
  function hasE2eTestGenCLI(testsRoot) {
12
53
  const cliPath = (0, path_1.join)(testsRoot, 'e2e-test-gen-cli.ts');
13
54
  return (0, fs_1.existsSync)(cliPath) ? cliPath : null;
@@ -44,6 +85,7 @@ function firstFlowFiles(flow) {
44
85
  return (flow.files || []).filter(Boolean).slice(0, 5);
45
86
  }
46
87
  function buildNativeStrategyOrder(flow) {
88
+ const flowId = (flow.id || '').toLowerCase();
47
89
  const haystack = [
48
90
  flow.id,
49
91
  flow.name,
@@ -52,22 +94,259 @@ function buildNativeStrategyOrder(flow) {
52
94
  ...(flow.keywords || []),
53
95
  ].join(' ').toLowerCase();
54
96
  const strategies = [];
97
+ if (flowId.includes('search')) {
98
+ strategies.push('search-baseline');
99
+ }
100
+ if (flowId.includes('threads') || flowId.includes('thread')) {
101
+ strategies.push('thread-reply');
102
+ }
103
+ if (flowId.includes('channels.lifecycle')) {
104
+ strategies.push('lifecycle-channel');
105
+ }
106
+ if (flowId.includes('channels.settings')) {
107
+ strategies.push('channel-settings');
108
+ }
109
+ if (flowId.includes('channels.switch')) {
110
+ strategies.push('channel-switch');
111
+ }
112
+ if (flowId.includes('messaging.markdown')) {
113
+ strategies.push('markdown-post');
114
+ }
115
+ if (flowId.includes('messaging.mentions')) {
116
+ strategies.push('mentions-post');
117
+ }
118
+ if (flowId.includes('messaging.realtime')) {
119
+ strategies.push('realtime-post');
120
+ }
55
121
  if (/(thread|reply|rhs|sidebar[_-]?right)/.test(haystack)) {
56
122
  strategies.push('thread-reply');
57
123
  }
124
+ if (/(create|join|leave|invite)/.test(haystack)) {
125
+ strategies.push('lifecycle-channel');
126
+ }
127
+ if (/(settings|preferences)/.test(haystack)) {
128
+ strategies.push('channel-settings');
129
+ }
130
+ if (/(switch|quick\\s*switch)/.test(haystack)) {
131
+ strategies.push('channel-switch');
132
+ }
133
+ if (/(markdown|format)/.test(haystack)) {
134
+ strategies.push('markdown-post');
135
+ }
136
+ if (/(mention|@)/.test(haystack)) {
137
+ strategies.push('mentions-post');
138
+ }
139
+ if (/(realtime|websocket|presence)/.test(haystack)) {
140
+ strategies.push('realtime-post');
141
+ }
142
+ if (/(search|find|spotlight)/.test(haystack)) {
143
+ strategies.push('search-baseline');
144
+ }
58
145
  if (/(message|post|realtime|websocket|chat)/.test(haystack)) {
59
146
  strategies.push('message-post');
60
147
  }
61
148
  if (/(channel|navigation|sidebar|switch)/.test(haystack)) {
62
149
  strategies.push('channel-baseline');
63
150
  }
64
- if (/(search|find|spotlight)/.test(haystack)) {
65
- strategies.push('search-baseline');
66
- }
67
151
  strategies.push('generic-baseline');
68
152
  return Array.from(new Set(strategies));
69
153
  }
70
- function validateGeneratedSpecContent(content) {
154
+ function createDefaultApiSurfaceCatalog() {
155
+ const pwNestedMethods = new Map();
156
+ pwNestedMethods.set('apiClient', new Set([
157
+ 'createPost',
158
+ 'createDirectChannel',
159
+ 'createChannel',
160
+ 'getChannels',
161
+ 'getChannelByName',
162
+ 'getPostsSince',
163
+ ]));
164
+ return {
165
+ pwProps: new Set([
166
+ 'initSetup',
167
+ 'testBrowser',
168
+ 'apiInitSetup',
169
+ 'apiAdminSetup',
170
+ 'apiCreateChannel',
171
+ 'apiCreateUser',
172
+ 'apiLogin',
173
+ 'apiClient',
174
+ ]),
175
+ pwNestedMethods,
176
+ initSetupKeys: new Set([
177
+ 'user',
178
+ 'team',
179
+ 'adminClient',
180
+ 'adminUser',
181
+ 'adminConfig',
182
+ 'userClient',
183
+ 'offTopicUrl',
184
+ 'townSquareUrl',
185
+ ]),
186
+ initSetupVariableMethods: new Map(),
187
+ testBrowserMethods: new Set([
188
+ 'login',
189
+ 'openNewBrowserContext',
190
+ 'newContext',
191
+ ]),
192
+ channelsPageMembers: new Set([
193
+ 'goto',
194
+ 'page',
195
+ 'postMessage',
196
+ 'getLastPost',
197
+ 'sidebarRight',
198
+ 'openChannelSettings',
199
+ 'newChannel',
200
+ 'globalHeader',
201
+ 'searchBox',
202
+ ]),
203
+ sidebarRightMembers: new Set([
204
+ 'openThreadForPost',
205
+ 'postMessage',
206
+ 'getLastPost',
207
+ ]),
208
+ };
209
+ }
210
+ function collectMatches(content, pattern) {
211
+ const out = new Set();
212
+ for (const match of content.matchAll(pattern)) {
213
+ const value = match[1];
214
+ if (value) {
215
+ out.add(value);
216
+ }
217
+ }
218
+ return out;
219
+ }
220
+ function addNestedMethod(catalog, objectName, methodName) {
221
+ const methods = catalog.pwNestedMethods.get(objectName) || new Set();
222
+ methods.add(methodName);
223
+ catalog.pwNestedMethods.set(objectName, methods);
224
+ }
225
+ function escapeRegExp(value) {
226
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
227
+ }
228
+ function parseInitSetupBindings(content) {
229
+ const bindings = [];
230
+ for (const match of content.matchAll(/(?:const|let|var)\s*\{\s*([^}]+)\s*\}\s*=\s*await\s+pw\.initSetup\s*\(/g)) {
231
+ const raw = match[1];
232
+ if (!raw) {
233
+ continue;
234
+ }
235
+ for (const part of raw.split(',')) {
236
+ const cleaned = part.trim();
237
+ if (!cleaned) {
238
+ continue;
239
+ }
240
+ const [leftRaw, rightRaw] = cleaned.split(':');
241
+ const key = (leftRaw || '').trim();
242
+ const variableCandidate = (rightRaw || leftRaw || '').trim().split('=')[0]?.trim();
243
+ if (!key || !variableCandidate) {
244
+ continue;
245
+ }
246
+ bindings.push({ key, variable: variableCandidate });
247
+ }
248
+ }
249
+ return bindings;
250
+ }
251
+ function collectDestructuredInitSetupKeys(content) {
252
+ return new Set(parseInitSetupBindings(content).map((binding) => binding.key));
253
+ }
254
+ function addInitSetupVariableMethod(catalog, variable, methodName) {
255
+ const methods = catalog.initSetupVariableMethods.get(variable) || new Set();
256
+ methods.add(methodName);
257
+ catalog.initSetupVariableMethods.set(variable, methods);
258
+ }
259
+ function collectApiSurfaceFromContent(content, catalog) {
260
+ for (const prop of collectMatches(content, /\bpw\.([A-Za-z_][A-Za-z0-9_]*)\b/g)) {
261
+ catalog.pwProps.add(prop);
262
+ }
263
+ for (const match of content.matchAll(/\bpw\.([A-Za-z_][A-Za-z0-9_]*)\.([A-Za-z_][A-Za-z0-9_]*)\b/g)) {
264
+ const objectName = match[1];
265
+ const methodName = match[2];
266
+ if (!objectName || !methodName) {
267
+ continue;
268
+ }
269
+ addNestedMethod(catalog, objectName, methodName);
270
+ }
271
+ for (const method of collectMatches(content, /\bpw\.testBrowser\.([A-Za-z_][A-Za-z0-9_]*)\b/g)) {
272
+ catalog.testBrowserMethods.add(method);
273
+ }
274
+ for (const member of collectMatches(content, /\bchannelsPage\.([A-Za-z_][A-Za-z0-9_]*)\b/g)) {
275
+ catalog.channelsPageMembers.add(member);
276
+ }
277
+ for (const member of collectMatches(content, /\bchannelsPage\.sidebarRight\.([A-Za-z_][A-Za-z0-9_]*)\b/g)) {
278
+ catalog.sidebarRightMembers.add(member);
279
+ }
280
+ for (const binding of parseInitSetupBindings(content)) {
281
+ catalog.initSetupKeys.add(binding.key);
282
+ const methodPattern = new RegExp(`\\b${escapeRegExp(binding.variable)}\\.([A-Za-z_][A-Za-z0-9_]*)\\b`, 'g');
283
+ for (const method of collectMatches(content, methodPattern)) {
284
+ addInitSetupVariableMethod(catalog, binding.variable, method);
285
+ }
286
+ }
287
+ }
288
+ function buildApiSurfaceCatalog(testsRoot, seedFile) {
289
+ const catalog = createDefaultApiSurfaceCatalog();
290
+ const candidateRoots = [
291
+ (0, path_1.join)(testsRoot, 'specs'),
292
+ (0, path_1.join)(testsRoot, 'tests'),
293
+ ];
294
+ const files = [];
295
+ for (const root of candidateRoots) {
296
+ if (!(0, fs_1.existsSync)(root)) {
297
+ continue;
298
+ }
299
+ const stack = [root];
300
+ while (stack.length > 0) {
301
+ const current = stack.pop();
302
+ let entries;
303
+ try {
304
+ entries = (0, fs_1.readdirSync)(current, { withFileTypes: true });
305
+ }
306
+ catch {
307
+ continue;
308
+ }
309
+ for (const entry of entries) {
310
+ const full = (0, path_1.join)(current, entry.name);
311
+ if (entry.isDirectory()) {
312
+ if (entry.name === 'node_modules' || entry.name === '.git' || entry.name === 'dist') {
313
+ continue;
314
+ }
315
+ stack.push(full);
316
+ continue;
317
+ }
318
+ if (!entry.isFile()) {
319
+ continue;
320
+ }
321
+ if (!/\.(spec|test)\.[jt]sx?$/.test(entry.name)) {
322
+ continue;
323
+ }
324
+ files.push(full);
325
+ }
326
+ }
327
+ }
328
+ const uniqueFiles = Array.from(new Set(files)).slice(0, 2500);
329
+ for (const filePath of uniqueFiles) {
330
+ try {
331
+ const content = (0, fs_1.readFileSync)(filePath, 'utf-8');
332
+ collectApiSurfaceFromContent(content, catalog);
333
+ }
334
+ catch {
335
+ continue;
336
+ }
337
+ }
338
+ const absoluteSeed = (0, path_1.join)(testsRoot, seedFile);
339
+ if ((0, fs_1.existsSync)(absoluteSeed)) {
340
+ try {
341
+ collectApiSurfaceFromContent((0, fs_1.readFileSync)(absoluteSeed, 'utf-8'), catalog);
342
+ }
343
+ catch {
344
+ // ignore seed read failures; defaults + catalog scan still apply
345
+ }
346
+ }
347
+ return catalog;
348
+ }
349
+ function validateGeneratedSpecContent(content, apiSurface) {
71
350
  const issues = [];
72
351
  if (/\btest\.describe\s*\(/.test(content)) {
73
352
  issues.push({
@@ -93,13 +372,64 @@ function validateGeneratedSpecContent(content) {
93
372
  message: 'Generated tests must use a single tag string, not a tag array.',
94
373
  });
95
374
  }
96
- const hasTagString = /\btag\s*:\s*['"][^'"]+['"]/.test(content);
97
- if (!hasTagString || !/@ai-assisted/.test(content)) {
375
+ const hasTagOption = /\btag\s*:\s*['"][^'"]+['"]/.test(content);
376
+ const hasTagInTitle = /\btest(?:\.\w+)?\s*\(\s*['"][^'"]*@ai-assisted[^'"]*['"]/.test(content);
377
+ if (!(hasTagOption || hasTagInTitle) || !/@ai-assisted/.test(content)) {
98
378
  issues.push({
99
379
  code: 'missing-tag',
100
- message: "Generated tests must include a single '@ai-assisted' tag.",
380
+ message: "Generated tests must include '@ai-assisted' either as tag option or in test title.",
101
381
  });
102
382
  }
383
+ if (apiSurface) {
384
+ const unknownPwProps = Array.from(collectMatches(content, /\bpw\.([A-Za-z_][A-Za-z0-9_]*)\b/g)).filter((prop) => !apiSurface.pwProps.has(prop));
385
+ const unknownBrowserMethods = Array.from(collectMatches(content, /\bpw\.testBrowser\.([A-Za-z_][A-Za-z0-9_]*)\b/g)).filter((method) => !apiSurface.testBrowserMethods.has(method));
386
+ const unknownNestedPwMembers = [];
387
+ for (const match of content.matchAll(/\bpw\.([A-Za-z_][A-Za-z0-9_]*)\.([A-Za-z_][A-Za-z0-9_]*)\b/g)) {
388
+ const objectName = match[1];
389
+ const methodName = match[2];
390
+ if (!objectName || !methodName || objectName === 'testBrowser') {
391
+ continue;
392
+ }
393
+ const knownMethods = apiSurface.pwNestedMethods.get(objectName);
394
+ if (!knownMethods || !knownMethods.has(methodName)) {
395
+ unknownNestedPwMembers.push(`pw.${objectName}.${methodName}`);
396
+ }
397
+ }
398
+ const unknownChannelMembers = Array.from(collectMatches(content, /\bchannelsPage\.([A-Za-z_][A-Za-z0-9_]*)\b/g)).filter((member) => !apiSurface.channelsPageMembers.has(member));
399
+ const unknownSidebarMembers = Array.from(collectMatches(content, /\bchannelsPage\.sidebarRight\.([A-Za-z_][A-Za-z0-9_]*)\b/g)).filter((member) => !apiSurface.sidebarRightMembers.has(member));
400
+ const initSetupBindings = parseInitSetupBindings(content);
401
+ const unknownInitSetupKeys = initSetupBindings
402
+ .map((binding) => binding.key)
403
+ .filter((key) => !apiSurface.initSetupKeys.has(key));
404
+ const unknownInitSetupVariableMethods = [];
405
+ for (const binding of initSetupBindings) {
406
+ const knownMethods = apiSurface.initSetupVariableMethods.get(binding.variable);
407
+ if (!knownMethods || knownMethods.size === 0) {
408
+ continue;
409
+ }
410
+ const methodPattern = new RegExp(`\\b${escapeRegExp(binding.variable)}\\.([A-Za-z_][A-Za-z0-9_]*)\\b`, 'g');
411
+ for (const method of collectMatches(content, methodPattern)) {
412
+ if (!knownMethods.has(method)) {
413
+ unknownInitSetupVariableMethods.push(`${binding.variable}.${method}`);
414
+ }
415
+ }
416
+ }
417
+ const unknown = [
418
+ ...unknownPwProps.map((value) => `pw.${value}`),
419
+ ...unknownBrowserMethods.map((value) => `pw.testBrowser.${value}`),
420
+ ...unknownNestedPwMembers,
421
+ ...unknownChannelMembers.map((value) => `channelsPage.${value}`),
422
+ ...unknownSidebarMembers.map((value) => `channelsPage.sidebarRight.${value}`),
423
+ ...unknownInitSetupKeys.map((value) => `pw.initSetup.{${value}}`),
424
+ ...unknownInitSetupVariableMethods,
425
+ ];
426
+ if (unknown.length > 0) {
427
+ issues.push({
428
+ code: 'unknown-api-surface',
429
+ message: `Generated test uses unknown API/page-object members: ${Array.from(new Set(unknown)).join(', ')}`,
430
+ });
431
+ }
432
+ }
103
433
  return issues;
104
434
  }
105
435
  function createNativePlaywrightSpec(flow, slug, strategy) {
@@ -130,10 +460,76 @@ function createNativePlaywrightSpec(flow, slug, strategy) {
130
460
  ...start,
131
461
  ` const parentMessage = \`ai-${slug}-parent-\${Date.now()}\`;`,
132
462
  ' await channelsPage.postMessage(parentMessage);',
133
- ' await channelsPage.openAThread(parentMessage);',
463
+ ' const rootPost = await channelsPage.getLastPost();',
464
+ ' await rootPost.openAThread();',
134
465
  ` const replyMessage = \`ai-${slug}-reply-\${Date.now()}\`;`,
135
466
  ' await channelsPage.sidebarRight.postMessage(replyMessage);',
136
- ' await expect(channelsPage.sidebarRight.getLastPost()).toContainText(replyMessage);',
467
+ ' const lastReply = await channelsPage.sidebarRight.getLastPost();',
468
+ ' await expect(lastReply.container).toContainText(replyMessage);',
469
+ ...end,
470
+ ].join('\n');
471
+ }
472
+ if (strategy === 'lifecycle-channel') {
473
+ return [
474
+ ...header,
475
+ ...start,
476
+ ` const channelName = \`ai-${slug}-\${Date.now().toString().slice(-6)}\`;`,
477
+ " await channelsPage.newChannel(channelName, 'O');",
478
+ ' await expect(channelsPage.page).toHaveURL(new RegExp(`/channels/${channelName}$`));',
479
+ ...end,
480
+ ].join('\n');
481
+ }
482
+ if (strategy === 'channel-settings') {
483
+ return [
484
+ ...header,
485
+ ...start,
486
+ ' await channelsPage.openChannelSettings();',
487
+ " await expect(channelsPage.page.getByRole('dialog', {name: 'Channel Settings'})).toBeVisible();",
488
+ " await channelsPage.page.keyboard.press('Escape');",
489
+ ...end,
490
+ ].join('\n');
491
+ }
492
+ if (strategy === 'channel-switch') {
493
+ return [
494
+ ...header,
495
+ ...start,
496
+ " await channelsPage.goto(team.name, 'off-topic');",
497
+ " await expect(channelsPage.page).toHaveURL(/\\/channels\\/off-topic$/);",
498
+ " await expect(channelsPage.page.locator('#channelHeaderTitle')).toContainText(/off-topic/i);",
499
+ ...end,
500
+ ].join('\n');
501
+ }
502
+ if (strategy === 'markdown-post') {
503
+ return [
504
+ ...header,
505
+ ...start,
506
+ ` const message = '**ai-${slug}-bold** _italic_';`,
507
+ ' await channelsPage.postMessage(message);',
508
+ ' const lastPost = await channelsPage.getLastPost();',
509
+ " await expect(lastPost.container.locator('strong')).toBeVisible();",
510
+ ...end,
511
+ ].join('\n');
512
+ }
513
+ if (strategy === 'mentions-post') {
514
+ return [
515
+ ...header,
516
+ ...start,
517
+ ' const mention = `@${user.username}`;',
518
+ ' await channelsPage.postMessage(`Ping ${mention}`);',
519
+ ' const lastPost = await channelsPage.getLastPost();',
520
+ ' await expect(lastPost.container).toContainText(mention);',
521
+ ...end,
522
+ ].join('\n');
523
+ }
524
+ if (strategy === 'realtime-post') {
525
+ return [
526
+ ...header,
527
+ ...start,
528
+ ` const message = \`ai-${slug}-realtime-\${Date.now()}\`;`,
529
+ ' await channelsPage.postMessage(message);',
530
+ ' const lastPost = await channelsPage.getLastPost();',
531
+ ' await expect(lastPost.container).toContainText(message);',
532
+ " await expect(channelsPage.page.locator('#channel_view')).toBeVisible();",
137
533
  ...end,
138
534
  ].join('\n');
139
535
  }
@@ -160,11 +556,12 @@ function createNativePlaywrightSpec(flow, slug, strategy) {
160
556
  return [
161
557
  ...header,
162
558
  ...start,
163
- ` const searchTerm = '${slug}'.slice(0, 20);`,
164
- " await channelsPage.page.keyboard.press('ControlOrMeta+K');",
165
- ' await channelsPage.page.keyboard.type(searchTerm);',
166
- " await channelsPage.page.keyboard.press('Escape');",
167
- ' await expect(channelsPage.page).toHaveURL(/\\/channels\\//);',
559
+ ` const searchTerm = \`ai-${slug}-\${Date.now().toString().slice(-6)}\`;`,
560
+ ' await channelsPage.postMessage(searchTerm);',
561
+ ' await channelsPage.globalHeader.openSearch();',
562
+ ' await channelsPage.searchBox.searchInput.fill(searchTerm);',
563
+ " await channelsPage.page.keyboard.press('Enter');",
564
+ " await expect(channelsPage.page.locator('#searchContainer')).toBeVisible();",
168
565
  ...end,
169
566
  ].join('\n');
170
567
  }
@@ -195,11 +592,11 @@ function summarizeCommandOutput(stdout, stderr) {
195
592
  const lines = combined.split('\n').slice(-20);
196
593
  return lines.join('\n').slice(0, 2000);
197
594
  }
198
- function runCommand(command, args, cwd) {
595
+ function runCommand(command, args, cwd, timeoutMs = 60 * 60 * 1000) {
199
596
  const result = (0, child_process_1.spawnSync)(command, args, {
200
597
  cwd,
201
598
  encoding: 'utf-8',
202
- timeout: 60 * 60 * 1000,
599
+ timeout: timeoutMs,
203
600
  stdio: 'pipe',
204
601
  });
205
602
  return {
@@ -209,6 +606,37 @@ function runCommand(command, args, cwd) {
209
606
  error: result.error ? result.error.message : undefined,
210
607
  };
211
608
  }
609
+ function runPlaywrightRuntimeValidation(testsRoot, testFile, pipeline, playwrightBinary) {
610
+ if (!playwrightBinary) {
611
+ return {
612
+ status: 'failed',
613
+ detail: 'Playwright binary not found; cannot execute runtime validation.',
614
+ };
615
+ }
616
+ const relativeSpecPath = (0, utils_js_1.normalizePath)((0, path_1.relative)(testsRoot, testFile));
617
+ if (relativeSpecPath.startsWith('../') || relativeSpecPath.startsWith('..\\')) {
618
+ return {
619
+ status: 'failed',
620
+ detail: 'Generated spec path resolved outside testsRoot during runtime validation.',
621
+ };
622
+ }
623
+ const args = ['test', relativeSpecPath, '--workers', '1', '--retries', '0', '--max-failures', '1', '--reporter', 'line'];
624
+ if (pipeline.headless === false) {
625
+ args.push('--headed');
626
+ }
627
+ if (pipeline.project) {
628
+ args.push('--project', pipeline.project);
629
+ }
630
+ const commandResult = runCommand(playwrightBinary, args, testsRoot, 10 * 60 * 1000);
631
+ if (commandResult.status === 0) {
632
+ return { status: 'passed' };
633
+ }
634
+ const summary = summarizeCommandOutput(commandResult.stdout, commandResult.stderr);
635
+ return {
636
+ status: 'failed',
637
+ detail: summary || commandResult.error || `playwright test failed with status ${commandResult.status}`,
638
+ };
639
+ }
212
640
  function runPlaywrightListValidation(testsRoot, testFile, pipeline, playwrightBinary) {
213
641
  if (!playwrightBinary) {
214
642
  return {
@@ -224,6 +652,9 @@ function runPlaywrightListValidation(testsRoot, testFile, pipeline, playwrightBi
224
652
  };
225
653
  }
226
654
  const args = ['test', '--list', relativeSpecPath];
655
+ if (pipeline.headless === false) {
656
+ args.push('--headed');
657
+ }
227
658
  if (pipeline.project) {
228
659
  args.push('--project', pipeline.project);
229
660
  }
@@ -243,7 +674,7 @@ function runPlaywrightListValidation(testsRoot, testFile, pipeline, playwrightBi
243
674
  detail: summary || commandResult.error || `playwright --list failed with status ${commandResult.status}`,
244
675
  };
245
676
  }
246
- function runPackageNativeFlow(testsRoot, flow, pipeline, outputDir, testFile, playwrightBinary) {
677
+ function runPackageNativeFlow(testsRoot, flow, pipeline, outputDir, testFile, playwrightBinary, apiSurface) {
247
678
  const flowId = flow.id;
248
679
  const flowName = flow.name;
249
680
  const existingFile = (0, fs_1.existsSync)(testFile);
@@ -284,7 +715,7 @@ function runPackageNativeFlow(testsRoot, flow, pipeline, outputDir, testFile, pl
284
715
  wroteNewFile = true;
285
716
  }
286
717
  const currentContent = candidate.write ? candidate.content : (originalContent || '');
287
- const qualityIssues = validateGeneratedSpecContent(currentContent);
718
+ const qualityIssues = validateGeneratedSpecContent(currentContent, apiSurface);
288
719
  if (qualityIssues.length > 0) {
289
720
  attempts.push(`${candidate.label}: ${qualityIssues.map((issue) => issue.message).join(' ')}`);
290
721
  if (pipeline.heal && i < candidates.length - 1) {
@@ -353,10 +784,10 @@ function runPackageNativeFlow(testsRoot, flow, pipeline, outputDir, testFile, pl
353
784
  }
354
785
  function runPackageNativePipeline(testsRoot, flows, pipeline, baseWarnings = []) {
355
786
  const warningSet = new Set(baseWarnings);
356
- if (pipeline.mcp) {
357
- warningSet.add('Package-native pipeline does not run Playwright MCP directly. Use follow-up heal workflows if MCP is required.');
358
- }
787
+ const mcp = createMcpStatus('package-native', Boolean(pipeline.mcp));
359
788
  const playwrightBinary = pipeline.heal ? resolvePlaywrightBinary(testsRoot) : null;
789
+ const seedFile = resolveAgentSeedSpec(testsRoot) || 'specs/seed.spec.ts';
790
+ const apiSurface = buildApiSurfaceCatalog(testsRoot, seedFile);
360
791
  if (pipeline.heal && !playwrightBinary) {
361
792
  warningSet.add('Playwright binary was not found. Heal uses static quality checks without runtime compile validation.');
362
793
  }
@@ -364,7 +795,7 @@ function runPackageNativePipeline(testsRoot, flows, pipeline, baseWarnings = [])
364
795
  const outputBase = (0, path_1.resolve)(testsRoot, pipeline.outputDir || 'specs/functional/ai-assisted');
365
796
  if (!(0, utils_js_1.isPathWithinRoot)(testsRoot, outputBase)) {
366
797
  warningSet.add(`Pipeline outputDir resolves outside testsRoot and was blocked: ${pipeline.outputDir}`);
367
- return { runner: 'unknown', results, warnings: Array.from(warningSet) };
798
+ return { runner: 'unknown', results, warnings: Array.from(warningSet), mcp: createMcpStatus('unknown', Boolean(pipeline.mcp)) };
368
799
  }
369
800
  for (const flow of flows) {
370
801
  if (flow.priority !== 'P0' && flow.priority !== 'P1') {
@@ -403,22 +834,26 @@ function runPackageNativePipeline(testsRoot, flows, pipeline, baseWarnings = [])
403
834
  });
404
835
  continue;
405
836
  }
406
- results.push(runPackageNativeFlow(testsRoot, flow, pipeline, outputDir, testFile, playwrightBinary));
837
+ results.push(runPackageNativeFlow(testsRoot, flow, pipeline, outputDir, testFile, playwrightBinary, apiSurface));
407
838
  }
408
- return { runner: 'package-native', results, warnings: Array.from(warningSet) };
839
+ return { runner: 'package-native', results, warnings: Array.from(warningSet), mcp };
409
840
  }
410
841
  function runTargetedSpecHeal(testsRoot, targets, pipeline) {
411
842
  const warnings = new Set();
412
843
  const results = [];
844
+ const mcp = createMcpStatus('package-native', Boolean(pipeline.mcp));
413
845
  if (targets.length === 0) {
414
846
  warnings.add('No targeted specs provided for heal.');
415
- return {
847
+ return finalizePipelineSummary({
416
848
  runner: 'package-native',
417
849
  results,
418
850
  warnings: Array.from(warnings),
419
- };
851
+ mcp,
852
+ });
420
853
  }
421
854
  const playwrightBinary = pipeline.heal ? resolvePlaywrightBinary(testsRoot) : null;
855
+ const seedFile = resolveAgentSeedSpec(testsRoot) || 'specs/seed.spec.ts';
856
+ const apiSurface = buildApiSurfaceCatalog(testsRoot, seedFile);
422
857
  if (pipeline.heal && !playwrightBinary) {
423
858
  warnings.add('Playwright binary was not found. Targeted heal uses static quality checks without runtime compile validation.');
424
859
  }
@@ -470,13 +905,14 @@ function runTargetedSpecHeal(testsRoot, targets, pipeline) {
470
905
  continue;
471
906
  }
472
907
  const syntheticFlow = buildSyntheticFlowFromSpecTarget(relativeSpecPath, target);
473
- results.push(runPackageNativeFlow(testsRoot, syntheticFlow, pipeline, (0, utils_js_1.normalizePath)((0, path_1.dirname)(absoluteSpecPath)), absoluteSpecPath, playwrightBinary));
908
+ results.push(runPackageNativeFlow(testsRoot, syntheticFlow, pipeline, (0, utils_js_1.normalizePath)((0, path_1.dirname)(absoluteSpecPath)), absoluteSpecPath, playwrightBinary, apiSurface));
474
909
  }
475
- return {
910
+ return finalizePipelineSummary({
476
911
  runner: 'package-native',
477
912
  results,
478
913
  warnings: Array.from(warnings),
479
- };
914
+ mcp,
915
+ });
480
916
  }
481
917
  function findSpecFiles(root) {
482
918
  if (!(0, fs_1.existsSync)(root)) {
@@ -499,17 +935,387 @@ function findDisallowedDescribeFiles(root) {
499
935
  const files = findSpecFiles(root);
500
936
  return files.filter((file) => /\btest\.describe\s*\(/.test((0, fs_1.readFileSync)(file, 'utf-8')));
501
937
  }
938
+ function hasCommand(command, cwd) {
939
+ const result = runCommand(command, ['--version'], cwd);
940
+ return result.status === 0;
941
+ }
942
+ function hasPlaywrightAgentDefinitions(testsRoot) {
943
+ const required = [
944
+ '.mcp.json',
945
+ '.claude/agents/playwright-test-planner.md',
946
+ '.claude/agents/playwright-test-generator.md',
947
+ '.claude/agents/playwright-test-healer.md',
948
+ ];
949
+ return required.every((path) => (0, fs_1.existsSync)((0, path_1.join)(testsRoot, path)));
950
+ }
951
+ function hasPlaywrightConfig(testsRoot) {
952
+ const candidates = [
953
+ 'playwright.config.ts',
954
+ 'playwright.config.js',
955
+ 'playwright.config.mts',
956
+ 'playwright.config.mjs',
957
+ 'playwright.config.cts',
958
+ 'playwright.config.cjs',
959
+ ];
960
+ return candidates.some((candidate) => (0, fs_1.existsSync)((0, path_1.join)(testsRoot, candidate)));
961
+ }
962
+ function bootstrapPlaywrightAgentDefinitions(testsRoot, pipeline) {
963
+ const args = ['playwright', 'init-agents', '--loop=claude', '--prompts'];
964
+ if (pipeline.project) {
965
+ args.push('--project', pipeline.project);
966
+ }
967
+ return runCommand('npx', args, testsRoot);
968
+ }
969
+ function resolveAgentSeedSpec(testsRoot) {
970
+ const preferred = (0, path_1.join)(testsRoot, 'specs', 'seed.spec.ts');
971
+ const specsRoot = (0, path_1.join)(testsRoot, 'specs');
972
+ const specFiles = findSpecFiles(specsRoot).filter((file) => !(0, utils_js_1.normalizePath)(file).includes('/functional/ai-assisted/'));
973
+ const scored = specFiles
974
+ .map((file) => {
975
+ const rel = (0, utils_js_1.normalizePath)((0, path_1.relative)(testsRoot, file));
976
+ const content = (0, fs_1.readFileSync)(file, 'utf-8');
977
+ let score = 0;
978
+ if (rel.endsWith('/seed.spec.ts')) {
979
+ // Generated default seed from init-agents is often a placeholder; prefer real tests.
980
+ if (!/generate code here/i.test(content)) {
981
+ score += 2;
982
+ }
983
+ }
984
+ if (content.includes('@mattermost/playwright-lib')) {
985
+ score += 8;
986
+ }
987
+ if (content.includes('pw.initSetup(')) {
988
+ score += 6;
989
+ }
990
+ if (content.includes('testBrowser.login(')) {
991
+ score += 4;
992
+ }
993
+ if (content.includes('channelsPage')) {
994
+ score += 2;
995
+ }
996
+ if (rel.includes('/functional/channels/')) {
997
+ score += 1;
998
+ }
999
+ return { rel, score };
1000
+ })
1001
+ .sort((a, b) => b.score - a.score);
1002
+ if (scored.length > 0 && scored[0].score > 0) {
1003
+ return scored[0].rel;
1004
+ }
1005
+ if ((0, fs_1.existsSync)(preferred)) {
1006
+ return (0, utils_js_1.normalizePath)((0, path_1.relative)(testsRoot, preferred));
1007
+ }
1008
+ return null;
1009
+ }
1010
+ function buildPlaywrightAgentsPrompt(flow, seedFile, planFile, testFile, includeHealer) {
1011
+ const linkedFiles = firstFlowFiles(flow).join(', ') || 'N/A';
1012
+ const reasons = (flow.reasons || []).slice(0, 5).join(' | ') || 'N/A';
1013
+ return [
1014
+ 'Use official Playwright Test agents (planner, generator, healer) to implement exactly one high-quality test for this flow.',
1015
+ '',
1016
+ `Flow ID: ${flow.id}`,
1017
+ `Flow Name: ${flow.name}`,
1018
+ `Priority: ${flow.priority}`,
1019
+ `Linked files: ${linkedFiles}`,
1020
+ `Risk reasons: ${reasons}`,
1021
+ '',
1022
+ 'Workflow requirements:',
1023
+ '1) Use #playwright-test-planner to explore and save a focused test plan.',
1024
+ '2) Use #playwright-test-generator to generate one test from that plan.',
1025
+ includeHealer
1026
+ ? '3) Use #playwright-test-healer to run and fix that generated test.'
1027
+ : '3) Skip runtime healing and focus on producing compile-ready test code.',
1028
+ '',
1029
+ `Seed file: ${seedFile}`,
1030
+ `Plan file to save: ${planFile}`,
1031
+ `Generated test file path (must be exact): ${testFile}`,
1032
+ '',
1033
+ 'Quality constraints (must follow):',
1034
+ '- The generated file must contain a standalone test() and must not use test.describe or test.only.',
1035
+ '- Do not mark the test with test.fixme unless user explicitly requests skipping.',
1036
+ "- The generated test must include a single tag string '@ai-assisted'.",
1037
+ '- Match fixture/import style from the seed file. Prefer existing page-object APIs over raw brittle selectors.',
1038
+ '- Only use `pw` and page-object methods that already exist in the seed/current specs (for example, do not invent APIs like `pw.mainClient.*`).',
1039
+ '- Keep the scenario strictly aligned to the flow and linked files, not broad unrelated flows.',
1040
+ '',
1041
+ 'At the end, return a short summary that includes the generated test file path and whether healing succeeded.',
1042
+ ].join('\n');
1043
+ }
1044
+ function buildPlaywrightHealerPrompt(testFile, extra) {
1045
+ const lines = [
1046
+ 'Heal this specific Playwright test file and keep edits minimal.',
1047
+ `Target test file: ${testFile}`,
1048
+ 'Constraints:',
1049
+ '- Do not use test.describe or test.only.',
1050
+ "- Keep a single tag string '@ai-assisted'.",
1051
+ '- Use only existing Mattermost Playwright fixture/page-object APIs; do not invent new `pw.*` clients or methods.',
1052
+ '- Keep the test intent unchanged and focused.',
1053
+ '',
1054
+ 'Run and fix this test until it compiles/passes, or mark test.fixme with a clear comment when behavior is truly broken.',
1055
+ ];
1056
+ if (extra) {
1057
+ lines.push('', `Context: ${extra}`);
1058
+ }
1059
+ return lines.join('\n');
1060
+ }
1061
+ function runPlaywrightAgentsFlow(testsRoot, flow, pipeline, outputDir, preferredTestFile, seedFile, apiSurface, playwrightBinary) {
1062
+ (0, fs_1.mkdirSync)(outputDir, { recursive: true });
1063
+ const slug = toSafeSlug(flow.id);
1064
+ const planFile = (0, utils_js_1.normalizePath)((0, path_1.relative)(testsRoot, (0, path_1.join)(outputDir, `${slug}.plan.md`)));
1065
+ const targetTestFile = (0, utils_js_1.normalizePath)((0, path_1.relative)(testsRoot, preferredTestFile));
1066
+ if (pipeline.dryRun) {
1067
+ return {
1068
+ flowId: flow.id,
1069
+ flowName: flow.name,
1070
+ generatedDir: outputDir,
1071
+ generateStatus: 'skipped',
1072
+ healStatus: pipeline.heal ? 'skipped' : undefined,
1073
+ };
1074
+ }
1075
+ const prompt = buildPlaywrightAgentsPrompt(flow, seedFile, planFile, targetTestFile, Boolean(pipeline.heal));
1076
+ const runArgs = [
1077
+ '-p',
1078
+ '--permission-mode',
1079
+ 'bypassPermissions',
1080
+ '--mcp-config',
1081
+ '.mcp.json',
1082
+ '--add-dir',
1083
+ testsRoot,
1084
+ '--',
1085
+ prompt,
1086
+ ];
1087
+ const runResult = runCommand('claude', runArgs, testsRoot);
1088
+ if (runResult.status !== 0) {
1089
+ return {
1090
+ flowId: flow.id,
1091
+ flowName: flow.name,
1092
+ generatedDir: outputDir,
1093
+ generateStatus: 'failed',
1094
+ healStatus: pipeline.heal ? 'failed' : undefined,
1095
+ error: summarizeCommandOutput(runResult.stdout, runResult.stderr) || runResult.error || 'Playwright agents run failed',
1096
+ };
1097
+ }
1098
+ let actualTestFile = preferredTestFile;
1099
+ if (!(0, fs_1.existsSync)(actualTestFile)) {
1100
+ const candidates = findSpecFiles(outputDir);
1101
+ if (candidates.length === 1) {
1102
+ actualTestFile = candidates[0];
1103
+ }
1104
+ }
1105
+ if (!(0, fs_1.existsSync)(actualTestFile)) {
1106
+ return {
1107
+ flowId: flow.id,
1108
+ flowName: flow.name,
1109
+ generatedDir: outputDir,
1110
+ generateStatus: 'failed',
1111
+ healStatus: pipeline.heal ? 'failed' : undefined,
1112
+ error: `Playwright agents did not produce expected test file: ${targetTestFile}`,
1113
+ };
1114
+ }
1115
+ const relativeActualTestFile = (0, utils_js_1.normalizePath)((0, path_1.relative)(testsRoot, actualTestFile));
1116
+ let qualityIssues = validateGeneratedSpecContent((0, fs_1.readFileSync)(actualTestFile, 'utf-8'), apiSurface);
1117
+ if (qualityIssues.length > 0 && pipeline.heal) {
1118
+ const healResult = runCommand('claude', [
1119
+ '-p',
1120
+ '--permission-mode',
1121
+ 'bypassPermissions',
1122
+ '--agent',
1123
+ 'playwright-test-healer',
1124
+ '--mcp-config',
1125
+ '.mcp.json',
1126
+ '--add-dir',
1127
+ testsRoot,
1128
+ '--',
1129
+ buildPlaywrightHealerPrompt(relativeActualTestFile, qualityIssues.map((issue) => issue.message).join(' | ')),
1130
+ ], testsRoot);
1131
+ if (healResult.status === 0 && (0, fs_1.existsSync)(actualTestFile)) {
1132
+ qualityIssues = validateGeneratedSpecContent((0, fs_1.readFileSync)(actualTestFile, 'utf-8'), apiSurface);
1133
+ }
1134
+ }
1135
+ if (qualityIssues.length > 0) {
1136
+ return {
1137
+ flowId: flow.id,
1138
+ flowName: flow.name,
1139
+ generatedDir: outputDir,
1140
+ generateStatus: 'failed',
1141
+ healStatus: pipeline.heal ? 'failed' : undefined,
1142
+ error: `Playwright agents produced invalid test content: ${qualityIssues.map((issue) => issue.message).join(' | ')}`,
1143
+ };
1144
+ }
1145
+ if (pipeline.heal) {
1146
+ let compileValidation = runPlaywrightListValidation(testsRoot, actualTestFile, pipeline, playwrightBinary);
1147
+ if (compileValidation.status === 'failed') {
1148
+ const healResult = runCommand('claude', [
1149
+ '-p',
1150
+ '--permission-mode',
1151
+ 'bypassPermissions',
1152
+ '--agent',
1153
+ 'playwright-test-healer',
1154
+ '--mcp-config',
1155
+ '.mcp.json',
1156
+ '--add-dir',
1157
+ testsRoot,
1158
+ '--',
1159
+ buildPlaywrightHealerPrompt(relativeActualTestFile, compileValidation.detail || 'playwright --list failed'),
1160
+ ], testsRoot);
1161
+ if (healResult.status === 0 && (0, fs_1.existsSync)(actualTestFile)) {
1162
+ compileValidation = runPlaywrightListValidation(testsRoot, actualTestFile, pipeline, playwrightBinary);
1163
+ }
1164
+ if (compileValidation.status === 'failed') {
1165
+ return {
1166
+ flowId: flow.id,
1167
+ flowName: flow.name,
1168
+ generatedDir: outputDir,
1169
+ generateStatus: 'failed',
1170
+ healStatus: 'failed',
1171
+ error: `Playwright agents compile validation failed: ${compileValidation.detail || 'playwright --list failed'}`,
1172
+ };
1173
+ }
1174
+ }
1175
+ let runtimeValidation = runPlaywrightRuntimeValidation(testsRoot, actualTestFile, pipeline, playwrightBinary);
1176
+ if (runtimeValidation.status === 'failed') {
1177
+ const healResult = runCommand('claude', [
1178
+ '-p',
1179
+ '--permission-mode',
1180
+ 'bypassPermissions',
1181
+ '--agent',
1182
+ 'playwright-test-healer',
1183
+ '--mcp-config',
1184
+ '.mcp.json',
1185
+ '--add-dir',
1186
+ testsRoot,
1187
+ '--',
1188
+ buildPlaywrightHealerPrompt(relativeActualTestFile, runtimeValidation.detail || 'playwright runtime failed'),
1189
+ ], testsRoot);
1190
+ if (healResult.status === 0 && (0, fs_1.existsSync)(actualTestFile)) {
1191
+ runtimeValidation = runPlaywrightRuntimeValidation(testsRoot, actualTestFile, pipeline, playwrightBinary);
1192
+ }
1193
+ if (runtimeValidation.status === 'failed') {
1194
+ return {
1195
+ flowId: flow.id,
1196
+ flowName: flow.name,
1197
+ generatedDir: outputDir,
1198
+ generateStatus: 'failed',
1199
+ healStatus: 'failed',
1200
+ error: `Playwright agents runtime validation failed: ${runtimeValidation.detail || 'playwright test failed'}`,
1201
+ };
1202
+ }
1203
+ }
1204
+ }
1205
+ return {
1206
+ flowId: flow.id,
1207
+ flowName: flow.name,
1208
+ generatedDir: outputDir,
1209
+ generateStatus: 'success',
1210
+ healStatus: pipeline.heal ? 'success' : undefined,
1211
+ };
1212
+ }
1213
+ function runPlaywrightAgentsPipeline(testsRoot, flows, pipeline) {
1214
+ const warnings = [];
1215
+ const results = [];
1216
+ if (!hasCommand('claude', testsRoot)) {
1217
+ warnings.push('Claude CLI is required for official Playwright planner/generator/healer execution but was not found.');
1218
+ return { runner: 'unknown', results, warnings, mcp: createMcpStatus('unknown', true) };
1219
+ }
1220
+ if (!hasPlaywrightConfig(testsRoot)) {
1221
+ warnings.push('Playwright config file not found in testsRoot; skipping official Playwright agents backend.');
1222
+ return { runner: 'unknown', results, warnings, mcp: createMcpStatus('unknown', true) };
1223
+ }
1224
+ if (!hasPlaywrightAgentDefinitions(testsRoot)) {
1225
+ const bootstrap = bootstrapPlaywrightAgentDefinitions(testsRoot, pipeline);
1226
+ if (bootstrap.status !== 0) {
1227
+ warnings.push(summarizeCommandOutput(bootstrap.stdout, bootstrap.stderr) ||
1228
+ bootstrap.error ||
1229
+ 'Failed to initialize Playwright agents via `npx playwright init-agents`.');
1230
+ return { runner: 'unknown', results, warnings, mcp: createMcpStatus('unknown', true) };
1231
+ }
1232
+ }
1233
+ if (!hasPlaywrightAgentDefinitions(testsRoot)) {
1234
+ warnings.push('Playwright agent definitions are missing after bootstrap.');
1235
+ return { runner: 'unknown', results, warnings, mcp: createMcpStatus('unknown', true) };
1236
+ }
1237
+ const seedFile = resolveAgentSeedSpec(testsRoot);
1238
+ if (!seedFile) {
1239
+ warnings.push('No seed spec file found under specs/. Playwright planner cannot be initialized.');
1240
+ return { runner: 'unknown', results, warnings, mcp: createMcpStatus('unknown', true) };
1241
+ }
1242
+ const playwrightBinary = pipeline.heal ? resolvePlaywrightBinary(testsRoot) : null;
1243
+ const apiSurface = buildApiSurfaceCatalog(testsRoot, seedFile);
1244
+ if (pipeline.heal && !playwrightBinary) {
1245
+ warnings.push('Playwright binary was not found. Healer runtime validation may be limited.');
1246
+ }
1247
+ const outputBase = (0, path_1.resolve)(testsRoot, pipeline.outputDir || 'specs/functional/ai-assisted');
1248
+ if (!(0, utils_js_1.isPathWithinRoot)(testsRoot, outputBase)) {
1249
+ warnings.push(`Pipeline outputDir resolves outside testsRoot and was blocked: ${pipeline.outputDir}`);
1250
+ return { runner: 'unknown', results, warnings, mcp: createMcpStatus('unknown', true) };
1251
+ }
1252
+ for (const flow of flows) {
1253
+ if (flow.priority !== 'P0' && flow.priority !== 'P1') {
1254
+ continue;
1255
+ }
1256
+ const slug = toSafeSlug(flow.id);
1257
+ const outputDir = (0, utils_js_1.normalizePath)((0, path_1.join)(outputBase, slug));
1258
+ if (!(0, utils_js_1.isPathWithinRoot)(testsRoot, outputDir)) {
1259
+ results.push({
1260
+ flowId: flow.id,
1261
+ flowName: flow.name,
1262
+ generatedDir: outputDir,
1263
+ generateStatus: 'failed',
1264
+ error: 'output directory resolves outside testsRoot',
1265
+ });
1266
+ continue;
1267
+ }
1268
+ const testFile = (0, utils_js_1.normalizePath)((0, path_1.join)(outputDir, `${slug}.spec.ts`));
1269
+ if (!(0, utils_js_1.isPathWithinRoot)(testsRoot, testFile)) {
1270
+ results.push({
1271
+ flowId: flow.id,
1272
+ flowName: flow.name,
1273
+ generatedDir: outputDir,
1274
+ generateStatus: 'failed',
1275
+ error: 'generated test path resolves outside testsRoot',
1276
+ });
1277
+ continue;
1278
+ }
1279
+ results.push(runPlaywrightAgentsFlow(testsRoot, flow, pipeline, outputDir, testFile, seedFile, apiSurface, playwrightBinary));
1280
+ }
1281
+ return { runner: 'playwright-agents', results, warnings, mcp: createMcpStatus('playwright-agents', true) };
1282
+ }
502
1283
  function runPlaywrightPipeline(testsRoot, flows, pipeline) {
1284
+ const mcpFallbackWarnings = [];
1285
+ if (pipeline.mcp) {
1286
+ const agentsSummary = runPlaywrightAgentsPipeline(testsRoot, flows, pipeline);
1287
+ if (agentsSummary.runner !== 'unknown' || agentsSummary.results.length > 0) {
1288
+ return finalizePipelineSummary(agentsSummary);
1289
+ }
1290
+ if (!pipeline.mcpAllowFallback) {
1291
+ const warnings = [
1292
+ ...agentsSummary.warnings,
1293
+ 'Official Playwright MCP mode is strict; fallback generation is disabled unless pipeline.mcpAllowFallback=true.',
1294
+ ];
1295
+ return finalizePipelineSummary({
1296
+ runner: 'unknown',
1297
+ results: agentsSummary.results,
1298
+ warnings,
1299
+ mcp: createMcpStatus('unknown', true),
1300
+ });
1301
+ }
1302
+ mcpFallbackWarnings.push(...agentsSummary.warnings);
1303
+ }
503
1304
  const cliPath = hasE2eTestGenCLI(testsRoot);
504
1305
  if (!cliPath) {
505
- return runPackageNativePipeline(testsRoot, flows, pipeline, ['e2e-test-gen-cli.ts not found; using package-native pipeline fallback.']);
1306
+ return finalizePipelineSummary(runPackageNativePipeline(testsRoot, flows, pipeline, mcpFallbackWarnings));
506
1307
  }
507
- const warnings = [];
1308
+ const warnings = [...mcpFallbackWarnings];
508
1309
  const results = [];
509
1310
  const outputBase = (0, path_1.resolve)(testsRoot, pipeline.outputDir || 'specs/functional/ai-assisted');
510
1311
  if (!(0, utils_js_1.isPathWithinRoot)(testsRoot, outputBase)) {
511
1312
  warnings.push(`Pipeline outputDir resolves outside testsRoot and was blocked: ${pipeline.outputDir}`);
512
- return { runner: 'unknown', results, warnings };
1313
+ return finalizePipelineSummary({
1314
+ runner: 'unknown',
1315
+ results,
1316
+ warnings,
1317
+ mcp: createMcpStatus('unknown', Boolean(pipeline.mcp)),
1318
+ });
513
1319
  }
514
1320
  for (const flow of flows) {
515
1321
  if (flow.priority !== 'P0' && flow.priority !== 'P1') {
@@ -605,5 +1411,10 @@ function runPlaywrightPipeline(testsRoot, flows, pipeline) {
605
1411
  healStatus,
606
1412
  });
607
1413
  }
608
- return { runner: 'e2e-test-gen', results, warnings };
1414
+ return finalizePipelineSummary({
1415
+ runner: 'e2e-test-gen',
1416
+ results,
1417
+ warnings,
1418
+ mcp: createMcpStatus('e2e-test-gen', Boolean(pipeline.mcp)),
1419
+ });
609
1420
  }