@matware/e2e-runner 1.0.2 → 1.1.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 CHANGED
@@ -128,6 +128,14 @@ npx e2e-runner pool start # Start Chrome container
128
128
  npx e2e-runner pool stop # Stop Chrome container
129
129
  npx e2e-runner pool status # Check pool health
130
130
 
131
+ # Issue-to-test
132
+ npx e2e-runner issue <url> # Fetch issue details
133
+ npx e2e-runner issue <url> --generate # Generate test file via AI
134
+ npx e2e-runner issue <url> --verify # Generate + run + report
135
+
136
+ # Dashboard
137
+ npx e2e-runner dashboard # Start web dashboard
138
+
131
139
  # Other
132
140
  npx e2e-runner list # List available suites
133
141
  npx e2e-runner init # Scaffold project
@@ -150,6 +158,7 @@ npx e2e-runner init # Scaffold project
150
158
  | `--env <name>` | `default` | Environment profile |
151
159
  | `--pool-port <port>` | `3333` | Chrome pool port |
152
160
  | `--max-sessions <n>` | `10` | Max concurrent Chrome sessions |
161
+ | `--project-name <name>` | dir name | Project display name for dashboard |
153
162
 
154
163
  ## Configuration
155
164
 
@@ -201,6 +210,9 @@ When `--env <name>` is set, the matching profile from `environments` overrides e
201
210
  | `TEST_TIMEOUT` | `testTimeout` |
202
211
  | `OUTPUT_FORMAT` | `outputFormat` |
203
212
  | `E2E_ENV` | `env` |
213
+ | `PROJECT_NAME` | `projectName` |
214
+ | `ANTHROPIC_API_KEY` | `anthropicApiKey` |
215
+ | `ANTHROPIC_MODEL` | `anthropicModel` |
204
216
 
205
217
  ## Hooks
206
218
 
@@ -342,12 +354,14 @@ claude mcp add --transport stdio --scope user e2e-runner \
342
354
  | `e2e_list` | List available test suites with test names and counts |
343
355
  | `e2e_create_test` | Create a new test JSON file |
344
356
  | `e2e_pool_status` | Check Chrome pool availability and capacity |
345
- | `e2e_pool_start` | Start the Chrome pool Docker container |
346
- | `e2e_pool_stop` | Stop the Chrome pool |
357
+ | `e2e_screenshot` | Retrieve a screenshot by its hash (e.g. `ss:a3f2b1c9`) |
358
+ | `e2e_issue` | Fetch a GitHub/GitLab issue and generate E2E tests |
359
+
360
+ > **Note:** Pool start/stop are only available via CLI (`e2e-runner pool start|stop`), not via MCP — restarting the pool kills all active sessions from other clients.
347
361
 
348
362
  All tools accept an optional `cwd` parameter (absolute path to the project root). Claude Code passes its current working directory so the MCP server resolves `e2e/tests/`, `e2e.config.js`, and `.e2e-pool/` relative to the correct project — even when switching between multiple projects in the same session.
349
363
 
350
- Once installed, Claude Code can run tests, analyze failures, create new test files, and manage the Chrome pool as part of its normal workflow. Just ask:
364
+ Once installed, Claude Code can run tests, analyze failures, and create new test files as part of its normal workflow. Just ask:
351
365
 
352
366
  > "Run all E2E tests"
353
367
  > "Create a test that verifies the checkout flow"
@@ -360,6 +374,70 @@ claude mcp list
360
374
  # e2e-runner: ... - Connected
