greenrun-cli 0.2.8 → 0.2.10

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.
@@ -50,7 +50,7 @@ export declare class ApiClient {
50
50
  name?: string;
51
51
  }): Promise<unknown>;
52
52
  deletePage(id: string): Promise<unknown>;
53
- listTests(projectId: string): Promise<unknown>;
53
+ listTests(projectId: string, compact?: boolean): Promise<unknown>;
54
54
  createTest(projectId: string, data: {
55
55
  name: string;
56
56
  instructions: string;
@@ -98,12 +98,10 @@ export declare class ApiClient {
98
98
  test_id: any;
99
99
  test_name: any;
100
100
  run_id: any;
101
- instructions: any;
102
101
  credential_name: any;
103
102
  pages: any;
104
103
  tags: any;
105
- script: any;
106
- script_generated_at: any;
104
+ has_script: any;
107
105
  }[];
108
106
  }>;
109
107
  }
@@ -61,8 +61,9 @@ export class ApiClient {
61
61
  return this.request('DELETE', `/pages/${id}`);
62
62
  }
63
63
  // Tests
64
- async listTests(projectId) {
65
- return this.request('GET', `/projects/${projectId}/tests`);
64
+ async listTests(projectId, compact) {
65
+ const query = compact ? '?compact=1' : '';
66
+ return this.request('GET', `/projects/${projectId}/tests${query}`);
66
67
  }
67
68
  async createTest(projectId, data) {
68
69
  return this.request('POST', `/projects/${projectId}/tests`, data);
@@ -106,7 +107,7 @@ export class ApiClient {
106
107
  async prepareTestBatch(projectId, filter, testIds) {
107
108
  const [projectResult, testsResult] = await Promise.all([
108
109
  this.getProject(projectId),
109
- this.listTests(projectId),
110
+ this.listTests(projectId, true),
110
111
  ]);
111
112
  const project = projectResult.project;
112
113
  let tests = (testsResult.tests || []).filter((t) => t.status === 'active');
@@ -127,44 +128,30 @@ export class ApiClient {
127
128
  tests = tests.filter((t) => (t.name || '').toLowerCase().includes(term));
128
129
  }
129
130
  }
131
+ const projectSummary = {
132
+ id: project.id, name: project.name, base_url: project.base_url,
133
+ auth_mode: project.auth_mode ?? 'none',
134
+ login_url: project.login_url ?? null,
135
+ register_url: project.register_url ?? null,
136
+ login_instructions: project.login_instructions ?? null,
137
+ register_instructions: project.register_instructions ?? null,
138
+ credentials: project.credentials ?? null,
139
+ };
130
140
  if (tests.length === 0) {
131
- return {
132
- project: {
133
- id: project.id, name: project.name, base_url: project.base_url,
134
- auth_mode: project.auth_mode ?? 'none',
135
- login_url: project.login_url ?? null,
136
- register_url: project.register_url ?? null,
137
- login_instructions: project.login_instructions ?? null,
138
- register_instructions: project.register_instructions ?? null,
139
- credentials: project.credentials ?? null,
140
- },
141
- tests: [],
142
- };
141
+ return { project: projectSummary, tests: [] };
143
142
  }
144
- // Fetch full test details in parallel
145
- const fullTests = await Promise.all(tests.map((t) => this.getTest(t.id)));
146
- // Start runs in parallel
143
+ // Start runs in parallel (listTests already has full details, no need for getTest)
147
144
  const runs = await Promise.all(tests.map((t) => this.startRun(t.id)));
148
145
  return {
149
- project: {
150
- id: project.id, name: project.name, base_url: project.base_url,
151
- auth_mode: project.auth_mode ?? 'none',
152
- login_url: project.login_url ?? null,
153
- register_url: project.register_url ?? null,
154
- login_instructions: project.login_instructions ?? null,
155
- register_instructions: project.register_instructions ?? null,
156
- credentials: project.credentials ?? null,
157
- },
158
- tests: fullTests.map((ft, i) => ({
159
- test_id: ft.test.id,
160
- test_name: ft.test.name,
146
+ project: projectSummary,
147
+ tests: tests.map((t, i) => ({
148
+ test_id: t.id,
149
+ test_name: t.name,
161
150
  run_id: runs[i].run.id,
162
- instructions: ft.test.instructions,
163
- credential_name: ft.test.credential_name ?? null,
164
- pages: ft.test.pages || [],
165
- tags: ft.test.tags || [],
166
- script: ft.test.script ?? null,
167
- script_generated_at: ft.test.script_generated_at ?? null,
151
+ credential_name: t.credential_name ?? null,
152
+ pages: (t.pages || []).map((p) => ({ id: p.id, url: p.url })),
153
+ tags: (t.tags || []).map((tg) => tg.name || tg),
154
+ has_script: t.has_script ?? !!t.script,
168
155
  })),
169
156
  };
170
157
  }
@@ -36,6 +36,43 @@ function prompt(rl, question) {
36
36
  });
37
37
  });
38
38
  }
39
+ function detectSystemChrome() {
40
+ const platform = process.platform;
41
+ if (platform === 'darwin') {
42
+ return existsSync('/Applications/Google Chrome.app/Contents/MacOS/Google Chrome');
43
+ }
44
+ if (platform === 'win32') {
45
+ const dirs = [process.env['PROGRAMFILES'], process.env['PROGRAMFILES(X86)'], process.env['LOCALAPPDATA']];
46
+ return dirs.some(dir => dir && existsSync(join(dir, 'Google', 'Chrome', 'Application', 'chrome.exe')));
47
+ }
48
+ // Linux
49
+ try {
50
+ execSync('which google-chrome-stable || which google-chrome || which chromium-browser || which chromium', { stdio: 'pipe' });
51
+ return true;
52
+ }
53
+ catch {
54
+ return false;
55
+ }
56
+ }
57
+ function installPlaywrightChromium() {
58
+ try {
59
+ console.log(' Installing @playwright/test (this may take a minute)...');
60
+ execSync('npm install -g @playwright/test@latest', { stdio: 'inherit' });
61
+ console.log(' Installing Chromium browser...');
62
+ execSync('npx playwright install --with-deps chromium', { stdio: 'inherit' });
63
+ return true;
64
+ }
65
+ catch {
66
+ console.error(' Failed to install Playwright. You can install manually:');
67
+ console.error(' npm install -g @playwright/test@latest');
68
+ console.error(' npx playwright install --with-deps chromium\n');
69
+ return false;
70
+ }
71
+ }
72
+ function checkNodeVersion() {
73
+ const match = process.version.match(/^v(\d+)\./);
74
+ return match ? parseInt(match[1], 10) >= 18 : false;
75
+ }
39
76
  function checkPrerequisites() {
40
77
  let claude = false;
41
78
  try {
@@ -55,14 +92,15 @@ async function validateToken(token) {
55
92
  'Accept': 'application/json',
56
93
  },
57
94
  });
58
- if (!response.ok)
59
- return { valid: false };
95
+ if (!response.ok) {
96
+ return { valid: false, error: `API returned HTTP ${response.status}` };
97
+ }
60
98
  const data = await response.json();
61
99
  const projects = Array.isArray(data) ? data : (data.data ?? []);
62
100
  return { valid: true, projectCount: projects.length };
63
101
  }
