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.
- package/dist/api-client.d.ts +2 -4
- package/dist/api-client.js +23 -36
- package/dist/commands/init.js +80 -11
- package/package.json +1 -1
- package/templates/claude-md.md +12 -0
- package/templates/commands/procedures.md +52 -6
package/dist/api-client.d.ts
CHANGED
|
@@ -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
|
-
|
|
106
|
-
script_generated_at: any;
|
|
104
|
+
has_script: any;
|
|
107
105
|
}[];
|
|
108
106
|
}>;
|
|
109
107
|
}
|
package/dist/api-client.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
}
|
package/dist/commands/init.js
CHANGED
|
@@ -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',
|
|
155
|
+
'--browser', browser,
|
|
118
156
|
'--user-data-dir', join(homedir(), '.greenrun', 'browser-profile'),
|
|
119
157
|
],
|
|
120
158
|
env: {},
|
|
121
159
|
});
|
|
122
|
-
console.log(
|
|
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(
|
|
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(
|
|
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(
|
|
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
package/templates/claude-md.md
CHANGED
|
@@ -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`, `
|
|
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 `
|
|
39
|
-
- **unscripted**: tests where `
|
|
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. **
|
|
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.
|
|
158
|
-
2.
|
|
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
|
|