361
375
  ```
362
376
 
377
+ ## Issue-to-Test
378
+
379
+ Turn GitHub and GitLab issues into executable E2E tests. Paste an issue URL and get runnable tests -- automatically.
380
+
381
+ ### How It Works
382
+
383
+ 1. **Fetch** -- Pulls issue details (title, body, labels) via `gh` or `glab` CLI
384
+ 2. **Generate** -- AI creates JSON test actions based on the issue description
385
+ 3. **Run** -- Optionally executes the tests immediately to verify if a bug is reproducible
386
+
387
+ ### Two Modes
388
+
389
+ **Prompt mode** (default, no API key): Returns issue data + a structured prompt. Claude Code uses its own intelligence to create tests via `e2e_create_test` and run them.
390
+
391
+ **Verify mode** (requires `ANTHROPIC_API_KEY`): Calls Claude API directly, generates tests, runs them, and reports whether the bug is confirmed or not reproducible.
392
+
393
+ ### CLI
394
+
395
+ ```bash
396
+ # Fetch and display issue details
397
+ e2e-runner issue https://github.com/owner/repo/issues/42
398
+
399
+ # Generate a test file via Claude API
400
+ e2e-runner issue https://github.com/owner/repo/issues/42 --generate
401
+ # -> Creates e2e/tests/issue-42.json
402
+
403
+ # Generate + run + report bug status
404
+ e2e-runner issue https://github.com/owner/repo/issues/42 --verify
405
+ # -> "BUG CONFIRMED" or "NOT REPRODUCIBLE"
406
+
407
+ # Output AI prompt as JSON (for piping)
408
+ e2e-runner issue https://github.com/owner/repo/issues/42 --prompt
409
+ ```
410
+
411
+ ### MCP
412
+
413
+ In Claude Code, the `e2e_issue` tool handles everything:
414
+
415
+ > "Fetch issue https://github.com/owner/repo/issues/42 and create E2E tests for it"
416
+
417
+ Claude Code receives the issue data, generates appropriate test actions, saves them via `e2e_create_test`, and runs them with `e2e_run`.
418
+
419
+ ### Auth Requirements
420
+
421
+ - **GitHub**: `gh` CLI authenticated (`gh auth login`)
422
+ - **GitLab**: `glab` CLI authenticated (`glab auth login`)
423
+
424
+ Provider is auto-detected from the URL. Self-hosted GitLab is supported via `glab` config.
425
+
426
+ ### Bug Verification Logic
427
+
428
+ Generated tests assert the **correct** behavior. If the tests fail, the correct behavior doesn't work -- bug confirmed. If all tests pass, the bug is not reproducible.
429
+
430
+ ## Web Dashboard
431
+
432
+ Real-time UI for running tests, viewing results, screenshots, and run history.
433
+
434
+ ```bash
435
+ e2e-runner dashboard # Start on default port 8484
436
+ e2e-runner dashboard --port 9090 # Custom port
437
+ ```
438
+
439
+ Features: live test execution, screenshot viewer with copy-to-clipboard hashes (`ss:a3f2b1c9`), multi-project support via SQLite, run history with auto-pruning.
440
+
363
441
  ## Architecture
364
442
 
365
443
  ```
@@ -371,6 +449,12 @@ src/runner.js Parallel test executor with retries and timeouts
371
449
  src/actions.js Action engine: maps JSON actions to Puppeteer calls
372
450
  src/reporter.js JSON reports, JUnit XML, console output
373
451
  src/mcp-server.js MCP server: exposes tools for Claude Code
452
+ src/mcp-tools.js Shared MCP tool definitions and handlers
453
+ src/dashboard.js Web dashboard: HTTP server, REST API, WebSocket
454
+ src/db.js SQLite multi-project database
455
+ src/issues.js GitHub/GitLab issue fetching (gh/glab CLI)
456
+ src/ai-generate.js AI test generation (prompt builder + Claude API)
457
+ src/verify.js Bug verification orchestrator
374
458
  src/logger.js ANSI colored logger
375
459
  src/index.js Programmatic API (createRunner)
376
460
  templates/ Scaffolding templates for init command
package/bin/cli.js CHANGED
@@ -13,6 +13,11 @@
13
13
  * e2e-runner pool stop Stop the Chrome Pool
14
14
  * e2e-runner pool status Show pool status
15
15
  * e2e-runner pool restart Restart the pool
