@yasserkhanorg/e2e-agents 0.10.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 -1060
- 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 -1054
- 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/esm/cli.js
CHANGED
|
@@ -1,522 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
3
3
|
// See LICENSE.txt for license information.
|
|
4
|
-
import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
5
|
-
import { dirname, join, resolve } from 'path';
|
|
6
4
|
import { resolveConfig } from './agent/config.js';
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import { extractPlaywrightUnstableSpecs } from './agent/playwright_report.js';
|
|
19
|
-
import { runPipeline } from './pipeline/orchestrator.js';
|
|
20
|
-
import { LLMProviderFactory } from './provider_factory.js';
|
|
21
|
-
import { runAgenticGeneration } from './agentic/runner.js';
|
|
22
|
-
import { loadOrBuildApiSurface } from './knowledge/api_surface.js';
|
|
23
|
-
const CONFIG_CANDIDATES = ['e2e-ai-agents.config.json', '.e2e-ai-agents.config.json'];
|
|
24
|
-
function findConfigUpwards(startDir) {
|
|
25
|
-
if (!startDir) {
|
|
26
|
-
return undefined;
|
|
27
|
-
}
|
|
28
|
-
let current = resolve(startDir);
|
|
29
|
-
while (true) {
|
|
30
|
-
for (const candidate of CONFIG_CANDIDATES) {
|
|
31
|
-
const fullPath = join(current, candidate);
|
|
32
|
-
if (existsSync(fullPath)) {
|
|
33
|
-
return fullPath;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
const parent = dirname(current);
|
|
37
|
-
if (parent === current) {
|
|
38
|
-
break;
|
|
39
|
-
}
|
|
40
|
-
current = parent;
|
|
41
|
-
}
|
|
42
|
-
return undefined;
|
|
43
|
-
}
|
|
44
|
-
function resolveAutoConfig(args) {
|
|
45
|
-
if (args.configPath) {
|
|
46
|
-
return args.configPath;
|
|
47
|
-
}
|
|
48
|
-
const searchRoots = [
|
|
49
|
-
process.cwd(),
|
|
50
|
-
args.testsRoot,
|
|
51
|
-
args.path,
|
|
52
|
-
].filter(Boolean);
|
|
53
|
-
for (const root of searchRoots) {
|
|
54
|
-
const found = findConfigUpwards(root);
|
|
55
|
-
if (found) {
|
|
56
|
-
return found;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
return undefined;
|
|
60
|
-
}
|
|
61
|
-
function printUsage() {
|
|
62
|
-
// eslint-disable-next-line no-console
|
|
63
|
-
console.log([
|
|
64
|
-
'Usage:',
|
|
65
|
-
' e2e-ai-agents impact --path <app-root> [options]',
|
|
66
|
-
' e2e-ai-agents plan --path <app-root> [options]',
|
|
67
|
-
' e2e-ai-agents suggest --path <app-root> [options]',
|
|
68
|
-
' e2e-ai-agents heal --path <app-root> --traceability-report <json> [options]',
|
|
69
|
-
' e2e-ai-agents finalize-generated-tests --path <app-root> [options]',
|
|
70
|
-
' e2e-ai-agents feedback --path <app-root> --feedback-input <json>',
|
|
71
|
-
' e2e-ai-agents traceability-capture --path <app-root> --traceability-report <json>',
|
|
72
|
-
' e2e-ai-agents traceability-ingest --path <app-root> --traceability-input <json>',
|
|
73
|
-
' e2e-ai-agents generate [--scenarios <path|json>] [--max-attempts <n>] [--dry-run]',
|
|
74
|
-
' e2e-ai-agents analyze --path <app-root> [--tests-root <path>] [--since <ref>] [--generate] [--generate-output <dir>] [--heal] [--heal-report <json>]',
|
|
75
|
-
' e2e-ai-agents llm-health',
|
|
76
|
-
'',
|
|
77
|
-
'Options:',
|
|
78
|
-
' --config <path> Path to e2e-ai-agents.config.json (auto-discovered if present)',
|
|
79
|
-
' --path <app-root> Path to the web app (required)',
|
|
80
|
-
' --profile <name> default | mattermost',
|
|
81
|
-
' --mattermost Shortcut for --profile mattermost',
|
|
82
|
-
' --tests-root <path> Path to tests root (optional)',
|
|
83
|
-
' --framework <name> auto | playwright | cypress | selenium',
|
|
84
|
-
' --patterns <globs> Comma-separated test patterns',
|
|
85
|
-
' --flow-patterns <g> Comma-separated flow discovery patterns',
|
|
86
|
-
' --flow-exclude <g> Comma-separated flow exclude patterns',
|
|
87
|
-
' --flow-catalog <path> Path to flow catalog JSON',
|
|
88
|
-
' --allow-fallback Allow impact analysis without diff',
|
|
89
|
-
' --pipeline Run Playwright AI pipeline for missing P0/P1 flows',
|
|
90
|
-
' --pipeline-scenarios Number of scenarios per flow (default 3)',
|
|
91
|
-
' --pipeline-output Output directory for generated tests',
|
|
92
|
-
' --pipeline-base-url Base URL for Playwright runs',
|
|
93
|
-
' --pipeline-browser Browser: chrome|chromium|firefox|webkit',
|
|
94
|
-
' --pipeline-headless Run in headless mode',
|
|
95
|
-
' --pipeline-headed Run in headed mode',
|
|
96
|
-
' --pipeline-project Playwright project name',
|
|
97
|
-
' --pipeline-parallel Enable parallel mode in generator',
|
|
98
|
-
' --pipeline-dry-run Do not execute pipeline (report only)',
|
|
99
|
-
' --pipeline-mcp Use Playwright MCP server for exploration/healing',
|
|
100
|
-
' --pipeline-mcp-allow-fallback Allow non-MCP fallback if official MCP setup fails',
|
|
101
|
-
' --pipeline-mcp-only Require MCP for UI exploration (fail if unavailable)',
|
|
102
|
-
' --pipeline-mcp-timeout-ms <n> Timeout per MCP CLI invocation in milliseconds',
|
|
103
|
-
' --pipeline-mcp-retries <n> Retry count for retryable MCP CLI failures',
|
|
104
|
-
' --spec <path> Optional spec PDF for context',
|
|
105
|
-
' --since <git-ref> Git ref for impact analysis (default HEAD~1)',
|
|
106
|
-
' --time <minutes> Time limit in minutes',
|
|
107
|
-
' --budget-usd <amount> Max LLM budget in USD',
|
|
108
|
-
' --budget-tokens <n> Max LLM tokens',
|
|
109
|
-
' --llm-provider <name> LLM provider: auto | anthropic | openai | ollama',
|
|
110
|
-
' --policy-min-confidence <n> Minimum confidence for targeted suite',
|
|
111
|
-
' --policy-safe-merge-confidence <n> Confidence needed for safe-to-merge',
|
|
112
|
-
' --policy-force-full-on-warnings <n> Escalate to full at warning count',
|
|
113
|
-
' --policy-risky-patterns <globs> Comma-separated risky file globs',
|
|
114
|
-
' --policy-enforcement-mode <mode> advisory | warn | block',
|
|
115
|
-
' --policy-block-actions <actions> Comma-separated CI actions to block/warn',
|
|
116
|
-
' --ci-comment-path <path> Write CI markdown summary',
|
|
117
|
-
' --github-output <path> Write GitHub Actions outputs',
|
|
118
|
-
' --fail-on-must-add-tests Exit non-zero on must-add-tests decision',
|
|
119
|
-
' --feedback-input <path> Path to recommendation feedback JSON',
|
|
120
|
-
' --traceability-report <path> Path to Playwright JSON report for traceability capture',
|
|
121
|
-
' --traceability-capture-output <path> Output path for generated traceability ingest JSON',
|
|
122
|
-
' --traceability-coverage-map <path> Optional coverage map (test<->files) to enrich traceability capture',
|
|
123
|
-
' --traceability-changed-files <path> Optional changed-files list/JSON fallback for traceability capture',
|
|
124
|
-
' --traceability-input <path> Path to traceability ingest JSON payload',
|
|
125
|
-
' --traceability-min-hits <n> Minimum signal hits required per file mapping',
|
|
126
|
-
' --traceability-max-files-per-test <n> Cap max mapped files per test',
|
|
127
|
-
' --traceability-max-age-days <n> Drop stale mappings older than N days',
|
|
128
|
-
' --branch <name> Optional handoff branch (prefixed with codex/)',
|
|
129
|
-
' --commit-message <m> Commit message for finalize-generated-tests',
|
|
130
|
-
' --create-pr Open PR with gh after commit',
|
|
131
|
-
' --pr-title <title> PR title for finalize-generated-tests',
|
|
132
|
-
' --pr-body <body> PR body for finalize-generated-tests',
|
|
133
|
-
' --pr-base <branch> PR base branch for finalize-generated-tests',
|
|
134
|
-
' (auto-heal-pr defaults to base=master)',
|
|
135
|
-
' --dry-run Preview actions without mutating git state',
|
|
136
|
-
' --max-attempts <n> Max fix attempts per scenario (default: 3)',
|
|
137
|
-
' --scenarios <path|json> Scenarios file/JSON for generate command',
|
|
138
|
-
' --apply Apply data-testid patches and generate tests',
|
|
139
|
-
' (legacy shortcut; prefer approve-and-generate)',
|
|
140
|
-
' --help Show help',
|
|
141
|
-
].join('\n'));
|
|
142
|
-
}
|
|
143
|
-
function parseArgs(argv) {
|
|
144
|
-
const parsed = { apply: false, help: false };
|
|
145
|
-
if (argv.length === 0) {
|
|
146
|
-
return parsed;
|
|
147
|
-
}
|
|
148
|
-
const command = argv[0];
|
|
149
|
-
if (command === 'impact'
|
|
150
|
-
|| command === 'plan'
|
|
151
|
-
|| command === 'heal'
|
|
152
|
-
|| command === 'suggest'
|
|
153
|
-
|| command === 'generate'
|
|
154
|
-
|| command === 'finalize-generated-tests'
|
|
155
|
-
|| command === 'feedback'
|
|
156
|
-
|| command === 'traceability-capture'
|
|
157
|
-
|| command === 'traceability-ingest'
|
|
158
|
-
|| command === 'analyze'
|
|
159
|
-
|| command === 'llm-health') {
|
|
160
|
-
parsed.command = command;
|
|
161
|
-
}
|
|
162
|
-
for (let i = 1; i < argv.length; i += 1) {
|
|
163
|
-
const arg = argv[i];
|
|
164
|
-
const next = argv[i + 1];
|
|
165
|
-
if (arg === '--help' || arg === '-h') {
|
|
166
|
-
parsed.help = true;
|
|
167
|
-
continue;
|
|
168
|
-
}
|
|
169
|
-
if (arg === '--max-attempts' && next) {
|
|
170
|
-
parsed.maxAttempts = parseInt(next, 10);
|
|
171
|
-
i += 1;
|
|
172
|
-
continue;
|
|
173
|
-
}
|
|
174
|
-
if (arg === '--scenarios' && next) {
|
|
175
|
-
parsed.generateScenarios = next;
|
|
176
|
-
i += 1;
|
|
177
|
-
continue;
|
|
178
|
-
}
|
|
179
|
-
if (arg === '--apply') {
|
|
180
|
-
parsed.apply = true;
|
|
181
|
-
continue;
|
|
182
|
-
}
|
|
183
|
-
if (arg === '--config' && next) {
|
|
184
|
-
parsed.configPath = next;
|
|
185
|
-
i += 1;
|
|
186
|
-
continue;
|
|
187
|
-
}
|
|
188
|
-
if (arg === '--path' && next) {
|
|
189
|
-
parsed.path = next;
|
|
190
|
-
i += 1;
|
|
191
|
-
continue;
|
|
192
|
-
}
|
|
193
|
-
if (arg === '--profile' && next) {
|
|
194
|
-
if (next === 'default' || next === 'mattermost') {
|
|
195
|
-
parsed.profile = next;
|
|
196
|
-
}
|
|
197
|
-
i += 1;
|
|
198
|
-
continue;
|
|
199
|
-
}
|
|
200
|
-
if (arg === '--mattermost') {
|
|
201
|
-
parsed.profile = 'mattermost';
|
|
202
|
-
continue;
|
|
203
|
-
}
|
|
204
|
-
if (arg === '--tests-root' && next) {
|
|
205
|
-
parsed.testsRoot = next;
|
|
206
|
-
i += 1;
|
|
207
|
-
continue;
|
|
208
|
-
}
|
|
209
|
-
if (arg === '--framework' && next) {
|
|
210
|
-
parsed.framework = next;
|
|
211
|
-
i += 1;
|
|
212
|
-
continue;
|
|
213
|
-
}
|
|
214
|
-
if (arg === '--patterns' && next) {
|
|
215
|
-
parsed.testPatterns = next.split(',').map((value) => value.trim()).filter(Boolean);
|
|
216
|
-
i += 1;
|
|
217
|
-
continue;
|
|
218
|
-
}
|
|
219
|
-
if (arg === '--flow-patterns' && next) {
|
|
220
|
-
parsed.flowPatterns = next.split(',').map((value) => value.trim()).filter(Boolean);
|
|
221
|
-
i += 1;
|
|
222
|
-
continue;
|
|
223
|
-
}
|
|
224
|
-
if (arg === '--flow-exclude' && next) {
|
|
225
|
-
parsed.flowExclude = next.split(',').map((value) => value.trim()).filter(Boolean);
|
|
226
|
-
i += 1;
|
|
227
|
-
continue;
|
|
228
|
-
}
|
|
229
|
-
if (arg === '--flow-catalog' && next) {
|
|
230
|
-
parsed.flowCatalogPath = next;
|
|
231
|
-
i += 1;
|
|
232
|
-
continue;
|
|
233
|
-
}
|
|
234
|
-
if (arg === '--allow-fallback') {
|
|
235
|
-
parsed.allowFallback = true;
|
|
236
|
-
continue;
|
|
237
|
-
}
|
|
238
|
-
if (arg === '--pipeline') {
|
|
239
|
-
parsed.pipeline = true;
|
|
240
|
-
continue;
|
|
241
|
-
}
|
|
242
|
-
if (arg === '--pipeline-mcp') {
|
|
243
|
-
parsed.pipelineMcp = true;
|
|
244
|
-
continue;
|
|
245
|
-
}
|
|
246
|
-
if (arg === '--pipeline-mcp-allow-fallback') {
|
|
247
|
-
parsed.pipelineMcpAllowFallback = true;
|
|
248
|
-
continue;
|
|
249
|
-
}
|
|
250
|
-
if (arg === '--pipeline-mcp-only') {
|
|
251
|
-
parsed.pipelineMcpOnly = true;
|
|
252
|
-
continue;
|
|
253
|
-
}
|
|
254
|
-
if (arg === '--pipeline-mcp-timeout-ms' && next) {
|
|
255
|
-
parsed.pipelineMcpTimeoutMs = Number(next);
|
|
256
|
-
i += 1;
|
|
257
|
-
continue;
|
|
258
|
-
}
|
|
259
|
-
if (arg === '--pipeline-mcp-retries' && next) {
|
|
260
|
-
parsed.pipelineMcpRetries = Number(next);
|
|
261
|
-
i += 1;
|
|
262
|
-
continue;
|
|
263
|
-
}
|
|
264
|
-
if (arg === '--pipeline-scenarios' && next) {
|
|
265
|
-
const value = Number(next);
|
|
266
|
-
if (Number.isFinite(value)) {
|
|
267
|
-
parsed.pipelineScenarios = value;
|
|
268
|
-
}
|
|
269
|
-
i += 1;
|
|
270
|
-
continue;
|
|
271
|
-
}
|
|
272
|
-
if (arg === '--pipeline-output' && next) {
|
|
273
|
-
parsed.pipelineOutput = next;
|
|
274
|
-
i += 1;
|
|
275
|
-
continue;
|
|
276
|
-
}
|
|
277
|
-
if (arg === '--pipeline-base-url' && next) {
|
|
278
|
-
parsed.pipelineBaseUrl = next;
|
|
279
|
-
i += 1;
|
|
280
|
-
continue;
|
|
281
|
-
}
|
|
282
|
-
if (arg === '--pipeline-browser' && next) {
|
|
283
|
-
const value = next;
|
|
284
|
-
if (value === 'chrome' || value === 'chromium' || value === 'firefox' || value === 'webkit') {
|
|
285
|
-
parsed.pipelineBrowser = value;
|
|
286
|
-
}
|
|
287
|
-
i += 1;
|
|
288
|
-
continue;
|
|
289
|
-
}
|
|
290
|
-
if (arg === '--pipeline-headless') {
|
|
291
|
-
parsed.pipelineHeadless = true;
|
|
292
|
-
continue;
|
|
293
|
-
}
|
|
294
|
-
if (arg === '--pipeline-headed') {
|
|
295
|
-
parsed.pipelineHeadless = false;
|
|
296
|
-
continue;
|
|
297
|
-
}
|
|
298
|
-
if (arg === '--pipeline-project' && next) {
|
|
299
|
-
parsed.pipelineProject = next;
|
|
300
|
-
i += 1;
|
|
301
|
-
continue;
|
|
302
|
-
}
|
|
303
|
-
if (arg === '--pipeline-parallel') {
|
|
304
|
-
parsed.pipelineParallel = true;
|
|
305
|
-
continue;
|
|
306
|
-
}
|
|
307
|
-
if (arg === '--pipeline-dry-run') {
|
|
308
|
-
parsed.pipelineDryRun = true;
|
|
309
|
-
continue;
|
|
310
|
-
}
|
|
311
|
-
if (arg === '--spec' && next) {
|
|
312
|
-
parsed.specPDF = next;
|
|
313
|
-
i += 1;
|
|
314
|
-
continue;
|
|
315
|
-
}
|
|
316
|
-
if (arg === '--since' && next) {
|
|
317
|
-
parsed.gitSince = next;
|
|
318
|
-
i += 1;
|
|
319
|
-
continue;
|
|
320
|
-
}
|
|
321
|
-
if (arg === '--time' && next) {
|
|
322
|
-
const value = Number(next);
|
|
323
|
-
if (Number.isFinite(value)) {
|
|
324
|
-
parsed.timeLimitMinutes = value;
|
|
325
|
-
}
|
|
326
|
-
i += 1;
|
|
327
|
-
continue;
|
|
328
|
-
}
|
|
329
|
-
if (arg === '--budget-usd' && next) {
|
|
330
|
-
const value = Number(next);
|
|
331
|
-
if (Number.isFinite(value)) {
|
|
332
|
-
parsed.budgetUSD = value;
|
|
333
|
-
}
|
|
334
|
-
i += 1;
|
|
335
|
-
continue;
|
|
336
|
-
}
|
|
337
|
-
if (arg === '--budget-tokens' && next) {
|
|
338
|
-
const value = Number(next);
|
|
339
|
-
if (Number.isFinite(value)) {
|
|
340
|
-
parsed.budgetTokens = value;
|
|
341
|
-
}
|
|
342
|
-
i += 1;
|
|
343
|
-
continue;
|
|
344
|
-
}
|
|
345
|
-
if (arg === '--llm-provider' && next) {
|
|
346
|
-
parsed.llmProvider = next;
|
|
347
|
-
i += 1;
|
|
348
|
-
continue;
|
|
349
|
-
}
|
|
350
|
-
if (arg === '--policy-min-confidence' && next) {
|
|
351
|
-
const value = Number(next);
|
|
352
|
-
if (Number.isFinite(value)) {
|
|
353
|
-
parsed.policyMinConfidence = value;
|
|
354
|
-
}
|
|
355
|
-
i += 1;
|
|
356
|
-
continue;
|
|
357
|
-
}
|
|
358
|
-
if (arg === '--policy-safe-merge-confidence' && next) {
|
|
359
|
-
const value = Number(next);
|
|
360
|
-
if (Number.isFinite(value)) {
|
|
361
|
-
parsed.policySafeMergeConfidence = value;
|
|
362
|
-
}
|
|
363
|
-
i += 1;
|
|
364
|
-
continue;
|
|
365
|
-
}
|
|
366
|
-
if (arg === '--policy-force-full-on-warnings' && next) {
|
|
367
|
-
const value = Number(next);
|
|
368
|
-
if (Number.isFinite(value)) {
|
|
369
|
-
parsed.policyWarningsThreshold = value;
|
|
370
|
-
}
|
|
371
|
-
i += 1;
|
|
372
|
-
continue;
|
|
373
|
-
}
|
|
374
|
-
if (arg === '--policy-risky-patterns' && next) {
|
|
375
|
-
parsed.policyRiskyPatterns = next.split(',').map((value) => value.trim()).filter(Boolean);
|
|
376
|
-
i += 1;
|
|
377
|
-
continue;
|
|
378
|
-
}
|
|
379
|
-
if (arg === '--policy-enforcement-mode' && next) {
|
|
380
|
-
if (next === 'advisory' || next === 'warn' || next === 'block') {
|
|
381
|
-
parsed.policyEnforcementMode = next;
|
|
382
|
-
}
|
|
383
|
-
i += 1;
|
|
384
|
-
continue;
|
|
385
|
-
}
|
|
386
|
-
if (arg === '--policy-block-actions' && next) {
|
|
387
|
-
parsed.policyBlockActions = next
|
|
388
|
-
.split(',')
|
|
389
|
-
.map((value) => value.trim())
|
|
390
|
-
.filter((value) => (value === 'run-now' || value === 'must-add-tests' || value === 'safe-to-merge'));
|
|
391
|
-
i += 1;
|
|
392
|
-
continue;
|
|
393
|
-
}
|
|
394
|
-
if (arg === '--ci-comment-path' && next) {
|
|
395
|
-
parsed.ciCommentPath = next;
|
|
396
|
-
i += 1;
|
|
397
|
-
continue;
|
|
398
|
-
}
|
|
399
|
-
if (arg === '--github-output' && next) {
|
|
400
|
-
parsed.githubOutputPath = next;
|
|
401
|
-
i += 1;
|
|
402
|
-
continue;
|
|
403
|
-
}
|
|
404
|
-
if (arg === '--fail-on-must-add-tests') {
|
|
405
|
-
parsed.failOnMustAddTests = true;
|
|
406
|
-
continue;
|
|
407
|
-
}
|
|
408
|
-
if (arg === '--feedback-input' && next) {
|
|
409
|
-
parsed.feedbackInputPath = next;
|
|
410
|
-
i += 1;
|
|
411
|
-
continue;
|
|
412
|
-
}
|
|
413
|
-
if (arg === '--traceability-report' && next) {
|
|
414
|
-
parsed.traceabilityReportPath = next;
|
|
415
|
-
i += 1;
|
|
416
|
-
continue;
|
|
417
|
-
}
|
|
418
|
-
if (arg === '--traceability-capture-output' && next) {
|
|
419
|
-
parsed.traceabilityCaptureOutputPath = next;
|
|
420
|
-
i += 1;
|
|
421
|
-
continue;
|
|
422
|
-
}
|
|
423
|
-
if (arg === '--traceability-coverage-map' && next) {
|
|
424
|
-
parsed.traceabilityCoverageMapPath = next;
|
|
425
|
-
i += 1;
|
|
426
|
-
continue;
|
|
427
|
-
}
|
|
428
|
-
if (arg === '--traceability-changed-files' && next) {
|
|
429
|
-
parsed.traceabilityChangedFilesPath = next;
|
|
430
|
-
i += 1;
|
|
431
|
-
continue;
|
|
432
|
-
}
|
|
433
|
-
if (arg === '--traceability-input' && next) {
|
|
434
|
-
parsed.traceabilityInputPath = next;
|
|
435
|
-
i += 1;
|
|
436
|
-
continue;
|
|
437
|
-
}
|
|
438
|
-
if (arg === '--traceability-min-hits' && next) {
|
|
439
|
-
const value = Number(next);
|
|
440
|
-
if (Number.isFinite(value)) {
|
|
441
|
-
parsed.traceabilityMinHits = value;
|
|
442
|
-
}
|
|
443
|
-
i += 1;
|
|
444
|
-
continue;
|
|
445
|
-
}
|
|
446
|
-
if (arg === '--traceability-max-files-per-test' && next) {
|
|
447
|
-
const value = Number(next);
|
|
448
|
-
if (Number.isFinite(value)) {
|
|
449
|
-
parsed.traceabilityMaxFilesPerTest = value;
|
|
450
|
-
}
|
|
451
|
-
i += 1;
|
|
452
|
-
continue;
|
|
453
|
-
}
|
|
454
|
-
if (arg === '--traceability-max-age-days' && next) {
|
|
455
|
-
const value = Number(next);
|
|
456
|
-
if (Number.isFinite(value)) {
|
|
457
|
-
parsed.traceabilityMaxAgeDays = value;
|
|
458
|
-
}
|
|
459
|
-
i += 1;
|
|
460
|
-
continue;
|
|
461
|
-
}
|
|
462
|
-
if (arg === '--branch' && next) {
|
|
463
|
-
parsed.branch = next;
|
|
464
|
-
i += 1;
|
|
465
|
-
continue;
|
|
466
|
-
}
|
|
467
|
-
if (arg === '--commit-message' && next) {
|
|
468
|
-
parsed.commitMessage = next;
|
|
469
|
-
i += 1;
|
|
470
|
-
continue;
|
|
471
|
-
}
|
|
472
|
-
if (arg === '--create-pr') {
|
|
473
|
-
parsed.createPr = true;
|
|
474
|
-
continue;
|
|
475
|
-
}
|
|
476
|
-
if (arg === '--pr-title' && next) {
|
|
477
|
-
parsed.prTitle = next;
|
|
478
|
-
i += 1;
|
|
479
|
-
continue;
|
|
480
|
-
}
|
|
481
|
-
if (arg === '--pr-body' && next) {
|
|
482
|
-
parsed.prBody = next;
|
|
483
|
-
i += 1;
|
|
484
|
-
continue;
|
|
485
|
-
}
|
|
486
|
-
if (arg === '--pr-base' && next) {
|
|
487
|
-
parsed.prBase = next;
|
|
488
|
-
i += 1;
|
|
489
|
-
continue;
|
|
490
|
-
}
|
|
491
|
-
if (arg === '--dry-run') {
|
|
492
|
-
parsed.dryRun = true;
|
|
493
|
-
continue;
|
|
494
|
-
}
|
|
495
|
-
if (arg === '--generate') {
|
|
496
|
-
parsed.analyzeGenerate = true;
|
|
497
|
-
continue;
|
|
498
|
-
}
|
|
499
|
-
if (arg === '--generate-output' && next) {
|
|
500
|
-
parsed.analyzeGenerateOutputDir = next;
|
|
501
|
-
i += 1;
|
|
502
|
-
continue;
|
|
503
|
-
}
|
|
504
|
-
if (arg === '--heal') {
|
|
505
|
-
parsed.analyzeHeal = true;
|
|
506
|
-
continue;
|
|
507
|
-
}
|
|
508
|
-
if (arg === '--heal-report' && next) {
|
|
509
|
-
parsed.analyzeHealReport = next;
|
|
510
|
-
i += 1;
|
|
511
|
-
continue;
|
|
512
|
-
}
|
|
513
|
-
if (arg === '--no-ai') {
|
|
514
|
-
parsed.noAi = true;
|
|
515
|
-
continue;
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
return parsed;
|
|
519
|
-
}
|
|
5
|
+
import { parseArgs, resolveAutoConfig } from './cli/parse_args.js';
|
|
6
|
+
import { printUsage } from './cli/usage.js';
|
|
7
|
+
import { runLlmHealth } from './cli/commands/llm_health.js';
|
|
8
|
+
import { runAnalyzeCommand } from './cli/commands/analyze.js';
|
|
9
|
+
import { runFeedbackCommand } from './cli/commands/feedback.js';
|
|
10
|
+
import { runTraceabilityCaptureCommand, runTraceabilityIngestCommand } from './cli/commands/traceability.js';
|
|
11
|
+
import { runFinalizeCommand } from './cli/commands/finalize.js';
|
|
12
|
+
import { runHealCommand } from './cli/commands/heal.js';
|
|
13
|
+
import { runImpactCommand } from './cli/commands/impact.js';
|
|
14
|
+
import { runPlanCommand } from './cli/commands/plan.js';
|
|
15
|
+
import { runGenerateCommand } from './cli/commands/generate.js';
|
|
520
16
|
async function main() {
|
|
521
17
|
const args = parseArgs(process.argv.slice(2));
|
|
522
18
|
const autoConfig = resolveAutoConfig(args);
|
|
@@ -529,310 +25,30 @@ async function main() {
|
|
|
529
25
|
return;
|
|
530
26
|
}
|
|
531
27
|
if (args.command === 'analyze') {
|
|
532
|
-
|
|
533
|
-
// eslint-disable-next-line no-console
|
|
534
|
-
console.error('Error: --path is required for analyze command');
|
|
535
|
-
process.exit(1);
|
|
536
|
-
}
|
|
537
|
-
const { config } = resolveConfig(process.cwd(), autoConfig, {
|
|
538
|
-
path: args.path,
|
|
539
|
-
profile: args.profile,
|
|
540
|
-
testsRoot: args.testsRoot,
|
|
541
|
-
mode: 'impact',
|
|
542
|
-
gitSince: args.gitSince,
|
|
543
|
-
llmProvider: args.llmProvider,
|
|
544
|
-
});
|
|
545
|
-
const testsRoot = config.testsRoot || config.path;
|
|
546
|
-
const analyzeStages = [
|
|
547
|
-
'preprocess', 'impact', 'coverage',
|
|
548
|
-
];
|
|
549
|
-
if (args.analyzeGenerate) {
|
|
550
|
-
analyzeStages.push('generation');
|
|
551
|
-
}
|
|
552
|
-
if (args.analyzeHeal || args.analyzeHealReport) {
|
|
553
|
-
analyzeStages.push('heal');
|
|
554
|
-
}
|
|
555
|
-
const result = await runPipeline({
|
|
556
|
-
appPath: config.path,
|
|
557
|
-
testsRoot,
|
|
558
|
-
gitSince: args.gitSince || config.git.since,
|
|
559
|
-
routeFamilies: config.routeFamilies,
|
|
560
|
-
apiSurface: config.apiSurface,
|
|
561
|
-
stages: analyzeStages,
|
|
562
|
-
generation: args.analyzeGenerate
|
|
563
|
-
? {
|
|
564
|
-
defaultOutputDir: args.analyzeGenerateOutputDir || 'specs/functional/ai-assisted',
|
|
565
|
-
dryRun: args.dryRun,
|
|
566
|
-
}
|
|
567
|
-
: undefined,
|
|
568
|
-
heal: (args.analyzeHeal || args.analyzeHealReport)
|
|
569
|
-
? {
|
|
570
|
-
mcp: args.pipelineMcp ?? true,
|
|
571
|
-
mcpAllowFallback: args.pipelineMcpAllowFallback ?? false,
|
|
572
|
-
mcpOnly: args.pipelineMcpOnly ?? false,
|
|
573
|
-
mcpCommandTimeoutMs: args.pipelineMcpTimeoutMs,
|
|
574
|
-
mcpRetries: args.pipelineMcpRetries ?? 1,
|
|
575
|
-
dryRun: args.dryRun,
|
|
576
|
-
}
|
|
577
|
-
: undefined,
|
|
578
|
-
playwrightReportPath: args.analyzeHealReport,
|
|
579
|
-
});
|
|
580
|
-
// eslint-disable-next-line no-console
|
|
581
|
-
console.log(`Analyze report: ${result.reportPath}`);
|
|
582
|
-
// eslint-disable-next-line no-console
|
|
583
|
-
console.log(`Analyze flows identified: ${result.report.summary.flowsIdentified}`);
|
|
584
|
-
// eslint-disable-next-line no-console
|
|
585
|
-
console.log(`Analyze flows covered: ${result.report.summary.flowsCovered}`);
|
|
586
|
-
// eslint-disable-next-line no-console
|
|
587
|
-
console.log(`Analyze flows uncovered: ${result.report.summary.flowsUncovered}`);
|
|
588
|
-
// eslint-disable-next-line no-console
|
|
589
|
-
console.log(`Analyze overall confidence: ${result.report.summary.overallConfidence}`);
|
|
590
|
-
// eslint-disable-next-line no-console
|
|
591
|
-
console.log(`Analyze route families: ${result.report.summary.routeFamiliesImpacted.join(', ') || 'none'}`);
|
|
592
|
-
if (result.generated && result.generated.length > 0) {
|
|
593
|
-
const written = result.generated.filter((g) => g.written).length;
|
|
594
|
-
// eslint-disable-next-line no-console
|
|
595
|
-
console.log(`Analyze generated specs: ${result.generated.length} (written=${written})`);
|
|
596
|
-
for (const g of result.generated) {
|
|
597
|
-
// eslint-disable-next-line no-console
|
|
598
|
-
console.log(` ${g.mode}: ${g.specPath}`);
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
if (result.healResult) {
|
|
602
|
-
const healed = result.healResult.summary.results.filter((r) => r.healStatus === 'success').length;
|
|
603
|
-
const healFailed = result.healResult.summary.results.filter((r) => r.healStatus === 'failed').length;
|
|
604
|
-
// eslint-disable-next-line no-console
|
|
605
|
-
console.log(`Analyze heal targets: ${result.healResult.targets.length} (healed=${healed}, failed=${healFailed})`);
|
|
606
|
-
}
|
|
607
|
-
if (result.warnings.length > 0) {
|
|
608
|
-
// eslint-disable-next-line no-console
|
|
609
|
-
console.log(`Analyze warnings: ${result.warnings.join(' | ')}`);
|
|
610
|
-
}
|
|
28
|
+
await runAnalyzeCommand(args, autoConfig);
|
|
611
29
|
return;
|
|
612
30
|
}
|
|
613
31
|
if (args.command === 'feedback') {
|
|
614
|
-
|
|
615
|
-
// eslint-disable-next-line no-console
|
|
616
|
-
console.error('Error: --path is required for feedback command');
|
|
617
|
-
process.exit(1);
|
|
618
|
-
}
|
|
619
|
-
if (!args.feedbackInputPath) {
|
|
620
|
-
// eslint-disable-next-line no-console
|
|
621
|
-
console.error('Error: --feedback-input <path> is required for feedback command');
|
|
622
|
-
process.exit(1);
|
|
623
|
-
}
|
|
624
|
-
const { config } = resolveConfig(process.cwd(), autoConfig, {
|
|
625
|
-
path: args.path,
|
|
626
|
-
profile: args.profile,
|
|
627
|
-
testsRoot: args.testsRoot,
|
|
628
|
-
mode: 'impact',
|
|
629
|
-
llmProvider: args.llmProvider,
|
|
630
|
-
});
|
|
631
|
-
const reportRoot = config.testsRoot || config.path;
|
|
632
|
-
const raw = JSON.parse(readFileSync(args.feedbackInputPath, 'utf-8'));
|
|
633
|
-
const payload = {
|
|
634
|
-
timestamp: raw.timestamp || new Date().toISOString(),
|
|
635
|
-
runSet: raw.runSet || 'targeted',
|
|
636
|
-
recommendedTests: raw.recommendedTests || [],
|
|
637
|
-
executedTests: raw.executedTests || [],
|
|
638
|
-
failedTests: raw.failedTests || [],
|
|
639
|
-
escapedFailures: raw.escapedFailures || [],
|
|
640
|
-
};
|
|
641
|
-
const output = appendFeedbackAndRecompute(reportRoot, payload);
|
|
642
|
-
// eslint-disable-next-line no-console
|
|
643
|
-
console.log(`Feedback data: ${output.feedbackPath}`);
|
|
644
|
-
// eslint-disable-next-line no-console
|
|
645
|
-
console.log(`Calibration data: ${output.calibrationPath}`);
|
|
646
|
-
// eslint-disable-next-line no-console
|
|
647
|
-
console.log(`Calibration overall: precision=${output.calibration.overall.precision}, recall=${output.calibration.overall.recall}, fnr=${output.calibration.overall.falseNegativeRate}`);
|
|
32
|
+
runFeedbackCommand(args, autoConfig);
|
|
648
33
|
return;
|
|
649
34
|
}
|
|
650
35
|
if (args.command === 'traceability-capture') {
|
|
651
|
-
|
|
652
|
-
// eslint-disable-next-line no-console
|
|
653
|
-
console.error('Error: --path is required for traceability-capture command');
|
|
654
|
-
process.exit(1);
|
|
655
|
-
}
|
|
656
|
-
if (!args.traceabilityReportPath) {
|
|
657
|
-
// eslint-disable-next-line no-console
|
|
658
|
-
console.error('Error: --traceability-report <path> is required for traceability-capture command');
|
|
659
|
-
process.exit(1);
|
|
660
|
-
}
|
|
661
|
-
const { config } = resolveConfig(process.cwd(), autoConfig, {
|
|
662
|
-
path: args.path,
|
|
663
|
-
profile: args.profile,
|
|
664
|
-
testsRoot: args.testsRoot,
|
|
665
|
-
mode: 'impact',
|
|
666
|
-
gitSince: args.gitSince,
|
|
667
|
-
llmProvider: args.llmProvider,
|
|
668
|
-
});
|
|
669
|
-
const reportRoot = config.testsRoot || config.path;
|
|
670
|
-
const output = captureTraceabilityInput({
|
|
671
|
-
appPath: config.path,
|
|
672
|
-
testsRoot: reportRoot,
|
|
673
|
-
reportPath: args.traceabilityReportPath,
|
|
674
|
-
sinceRef: args.gitSince || config.git.since,
|
|
675
|
-
outputPath: args.traceabilityCaptureOutputPath,
|
|
676
|
-
coverageMapPath: args.traceabilityCoverageMapPath,
|
|
677
|
-
changedFilesPath: args.traceabilityChangedFilesPath,
|
|
678
|
-
});
|
|
679
|
-
// eslint-disable-next-line no-console
|
|
680
|
-
console.log(`Traceability input: ${output.outputPath}`);
|
|
681
|
-
// eslint-disable-next-line no-console
|
|
682
|
-
console.log(`Traceability tests seen: ${output.testsSeen}`);
|
|
683
|
-
// eslint-disable-next-line no-console
|
|
684
|
-
console.log(`Traceability runs generated: ${output.runsGenerated}`);
|
|
685
|
-
// eslint-disable-next-line no-console
|
|
686
|
-
console.log(`Traceability changed files used: ${output.changedFilesUsed}`);
|
|
687
|
-
if (output.warnings.length > 0) {
|
|
688
|
-
// eslint-disable-next-line no-console
|
|
689
|
-
console.log(`Traceability warnings: ${output.warnings.join(' | ')}`);
|
|
690
|
-
}
|
|
36
|
+
runTraceabilityCaptureCommand(args, autoConfig);
|
|
691
37
|
return;
|
|
692
38
|
}
|
|
693
39
|
if (args.command === 'traceability-ingest') {
|
|
694
|
-
|
|
695
|
-
// eslint-disable-next-line no-console
|
|
696
|
-
console.error('Error: --path is required for traceability-ingest command');
|
|
697
|
-
process.exit(1);
|
|
698
|
-
}
|
|
699
|
-
if (!args.traceabilityInputPath) {
|
|
700
|
-
// eslint-disable-next-line no-console
|
|
701
|
-
console.error('Error: --traceability-input <path> is required for traceability-ingest command');
|
|
702
|
-
process.exit(1);
|
|
703
|
-
}
|
|
704
|
-
const { config } = resolveConfig(process.cwd(), autoConfig, {
|
|
705
|
-
path: args.path,
|
|
706
|
-
profile: args.profile,
|
|
707
|
-
testsRoot: args.testsRoot,
|
|
708
|
-
mode: 'impact',
|
|
709
|
-
llmProvider: args.llmProvider,
|
|
710
|
-
});
|
|
711
|
-
const reportRoot = config.testsRoot || config.path;
|
|
712
|
-
const raw = JSON.parse(readFileSync(args.traceabilityInputPath, 'utf-8'));
|
|
713
|
-
const output = ingestTraceabilityInput(reportRoot, config.impact.traceability, raw, {
|
|
714
|
-
minHits: args.traceabilityMinHits,
|
|
715
|
-
maxFilesPerTest: args.traceabilityMaxFilesPerTest,
|
|
716
|
-
maxAgeDays: args.traceabilityMaxAgeDays,
|
|
717
|
-
});
|
|
718
|
-
// eslint-disable-next-line no-console
|
|
719
|
-
console.log(`Traceability manifest: ${output.manifestPath}`);
|
|
720
|
-
// eslint-disable-next-line no-console
|
|
721
|
-
console.log(`Traceability state: ${output.statePath}`);
|
|
722
|
-
// eslint-disable-next-line no-console
|
|
723
|
-
console.log(`Traceability ingested entries: ${output.entriesIngested}`);
|
|
724
|
-
// eslint-disable-next-line no-console
|
|
725
|
-
console.log(`Traceability tracked tests: ${output.testsTracked}`);
|
|
726
|
-
// eslint-disable-next-line no-console
|
|
727
|
-
console.log(`Traceability tracked edges: ${output.edgesTracked}`);
|
|
728
|
-
if (output.warnings.length > 0) {
|
|
729
|
-
// eslint-disable-next-line no-console
|
|
730
|
-
console.log(`Traceability warnings: ${output.warnings.join(' | ')}`);
|
|
731
|
-
}
|
|
40
|
+
runTraceabilityIngestCommand(args, autoConfig);
|
|
732
41
|
return;
|
|
733
42
|
}
|
|
734
43
|
if (args.command === 'finalize-generated-tests') {
|
|
735
|
-
|
|
736
|
-
// eslint-disable-next-line no-console
|
|
737
|
-
console.error('Error: --path is required for finalize-generated-tests command');
|
|
738
|
-
process.exit(1);
|
|
739
|
-
}
|
|
740
|
-
const { config } = resolveConfig(process.cwd(), autoConfig, {
|
|
741
|
-
path: args.path,
|
|
742
|
-
profile: args.profile,
|
|
743
|
-
testsRoot: args.testsRoot,
|
|
744
|
-
mode: 'gap',
|
|
745
|
-
llmProvider: args.llmProvider,
|
|
746
|
-
});
|
|
747
|
-
const result = finalizeGeneratedTests({
|
|
748
|
-
appPath: config.path,
|
|
749
|
-
testsRoot: config.testsRoot || config.path,
|
|
750
|
-
branch: args.branch,
|
|
751
|
-
commitMessage: args.commitMessage,
|
|
752
|
-
createPr: args.createPr,
|
|
753
|
-
prTitle: args.prTitle,
|
|
754
|
-
prBody: args.prBody,
|
|
755
|
-
baseBranch: args.prBase,
|
|
756
|
-
dryRun: args.dryRun,
|
|
757
|
-
});
|
|
758
|
-
// eslint-disable-next-line no-console
|
|
759
|
-
console.log(`Finalize repo root: ${result.repoRoot}`);
|
|
760
|
-
// eslint-disable-next-line no-console
|
|
761
|
-
console.log(`Finalize branch: ${result.branch}`);
|
|
762
|
-
// eslint-disable-next-line no-console
|
|
763
|
-
console.log(`Finalize staged paths: ${result.stagedPaths.join(', ') || 'none'}`);
|
|
764
|
-
// eslint-disable-next-line no-console
|
|
765
|
-
console.log(`Finalize commit: ${result.committed ? 'created' : 'skipped'}`);
|
|
766
|
-
if (result.commitSha) {
|
|
767
|
-
// eslint-disable-next-line no-console
|
|
768
|
-
console.log(`Finalize commit sha: ${result.commitSha}`);
|
|
769
|
-
}
|
|
770
|
-
if (result.prUrl) {
|
|
771
|
-
// eslint-disable-next-line no-console
|
|
772
|
-
console.log(`Finalize PR: ${result.prUrl}`);
|
|
773
|
-
}
|
|
44
|
+
runFinalizeCommand(args, autoConfig);
|
|
774
45
|
return;
|
|
775
46
|
}
|
|
776
47
|
if (args.command === 'heal') {
|
|
777
|
-
|
|
778
|
-
// eslint-disable-next-line no-console
|
|
779
|
-
console.error('Error: --path is required for heal command');
|
|
780
|
-
process.exit(1);
|
|
781
|
-
}
|
|
782
|
-
if (!args.traceabilityReportPath) {
|
|
783
|
-
// eslint-disable-next-line no-console
|
|
784
|
-
console.error('Error: --traceability-report <path> is required for heal command');
|
|
785
|
-
process.exit(1);
|
|
786
|
-
}
|
|
787
|
-
const { config } = resolveConfig(process.cwd(), autoConfig, {
|
|
788
|
-
path: args.path,
|
|
789
|
-
profile: args.profile,
|
|
790
|
-
testsRoot: args.testsRoot,
|
|
791
|
-
mode: 'gap',
|
|
792
|
-
framework: args.framework,
|
|
793
|
-
pipeline: {
|
|
794
|
-
enabled: true,
|
|
795
|
-
scenarios: args.pipelineScenarios,
|
|
796
|
-
outputDir: args.pipelineOutput,
|
|
797
|
-
baseUrl: args.pipelineBaseUrl,
|
|
798
|
-
browser: args.pipelineBrowser,
|
|
799
|
-
headless: args.pipelineHeadless,
|
|
800
|
-
project: args.pipelineProject,
|
|
801
|
-
parallel: args.pipelineParallel,
|
|
802
|
-
dryRun: args.pipelineDryRun,
|
|
803
|
-
mcp: args.pipelineMcp,
|
|
804
|
-
mcpAllowFallback: args.pipelineMcpAllowFallback,
|
|
805
|
-
mcpOnly: args.pipelineMcpOnly,
|
|
806
|
-
},
|
|
807
|
-
llmProvider: args.llmProvider,
|
|
808
|
-
});
|
|
809
|
-
const reportRoot = config.testsRoot || config.path;
|
|
810
|
-
const unstableSpecs = extractPlaywrightUnstableSpecs(args.traceabilityReportPath, [reportRoot, config.path]);
|
|
811
|
-
if (unstableSpecs.length === 0) {
|
|
812
|
-
// eslint-disable-next-line no-console
|
|
813
|
-
console.log('Heal targeted unstable specs: 0');
|
|
814
|
-
return;
|
|
815
|
-
}
|
|
816
|
-
const targetedSummary = runTargetedSpecHeal(reportRoot, unstableSpecs.map((spec) => ({
|
|
817
|
-
specPath: spec.specPath,
|
|
818
|
-
status: spec.status,
|
|
819
|
-
reason: `Playwright report: failingTests=${spec.failingTests}, flakyTests=${spec.flakyTests}`,
|
|
820
|
-
})), {
|
|
821
|
-
...config.pipeline,
|
|
822
|
-
enabled: true,
|
|
823
|
-
heal: true,
|
|
824
|
-
});
|
|
825
|
-
const healedCount = targetedSummary.results.filter((result) => result.healStatus === 'success').length;
|
|
826
|
-
// eslint-disable-next-line no-console
|
|
827
|
-
console.log(`Heal targeted unstable specs: ${unstableSpecs.length} (healed=${healedCount})`);
|
|
828
|
-
if (targetedSummary.warnings.length > 0) {
|
|
829
|
-
// eslint-disable-next-line no-console
|
|
830
|
-
console.log(`Heal warnings: ${targetedSummary.warnings.join(' | ')}`);
|
|
831
|
-
}
|
|
48
|
+
runHealCommand(args, autoConfig);
|
|
832
49
|
return;
|
|
833
50
|
}
|
|
834
51
|
if (!args.path && !autoConfig) {
|
|
835
|
-
// eslint-disable-next-line no-console
|
|
836
52
|
console.error('Error: --path is required (or provide a config file with path set)');
|
|
837
53
|
printUsage();
|
|
838
54
|
process.exit(1);
|
|
@@ -890,272 +106,22 @@ async function main() {
|
|
|
890
106
|
: undefined,
|
|
891
107
|
});
|
|
892
108
|
if (args.command === 'impact') {
|
|
893
|
-
|
|
894
|
-
const gitResult = getChangedFiles(config.path, config.git.since, { includeUncommitted: config.git.includeUncommitted });
|
|
895
|
-
const impactResult = analyzeImpactV2(gitResult.files, {
|
|
896
|
-
testsRoot: reportRoot,
|
|
897
|
-
routeFamilies: config.routeFamilies,
|
|
898
|
-
});
|
|
899
|
-
// eslint-disable-next-line no-console
|
|
900
|
-
console.log(`Impact: ${impactResult.changedFiles.length} changed files → ${impactResult.impactedFeatures.length} features impacted`);
|
|
901
|
-
// eslint-disable-next-line no-console
|
|
902
|
-
console.log(`Unbound files: ${impactResult.unboundFiles.length}`);
|
|
903
|
-
for (const f of impactResult.impactedFeatures) {
|
|
904
|
-
const label = f.featureId || f.familyId;
|
|
905
|
-
// eslint-disable-next-line no-console
|
|
906
|
-
console.log(` [${f.priority}] ${label}: ${f.coverageStatus} (PW=${f.playwrightSpecs.length}, Cy=${f.cypressSpecs.length})`);
|
|
907
|
-
}
|
|
908
|
-
if (impactResult.warnings.length > 0) {
|
|
909
|
-
for (const w of impactResult.warnings) {
|
|
910
|
-
// eslint-disable-next-line no-console
|
|
911
|
-
console.warn(` Warning: ${w}`);
|
|
912
|
-
}
|
|
913
|
-
}
|
|
109
|
+
runImpactCommand(args, config);
|
|
914
110
|
return;
|
|
915
111
|
}
|
|
916
112
|
if (args.command === 'suggest' || args.command === 'plan') {
|
|
917
|
-
|
|
918
|
-
const apiOptions = {
|
|
919
|
-
cwd: process.cwd(),
|
|
920
|
-
configPath: autoConfig,
|
|
921
|
-
path: args.path,
|
|
922
|
-
profile: args.profile,
|
|
923
|
-
testsRoot: args.testsRoot,
|
|
924
|
-
gitSince: args.gitSince,
|
|
925
|
-
llmProvider: args.llmProvider,
|
|
926
|
-
policy: args.policyMinConfidence !== undefined ||
|
|
927
|
-
args.policySafeMergeConfidence !== undefined ||
|
|
928
|
-
args.policyWarningsThreshold !== undefined ||
|
|
929
|
-
(args.policyRiskyPatterns && args.policyRiskyPatterns.length > 0) ||
|
|
930
|
-
args.policyEnforcementMode !== undefined ||
|
|
931
|
-
(args.policyBlockActions && args.policyBlockActions.length > 0)
|
|
932
|
-
? {
|
|
933
|
-
minConfidenceForTargeted: args.policyMinConfidence,
|
|
934
|
-
safeMergeMinConfidence: args.policySafeMergeConfidence,
|
|
935
|
-
forceFullOnWarningsAtOrAbove: args.policyWarningsThreshold,
|
|
936
|
-
riskyFilePatterns: args.policyRiskyPatterns,
|
|
937
|
-
enforcementMode: args.policyEnforcementMode,
|
|
938
|
-
blockOnActions: args.policyBlockActions,
|
|
939
|
-
}
|
|
940
|
-
: undefined,
|
|
941
|
-
};
|
|
942
|
-
let result;
|
|
943
|
-
if (args.noAi) {
|
|
944
|
-
result = recommendTestsDeterministic(apiOptions);
|
|
945
|
-
}
|
|
946
|
-
else {
|
|
947
|
-
result = await recommendTestsAI(apiOptions);
|
|
948
|
-
if (result.aiEnrichment) {
|
|
949
|
-
const { aiEnrichment } = result;
|
|
950
|
-
// eslint-disable-next-line no-console
|
|
951
|
-
console.log(`AI enrichment: ${aiEnrichment.enrichedFeatures.length} features enriched (${aiEnrichment.tokenUsage.input + aiEnrichment.tokenUsage.output} tokens)`);
|
|
952
|
-
}
|
|
953
|
-
else if (!process.env.ANTHROPIC_API_KEY) {
|
|
954
|
-
// eslint-disable-next-line no-console
|
|
955
|
-
console.log('Tip: set ANTHROPIC_API_KEY to enable AI-powered enrichment');
|
|
956
|
-
}
|
|
957
|
-
}
|
|
958
|
-
const { plan, planPath, ciSummaryMarkdown, ciSummaryPath } = result;
|
|
959
|
-
// Write CI summary to an additional path if --ci-comment-path was specified
|
|
960
|
-
if (args.ciCommentPath) {
|
|
961
|
-
writeCiSummary(reportRoot, ciSummaryMarkdown, args.ciCommentPath);
|
|
962
|
-
}
|
|
963
|
-
const summaryPath = ciSummaryPath;
|
|
964
|
-
// Compute metrics paths (api already wrote metrics; derive paths for GHA output)
|
|
965
|
-
const metricsEventsPath = join(reportRoot, '.e2e-ai-agents/metrics.jsonl');
|
|
966
|
-
const metricsSummaryPath = join(reportRoot, '.e2e-ai-agents/metrics-summary.json');
|
|
967
|
-
const ghaOutput = args.githubOutputPath || process.env.GITHUB_OUTPUT;
|
|
968
|
-
if (ghaOutput) {
|
|
969
|
-
appendFileSync(ghaOutput, `run_set=${plan.runSet}\n`);
|
|
970
|
-
appendFileSync(ghaOutput, `action=${plan.decision.action}\n`);
|
|
971
|
-
appendFileSync(ghaOutput, `confidence=${plan.confidence}\n`);
|
|
972
|
-
appendFileSync(ghaOutput, `enforcement_mode=${plan.enforcement.mode}\n`);
|
|
973
|
-
appendFileSync(ghaOutput, `enforcement_should_fail=${plan.enforcement.shouldFail}\n`);
|
|
974
|
-
appendFileSync(ghaOutput, `recommended_tests_count=${plan.recommendedTests.length}\n`);
|
|
975
|
-
appendFileSync(ghaOutput, `required_new_tests_count=${plan.requiredNewTests.length}\n`);
|
|
976
|
-
appendFileSync(ghaOutput, `plan_path=${planPath}\n`);
|
|
977
|
-
appendFileSync(ghaOutput, `summary_path=${summaryPath}\n`);
|
|
978
|
-
appendFileSync(ghaOutput, `metrics_events_path=${metricsEventsPath}\n`);
|
|
979
|
-
appendFileSync(ghaOutput, `metrics_summary_path=${metricsSummaryPath}\n`);
|
|
980
|
-
}
|
|
981
|
-
// eslint-disable-next-line no-console
|
|
982
|
-
console.log(`Suggested run set: ${plan.runSet} (confidence ${plan.confidence})`);
|
|
983
|
-
// eslint-disable-next-line no-console
|
|
984
|
-
console.log(`Decision: ${plan.decision.action} - ${plan.decision.summary}`);
|
|
985
|
-
// eslint-disable-next-line no-console
|
|
986
|
-
console.log(`Enforcement: ${plan.enforcement.mode} (shouldFail=${plan.enforcement.shouldFail})`);
|
|
987
|
-
// eslint-disable-next-line no-console
|
|
988
|
-
console.log(`Plan data: ${planPath}`);
|
|
989
|
-
// eslint-disable-next-line no-console
|
|
990
|
-
console.log(`CI summary: ${summaryPath}`);
|
|
991
|
-
// eslint-disable-next-line no-console
|
|
992
|
-
console.log(`Plan metrics: ${metricsSummaryPath}`);
|
|
993
|
-
const failOnLegacyFlag = args.failOnMustAddTests && plan.decision.action === 'must-add-tests';
|
|
994
|
-
if (failOnLegacyFlag || plan.enforcement.shouldFail) {
|
|
995
|
-
process.exit(2);
|
|
996
|
-
}
|
|
113
|
+
await runPlanCommand(args, autoConfig, config);
|
|
997
114
|
return;
|
|
998
115
|
}
|
|
999
116
|
if (args.command === 'generate') {
|
|
1000
|
-
|
|
1001
|
-
// Load scenarios from --scenarios flag or plan-report.json
|
|
1002
|
-
let scenarios = [];
|
|
1003
|
-
if (args.generateScenarios) {
|
|
1004
|
-
let raw;
|
|
1005
|
-
if (existsSync(args.generateScenarios)) {
|
|
1006
|
-
raw = JSON.parse(readFileSync(args.generateScenarios, 'utf-8'));
|
|
1007
|
-
}
|
|
1008
|
-
else {
|
|
1009
|
-
raw = JSON.parse(args.generateScenarios);
|
|
1010
|
-
}
|
|
1011
|
-
if (!Array.isArray(raw)) {
|
|
1012
|
-
// eslint-disable-next-line no-console
|
|
1013
|
-
console.error('--scenarios must be a JSON array of ScenarioInput objects.');
|
|
1014
|
-
process.exit(1);
|
|
1015
|
-
}
|
|
1016
|
-
for (const item of raw) {
|
|
1017
|
-
if (!item.id || !item.name || !Array.isArray(item.scenarios) || !item.routeFamily || !item.priority) {
|
|
1018
|
-
// eslint-disable-next-line no-console
|
|
1019
|
-
console.error(`Invalid scenario: each must have id, name, scenarios[], routeFamily, priority.`);
|
|
1020
|
-
process.exit(1);
|
|
1021
|
-
}
|
|
1022
|
-
}
|
|
1023
|
-
scenarios = raw;
|
|
1024
|
-
}
|
|
1025
|
-
else {
|
|
1026
|
-
// Try plan.json first (written by plan/suggest command), then plan-report.json (legacy)
|
|
1027
|
-
const planJsonPath = join(reportRoot, '.e2e-ai-agents', 'plan.json');
|
|
1028
|
-
const planReportPath = join(reportRoot, '.e2e-ai-agents', 'plan-report.json');
|
|
1029
|
-
const resolvedPlanPath = existsSync(planJsonPath) ? planJsonPath : existsSync(planReportPath) ? planReportPath : null;
|
|
1030
|
-
if (!resolvedPlanPath) {
|
|
1031
|
-
// eslint-disable-next-line no-console
|
|
1032
|
-
console.error('No plan report found. Run `plan` first or pass --scenarios.');
|
|
1033
|
-
process.exit(1);
|
|
1034
|
-
}
|
|
1035
|
-
const planReport = JSON.parse(readFileSync(resolvedPlanPath, 'utf-8'));
|
|
1036
|
-
scenarios = (planReport.gapDetails || []).map((gap) => ({
|
|
1037
|
-
id: gap.id,
|
|
1038
|
-
name: gap.id,
|
|
1039
|
-
scenarios: gap.missingScenarios || gap.reasons || ['Verify core user flow'],
|
|
1040
|
-
routeFamily: gap.id.split('.')[0] || gap.id,
|
|
1041
|
-
priority: 'P1',
|
|
1042
|
-
}));
|
|
1043
|
-
}
|
|
1044
|
-
if (scenarios.length === 0) {
|
|
1045
|
-
// eslint-disable-next-line no-console
|
|
1046
|
-
console.log('No scenarios to generate tests for.');
|
|
1047
|
-
return;
|
|
1048
|
-
}
|
|
1049
|
-
let apiSurface;
|
|
1050
|
-
try {
|
|
1051
|
-
apiSurface = loadOrBuildApiSurface(reportRoot, config.apiSurface);
|
|
1052
|
-
}
|
|
1053
|
-
catch {
|
|
1054
|
-
// eslint-disable-next-line no-console
|
|
1055
|
-
console.warn('Could not load API surface catalog. Generation will use generic selectors.');
|
|
1056
|
-
}
|
|
1057
|
-
const provider = await LLMProviderFactory.createFromEnv();
|
|
1058
|
-
// eslint-disable-next-line no-console
|
|
1059
|
-
console.log(`Generating tests for ${scenarios.length} scenario(s)...`);
|
|
1060
|
-
const summary = await runAgenticGeneration({
|
|
1061
|
-
scenarios,
|
|
1062
|
-
config: {
|
|
1063
|
-
maxAttempts: args.maxAttempts || 3,
|
|
1064
|
-
project: args.pipelineProject || 'chrome',
|
|
1065
|
-
baseUrl: args.pipelineBaseUrl,
|
|
1066
|
-
testTimeoutMs: 120000,
|
|
1067
|
-
testsRoot: reportRoot,
|
|
1068
|
-
dryRun: args.dryRun,
|
|
1069
|
-
},
|
|
1070
|
-
provider,
|
|
1071
|
-
apiSurface,
|
|
1072
|
-
});
|
|
1073
|
-
// eslint-disable-next-line no-console
|
|
1074
|
-
console.log(`\nAgentic Generation Summary:`);
|
|
1075
|
-
// eslint-disable-next-line no-console
|
|
1076
|
-
console.log(` Generated: ${summary.totalGenerated}`);
|
|
1077
|
-
// eslint-disable-next-line no-console
|
|
1078
|
-
console.log(` Passed: ${summary.totalPassed}`);
|
|
1079
|
-
// eslint-disable-next-line no-console
|
|
1080
|
-
console.log(` Failed: ${summary.totalFailed}`);
|
|
1081
|
-
// eslint-disable-next-line no-console
|
|
1082
|
-
console.log(` Attempts: ${summary.totalAttempts}`);
|
|
1083
|
-
// eslint-disable-next-line no-console
|
|
1084
|
-
console.log(` Duration: ${(summary.durationMs / 1000).toFixed(1)}s`);
|
|
1085
|
-
for (const result of summary.results) {
|
|
1086
|
-
const icon = result.status === 'passed' ? 'PASS' : result.status === 'skipped' ? 'SKIP' : 'FAIL';
|
|
1087
|
-
// eslint-disable-next-line no-console
|
|
1088
|
-
console.log(` [${icon}] ${result.scenarioSource} (${result.attempts} attempts)`);
|
|
1089
|
-
if (result.status === 'passed' || result.status === 'skipped') {
|
|
1090
|
-
// eslint-disable-next-line no-console
|
|
1091
|
-
console.log(` ${result.specPath}`);
|
|
1092
|
-
}
|
|
1093
|
-
}
|
|
1094
|
-
if (summary.warnings.length > 0) {
|
|
1095
|
-
// eslint-disable-next-line no-console
|
|
1096
|
-
console.log(`\nWarnings:`);
|
|
1097
|
-
for (const w of summary.warnings) {
|
|
1098
|
-
// eslint-disable-next-line no-console
|
|
1099
|
-
console.warn(` - ${w}`);
|
|
1100
|
-
}
|
|
1101
|
-
}
|
|
1102
|
-
const summaryDir = join(reportRoot, '.e2e-ai-agents');
|
|
1103
|
-
if (!existsSync(summaryDir)) {
|
|
1104
|
-
mkdirSync(summaryDir, { recursive: true });
|
|
1105
|
-
}
|
|
1106
|
-
const summaryPath = join(summaryDir, 'agentic-summary.json');
|
|
1107
|
-
writeFileSync(summaryPath, JSON.stringify(summary, null, 2), 'utf-8');
|
|
1108
|
-
// eslint-disable-next-line no-console
|
|
1109
|
-
console.log(`\nReport: ${summaryPath}`);
|
|
1110
|
-
if (summary.totalFailed > 0) {
|
|
1111
|
-
process.exit(1);
|
|
1112
|
-
}
|
|
117
|
+
await runGenerateCommand(args, config);
|
|
1113
118
|
return;
|
|
1114
119
|
}
|
|
1115
|
-
// eslint-disable-next-line no-console
|
|
1116
120
|
console.error(`Unknown command: ${args.command}`);
|
|
1117
121
|
printUsage();
|
|
1118
122
|
process.exit(1);
|
|
1119
123
|
}
|
|
1120
|
-
async function runLlmHealth() {
|
|
1121
|
-
if (!process.env.ANTHROPIC_API_KEY) {
|
|
1122
|
-
// eslint-disable-next-line no-console
|
|
1123
|
-
console.error('ANTHROPIC_API_KEY is required for llm-health.');
|
|
1124
|
-
process.exit(1);
|
|
1125
|
-
}
|
|
1126
|
-
const model = process.env.ANTHROPIC_MODEL || 'claude-sonnet-4-5-20250929';
|
|
1127
|
-
const provider = new AnthropicProvider({
|
|
1128
|
-
apiKey: process.env.ANTHROPIC_API_KEY,
|
|
1129
|
-
model,
|
|
1130
|
-
});
|
|
1131
|
-
try {
|
|
1132
|
-
const response = await provider.generateText('Reply with OK.', { maxTokens: 8, timeout: 15000 });
|
|
1133
|
-
const text = response.text.trim();
|
|
1134
|
-
// eslint-disable-next-line no-console
|
|
1135
|
-
console.log(`Anthropic OK (${model}) -> ${text}`);
|
|
1136
|
-
}
|
|
1137
|
-
catch (error) {
|
|
1138
|
-
if (error instanceof LLMProviderError) {
|
|
1139
|
-
// eslint-disable-next-line no-console
|
|
1140
|
-
console.error(`Anthropic failed: ${error.message}`);
|
|
1141
|
-
if (error.cause instanceof Error) {
|
|
1142
|
-
// eslint-disable-next-line no-console
|
|
1143
|
-
console.error(`Cause: ${error.cause.message}`);
|
|
1144
|
-
}
|
|
1145
|
-
}
|
|
1146
|
-
else if (error instanceof Error) {
|
|
1147
|
-
// eslint-disable-next-line no-console
|
|
1148
|
-
console.error(`Anthropic failed: ${error.message}`);
|
|
1149
|
-
}
|
|
1150
|
-
else {
|
|
1151
|
-
// eslint-disable-next-line no-console
|
|
1152
|
-
console.error(`Anthropic failed: ${String(error)}`);
|
|
1153
|
-
}
|
|
1154
|
-
process.exit(1);
|
|
1155
|
-
}
|
|
1156
|
-
}
|
|
1157
124
|
main().catch((error) => {
|
|
1158
|
-
// eslint-disable-next-line no-console
|
|
1159
125
|
console.error(error instanceof Error ? error.message : String(error));
|
|
1160
126
|
process.exit(1);
|
|
1161
127
|
});
|