@yasserkhanorg/e2e-agents 0.3.2 → 0.3.3
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.
- package/README.md +22 -18
- package/dist/agent/config.d.ts +1 -0
- package/dist/agent/config.d.ts.map +1 -1
- package/dist/agent/config.js +10 -0
- package/dist/agent/pipeline.d.ts +6 -1
- package/dist/agent/pipeline.d.ts.map +1 -1
- package/dist/agent/pipeline.js +627 -27
- package/dist/agent/report.d.ts +5 -0
- package/dist/agent/report.d.ts.map +1 -1
- package/dist/agent/report.js +3 -0
- package/dist/agent/runner.d.ts.map +1 -1
- package/dist/agent/runner.js +25 -6
- package/dist/agent/tests.d.ts.map +1 -1
- package/dist/agent/tests.js +12 -2
- package/dist/cli.js +73 -5
- package/dist/esm/agent/config.js +10 -0
- package/dist/esm/agent/pipeline.js +627 -27
- package/dist/esm/agent/report.js +3 -0
- package/dist/esm/agent/runner.js +25 -6
- package/dist/esm/agent/tests.js +12 -2
- package/dist/esm/cli.js +73 -5
- package/package.json +1 -1
- package/dist/agent/cache_utils.d.ts +0 -38
- package/dist/agent/cache_utils.d.ts.map +0 -1
- package/dist/agent/cache_utils.js +0 -67
- package/dist/agent/impact-analyzer.d.ts +0 -114
- package/dist/agent/impact-analyzer.d.ts.map +0 -1
- package/dist/agent/impact-analyzer.js +0 -557
- package/dist/agent/index.d.ts +0 -21
- package/dist/agent/index.d.ts.map +0 -1
- package/dist/agent/index.js +0 -38
- package/dist/agent/model-router.d.ts +0 -57
- package/dist/agent/model-router.d.ts.map +0 -1
- package/dist/agent/model-router.js +0 -154
- package/dist/agent/report-generator.d.ts +0 -24
- package/dist/agent/report-generator.d.ts.map +0 -1
- package/dist/agent/report-generator.js +0 -250
- package/dist/agent/spec-bridge.d.ts +0 -101
- package/dist/agent/spec-bridge.d.ts.map +0 -1
- package/dist/agent/spec-bridge.js +0 -273
- package/dist/agent/spec-builder.d.ts +0 -102
- package/dist/agent/spec-builder.d.ts.map +0 -1
- package/dist/agent/spec-builder.js +0 -273
- package/dist/agent/telemetry.d.ts +0 -84
- package/dist/agent/telemetry.d.ts.map +0 -1
- package/dist/agent/telemetry.js +0 -220
- package/dist/agent/validators/selector-validator.d.ts +0 -74
- package/dist/agent/validators/selector-validator.d.ts.map +0 -1
- package/dist/agent/validators/selector-validator.js +0 -165
- package/dist/e2e-test-gen/index.d.ts +0 -51
- package/dist/e2e-test-gen/index.d.ts.map +0 -1
- package/dist/e2e-test-gen/index.js +0 -57
- package/dist/e2e-test-gen/spec_parser.d.ts +0 -142
- package/dist/e2e-test-gen/spec_parser.d.ts.map +0 -1
- package/dist/e2e-test-gen/spec_parser.js +0 -786
- package/dist/e2e-test-gen/types.d.ts +0 -185
- package/dist/e2e-test-gen/types.d.ts.map +0 -1
- package/dist/e2e-test-gen/types.js +0 -4
- package/dist/esm/agent/cache_utils.js +0 -63
- package/dist/esm/agent/impact-analyzer.js +0 -548
- package/dist/esm/agent/index.js +0 -22
- package/dist/esm/agent/model-router.js +0 -150
- package/dist/esm/agent/report-generator.js +0 -247
- package/dist/esm/agent/spec-bridge.js +0 -267
- package/dist/esm/agent/spec-builder.js +0 -267
- package/dist/esm/agent/telemetry.js +0 -216
- package/dist/esm/agent/validators/selector-validator.js +0 -160
- package/dist/esm/e2e-test-gen/index.js +0 -50
- package/dist/esm/e2e-test-gen/spec_parser.js +0 -782
- package/dist/esm/e2e-test-gen/types.js +0 -3
- package/dist/esm/plan-and-test-constants.js +0 -126
- package/dist/plan-and-test-constants.d.ts +0 -110
- package/dist/plan-and-test-constants.d.ts.map +0 -1
- package/dist/plan-and-test-constants.js +0 -132
|
@@ -4,6 +4,13 @@ import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync
|
|
|
4
4
|
import { basename, dirname, join, relative, resolve } from 'path';
|
|
5
5
|
import { spawnSync } from 'child_process';
|
|
6
6
|
import { baseNameWithoutExt, isPathWithinRoot, normalizePath, titleCase, tokenize, uniqueTokens } from './utils.js';
|
|
7
|
+
function createMcpStatus(backend, requested) {
|
|
8
|
+
return {
|
|
9
|
+
requested,
|
|
10
|
+
active: requested && (backend === 'e2e-test-gen' || backend === 'playwright-agents'),
|
|
11
|
+
backend,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
7
14
|
function hasE2eTestGenCLI(testsRoot) {
|
|
8
15
|
const cliPath = join(testsRoot, 'e2e-test-gen-cli.ts');
|
|
9
16
|
return existsSync(cliPath) ? cliPath : null;
|
|
@@ -40,6 +47,7 @@ function firstFlowFiles(flow) {
|
|
|
40
47
|
return (flow.files || []).filter(Boolean).slice(0, 5);
|
|
41
48
|
}
|
|
42
49
|
function buildNativeStrategyOrder(flow) {
|
|
50
|
+
const flowId = (flow.id || '').toLowerCase();
|
|
43
51
|
const haystack = [
|
|
44
52
|
flow.id,
|
|
45
53
|
flow.name,
|
|
@@ -48,22 +56,183 @@ function buildNativeStrategyOrder(flow) {
|
|
|
48
56
|
...(flow.keywords || []),
|
|
49
57
|
].join(' ').toLowerCase();
|
|
50
58
|
const strategies = [];
|
|
59
|
+
if (flowId.includes('search')) {
|
|
60
|
+
strategies.push('search-baseline');
|
|
61
|
+
}
|
|
62
|
+
if (flowId.includes('threads') || flowId.includes('thread')) {
|
|
63
|
+
strategies.push('thread-reply');
|
|
64
|
+
}
|
|
65
|
+
if (flowId.includes('channels.lifecycle')) {
|
|
66
|
+
strategies.push('lifecycle-channel');
|
|
67
|
+
}
|
|
68
|
+
if (flowId.includes('channels.settings')) {
|
|
69
|
+
strategies.push('channel-settings');
|
|
70
|
+
}
|
|
71
|
+
if (flowId.includes('channels.switch')) {
|
|
72
|
+
strategies.push('channel-switch');
|
|
73
|
+
}
|
|
74
|
+
if (flowId.includes('messaging.markdown')) {
|
|
75
|
+
strategies.push('markdown-post');
|
|
76
|
+
}
|
|
77
|
+
if (flowId.includes('messaging.mentions')) {
|
|
78
|
+
strategies.push('mentions-post');
|
|
79
|
+
}
|
|
80
|
+
if (flowId.includes('messaging.realtime')) {
|
|
81
|
+
strategies.push('realtime-post');
|
|
82
|
+
}
|
|
51
83
|
if (/(thread|reply|rhs|sidebar[_-]?right)/.test(haystack)) {
|
|
52
84
|
strategies.push('thread-reply');
|
|
53
85
|
}
|
|
86
|
+
if (/(create|join|leave|invite)/.test(haystack)) {
|
|
87
|
+
strategies.push('lifecycle-channel');
|
|
88
|
+
}
|
|
89
|
+
if (/(settings|preferences)/.test(haystack)) {
|
|
90
|
+
strategies.push('channel-settings');
|
|
91
|
+
}
|
|
92
|
+
if (/(switch|quick\\s*switch)/.test(haystack)) {
|
|
93
|
+
strategies.push('channel-switch');
|
|
94
|
+
}
|
|
95
|
+
if (/(markdown|format)/.test(haystack)) {
|
|
96
|
+
strategies.push('markdown-post');
|
|
97
|
+
}
|
|
98
|
+
if (/(mention|@)/.test(haystack)) {
|
|
99
|
+
strategies.push('mentions-post');
|
|
100
|
+
}
|
|
101
|
+
if (/(realtime|websocket|presence)/.test(haystack)) {
|
|
102
|
+
strategies.push('realtime-post');
|
|
103
|
+
}
|
|
104
|
+
if (/(search|find|spotlight)/.test(haystack)) {
|
|
105
|
+
strategies.push('search-baseline');
|
|
106
|
+
}
|
|
54
107
|
if (/(message|post|realtime|websocket|chat)/.test(haystack)) {
|
|
55
108
|
strategies.push('message-post');
|
|
56
109
|
}
|
|
57
110
|
if (/(channel|navigation|sidebar|switch)/.test(haystack)) {
|
|
58
111
|
strategies.push('channel-baseline');
|
|
59
112
|
}
|
|
60
|
-
if (/(search|find|spotlight)/.test(haystack)) {
|
|
61
|
-
strategies.push('search-baseline');
|
|
62
|
-
}
|
|
63
113
|
strategies.push('generic-baseline');
|
|
64
114
|
return Array.from(new Set(strategies));
|
|
65
115
|
}
|
|
66
|
-
function
|
|
116
|
+
function createDefaultApiSurfaceCatalog() {
|
|
117
|
+
return {
|
|
118
|
+
pwProps: new Set([
|
|
119
|
+
'initSetup',
|
|
120
|
+
'testBrowser',
|
|
121
|
+
'apiInitSetup',
|
|
122
|
+
'apiAdminSetup',
|
|
123
|
+
'apiCreateChannel',
|
|
124
|
+
'apiCreateUser',
|
|
125
|
+
'apiLogin',
|
|
126
|
+
]),
|
|
127
|
+
testBrowserMethods: new Set([
|
|
128
|
+
'login',
|
|
129
|
+
'openNewBrowserContext',
|
|
130
|
+
'newContext',
|
|
131
|
+
]),
|
|
132
|
+
channelsPageMembers: new Set([
|
|
133
|
+
'goto',
|
|
134
|
+
'page',
|
|
135
|
+
'postMessage',
|
|
136
|
+
'getLastPost',
|
|
137
|
+
'sidebarRight',
|
|
138
|
+
'openChannelSettings',
|
|
139
|
+
'newChannel',
|
|
140
|
+
'globalHeader',
|
|
141
|
+
'searchBox',
|
|
142
|
+
]),
|
|
143
|
+
sidebarRightMembers: new Set([
|
|
144
|
+
'openThreadForPost',
|
|
145
|
+
'postMessage',
|
|
146
|
+
'getLastPost',
|
|
147
|
+
]),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
function collectMatches(content, pattern) {
|
|
151
|
+
const out = new Set();
|
|
152
|
+
for (const match of content.matchAll(pattern)) {
|
|
153
|
+
const value = match[1];
|
|
154
|
+
if (value) {
|
|
155
|
+
out.add(value);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return out;
|
|
159
|
+
}
|
|
160
|
+
function collectApiSurfaceFromContent(content, catalog) {
|
|
161
|
+
for (const prop of collectMatches(content, /\bpw\.([A-Za-z_][A-Za-z0-9_]*)\b/g)) {
|
|
162
|
+
catalog.pwProps.add(prop);
|
|
163
|
+
}
|
|
164
|
+
for (const method of collectMatches(content, /\bpw\.testBrowser\.([A-Za-z_][A-Za-z0-9_]*)\b/g)) {
|
|
165
|
+
catalog.testBrowserMethods.add(method);
|
|
166
|
+
}
|
|
167
|
+
for (const member of collectMatches(content, /\bchannelsPage\.([A-Za-z_][A-Za-z0-9_]*)\b/g)) {
|
|
168
|
+
catalog.channelsPageMembers.add(member);
|
|
169
|
+
}
|
|
170
|
+
for (const member of collectMatches(content, /\bchannelsPage\.sidebarRight\.([A-Za-z_][A-Za-z0-9_]*)\b/g)) {
|
|
171
|
+
catalog.sidebarRightMembers.add(member);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
function buildApiSurfaceCatalog(testsRoot, seedFile) {
|
|
175
|
+
const catalog = createDefaultApiSurfaceCatalog();
|
|
176
|
+
const candidateRoots = [
|
|
177
|
+
join(testsRoot, 'specs'),
|
|
178
|
+
join(testsRoot, 'tests'),
|
|
179
|
+
];
|
|
180
|
+
const files = [];
|
|
181
|
+
for (const root of candidateRoots) {
|
|
182
|
+
if (!existsSync(root)) {
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
const stack = [root];
|
|
186
|
+
while (stack.length > 0) {
|
|
187
|
+
const current = stack.pop();
|
|
188
|
+
let entries;
|
|
189
|
+
try {
|
|
190
|
+
entries = readdirSync(current, { withFileTypes: true });
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
for (const entry of entries) {
|
|
196
|
+
const full = join(current, entry.name);
|
|
197
|
+
if (entry.isDirectory()) {
|
|
198
|
+
if (entry.name === 'node_modules' || entry.name === '.git' || entry.name === 'dist') {
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
stack.push(full);
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
if (!entry.isFile()) {
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
if (!/\.(spec|test)\.[jt]sx?$/.test(entry.name)) {
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
files.push(full);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
const uniqueFiles = Array.from(new Set(files)).slice(0, 2500);
|
|
215
|
+
for (const filePath of uniqueFiles) {
|
|
216
|
+
try {
|
|
217
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
218
|
+
collectApiSurfaceFromContent(content, catalog);
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
const absoluteSeed = join(testsRoot, seedFile);
|
|
225
|
+
if (existsSync(absoluteSeed)) {
|
|
226
|
+
try {
|
|
227
|
+
collectApiSurfaceFromContent(readFileSync(absoluteSeed, 'utf-8'), catalog);
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
// ignore seed read failures; defaults + catalog scan still apply
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return catalog;
|
|
234
|
+
}
|
|
235
|
+
function validateGeneratedSpecContent(content, apiSurface) {
|
|
67
236
|
const issues = [];
|
|
68
237
|
if (/\btest\.describe\s*\(/.test(content)) {
|
|
69
238
|
issues.push({
|
|
@@ -89,13 +258,32 @@ function validateGeneratedSpecContent(content) {
|
|
|
89
258
|
message: 'Generated tests must use a single tag string, not a tag array.',
|
|
90
259
|
});
|
|
91
260
|
}
|
|
92
|
-
const
|
|
93
|
-
|
|
261
|
+
const hasTagOption = /\btag\s*:\s*['"][^'"]+['"]/.test(content);
|
|
262
|
+
const hasTagInTitle = /\btest(?:\.\w+)?\s*\(\s*['"][^'"]*@ai-assisted[^'"]*['"]/.test(content);
|
|
263
|
+
if (!(hasTagOption || hasTagInTitle) || !/@ai-assisted/.test(content)) {
|
|
94
264
|
issues.push({
|
|
95
265
|
code: 'missing-tag',
|
|
96
|
-
message: "Generated tests must include
|
|
266
|
+
message: "Generated tests must include '@ai-assisted' either as tag option or in test title.",
|
|
97
267
|
});
|
|
98
268
|
}
|
|
269
|
+
if (apiSurface) {
|
|
270
|
+
const unknownPwProps = Array.from(collectMatches(content, /\bpw\.([A-Za-z_][A-Za-z0-9_]*)\b/g)).filter((prop) => !apiSurface.pwProps.has(prop));
|
|
271
|
+
const unknownBrowserMethods = Array.from(collectMatches(content, /\bpw\.testBrowser\.([A-Za-z_][A-Za-z0-9_]*)\b/g)).filter((method) => !apiSurface.testBrowserMethods.has(method));
|
|
272
|
+
const unknownChannelMembers = Array.from(collectMatches(content, /\bchannelsPage\.([A-Za-z_][A-Za-z0-9_]*)\b/g)).filter((member) => !apiSurface.channelsPageMembers.has(member));
|
|
273
|
+
const unknownSidebarMembers = Array.from(collectMatches(content, /\bchannelsPage\.sidebarRight\.([A-Za-z_][A-Za-z0-9_]*)\b/g)).filter((member) => !apiSurface.sidebarRightMembers.has(member));
|
|
274
|
+
const unknown = [
|
|
275
|
+
...unknownPwProps.map((value) => `pw.${value}`),
|
|
276
|
+
...unknownBrowserMethods.map((value) => `pw.testBrowser.${value}`),
|
|
277
|
+
...unknownChannelMembers.map((value) => `channelsPage.${value}`),
|
|
278
|
+
...unknownSidebarMembers.map((value) => `channelsPage.sidebarRight.${value}`),
|
|
279
|
+
];
|
|
280
|
+
if (unknown.length > 0) {
|
|
281
|
+
issues.push({
|
|
282
|
+
code: 'unknown-api-surface',
|
|
283
|
+
message: `Generated test uses unknown API/page-object members: ${Array.from(new Set(unknown)).join(', ')}`,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
}
|
|
99
287
|
return issues;
|
|
100
288
|
}
|
|
101
289
|
function createNativePlaywrightSpec(flow, slug, strategy) {
|
|
@@ -126,10 +314,76 @@ function createNativePlaywrightSpec(flow, slug, strategy) {
|
|
|
126
314
|
...start,
|
|
127
315
|
` const parentMessage = \`ai-${slug}-parent-\${Date.now()}\`;`,
|
|
128
316
|
' await channelsPage.postMessage(parentMessage);',
|
|
129
|
-
' await channelsPage.
|
|
317
|
+
' const rootPost = await channelsPage.getLastPost();',
|
|
318
|
+
' await rootPost.openAThread();',
|
|
130
319
|
` const replyMessage = \`ai-${slug}-reply-\${Date.now()}\`;`,
|
|
131
320
|
' await channelsPage.sidebarRight.postMessage(replyMessage);',
|
|
132
|
-
' await
|
|
321
|
+
' const lastReply = await channelsPage.sidebarRight.getLastPost();',
|
|
322
|
+
' await expect(lastReply.container).toContainText(replyMessage);',
|
|
323
|
+
...end,
|
|
324
|
+
].join('\n');
|
|
325
|
+
}
|
|
326
|
+
if (strategy === 'lifecycle-channel') {
|
|
327
|
+
return [
|
|
328
|
+
...header,
|
|
329
|
+
...start,
|
|
330
|
+
` const channelName = \`ai-${slug}-\${Date.now().toString().slice(-6)}\`;`,
|
|
331
|
+
" await channelsPage.newChannel(channelName, 'O');",
|
|
332
|
+
' await expect(channelsPage.page).toHaveURL(new RegExp(`/channels/${channelName}$`));',
|
|
333
|
+
...end,
|
|
334
|
+
].join('\n');
|
|
335
|
+
}
|
|
336
|
+
if (strategy === 'channel-settings') {
|
|
337
|
+
return [
|
|
338
|
+
...header,
|
|
339
|
+
...start,
|
|
340
|
+
' await channelsPage.openChannelSettings();',
|
|
341
|
+
" await expect(channelsPage.page.getByRole('dialog', {name: 'Channel Settings'})).toBeVisible();",
|
|
342
|
+
" await channelsPage.page.keyboard.press('Escape');",
|
|
343
|
+
...end,
|
|
344
|
+
].join('\n');
|
|
345
|
+
}
|
|
346
|
+
if (strategy === 'channel-switch') {
|
|
347
|
+
return [
|
|
348
|
+
...header,
|
|
349
|
+
...start,
|
|
350
|
+
" await channelsPage.goto(team.name, 'off-topic');",
|
|
351
|
+
" await expect(channelsPage.page).toHaveURL(/\\/channels\\/off-topic$/);",
|
|
352
|
+
" await expect(channelsPage.page.locator('#channelHeaderTitle')).toContainText(/off-topic/i);",
|
|
353
|
+
...end,
|
|
354
|
+
].join('\n');
|
|
355
|
+
}
|
|
356
|
+
if (strategy === 'markdown-post') {
|
|
357
|
+
return [
|
|
358
|
+
...header,
|
|
359
|
+
...start,
|
|
360
|
+
` const message = '**ai-${slug}-bold** _italic_';`,
|
|
361
|
+
' await channelsPage.postMessage(message);',
|
|
362
|
+
' const lastPost = await channelsPage.getLastPost();',
|
|
363
|
+
" await expect(lastPost.container.locator('strong')).toBeVisible();",
|
|
364
|
+
...end,
|
|
365
|
+
].join('\n');
|
|
366
|
+
}
|
|
367
|
+
if (strategy === 'mentions-post') {
|
|
368
|
+
return [
|
|
369
|
+
...header,
|
|
370
|
+
...start,
|
|
371
|
+
' const mention = `@${user.username}`;',
|
|
372
|
+
' await channelsPage.postMessage(`Ping ${mention}`);',
|
|
373
|
+
' const lastPost = await channelsPage.getLastPost();',
|
|
374
|
+
' await expect(lastPost.container).toContainText(mention);',
|
|
375
|
+
...end,
|
|
376
|
+
].join('\n');
|
|
377
|
+
}
|
|
378
|
+
if (strategy === 'realtime-post') {
|
|
379
|
+
return [
|
|
380
|
+
...header,
|
|
381
|
+
...start,
|
|
382
|
+
` const message = \`ai-${slug}-realtime-\${Date.now()}\`;`,
|
|
383
|
+
' await channelsPage.postMessage(message);',
|
|
384
|
+
' const lastPost = await channelsPage.getLastPost();',
|
|
385
|
+
' await expect(lastPost.container).toContainText(message);',
|
|
386
|
+
" await expect(channelsPage.page.locator('#channel_view')).toBeVisible();",
|
|
133
387
|
...end,
|
|
134
388
|
].join('\n');
|
|
135
389
|
}
|
|
@@ -156,11 +410,12 @@ function createNativePlaywrightSpec(flow, slug, strategy) {
|
|
|
156
410
|
return [
|
|
157
411
|
...header,
|
|
158
412
|
...start,
|
|
159
|
-
` const searchTerm =
|
|
160
|
-
|
|
161
|
-
' await channelsPage.
|
|
162
|
-
|
|
163
|
-
|
|
413
|
+
` const searchTerm = \`ai-${slug}-\${Date.now().toString().slice(-6)}\`;`,
|
|
414
|
+
' await channelsPage.postMessage(searchTerm);',
|
|
415
|
+
' await channelsPage.globalHeader.openSearch();',
|
|
416
|
+
' await channelsPage.searchBox.searchInput.fill(searchTerm);',
|
|
417
|
+
" await channelsPage.page.keyboard.press('Enter');",
|
|
418
|
+
" await expect(channelsPage.page.locator('#searchContainer')).toBeVisible();",
|
|
164
419
|
...end,
|
|
165
420
|
].join('\n');
|
|
166
421
|
}
|
|
@@ -239,7 +494,7 @@ function runPlaywrightListValidation(testsRoot, testFile, pipeline, playwrightBi
|
|
|
239
494
|
detail: summary || commandResult.error || `playwright --list failed with status ${commandResult.status}`,
|
|
240
495
|
};
|
|
241
496
|
}
|
|
242
|
-
function runPackageNativeFlow(testsRoot, flow, pipeline, outputDir, testFile, playwrightBinary) {
|
|
497
|
+
function runPackageNativeFlow(testsRoot, flow, pipeline, outputDir, testFile, playwrightBinary, apiSurface) {
|
|
243
498
|
const flowId = flow.id;
|
|
244
499
|
const flowName = flow.name;
|
|
245
500
|
const existingFile = existsSync(testFile);
|
|
@@ -280,7 +535,7 @@ function runPackageNativeFlow(testsRoot, flow, pipeline, outputDir, testFile, pl
|
|
|
280
535
|
wroteNewFile = true;
|
|
281
536
|
}
|
|
282
537
|
const currentContent = candidate.write ? candidate.content : (originalContent || '');
|
|
283
|
-
const qualityIssues = validateGeneratedSpecContent(currentContent);
|
|
538
|
+
const qualityIssues = validateGeneratedSpecContent(currentContent, apiSurface);
|
|
284
539
|
if (qualityIssues.length > 0) {
|
|
285
540
|
attempts.push(`${candidate.label}: ${qualityIssues.map((issue) => issue.message).join(' ')}`);
|
|
286
541
|
if (pipeline.heal && i < candidates.length - 1) {
|
|
@@ -349,10 +604,10 @@ function runPackageNativeFlow(testsRoot, flow, pipeline, outputDir, testFile, pl
|
|
|
349
604
|
}
|
|
350
605
|
function runPackageNativePipeline(testsRoot, flows, pipeline, baseWarnings = []) {
|
|
351
606
|
const warningSet = new Set(baseWarnings);
|
|
352
|
-
|
|
353
|
-
warningSet.add('Package-native pipeline does not run Playwright MCP directly. Use follow-up heal workflows if MCP is required.');
|
|
354
|
-
}
|
|
607
|
+
const mcp = createMcpStatus('package-native', Boolean(pipeline.mcp));
|
|
355
608
|
const playwrightBinary = pipeline.heal ? resolvePlaywrightBinary(testsRoot) : null;
|
|
609
|
+
const seedFile = resolveAgentSeedSpec(testsRoot) || 'specs/seed.spec.ts';
|
|
610
|
+
const apiSurface = buildApiSurfaceCatalog(testsRoot, seedFile);
|
|
356
611
|
if (pipeline.heal && !playwrightBinary) {
|
|
357
612
|
warningSet.add('Playwright binary was not found. Heal uses static quality checks without runtime compile validation.');
|
|
358
613
|
}
|
|
@@ -360,7 +615,7 @@ function runPackageNativePipeline(testsRoot, flows, pipeline, baseWarnings = [])
|
|
|
360
615
|
const outputBase = resolve(testsRoot, pipeline.outputDir || 'specs/functional/ai-assisted');
|
|
361
616
|
if (!isPathWithinRoot(testsRoot, outputBase)) {
|
|
362
617
|
warningSet.add(`Pipeline outputDir resolves outside testsRoot and was blocked: ${pipeline.outputDir}`);
|
|
363
|
-
return { runner: 'unknown', results, warnings: Array.from(warningSet) };
|
|
618
|
+
return { runner: 'unknown', results, warnings: Array.from(warningSet), mcp: createMcpStatus('unknown', Boolean(pipeline.mcp)) };
|
|
364
619
|
}
|
|
365
620
|
for (const flow of flows) {
|
|
366
621
|
if (flow.priority !== 'P0' && flow.priority !== 'P1') {
|
|
@@ -399,22 +654,26 @@ function runPackageNativePipeline(testsRoot, flows, pipeline, baseWarnings = [])
|
|
|
399
654
|
});
|
|
400
655
|
continue;
|
|
401
656
|
}
|
|
402
|
-
results.push(runPackageNativeFlow(testsRoot, flow, pipeline, outputDir, testFile, playwrightBinary));
|
|
657
|
+
results.push(runPackageNativeFlow(testsRoot, flow, pipeline, outputDir, testFile, playwrightBinary, apiSurface));
|
|
403
658
|
}
|
|
404
|
-
return { runner: 'package-native', results, warnings: Array.from(warningSet) };
|
|
659
|
+
return { runner: 'package-native', results, warnings: Array.from(warningSet), mcp };
|
|
405
660
|
}
|
|
406
661
|
export function runTargetedSpecHeal(testsRoot, targets, pipeline) {
|
|
407
662
|
const warnings = new Set();
|
|
408
663
|
const results = [];
|
|
664
|
+
const mcp = createMcpStatus('package-native', Boolean(pipeline.mcp));
|
|
409
665
|
if (targets.length === 0) {
|
|
410
666
|
warnings.add('No targeted specs provided for heal.');
|
|
411
667
|
return {
|
|
412
668
|
runner: 'package-native',
|
|
413
669
|
results,
|
|
414
670
|
warnings: Array.from(warnings),
|
|
671
|
+
mcp,
|
|
415
672
|
};
|
|
416
673
|
}
|
|
417
674
|
const playwrightBinary = pipeline.heal ? resolvePlaywrightBinary(testsRoot) : null;
|
|
675
|
+
const seedFile = resolveAgentSeedSpec(testsRoot) || 'specs/seed.spec.ts';
|
|
676
|
+
const apiSurface = buildApiSurfaceCatalog(testsRoot, seedFile);
|
|
418
677
|
if (pipeline.heal && !playwrightBinary) {
|
|
419
678
|
warnings.add('Playwright binary was not found. Targeted heal uses static quality checks without runtime compile validation.');
|
|
420
679
|
}
|
|
@@ -466,12 +725,13 @@ export function runTargetedSpecHeal(testsRoot, targets, pipeline) {
|
|
|
466
725
|
continue;
|
|
467
726
|
}
|
|
468
727
|
const syntheticFlow = buildSyntheticFlowFromSpecTarget(relativeSpecPath, target);
|
|
469
|
-
results.push(runPackageNativeFlow(testsRoot, syntheticFlow, pipeline, normalizePath(dirname(absoluteSpecPath)), absoluteSpecPath, playwrightBinary));
|
|
728
|
+
results.push(runPackageNativeFlow(testsRoot, syntheticFlow, pipeline, normalizePath(dirname(absoluteSpecPath)), absoluteSpecPath, playwrightBinary, apiSurface));
|
|
470
729
|
}
|
|
471
730
|
return {
|
|
472
731
|
runner: 'package-native',
|
|
473
732
|
results,
|
|
474
733
|
warnings: Array.from(warnings),
|
|
734
|
+
mcp,
|
|
475
735
|
};
|
|
476
736
|
}
|
|
477
737
|
function findSpecFiles(root) {
|
|
@@ -495,17 +755,357 @@ function findDisallowedDescribeFiles(root) {
|
|
|
495
755
|
const files = findSpecFiles(root);
|
|
496
756
|
return files.filter((file) => /\btest\.describe\s*\(/.test(readFileSync(file, 'utf-8')));
|
|
497
757
|
}
|
|
758
|
+
function hasCommand(command, cwd) {
|
|
759
|
+
const result = runCommand(command, ['--version'], cwd);
|
|
760
|
+
return result.status === 0;
|
|
761
|
+
}
|
|
762
|
+
function hasPlaywrightAgentDefinitions(testsRoot) {
|
|
763
|
+
const required = [
|
|
764
|
+
'.mcp.json',
|
|
765
|
+
'.claude/agents/playwright-test-planner.md',
|
|
766
|
+
'.claude/agents/playwright-test-generator.md',
|
|
767
|
+
'.claude/agents/playwright-test-healer.md',
|
|
768
|
+
];
|
|
769
|
+
return required.every((path) => existsSync(join(testsRoot, path)));
|
|
770
|
+
}
|
|
771
|
+
function hasPlaywrightConfig(testsRoot) {
|
|
772
|
+
const candidates = [
|
|
773
|
+
'playwright.config.ts',
|
|
774
|
+
'playwright.config.js',
|
|
775
|
+
'playwright.config.mts',
|
|
776
|
+
'playwright.config.mjs',
|
|
777
|
+
'playwright.config.cts',
|
|
778
|
+
'playwright.config.cjs',
|
|
779
|
+
];
|
|
780
|
+
return candidates.some((candidate) => existsSync(join(testsRoot, candidate)));
|
|
781
|
+
}
|
|
782
|
+
function bootstrapPlaywrightAgentDefinitions(testsRoot, pipeline) {
|
|
783
|
+
const args = ['playwright', 'init-agents', '--loop=claude', '--prompts'];
|
|
784
|
+
if (pipeline.project) {
|
|
785
|
+
args.push('--project', pipeline.project);
|
|
786
|
+
}
|
|
787
|
+
return runCommand('npx', args, testsRoot);
|
|
788
|
+
}
|
|
789
|
+
function resolveAgentSeedSpec(testsRoot) {
|
|
790
|
+
const preferred = join(testsRoot, 'specs', 'seed.spec.ts');
|
|
791
|
+
const specsRoot = join(testsRoot, 'specs');
|
|
792
|
+
const specFiles = findSpecFiles(specsRoot).filter((file) => !normalizePath(file).includes('/functional/ai-assisted/'));
|
|
793
|
+
const scored = specFiles
|
|
794
|
+
.map((file) => {
|
|
795
|
+
const rel = normalizePath(relative(testsRoot, file));
|
|
796
|
+
const content = readFileSync(file, 'utf-8');
|
|
797
|
+
let score = 0;
|
|
798
|
+
if (rel.endsWith('/seed.spec.ts')) {
|
|
799
|
+
// Generated default seed from init-agents is often a placeholder; prefer real tests.
|
|
800
|
+
if (!/generate code here/i.test(content)) {
|
|
801
|
+
score += 2;
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
if (content.includes('@mattermost/playwright-lib')) {
|
|
805
|
+
score += 8;
|
|
806
|
+
}
|
|
807
|
+
if (content.includes('pw.initSetup(')) {
|
|
808
|
+
score += 6;
|
|
809
|
+
}
|
|
810
|
+
if (content.includes('testBrowser.login(')) {
|
|
811
|
+
score += 4;
|
|
812
|
+
}
|
|
813
|
+
if (content.includes('channelsPage')) {
|
|
814
|
+
score += 2;
|
|
815
|
+
}
|
|
816
|
+
if (rel.includes('/functional/channels/')) {
|
|
817
|
+
score += 1;
|
|
818
|
+
}
|
|
819
|
+
return { rel, score };
|
|
820
|
+
})
|
|
821
|
+
.sort((a, b) => b.score - a.score);
|
|
822
|
+
if (scored.length > 0 && scored[0].score > 0) {
|
|
823
|
+
return scored[0].rel;
|
|
824
|
+
}
|
|
825
|
+
if (existsSync(preferred)) {
|
|
826
|
+
return normalizePath(relative(testsRoot, preferred));
|
|
827
|
+
}
|
|
828
|
+
return null;
|
|
829
|
+
}
|
|
830
|
+
function buildPlaywrightAgentsPrompt(flow, seedFile, planFile, testFile, includeHealer) {
|
|
831
|
+
const linkedFiles = firstFlowFiles(flow).join(', ') || 'N/A';
|
|
832
|
+
const reasons = (flow.reasons || []).slice(0, 5).join(' | ') || 'N/A';
|
|
833
|
+
return [
|
|
834
|
+
'Use official Playwright Test agents (planner, generator, healer) to implement exactly one high-quality test for this flow.',
|
|
835
|
+
'',
|
|
836
|
+
`Flow ID: ${flow.id}`,
|
|
837
|
+
`Flow Name: ${flow.name}`,
|
|
838
|
+
`Priority: ${flow.priority}`,
|
|
839
|
+
`Linked files: ${linkedFiles}`,
|
|
840
|
+
`Risk reasons: ${reasons}`,
|
|
841
|
+
'',
|
|
842
|
+
'Workflow requirements:',
|
|
843
|
+
'1) Use #playwright-test-planner to explore and save a focused test plan.',
|
|
844
|
+
'2) Use #playwright-test-generator to generate one test from that plan.',
|
|
845
|
+
includeHealer
|
|
846
|
+
? '3) Use #playwright-test-healer to run and fix that generated test.'
|
|
847
|
+
: '3) Skip runtime healing and focus on producing compile-ready test code.',
|
|
848
|
+
'',
|
|
849
|
+
`Seed file: ${seedFile}`,
|
|
850
|
+
`Plan file to save: ${planFile}`,
|
|
851
|
+
`Generated test file path (must be exact): ${testFile}`,
|
|
852
|
+
'',
|
|
853
|
+
'Quality constraints (must follow):',
|
|
854
|
+
'- The generated file must contain a standalone test() and must not use test.describe or test.only.',
|
|
855
|
+
'- Do not mark the test with test.fixme unless user explicitly requests skipping.',
|
|
856
|
+
"- The generated test must include a single tag string '@ai-assisted'.",
|
|
857
|
+
'- Match fixture/import style from the seed file. Prefer existing page-object APIs over raw brittle selectors.',
|
|
858
|
+
'- Only use `pw` and page-object methods that already exist in the seed/current specs (for example, do not invent APIs like `pw.mainClient.*`).',
|
|
859
|
+
'- Keep the scenario strictly aligned to the flow and linked files, not broad unrelated flows.',
|
|
860
|
+
'',
|
|
861
|
+
'At the end, return a short summary that includes the generated test file path and whether healing succeeded.',
|
|
862
|
+
].join('\n');
|
|
863
|
+
}
|
|
864
|
+
function buildPlaywrightHealerPrompt(testFile, extra) {
|
|
865
|
+
const lines = [
|
|
866
|
+
'Heal this specific Playwright test file and keep edits minimal.',
|
|
867
|
+
`Target test file: ${testFile}`,
|
|
868
|
+
'Constraints:',
|
|
869
|
+
'- Do not use test.describe or test.only.',
|
|
870
|
+
"- Keep a single tag string '@ai-assisted'.",
|
|
871
|
+
'- Use only existing Mattermost Playwright fixture/page-object APIs; do not invent new `pw.*` clients or methods.',
|
|
872
|
+
'- Keep the test intent unchanged and focused.',
|
|
873
|
+
'',
|
|
874
|
+
'Run and fix this test until it compiles/passes, or mark test.fixme with a clear comment when behavior is truly broken.',
|
|
875
|
+
];
|
|
876
|
+
if (extra) {
|
|
877
|
+
lines.push('', `Context: ${extra}`);
|
|
878
|
+
}
|
|
879
|
+
return lines.join('\n');
|
|
880
|
+
}
|
|
881
|
+
function runPlaywrightAgentsFlow(testsRoot, flow, pipeline, outputDir, preferredTestFile, seedFile, apiSurface, playwrightBinary, allowRuntimeHeal) {
|
|
882
|
+
mkdirSync(outputDir, { recursive: true });
|
|
883
|
+
const slug = toSafeSlug(flow.id);
|
|
884
|
+
const planFile = normalizePath(relative(testsRoot, join(outputDir, `${slug}.plan.md`)));
|
|
885
|
+
const targetTestFile = normalizePath(relative(testsRoot, preferredTestFile));
|
|
886
|
+
if (pipeline.dryRun) {
|
|
887
|
+
return {
|
|
888
|
+
flowId: flow.id,
|
|
889
|
+
flowName: flow.name,
|
|
890
|
+
generatedDir: outputDir,
|
|
891
|
+
generateStatus: 'skipped',
|
|
892
|
+
healStatus: pipeline.heal ? 'skipped' : undefined,
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
const prompt = buildPlaywrightAgentsPrompt(flow, seedFile, planFile, targetTestFile, allowRuntimeHeal);
|
|
896
|
+
const runArgs = [
|
|
897
|
+
'-p',
|
|
898
|
+
'--permission-mode',
|
|
899
|
+
'bypassPermissions',
|
|
900
|
+
'--mcp-config',
|
|
901
|
+
'.mcp.json',
|
|
902
|
+
'--add-dir',
|
|
903
|
+
testsRoot,
|
|
904
|
+
'--',
|
|
905
|
+
prompt,
|
|
906
|
+
];
|
|
907
|
+
const runResult = runCommand('claude', runArgs, testsRoot);
|
|
908
|
+
if (runResult.status !== 0) {
|
|
909
|
+
return {
|
|
910
|
+
flowId: flow.id,
|
|
911
|
+
flowName: flow.name,
|
|
912
|
+
generatedDir: outputDir,
|
|
913
|
+
generateStatus: 'failed',
|
|
914
|
+
healStatus: pipeline.heal ? 'failed' : undefined,
|
|
915
|
+
error: summarizeCommandOutput(runResult.stdout, runResult.stderr) || runResult.error || 'Playwright agents run failed',
|
|
916
|
+
};
|
|
917
|
+
}
|
|
918
|
+
let actualTestFile = preferredTestFile;
|
|
919
|
+
if (!existsSync(actualTestFile)) {
|
|
920
|
+
const candidates = findSpecFiles(outputDir);
|
|
921
|
+
if (candidates.length === 1) {
|
|
922
|
+
actualTestFile = candidates[0];
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
if (!existsSync(actualTestFile)) {
|
|
926
|
+
return {
|
|
927
|
+
flowId: flow.id,
|
|
928
|
+
flowName: flow.name,
|
|
929
|
+
generatedDir: outputDir,
|
|
930
|
+
generateStatus: 'failed',
|
|
931
|
+
healStatus: pipeline.heal ? 'failed' : undefined,
|
|
932
|
+
error: `Playwright agents did not produce expected test file: ${targetTestFile}`,
|
|
933
|
+
};
|
|
934
|
+
}
|
|
935
|
+
const relativeActualTestFile = normalizePath(relative(testsRoot, actualTestFile));
|
|
936
|
+
let qualityIssues = validateGeneratedSpecContent(readFileSync(actualTestFile, 'utf-8'), apiSurface);
|
|
937
|
+
if (qualityIssues.length > 0 && allowRuntimeHeal) {
|
|
938
|
+
const healResult = runCommand('claude', [
|
|
939
|
+
'-p',
|
|
940
|
+
'--permission-mode',
|
|
941
|
+
'bypassPermissions',
|
|
942
|
+
'--agent',
|
|
943
|
+
'playwright-test-healer',
|
|
944
|
+
'--mcp-config',
|
|
945
|
+
'.mcp.json',
|
|
946
|
+
'--add-dir',
|
|
947
|
+
testsRoot,
|
|
948
|
+
'--',
|
|
949
|
+
buildPlaywrightHealerPrompt(relativeActualTestFile, qualityIssues.map((issue) => issue.message).join(' | ')),
|
|
950
|
+
], testsRoot);
|
|
951
|
+
if (healResult.status === 0 && existsSync(actualTestFile)) {
|
|
952
|
+
qualityIssues = validateGeneratedSpecContent(readFileSync(actualTestFile, 'utf-8'), apiSurface);
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
if (qualityIssues.length > 0) {
|
|
956
|
+
return {
|
|
957
|
+
flowId: flow.id,
|
|
958
|
+
flowName: flow.name,
|
|
959
|
+
generatedDir: outputDir,
|
|
960
|
+
generateStatus: 'failed',
|
|
961
|
+
healStatus: pipeline.heal ? 'failed' : undefined,
|
|
962
|
+
error: `Playwright agents produced invalid test content: ${qualityIssues.map((issue) => issue.message).join(' | ')}`,
|
|
963
|
+
};
|
|
964
|
+
}
|
|
965
|
+
if (allowRuntimeHeal) {
|
|
966
|
+
let validation = runPlaywrightListValidation(testsRoot, actualTestFile, pipeline, playwrightBinary);
|
|
967
|
+
if (validation.status === 'failed') {
|
|
968
|
+
const healResult = runCommand('claude', [
|
|
969
|
+
'-p',
|
|
970
|
+
'--permission-mode',
|
|
971
|
+
'bypassPermissions',
|
|
972
|
+
'--agent',
|
|
973
|
+
'playwright-test-healer',
|
|
974
|
+
'--mcp-config',
|
|
975
|
+
'.mcp.json',
|
|
976
|
+
'--add-dir',
|
|
977
|
+
testsRoot,
|
|
978
|
+
'--',
|
|
979
|
+
buildPlaywrightHealerPrompt(relativeActualTestFile, validation.detail || 'playwright --list failed'),
|
|
980
|
+
], testsRoot);
|
|
981
|
+
if (healResult.status === 0 && existsSync(actualTestFile)) {
|
|
982
|
+
validation = runPlaywrightListValidation(testsRoot, actualTestFile, pipeline, playwrightBinary);
|
|
983
|
+
}
|
|
984
|
+
if (validation.status === 'failed') {
|
|
985
|
+
return {
|
|
986
|
+
flowId: flow.id,
|
|
987
|
+
flowName: flow.name,
|
|
988
|
+
generatedDir: outputDir,
|
|
989
|
+
generateStatus: 'failed',
|
|
990
|
+
healStatus: 'failed',
|
|
991
|
+
error: `Playwright agents heal failed: ${validation.detail || 'playwright validation failed'}`,
|
|
992
|
+
};
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
return {
|
|
997
|
+
flowId: flow.id,
|
|
998
|
+
flowName: flow.name,
|
|
999
|
+
generatedDir: outputDir,
|
|
1000
|
+
generateStatus: 'success',
|
|
1001
|
+
healStatus: pipeline.heal ? 'success' : undefined,
|
|
1002
|
+
};
|
|
1003
|
+
}
|
|
1004
|
+
function runPlaywrightAgentsPipeline(testsRoot, flows, pipeline) {
|
|
1005
|
+
const warnings = [];
|
|
1006
|
+
const results = [];
|
|
1007
|
+
if (!hasCommand('claude', testsRoot)) {
|
|
1008
|
+
warnings.push('Claude CLI is required for official Playwright planner/generator/healer execution but was not found.');
|
|
1009
|
+
return { runner: 'unknown', results, warnings, mcp: createMcpStatus('unknown', true) };
|
|
1010
|
+
}
|
|
1011
|
+
if (!hasPlaywrightConfig(testsRoot)) {
|
|
1012
|
+
warnings.push('Playwright config file not found in testsRoot; skipping official Playwright agents backend.');
|
|
1013
|
+
return { runner: 'unknown', results, warnings, mcp: createMcpStatus('unknown', true) };
|
|
1014
|
+
}
|
|
1015
|
+
if (!hasPlaywrightAgentDefinitions(testsRoot)) {
|
|
1016
|
+
const bootstrap = bootstrapPlaywrightAgentDefinitions(testsRoot, pipeline);
|
|
1017
|
+
if (bootstrap.status !== 0) {
|
|
1018
|
+
warnings.push(summarizeCommandOutput(bootstrap.stdout, bootstrap.stderr) ||
|
|
1019
|
+
bootstrap.error ||
|
|
1020
|
+
'Failed to initialize Playwright agents via `npx playwright init-agents`.');
|
|
1021
|
+
return { runner: 'unknown', results, warnings, mcp: createMcpStatus('unknown', true) };
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
if (!hasPlaywrightAgentDefinitions(testsRoot)) {
|
|
1025
|
+
warnings.push('Playwright agent definitions are missing after bootstrap.');
|
|
1026
|
+
return { runner: 'unknown', results, warnings, mcp: createMcpStatus('unknown', true) };
|
|
1027
|
+
}
|
|
1028
|
+
const seedFile = resolveAgentSeedSpec(testsRoot);
|
|
1029
|
+
if (!seedFile) {
|
|
1030
|
+
warnings.push('No seed spec file found under specs/. Playwright planner cannot be initialized.');
|
|
1031
|
+
return { runner: 'unknown', results, warnings, mcp: createMcpStatus('unknown', true) };
|
|
1032
|
+
}
|
|
1033
|
+
const allowRuntimeHeal = Boolean(pipeline.heal && pipeline.baseUrl);
|
|
1034
|
+
const playwrightBinary = allowRuntimeHeal ? resolvePlaywrightBinary(testsRoot) : null;
|
|
1035
|
+
const apiSurface = buildApiSurfaceCatalog(testsRoot, seedFile);
|
|
1036
|
+
if (allowRuntimeHeal && !playwrightBinary) {
|
|
1037
|
+
warnings.push('Playwright binary was not found. Healer runtime validation may be limited.');
|
|
1038
|
+
}
|
|
1039
|
+
if (pipeline.heal && !allowRuntimeHeal) {
|
|
1040
|
+
warnings.push('Skipping runtime healer in official MCP mode because no --pipeline-base-url was provided.');
|
|
1041
|
+
}
|
|
1042
|
+
const outputBase = resolve(testsRoot, pipeline.outputDir || 'specs/functional/ai-assisted');
|
|
1043
|
+
if (!isPathWithinRoot(testsRoot, outputBase)) {
|
|
1044
|
+
warnings.push(`Pipeline outputDir resolves outside testsRoot and was blocked: ${pipeline.outputDir}`);
|
|
1045
|
+
return { runner: 'unknown', results, warnings, mcp: createMcpStatus('unknown', true) };
|
|
1046
|
+
}
|
|
1047
|
+
for (const flow of flows) {
|
|
1048
|
+
if (flow.priority !== 'P0' && flow.priority !== 'P1') {
|
|
1049
|
+
continue;
|
|
1050
|
+
}
|
|
1051
|
+
const slug = toSafeSlug(flow.id);
|
|
1052
|
+
const outputDir = normalizePath(join(outputBase, slug));
|
|
1053
|
+
if (!isPathWithinRoot(testsRoot, outputDir)) {
|
|
1054
|
+
results.push({
|
|
1055
|
+
flowId: flow.id,
|
|
1056
|
+
flowName: flow.name,
|
|
1057
|
+
generatedDir: outputDir,
|
|
1058
|
+
generateStatus: 'failed',
|
|
1059
|
+
error: 'output directory resolves outside testsRoot',
|
|
1060
|
+
});
|
|
1061
|
+
continue;
|
|
1062
|
+
}
|
|
1063
|
+
const testFile = normalizePath(join(outputDir, `${slug}.spec.ts`));
|
|
1064
|
+
if (!isPathWithinRoot(testsRoot, testFile)) {
|
|
1065
|
+
results.push({
|
|
1066
|
+
flowId: flow.id,
|
|
1067
|
+
flowName: flow.name,
|
|
1068
|
+
generatedDir: outputDir,
|
|
1069
|
+
generateStatus: 'failed',
|
|
1070
|
+
error: 'generated test path resolves outside testsRoot',
|
|
1071
|
+
});
|
|
1072
|
+
continue;
|
|
1073
|
+
}
|
|
1074
|
+
results.push(runPlaywrightAgentsFlow(testsRoot, flow, pipeline, outputDir, testFile, seedFile, apiSurface, playwrightBinary, allowRuntimeHeal));
|
|
1075
|
+
}
|
|
1076
|
+
return { runner: 'playwright-agents', results, warnings, mcp: createMcpStatus('playwright-agents', true) };
|
|
1077
|
+
}
|
|
498
1078
|
export function runPlaywrightPipeline(testsRoot, flows, pipeline) {
|
|
1079
|
+
const mcpFallbackWarnings = [];
|
|
1080
|
+
if (pipeline.mcp) {
|
|
1081
|
+
const agentsSummary = runPlaywrightAgentsPipeline(testsRoot, flows, pipeline);
|
|
1082
|
+
if (agentsSummary.runner !== 'unknown' || agentsSummary.results.length > 0) {
|
|
1083
|
+
return agentsSummary;
|
|
1084
|
+
}
|
|
1085
|
+
if (!pipeline.mcpAllowFallback) {
|
|
1086
|
+
const warnings = [
|
|
1087
|
+
...agentsSummary.warnings,
|
|
1088
|
+
'Official Playwright MCP mode is strict; fallback generation is disabled unless pipeline.mcpAllowFallback=true.',
|
|
1089
|
+
];
|
|
1090
|
+
return {
|
|
1091
|
+
runner: 'unknown',
|
|
1092
|
+
results: agentsSummary.results,
|
|
1093
|
+
warnings,
|
|
1094
|
+
mcp: createMcpStatus('unknown', true),
|
|
1095
|
+
};
|
|
1096
|
+
}
|
|
1097
|
+
mcpFallbackWarnings.push(...agentsSummary.warnings);
|
|
1098
|
+
}
|
|
499
1099
|
const cliPath = hasE2eTestGenCLI(testsRoot);
|
|
500
1100
|
if (!cliPath) {
|
|
501
|
-
return runPackageNativePipeline(testsRoot, flows, pipeline,
|
|
1101
|
+
return runPackageNativePipeline(testsRoot, flows, pipeline, mcpFallbackWarnings);
|
|
502
1102
|
}
|
|
503
|
-
const warnings = [];
|
|
1103
|
+
const warnings = [...mcpFallbackWarnings];
|
|
504
1104
|
const results = [];
|
|
505
1105
|
const outputBase = resolve(testsRoot, pipeline.outputDir || 'specs/functional/ai-assisted');
|
|
506
1106
|
if (!isPathWithinRoot(testsRoot, outputBase)) {
|
|
507
1107
|
warnings.push(`Pipeline outputDir resolves outside testsRoot and was blocked: ${pipeline.outputDir}`);
|
|
508
|
-
return { runner: 'unknown', results, warnings };
|
|
1108
|
+
return { runner: 'unknown', results, warnings, mcp: createMcpStatus('unknown', Boolean(pipeline.mcp)) };
|
|
509
1109
|
}
|
|
510
1110
|
for (const flow of flows) {
|
|
511
1111
|
if (flow.priority !== 'P0' && flow.priority !== 'P1') {
|
|
@@ -601,5 +1201,5 @@ export function runPlaywrightPipeline(testsRoot, flows, pipeline) {
|
|
|
601
1201
|
healStatus,
|
|
602
1202
|
});
|
|
603
1203
|
}
|
|
604
|
-
return { runner: 'e2e-test-gen', results, warnings };
|
|
1204
|
+
return { runner: 'e2e-test-gen', results, warnings, mcp: createMcpStatus('e2e-test-gen', Boolean(pipeline.mcp)) };
|
|
605
1205
|
}
|