16
+ * e2e-runner dashboard Start the web dashboard
17
+ * e2e-runner issue <url> Fetch issue and show details
18
+ * e2e-runner issue <url> --generate Generate test file via Claude API
19
+ * e2e-runner issue <url> --verify Generate + run + report bug status
20
+ * e2e-runner issue <url> --prompt Output the AI prompt (for piping)
16
21
  * e2e-runner init Scaffold e2e/ in the current project
17
22
  * e2e-runner --help Show help
18
23
  * e2e-runner --version Show version
@@ -20,11 +25,16 @@
20
25
 
21
26
  import fs from 'fs';
22
27
  import path from 'path';
28
+ import http from 'http';
23
29
  import { fileURLToPath } from 'url';
24
30
  import { loadConfig } from '../src/config.js';
25
31
  import { startPool, stopPool, restartPool, getPoolStatus, waitForPool } from '../src/pool.js';
26
32
  import { runTestsParallel, loadTestFile, loadTestSuite, loadAllSuites, listSuites } from '../src/runner.js';
27
- import { generateReport, saveReport, printReport } from '../src/reporter.js';
33
+ import { generateReport, saveReport, printReport, persistRun } from '../src/reporter.js';
34
+ import { startDashboard } from '../src/dashboard.js';
35
+ import { fetchIssue } from '../src/issues.js';
36
+ import { buildPrompt, generateTests, hasApiKey } from '../src/ai-generate.js';
37
+ import { verifyIssue } from '../src/verify.js';
28
38
  import { log, colors as C } from '../src/logger.js';
29
39
 
30
40
  const __filename = fileURLToPath(import.meta.url);
@@ -59,6 +69,9 @@ function parseCLIConfig() {
59
69
  if (getFlag('--test-timeout')) cliArgs.testTimeout = parseInt(getFlag('--test-timeout'));
60
70
  if (getFlag('--output')) cliArgs.outputFormat = getFlag('--output');
61
71
  if (getFlag('--env')) cliArgs.env = getFlag('--env');
72
+ if (getFlag('--port')) cliArgs.dashboardPort = parseInt(getFlag('--port'));
73
+ if (getFlag('--dashboard-port')) cliArgs.dashboardPort = parseInt(getFlag('--dashboard-port'));
74
+ if (getFlag('--project-name')) cliArgs.projectName = getFlag('--project-name');
62
75
  return cliArgs;
63
76
  }
64
77
 
@@ -75,6 +88,14 @@ ${C.bold}Usage:${C.reset}
75
88
 
76
89
  e2e-runner list List available suites
77
90
 
91
+ e2e-runner dashboard Start the web dashboard
92
+ e2e-runner dashboard --port <port> Custom port (default: 8484)
93
+
94
+ e2e-runner issue <url> Fetch issue and show details
95
+ e2e-runner issue <url> --generate Generate test file via Claude API
96
+ e2e-runner issue <url> --verify Generate + run + report bug status
97
+ e2e-runner issue <url> --prompt Output the AI prompt (for piping)
98
+
78
99
  e2e-runner pool start Start the Chrome Pool
79
100
  e2e-runner pool stop Stop the Chrome Pool
80
101
  e2e-runner pool status Show pool status
@@ -96,6 +117,7 @@ ${C.bold}Options:${C.reset}
96
117
  --test-timeout <ms> Per-test timeout (default: 60000)
97
118
  --output <format> Report format: json, junit, both (default: json)
98
119
  --env <name> Environment profile from config (default: default)
120
+ --project-name <name> Project display name for dashboard (default: directory name)
99
121
 
100
122
  ${C.bold}Config:${C.reset}
101
123
  Looks for e2e.config.js or e2e.config.json in the current directory.
