@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.
Files changed (93) hide show
  1. package/README.md +112 -584
  2. package/dist/agent/api_catalog.d.ts +11 -0
  3. package/dist/agent/api_catalog.d.ts.map +1 -0
  4. package/dist/agent/api_catalog.js +210 -0
  5. package/dist/agent/llm_agents_flow.d.ts +15 -0
  6. package/dist/agent/llm_agents_flow.d.ts.map +1 -0
  7. package/dist/agent/llm_agents_flow.js +434 -0
  8. package/dist/agent/native_flow.d.ts +6 -0
  9. package/dist/agent/native_flow.d.ts.map +1 -0
  10. package/dist/agent/native_flow.js +179 -0
  11. package/dist/agent/pipeline.d.ts +2 -25
  12. package/dist/agent/pipeline.d.ts.map +1 -1
  13. package/dist/agent/pipeline.js +30 -1329
  14. package/dist/agent/pipeline_types.d.ts +54 -0
  15. package/dist/agent/pipeline_types.d.ts.map +1 -0
  16. package/dist/agent/pipeline_types.js +4 -0
  17. package/dist/agent/pipeline_utils.d.ts +12 -0
  18. package/dist/agent/pipeline_utils.d.ts.map +1 -0
  19. package/dist/agent/pipeline_utils.js +156 -0
  20. package/dist/agent/process_runner.d.ts +10 -0
  21. package/dist/agent/process_runner.d.ts.map +1 -0
  22. package/dist/agent/process_runner.js +92 -0
  23. package/dist/agent/spec_generator.d.ts +5 -0
  24. package/dist/agent/spec_generator.d.ts.map +1 -0
  25. package/dist/agent/spec_generator.js +253 -0
  26. package/dist/agent/validation_runner.d.ts +5 -0
  27. package/dist/agent/validation_runner.d.ts.map +1 -0
  28. package/dist/agent/validation_runner.js +77 -0
  29. package/dist/agentic/playwright_runner.js +1 -1
  30. package/dist/cli/commands/analyze.d.ts +3 -0
  31. package/dist/cli/commands/analyze.d.ts.map +1 -0
  32. package/dist/cli/commands/analyze.js +77 -0
  33. package/dist/cli/commands/feedback.d.ts +3 -0
  34. package/dist/cli/commands/feedback.d.ts.map +1 -0
  35. package/dist/cli/commands/feedback.js +39 -0
  36. package/dist/cli/commands/finalize.d.ts +3 -0
  37. package/dist/cli/commands/finalize.d.ts.map +1 -0
  38. package/dist/cli/commands/finalize.js +41 -0
  39. package/dist/cli/commands/generate.d.ts +4 -0
  40. package/dist/cli/commands/generate.d.ts.map +1 -0
  41. package/dist/cli/commands/generate.js +108 -0
  42. package/dist/cli/commands/heal.d.ts +3 -0
  43. package/dist/cli/commands/heal.d.ts.map +1 -0
  44. package/dist/cli/commands/heal.js +60 -0
  45. package/dist/cli/commands/impact.d.ts +4 -0
  46. package/dist/cli/commands/impact.d.ts.map +1 -0
  47. package/dist/cli/commands/impact.js +26 -0
  48. package/dist/cli/commands/llm_health.d.ts +2 -0
  49. package/dist/cli/commands/llm_health.d.ts.map +1 -0
  50. package/dist/cli/commands/llm_health.js +38 -0
  51. package/dist/cli/commands/plan.d.ts +4 -0
  52. package/dist/cli/commands/plan.d.ts.map +1 -0
  53. package/dist/cli/commands/plan.js +83 -0
  54. package/dist/cli/commands/traceability.d.ts +4 -0
  55. package/dist/cli/commands/traceability.d.ts.map +1 -0
  56. package/dist/cli/commands/traceability.js +77 -0
  57. package/dist/cli/parse_args.d.ts +6 -0
  58. package/dist/cli/parse_args.d.ts.map +1 -0
  59. package/dist/cli/parse_args.js +216 -0
  60. package/dist/cli/types.d.ts +70 -0
  61. package/dist/cli/types.d.ts.map +1 -0
  62. package/dist/cli/types.js +4 -0
  63. package/dist/cli/usage.d.ts +2 -0
  64. package/dist/cli/usage.d.ts.map +1 -0
  65. package/dist/cli/usage.js +86 -0
  66. package/dist/cli.js +26 -1060
  67. package/dist/esm/agent/api_catalog.js +199 -0
  68. package/dist/esm/agent/llm_agents_flow.js +421 -0
  69. package/dist/esm/agent/native_flow.js +175 -0
  70. package/dist/esm/agent/pipeline.js +8 -1307
  71. package/dist/esm/agent/pipeline_types.js +3 -0
  72. package/dist/esm/agent/pipeline_utils.js +146 -0
  73. package/dist/esm/agent/process_runner.js +83 -0
  74. package/dist/esm/agent/spec_generator.js +249 -0
  75. package/dist/esm/agent/validation_runner.js +73 -0
  76. package/dist/esm/agentic/playwright_runner.js +1 -1
  77. package/dist/esm/cli/commands/analyze.js +74 -0
  78. package/dist/esm/cli/commands/feedback.js +36 -0
  79. package/dist/esm/cli/commands/finalize.js +38 -0
  80. package/dist/esm/cli/commands/generate.js +105 -0
  81. package/dist/esm/cli/commands/heal.js +57 -0
  82. package/dist/esm/cli/commands/impact.js +23 -0
  83. package/dist/esm/cli/commands/llm_health.js +35 -0
  84. package/dist/esm/cli/commands/plan.js +80 -0
  85. package/dist/esm/cli/commands/traceability.js +73 -0
  86. package/dist/esm/cli/parse_args.js +210 -0
  87. package/dist/esm/cli/types.js +3 -0
  88. package/dist/esm/cli/usage.js +83 -0
  89. package/dist/esm/cli.js +20 -1054
  90. package/dist/esm/mcp-server.js +18 -1
  91. package/dist/mcp-server.d.ts.map +1 -1
  92. package/dist/mcp-server.js +17 -0
  93. 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 { AnthropicProvider } from './anthropic_provider.js';
8
- import { LLMProviderError } from './provider_interface.js';
9
- import { analyzeImpact as analyzeImpactV2 } from './engine/impact_engine.js';
10
- import { writeCiSummary } from './engine/plan_builder.js';
11
- import { getChangedFiles } from './agent/git.js';
12
- import { recommendTestsAI, recommendTestsDeterministic } from './api.js';
13
- import { appendFeedbackAndRecompute } from './agent/feedback.js';
14
- import { finalizeGeneratedTests } from './agent/handoff.js';
15
- import { ingestTraceabilityInput } from './agent/traceability_ingest.js';
16
- import { captureTraceabilityInput } from './agent/traceability_capture.js';
17
- import { runTargetedSpecHeal } from './agent/pipeline.js';
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
- if (!args.path && !autoConfig) {
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
- if (!args.path && !autoConfig) {
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
- if (!args.path && !autoConfig) {
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
- if (!args.path && !autoConfig) {
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
- if (!args.path && !autoConfig) {
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
- if (!args.path && !autoConfig) {
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
- const reportRoot = config.testsRoot || config.path;
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
- const reportRoot = config.testsRoot || config.path;
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
- const reportRoot = config.testsRoot || config.path;
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
  });