64
- catch {
65
- return { valid: false };
102
+ catch (err) {
103
+ return { valid: false, error: err?.message || String(err) };
66
104
  }
67
105
  }
68
106
  function getClaudeConfigPath() {
@@ -107,24 +145,24 @@ function configureMcpLocal(token) {
107
145
  console.error(` claude mcp add greenrun --transport stdio -e GREENRUN_API_TOKEN=${token} -- npx -y greenrun-cli@latest\n`);
108
146
  }
109
147
  }
110
- function configurePlaywrightMcp() {
148
+ function configurePlaywrightMcp(browser = 'chrome') {
111
149
  try {
112
150
  setLocalMcpServer('playwright', {
113
151
  type: 'stdio',
114
152
  command: 'npx',
115
153
  args: [
116
154
  '@playwright/mcp@latest',
117
- '--browser', 'chrome',
155
+ '--browser', browser,
118
156
  '--user-data-dir', join(homedir(), '.greenrun', 'browser-profile'),
119
157
  ],
120
158
  env: {},
121
159
  });
122
- console.log(' Configured playwright MCP server');
160
+ console.log(` Configured playwright MCP server (${browser})`);
123
161
  }
124
162
  catch {
125
163
  console.error('\nFailed to write Playwright MCP config to ~/.claude.json');
126
164
  console.error('You can add it manually:\n');
127
- console.error(' claude mcp add playwright -- npx @playwright/mcp@latest --browser chrome --user-data-dir ~/.greenrun/browser-profile\n');
165
+ console.error(` claude mcp add playwright -- npx @playwright/mcp@latest --browser ${browser} --user-data-dir ~/.greenrun/browser-profile\n`);
128
166
  }
129
167
  }
130
168
  function configureMcpProject(token) {
@@ -287,6 +325,12 @@ export async function runInit(args) {
287
325
  const opts = parseFlags(args);
288
326
  const interactive = !opts.token;
289
327
  console.log('\nGreenrun - Browser Test Management for Claude Code\n');
328
+ // Node version gate
329
+ if (!checkNodeVersion()) {
330
+ console.error(`Error: Node.js 18 or later is required (detected ${process.version}).`);
331
+ console.error('Install a newer version: https://nodejs.org/\n');
332
+ process.exit(1);
333
+ }
290
334
  // Prerequisites
291
335
  console.log('Prerequisites:');
292
336
  const prereqs = checkPrerequisites();
@@ -318,7 +362,7 @@ export async function runInit(args) {
318
362
  process.stdout.write(' Validating... ');
319
363
  const validation = await validateToken(token);
320
364
  if (!validation.valid) {
321
- console.log('Failed! Invalid token or cannot reach the API.');
365
+ console.log(`Failed! ${validation.error || 'Invalid token or cannot reach the API.'}`);
322
366
  rl.close();
323
367
  process.exit(1);
324
368
  }
@@ -348,12 +392,37 @@ export async function runInit(args) {
348
392
  process.stdout.write('Validating token... ');
349
393
  const validation = await validateToken(token);
350
394
  if (!validation.valid) {
351
- console.log('Failed!');
395
+ console.log(`Failed! ${validation.error || 'Invalid token or cannot reach the API.'}`);
352
396
  process.exit(1);
353
397
  }
354
398
  console.log(`Connected! (${validation.projectCount} project${validation.projectCount === 1 ? '' : 's'} found)`);
355
399
  scope = scope || 'local';
356
400
  }
401
+ // Detect browser
402
+ let browser = 'chrome';
403
+ if (!detectSystemChrome()) {
404
+ if (interactive) {
405
+ const rl2 = createInterface({ input: process.stdin, output: process.stdout });
406
+ console.log('Chrome not detected on this system.');
407
+ const installChoice = await prompt(rl2, ' Install Playwright Chromium? [Y/n]: ');
408
+ rl2.close();
409
+ if (installChoice.toLowerCase() !== 'n') {
410
+ if (installPlaywrightChromium()) {
411
+ browser = 'chromium';
412
+ }
413
+ else {
414
+ console.log(' Continuing with chrome config. You can install Chrome manually later.\n');
415
+ }
416
+ }
417
+ }
418
+ else {
419
+ console.log('Chrome not detected. Installing Playwright Chromium...');
420
+ if (installPlaywrightChromium()) {
421
+ browser = 'chromium';
422
+ }
423
+ }
424
+ console.log();
425
+ }
357
426
  // Configure MCP
358
427
  console.log('Configuring MCP servers...');
359
428
  if (scope === 'project') {
@@ -362,7 +431,7 @@ export async function runInit(args) {
362
431
  else {
363
432
  configureMcpLocal(token);
364
433
  }
365
- configurePlaywrightMcp();
434
+ configurePlaywrightMcp(browser);
366
435
  console.log(' MCP servers configured.\n');
367
436
  // Install extras
368
437
  if (opts.claudeMd) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "greenrun-cli",
3
- "version": "0.2.8",
3
+ "version": "0.2.10",
4
4
  "description": "CLI and MCP server for Greenrun - browser test management for Claude Code",
5
5
  "type": "module",
6
6
  "main": "dist/server.js",
@@ -63,6 +63,18 @@ Don't ask the user for information you can derive from the codebase (base URL, l
63
63
  3. Use `create_page` to register the page URL if not already registered
64
64
  4. Use `create_test` with the instructions and page IDs
65
65
 
66
+ ### Bug Detection During Test Creation
67
+
68
+ When exploring pages to write tests, if something doesn't work as expected:
69
+
70
+ - **If the test steps are wrong** (wrong field names, missing prerequisite, bad selectors) -- fix the instructions and retry. Always try to make the test work before giving up.
71
+ - **If there's a real application bug** (form won't submit with certain data, unexpected error, broken feature):
72
+ 1. Adjust the original test to work around the bug so it captures the happy path
73
+ 2. Create a **separate bug test** tagged `bug` with steps that reproduce the failure, describing expected vs actual behaviour
74
+ 3. Start a run for the bug test and complete it as `failed` with a summary of what went wrong
75
+
76
+ This way the main test suite tracks working functionality while bugs are captured as individual failing tests.
77
+
66
78
  ### Impact Analysis
67
79
 
68
80
  After making code changes, use the `/greenrun-sweep` command or the `sweep` tool to find which tests are affected by the pages you changed. This helps you run only the relevant tests.
@@ -23,7 +23,9 @@ If auth fails (login form still visible after following instructions), report al
23
23
 
24
24
  ## Execute
25
25
 
26
- You have a batch result from `prepare_test_batch` containing `project` (with `credentials` array) and `tests[]` (each with `test_id`, `test_name`, `run_id`, `instructions`, `credential_name`, `pages`, `tags`, `script`, `script_generated_at`).
26
+ You have a batch result from `prepare_test_batch` containing `project` (with `credentials` array) and `tests[]` (each with `test_id`, `test_name`, `run_id`, `instructions`, `credential_name`, `pages`, `tags`, `has_script`).
27
+
28
+ Note: `has_script` is a boolean indicating whether a cached Playwright script exists. To fetch the actual script content, call `get_test(test_id)` — only do this when you need the script (e.g. in Step 5 when writing test files).
27
29
 
28
30
  If `tests` is empty, tell the user no matching active tests were found and stop.
29
31
 
@@ -35,8 +37,8 @@ Run the Authenticate procedure above once, using the standard Playwright tools (
35
37
 
36
38
  Split the batch into two groups:
37
39
 
38
- - **scripted**: tests where `script` is non-null (cached Playwright scripts ready to run)
39
- - **unscripted**: tests where `script` is null (need script generation)
40
+ - **scripted**: tests where `has_script` is true (cached Playwright scripts ready to run)
41
+ - **unscripted**: tests where `has_script` is false (need script generation)
40
42
 
41
43
  If all tests are scripted, skip to Step 4.
42
44
 
@@ -60,6 +62,42 @@ For each unscripted test (in difficulty order), do a **scouting pass** — actua
60
62
  4. Snapshot after each state change to capture: validation errors, success banners, modal dialogs, redirected pages, dynamically loaded content
61
63
  5. Collect all observed elements and selectors as context
62
64
 
65
+ #### Handling failures during scouting
66
+
67
+ If a step doesn't work as expected during the scouting pass, investigate before moving on:
68
+
69
+ 1. **Determine the cause**: Is it a test problem (wrong instructions, bad selectors, missing prerequisite) or an application bug (form won't submit, unexpected error, broken functionality)?
70
+
71
+ 2. **If the test is wrong** — fix and retry:
72
+ - Adjust the instructions to match what the UI actually requires (e.g. a required field the instructions missed, a different button label, an extra confirmation step)
73
+ - Update the test via `update_test` with corrected instructions
74
+ - Retry the failing step
75
+
76
+ 3. **If it's an application bug** — work around it and record the bug:
77
+ - Find a way to make the original test pass by avoiding the broken path (e.g. if a discount code field breaks form submission, leave it blank)
78
+ - Update the original test instructions if needed to use the working path
79
+ - Create a **new bug test** that reproduces the specific failure:
80
+ ```
81
+ create_test(project_id, {
82
+ name: "BUG: [description of the failure]",
83
+ instructions: "[steps that reproduce the bug, ending with the expected vs actual behaviour]",
84
+ tags: ["bug"],
85
+ page_ids: [relevant page IDs],
86
+ credential_name: same as original test
87
+ })
88
+ ```
89
+ - Start a run for the bug test and immediately complete it as failed:
90
+ ```
91
+ start_run(bug_test_id) → complete_run(run_id, "failed", "description of what went wrong")
92
+ ```
93
+ - Continue scouting the original test with the workaround
94
+
95
+ This ensures the original test captures the happy path while bugs are tracked as separate failing tests that will show up in future runs.
96
+
97
+ After each test's scouting pass, close the browser so the next test starts with a clean context (no leftover cookies, storage, or page state):
98
+
99
+ Call `browser_close` after collecting all observations. The next `browser_navigate` call will automatically open a fresh browser context.
100
+
63
101
  Then generate a `.spec.ts` script using the observed elements:
64
102
 
65
103
  ```ts
@@ -108,7 +146,7 @@ Call this via `browser_run_code`. If `auth_mode` is `none`, skip this step.
108
146
 
109
147
  Gather all tests that have scripts (previously scripted + newly generated from Step 3).
110
148
 
111
- 1. **Write test files**: For each scripted test, write the script to `/tmp/greenrun-tests/{test_id}.spec.ts`
149
+ 1. **Fetch and write test files**: For each scripted test, call `get_test(test_id)` to retrieve the full script content, then write it to `/tmp/greenrun-tests/{test_id}.spec.ts`. Fetch scripts in parallel to minimize latency.
112
150
 
113
151
  2. **Write config**: Write `/tmp/greenrun-tests/playwright.config.ts`:
114
152
 
@@ -137,6 +175,12 @@ npx playwright test --config /tmp/greenrun-tests/playwright.config.ts
137
175
 
138
176
  5. **Report results**: Call `complete_run(run_id, status, result_summary)` for each test. Map Playwright statuses: `passed` → `passed`, `failed`/`timedOut` → `failed`, other → `error`.
139
177
 
178
+ 6. **Clean up browsers**: After native execution completes, close any browsers left behind by the test runner:
179
+ ```bash
180
+ npx playwright test --config /tmp/greenrun-tests/playwright.config.ts --list 2>/dev/null; true
181
+ ```
182
+ The Playwright Test runner normally cleans up after itself, but if tests crash or timeout, browser processes may linger. Also call `browser_close` to reset the MCP browser context before any subsequent AI fallback execution.
183
+
140
184
  ### Step 6: Handle unscripted tests without scripts
141
185
 
142
186
  Any tests that still don't have scripts (e.g. because the background agent hasn't finished, or script generation failed) need to be executed via AI agents using the legacy approach. Follow Step 7 for these tests.
@@ -154,8 +198,10 @@ After parsing all native results, walk through them in completion order. Track c
154
198
 
155
199
  For tests that **failed** in native execution (and circuit breaker has not tripped):
156
200
 
157
- 1. Start new runs via `start_run(test_id)` (the original runs were already completed in Step 5)
158
- 2. Launch background Task agents using the tab-isolation pattern:
201
+ 1. Close the current browser context with `browser_close` so the fallback starts fresh
202
+ 2. Re-authenticate by navigating to the login page and following the Authenticate procedure
203
+ 3. Start new runs via `start_run(test_id)` (the original runs were already completed in Step 5)
204
+ 4. Launch background Task agents using the tab-isolation pattern:
159
205
 
160
206
  Create tabs and launch agents in batches of 20:
161
207