@@ -151,13 +173,35 @@ async function cmdRun() {
151
173
  const pressure = await waitForPool(config.poolUrl);
152
174
  log('✅', `Pool ready (${pressure.running}/${pressure.maxConcurrent} sessions, queued: ${pressure.queued})`);
153
175
 
176
+ // Wire up live progress to dashboard if running
177
+ let _lastBroadcast = null;
178
+ try {
179
+ const res = await fetch('http://127.0.0.1:' + (config.dashboardPort || 8484) + '/api/status');
180
+ if (res.ok) {
181
+ const dp = config.dashboardPort || 8484;
182
+ config.onProgress = (data) => {
183
+ const body = JSON.stringify(data);
184
+ _lastBroadcast = new Promise((resolve) => {
185
+ const req = http.request({ hostname: '127.0.0.1', port: dp, path: '/api/broadcast', method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) }, timeout: 1000 });
186
+ req.on('error', () => resolve());
187
+ req.on('close', () => resolve());
188
+ req.end(body);
189
+ });
190
+ };
191
+ }
192
+ } catch { /* dashboard not running */ }
193
+
154
194
  // Execute tests
155
195
  console.log('');
196
+ const suiteName = getFlag('--suite') || (hasFlag('--all') ? null : null);
156
197
  const results = await runTestsParallel(tests, config, hooks);
157
198
  const report = generateReport(results);
158
199
  saveReport(report, config.screenshotsDir, config);
200
+ persistRun(report, config, suiteName);
159
201
  printReport(report, config.screenshotsDir);
160
202
 
203
+ // Wait for the last dashboard broadcast (run:complete) to flush before exiting
204
+ if (_lastBroadcast) await _lastBroadcast;
161
205
  process.exit(report.summary.failed > 0 ? 1 : 0);
162
206
  }
163
207
 
