@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
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
2
|
+
// See LICENSE.txt for license information.
|
|
3
|
+
import { basename } from 'path';
|
|
4
|
+
import { baseNameWithoutExt, normalizePath, titleCase, tokenize, uniqueTokens } from './utils.js';
|
|
5
|
+
export function createMcpStatus(backend, requested) {
|
|
6
|
+
return {
|
|
7
|
+
requested,
|
|
8
|
+
active: requested && (backend === 'e2e-test-gen' || backend === 'playwright-agents'),
|
|
9
|
+
backend,
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
export function classifyPipelineFailure(result) {
|
|
13
|
+
if (result.failureCategory || result.failureCode) {
|
|
14
|
+
return result;
|
|
15
|
+
}
|
|
16
|
+
if (!result.error) {
|
|
17
|
+
return result;
|
|
18
|
+
}
|
|
19
|
+
const errorText = result.error.toLowerCase();
|
|
20
|
+
if (errorText.includes('etimedout') || errorText.includes('timed out')) {
|
|
21
|
+
return { ...result, failureCategory: 'environment', failureCode: 'mcp_timeout' };
|
|
22
|
+
}
|
|
23
|
+
if (errorText.includes('outside testsroot')) {
|
|
24
|
+
return { ...result, failureCategory: 'path-safety', failureCode: 'path_outside_tests_root' };
|
|
25
|
+
}
|
|
26
|
+
if (errorText.includes('playwright binary') || errorText.includes('not found')) {
|
|
27
|
+
return { ...result, failureCategory: 'environment', failureCode: 'dependency_missing' };
|
|
28
|
+
}
|
|
29
|
+
if (errorText.includes('compile validation')) {
|
|
30
|
+
return { ...result, failureCategory: 'validation', failureCode: 'compile_validation_failed' };
|
|
31
|
+
}
|
|
32
|
+
if (errorText.includes('runtime validation') || errorText.includes('playwright test failed')) {
|
|
33
|
+
return { ...result, failureCategory: 'runtime', failureCode: 'runtime_validation_failed' };
|
|
34
|
+
}
|
|
35
|
+
if (errorText.includes('quality checks failed') || errorText.includes('invalid test content')) {
|
|
36
|
+
return { ...result, failureCategory: 'quality', failureCode: 'quality_guard_failed' };
|
|
37
|
+
}
|
|
38
|
+
if (errorText.includes('generate failed') || errorText.includes('did not produce expected test file')) {
|
|
39
|
+
return { ...result, failureCategory: 'generation', failureCode: 'generation_failed' };
|
|
40
|
+
}
|
|
41
|
+
return { ...result, failureCategory: 'unknown', failureCode: 'unknown' };
|
|
42
|
+
}
|
|
43
|
+
export function finalizePipelineSummary(summary) {
|
|
44
|
+
return {
|
|
45
|
+
...summary,
|
|
46
|
+
results: summary.results.map(classifyPipelineFailure),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
export function toSafeSlug(value) {
|
|
50
|
+
return value.replace(/[^a-zA-Z0-9._-]+/g, '-').replace(/^-+|-+$/g, '') || 'flow';
|
|
51
|
+
}
|
|
52
|
+
export function stripSpecSuffix(value) {
|
|
53
|
+
return value.replace(/\.(spec|test)\.[^.]+$/i, '').replace(/\.[^.]+$/, '');
|
|
54
|
+
}
|
|
55
|
+
export function buildSyntheticFlowFromSpecTarget(relativeSpecPath, target) {
|
|
56
|
+
const normalizedSpecPath = normalizePath(relativeSpecPath);
|
|
57
|
+
const noSuffix = stripSpecSuffix(normalizedSpecPath);
|
|
58
|
+
const flowId = toSafeSlug(noSuffix.replace(/\//g, '.'));
|
|
59
|
+
const base = baseNameWithoutExt(stripSpecSuffix(basename(normalizedSpecPath)));
|
|
60
|
+
const flowName = titleCase(base.replace(/[._-]+/g, ' ')) || 'Recovered Spec';
|
|
61
|
+
const keywords = uniqueTokens(tokenize(noSuffix.replace(/[/.]/g, ' ')));
|
|
62
|
+
const reasons = [
|
|
63
|
+
`Playwright report marked this spec as ${target.status || 'unstable'}.`,
|
|
64
|
+
target.reason || `Auto-heal target: ${normalizedSpecPath}`,
|
|
65
|
+
];
|
|
66
|
+
return {
|
|
67
|
+
id: flowId,
|
|
68
|
+
name: flowName,
|
|
69
|
+
kind: 'flow',
|
|
70
|
+
score: target.status === 'failed' ? 12 : 9,
|
|
71
|
+
priority: target.status === 'failed' ? 'P0' : 'P1',
|
|
72
|
+
reasons,
|
|
73
|
+
keywords,
|
|
74
|
+
files: [normalizedSpecPath],
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
export function firstFlowFiles(flow) {
|
|
78
|
+
return (flow.files || []).filter(Boolean).slice(0, 5);
|
|
79
|
+
}
|
|
80
|
+
export function buildNativeStrategyOrder(flow) {
|
|
81
|
+
const flowId = (flow.id || '').toLowerCase();
|
|
82
|
+
const haystack = [
|
|
83
|
+
flow.id,
|
|
84
|
+
flow.name,
|
|
85
|
+
...(flow.files || []),
|
|
86
|
+
...(flow.reasons || []),
|
|
87
|
+
...(flow.keywords || []),
|
|
88
|
+
].join(' ').toLowerCase();
|
|
89
|
+
const strategies = [];
|
|
90
|
+
if (flowId.includes('search')) {
|
|
91
|
+
strategies.push('search-baseline');
|
|
92
|
+
}
|
|
93
|
+
if (flowId.includes('threads') || flowId.includes('thread')) {
|
|
94
|
+
strategies.push('thread-reply');
|
|
95
|
+
}
|
|
96
|
+
if (flowId.includes('channels.lifecycle')) {
|
|
97
|
+
strategies.push('lifecycle-channel');
|
|
98
|
+
}
|
|
99
|
+
if (flowId.includes('channels.settings')) {
|
|
100
|
+
strategies.push('channel-settings');
|
|
101
|
+
}
|
|
102
|
+
if (flowId.includes('channels.switch')) {
|
|
103
|
+
strategies.push('channel-switch');
|
|
104
|
+
}
|
|
105
|
+
if (flowId.includes('messaging.markdown')) {
|
|
106
|
+
strategies.push('markdown-post');
|
|
107
|
+
}
|
|
108
|
+
if (flowId.includes('messaging.mentions')) {
|
|
109
|
+
strategies.push('mentions-post');
|
|
110
|
+
}
|
|
111
|
+
if (flowId.includes('messaging.realtime')) {
|
|
112
|
+
strategies.push('realtime-post');
|
|
113
|
+
}
|
|
114
|
+
if (/(thread|reply|rhs|sidebar[_-]?right)/.test(haystack)) {
|
|
115
|
+
strategies.push('thread-reply');
|
|
116
|
+
}
|
|
117
|
+
if (/(create|join|leave|invite)/.test(haystack)) {
|
|
118
|
+
strategies.push('lifecycle-channel');
|
|
119
|
+
}
|
|
120
|
+
if (/(settings|preferences)/.test(haystack)) {
|
|
121
|
+
strategies.push('channel-settings');
|
|
122
|
+
}
|
|
123
|
+
if (/(switch|quick\\s*switch)/.test(haystack)) {
|
|
124
|
+
strategies.push('channel-switch');
|
|
125
|
+
}
|
|
126
|
+
if (/(markdown|format)/.test(haystack)) {
|
|
127
|
+
strategies.push('markdown-post');
|
|
128
|
+
}
|
|
129
|
+
if (/(mention|@)/.test(haystack)) {
|
|
130
|
+
strategies.push('mentions-post');
|
|
131
|
+
}
|
|
132
|
+
if (/(realtime|websocket|presence)/.test(haystack)) {
|
|
133
|
+
strategies.push('realtime-post');
|
|
134
|
+
}
|
|
135
|
+
if (/(search|find|spotlight)/.test(haystack)) {
|
|
136
|
+
strategies.push('search-baseline');
|
|
137
|
+
}
|
|
138
|
+
if (/(message|post|realtime|websocket|chat)/.test(haystack)) {
|
|
139
|
+
strategies.push('message-post');
|
|
140
|
+
}
|
|
141
|
+
if (/(channel|navigation|sidebar|switch)/.test(haystack)) {
|
|
142
|
+
strategies.push('channel-baseline');
|
|
143
|
+
}
|
|
144
|
+
strategies.push('generic-baseline');
|
|
145
|
+
return Array.from(new Set(strategies));
|
|
146
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
2
|
+
// See LICENSE.txt for license information.
|
|
3
|
+
import { existsSync } from 'fs';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
import { spawnSync } from 'child_process';
|
|
6
|
+
export function resolvePlaywrightBinary(testsRoot) {
|
|
7
|
+
const unixPath = join(testsRoot, 'node_modules', '.bin', 'playwright');
|
|
8
|
+
const windowsPath = join(testsRoot, 'node_modules', '.bin', 'playwright.cmd');
|
|
9
|
+
if (existsSync(unixPath)) {
|
|
10
|
+
return unixPath;
|
|
11
|
+
}
|
|
12
|
+
if (existsSync(windowsPath)) {
|
|
13
|
+
return windowsPath;
|
|
14
|
+
}
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
export function summarizeCommandOutput(stdout, stderr) {
|
|
18
|
+
const combined = [stdout, stderr].filter(Boolean).join('\n').trim();
|
|
19
|
+
if (!combined) {
|
|
20
|
+
return '';
|
|
21
|
+
}
|
|
22
|
+
const lines = combined.split('\n').slice(-20);
|
|
23
|
+
return lines.join('\n').slice(0, 2000);
|
|
24
|
+
}
|
|
25
|
+
export function runCommand(command, args, cwd, timeoutMs = 60 * 60 * 1000) {
|
|
26
|
+
// When spawning `claude`, unset CLAUDECODE so nested invocations are allowed.
|
|
27
|
+
// Claude Code sets this variable to block nested sessions; child processes
|
|
28
|
+
// that spawn their own claude instance must run without it.
|
|
29
|
+
let env;
|
|
30
|
+
if (command === 'claude') {
|
|
31
|
+
const { CLAUDECODE: _, ...rest } = process.env;
|
|
32
|
+
env = rest;
|
|
33
|
+
}
|
|
34
|
+
const result = spawnSync(command, args, {
|
|
35
|
+
cwd,
|
|
36
|
+
encoding: 'utf-8',
|
|
37
|
+
timeout: timeoutMs,
|
|
38
|
+
stdio: 'pipe',
|
|
39
|
+
...(env ? { env } : {}),
|
|
40
|
+
});
|
|
41
|
+
return {
|
|
42
|
+
status: result.status ?? 1,
|
|
43
|
+
stdout: result.stdout || '',
|
|
44
|
+
stderr: result.stderr || '',
|
|
45
|
+
error: result.error ? result.error.message : undefined,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
export function resolveMcpCommandTimeoutMs(pipeline) {
|
|
49
|
+
const value = pipeline.mcpCommandTimeoutMs;
|
|
50
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
51
|
+
return 180000;
|
|
52
|
+
}
|
|
53
|
+
return Math.max(60000, Math.min(15 * 60 * 1000, Math.round(value)));
|
|
54
|
+
}
|
|
55
|
+
export function resolveMcpRetries(pipeline) {
|
|
56
|
+
const value = pipeline.mcpRetries;
|
|
57
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
58
|
+
return 1;
|
|
59
|
+
}
|
|
60
|
+
return Math.max(0, Math.min(5, Math.round(value)));
|
|
61
|
+
}
|
|
62
|
+
export function isRetryableMcpFailure(result) {
|
|
63
|
+
const haystack = [result.error || '', result.stderr || '', result.stdout || ''].join('\n').toLowerCase();
|
|
64
|
+
return haystack.includes('etimedout') ||
|
|
65
|
+
haystack.includes('timed out') ||
|
|
66
|
+
haystack.includes('econnreset') ||
|
|
67
|
+
haystack.includes('429') ||
|
|
68
|
+
haystack.includes('rate limit') ||
|
|
69
|
+
haystack.includes('temporar');
|
|
70
|
+
}
|
|
71
|
+
export function runCommandWithRetries(command, args, cwd, timeoutMs, retries) {
|
|
72
|
+
let result = runCommand(command, args, cwd, timeoutMs);
|
|
73
|
+
for (let attempt = 1; attempt <= retries; attempt += 1) {
|
|
74
|
+
if (result.status === 0) {
|
|
75
|
+
return result;
|
|
76
|
+
}
|
|
77
|
+
if (!isRetryableMcpFailure(result)) {
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
result = runCommand(command, args, cwd, timeoutMs);
|
|
81
|
+
}
|
|
82
|
+
return result;
|
|
83
|
+
}
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
2
|
+
// See LICENSE.txt for license information.
|
|
3
|
+
import { collectMatches, escapeRegExp, parseInitSetupBindings } from './api_catalog.js';
|
|
4
|
+
import { firstFlowFiles } from './pipeline_utils.js';
|
|
5
|
+
export function validateGeneratedSpecContent(content, apiSurface) {
|
|
6
|
+
const issues = [];
|
|
7
|
+
if (/\btest\.describe\s*\(/.test(content)) {
|
|
8
|
+
issues.push({
|
|
9
|
+
code: 'disallowed-describe',
|
|
10
|
+
message: 'Generated tests must not use test.describe.',
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
if (/\btest\.only\s*\(/.test(content)) {
|
|
14
|
+
issues.push({
|
|
15
|
+
code: 'disallowed-only',
|
|
16
|
+
message: 'Generated tests must not use test.only.',
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
if (!/\btest\s*\(/.test(content)) {
|
|
20
|
+
issues.push({
|
|
21
|
+
code: 'missing-test',
|
|
22
|
+
message: 'Generated file does not include a test() declaration.',
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
if (/\btag\s*:\s*\[/.test(content)) {
|
|
26
|
+
issues.push({
|
|
27
|
+
code: 'tag-array-disallowed',
|
|
28
|
+
message: 'Generated tests must use a single tag string, not a tag array.',
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
const hasTagOption = /\btag\s*:\s*['"][^'"]+['"]/.test(content);
|
|
32
|
+
const hasTagInTitle = /\btest(?:\.\w+)?\s*\(\s*['"][^'"]*@ai-assisted[^'"]*['"]/.test(content);
|
|
33
|
+
if (!(hasTagOption || hasTagInTitle) || !/@ai-assisted/.test(content)) {
|
|
34
|
+
issues.push({
|
|
35
|
+
code: 'missing-tag',
|
|
36
|
+
message: "Generated tests must include '@ai-assisted' either as tag option or in test title.",
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
if (/\bsystemConsolePage\.toBeVisible\s*\(/.test(content)) {
|
|
40
|
+
issues.push({
|
|
41
|
+
code: 'fragile-system-console-visibility',
|
|
42
|
+
message: 'Avoid systemConsolePage.toBeVisible(); it relies on legacy backstage navigation that may be absent.',
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
const fragileSelectors = [
|
|
46
|
+
'.backstage-navbar',
|
|
47
|
+
'.admin-console__wrapper',
|
|
48
|
+
'.left-panel',
|
|
49
|
+
'.panel-card',
|
|
50
|
+
].filter((selector) => content.includes(selector));
|
|
51
|
+
if (fragileSelectors.length > 0) {
|
|
52
|
+
issues.push({
|
|
53
|
+
code: 'fragile-selector',
|
|
54
|
+
message: `Avoid brittle class selectors in generated tests: ${Array.from(new Set(fragileSelectors)).join(', ')}`,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
if (apiSurface) {
|
|
58
|
+
const unknownPwProps = Array.from(collectMatches(content, /\bpw\.([A-Za-z_][A-Za-z0-9_]*)\b/g)).filter((prop) => !apiSurface.pwProps.has(prop));
|
|
59
|
+
const unknownBrowserMethods = Array.from(collectMatches(content, /\bpw\.testBrowser\.([A-Za-z_][A-Za-z0-9_]*)\b/g)).filter((method) => !apiSurface.testBrowserMethods.has(method));
|
|
60
|
+
const unknownNestedPwMembers = [];
|
|
61
|
+
for (const match of content.matchAll(/\bpw\.([A-Za-z_][A-Za-z0-9_]*)\.([A-Za-z_][A-Za-z0-9_]*)\b/g)) {
|
|
62
|
+
const objectName = match[1];
|
|
63
|
+
const methodName = match[2];
|
|
64
|
+
if (!objectName || !methodName || objectName === 'testBrowser') {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
const knownMethods = apiSurface.pwNestedMethods.get(objectName);
|
|
68
|
+
if (!knownMethods || !knownMethods.has(methodName)) {
|
|
69
|
+
unknownNestedPwMembers.push(`pw.${objectName}.${methodName}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const unknownChannelMembers = Array.from(collectMatches(content, /\bchannelsPage\.([A-Za-z_][A-Za-z0-9_]*)\b/g)).filter((member) => !apiSurface.channelsPageMembers.has(member));
|
|
73
|
+
const unknownSidebarMembers = Array.from(collectMatches(content, /\bchannelsPage\.sidebarRight\.([A-Za-z_][A-Za-z0-9_]*)\b/g)).filter((member) => !apiSurface.sidebarRightMembers.has(member));
|
|
74
|
+
const initSetupBindings = parseInitSetupBindings(content);
|
|
75
|
+
const unknownInitSetupKeys = initSetupBindings
|
|
76
|
+
.map((binding) => binding.key)
|
|
77
|
+
.filter((key) => !apiSurface.initSetupKeys.has(key));
|
|
78
|
+
const unknownInitSetupVariableMethods = [];
|
|
79
|
+
for (const binding of initSetupBindings) {
|
|
80
|
+
const knownMethods = apiSurface.initSetupVariableMethods.get(binding.variable);
|
|
81
|
+
if (!knownMethods || knownMethods.size === 0) {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
const methodPattern = new RegExp(`\\b${escapeRegExp(binding.variable)}\\.([A-Za-z_][A-Za-z0-9_]*)\\b`, 'g');
|
|
85
|
+
for (const method of collectMatches(content, methodPattern)) {
|
|
86
|
+
if (!knownMethods.has(method)) {
|
|
87
|
+
unknownInitSetupVariableMethods.push(`${binding.variable}.${method}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const unknown = [
|
|
92
|
+
...unknownPwProps.map((value) => `pw.${value}`),
|
|
93
|
+
...unknownBrowserMethods.map((value) => `pw.testBrowser.${value}`),
|
|
94
|
+
...unknownNestedPwMembers,
|
|
95
|
+
...unknownChannelMembers.map((value) => `channelsPage.${value}`),
|
|
96
|
+
...unknownSidebarMembers.map((value) => `channelsPage.sidebarRight.${value}`),
|
|
97
|
+
...unknownInitSetupKeys.map((value) => `pw.initSetup.{${value}}`),
|
|
98
|
+
...unknownInitSetupVariableMethods,
|
|
99
|
+
];
|
|
100
|
+
if (unknown.length > 0) {
|
|
101
|
+
issues.push({
|
|
102
|
+
code: 'unknown-api-surface',
|
|
103
|
+
message: `Generated test uses unknown API/page-object members: ${Array.from(new Set(unknown)).join(', ')}`,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return issues;
|
|
108
|
+
}
|
|
109
|
+
export function createNativePlaywrightSpec(flow, slug, strategy) {
|
|
110
|
+
const linkedFiles = firstFlowFiles(flow).join(', ') || 'N/A';
|
|
111
|
+
const header = [
|
|
112
|
+
"import {test, expect} from '@mattermost/playwright-lib';",
|
|
113
|
+
'',
|
|
114
|
+
'/**',
|
|
115
|
+
` * Auto-generated by @yasserkhanorg/e2e-agents`,
|
|
116
|
+
` * Flow: ${flow.id} (${flow.name})`,
|
|
117
|
+
` * Strategy: ${strategy}`,
|
|
118
|
+
` * Linked files: ${linkedFiles}`,
|
|
119
|
+
' */',
|
|
120
|
+
];
|
|
121
|
+
const start = [
|
|
122
|
+
`test('${flow.priority}: ${flow.name} generated coverage', {tag: '@ai-assisted'}, async ({pw}) => {`,
|
|
123
|
+
' const {user, team} = await pw.initSetup();',
|
|
124
|
+
' const {channelsPage} = await pw.testBrowser.login(user);',
|
|
125
|
+
' await channelsPage.goto(team.name);',
|
|
126
|
+
];
|
|
127
|
+
const end = [
|
|
128
|
+
'});',
|
|
129
|
+
'',
|
|
130
|
+
];
|
|
131
|
+
if (strategy === 'thread-reply') {
|
|
132
|
+
return [
|
|
133
|
+
...header,
|
|
134
|
+
...start,
|
|
135
|
+
` const parentMessage = \`ai-${slug}-parent-\${Date.now()}\`;`,
|
|
136
|
+
' await channelsPage.postMessage(parentMessage);',
|
|
137
|
+
' const rootPost = await channelsPage.getLastPost();',
|
|
138
|
+
' await rootPost.openAThread();',
|
|
139
|
+
` const replyMessage = \`ai-${slug}-reply-\${Date.now()}\`;`,
|
|
140
|
+
' await channelsPage.sidebarRight.postMessage(replyMessage);',
|
|
141
|
+
' const lastReply = await channelsPage.sidebarRight.getLastPost();',
|
|
142
|
+
' await expect(lastReply.container).toContainText(replyMessage);',
|
|
143
|
+
...end,
|
|
144
|
+
].join('\n');
|
|
145
|
+
}
|
|
146
|
+
if (strategy === 'lifecycle-channel') {
|
|
147
|
+
return [
|
|
148
|
+
...header,
|
|
149
|
+
...start,
|
|
150
|
+
` const channelName = \`ai-${slug}-\${Date.now().toString().slice(-6)}\`;`,
|
|
151
|
+
" await channelsPage.newChannel(channelName, 'O');",
|
|
152
|
+
' await expect(channelsPage.page).toHaveURL(new RegExp(`/channels/${channelName}$`));',
|
|
153
|
+
...end,
|
|
154
|
+
].join('\n');
|
|
155
|
+
}
|
|
156
|
+
if (strategy === 'channel-settings') {
|
|
157
|
+
return [
|
|
158
|
+
...header,
|
|
159
|
+
...start,
|
|
160
|
+
' await channelsPage.openChannelSettings();',
|
|
161
|
+
" await expect(channelsPage.page.getByRole('dialog', {name: 'Channel Settings'})).toBeVisible();",
|
|
162
|
+
" await channelsPage.page.keyboard.press('Escape');",
|
|
163
|
+
...end,
|
|
164
|
+
].join('\n');
|
|
165
|
+
}
|
|
166
|
+
if (strategy === 'channel-switch') {
|
|
167
|
+
return [
|
|
168
|
+
...header,
|
|
169
|
+
...start,
|
|
170
|
+
" await channelsPage.goto(team.name, 'off-topic');",
|
|
171
|
+
" await expect(channelsPage.page).toHaveURL(/\\/channels\\/off-topic$/);",
|
|
172
|
+
" await expect(channelsPage.page.locator('#channelHeaderTitle')).toContainText(/off-topic/i);",
|
|
173
|
+
...end,
|
|
174
|
+
].join('\n');
|
|
175
|
+
}
|
|
176
|
+
if (strategy === 'markdown-post') {
|
|
177
|
+
return [
|
|
178
|
+
...header,
|
|
179
|
+
...start,
|
|
180
|
+
` const message = '**ai-${slug}-bold** _italic_';`,
|
|
181
|
+
' await channelsPage.postMessage(message);',
|
|
182
|
+
' const lastPost = await channelsPage.getLastPost();',
|
|
183
|
+
" await expect(lastPost.container.locator('strong')).toBeVisible();",
|
|
184
|
+
...end,
|
|
185
|
+
].join('\n');
|
|
186
|
+
}
|
|
187
|
+
if (strategy === 'mentions-post') {
|
|
188
|
+
return [
|
|
189
|
+
...header,
|
|
190
|
+
...start,
|
|
191
|
+
' const mention = `@${user.username}`;',
|
|
192
|
+
' await channelsPage.postMessage(`Ping ${mention}`);',
|
|
193
|
+
' const lastPost = await channelsPage.getLastPost();',
|
|
194
|
+
' await expect(lastPost.container).toContainText(mention);',
|
|
195
|
+
...end,
|
|
196
|
+
].join('\n');
|
|
197
|
+
}
|
|
198
|
+
if (strategy === 'realtime-post') {
|
|
199
|
+
return [
|
|
200
|
+
...header,
|
|
201
|
+
...start,
|
|
202
|
+
` const message = \`ai-${slug}-realtime-\${Date.now()}\`;`,
|
|
203
|
+
' await channelsPage.postMessage(message);',
|
|
204
|
+
' const lastPost = await channelsPage.getLastPost();',
|
|
205
|
+
' await expect(lastPost.container).toContainText(message);',
|
|
206
|
+
" await expect(channelsPage.page.locator('#channel_view')).toBeVisible();",
|
|
207
|
+
...end,
|
|
208
|
+
].join('\n');
|
|
209
|
+
}
|
|
210
|
+
if (strategy === 'message-post') {
|
|
211
|
+
return [
|
|
212
|
+
...header,
|
|
213
|
+
...start,
|
|
214
|
+
` const message = \`ai-${slug}-message-\${Date.now()}\`;`,
|
|
215
|
+
' await channelsPage.postMessage(message);',
|
|
216
|
+
' await expect(channelsPage.getLastPost()).toContainText(message);',
|
|
217
|
+
...end,
|
|
218
|
+
].join('\n');
|
|
219
|
+
}
|
|
220
|
+
if (strategy === 'channel-baseline') {
|
|
221
|
+
return [
|
|
222
|
+
...header,
|
|
223
|
+
...start,
|
|
224
|
+
" await expect(channelsPage.page.locator('#channelHeaderTitle')).toBeVisible();",
|
|
225
|
+
" await expect(channelsPage.page.locator('#SidebarContainer')).toBeVisible();",
|
|
226
|
+
...end,
|
|
227
|
+
].join('\n');
|
|
228
|
+
}
|
|
229
|
+
if (strategy === 'search-baseline') {
|
|
230
|
+
return [
|
|
231
|
+
...header,
|
|
232
|
+
...start,
|
|
233
|
+
` const searchTerm = \`ai-${slug}-\${Date.now().toString().slice(-6)}\`;`,
|
|
234
|
+
' await channelsPage.postMessage(searchTerm);',
|
|
235
|
+
' await channelsPage.globalHeader.openSearch();',
|
|
236
|
+
' await channelsPage.searchBox.searchInput.fill(searchTerm);',
|
|
237
|
+
" await channelsPage.page.keyboard.press('Enter');",
|
|
238
|
+
" await expect(channelsPage.page.locator('#searchContainer')).toBeVisible();",
|
|
239
|
+
...end,
|
|
240
|
+
].join('\n');
|
|
241
|
+
}
|
|
242
|
+
return [
|
|
243
|
+
...header,
|
|
244
|
+
...start,
|
|
245
|
+
' await expect(channelsPage.page).toHaveURL(/\\/channels\\//);',
|
|
246
|
+
" await expect(channelsPage.page.locator('#channelHeaderTitle')).toBeVisible();",
|
|
247
|
+
...end,
|
|
248
|
+
].join('\n');
|
|
249
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
2
|
+
// See LICENSE.txt for license information.
|
|
3
|
+
import { relative } from 'path';
|
|
4
|
+
import { normalizePath } from './utils.js';
|
|
5
|
+
import { runCommand, summarizeCommandOutput } from './process_runner.js';
|
|
6
|
+
export function runPlaywrightRuntimeValidation(testsRoot, testFile, pipeline, playwrightBinary) {
|
|
7
|
+
if (!playwrightBinary) {
|
|
8
|
+
return {
|
|
9
|
+
status: 'failed',
|
|
10
|
+
detail: 'Playwright binary not found; cannot execute runtime validation.',
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
const relativeSpecPath = normalizePath(relative(testsRoot, testFile));
|
|
14
|
+
if (relativeSpecPath.startsWith('../') || relativeSpecPath.startsWith('..\\')) {
|
|
15
|
+
return {
|
|
16
|
+
status: 'failed',
|
|
17
|
+
detail: 'Generated spec path resolved outside testsRoot during runtime validation.',
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
const args = ['test', relativeSpecPath, '--workers', '1', '--retries', '0', '--max-failures', '1', '--reporter', 'line'];
|
|
21
|
+
if (pipeline.headless === false) {
|
|
22
|
+
args.push('--headed');
|
|
23
|
+
}
|
|
24
|
+
if (pipeline.project) {
|
|
25
|
+
args.push('--project', pipeline.project);
|
|
26
|
+
}
|
|
27
|
+
const commandResult = runCommand(playwrightBinary, args, testsRoot, 10 * 60 * 1000);
|
|
28
|
+
if (commandResult.status === 0) {
|
|
29
|
+
return { status: 'passed' };
|
|
30
|
+
}
|
|
31
|
+
const summary = summarizeCommandOutput(commandResult.stdout, commandResult.stderr);
|
|
32
|
+
return {
|
|
33
|
+
status: 'failed',
|
|
34
|
+
detail: summary || commandResult.error || `playwright test failed with status ${commandResult.status}`,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
export function runPlaywrightListValidation(testsRoot, testFile, pipeline, playwrightBinary) {
|
|
38
|
+
if (!playwrightBinary) {
|
|
39
|
+
return {
|
|
40
|
+
status: 'skipped',
|
|
41
|
+
detail: 'Playwright binary not found under testsRoot/node_modules/.bin; runtime compile validation skipped.',
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
const relativeSpecPath = normalizePath(relative(testsRoot, testFile));
|
|
45
|
+
if (relativeSpecPath.startsWith('../') || relativeSpecPath.startsWith('..\\')) {
|
|
46
|
+
return {
|
|
47
|
+
status: 'failed',
|
|
48
|
+
detail: 'Generated spec path resolved outside testsRoot during validation.',
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
const args = ['test', '--list', relativeSpecPath];
|
|
52
|
+
if (pipeline.headless === false) {
|
|
53
|
+
args.push('--headed');
|
|
54
|
+
}
|
|
55
|
+
if (pipeline.project) {
|
|
56
|
+
args.push('--project', pipeline.project);
|
|
57
|
+
}
|
|
58
|
+
const commandResult = runCommand(playwrightBinary, args, testsRoot);
|
|
59
|
+
if (commandResult.error && /ENOENT/.test(commandResult.error)) {
|
|
60
|
+
return {
|
|
61
|
+
status: 'skipped',
|
|
62
|
+
detail: 'Playwright binary was not executable; runtime compile validation skipped.',
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
if (commandResult.status === 0) {
|
|
66
|
+
return { status: 'passed' };
|
|
67
|
+
}
|
|
68
|
+
const summary = summarizeCommandOutput(commandResult.stdout, commandResult.stderr);
|
|
69
|
+
return {
|
|
70
|
+
status: 'failed',
|
|
71
|
+
detail: summary || commandResult.error || `playwright --list failed with status ${commandResult.status}`,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
@@ -73,7 +73,7 @@ export function runPlaywrightSpec(specPath, testsRoot, options) {
|
|
|
73
73
|
if (!existsSync(reportDir)) {
|
|
74
74
|
mkdirSync(reportDir, { recursive: true });
|
|
75
75
|
}
|
|
76
|
-
const reportPath = join(reportDir, `report-${Date.now()}.json`);
|
|
76
|
+
const reportPath = join(reportDir, `report-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.json`);
|
|
77
77
|
const args = [
|
|
78
78
|
'playwright', 'test',
|
|
79
79
|
specPath,
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
2
|
+
// See LICENSE.txt for license information.
|
|
3
|
+
import { resolveConfig } from '../../agent/config.js';
|
|
4
|
+
import { runPipeline } from '../../pipeline/orchestrator.js';
|
|
5
|
+
export async function runAnalyzeCommand(args, autoConfig) {
|
|
6
|
+
if (!args.path && !autoConfig) {
|
|
7
|
+
console.error('Error: --path is required for analyze command');
|
|
8
|
+
process.exit(1);
|
|
9
|
+
}
|
|
10
|
+
const { config } = resolveConfig(process.cwd(), autoConfig, {
|
|
11
|
+
path: args.path,
|
|
12
|
+
profile: args.profile,
|
|
13
|
+
testsRoot: args.testsRoot,
|
|
14
|
+
mode: 'impact',
|
|
15
|
+
gitSince: args.gitSince,
|
|
16
|
+
llmProvider: args.llmProvider,
|
|
17
|
+
});
|
|
18
|
+
const testsRoot = config.testsRoot || config.path;
|
|
19
|
+
const analyzeStages = [
|
|
20
|
+
'preprocess', 'impact', 'coverage',
|
|
21
|
+
];
|
|
22
|
+
if (args.analyzeGenerate) {
|
|
23
|
+
analyzeStages.push('generation');
|
|
24
|
+
}
|
|
25
|
+
if (args.analyzeHeal || args.analyzeHealReport) {
|
|
26
|
+
analyzeStages.push('heal');
|
|
27
|
+
}
|
|
28
|
+
const result = await runPipeline({
|
|
29
|
+
appPath: config.path,
|
|
30
|
+
testsRoot,
|
|
31
|
+
gitSince: args.gitSince || config.git.since,
|
|
32
|
+
routeFamilies: config.routeFamilies,
|
|
33
|
+
apiSurface: config.apiSurface,
|
|
34
|
+
stages: analyzeStages,
|
|
35
|
+
generation: args.analyzeGenerate
|
|
36
|
+
? {
|
|
37
|
+
defaultOutputDir: args.analyzeGenerateOutputDir || 'specs/functional/ai-assisted',
|
|
38
|
+
dryRun: args.dryRun,
|
|
39
|
+
}
|
|
40
|
+
: undefined,
|
|
41
|
+
heal: (args.analyzeHeal || args.analyzeHealReport)
|
|
42
|
+
? {
|
|
43
|
+
mcp: args.pipelineMcp ?? true,
|
|
44
|
+
mcpAllowFallback: args.pipelineMcpAllowFallback ?? false,
|
|
45
|
+
mcpOnly: args.pipelineMcpOnly ?? false,
|
|
46
|
+
mcpCommandTimeoutMs: args.pipelineMcpTimeoutMs,
|
|
47
|
+
mcpRetries: args.pipelineMcpRetries ?? 1,
|
|
48
|
+
dryRun: args.dryRun,
|
|
49
|
+
}
|
|
50
|
+
: undefined,
|
|
51
|
+
playwrightReportPath: args.analyzeHealReport,
|
|
52
|
+
});
|
|
53
|
+
console.log(`Analyze report: ${result.reportPath}`);
|
|
54
|
+
console.log(`Analyze flows identified: ${result.report.summary.flowsIdentified}`);
|
|
55
|
+
console.log(`Analyze flows covered: ${result.report.summary.flowsCovered}`);
|
|
56
|
+
console.log(`Analyze flows uncovered: ${result.report.summary.flowsUncovered}`);
|
|
57
|
+
console.log(`Analyze overall confidence: ${result.report.summary.overallConfidence}`);
|
|
58
|
+
console.log(`Analyze route families: ${result.report.summary.routeFamiliesImpacted.join(', ') || 'none'}`);
|
|
59
|
+
if (result.generated && result.generated.length > 0) {
|
|
60
|
+
const written = result.generated.filter((g) => g.written).length;
|
|
61
|
+
console.log(`Analyze generated specs: ${result.generated.length} (written=${written})`);
|
|
62
|
+
for (const g of result.generated) {
|
|
63
|
+
console.log(` ${g.mode}: ${g.specPath}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (result.healResult) {
|
|
67
|
+
const healed = result.healResult.summary.results.filter((r) => r.healStatus === 'success').length;
|
|
68
|
+
const healFailed = result.healResult.summary.results.filter((r) => r.healStatus === 'failed').length;
|
|
69
|
+
console.log(`Analyze heal targets: ${result.healResult.targets.length} (healed=${healed}, failed=${healFailed})`);
|
|
70
|
+
}
|
|
71
|
+
if (result.warnings.length > 0) {
|
|
72
|
+
console.log(`Analyze warnings: ${result.warnings.join(' | ')}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
2
|
+
// See LICENSE.txt for license information.
|
|
3
|
+
import { readFileSync } from 'fs';
|
|
4
|
+
import { resolveConfig } from '../../agent/config.js';
|
|
5
|
+
import { appendFeedbackAndRecompute } from '../../agent/feedback.js';
|
|
6
|
+
export function runFeedbackCommand(args, autoConfig) {
|
|
7
|
+
if (!args.path && !autoConfig) {
|
|
8
|
+
console.error('Error: --path is required for feedback command');
|
|
9
|
+
process.exit(1);
|
|
10
|
+
}
|
|
11
|
+
if (!args.feedbackInputPath) {
|
|
12
|
+
console.error('Error: --feedback-input <path> is required for feedback command');
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
const { config } = resolveConfig(process.cwd(), autoConfig, {
|
|
16
|
+
path: args.path,
|
|
17
|
+
profile: args.profile,
|
|
18
|
+
testsRoot: args.testsRoot,
|
|
19
|
+
mode: 'impact',
|
|
20
|
+
llmProvider: args.llmProvider,
|
|
21
|
+
});
|
|
22
|
+
const reportRoot = config.testsRoot || config.path;
|
|
23
|
+
const raw = JSON.parse(readFileSync(args.feedbackInputPath, 'utf-8'));
|
|
24
|
+
const payload = {
|
|
25
|
+
timestamp: raw.timestamp || new Date().toISOString(),
|
|
26
|
+
runSet: raw.runSet || 'targeted',
|
|
27
|
+
recommendedTests: raw.recommendedTests || [],
|
|
28
|
+
executedTests: raw.executedTests || [],
|
|
29
|
+
failedTests: raw.failedTests || [],
|
|
30
|
+
escapedFailures: raw.escapedFailures || [],
|
|
31
|
+
};
|
|
32
|
+
const output = appendFeedbackAndRecompute(reportRoot, payload);
|
|
33
|
+
console.log(`Feedback data: ${output.feedbackPath}`);
|
|
34
|
+
console.log(`Calibration data: ${output.calibrationPath}`);
|
|
35
|
+
console.log(`Calibration overall: precision=${output.calibration.overall.precision}, recall=${output.calibration.overall.recall}, fnr=${output.calibration.overall.falseNegativeRate}`);
|
|
36
|
+
}
|