@yasserkhanorg/e2e-agents 0.9.0 → 0.11.0
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 +112 -584
- package/dist/agent/api_catalog.d.ts +11 -0
- package/dist/agent/api_catalog.d.ts.map +1 -0
- package/dist/agent/api_catalog.js +210 -0
- package/dist/agent/llm_agents_flow.d.ts +15 -0
- package/dist/agent/llm_agents_flow.d.ts.map +1 -0
- package/dist/agent/llm_agents_flow.js +434 -0
- package/dist/agent/native_flow.d.ts +6 -0
- package/dist/agent/native_flow.d.ts.map +1 -0
- package/dist/agent/native_flow.js +179 -0
- package/dist/agent/pipeline.d.ts +2 -25
- package/dist/agent/pipeline.d.ts.map +1 -1
- package/dist/agent/pipeline.js +30 -1329
- package/dist/agent/pipeline_types.d.ts +54 -0
- package/dist/agent/pipeline_types.d.ts.map +1 -0
- package/dist/agent/pipeline_types.js +4 -0
- package/dist/agent/pipeline_utils.d.ts +12 -0
- package/dist/agent/pipeline_utils.d.ts.map +1 -0
- package/dist/agent/pipeline_utils.js +156 -0
- package/dist/agent/process_runner.d.ts +10 -0
- package/dist/agent/process_runner.d.ts.map +1 -0
- package/dist/agent/process_runner.js +92 -0
- package/dist/agent/spec_generator.d.ts +5 -0
- package/dist/agent/spec_generator.d.ts.map +1 -0
- package/dist/agent/spec_generator.js +253 -0
- package/dist/agent/validation_runner.d.ts +5 -0
- package/dist/agent/validation_runner.d.ts.map +1 -0
- package/dist/agent/validation_runner.js +77 -0
- package/dist/agentic/playwright_runner.js +1 -1
- package/dist/cli/commands/analyze.d.ts +3 -0
- package/dist/cli/commands/analyze.d.ts.map +1 -0
- package/dist/cli/commands/analyze.js +77 -0
- package/dist/cli/commands/feedback.d.ts +3 -0
- package/dist/cli/commands/feedback.d.ts.map +1 -0
- package/dist/cli/commands/feedback.js +39 -0
- package/dist/cli/commands/finalize.d.ts +3 -0
- package/dist/cli/commands/finalize.d.ts.map +1 -0
- package/dist/cli/commands/finalize.js +41 -0
- package/dist/cli/commands/generate.d.ts +4 -0
- package/dist/cli/commands/generate.d.ts.map +1 -0
- package/dist/cli/commands/generate.js +108 -0
- package/dist/cli/commands/heal.d.ts +3 -0
- package/dist/cli/commands/heal.d.ts.map +1 -0
- package/dist/cli/commands/heal.js +60 -0
- package/dist/cli/commands/impact.d.ts +4 -0
- package/dist/cli/commands/impact.d.ts.map +1 -0
- package/dist/cli/commands/impact.js +26 -0
- package/dist/cli/commands/llm_health.d.ts +2 -0
- package/dist/cli/commands/llm_health.d.ts.map +1 -0
- package/dist/cli/commands/llm_health.js +38 -0
- package/dist/cli/commands/plan.d.ts +4 -0
- package/dist/cli/commands/plan.d.ts.map +1 -0
- package/dist/cli/commands/plan.js +83 -0
- package/dist/cli/commands/traceability.d.ts +4 -0
- package/dist/cli/commands/traceability.d.ts.map +1 -0
- package/dist/cli/commands/traceability.js +77 -0
- package/dist/cli/parse_args.d.ts +6 -0
- package/dist/cli/parse_args.d.ts.map +1 -0
- package/dist/cli/parse_args.js +216 -0
- package/dist/cli/types.d.ts +70 -0
- package/dist/cli/types.d.ts.map +1 -0
- package/dist/cli/types.js +4 -0
- package/dist/cli/usage.d.ts +2 -0
- package/dist/cli/usage.d.ts.map +1 -0
- package/dist/cli/usage.js +86 -0
- package/dist/cli.js +26 -1057
- package/dist/esm/agent/api_catalog.js +199 -0
- package/dist/esm/agent/llm_agents_flow.js +421 -0
- package/dist/esm/agent/native_flow.js +175 -0
- package/dist/esm/agent/pipeline.js +8 -1307
- package/dist/esm/agent/pipeline_types.js +3 -0
- package/dist/esm/agent/pipeline_utils.js +146 -0
- package/dist/esm/agent/process_runner.js +83 -0
- package/dist/esm/agent/spec_generator.js +249 -0
- package/dist/esm/agent/validation_runner.js +73 -0
- package/dist/esm/agentic/playwright_runner.js +1 -1
- package/dist/esm/cli/commands/analyze.js +74 -0
- package/dist/esm/cli/commands/feedback.js +36 -0
- package/dist/esm/cli/commands/finalize.js +38 -0
- package/dist/esm/cli/commands/generate.js +105 -0
- package/dist/esm/cli/commands/heal.js +57 -0
- package/dist/esm/cli/commands/impact.js +23 -0
- package/dist/esm/cli/commands/llm_health.js +35 -0
- package/dist/esm/cli/commands/plan.js +80 -0
- package/dist/esm/cli/commands/traceability.js +73 -0
- package/dist/esm/cli/parse_args.js +210 -0
- package/dist/esm/cli/types.js +3 -0
- package/dist/esm/cli/usage.js +83 -0
- package/dist/esm/cli.js +20 -1051
- package/dist/esm/mcp-server.js +18 -1
- package/dist/mcp-server.d.ts.map +1 -1
- package/dist/mcp-server.js +17 -0
- package/package.json +2 -4
package/dist/agent/pipeline.js
CHANGED
|
@@ -6,920 +6,32 @@ exports.runTargetedSpecHeal = runTargetedSpecHeal;
|
|
|
6
6
|
exports.runPlaywrightPipeline = runPlaywrightPipeline;
|
|
7
7
|
const fs_1 = require("fs");
|
|
8
8
|
const path_1 = require("path");
|
|
9
|
-
const child_process_1 = require("child_process");
|
|
10
9
|
const utils_js_1 = require("./utils.js");
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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('etimedout') || errorText.includes('timed out')) {
|
|
27
|
-
return { ...result, failureCategory: 'environment', failureCode: 'mcp_timeout' };
|
|
28
|
-
}
|
|
29
|
-
if (errorText.includes('outside testsroot')) {
|
|
30
|
-
return { ...result, failureCategory: 'path-safety', failureCode: 'path_outside_tests_root' };
|
|
31
|
-
}
|
|
32
|
-
if (errorText.includes('playwright binary') || errorText.includes('not found')) {
|
|
33
|
-
return { ...result, failureCategory: 'environment', failureCode: 'dependency_missing' };
|
|
34
|
-
}
|
|
35
|
-
if (errorText.includes('compile validation')) {
|
|
36
|
-
return { ...result, failureCategory: 'validation', failureCode: 'compile_validation_failed' };
|
|
37
|
-
}
|
|
38
|
-
if (errorText.includes('runtime validation') || errorText.includes('playwright test failed')) {
|
|
39
|
-
return { ...result, failureCategory: 'runtime', failureCode: 'runtime_validation_failed' };
|
|
40
|
-
}
|
|
41
|
-
if (errorText.includes('quality checks failed') || errorText.includes('invalid test content')) {
|
|
42
|
-
return { ...result, failureCategory: 'quality', failureCode: 'quality_guard_failed' };
|
|
43
|
-
}
|
|
44
|
-
if (errorText.includes('generate failed') || errorText.includes('did not produce expected test file')) {
|
|
45
|
-
return { ...result, failureCategory: 'generation', failureCode: 'generation_failed' };
|
|
46
|
-
}
|
|
47
|
-
return { ...result, failureCategory: 'unknown', failureCode: 'unknown' };
|
|
48
|
-
}
|
|
49
|
-
function finalizePipelineSummary(summary) {
|
|
50
|
-
return {
|
|
51
|
-
...summary,
|
|
52
|
-
results: summary.results.map(classifyPipelineFailure),
|
|
53
|
-
};
|
|
54
|
-
}
|
|
10
|
+
const pipeline_utils_js_1 = require("./pipeline_utils.js");
|
|
11
|
+
const api_catalog_js_1 = require("./api_catalog.js");
|
|
12
|
+
const process_runner_js_1 = require("./process_runner.js");
|
|
13
|
+
const native_flow_js_1 = require("./native_flow.js");
|
|
14
|
+
const llm_agents_flow_js_1 = require("./llm_agents_flow.js");
|
|
55
15
|
function hasE2eTestGenCLI(testsRoot) {
|
|
56
16
|
const cliPath = (0, path_1.join)(testsRoot, 'e2e-test-gen-cli.ts');
|
|
57
17
|
return (0, fs_1.existsSync)(cliPath) ? cliPath : null;
|
|
58
18
|
}
|
|
59
|
-
function toSafeSlug(value) {
|
|
60
|
-
return value.replace(/[^a-zA-Z0-9._-]+/g, '-').replace(/^-+|-+$/g, '') || 'flow';
|
|
61
|
-
}
|
|
62
|
-
function stripSpecSuffix(value) {
|
|
63
|
-
return value.replace(/\.(spec|test)\.[^.]+$/i, '').replace(/\.[^.]+$/, '');
|
|
64
|
-
}
|
|
65
|
-
function buildSyntheticFlowFromSpecTarget(relativeSpecPath, target) {
|
|
66
|
-
const normalizedSpecPath = (0, utils_js_1.normalizePath)(relativeSpecPath);
|
|
67
|
-
const noSuffix = stripSpecSuffix(normalizedSpecPath);
|
|
68
|
-
const flowId = toSafeSlug(noSuffix.replace(/\//g, '.'));
|
|
69
|
-
const base = (0, utils_js_1.baseNameWithoutExt)(stripSpecSuffix((0, path_1.basename)(normalizedSpecPath)));
|
|
70
|
-
const flowName = (0, utils_js_1.titleCase)(base.replace(/[._-]+/g, ' ')) || 'Recovered Spec';
|
|
71
|
-
const keywords = (0, utils_js_1.uniqueTokens)((0, utils_js_1.tokenize)(noSuffix.replace(/[/.]/g, ' ')));
|
|
72
|
-
const reasons = [
|
|
73
|
-
`Playwright report marked this spec as ${target.status || 'unstable'}.`,
|
|
74
|
-
target.reason || `Auto-heal target: ${normalizedSpecPath}`,
|
|
75
|
-
];
|
|
76
|
-
return {
|
|
77
|
-
id: flowId,
|
|
78
|
-
name: flowName,
|
|
79
|
-
kind: 'flow',
|
|
80
|
-
score: target.status === 'failed' ? 12 : 9,
|
|
81
|
-
priority: target.status === 'failed' ? 'P0' : 'P1',
|
|
82
|
-
reasons,
|
|
83
|
-
keywords,
|
|
84
|
-
files: [normalizedSpecPath],
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
function firstFlowFiles(flow) {
|
|
88
|
-
return (flow.files || []).filter(Boolean).slice(0, 5);
|
|
89
|
-
}
|
|
90
|
-
function buildNativeStrategyOrder(flow) {
|
|
91
|
-
const flowId = (flow.id || '').toLowerCase();
|
|
92
|
-
const haystack = [
|
|
93
|
-
flow.id,
|
|
94
|
-
flow.name,
|
|
95
|
-
...(flow.files || []),
|
|
96
|
-
...(flow.reasons || []),
|
|
97
|
-
...(flow.keywords || []),
|
|
98
|
-
].join(' ').toLowerCase();
|
|
99
|
-
const strategies = [];
|
|
100
|
-
if (flowId.includes('search')) {
|
|
101
|
-
strategies.push('search-baseline');
|
|
102
|
-
}
|
|
103
|
-
if (flowId.includes('threads') || flowId.includes('thread')) {
|
|
104
|
-
strategies.push('thread-reply');
|
|
105
|
-
}
|
|
106
|
-
if (flowId.includes('channels.lifecycle')) {
|
|
107
|
-
strategies.push('lifecycle-channel');
|
|
108
|
-
}
|
|
109
|
-
if (flowId.includes('channels.settings')) {
|
|
110
|
-
strategies.push('channel-settings');
|
|
111
|
-
}
|
|
112
|
-
if (flowId.includes('channels.switch')) {
|
|
113
|
-
strategies.push('channel-switch');
|
|
114
|
-
}
|
|
115
|
-
if (flowId.includes('messaging.markdown')) {
|
|
116
|
-
strategies.push('markdown-post');
|
|
117
|
-
}
|
|
118
|
-
if (flowId.includes('messaging.mentions')) {
|
|
119
|
-
strategies.push('mentions-post');
|
|
120
|
-
}
|
|
121
|
-
if (flowId.includes('messaging.realtime')) {
|
|
122
|
-
strategies.push('realtime-post');
|
|
123
|
-
}
|
|
124
|
-
if (/(thread|reply|rhs|sidebar[_-]?right)/.test(haystack)) {
|
|
125
|
-
strategies.push('thread-reply');
|
|
126
|
-
}
|
|
127
|
-
if (/(create|join|leave|invite)/.test(haystack)) {
|
|
128
|
-
strategies.push('lifecycle-channel');
|
|
129
|
-
}
|
|
130
|
-
if (/(settings|preferences)/.test(haystack)) {
|
|
131
|
-
strategies.push('channel-settings');
|
|
132
|
-
}
|
|
133
|
-
if (/(switch|quick\\s*switch)/.test(haystack)) {
|
|
134
|
-
strategies.push('channel-switch');
|
|
135
|
-
}
|
|
136
|
-
if (/(markdown|format)/.test(haystack)) {
|
|
137
|
-
strategies.push('markdown-post');
|
|
138
|
-
}
|
|
139
|
-
if (/(mention|@)/.test(haystack)) {
|
|
140
|
-
strategies.push('mentions-post');
|
|
141
|
-
}
|
|
142
|
-
if (/(realtime|websocket|presence)/.test(haystack)) {
|
|
143
|
-
strategies.push('realtime-post');
|
|
144
|
-
}
|
|
145
|
-
if (/(search|find|spotlight)/.test(haystack)) {
|
|
146
|
-
strategies.push('search-baseline');
|
|
147
|
-
}
|
|
148
|
-
if (/(message|post|realtime|websocket|chat)/.test(haystack)) {
|
|
149
|
-
strategies.push('message-post');
|
|
150
|
-
}
|
|
151
|
-
if (/(channel|navigation|sidebar|switch)/.test(haystack)) {
|
|
152
|
-
strategies.push('channel-baseline');
|
|
153
|
-
}
|
|
154
|
-
strategies.push('generic-baseline');
|
|
155
|
-
return Array.from(new Set(strategies));
|
|
156
|
-
}
|
|
157
|
-
function createDefaultApiSurfaceCatalog() {
|
|
158
|
-
const pwNestedMethods = new Map();
|
|
159
|
-
pwNestedMethods.set('apiClient', new Set([
|
|
160
|
-
'createPost',
|
|
161
|
-
'createDirectChannel',
|
|
162
|
-
'createChannel',
|
|
163
|
-
'getChannels',
|
|
164
|
-
'getChannelByName',
|
|
165
|
-
'getPostsSince',
|
|
166
|
-
]));
|
|
167
|
-
return {
|
|
168
|
-
pwProps: new Set([
|
|
169
|
-
'initSetup',
|
|
170
|
-
'testBrowser',
|
|
171
|
-
'apiInitSetup',
|
|
172
|
-
'apiAdminSetup',
|
|
173
|
-
'apiCreateChannel',
|
|
174
|
-
'apiCreateUser',
|
|
175
|
-
'apiLogin',
|
|
176
|
-
'apiClient',
|
|
177
|
-
]),
|
|
178
|
-
pwNestedMethods,
|
|
179
|
-
initSetupKeys: new Set([
|
|
180
|
-
'user',
|
|
181
|
-
'team',
|
|
182
|
-
'adminClient',
|
|
183
|
-
'adminUser',
|
|
184
|
-
'adminConfig',
|
|
185
|
-
'userClient',
|
|
186
|
-
'offTopicUrl',
|
|
187
|
-
'townSquareUrl',
|
|
188
|
-
]),
|
|
189
|
-
initSetupVariableMethods: new Map(),
|
|
190
|
-
testBrowserMethods: new Set([
|
|
191
|
-
'login',
|
|
192
|
-
'openNewBrowserContext',
|
|
193
|
-
'newContext',
|
|
194
|
-
]),
|
|
195
|
-
channelsPageMembers: new Set([
|
|
196
|
-
'goto',
|
|
197
|
-
'page',
|
|
198
|
-
'postMessage',
|
|
199
|
-
'getLastPost',
|
|
200
|
-
'sidebarRight',
|
|
201
|
-
'openChannelSettings',
|
|
202
|
-
'newChannel',
|
|
203
|
-
'globalHeader',
|
|
204
|
-
'searchBox',
|
|
205
|
-
]),
|
|
206
|
-
sidebarRightMembers: new Set([
|
|
207
|
-
'openThreadForPost',
|
|
208
|
-
'postMessage',
|
|
209
|
-
'getLastPost',
|
|
210
|
-
]),
|
|
211
|
-
};
|
|
212
|
-
}
|
|
213
|
-
function collectMatches(content, pattern) {
|
|
214
|
-
const out = new Set();
|
|
215
|
-
for (const match of content.matchAll(pattern)) {
|
|
216
|
-
const value = match[1];
|
|
217
|
-
if (value) {
|
|
218
|
-
out.add(value);
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
return out;
|
|
222
|
-
}
|
|
223
|
-
function addNestedMethod(catalog, objectName, methodName) {
|
|
224
|
-
const methods = catalog.pwNestedMethods.get(objectName) || new Set();
|
|
225
|
-
methods.add(methodName);
|
|
226
|
-
catalog.pwNestedMethods.set(objectName, methods);
|
|
227
|
-
}
|
|
228
|
-
function escapeRegExp(value) {
|
|
229
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
230
|
-
}
|
|
231
|
-
function parseInitSetupBindings(content) {
|
|
232
|
-
const bindings = [];
|
|
233
|
-
for (const match of content.matchAll(/(?:const|let|var)\s*\{\s*([^}]+)\s*\}\s*=\s*await\s+pw\.initSetup\s*\(/g)) {
|
|
234
|
-
const raw = match[1];
|
|
235
|
-
if (!raw) {
|
|
236
|
-
continue;
|
|
237
|
-
}
|
|
238
|
-
for (const part of raw.split(',')) {
|
|
239
|
-
const cleaned = part.trim();
|
|
240
|
-
if (!cleaned) {
|
|
241
|
-
continue;
|
|
242
|
-
}
|
|
243
|
-
const [leftRaw, rightRaw] = cleaned.split(':');
|
|
244
|
-
const key = (leftRaw || '').trim();
|
|
245
|
-
const variableCandidate = (rightRaw || leftRaw || '').trim().split('=')[0]?.trim();
|
|
246
|
-
if (!key || !variableCandidate) {
|
|
247
|
-
continue;
|
|
248
|
-
}
|
|
249
|
-
bindings.push({ key, variable: variableCandidate });
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
return bindings;
|
|
253
|
-
}
|
|
254
|
-
function collectDestructuredInitSetupKeys(content) {
|
|
255
|
-
return new Set(parseInitSetupBindings(content).map((binding) => binding.key));
|
|
256
|
-
}
|
|
257
|
-
function addInitSetupVariableMethod(catalog, variable, methodName) {
|
|
258
|
-
const methods = catalog.initSetupVariableMethods.get(variable) || new Set();
|
|
259
|
-
methods.add(methodName);
|
|
260
|
-
catalog.initSetupVariableMethods.set(variable, methods);
|
|
261
|
-
}
|
|
262
|
-
function collectApiSurfaceFromContent(content, catalog) {
|
|
263
|
-
for (const prop of collectMatches(content, /\bpw\.([A-Za-z_][A-Za-z0-9_]*)\b/g)) {
|
|
264
|
-
catalog.pwProps.add(prop);
|
|
265
|
-
}
|
|
266
|
-
for (const match of content.matchAll(/\bpw\.([A-Za-z_][A-Za-z0-9_]*)\.([A-Za-z_][A-Za-z0-9_]*)\b/g)) {
|
|
267
|
-
const objectName = match[1];
|
|
268
|
-
const methodName = match[2];
|
|
269
|
-
if (!objectName || !methodName) {
|
|
270
|
-
continue;
|
|
271
|
-
}
|
|
272
|
-
addNestedMethod(catalog, objectName, methodName);
|
|
273
|
-
}
|
|
274
|
-
for (const method of collectMatches(content, /\bpw\.testBrowser\.([A-Za-z_][A-Za-z0-9_]*)\b/g)) {
|
|
275
|
-
catalog.testBrowserMethods.add(method);
|
|
276
|
-
}
|
|
277
|
-
for (const member of collectMatches(content, /\bchannelsPage\.([A-Za-z_][A-Za-z0-9_]*)\b/g)) {
|
|
278
|
-
catalog.channelsPageMembers.add(member);
|
|
279
|
-
}
|
|
280
|
-
for (const member of collectMatches(content, /\bchannelsPage\.sidebarRight\.([A-Za-z_][A-Za-z0-9_]*)\b/g)) {
|
|
281
|
-
catalog.sidebarRightMembers.add(member);
|
|
282
|
-
}
|
|
283
|
-
for (const binding of parseInitSetupBindings(content)) {
|
|
284
|
-
catalog.initSetupKeys.add(binding.key);
|
|
285
|
-
const methodPattern = new RegExp(`\\b${escapeRegExp(binding.variable)}\\.([A-Za-z_][A-Za-z0-9_]*)\\b`, 'g');
|
|
286
|
-
for (const method of collectMatches(content, methodPattern)) {
|
|
287
|
-
addInitSetupVariableMethod(catalog, binding.variable, method);
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
function buildApiSurfaceCatalog(testsRoot, seedFile) {
|
|
292
|
-
const catalog = createDefaultApiSurfaceCatalog();
|
|
293
|
-
const candidateRoots = [
|
|
294
|
-
(0, path_1.join)(testsRoot, 'specs'),
|
|
295
|
-
(0, path_1.join)(testsRoot, 'tests'),
|
|
296
|
-
];
|
|
297
|
-
const files = [];
|
|
298
|
-
for (const root of candidateRoots) {
|
|
299
|
-
if (!(0, fs_1.existsSync)(root)) {
|
|
300
|
-
continue;
|
|
301
|
-
}
|
|
302
|
-
const stack = [root];
|
|
303
|
-
while (stack.length > 0) {
|
|
304
|
-
const current = stack.pop();
|
|
305
|
-
let entries;
|
|
306
|
-
try {
|
|
307
|
-
entries = (0, fs_1.readdirSync)(current, { withFileTypes: true });
|
|
308
|
-
}
|
|
309
|
-
catch {
|
|
310
|
-
continue;
|
|
311
|
-
}
|
|
312
|
-
for (const entry of entries) {
|
|
313
|
-
const full = (0, path_1.join)(current, entry.name);
|
|
314
|
-
if (entry.isDirectory()) {
|
|
315
|
-
if (entry.name === 'node_modules' || entry.name === '.git' || entry.name === 'dist') {
|
|
316
|
-
continue;
|
|
317
|
-
}
|
|
318
|
-
stack.push(full);
|
|
319
|
-
continue;
|
|
320
|
-
}
|
|
321
|
-
if (!entry.isFile()) {
|
|
322
|
-
continue;
|
|
323
|
-
}
|
|
324
|
-
if (!/\.(spec|test)\.[jt]sx?$/.test(entry.name)) {
|
|
325
|
-
continue;
|
|
326
|
-
}
|
|
327
|
-
files.push(full);
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
const uniqueFiles = Array.from(new Set(files)).slice(0, 2500);
|
|
332
|
-
for (const filePath of uniqueFiles) {
|
|
333
|
-
try {
|
|
334
|
-
const content = (0, fs_1.readFileSync)(filePath, 'utf-8');
|
|
335
|
-
collectApiSurfaceFromContent(content, catalog);
|
|
336
|
-
}
|
|
337
|
-
catch {
|
|
338
|
-
continue;
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
const absoluteSeed = (0, path_1.join)(testsRoot, seedFile);
|
|
342
|
-
if ((0, fs_1.existsSync)(absoluteSeed)) {
|
|
343
|
-
try {
|
|
344
|
-
collectApiSurfaceFromContent((0, fs_1.readFileSync)(absoluteSeed, 'utf-8'), catalog);
|
|
345
|
-
}
|
|
346
|
-
catch {
|
|
347
|
-
// ignore seed read failures; defaults + catalog scan still apply
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
return catalog;
|
|
351
|
-
}
|
|
352
|
-
function validateGeneratedSpecContent(content, apiSurface) {
|
|
353
|
-
const issues = [];
|
|
354
|
-
if (/\btest\.describe\s*\(/.test(content)) {
|
|
355
|
-
issues.push({
|
|
356
|
-
code: 'disallowed-describe',
|
|
357
|
-
message: 'Generated tests must not use test.describe.',
|
|
358
|
-
});
|
|
359
|
-
}
|
|
360
|
-
if (/\btest\.only\s*\(/.test(content)) {
|
|
361
|
-
issues.push({
|
|
362
|
-
code: 'disallowed-only',
|
|
363
|
-
message: 'Generated tests must not use test.only.',
|
|
364
|
-
});
|
|
365
|
-
}
|
|
366
|
-
if (!/\btest\s*\(/.test(content)) {
|
|
367
|
-
issues.push({
|
|
368
|
-
code: 'missing-test',
|
|
369
|
-
message: 'Generated file does not include a test() declaration.',
|
|
370
|
-
});
|
|
371
|
-
}
|
|
372
|
-
if (/\btag\s*:\s*\[/.test(content)) {
|
|
373
|
-
issues.push({
|
|
374
|
-
code: 'tag-array-disallowed',
|
|
375
|
-
message: 'Generated tests must use a single tag string, not a tag array.',
|
|
376
|
-
});
|
|
377
|
-
}
|
|
378
|
-
const hasTagOption = /\btag\s*:\s*['"][^'"]+['"]/.test(content);
|
|
379
|
-
const hasTagInTitle = /\btest(?:\.\w+)?\s*\(\s*['"][^'"]*@ai-assisted[^'"]*['"]/.test(content);
|
|
380
|
-
if (!(hasTagOption || hasTagInTitle) || !/@ai-assisted/.test(content)) {
|
|
381
|
-
issues.push({
|
|
382
|
-
code: 'missing-tag',
|
|
383
|
-
message: "Generated tests must include '@ai-assisted' either as tag option or in test title.",
|
|
384
|
-
});
|
|
385
|
-
}
|
|
386
|
-
if (/\bsystemConsolePage\.toBeVisible\s*\(/.test(content)) {
|
|
387
|
-
issues.push({
|
|
388
|
-
code: 'fragile-system-console-visibility',
|
|
389
|
-
message: 'Avoid systemConsolePage.toBeVisible(); it relies on legacy backstage navigation that may be absent.',
|
|
390
|
-
});
|
|
391
|
-
}
|
|
392
|
-
const fragileSelectors = [
|
|
393
|
-
'.backstage-navbar',
|
|
394
|
-
'.admin-console__wrapper',
|
|
395
|
-
'.left-panel',
|
|
396
|
-
'.panel-card',
|
|
397
|
-
].filter((selector) => content.includes(selector));
|
|
398
|
-
if (fragileSelectors.length > 0) {
|
|
399
|
-
issues.push({
|
|
400
|
-
code: 'fragile-selector',
|
|
401
|
-
message: `Avoid brittle class selectors in generated tests: ${Array.from(new Set(fragileSelectors)).join(', ')}`,
|
|
402
|
-
});
|
|
403
|
-
}
|
|
404
|
-
if (apiSurface) {
|
|
405
|
-
const unknownPwProps = Array.from(collectMatches(content, /\bpw\.([A-Za-z_][A-Za-z0-9_]*)\b/g)).filter((prop) => !apiSurface.pwProps.has(prop));
|
|
406
|
-
const unknownBrowserMethods = Array.from(collectMatches(content, /\bpw\.testBrowser\.([A-Za-z_][A-Za-z0-9_]*)\b/g)).filter((method) => !apiSurface.testBrowserMethods.has(method));
|
|
407
|
-
const unknownNestedPwMembers = [];
|
|
408
|
-
for (const match of content.matchAll(/\bpw\.([A-Za-z_][A-Za-z0-9_]*)\.([A-Za-z_][A-Za-z0-9_]*)\b/g)) {
|
|
409
|
-
const objectName = match[1];
|
|
410
|
-
const methodName = match[2];
|
|
411
|
-
if (!objectName || !methodName || objectName === 'testBrowser') {
|
|
412
|
-
continue;
|
|
413
|
-
}
|
|
414
|
-
const knownMethods = apiSurface.pwNestedMethods.get(objectName);
|
|
415
|
-
if (!knownMethods || !knownMethods.has(methodName)) {
|
|
416
|
-
unknownNestedPwMembers.push(`pw.${objectName}.${methodName}`);
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
const unknownChannelMembers = Array.from(collectMatches(content, /\bchannelsPage\.([A-Za-z_][A-Za-z0-9_]*)\b/g)).filter((member) => !apiSurface.channelsPageMembers.has(member));
|
|
420
|
-
const unknownSidebarMembers = Array.from(collectMatches(content, /\bchannelsPage\.sidebarRight\.([A-Za-z_][A-Za-z0-9_]*)\b/g)).filter((member) => !apiSurface.sidebarRightMembers.has(member));
|
|
421
|
-
const initSetupBindings = parseInitSetupBindings(content);
|
|
422
|
-
const unknownInitSetupKeys = initSetupBindings
|
|
423
|
-
.map((binding) => binding.key)
|
|
424
|
-
.filter((key) => !apiSurface.initSetupKeys.has(key));
|
|
425
|
-
const unknownInitSetupVariableMethods = [];
|
|
426
|
-
for (const binding of initSetupBindings) {
|
|
427
|
-
const knownMethods = apiSurface.initSetupVariableMethods.get(binding.variable);
|
|
428
|
-
if (!knownMethods || knownMethods.size === 0) {
|
|
429
|
-
continue;
|
|
430
|
-
}
|
|
431
|
-
const methodPattern = new RegExp(`\\b${escapeRegExp(binding.variable)}\\.([A-Za-z_][A-Za-z0-9_]*)\\b`, 'g');
|
|
432
|
-
for (const method of collectMatches(content, methodPattern)) {
|
|
433
|
-
if (!knownMethods.has(method)) {
|
|
434
|
-
unknownInitSetupVariableMethods.push(`${binding.variable}.${method}`);
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
const unknown = [
|
|
439
|
-
...unknownPwProps.map((value) => `pw.${value}`),
|
|
440
|
-
...unknownBrowserMethods.map((value) => `pw.testBrowser.${value}`),
|
|
441
|
-
...unknownNestedPwMembers,
|
|
442
|
-
...unknownChannelMembers.map((value) => `channelsPage.${value}`),
|
|
443
|
-
...unknownSidebarMembers.map((value) => `channelsPage.sidebarRight.${value}`),
|
|
444
|
-
...unknownInitSetupKeys.map((value) => `pw.initSetup.{${value}}`),
|
|
445
|
-
...unknownInitSetupVariableMethods,
|
|
446
|
-
];
|
|
447
|
-
if (unknown.length > 0) {
|
|
448
|
-
issues.push({
|
|
449
|
-
code: 'unknown-api-surface',
|
|
450
|
-
message: `Generated test uses unknown API/page-object members: ${Array.from(new Set(unknown)).join(', ')}`,
|
|
451
|
-
});
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
return issues;
|
|
455
|
-
}
|
|
456
|
-
function createNativePlaywrightSpec(flow, slug, strategy) {
|
|
457
|
-
const linkedFiles = firstFlowFiles(flow).join(', ') || 'N/A';
|
|
458
|
-
const header = [
|
|
459
|
-
"import {test, expect} from '@mattermost/playwright-lib';",
|
|
460
|
-
'',
|
|
461
|
-
'/**',
|
|
462
|
-
` * Auto-generated by @yasserkhanorg/e2e-agents`,
|
|
463
|
-
` * Flow: ${flow.id} (${flow.name})`,
|
|
464
|
-
` * Strategy: ${strategy}`,
|
|
465
|
-
` * Linked files: ${linkedFiles}`,
|
|
466
|
-
' */',
|
|
467
|
-
];
|
|
468
|
-
const start = [
|
|
469
|
-
`test('${flow.priority}: ${flow.name} generated coverage', {tag: '@ai-assisted'}, async ({pw}) => {`,
|
|
470
|
-
' const {user, team} = await pw.initSetup();',
|
|
471
|
-
' const {channelsPage} = await pw.testBrowser.login(user);',
|
|
472
|
-
' await channelsPage.goto(team.name);',
|
|
473
|
-
];
|
|
474
|
-
const end = [
|
|
475
|
-
'});',
|
|
476
|
-
'',
|
|
477
|
-
];
|
|
478
|
-
if (strategy === 'thread-reply') {
|
|
479
|
-
return [
|
|
480
|
-
...header,
|
|
481
|
-
...start,
|
|
482
|
-
` const parentMessage = \`ai-${slug}-parent-\${Date.now()}\`;`,
|
|
483
|
-
' await channelsPage.postMessage(parentMessage);',
|
|
484
|
-
' const rootPost = await channelsPage.getLastPost();',
|
|
485
|
-
' await rootPost.openAThread();',
|
|
486
|
-
` const replyMessage = \`ai-${slug}-reply-\${Date.now()}\`;`,
|
|
487
|
-
' await channelsPage.sidebarRight.postMessage(replyMessage);',
|
|
488
|
-
' const lastReply = await channelsPage.sidebarRight.getLastPost();',
|
|
489
|
-
' await expect(lastReply.container).toContainText(replyMessage);',
|
|
490
|
-
...end,
|
|
491
|
-
].join('\n');
|
|
492
|
-
}
|
|
493
|
-
if (strategy === 'lifecycle-channel') {
|
|
494
|
-
return [
|
|
495
|
-
...header,
|
|
496
|
-
...start,
|
|
497
|
-
` const channelName = \`ai-${slug}-\${Date.now().toString().slice(-6)}\`;`,
|
|
498
|
-
" await channelsPage.newChannel(channelName, 'O');",
|
|
499
|
-
' await expect(channelsPage.page).toHaveURL(new RegExp(`/channels/${channelName}$`));',
|
|
500
|
-
...end,
|
|
501
|
-
].join('\n');
|
|
502
|
-
}
|
|
503
|
-
if (strategy === 'channel-settings') {
|
|
504
|
-
return [
|
|
505
|
-
...header,
|
|
506
|
-
...start,
|
|
507
|
-
' await channelsPage.openChannelSettings();',
|
|
508
|
-
" await expect(channelsPage.page.getByRole('dialog', {name: 'Channel Settings'})).toBeVisible();",
|
|
509
|
-
" await channelsPage.page.keyboard.press('Escape');",
|
|
510
|
-
...end,
|
|
511
|
-
].join('\n');
|
|
512
|
-
}
|
|
513
|
-
if (strategy === 'channel-switch') {
|
|
514
|
-
return [
|
|
515
|
-
...header,
|
|
516
|
-
...start,
|
|
517
|
-
" await channelsPage.goto(team.name, 'off-topic');",
|
|
518
|
-
" await expect(channelsPage.page).toHaveURL(/\\/channels\\/off-topic$/);",
|
|
519
|
-
" await expect(channelsPage.page.locator('#channelHeaderTitle')).toContainText(/off-topic/i);",
|
|
520
|
-
...end,
|
|
521
|
-
].join('\n');
|
|
522
|
-
}
|
|
523
|
-
if (strategy === 'markdown-post') {
|
|
524
|
-
return [
|
|
525
|
-
...header,
|
|
526
|
-
...start,
|
|
527
|
-
` const message = '**ai-${slug}-bold** _italic_';`,
|
|
528
|
-
' await channelsPage.postMessage(message);',
|
|
529
|
-
' const lastPost = await channelsPage.getLastPost();',
|
|
530
|
-
" await expect(lastPost.container.locator('strong')).toBeVisible();",
|
|
531
|
-
...end,
|
|
532
|
-
].join('\n');
|
|
533
|
-
}
|
|
534
|
-
if (strategy === 'mentions-post') {
|
|
535
|
-
return [
|
|
536
|
-
...header,
|
|
537
|
-
...start,
|
|
538
|
-
' const mention = `@${user.username}`;',
|
|
539
|
-
' await channelsPage.postMessage(`Ping ${mention}`);',
|
|
540
|
-
' const lastPost = await channelsPage.getLastPost();',
|
|
541
|
-
' await expect(lastPost.container).toContainText(mention);',
|
|
542
|
-
...end,
|
|
543
|
-
].join('\n');
|
|
544
|
-
}
|
|
545
|
-
if (strategy === 'realtime-post') {
|
|
546
|
-
return [
|
|
547
|
-
...header,
|
|
548
|
-
...start,
|
|
549
|
-
` const message = \`ai-${slug}-realtime-\${Date.now()}\`;`,
|
|
550
|
-
' await channelsPage.postMessage(message);',
|
|
551
|
-
' const lastPost = await channelsPage.getLastPost();',
|
|
552
|
-
' await expect(lastPost.container).toContainText(message);',
|
|
553
|
-
" await expect(channelsPage.page.locator('#channel_view')).toBeVisible();",
|
|
554
|
-
...end,
|
|
555
|
-
].join('\n');
|
|
556
|
-
}
|
|
557
|
-
if (strategy === 'message-post') {
|
|
558
|
-
return [
|
|
559
|
-
...header,
|
|
560
|
-
...start,
|
|
561
|
-
` const message = \`ai-${slug}-message-\${Date.now()}\`;`,
|
|
562
|
-
' await channelsPage.postMessage(message);',
|
|
563
|
-
' await expect(channelsPage.getLastPost()).toContainText(message);',
|
|
564
|
-
...end,
|
|
565
|
-
].join('\n');
|
|
566
|
-
}
|
|
567
|
-
if (strategy === 'channel-baseline') {
|
|
568
|
-
return [
|
|
569
|
-
...header,
|
|
570
|
-
...start,
|
|
571
|
-
" await expect(channelsPage.page.locator('#channelHeaderTitle')).toBeVisible();",
|
|
572
|
-
" await expect(channelsPage.page.locator('#SidebarContainer')).toBeVisible();",
|
|
573
|
-
...end,
|
|
574
|
-
].join('\n');
|
|
575
|
-
}
|
|
576
|
-
if (strategy === 'search-baseline') {
|
|
577
|
-
return [
|
|
578
|
-
...header,
|
|
579
|
-
...start,
|
|
580
|
-
` const searchTerm = \`ai-${slug}-\${Date.now().toString().slice(-6)}\`;`,
|
|
581
|
-
' await channelsPage.postMessage(searchTerm);',
|
|
582
|
-
' await channelsPage.globalHeader.openSearch();',
|
|
583
|
-
' await channelsPage.searchBox.searchInput.fill(searchTerm);',
|
|
584
|
-
" await channelsPage.page.keyboard.press('Enter');",
|
|
585
|
-
" await expect(channelsPage.page.locator('#searchContainer')).toBeVisible();",
|
|
586
|
-
...end,
|
|
587
|
-
].join('\n');
|
|
588
|
-
}
|
|
589
|
-
return [
|
|
590
|
-
...header,
|
|
591
|
-
...start,
|
|
592
|
-
' await expect(channelsPage.page).toHaveURL(/\\/channels\\//);',
|
|
593
|
-
" await expect(channelsPage.page.locator('#channelHeaderTitle')).toBeVisible();",
|
|
594
|
-
...end,
|
|
595
|
-
].join('\n');
|
|
596
|
-
}
|
|
597
|
-
function resolvePlaywrightBinary(testsRoot) {
|
|
598
|
-
const unixPath = (0, path_1.join)(testsRoot, 'node_modules', '.bin', 'playwright');
|
|
599
|
-
const windowsPath = (0, path_1.join)(testsRoot, 'node_modules', '.bin', 'playwright.cmd');
|
|
600
|
-
if ((0, fs_1.existsSync)(unixPath)) {
|
|
601
|
-
return unixPath;
|
|
602
|
-
}
|
|
603
|
-
if ((0, fs_1.existsSync)(windowsPath)) {
|
|
604
|
-
return windowsPath;
|
|
605
|
-
}
|
|
606
|
-
return null;
|
|
607
|
-
}
|
|
608
|
-
function summarizeCommandOutput(stdout, stderr) {
|
|
609
|
-
const combined = [stdout, stderr].filter(Boolean).join('\n').trim();
|
|
610
|
-
if (!combined) {
|
|
611
|
-
return '';
|
|
612
|
-
}
|
|
613
|
-
const lines = combined.split('\n').slice(-20);
|
|
614
|
-
return lines.join('\n').slice(0, 2000);
|
|
615
|
-
}
|
|
616
|
-
function runCommand(command, args, cwd, timeoutMs = 60 * 60 * 1000) {
|
|
617
|
-
// When spawning `claude`, unset CLAUDECODE so nested invocations are allowed.
|
|
618
|
-
// Claude Code sets this variable to block nested sessions; child processes
|
|
619
|
-
// that spawn their own claude instance must run without it.
|
|
620
|
-
let env;
|
|
621
|
-
if (command === 'claude') {
|
|
622
|
-
const { CLAUDECODE: _, ...rest } = process.env;
|
|
623
|
-
env = rest;
|
|
624
|
-
}
|
|
625
|
-
const result = (0, child_process_1.spawnSync)(command, args, {
|
|
626
|
-
cwd,
|
|
627
|
-
encoding: 'utf-8',
|
|
628
|
-
timeout: timeoutMs,
|
|
629
|
-
stdio: 'pipe',
|
|
630
|
-
...(env ? { env } : {}),
|
|
631
|
-
});
|
|
632
|
-
return {
|
|
633
|
-
status: result.status ?? 1,
|
|
634
|
-
stdout: result.stdout || '',
|
|
635
|
-
stderr: result.stderr || '',
|
|
636
|
-
error: result.error ? result.error.message : undefined,
|
|
637
|
-
};
|
|
638
|
-
}
|
|
639
|
-
function resolveMcpCommandTimeoutMs(pipeline) {
|
|
640
|
-
const value = pipeline.mcpCommandTimeoutMs;
|
|
641
|
-
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
642
|
-
return 180000;
|
|
643
|
-
}
|
|
644
|
-
return Math.max(60000, Math.min(15 * 60 * 1000, Math.round(value)));
|
|
645
|
-
}
|
|
646
|
-
function resolveMcpRetries(pipeline) {
|
|
647
|
-
const value = pipeline.mcpRetries;
|
|
648
|
-
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
649
|
-
return 1;
|
|
650
|
-
}
|
|
651
|
-
return Math.max(0, Math.min(5, Math.round(value)));
|
|
652
|
-
}
|
|
653
|
-
function isRetryableMcpFailure(result) {
|
|
654
|
-
const haystack = [result.error || '', result.stderr || '', result.stdout || ''].join('\n').toLowerCase();
|
|
655
|
-
return haystack.includes('etimedout') ||
|
|
656
|
-
haystack.includes('timed out') ||
|
|
657
|
-
haystack.includes('econnreset') ||
|
|
658
|
-
haystack.includes('429') ||
|
|
659
|
-
haystack.includes('rate limit') ||
|
|
660
|
-
haystack.includes('temporar');
|
|
661
|
-
}
|
|
662
|
-
function runCommandWithRetries(command, args, cwd, timeoutMs, retries) {
|
|
663
|
-
let result = runCommand(command, args, cwd, timeoutMs);
|
|
664
|
-
for (let attempt = 1; attempt <= retries; attempt += 1) {
|
|
665
|
-
if (result.status === 0) {
|
|
666
|
-
return result;
|
|
667
|
-
}
|
|
668
|
-
if (!isRetryableMcpFailure(result)) {
|
|
669
|
-
return result;
|
|
670
|
-
}
|
|
671
|
-
result = runCommand(command, args, cwd, timeoutMs);
|
|
672
|
-
}
|
|
673
|
-
return result;
|
|
674
|
-
}
|
|
675
|
-
function runPlaywrightRuntimeValidation(testsRoot, testFile, pipeline, playwrightBinary) {
|
|
676
|
-
if (!playwrightBinary) {
|
|
677
|
-
return {
|
|
678
|
-
status: 'failed',
|
|
679
|
-
detail: 'Playwright binary not found; cannot execute runtime validation.',
|
|
680
|
-
};
|
|
681
|
-
}
|
|
682
|
-
const relativeSpecPath = (0, utils_js_1.normalizePath)((0, path_1.relative)(testsRoot, testFile));
|
|
683
|
-
if (relativeSpecPath.startsWith('../') || relativeSpecPath.startsWith('..\\')) {
|
|
684
|
-
return {
|
|
685
|
-
status: 'failed',
|
|
686
|
-
detail: 'Generated spec path resolved outside testsRoot during runtime validation.',
|
|
687
|
-
};
|
|
688
|
-
}
|
|
689
|
-
const args = ['test', relativeSpecPath, '--workers', '1', '--retries', '0', '--max-failures', '1', '--reporter', 'line'];
|
|
690
|
-
if (pipeline.headless === false) {
|
|
691
|
-
args.push('--headed');
|
|
692
|
-
}
|
|
693
|
-
if (pipeline.project) {
|
|
694
|
-
args.push('--project', pipeline.project);
|
|
695
|
-
}
|
|
696
|
-
const commandResult = runCommand(playwrightBinary, args, testsRoot, 10 * 60 * 1000);
|
|
697
|
-
if (commandResult.status === 0) {
|
|
698
|
-
return { status: 'passed' };
|
|
699
|
-
}
|
|
700
|
-
const summary = summarizeCommandOutput(commandResult.stdout, commandResult.stderr);
|
|
701
|
-
return {
|
|
702
|
-
status: 'failed',
|
|
703
|
-
detail: summary || commandResult.error || `playwright test failed with status ${commandResult.status}`,
|
|
704
|
-
};
|
|
705
|
-
}
|
|
706
|
-
function runPlaywrightListValidation(testsRoot, testFile, pipeline, playwrightBinary) {
|
|
707
|
-
if (!playwrightBinary) {
|
|
708
|
-
return {
|
|
709
|
-
status: 'skipped',
|
|
710
|
-
detail: 'Playwright binary not found under testsRoot/node_modules/.bin; runtime compile validation skipped.',
|
|
711
|
-
};
|
|
712
|
-
}
|
|
713
|
-
const relativeSpecPath = (0, utils_js_1.normalizePath)((0, path_1.relative)(testsRoot, testFile));
|
|
714
|
-
if (relativeSpecPath.startsWith('../') || relativeSpecPath.startsWith('..\\')) {
|
|
715
|
-
return {
|
|
716
|
-
status: 'failed',
|
|
717
|
-
detail: 'Generated spec path resolved outside testsRoot during validation.',
|
|
718
|
-
};
|
|
719
|
-
}
|
|
720
|
-
const args = ['test', '--list', relativeSpecPath];
|
|
721
|
-
if (pipeline.headless === false) {
|
|
722
|
-
args.push('--headed');
|
|
723
|
-
}
|
|
724
|
-
if (pipeline.project) {
|
|
725
|
-
args.push('--project', pipeline.project);
|
|
726
|
-
}
|
|
727
|
-
const commandResult = runCommand(playwrightBinary, args, testsRoot);
|
|
728
|
-
if (commandResult.error && /ENOENT/.test(commandResult.error)) {
|
|
729
|
-
return {
|
|
730
|
-
status: 'skipped',
|
|
731
|
-
detail: 'Playwright binary was not executable; runtime compile validation skipped.',
|
|
732
|
-
};
|
|
733
|
-
}
|
|
734
|
-
if (commandResult.status === 0) {
|
|
735
|
-
return { status: 'passed' };
|
|
736
|
-
}
|
|
737
|
-
const summary = summarizeCommandOutput(commandResult.stdout, commandResult.stderr);
|
|
738
|
-
return {
|
|
739
|
-
status: 'failed',
|
|
740
|
-
detail: summary || commandResult.error || `playwright --list failed with status ${commandResult.status}`,
|
|
741
|
-
};
|
|
742
|
-
}
|
|
743
|
-
function runPackageNativeFlow(testsRoot, flow, pipeline, outputDir, testFile, playwrightBinary, apiSurface) {
|
|
744
|
-
const flowId = flow.id;
|
|
745
|
-
const flowName = flow.name;
|
|
746
|
-
const existingFile = (0, fs_1.existsSync)(testFile);
|
|
747
|
-
const originalContent = existingFile ? (0, fs_1.readFileSync)(testFile, 'utf-8') : null;
|
|
748
|
-
if (existingFile && !pipeline.heal) {
|
|
749
|
-
return {
|
|
750
|
-
flowId,
|
|
751
|
-
flowName,
|
|
752
|
-
generatedDir: outputDir,
|
|
753
|
-
generateStatus: 'skipped',
|
|
754
|
-
};
|
|
755
|
-
}
|
|
756
|
-
const slug = toSafeSlug(flow.id);
|
|
757
|
-
const strategies = buildNativeStrategyOrder(flow);
|
|
758
|
-
const attempts = [];
|
|
759
|
-
const candidates = [];
|
|
760
|
-
if (pipeline.heal && originalContent !== null) {
|
|
761
|
-
candidates.push({
|
|
762
|
-
label: 'existing',
|
|
763
|
-
content: originalContent,
|
|
764
|
-
write: false,
|
|
765
|
-
});
|
|
766
|
-
}
|
|
767
|
-
for (const strategy of strategies) {
|
|
768
|
-
candidates.push({
|
|
769
|
-
label: strategy,
|
|
770
|
-
strategy,
|
|
771
|
-
content: createNativePlaywrightSpec(flow, slug, strategy),
|
|
772
|
-
write: true,
|
|
773
|
-
});
|
|
774
|
-
}
|
|
775
|
-
(0, fs_1.mkdirSync)(outputDir, { recursive: true });
|
|
776
|
-
let wroteNewFile = false;
|
|
777
|
-
for (let i = 0; i < candidates.length; i += 1) {
|
|
778
|
-
const candidate = candidates[i];
|
|
779
|
-
if (candidate.write) {
|
|
780
|
-
(0, fs_1.writeFileSync)(testFile, candidate.content, 'utf-8');
|
|
781
|
-
wroteNewFile = true;
|
|
782
|
-
}
|
|
783
|
-
const currentContent = candidate.write ? candidate.content : (originalContent || '');
|
|
784
|
-
const qualityIssues = validateGeneratedSpecContent(currentContent, apiSurface);
|
|
785
|
-
if (qualityIssues.length > 0) {
|
|
786
|
-
attempts.push(`${candidate.label}: ${qualityIssues.map((issue) => issue.message).join(' ')}`);
|
|
787
|
-
if (pipeline.heal && i < candidates.length - 1) {
|
|
788
|
-
continue;
|
|
789
|
-
}
|
|
790
|
-
if (originalContent !== null) {
|
|
791
|
-
(0, fs_1.writeFileSync)(testFile, originalContent, 'utf-8');
|
|
792
|
-
}
|
|
793
|
-
else if (wroteNewFile && (0, fs_1.existsSync)(testFile)) {
|
|
794
|
-
(0, fs_1.rmSync)(testFile, { force: true });
|
|
795
|
-
}
|
|
796
|
-
return {
|
|
797
|
-
flowId,
|
|
798
|
-
flowName,
|
|
799
|
-
generatedDir: outputDir,
|
|
800
|
-
generateStatus: 'failed',
|
|
801
|
-
healStatus: pipeline.heal ? 'failed' : undefined,
|
|
802
|
-
error: `Quality checks failed. Attempts: ${attempts.join(' | ')}`,
|
|
803
|
-
};
|
|
804
|
-
}
|
|
805
|
-
if (pipeline.heal) {
|
|
806
|
-
const validation = runPlaywrightListValidation(testsRoot, testFile, pipeline, playwrightBinary);
|
|
807
|
-
if (validation.status === 'failed') {
|
|
808
|
-
attempts.push(`${candidate.label}: ${validation.detail || 'playwright validation failed'}`);
|
|
809
|
-
if (i < candidates.length - 1) {
|
|
810
|
-
continue;
|
|
811
|
-
}
|
|
812
|
-
if (originalContent !== null) {
|
|
813
|
-
(0, fs_1.writeFileSync)(testFile, originalContent, 'utf-8');
|
|
814
|
-
}
|
|
815
|
-
else if (wroteNewFile && (0, fs_1.existsSync)(testFile)) {
|
|
816
|
-
(0, fs_1.rmSync)(testFile, { force: true });
|
|
817
|
-
}
|
|
818
|
-
return {
|
|
819
|
-
flowId,
|
|
820
|
-
flowName,
|
|
821
|
-
generatedDir: outputDir,
|
|
822
|
-
generateStatus: 'failed',
|
|
823
|
-
healStatus: 'failed',
|
|
824
|
-
error: `Heal validation failed. Attempts: ${attempts.join(' | ')}`,
|
|
825
|
-
};
|
|
826
|
-
}
|
|
827
|
-
}
|
|
828
|
-
return {
|
|
829
|
-
flowId,
|
|
830
|
-
flowName,
|
|
831
|
-
generatedDir: outputDir,
|
|
832
|
-
generateStatus: candidate.write ? 'success' : 'skipped',
|
|
833
|
-
healStatus: pipeline.heal ? 'success' : undefined,
|
|
834
|
-
};
|
|
835
|
-
}
|
|
836
|
-
if (originalContent !== null) {
|
|
837
|
-
(0, fs_1.writeFileSync)(testFile, originalContent, 'utf-8');
|
|
838
|
-
}
|
|
839
|
-
else if (wroteNewFile && (0, fs_1.existsSync)(testFile)) {
|
|
840
|
-
(0, fs_1.rmSync)(testFile, { force: true });
|
|
841
|
-
}
|
|
842
|
-
return {
|
|
843
|
-
flowId,
|
|
844
|
-
flowName,
|
|
845
|
-
generatedDir: outputDir,
|
|
846
|
-
generateStatus: 'failed',
|
|
847
|
-
healStatus: pipeline.heal ? 'failed' : undefined,
|
|
848
|
-
error: attempts.length > 0 ? attempts.join(' | ') : 'No generation candidates were available.',
|
|
849
|
-
};
|
|
850
|
-
}
|
|
851
|
-
function runPackageNativePipeline(testsRoot, flows, pipeline, baseWarnings = []) {
|
|
852
|
-
const warningSet = new Set(baseWarnings);
|
|
853
|
-
const mcp = createMcpStatus('package-native', Boolean(pipeline.mcp));
|
|
854
|
-
const playwrightBinary = pipeline.heal ? resolvePlaywrightBinary(testsRoot) : null;
|
|
855
|
-
const seedFile = resolveAgentSeedSpec(testsRoot) || 'specs/seed.spec.ts';
|
|
856
|
-
const apiSurface = buildApiSurfaceCatalog(testsRoot, seedFile);
|
|
857
|
-
if (pipeline.heal && !playwrightBinary) {
|
|
858
|
-
warningSet.add('Playwright binary was not found. Heal uses static quality checks without runtime compile validation.');
|
|
859
|
-
}
|
|
860
|
-
const results = [];
|
|
861
|
-
const outputBase = (0, path_1.resolve)(testsRoot, pipeline.outputDir || 'specs/functional/ai-assisted');
|
|
862
|
-
if (!(0, utils_js_1.isPathWithinRoot)(testsRoot, outputBase)) {
|
|
863
|
-
warningSet.add(`Pipeline outputDir resolves outside testsRoot and was blocked: ${pipeline.outputDir}`);
|
|
864
|
-
return { runner: 'unknown', results, warnings: Array.from(warningSet), mcp: createMcpStatus('unknown', Boolean(pipeline.mcp)) };
|
|
865
|
-
}
|
|
866
|
-
for (const flow of flows) {
|
|
867
|
-
if (flow.priority !== 'P0' && flow.priority !== 'P1') {
|
|
868
|
-
continue;
|
|
869
|
-
}
|
|
870
|
-
const slug = toSafeSlug(flow.id);
|
|
871
|
-
const outputDir = (0, utils_js_1.normalizePath)((0, path_1.join)(outputBase, slug));
|
|
872
|
-
if (!(0, utils_js_1.isPathWithinRoot)(testsRoot, outputDir)) {
|
|
873
|
-
results.push({
|
|
874
|
-
flowId: flow.id,
|
|
875
|
-
flowName: flow.name,
|
|
876
|
-
generatedDir: outputDir,
|
|
877
|
-
generateStatus: 'failed',
|
|
878
|
-
error: 'output directory resolves outside testsRoot',
|
|
879
|
-
});
|
|
880
|
-
continue;
|
|
881
|
-
}
|
|
882
|
-
if (pipeline.dryRun) {
|
|
883
|
-
results.push({
|
|
884
|
-
flowId: flow.id,
|
|
885
|
-
flowName: flow.name,
|
|
886
|
-
generatedDir: outputDir,
|
|
887
|
-
generateStatus: 'skipped',
|
|
888
|
-
healStatus: pipeline.heal ? 'skipped' : undefined,
|
|
889
|
-
});
|
|
890
|
-
continue;
|
|
891
|
-
}
|
|
892
|
-
const testFile = (0, utils_js_1.normalizePath)((0, path_1.join)(outputDir, `${slug}.spec.ts`));
|
|
893
|
-
if (!(0, utils_js_1.isPathWithinRoot)(testsRoot, testFile)) {
|
|
894
|
-
results.push({
|
|
895
|
-
flowId: flow.id,
|
|
896
|
-
flowName: flow.name,
|
|
897
|
-
generatedDir: outputDir,
|
|
898
|
-
generateStatus: 'failed',
|
|
899
|
-
error: 'generated test path resolves outside testsRoot',
|
|
900
|
-
});
|
|
901
|
-
continue;
|
|
902
|
-
}
|
|
903
|
-
results.push(runPackageNativeFlow(testsRoot, flow, pipeline, outputDir, testFile, playwrightBinary, apiSurface));
|
|
904
|
-
}
|
|
905
|
-
return { runner: 'package-native', results, warnings: Array.from(warningSet), mcp };
|
|
906
|
-
}
|
|
907
19
|
function runTargetedSpecHeal(testsRoot, targets, pipeline) {
|
|
908
20
|
const warnings = new Set();
|
|
909
21
|
const results = [];
|
|
910
|
-
const mcp = createMcpStatus('package-native', Boolean(pipeline.mcp));
|
|
22
|
+
const mcp = (0, pipeline_utils_js_1.createMcpStatus)('package-native', Boolean(pipeline.mcp));
|
|
911
23
|
if (targets.length === 0) {
|
|
912
24
|
warnings.add('No targeted specs provided for heal.');
|
|
913
|
-
return finalizePipelineSummary({
|
|
25
|
+
return (0, pipeline_utils_js_1.finalizePipelineSummary)({
|
|
914
26
|
runner: 'package-native',
|
|
915
27
|
results,
|
|
916
28
|
warnings: Array.from(warnings),
|
|
917
29
|
mcp,
|
|
918
30
|
});
|
|
919
31
|
}
|
|
920
|
-
const playwrightBinary = pipeline.heal ? resolvePlaywrightBinary(testsRoot) : null;
|
|
921
|
-
const seedFile = resolveAgentSeedSpec(testsRoot) || 'specs/seed.spec.ts';
|
|
922
|
-
const apiSurface = buildApiSurfaceCatalog(testsRoot, seedFile);
|
|
32
|
+
const playwrightBinary = pipeline.heal ? (0, process_runner_js_1.resolvePlaywrightBinary)(testsRoot) : null;
|
|
33
|
+
const seedFile = (0, llm_agents_flow_js_1.resolveAgentSeedSpec)(testsRoot) || 'specs/seed.spec.ts';
|
|
34
|
+
const apiSurface = (0, api_catalog_js_1.buildApiSurfaceCatalog)(testsRoot, seedFile);
|
|
923
35
|
if (pipeline.heal && !playwrightBinary) {
|
|
924
36
|
warnings.add('Playwright binary was not found. Targeted heal uses static quality checks without runtime compile validation.');
|
|
925
37
|
}
|
|
@@ -970,427 +82,16 @@ function runTargetedSpecHeal(testsRoot, targets, pipeline) {
|
|
|
970
82
|
});
|
|
971
83
|
continue;
|
|
972
84
|
}
|
|
973
|
-
const syntheticFlow = buildSyntheticFlowFromSpecTarget(relativeSpecPath, target);
|
|
974
|
-
results.push(runPackageNativeFlow(testsRoot, syntheticFlow, pipeline, (0, utils_js_1.normalizePath)((0, path_1.dirname)(absoluteSpecPath)), absoluteSpecPath, playwrightBinary, apiSurface));
|
|
85
|
+
const syntheticFlow = (0, pipeline_utils_js_1.buildSyntheticFlowFromSpecTarget)(relativeSpecPath, target);
|
|
86
|
+
results.push((0, native_flow_js_1.runPackageNativeFlow)(testsRoot, syntheticFlow, pipeline, (0, utils_js_1.normalizePath)((0, path_1.dirname)(absoluteSpecPath)), absoluteSpecPath, playwrightBinary, apiSurface));
|
|
975
87
|
}
|
|
976
|
-
return finalizePipelineSummary({
|
|
88
|
+
return (0, pipeline_utils_js_1.finalizePipelineSummary)({
|
|
977
89
|
runner: 'package-native',
|
|
978
90
|
results,
|
|
979
91
|
warnings: Array.from(warnings),
|
|
980
92
|
mcp,
|
|
981
93
|
});
|
|
982
94
|
}
|
|
983
|
-
function findSpecFiles(root) {
|
|
984
|
-
if (!(0, fs_1.existsSync)(root)) {
|
|
985
|
-
return [];
|
|
986
|
-
}
|
|
987
|
-
const entries = (0, fs_1.readdirSync)(root, { withFileTypes: true });
|
|
988
|
-
const files = [];
|
|
989
|
-
for (const entry of entries) {
|
|
990
|
-
const fullPath = (0, path_1.join)(root, entry.name);
|
|
991
|
-
if (entry.isDirectory()) {
|
|
992
|
-
files.push(...findSpecFiles(fullPath));
|
|
993
|
-
}
|
|
994
|
-
else if (entry.isFile() && entry.name.endsWith('.spec.ts')) {
|
|
995
|
-
files.push(fullPath);
|
|
996
|
-
}
|
|
997
|
-
}
|
|
998
|
-
return files;
|
|
999
|
-
}
|
|
1000
|
-
function findDisallowedDescribeFiles(root) {
|
|
1001
|
-
const files = findSpecFiles(root);
|
|
1002
|
-
return files.filter((file) => /\btest\.describe\s*\(/.test((0, fs_1.readFileSync)(file, 'utf-8')));
|
|
1003
|
-
}
|
|
1004
|
-
function hasCommand(command, cwd) {
|
|
1005
|
-
const result = runCommand(command, ['--version'], cwd);
|
|
1006
|
-
return result.status === 0;
|
|
1007
|
-
}
|
|
1008
|
-
function hasPlaywrightAgentDefinitions(testsRoot) {
|
|
1009
|
-
const required = [
|
|
1010
|
-
'.mcp.json',
|
|
1011
|
-
'.claude/agents/playwright-test-planner.md',
|
|
1012
|
-
'.claude/agents/playwright-test-generator.md',
|
|
1013
|
-
'.claude/agents/playwright-test-healer.md',
|
|
1014
|
-
];
|
|
1015
|
-
return required.every((path) => (0, fs_1.existsSync)((0, path_1.join)(testsRoot, path)));
|
|
1016
|
-
}
|
|
1017
|
-
function hasPlaywrightConfig(testsRoot) {
|
|
1018
|
-
const candidates = [
|
|
1019
|
-
'playwright.config.ts',
|
|
1020
|
-
'playwright.config.js',
|
|
1021
|
-
'playwright.config.mts',
|
|
1022
|
-
'playwright.config.mjs',
|
|
1023
|
-
'playwright.config.cts',
|
|
1024
|
-
'playwright.config.cjs',
|
|
1025
|
-
];
|
|
1026
|
-
return candidates.some((candidate) => (0, fs_1.existsSync)((0, path_1.join)(testsRoot, candidate)));
|
|
1027
|
-
}
|
|
1028
|
-
function bootstrapPlaywrightAgentDefinitions(testsRoot, pipeline, timeoutMs) {
|
|
1029
|
-
const args = ['playwright', 'init-agents', '--loop=claude', '--prompts'];
|
|
1030
|
-
if (pipeline.project) {
|
|
1031
|
-
args.push('--project', pipeline.project);
|
|
1032
|
-
}
|
|
1033
|
-
return runCommand('npx', args, testsRoot, timeoutMs);
|
|
1034
|
-
}
|
|
1035
|
-
function resolveAgentSeedSpec(testsRoot) {
|
|
1036
|
-
const preferred = (0, path_1.join)(testsRoot, 'specs', 'seed.spec.ts');
|
|
1037
|
-
const specsRoot = (0, path_1.join)(testsRoot, 'specs');
|
|
1038
|
-
const specFiles = findSpecFiles(specsRoot).filter((file) => !(0, utils_js_1.normalizePath)(file).includes('/functional/ai-assisted/'));
|
|
1039
|
-
const scored = specFiles
|
|
1040
|
-
.map((file) => {
|
|
1041
|
-
const rel = (0, utils_js_1.normalizePath)((0, path_1.relative)(testsRoot, file));
|
|
1042
|
-
const content = (0, fs_1.readFileSync)(file, 'utf-8');
|
|
1043
|
-
let score = 0;
|
|
1044
|
-
if (rel.endsWith('/seed.spec.ts')) {
|
|
1045
|
-
// Generated default seed from init-agents is often a placeholder; prefer real tests.
|
|
1046
|
-
if (!/generate code here/i.test(content)) {
|
|
1047
|
-
score += 2;
|
|
1048
|
-
}
|
|
1049
|
-
}
|
|
1050
|
-
if (content.includes('@mattermost/playwright-lib')) {
|
|
1051
|
-
score += 8;
|
|
1052
|
-
}
|
|
1053
|
-
if (content.includes('pw.initSetup(')) {
|
|
1054
|
-
score += 6;
|
|
1055
|
-
}
|
|
1056
|
-
if (content.includes('testBrowser.login(')) {
|
|
1057
|
-
score += 4;
|
|
1058
|
-
}
|
|
1059
|
-
if (content.includes('channelsPage')) {
|
|
1060
|
-
score += 2;
|
|
1061
|
-
}
|
|
1062
|
-
if (rel.includes('/functional/channels/')) {
|
|
1063
|
-
score += 1;
|
|
1064
|
-
}
|
|
1065
|
-
return { rel, score };
|
|
1066
|
-
})
|
|
1067
|
-
.sort((a, b) => b.score - a.score);
|
|
1068
|
-
if (scored.length > 0 && scored[0].score > 0) {
|
|
1069
|
-
return scored[0].rel;
|
|
1070
|
-
}
|
|
1071
|
-
if ((0, fs_1.existsSync)(preferred)) {
|
|
1072
|
-
return (0, utils_js_1.normalizePath)((0, path_1.relative)(testsRoot, preferred));
|
|
1073
|
-
}
|
|
1074
|
-
return null;
|
|
1075
|
-
}
|
|
1076
|
-
function buildPlaywrightAgentsPrompt(flow, seedFile, planFile, testFile, includeHealer) {
|
|
1077
|
-
const linkedFiles = firstFlowFiles(flow).join(', ') || 'N/A';
|
|
1078
|
-
const reasons = (flow.reasons || []).slice(0, 5).join(' | ') || 'N/A';
|
|
1079
|
-
return [
|
|
1080
|
-
'Use official Playwright Test agents (planner, generator, healer) to implement exactly one high-quality test for this flow.',
|
|
1081
|
-
'',
|
|
1082
|
-
`Flow ID: ${flow.id}`,
|
|
1083
|
-
`Flow Name: ${flow.name}`,
|
|
1084
|
-
`Priority: ${flow.priority}`,
|
|
1085
|
-
`Linked files: ${linkedFiles}`,
|
|
1086
|
-
`Risk reasons: ${reasons}`,
|
|
1087
|
-
'',
|
|
1088
|
-
'Workflow requirements:',
|
|
1089
|
-
'1) Use #playwright-test-planner to explore and save a focused test plan.',
|
|
1090
|
-
'2) Use #playwright-test-generator to generate one test from that plan.',
|
|
1091
|
-
includeHealer
|
|
1092
|
-
? '3) Use #playwright-test-healer to run and fix that generated test.'
|
|
1093
|
-
: '3) Skip runtime healing and focus on producing compile-ready test code.',
|
|
1094
|
-
'',
|
|
1095
|
-
`Seed file: ${seedFile}`,
|
|
1096
|
-
`Plan file to save: ${planFile}`,
|
|
1097
|
-
`Generated test file path (must be exact): ${testFile}`,
|
|
1098
|
-
'',
|
|
1099
|
-
'Quality constraints (must follow):',
|
|
1100
|
-
'- The generated file must contain a standalone test() and must not use test.describe or test.only.',
|
|
1101
|
-
'- Do not mark the test with test.fixme unless user explicitly requests skipping.',
|
|
1102
|
-
"- The generated test must include a single tag string '@ai-assisted'.",
|
|
1103
|
-
'- Match fixture/import style from the seed file. Prefer existing page-object APIs over raw brittle selectors.',
|
|
1104
|
-
'- Only use `pw` and page-object methods that already exist in the seed/current specs (for example, do not invent APIs like `pw.mainClient.*`).',
|
|
1105
|
-
'- For system-console/admin flows, avoid `systemConsolePage.toBeVisible()` and brittle class selectors (`.backstage-navbar`, `.admin-console__wrapper`, `.left-panel`, `.panel-card`).',
|
|
1106
|
-
'- Prefer stable assertions using URL patterns, test IDs, roles, labels, and established page-object methods.',
|
|
1107
|
-
'- Keep the scenario strictly aligned to the flow and linked files, not broad unrelated flows.',
|
|
1108
|
-
'',
|
|
1109
|
-
'At the end, return a short summary that includes the generated test file path and whether healing succeeded.',
|
|
1110
|
-
].join('\n');
|
|
1111
|
-
}
|
|
1112
|
-
function buildPlaywrightHealerPrompt(testFile, extra) {
|
|
1113
|
-
const lines = [
|
|
1114
|
-
'Heal this specific Playwright test file and keep edits minimal.',
|
|
1115
|
-
`Target test file: ${testFile}`,
|
|
1116
|
-
'Constraints:',
|
|
1117
|
-
'- Do not use test.describe or test.only.',
|
|
1118
|
-
"- Keep a single tag string '@ai-assisted'.",
|
|
1119
|
-
'- Use only existing Mattermost Playwright fixture/page-object APIs; do not invent new `pw.*` clients or methods.',
|
|
1120
|
-
'- Avoid `systemConsolePage.toBeVisible()` and brittle class selectors (`.backstage-navbar`, `.admin-console__wrapper`, `.left-panel`, `.panel-card`).',
|
|
1121
|
-
'- Prefer stable checks with URL/test IDs/roles/page-object methods.',
|
|
1122
|
-
'- Keep the test intent unchanged and focused.',
|
|
1123
|
-
'',
|
|
1124
|
-
'Run and fix this test until it compiles/passes, or mark test.fixme with a clear comment when behavior is truly broken.',
|
|
1125
|
-
];
|
|
1126
|
-
if (extra) {
|
|
1127
|
-
lines.push('', `Context: ${extra}`);
|
|
1128
|
-
}
|
|
1129
|
-
return lines.join('\n');
|
|
1130
|
-
}
|
|
1131
|
-
function runPlaywrightAgentsFlow(testsRoot, flow, pipeline, outputDir, preferredTestFile, seedFile, apiSurface, playwrightBinary, mcpTimeoutMs, mcpRetries) {
|
|
1132
|
-
(0, fs_1.mkdirSync)(outputDir, { recursive: true });
|
|
1133
|
-
const slug = toSafeSlug(flow.id);
|
|
1134
|
-
const planFile = (0, utils_js_1.normalizePath)((0, path_1.relative)(testsRoot, (0, path_1.join)(outputDir, `${slug}.plan.md`)));
|
|
1135
|
-
const absolutePlanFile = (0, path_1.join)(testsRoot, planFile);
|
|
1136
|
-
const targetTestFile = (0, utils_js_1.normalizePath)((0, path_1.relative)(testsRoot, preferredTestFile));
|
|
1137
|
-
const existingSpecFiles = findSpecFiles(outputDir);
|
|
1138
|
-
const existingSpecSnapshots = new Map();
|
|
1139
|
-
for (const specFile of existingSpecFiles) {
|
|
1140
|
-
try {
|
|
1141
|
-
existingSpecSnapshots.set(specFile, (0, fs_1.readFileSync)(specFile, 'utf-8'));
|
|
1142
|
-
}
|
|
1143
|
-
catch {
|
|
1144
|
-
continue;
|
|
1145
|
-
}
|
|
1146
|
-
}
|
|
1147
|
-
const originalPlanContent = (0, fs_1.existsSync)(absolutePlanFile) ? (0, fs_1.readFileSync)(absolutePlanFile, 'utf-8') : null;
|
|
1148
|
-
const restoreArtifactsOnFailure = () => {
|
|
1149
|
-
for (const currentSpecFile of findSpecFiles(outputDir)) {
|
|
1150
|
-
const originalSpecContent = existingSpecSnapshots.get(currentSpecFile);
|
|
1151
|
-
if (originalSpecContent === undefined) {
|
|
1152
|
-
(0, fs_1.rmSync)(currentSpecFile, { force: true });
|
|
1153
|
-
continue;
|
|
1154
|
-
}
|
|
1155
|
-
try {
|
|
1156
|
-
if ((0, fs_1.readFileSync)(currentSpecFile, 'utf-8') !== originalSpecContent) {
|
|
1157
|
-
(0, fs_1.writeFileSync)(currentSpecFile, originalSpecContent, 'utf-8');
|
|
1158
|
-
}
|
|
1159
|
-
}
|
|
1160
|
-
catch {
|
|
1161
|
-
// best-effort restore only
|
|
1162
|
-
}
|
|
1163
|
-
}
|
|
1164
|
-
for (const [specFile, originalSpecContent] of existingSpecSnapshots.entries()) {
|
|
1165
|
-
if (!(0, fs_1.existsSync)(specFile)) {
|
|
1166
|
-
(0, fs_1.writeFileSync)(specFile, originalSpecContent, 'utf-8');
|
|
1167
|
-
}
|
|
1168
|
-
}
|
|
1169
|
-
if (originalPlanContent === null) {
|
|
1170
|
-
(0, fs_1.rmSync)(absolutePlanFile, { force: true });
|
|
1171
|
-
}
|
|
1172
|
-
else {
|
|
1173
|
-
try {
|
|
1174
|
-
if (!(0, fs_1.existsSync)(absolutePlanFile) || (0, fs_1.readFileSync)(absolutePlanFile, 'utf-8') !== originalPlanContent) {
|
|
1175
|
-
(0, fs_1.writeFileSync)(absolutePlanFile, originalPlanContent, 'utf-8');
|
|
1176
|
-
}
|
|
1177
|
-
}
|
|
1178
|
-
catch {
|
|
1179
|
-
// best-effort restore only
|
|
1180
|
-
}
|
|
1181
|
-
}
|
|
1182
|
-
};
|
|
1183
|
-
const failFlow = (error) => {
|
|
1184
|
-
restoreArtifactsOnFailure();
|
|
1185
|
-
return {
|
|
1186
|
-
flowId: flow.id,
|
|
1187
|
-
flowName: flow.name,
|
|
1188
|
-
generatedDir: outputDir,
|
|
1189
|
-
generateStatus: 'failed',
|
|
1190
|
-
healStatus: pipeline.heal ? 'failed' : undefined,
|
|
1191
|
-
error,
|
|
1192
|
-
};
|
|
1193
|
-
};
|
|
1194
|
-
if (pipeline.dryRun) {
|
|
1195
|
-
return {
|
|
1196
|
-
flowId: flow.id,
|
|
1197
|
-
flowName: flow.name,
|
|
1198
|
-
generatedDir: outputDir,
|
|
1199
|
-
generateStatus: 'skipped',
|
|
1200
|
-
healStatus: pipeline.heal ? 'skipped' : undefined,
|
|
1201
|
-
};
|
|
1202
|
-
}
|
|
1203
|
-
const prompt = buildPlaywrightAgentsPrompt(flow, seedFile, planFile, targetTestFile, Boolean(pipeline.heal));
|
|
1204
|
-
const runArgs = [
|
|
1205
|
-
'-p',
|
|
1206
|
-
'--permission-mode',
|
|
1207
|
-
'bypassPermissions',
|
|
1208
|
-
'--setting-sources',
|
|
1209
|
-
'project,local',
|
|
1210
|
-
'--strict-mcp-config',
|
|
1211
|
-
'--mcp-config',
|
|
1212
|
-
'.mcp.json',
|
|
1213
|
-
'--add-dir',
|
|
1214
|
-
testsRoot,
|
|
1215
|
-
'--',
|
|
1216
|
-
prompt,
|
|
1217
|
-
];
|
|
1218
|
-
const runResult = runCommandWithRetries('claude', runArgs, testsRoot, mcpTimeoutMs, mcpRetries);
|
|
1219
|
-
if (runResult.status !== 0) {
|
|
1220
|
-
return failFlow(summarizeCommandOutput(runResult.stdout, runResult.stderr) || runResult.error || 'Playwright agents run failed');
|
|
1221
|
-
}
|
|
1222
|
-
let actualTestFile = preferredTestFile;
|
|
1223
|
-
if (!(0, fs_1.existsSync)(actualTestFile)) {
|
|
1224
|
-
const candidates = findSpecFiles(outputDir);
|
|
1225
|
-
if (candidates.length === 1) {
|
|
1226
|
-
actualTestFile = candidates[0];
|
|
1227
|
-
}
|
|
1228
|
-
}
|
|
1229
|
-
if (!(0, fs_1.existsSync)(actualTestFile)) {
|
|
1230
|
-
return failFlow(`Playwright agents did not produce expected test file: ${targetTestFile}`);
|
|
1231
|
-
}
|
|
1232
|
-
const relativeActualTestFile = (0, utils_js_1.normalizePath)((0, path_1.relative)(testsRoot, actualTestFile));
|
|
1233
|
-
let qualityIssues = validateGeneratedSpecContent((0, fs_1.readFileSync)(actualTestFile, 'utf-8'), apiSurface);
|
|
1234
|
-
if (qualityIssues.length > 0 && pipeline.heal) {
|
|
1235
|
-
const healResult = runCommandWithRetries('claude', [
|
|
1236
|
-
'-p',
|
|
1237
|
-
'--permission-mode',
|
|
1238
|
-
'bypassPermissions',
|
|
1239
|
-
'--setting-sources',
|
|
1240
|
-
'project,local',
|
|
1241
|
-
'--strict-mcp-config',
|
|
1242
|
-
'--agent',
|
|
1243
|
-
'playwright-test-healer',
|
|
1244
|
-
'--mcp-config',
|
|
1245
|
-
'.mcp.json',
|
|
1246
|
-
'--add-dir',
|
|
1247
|
-
testsRoot,
|
|
1248
|
-
'--',
|
|
1249
|
-
buildPlaywrightHealerPrompt(relativeActualTestFile, qualityIssues.map((issue) => issue.message).join(' | ')),
|
|
1250
|
-
], testsRoot, mcpTimeoutMs, mcpRetries);
|
|
1251
|
-
if (healResult.status === 0 && (0, fs_1.existsSync)(actualTestFile)) {
|
|
1252
|
-
qualityIssues = validateGeneratedSpecContent((0, fs_1.readFileSync)(actualTestFile, 'utf-8'), apiSurface);
|
|
1253
|
-
}
|
|
1254
|
-
}
|
|
1255
|
-
if (qualityIssues.length > 0) {
|
|
1256
|
-
return failFlow(`Playwright agents produced invalid test content: ${qualityIssues.map((issue) => issue.message).join(' | ')}`);
|
|
1257
|
-
}
|
|
1258
|
-
if (pipeline.heal) {
|
|
1259
|
-
let compileValidation = runPlaywrightListValidation(testsRoot, actualTestFile, pipeline, playwrightBinary);
|
|
1260
|
-
if (compileValidation.status === 'failed') {
|
|
1261
|
-
const healResult = runCommandWithRetries('claude', [
|
|
1262
|
-
'-p',
|
|
1263
|
-
'--permission-mode',
|
|
1264
|
-
'bypassPermissions',
|
|
1265
|
-
'--setting-sources',
|
|
1266
|
-
'project,local',
|
|
1267
|
-
'--strict-mcp-config',
|
|
1268
|
-
'--agent',
|
|
1269
|
-
'playwright-test-healer',
|
|
1270
|
-
'--mcp-config',
|
|
1271
|
-
'.mcp.json',
|
|
1272
|
-
'--add-dir',
|
|
1273
|
-
testsRoot,
|
|
1274
|
-
'--',
|
|
1275
|
-
buildPlaywrightHealerPrompt(relativeActualTestFile, compileValidation.detail || 'playwright --list failed'),
|
|
1276
|
-
], testsRoot, mcpTimeoutMs, mcpRetries);
|
|
1277
|
-
if (healResult.status === 0 && (0, fs_1.existsSync)(actualTestFile)) {
|
|
1278
|
-
compileValidation = runPlaywrightListValidation(testsRoot, actualTestFile, pipeline, playwrightBinary);
|
|
1279
|
-
}
|
|
1280
|
-
if (compileValidation.status === 'failed') {
|
|
1281
|
-
return failFlow(`Playwright agents compile validation failed: ${compileValidation.detail || 'playwright --list failed'}`);
|
|
1282
|
-
}
|
|
1283
|
-
}
|
|
1284
|
-
let runtimeValidation = runPlaywrightRuntimeValidation(testsRoot, actualTestFile, pipeline, playwrightBinary);
|
|
1285
|
-
if (runtimeValidation.status === 'failed') {
|
|
1286
|
-
const healResult = runCommandWithRetries('claude', [
|
|
1287
|
-
'-p',
|
|
1288
|
-
'--permission-mode',
|
|
1289
|
-
'bypassPermissions',
|
|
1290
|
-
'--setting-sources',
|
|
1291
|
-
'project,local',
|
|
1292
|
-
'--strict-mcp-config',
|
|
1293
|
-
'--agent',
|
|
1294
|
-
'playwright-test-healer',
|
|
1295
|
-
'--mcp-config',
|
|
1296
|
-
'.mcp.json',
|
|
1297
|
-
'--add-dir',
|
|
1298
|
-
testsRoot,
|
|
1299
|
-
'--',
|
|
1300
|
-
buildPlaywrightHealerPrompt(relativeActualTestFile, runtimeValidation.detail || 'playwright runtime failed'),
|
|
1301
|
-
], testsRoot, mcpTimeoutMs, mcpRetries);
|
|
1302
|
-
if (healResult.status === 0 && (0, fs_1.existsSync)(actualTestFile)) {
|
|
1303
|
-
runtimeValidation = runPlaywrightRuntimeValidation(testsRoot, actualTestFile, pipeline, playwrightBinary);
|
|
1304
|
-
}
|
|
1305
|
-
if (runtimeValidation.status === 'failed') {
|
|
1306
|
-
return failFlow(`Playwright agents runtime validation failed: ${runtimeValidation.detail || 'playwright test failed'}`);
|
|
1307
|
-
}
|
|
1308
|
-
}
|
|
1309
|
-
}
|
|
1310
|
-
return {
|
|
1311
|
-
flowId: flow.id,
|
|
1312
|
-
flowName: flow.name,
|
|
1313
|
-
generatedDir: outputDir,
|
|
1314
|
-
generateStatus: 'success',
|
|
1315
|
-
healStatus: pipeline.heal ? 'success' : undefined,
|
|
1316
|
-
};
|
|
1317
|
-
}
|
|
1318
|
-
function runPlaywrightAgentsPipeline(testsRoot, flows, pipeline) {
|
|
1319
|
-
const warnings = [];
|
|
1320
|
-
const results = [];
|
|
1321
|
-
const mcpTimeoutMs = resolveMcpCommandTimeoutMs(pipeline);
|
|
1322
|
-
const mcpRetries = resolveMcpRetries(pipeline);
|
|
1323
|
-
if (!hasCommand('claude', testsRoot)) {
|
|
1324
|
-
warnings.push('Claude CLI is required for official Playwright planner/generator/healer execution but was not found.');
|
|
1325
|
-
return { runner: 'unknown', results, warnings, mcp: createMcpStatus('unknown', true) };
|
|
1326
|
-
}
|
|
1327
|
-
if (!hasPlaywrightConfig(testsRoot)) {
|
|
1328
|
-
warnings.push('Playwright config file not found in testsRoot; skipping official Playwright agents backend.');
|
|
1329
|
-
return { runner: 'unknown', results, warnings, mcp: createMcpStatus('unknown', true) };
|
|
1330
|
-
}
|
|
1331
|
-
if (!hasPlaywrightAgentDefinitions(testsRoot)) {
|
|
1332
|
-
const bootstrap = bootstrapPlaywrightAgentDefinitions(testsRoot, pipeline, mcpTimeoutMs);
|
|
1333
|
-
if (bootstrap.status !== 0) {
|
|
1334
|
-
warnings.push(summarizeCommandOutput(bootstrap.stdout, bootstrap.stderr) ||
|
|
1335
|
-
bootstrap.error ||
|
|
1336
|
-
'Failed to initialize Playwright agents via `npx playwright init-agents`.');
|
|
1337
|
-
return { runner: 'unknown', results, warnings, mcp: createMcpStatus('unknown', true) };
|
|
1338
|
-
}
|
|
1339
|
-
}
|
|
1340
|
-
if (!hasPlaywrightAgentDefinitions(testsRoot)) {
|
|
1341
|
-
warnings.push('Playwright agent definitions are missing after bootstrap.');
|
|
1342
|
-
return { runner: 'unknown', results, warnings, mcp: createMcpStatus('unknown', true) };
|
|
1343
|
-
}
|
|
1344
|
-
const seedFile = resolveAgentSeedSpec(testsRoot);
|
|
1345
|
-
if (!seedFile) {
|
|
1346
|
-
warnings.push('No seed spec file found under specs/. Playwright planner cannot be initialized.');
|
|
1347
|
-
return { runner: 'unknown', results, warnings, mcp: createMcpStatus('unknown', true) };
|
|
1348
|
-
}
|
|
1349
|
-
const playwrightBinary = pipeline.heal ? resolvePlaywrightBinary(testsRoot) : null;
|
|
1350
|
-
const apiSurface = buildApiSurfaceCatalog(testsRoot, seedFile);
|
|
1351
|
-
if (pipeline.heal && !playwrightBinary) {
|
|
1352
|
-
warnings.push('Playwright binary was not found. Healer runtime validation may be limited.');
|
|
1353
|
-
}
|
|
1354
|
-
const outputBase = (0, path_1.resolve)(testsRoot, pipeline.outputDir || 'specs/functional/ai-assisted');
|
|
1355
|
-
if (!(0, utils_js_1.isPathWithinRoot)(testsRoot, outputBase)) {
|
|
1356
|
-
warnings.push(`Pipeline outputDir resolves outside testsRoot and was blocked: ${pipeline.outputDir}`);
|
|
1357
|
-
return { runner: 'unknown', results, warnings, mcp: createMcpStatus('unknown', true) };
|
|
1358
|
-
}
|
|
1359
|
-
for (const flow of flows) {
|
|
1360
|
-
if (flow.priority !== 'P0' && flow.priority !== 'P1') {
|
|
1361
|
-
continue;
|
|
1362
|
-
}
|
|
1363
|
-
const slug = toSafeSlug(flow.id);
|
|
1364
|
-
const outputDir = (0, utils_js_1.normalizePath)((0, path_1.join)(outputBase, slug));
|
|
1365
|
-
if (!(0, utils_js_1.isPathWithinRoot)(testsRoot, outputDir)) {
|
|
1366
|
-
results.push({
|
|
1367
|
-
flowId: flow.id,
|
|
1368
|
-
flowName: flow.name,
|
|
1369
|
-
generatedDir: outputDir,
|
|
1370
|
-
generateStatus: 'failed',
|
|
1371
|
-
error: 'output directory resolves outside testsRoot',
|
|
1372
|
-
});
|
|
1373
|
-
continue;
|
|
1374
|
-
}
|
|
1375
|
-
const testFile = (0, utils_js_1.normalizePath)((0, path_1.join)(outputDir, `${slug}.spec.ts`));
|
|
1376
|
-
if (!(0, utils_js_1.isPathWithinRoot)(testsRoot, testFile)) {
|
|
1377
|
-
results.push({
|
|
1378
|
-
flowId: flow.id,
|
|
1379
|
-
flowName: flow.name,
|
|
1380
|
-
generatedDir: outputDir,
|
|
1381
|
-
generateStatus: 'failed',
|
|
1382
|
-
error: 'generated test path resolves outside testsRoot',
|
|
1383
|
-
});
|
|
1384
|
-
continue;
|
|
1385
|
-
}
|
|
1386
|
-
results.push(runPlaywrightAgentsFlow(testsRoot, flow, pipeline, outputDir, testFile, seedFile, apiSurface, playwrightBinary, mcpTimeoutMs, mcpRetries));
|
|
1387
|
-
if (pipeline.mcpOnly && results[results.length - 1].generateStatus === 'failed') {
|
|
1388
|
-
warnings.push(`MCP-only mode: stopping after first failed flow (${flow.id}).`);
|
|
1389
|
-
break;
|
|
1390
|
-
}
|
|
1391
|
-
}
|
|
1392
|
-
return { runner: 'playwright-agents', results, warnings, mcp: createMcpStatus('playwright-agents', true) };
|
|
1393
|
-
}
|
|
1394
95
|
function runPlaywrightPipeline(testsRoot, flows, pipeline) {
|
|
1395
96
|
const mcpFallbackWarnings = [];
|
|
1396
97
|
// MCP-only mode requires MCP to be enabled
|
|
@@ -1399,17 +100,17 @@ function runPlaywrightPipeline(testsRoot, flows, pipeline) {
|
|
|
1399
100
|
'❌ MCP-Only Mode Error: --pipeline-mcp-only requires --pipeline-mcp flag',
|
|
1400
101
|
'Run with: npm run gen:tests -- --pipeline-mcp',
|
|
1401
102
|
];
|
|
1402
|
-
return finalizePipelineSummary({
|
|
103
|
+
return (0, pipeline_utils_js_1.finalizePipelineSummary)({
|
|
1403
104
|
runner: 'unknown',
|
|
1404
105
|
results: [],
|
|
1405
106
|
warnings,
|
|
1406
|
-
mcp: createMcpStatus('unknown', false),
|
|
107
|
+
mcp: (0, pipeline_utils_js_1.createMcpStatus)('unknown', false),
|
|
1407
108
|
});
|
|
1408
109
|
}
|
|
1409
110
|
if (pipeline.mcp) {
|
|
1410
|
-
const agentsSummary = runPlaywrightAgentsPipeline(testsRoot, flows, pipeline);
|
|
111
|
+
const agentsSummary = (0, llm_agents_flow_js_1.runPlaywrightAgentsPipeline)(testsRoot, flows, pipeline);
|
|
1411
112
|
if (agentsSummary.runner !== 'unknown' || agentsSummary.results.length > 0) {
|
|
1412
|
-
return finalizePipelineSummary(agentsSummary);
|
|
113
|
+
return (0, pipeline_utils_js_1.finalizePipelineSummary)(agentsSummary);
|
|
1413
114
|
}
|
|
1414
115
|
// Handle strict MCP-only mode
|
|
1415
116
|
if (pipeline.mcpOnly) {
|
|
@@ -1419,11 +120,11 @@ function runPlaywrightPipeline(testsRoot, flows, pipeline) {
|
|
|
1419
120
|
'Please install Claude Code CLI: brew install anthropic/tap/claude-code',
|
|
1420
121
|
'Or check that the MCP server is properly configured',
|
|
1421
122
|
];
|
|
1422
|
-
return finalizePipelineSummary({
|
|
123
|
+
return (0, pipeline_utils_js_1.finalizePipelineSummary)({
|
|
1423
124
|
runner: 'unknown',
|
|
1424
125
|
results: agentsSummary.results,
|
|
1425
126
|
warnings,
|
|
1426
|
-
mcp: createMcpStatus('unknown', true),
|
|
127
|
+
mcp: (0, pipeline_utils_js_1.createMcpStatus)('unknown', true),
|
|
1427
128
|
});
|
|
1428
129
|
}
|
|
1429
130
|
if (!pipeline.mcpAllowFallback) {
|
|
@@ -1431,29 +132,29 @@ function runPlaywrightPipeline(testsRoot, flows, pipeline) {
|
|
|
1431
132
|
...agentsSummary.warnings,
|
|
1432
133
|
'Official Playwright MCP mode is strict; fallback generation is disabled unless pipeline.mcpAllowFallback=true.',
|
|
1433
134
|
];
|
|
1434
|
-
return finalizePipelineSummary({
|
|
135
|
+
return (0, pipeline_utils_js_1.finalizePipelineSummary)({
|
|
1435
136
|
runner: 'unknown',
|
|
1436
137
|
results: agentsSummary.results,
|
|
1437
138
|
warnings,
|
|
1438
|
-
mcp: createMcpStatus('unknown', true),
|
|
139
|
+
mcp: (0, pipeline_utils_js_1.createMcpStatus)('unknown', true),
|
|
1439
140
|
});
|
|
1440
141
|
}
|
|
1441
142
|
mcpFallbackWarnings.push(...agentsSummary.warnings);
|
|
1442
143
|
}
|
|
1443
144
|
const cliPath = hasE2eTestGenCLI(testsRoot);
|
|
1444
145
|
if (!cliPath) {
|
|
1445
|
-
return finalizePipelineSummary(runPackageNativePipeline(testsRoot, flows, pipeline, mcpFallbackWarnings));
|
|
146
|
+
return (0, pipeline_utils_js_1.finalizePipelineSummary)((0, native_flow_js_1.runPackageNativePipeline)(testsRoot, flows, pipeline, mcpFallbackWarnings));
|
|
1446
147
|
}
|
|
1447
148
|
const warnings = [...mcpFallbackWarnings];
|
|
1448
149
|
const results = [];
|
|
1449
150
|
const outputBase = (0, path_1.resolve)(testsRoot, pipeline.outputDir || 'specs/functional/ai-assisted');
|
|
1450
151
|
if (!(0, utils_js_1.isPathWithinRoot)(testsRoot, outputBase)) {
|
|
1451
152
|
warnings.push(`Pipeline outputDir resolves outside testsRoot and was blocked: ${pipeline.outputDir}`);
|
|
1452
|
-
return finalizePipelineSummary({
|
|
153
|
+
return (0, pipeline_utils_js_1.finalizePipelineSummary)({
|
|
1453
154
|
runner: 'unknown',
|
|
1454
155
|
results,
|
|
1455
156
|
warnings,
|
|
1456
|
-
mcp: createMcpStatus('unknown', Boolean(pipeline.mcp)),
|
|
157
|
+
mcp: (0, pipeline_utils_js_1.createMcpStatus)('unknown', Boolean(pipeline.mcp)),
|
|
1457
158
|
});
|
|
1458
159
|
}
|
|
1459
160
|
for (const flow of flows) {
|
|
@@ -1501,14 +202,14 @@ function runPlaywrightPipeline(testsRoot, flows, pipeline) {
|
|
|
1501
202
|
if (pipeline.mcp) {
|
|
1502
203
|
generateArgs.push('--mcp');
|
|
1503
204
|
}
|
|
1504
|
-
const generateResult = runCommand('npx', generateArgs, testsRoot);
|
|
205
|
+
const generateResult = (0, process_runner_js_1.runCommand)('npx', generateArgs, testsRoot);
|
|
1505
206
|
if (generateResult.status !== 0) {
|
|
1506
207
|
results.push({
|
|
1507
208
|
flowId: flow.id,
|
|
1508
209
|
flowName: flow.name,
|
|
1509
210
|
generatedDir: outputDir,
|
|
1510
211
|
generateStatus: 'failed',
|
|
1511
|
-
error: summarizeCommandOutput(generateResult.stdout, generateResult.stderr) || generateResult.error || 'generate failed',
|
|
212
|
+
error: (0, process_runner_js_1.summarizeCommandOutput)(generateResult.stdout, generateResult.stderr) || generateResult.error || 'generate failed',
|
|
1512
213
|
});
|
|
1513
214
|
continue;
|
|
1514
215
|
}
|
|
@@ -1527,10 +228,10 @@ function runPlaywrightPipeline(testsRoot, flows, pipeline) {
|
|
|
1527
228
|
if (pipeline.mcp) {
|
|
1528
229
|
healArgs.push('--mcp');
|
|
1529
230
|
}
|
|
1530
|
-
const healResult = runCommand('npx', healArgs, testsRoot);
|
|
231
|
+
const healResult = (0, process_runner_js_1.runCommand)('npx', healArgs, testsRoot);
|
|
1531
232
|
healStatus = healResult.status === 0 ? 'success' : 'failed';
|
|
1532
233
|
}
|
|
1533
|
-
const disallowedDescribeFiles = findDisallowedDescribeFiles(outputDir);
|
|
234
|
+
const disallowedDescribeFiles = (0, llm_agents_flow_js_1.findDisallowedDescribeFiles)(outputDir);
|
|
1534
235
|
if (disallowedDescribeFiles.length > 0) {
|
|
1535
236
|
results.push({
|
|
1536
237
|
flowId: flow.id,
|
|
@@ -1550,10 +251,10 @@ function runPlaywrightPipeline(testsRoot, flows, pipeline) {
|
|
|
1550
251
|
healStatus,
|
|
1551
252
|
});
|
|
1552
253
|
}
|
|
1553
|
-
return finalizePipelineSummary({
|
|
254
|
+
return (0, pipeline_utils_js_1.finalizePipelineSummary)({
|
|
1554
255
|
runner: 'e2e-test-gen',
|
|
1555
256
|
results,
|
|
1556
257
|
warnings,
|
|
1557
|
-
mcp: createMcpStatus('e2e-test-gen', Boolean(pipeline.mcp)),
|
|
258
|
+
mcp: (0, pipeline_utils_js_1.createMcpStatus)('e2e-test-gen', Boolean(pipeline.mcp)),
|
|
1558
259
|
});
|
|
1559
260
|
}
|