@@ -288,6 +332,114 @@ ${C.bold}Next steps:${C.reset}
288
332
  `);
289
333
  }
290
334
 
335
+ async function cmdDashboard() {
336
+ const cliArgs = parseCLIConfig();
337
+ const config = await loadConfig(cliArgs);
338
+
339
+ console.log(`\n${C.bold}${C.cyan}@matware/e2e-runner${C.reset} v${pkg.version}`);
340
+ console.log(`${C.dim}Starting dashboard on port ${config.dashboardPort}...${C.reset}\n`);
341
+
342
+ const handle = await startDashboard(config);
343
+
344
+ // Keep process alive until SIGINT/SIGTERM
345
+ const shutdown = () => {
346
+ console.log(`\n${C.dim}Shutting down dashboard...${C.reset}`);
347
+ handle.close();
348
+ process.exit(0);
349
+ };
350
+ process.on('SIGINT', shutdown);
351
+ process.on('SIGTERM', shutdown);
352
+ }
353
+
354
+ async function cmdIssue() {
355
+ const url = args[1];
356
+ if (!url || url.startsWith('--')) {
357
+ console.error(`${C.red}Usage: e2e-runner issue <url> [--generate|--verify|--prompt]${C.reset}`);
358
+ process.exit(1);
359
+ }
360
+
361
+ const cliArgs = parseCLIConfig();
362
+ const config = await loadConfig(cliArgs);
363
+
364
+ if (hasFlag('--prompt')) {
365
+ // Output AI prompt as JSON to stdout
366
+ const issue = fetchIssue(url);
367
+ const promptData = buildPrompt(issue, config);
368
+ console.log(JSON.stringify(promptData, null, 2));
369
+ return;
370
+ }
371
+
372
+ if (hasFlag('--verify')) {
373
+ // Generate + run + report
374
+ if (!hasApiKey(config)) {
375
+ console.error(`${C.red}ANTHROPIC_API_KEY is required for --verify mode.${C.reset}`);
376
+ process.exit(1);
377
+ }
378
+
379
+ console.log(`\n${C.bold}${C.cyan}@matware/e2e-runner${C.reset} v${pkg.version}`);
380
+ log('🔍', 'Fetching issue...');
381
+
382
+ const result = await verifyIssue(url, config);
383
+ const { issue, report, bugConfirmed } = result;
384
+
385
+ console.log('');
386
+ if (bugConfirmed) {
387
+ log('🐛', `${C.red}${C.bold}BUG CONFIRMED${C.reset} — ${issue.title}`);
388
+ log('', `${C.dim}${report.summary.failed} of ${report.summary.total} tests failed${C.reset}`);
389
+ } else {
390
+ log('✅', `${C.green}${C.bold}NOT REPRODUCIBLE${C.reset} — ${issue.title}`);
391
+ log('', `${C.dim}All ${report.summary.total} tests passed${C.reset}`);
392
+ }
393
+ console.log(`${C.dim}Issue: ${issue.url}${C.reset}\n`);
394
+
395
+ process.exit(bugConfirmed ? 1 : 0);
396
+ }
397
+
398
+ if (hasFlag('--generate')) {
399
+ // Generate test file via Claude API
400
+ if (!hasApiKey(config)) {
401
+ console.error(`${C.red}ANTHROPIC_API_KEY is required for --generate mode.${C.reset}`);
402
+ process.exit(1);
403
+ }
404
+
405
+ console.log(`\n${C.bold}${C.cyan}@matware/e2e-runner${C.reset} v${pkg.version}`);
406
+ log('🔍', 'Fetching issue...');
407
+
408
+ const issue = fetchIssue(url);
409
+ log('📋', `${C.cyan}${issue.title}${C.reset}`);
410
+ log('🤖', 'Generating tests via Claude API...');
411
+
412
+ const { tests, suiteName } = await generateTests(issue, config);
413
+
414
+ if (!fs.existsSync(config.testsDir)) {
415
+ fs.mkdirSync(config.testsDir, { recursive: true });
416
+ }
417
+ const filePath = path.join(config.testsDir, `${suiteName}.json`);
418
+ fs.writeFileSync(filePath, JSON.stringify(tests, null, 2) + '\n');
419
+
420
+ log('✅', `Created ${C.cyan}${filePath}${C.reset} (${tests.length} tests)`);
421
+ console.log(`${C.dim}Run with: e2e-runner run --suite ${suiteName}${C.reset}\n`);
422
+ return;
423
+ }
424
+
425
+ // Default: fetch and display issue
426
+ log('🔍', 'Fetching issue...');
427
+ const issue = fetchIssue(url);
428
+
429
+ console.log(`\n${C.bold}${issue.title}${C.reset}`);
430
+ console.log(`${C.dim}${'─'.repeat(50)}${C.reset}`);
431
+ console.log(` Repo: ${C.cyan}${issue.repo}${C.reset}`);
432
+ console.log(` Number: #${issue.number}`);
433
+ console.log(` State: ${issue.state === 'open' ? C.green : C.red}${issue.state}${C.reset}`);
434
+ console.log(` Labels: ${issue.labels.length ? issue.labels.join(', ') : C.dim + 'none' + C.reset}`);
435
+ console.log(` URL: ${C.dim}${issue.url}${C.reset}`);
436
+ if (issue.body) {
437
+ console.log(`\n${C.bold}Description:${C.reset}`);
438
+ console.log(issue.body.length > 500 ? issue.body.substring(0, 500) + '...' : issue.body);
439
+ }
440
+ console.log('');
441
+ }
442
+
291
443
  // ==================== Main ====================
292
444
 
293
445
  async function main() {
@@ -316,6 +468,14 @@ async function main() {
316
468
  await cmdPool();
317
469
  break;
318
470
 
471
+ case 'dashboard':
472
+ await cmdDashboard();
473
+ break;
474
+
475
+ case 'issue':
476
+ await cmdIssue();
477
+ break;
478
+
319
479
  case 'init':
320
480
  cmdInit();
321
481
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@matware/e2e-runner",
3
- "version": "1.0.2",
3
+ "version": "1.1.0",
4
4
  "mcpName": "io.github.fastslack/e2e-runner",
5
5
  "description": "E2E test runner using Chrome Pool (browserless/chrome) with parallel execution",
6
6
  "type": "module",
@@ -25,7 +25,9 @@
25
25
  "browserless",
26
26
  "parallel",
27
27
  "mcp",
28
- "claude-code"
28
+ "claude-code",
29
+ "github-issues",
30
+ "ai-testing"
29
31
  ],
30
32
  "author": "Matware",
31
33
  "license": "Apache-2.0",
@@ -36,6 +38,7 @@
36
38
  "homepage": "https://github.com/fastslack/mtw-e2e-runner#readme",
37
39
  "dependencies": {
38
40
  "@modelcontextprotocol/sdk": "^1.12.1",
41
+ "better-sqlite3": "^11.0.0",
39
42
  "puppeteer-core": "^24.0.0"
40
43
  },
41
44
  "engines": {
package/src/actions.js CHANGED
@@ -7,6 +7,7 @@
7
7
  * The JS comes from team-authored JSON test files.
8
8
  */
9
9
 
10
+ import path from 'path';
10
11
  import { log } from './logger.js';
11
12
 
12
13
  function sleep(ms) {
@@ -70,7 +71,9 @@ export async function executeAction(page, action, config) {
70
71
  if (!/\.(png|jpg|jpeg|webp)$/i.test(filename)) {
71
72
  filename += '.png';
72
73
  }
73
- const filepath = `${screenshotsDir}/${filename}`;
74
+ // Sanitize: use only the basename to prevent path traversal
75
+ filename = path.basename(filename);
76
+ const filepath = path.join(screenshotsDir, filename);
74
77
  await page.screenshot({ path: filepath, fullPage: action.fullPage || false });
75
78
  return { screenshot: filepath };
76
79
  }
@@ -146,6 +149,19 @@ export async function executeAction(page, action, config) {
146
149
  await page.hover(selector);
147
150
  break;
148
151
 
152
+ case 'navigate': {
153
+ const navUrl = value.startsWith('http') ? value : `${baseUrl}${value}`;
154
+ // Navigate with a race: try page.goto but don't block more than 5s
155
+ // This handles SPAs where domcontentloaded may not fire on client-side routing
156
+ try {
157
+ await Promise.race([
158
+ page.goto(navUrl, { waitUntil: 'load', timeout: 30000 }),
159
+ sleep(5000),
160
+ ]);
161
+ } catch { /* navigation may still be loading */ }
162
+ break;
163
+ }
164
+
149
165
  case 'evaluate':
150
166
  // Intentional: runs JS in browser page context (from test JSON files)
151
167
  await page.evaluate(value);
@@ -0,0 +1,185 @@
1
+ /**
2
+ * AI Test Generation — builds prompts and optionally calls Claude API
3
+ *
4
+ * Two modes:
5
+ * 1. buildPrompt() — Returns issue data + prompt for Claude Code (MCP mode, no API key)
6
+ * 2. generateTests() — Calls Claude API directly (CLI automation, requires ANTHROPIC_API_KEY)
7
+ */
8
+
9
+ import fs from 'fs';
10
+ import { listSuites } from './runner.js';
11
+
12
+ const SYSTEM_PROMPT = `You are an E2E test generator for a JSON-driven browser test runner.
13
+
14
+ You output ONLY valid JSON — no markdown fences, no explanation, no comments.
15
+
16
+ The test format is:
17
+ [
18
+ {
19
+ "name": "descriptive-test-name",
20
+ "actions": [
21
+ { "type": "goto", "value": "/path" },
22
+ { "type": "click", "selector": "#btn" },
23
+ { "type": "click", "text": "Button Label" },
24
+ { "type": "type", "selector": "input[name=email]", "value": "user@example.com" },
25
+ { "type": "wait", "selector": ".loaded" },
26
+ { "type": "wait", "text": "Expected text" },
27
+ { "type": "wait", "value": "2000" },
28
+ { "type": "assert_text", "text": "Expected text on page" },
29
+ { "type": "assert_url", "value": "/expected-path" },
30
+ { "type": "assert_visible", "selector": ".element" },
31
+ { "type": "assert_count", "selector": ".items", "value": "5" },
32
+ { "type": "screenshot", "value": "step-name.png" },
33
+ { "type": "select", "selector": "select#role", "value": "admin" },
34
+ { "type": "clear", "selector": "input" },
35
+ { "type": "press", "value": "Enter" },
36
+ { "type": "scroll", "selector": ".target" },
37
+ { "type": "hover", "selector": ".menu" },
38
+ { "type": "evaluate", "value": "document.title" }
39
+ ]
40
+ }
41
+ ]
42
+
43
+ Rules:
44
+ - Output a JSON array of test objects
45
+ - Use only the action types listed above
46
+ - "click" with "text" (no selector) finds buttons/links by visible text
47
+ - "goto" values starting with "/" are relative to the app's base URL
48
+ - Include a screenshot action before key assertions for debugging
49
+ - For bug reports: write tests that assert the CORRECT behavior. If the test fails, the bug is confirmed
50
+ - Keep test names descriptive and kebab-case
51
+ - Prefer CSS selectors that are stable (data-testid, name, role) over fragile ones (nth-child, classes)
52
+ - If the issue description is vague, create a reasonable test that covers the described scenario`;
53
+
54
+ /**
55
+ * Returns a structured prompt + issue data for Claude Code to consume.
56
+ * Claude Code uses its own intelligence to create tests via e2e_create_test.
57
+ * No API key needed.
58
+ *
59
+ * @param {object} issue - Normalized issue from fetchIssue()
60
+ * @param {object} config - Loaded config
61
+ * @returns {object}
62
+ */
63
+ export function buildPrompt(issue, config) {
64
+ let existingSuites = [];
65
+ try {
66
+ existingSuites = listSuites(config.testsDir).map(s => s.name);
67
+ } catch { /* no suites yet */ }
68
+
69
+ const prompt = `Based on the following issue, generate E2E test actions using the e2e_create_test tool.
70
+
71
+ ## Issue: ${issue.title}
72
+ **Repo:** ${issue.repo}
73
+ **Labels:** ${issue.labels.join(', ') || 'none'}
74
+ **State:** ${issue.state}
75
+ **URL:** ${issue.url}
76
+
77
+ ### Description
78
+ ${issue.body || 'No description provided.'}
79
+
80
+ ## Instructions
81
+ 1. Analyze the issue and determine what user flows to test
82
+ 2. Create one or more tests that verify the expected behavior
83
+ 3. For bug reports: assert the CORRECT behavior (test failure = bug confirmed)
84
+ 4. Use the \`e2e_create_test\` tool with suite name \`issue-${issue.number}\`
85
+ 5. After creating the test, use \`e2e_run\` with suite \`issue-${issue.number}\` to execute it
86
+
87
+ Base URL: ${config.baseUrl}
88
+ Existing suites: ${existingSuites.join(', ') || 'none'}`;
89
+
90
+ return {
91
+ issue: {
92
+ title: issue.title,
93
+ body: issue.body,
94
+ labels: issue.labels,
95
+ url: issue.url,
96
+ number: issue.number,
97
+ repo: issue.repo,
98
+ state: issue.state,
99
+ },
100
+ baseUrl: config.baseUrl,
101
+ prompt,
102
+ existingSuites,
103
+ };
104
+ }
105
+
106
+ /**
107
+ * Checks if the Anthropic API key is available.
108
+ * @returns {boolean}
109
+ */
110
+ export function hasApiKey(config = {}) {
111
+ return !!(config.anthropicApiKey || process.env.ANTHROPIC_API_KEY);
112
+ }
113
+
114
+ /**
115
+ * Calls Claude API directly to generate E2E tests from an issue.
116
+ * Requires ANTHROPIC_API_KEY env var or config.anthropicApiKey.
117
+ *
118
+ * @param {object} issue - Normalized issue from fetchIssue()
119
+ * @param {object} config - Loaded config
120
+ * @returns {Promise<{ tests: object[], suiteName: string }>}
121
+ */
122
+ export async function generateTests(issue, config) {
123
+ const apiKey = config.anthropicApiKey || process.env.ANTHROPIC_API_KEY;
124
+ if (!apiKey) {
125
+ throw new Error('ANTHROPIC_API_KEY is required for test generation. Set it as an environment variable or in config.');
126
+ }
127
+
128
+ const model = config.anthropicModel || 'claude-sonnet-4-5-20250929';
129
+ const suiteName = `issue-${issue.number}`;
130
+
131
+ const userMessage = `Generate E2E tests for this issue:
132
+
133
+ Title: ${issue.title}
134
+ Repo: ${issue.repo}
135
+ Labels: ${issue.labels.join(', ') || 'none'}
136
+ State: ${issue.state}
137
+
138
+ Description:
139
+ ${issue.body || 'No description provided.'}
140
+
141
+ Base URL: ${config.baseUrl}
142
+
143
+ Output a JSON array of test objects. Nothing else.`;
144
+
145
+ const response = await fetch('https://api.anthropic.com/v1/messages', {
146
+ method: 'POST',
147
+ headers: {
148
+ 'Content-Type': 'application/json',
149
+ 'x-api-key': apiKey,
150
+ 'anthropic-version': '2023-06-01',
151
+ },
152
+ body: JSON.stringify({
153
+ model,
154
+ max_tokens: 4096,
155
+ system: SYSTEM_PROMPT,
156
+ messages: [{ role: 'user', content: userMessage }],
157
+ }),
158
+ });
159
+
160
+ if (!response.ok) {
161
+ const body = await response.text();
162
+ throw new Error(`Claude API error (${response.status}): ${body}`);
163
+ }
164
+
165
+ const result = await response.json();
166
+ const text = result.content?.[0]?.text;
167
+ if (!text) {
168
+ throw new Error('Claude API returned empty response');
169
+ }
170
+
171
+ // Parse JSON — strip markdown fences if present
172
+ const cleaned = text.replace(/^```(?:json)?\s*\n?/m, '').replace(/\n?```\s*$/m, '').trim();
173
+ let tests;
174
+ try {
175
+ tests = JSON.parse(cleaned);
176
+ } catch (err) {
177
+ throw new Error(`Failed to parse generated tests as JSON: ${err.message}\n\nRaw output:\n${text}`);
178
+ }
179
+
180
+ if (!Array.isArray(tests)) {
181
+ throw new Error('Generated tests must be a JSON array');
182
+ }
183
+
184
+ return { tests, suiteName };
185
+ }
package/src/config.js CHANGED
@@ -30,6 +30,11 @@ const DEFAULTS = {
30
30
  outputFormat: 'json',
31
31
  env: 'default',
32
32
  hooks: { beforeAll: [], afterAll: [], beforeEach: [], afterEach: [] },
33
+ dashboardPort: 8484,
34
+ maxHistoryRuns: 100,
35
+ projectName: null,
36
+ anthropicApiKey: null,
37
+ anthropicModel: 'claude-sonnet-4-5-20250929',
33
38
  };
34
39
 
35
40
  function loadEnvVars() {
@@ -47,6 +52,9 @@ function loadEnvVars() {
47
52
  if (process.env.TEST_TIMEOUT) env.testTimeout = parseInt(process.env.TEST_TIMEOUT);
48
53
  if (process.env.OUTPUT_FORMAT) env.outputFormat = process.env.OUTPUT_FORMAT;
49
54
  if (process.env.E2E_ENV) env.env = process.env.E2E_ENV;
55
+ if (process.env.PROJECT_NAME) env.projectName = process.env.PROJECT_NAME;
56
+ if (process.env.ANTHROPIC_API_KEY) env.anthropicApiKey = process.env.ANTHROPIC_API_KEY;
57
+ if (process.env.ANTHROPIC_MODEL) env.anthropicModel = process.env.ANTHROPIC_MODEL;
50
58
  return env;
51
59
  }
52
60
 
@@ -100,5 +108,11 @@ export async function loadConfig(cliArgs = {}, cwd = null) {
100
108
  fs.mkdirSync(config.screenshotsDir, { recursive: true });
101
109
  }
102
110
 
111
+ // Stash cwd for project identity (used by db.js)
112
+ config._cwd = cwd;
113
+ if (!config.projectName) {
114
+ config.projectName = path.basename(cwd);
115
+ }
116
+
103
117
  return config;
104
118
  }