greenrun-cli 0.2.15 → 0.3.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 +3 -1
- package/dist/api-client.d.ts +178 -49
- package/dist/api-client.js +104 -44
- package/dist/commands/init.js +29 -22
- package/dist/server.js +86 -32
- package/package.json +3 -2
- package/templates/claude-md.md +3 -1
- package/templates/commands/procedures.md +12 -6
package/README.md
CHANGED
|
@@ -78,9 +78,11 @@ Detect which tests are impacted by recent git changes and offer to run them.
|
|
|
78
78
|
| `update_test` | Update test (auto-invalidates script on content change) |
|
|
79
79
|
| `prepare_test_batch` | Fetch, filter, and start runs for a batch of tests |
|
|
80
80
|
| `export_test_script` | Write a test's cached Playwright script to a local file (keeps scripts out of context) |
|
|
81
|
+
| `export_test_instructions` | Write a test's instructions to a local file (keeps instructions out of context) |
|
|
81
82
|
| `sweep` | Find tests affected by specific pages |
|
|
82
83
|
| `start_run` | Start a test run |
|
|
83
|
-
| `complete_run` | Record test result |
|
|
84
|
+
| `complete_run` | Record a single test result |
|
|
85
|
+
| `batch_complete_runs` | Record results for multiple test runs in one call |
|
|
84
86
|
| `get_run` | Get run details |
|
|
85
87
|
| `list_runs` | List run history |
|
|
86
88
|
|
package/dist/api-client.d.ts
CHANGED
|
@@ -1,13 +1,112 @@
|
|
|
1
|
+
/** Configuration for connecting to the Greenrun API. */
|
|
1
2
|
export interface ApiConfig {
|
|
2
3
|
baseUrl: string;
|
|
3
4
|
token: string;
|
|
4
5
|
}
|
|
6
|
+
/** Credential set stored on a project. */
|
|
7
|
+
export interface Credential {
|
|
8
|
+
name: string;
|
|
9
|
+
email: string;
|
|
10
|
+
password: string;
|
|
11
|
+
}
|
|
12
|
+
/** Project data returned by the API. */
|
|
13
|
+
export interface ProjectResponse {
|
|
14
|
+
id: string;
|
|
15
|
+
name: string;
|
|
16
|
+
base_url?: string;
|
|
17
|
+
description?: string;
|
|
18
|
+
auth_mode?: string;
|
|
19
|
+
login_url?: string;
|
|
20
|
+
register_url?: string;
|
|
21
|
+
login_instructions?: string;
|
|
22
|
+
register_instructions?: string;
|
|
23
|
+
credentials?: Credential[];
|
|
24
|
+
tests_count?: number;
|
|
25
|
+
pages_count?: number;
|
|
26
|
+
}
|
|
27
|
+
/** Tag data as returned by the API (may be a string or object with name). */
|
|
28
|
+
interface TagData {
|
|
29
|
+
name?: string;
|
|
30
|
+
[key: string]: unknown;
|
|
31
|
+
}
|
|
32
|
+
/** Page data as returned by the API. */
|
|
33
|
+
interface PageData {
|
|
34
|
+
id: string;
|
|
35
|
+
url: string;
|
|
36
|
+
[key: string]: unknown;
|
|
37
|
+
}
|
|
38
|
+
/** Test data returned by the API. */
|
|
39
|
+
export interface TestResponse {
|
|
40
|
+
id: string;
|
|
41
|
+
name: string;
|
|
42
|
+
instructions?: string;
|
|
43
|
+
status?: string;
|
|
44
|
+
credential_name?: string;
|
|
45
|
+
has_script?: boolean;
|
|
46
|
+
script?: string;
|
|
47
|
+
script_generated_at?: string;
|
|
48
|
+
pages?: PageData[];
|
|
49
|
+
tags?: (string | TagData)[];
|
|
50
|
+
[key: string]: unknown;
|
|
51
|
+
}
|
|
52
|
+
/** Run data returned by the API. */
|
|
53
|
+
export interface RunResponse {
|
|
54
|
+
id: string;
|
|
55
|
+
status: string;
|
|
56
|
+
result?: string;
|
|
57
|
+
started_at?: string;
|
|
58
|
+
completed_at?: string;
|
|
59
|
+
duration_ms?: number;
|
|
60
|
+
[key: string]: unknown;
|
|
61
|
+
}
|
|
62
|
+
/** Summary of a test in a prepared batch. */
|
|
63
|
+
export interface BatchTestSummary {
|
|
64
|
+
test_id: string;
|
|
65
|
+
test_name: string;
|
|
66
|
+
run_id: string;
|
|
67
|
+
credential_name: string | null;
|
|
68
|
+
pages: {
|
|
69
|
+
id: string;
|
|
70
|
+
url: string;
|
|
71
|
+
}[];
|
|
72
|
+
tags: string[];
|
|
73
|
+
has_script: boolean;
|
|
74
|
+
}
|
|
75
|
+
/** Project summary included in a batch result, with auth fields filtered by relevance. */
|
|
76
|
+
export interface BatchProjectSummary {
|
|
77
|
+
id: string;
|
|
78
|
+
name: string;
|
|
79
|
+
base_url?: string;
|
|
80
|
+
auth_mode: string;
|
|
81
|
+
login_url?: string;
|
|
82
|
+
register_url?: string;
|
|
83
|
+
login_instructions?: string;
|
|
84
|
+
register_instructions?: string;
|
|
85
|
+
credentials?: Credential[];
|
|
86
|
+
}
|
|
87
|
+
/** Result of preparing a test batch for execution. */
|
|
88
|
+
export interface BatchResult {
|
|
89
|
+
project: BatchProjectSummary;
|
|
90
|
+
tests: BatchTestSummary[];
|
|
91
|
+
}
|
|
92
|
+
/** A single run result for batch completion. */
|
|
93
|
+
export interface RunResult {
|
|
94
|
+
run_id: string;
|
|
95
|
+
status: string;
|
|
96
|
+
result?: string;
|
|
97
|
+
}
|
|
98
|
+
/** HTTP client for the Greenrun API. */
|
|
5
99
|
export declare class ApiClient {
|
|
6
100
|
private baseUrl;
|
|
7
101
|
private token;
|
|
8
102
|
constructor(config: ApiConfig);
|
|
103
|
+
/** Make an authenticated request to the API. */
|
|
9
104
|
private request;
|
|
10
|
-
|
|
105
|
+
/** List all projects accessible to the authenticated user. */
|
|
106
|
+
listProjects(): Promise<{
|
|
107
|
+
projects: ProjectResponse[];
|
|
108
|
+
}>;
|
|
109
|
+
/** Create a new project. */
|
|
11
110
|
createProject(data: {
|
|
12
111
|
name: string;
|
|
13
112
|
base_url?: string;
|
|
@@ -17,13 +116,15 @@ export declare class ApiClient {
|
|
|
17
116
|
register_url?: string;
|
|
18
117
|
login_instructions?: string;
|
|
19
118
|
register_instructions?: string;
|
|
20
|
-
credentials?:
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
119
|
+
credentials?: Credential[];
|
|
120
|
+
}): Promise<{
|
|
121
|
+
project: ProjectResponse;
|
|
122
|
+
}>;
|
|
123
|
+
/** Get a project by ID. */
|
|
124
|
+
getProject(id: string): Promise<{
|
|
125
|
+
project: ProjectResponse;
|
|
126
|
+
}>;
|
|
127
|
+
/** Update a project's settings. */
|
|
27
128
|
updateProject(id: string, data: {
|
|
28
129
|
name?: string;
|
|
29
130
|
base_url?: string;
|
|
@@ -33,24 +134,41 @@ export declare class ApiClient {
|
|
|
33
134
|
register_url?: string;
|
|
34
135
|
login_instructions?: string;
|
|
35
136
|
register_instructions?: string;
|
|
36
|
-
credentials?:
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
137
|
+
credentials?: Credential[];
|
|
138
|
+
}): Promise<{
|
|
139
|
+
project: ProjectResponse;
|
|
140
|
+
}>;
|
|
141
|
+
/** Delete a project by ID. */
|
|
142
|
+
deleteProject(id: string): Promise<{
|
|
143
|
+
message: string;
|
|
144
|
+
}>;
|
|
145
|
+
/** List all pages in a project. */
|
|
146
|
+
listPages(projectId: string): Promise<{
|
|
147
|
+
pages: PageData[];
|
|
148
|
+
}>;
|
|
149
|
+
/** Register a new page URL in a project. */
|
|
44
150
|
createPage(projectId: string, data: {
|
|
45
151
|
url: string;
|
|
46
152
|
name?: string;
|
|
47
|
-
}): Promise<
|
|
153
|
+
}): Promise<{
|
|
154
|
+
page: PageData;
|
|
155
|
+
}>;
|
|
156
|
+
/** Update a page's URL or name. */
|
|
48
157
|
updatePage(id: string, data: {
|
|
49
158
|
url?: string;
|
|
50
159
|
name?: string;
|
|
51
|
-
}): Promise<
|
|
52
|
-
|
|
53
|
-
|
|
160
|
+
}): Promise<{
|
|
161
|
+
page: PageData;
|
|
162
|
+
}>;
|
|
163
|
+
/** Delete a page by ID. */
|
|
164
|
+
deletePage(id: string): Promise<{
|
|
165
|
+
message: string;
|
|
166
|
+
}>;
|
|
167
|
+
/** List tests in a project. Pass compact=true to omit instructions/script content. */
|
|
168
|
+
listTests(projectId: string, compact?: boolean): Promise<{
|
|
169
|
+
tests: TestResponse[];
|
|
170
|
+
}>;
|
|
171
|
+
/** Create a new test in a project. */
|
|
54
172
|
createTest(projectId: string, data: {
|
|
55
173
|
name: string;
|
|
56
174
|
instructions: string;
|
|
@@ -58,8 +176,14 @@ export declare class ApiClient {
|
|
|
58
176
|
status?: string;
|
|
59
177
|
tags?: string[];
|
|
60
178
|
credential_name?: string;
|
|
61
|
-
}): Promise<
|
|
62
|
-
|
|
179
|
+
}): Promise<{
|
|
180
|
+
test: TestResponse;
|
|
181
|
+
}>;
|
|
182
|
+
/** Get full test details including instructions, pages, and recent runs. */
|
|
183
|
+
getTest(id: string): Promise<{
|
|
184
|
+
test: TestResponse;
|
|
185
|
+
}>;
|
|
186
|
+
/** Update a test's properties. */
|
|
63
187
|
updateTest(id: string, data: {
|
|
64
188
|
name?: string;
|
|
65
189
|
instructions?: string;
|
|
@@ -69,39 +193,44 @@ export declare class ApiClient {
|
|
|
69
193
|
credential_name?: string | null;
|
|
70
194
|
script?: string | null;
|
|
71
195
|
script_generated_at?: string | null;
|
|
72
|
-
}): Promise<
|
|
73
|
-
|
|
196
|
+
}): Promise<{
|
|
197
|
+
test: TestResponse;
|
|
198
|
+
}>;
|
|
199
|
+
/** Delete a test by ID. */
|
|
200
|
+
deleteTest(id: string): Promise<{
|
|
201
|
+
message: string;
|
|
202
|
+
}>;
|
|
203
|
+
/** Find tests affected by specific pages (impact analysis). */
|
|
74
204
|
sweep(projectId: string, params: {
|
|
75
205
|
pages?: string[];
|
|
76
206
|
url_pattern?: string;
|
|
77
207
|
}): Promise<unknown>;
|
|
78
|
-
|
|
208
|
+
/** Start a new test run (sets status to running). */
|
|
209
|
+
startRun(testId: string): Promise<{
|
|
210
|
+
run: RunResponse;
|
|
211
|
+
}>;
|
|
212
|
+
/** Record the result of a single test run. */
|
|
79
213
|
completeRun(runId: string, data: {
|
|
80
214
|
status: string;
|
|
81
215
|
result?: string;
|
|
82
|
-
}): Promise<
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
name: any;
|
|
89
|
-
base_url: any;
|
|
90
|
-
auth_mode: any;
|
|
91
|
-
login_url: any;
|
|
92
|
-
register_url: any;
|
|
93
|
-
login_instructions: any;
|
|
94
|
-
register_instructions: any;
|
|
95
|
-
credentials: any;
|
|
96
|
-
};
|
|
97
|
-
tests: {
|
|
98
|
-
test_id: any;
|
|
99
|
-
test_name: any;
|
|
100
|
-
run_id: any;
|
|
101
|
-
credential_name: any;
|
|
102
|
-
pages: any;
|
|
103
|
-
tags: any;
|
|
104
|
-
has_script: any;
|
|
105
|
-
}[];
|
|
216
|
+
}): Promise<{
|
|
217
|
+
run: RunResponse;
|
|
218
|
+
}>;
|
|
219
|
+
/** Complete multiple test runs in a single batch call. */
|
|
220
|
+
batchCompleteRuns(runs: RunResult[]): Promise<{
|
|
221
|
+
completed: number;
|
|
106
222
|
}>;
|
|
223
|
+
/** Get details of a specific test run. */
|
|
224
|
+
getRun(runId: string): Promise<{
|
|
225
|
+
run: RunResponse;
|
|
226
|
+
}>;
|
|
227
|
+
/** List run history for a test (newest first). */
|
|
228
|
+
listRuns(testId: string): Promise<unknown>;
|
|
229
|
+
/**
|
|
230
|
+
* Prepare a batch of tests for execution. Lists tests, applies filters,
|
|
231
|
+
* fetches project details, and starts runs — all in one call.
|
|
232
|
+
* Only includes credentials referenced by the batch's tests.
|
|
233
|
+
*/
|
|
234
|
+
prepareTestBatch(projectId: string, filter?: string, testIds?: string[]): Promise<BatchResult>;
|
|
107
235
|
}
|
|
236
|
+
export {};
|
package/dist/api-client.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/** HTTP client for the Greenrun API. */
|
|
1
2
|
export class ApiClient {
|
|
2
3
|
baseUrl;
|
|
3
4
|
token;
|
|
@@ -5,6 +6,7 @@ export class ApiClient {
|
|
|
5
6
|
this.baseUrl = config.baseUrl.replace(/\/+$/, '');
|
|
6
7
|
this.token = config.token;
|
|
7
8
|
}
|
|
9
|
+
/** Make an authenticated request to the API. */
|
|
8
10
|
async request(method, path, body) {
|
|
9
11
|
const url = `${this.baseUrl}/api/v1${path}`;
|
|
10
12
|
const headers = {
|
|
@@ -31,53 +33,68 @@ export class ApiClient {
|
|
|
31
33
|
}
|
|
32
34
|
return response.json();
|
|
33
35
|
}
|
|
34
|
-
// Projects
|
|
36
|
+
// --- Projects ---
|
|
37
|
+
/** List all projects accessible to the authenticated user. */
|
|
35
38
|
async listProjects() {
|
|
36
39
|
return this.request('GET', '/projects');
|
|
37
40
|
}
|
|
41
|
+
/** Create a new project. */
|
|
38
42
|
async createProject(data) {
|
|
39
43
|
return this.request('POST', '/projects', data);
|
|
40
44
|
}
|
|
45
|
+
/** Get a project by ID. */
|
|
41
46
|
async getProject(id) {
|
|
42
47
|
return this.request('GET', `/projects/${id}`);
|
|
43
48
|
}
|
|
49
|
+
/** Update a project's settings. */
|
|
44
50
|
async updateProject(id, data) {
|
|
45
51
|
return this.request('PUT', `/projects/${id}`, data);
|
|
46
52
|
}
|
|
53
|
+
/** Delete a project by ID. */
|
|
47
54
|
async deleteProject(id) {
|
|
48
55
|
return this.request('DELETE', `/projects/${id}`);
|
|
49
56
|
}
|
|
50
|
-
// Pages
|
|
57
|
+
// --- Pages ---
|
|
58
|
+
/** List all pages in a project. */
|
|
51
59
|
async listPages(projectId) {
|
|
52
60
|
return this.request('GET', `/projects/${projectId}/pages`);
|
|
53
61
|
}
|
|
62
|
+
/** Register a new page URL in a project. */
|
|
54
63
|
async createPage(projectId, data) {
|
|
55
64
|
return this.request('POST', `/projects/${projectId}/pages`, data);
|
|
56
65
|
}
|
|
66
|
+
/** Update a page's URL or name. */
|
|
57
67
|
async updatePage(id, data) {
|
|
58
68
|
return this.request('PUT', `/pages/${id}`, data);
|
|
59
69
|
}
|
|
70
|
+
/** Delete a page by ID. */
|
|
60
71
|
async deletePage(id) {
|
|
61
72
|
return this.request('DELETE', `/pages/${id}`);
|
|
62
73
|
}
|
|
63
|
-
// Tests
|
|
74
|
+
// --- Tests ---
|
|
75
|
+
/** List tests in a project. Pass compact=true to omit instructions/script content. */
|
|
64
76
|
async listTests(projectId, compact) {
|
|
65
77
|
const query = compact ? '?compact=1' : '';
|
|
66
78
|
return this.request('GET', `/projects/${projectId}/tests${query}`);
|
|
67
79
|
}
|
|
80
|
+
/** Create a new test in a project. */
|
|
68
81
|
async createTest(projectId, data) {
|
|
69
82
|
return this.request('POST', `/projects/${projectId}/tests`, data);
|
|
70
83
|
}
|
|
84
|
+
/** Get full test details including instructions, pages, and recent runs. */
|
|
71
85
|
async getTest(id) {
|
|
72
86
|
return this.request('GET', `/tests/${id}`);
|
|
73
87
|
}
|
|
88
|
+
/** Update a test's properties. */
|
|
74
89
|
async updateTest(id, data) {
|
|
75
90
|
return this.request('PUT', `/tests/${id}`, data);
|
|
76
91
|
}
|
|
92
|
+
/** Delete a test by ID. */
|
|
77
93
|
async deleteTest(id) {
|
|
78
94
|
return this.request('DELETE', `/tests/${id}`);
|
|
79
95
|
}
|
|
80
|
-
// Sweep
|
|
96
|
+
// --- Sweep ---
|
|
97
|
+
/** Find tests affected by specific pages (impact analysis). */
|
|
81
98
|
async sweep(projectId, params) {
|
|
82
99
|
const searchParams = new URLSearchParams();
|
|
83
100
|
if (params.pages) {
|
|
@@ -90,69 +107,112 @@ export class ApiClient {
|
|
|
90
107
|
}
|
|
91
108
|
return this.request('GET', `/projects/${projectId}/sweep?${searchParams.toString()}`);
|
|
92
109
|
}
|
|
93
|
-
// Test Runs
|
|
110
|
+
// --- Test Runs ---
|
|
111
|
+
/** Start a new test run (sets status to running). */
|
|
94
112
|
async startRun(testId) {
|
|
95
113
|
return this.request('POST', `/tests/${testId}/runs`);
|
|
96
114
|
}
|
|
115
|
+
/** Record the result of a single test run. */
|
|
97
116
|
async completeRun(runId, data) {
|
|
98
117
|
return this.request('PUT', `/runs/${runId}`, data);
|
|
99
118
|
}
|
|
119
|
+
/** Complete multiple test runs in a single batch call. */
|
|
120
|
+
async batchCompleteRuns(runs) {
|
|
121
|
+
return this.request('PUT', '/runs/batch', { runs });
|
|
122
|
+
}
|
|
123
|
+
/** Get details of a specific test run. */
|
|
100
124
|
async getRun(runId) {
|
|
101
125
|
return this.request('GET', `/runs/${runId}`);
|
|
102
126
|
}
|
|
127
|
+
/** List run history for a test (newest first). */
|
|
103
128
|
async listRuns(testId) {
|
|
104
129
|
return this.request('GET', `/tests/${testId}/runs`);
|
|
105
130
|
}
|
|
106
|
-
// Batch
|
|
131
|
+
// --- Batch Operations ---
|
|
132
|
+
/**
|
|
133
|
+
* Prepare a batch of tests for execution. Lists tests, applies filters,
|
|
134
|
+
* fetches project details, and starts runs — all in one call.
|
|
135
|
+
* Only includes credentials referenced by the batch's tests.
|
|
136
|
+
*/
|
|
107
137
|
async prepareTestBatch(projectId, filter, testIds) {
|
|
108
138
|
const [projectResult, testsResult] = await Promise.all([
|
|
109
139
|
this.getProject(projectId),
|
|
110
140
|
this.listTests(projectId, true),
|
|
111
141
|
]);
|
|
112
142
|
const project = projectResult.project;
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
tests = tests.filter((t) => idSet.has(t.id));
|
|
117
|
-
}
|
|
118
|
-
else if (filter) {
|
|
119
|
-
if (filter.startsWith('tag:')) {
|
|
120
|
-
const tag = filter.slice(4).toLowerCase();
|
|
121
|
-
tests = tests.filter((t) => (t.tags || []).some((tg) => (tg.name || tg).toLowerCase() === tag));
|
|
122
|
-
}
|
|
123
|
-
else if (filter.startsWith('/')) {
|
|
124
|
-
tests = tests.filter((t) => (t.pages || []).some((p) => (p.url || '').includes(filter)));
|
|
125
|
-
}
|
|
126
|
-
else {
|
|
127
|
-
const term = filter.toLowerCase();
|
|
128
|
-
tests = tests.filter((t) => (t.name || '').toLowerCase().includes(term));
|
|
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
|
-
};
|
|
140
|
-
if (tests.length === 0) {
|
|
143
|
+
const activeTests = filterTests(testsResult.tests || [], filter, testIds);
|
|
144
|
+
const projectSummary = buildProjectSummary(project, activeTests);
|
|
145
|
+
if (activeTests.length === 0) {
|
|
141
146
|
return { project: projectSummary, tests: [] };
|
|
142
147
|
}
|
|
143
|
-
|
|
144
|
-
const runs = await Promise.all(tests.map((t) => this.startRun(t.id)));
|
|
148
|
+
const runs = await startBatchRuns(activeTests, (testId) => this.startRun(testId));
|
|
145
149
|
return {
|
|
146
150
|
project: projectSummary,
|
|
147
|
-
tests:
|
|
148
|
-
test_id:
|
|
149
|
-
test_name:
|
|
150
|
-
run_id: runs[
|
|
151
|
-
credential_name:
|
|
152
|
-
pages: (
|
|
153
|
-
tags: (
|
|
154
|
-
has_script:
|
|
151
|
+
tests: activeTests.map((test, index) => ({
|
|
152
|
+
test_id: test.id,
|
|
153
|
+
test_name: test.name,
|
|
154
|
+
run_id: runs[index].run.id,
|
|
155
|
+
credential_name: test.credential_name ?? null,
|
|
156
|
+
pages: (test.pages || []).map((page) => ({ id: page.id, url: page.url })),
|
|
157
|
+
tags: (test.tags || []).map((tag) => typeof tag === 'string' ? tag : tag.name || ''),
|
|
158
|
+
has_script: test.has_script ?? !!test.script,
|
|
155
159
|
})),
|
|
156
160
|
};
|
|
157
161
|
}
|
|
158
162
|
}
|
|
163
|
+
/** Filter tests by test IDs, tag, page URL, or name substring. */
|
|
164
|
+
function filterTests(tests, filter, testIds) {
|
|
165
|
+
let activeTests = tests.filter((test) => test.status === 'active');
|
|
166
|
+
if (testIds && testIds.length > 0) {
|
|
167
|
+
const idSet = new Set(testIds);
|
|
168
|
+
activeTests = activeTests.filter((test) => idSet.has(test.id));
|
|
169
|
+
}
|
|
170
|
+
else if (filter) {
|
|
171
|
+
if (filter.startsWith('tag:')) {
|
|
172
|
+
const tagName = filter.slice(4).toLowerCase();
|
|
173
|
+
activeTests = activeTests.filter((test) => (test.tags || []).some((tag) => {
|
|
174
|
+
const name = typeof tag === 'string' ? tag : tag.name || '';
|
|
175
|
+
return name.toLowerCase() === tagName;
|
|
176
|
+
}));
|
|
177
|
+
}
|
|
178
|
+
else if (filter.startsWith('/')) {
|
|
179
|
+
activeTests = activeTests.filter((test) => (test.pages || []).some((page) => (page.url || '').includes(filter)));
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
const term = filter.toLowerCase();
|
|
183
|
+
activeTests = activeTests.filter((test) => (test.name || '').toLowerCase().includes(term));
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return activeTests;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Build a project summary for the batch response.
|
|
190
|
+
* Omits auth fields when auth_mode is 'none'.
|
|
191
|
+
* Only includes credentials referenced by the batch's tests.
|
|
192
|
+
*/
|
|
193
|
+
function buildProjectSummary(project, tests) {
|
|
194
|
+
const authMode = project.auth_mode ?? 'none';
|
|
195
|
+
if (authMode === 'none') {
|
|
196
|
+
return { id: project.id, name: project.name, base_url: project.base_url, auth_mode: 'none' };
|
|
197
|
+
}
|
|
198
|
+
const referencedNames = new Set(tests.map((test) => test.credential_name).filter((name) => !!name));
|
|
199
|
+
const allCredentials = project.credentials ?? [];
|
|
200
|
+
const filteredCredentials = referencedNames.size > 0
|
|
201
|
+
? allCredentials.filter((cred) => referencedNames.has(cred.name))
|
|
202
|
+
: allCredentials;
|
|
203
|
+
return {
|
|
204
|
+
id: project.id,
|
|
205
|
+
name: project.name,
|
|
206
|
+
base_url: project.base_url,
|
|
207
|
+
auth_mode: authMode,
|
|
208
|
+
login_url: project.login_url,
|
|
209
|
+
register_url: project.register_url,
|
|
210
|
+
login_instructions: project.login_instructions,
|
|
211
|
+
register_instructions: project.register_instructions,
|
|
212
|
+
credentials: filteredCredentials.length > 0 ? filteredCredentials : undefined,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
/** Start runs for all tests in parallel. */
|
|
216
|
+
async function startBatchRuns(tests, startRun) {
|
|
217
|
+
return Promise.all(tests.map((test) => startRun(test.id)));
|
|
218
|
+
}
|
package/dist/commands/init.js
CHANGED
|
@@ -100,12 +100,14 @@ async function validateToken(token) {
|
|
|
100
100
|
return { valid: true, projectCount: projects.length };
|
|
101
101
|
}
|
|
102
102
|
catch (err) {
|
|
103
|
-
|
|
103
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
104
|
+
return { valid: false, error: message };
|
|
104
105
|
}
|
|
105
106
|
}
|
|
106
107
|
function getClaudeConfigPath() {
|
|
107
108
|
return join(homedir(), '.claude.json');
|
|
108
109
|
}
|
|
110
|
+
/** Read the Claude Code config from ~/.claude.json. */
|
|
109
111
|
function readClaudeConfig() {
|
|
110
112
|
const configPath = getClaudeConfigPath();
|
|
111
113
|
if (!existsSync(configPath))
|
|
@@ -117,6 +119,7 @@ function readClaudeConfig() {
|
|
|
117
119
|
return {};
|
|
118
120
|
}
|
|
119
121
|
}
|
|
122
|
+
/** Write the Claude Code config to ~/.claude.json. */
|
|
120
123
|
function writeClaudeConfig(config) {
|
|
121
124
|
writeFileSync(getClaudeConfigPath(), JSON.stringify(config, null, 2) + '\n');
|
|
122
125
|
}
|
|
@@ -233,19 +236,8 @@ function installClaudeMd() {
|
|
|
233
236
|
console.log(' Created CLAUDE.md with Greenrun instructions');
|
|
234
237
|
}
|
|
235
238
|
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
mkdirSync(settingsDir, { recursive: true });
|
|
239
|
-
const settingsPath = join(settingsDir, 'settings.local.json');
|
|
240
|
-
let existing = {};
|
|
241
|
-
if (existsSync(settingsPath)) {
|
|
242
|
-
try {
|
|
243
|
-
existing = JSON.parse(readFileSync(settingsPath, 'utf-8'));
|
|
244
|
-
}
|
|
245
|
-
catch {
|
|
246
|
-
// overwrite invalid JSON
|
|
247
|
-
}
|
|
248
|
-
}
|
|
239
|
+
/** Build the list of MCP tool permissions needed for Greenrun and Playwright. */
|
|
240
|
+
function buildPermissionsList() {
|
|
249
241
|
const greenrunTools = [
|
|
250
242
|
'mcp__greenrun__list_projects',
|
|
251
243
|
'mcp__greenrun__get_project',
|
|
@@ -259,10 +251,13 @@ function installSettings() {
|
|
|
259
251
|
'mcp__greenrun__update_test',
|
|
260
252
|
'mcp__greenrun__start_run',
|
|
261
253
|
'mcp__greenrun__complete_run',
|
|
254
|
+
'mcp__greenrun__batch_complete_runs',
|
|
262
255
|
'mcp__greenrun__get_run',
|
|
263
256
|
'mcp__greenrun__list_runs',
|
|
264
257
|
'mcp__greenrun__sweep',
|
|
265
258
|
'mcp__greenrun__prepare_test_batch',
|
|
259
|
+
'mcp__greenrun__export_test_script',
|
|
260
|
+
'mcp__greenrun__export_test_instructions',
|
|
266
261
|
];
|
|
267
262
|
const browserTools = [
|
|
268
263
|
'mcp__playwright__browser_navigate',
|
|
@@ -270,10 +265,6 @@ function installSettings() {
|
|
|
270
265
|
'mcp__playwright__browser_click',
|
|
271
266
|
'mcp__playwright__browser_type',
|
|
272
267
|
'mcp__playwright__browser_handle_dialog',
|
|
273
|
-
'mcp__playwright__browser_tab_list',
|
|
274
|
-
'mcp__playwright__browser_tab_new',
|
|
275
|
-
'mcp__playwright__browser_tab_select',
|
|
276
|
-
'mcp__playwright__browser_tab_close',
|
|
277
268
|
'mcp__playwright__browser_select_option',
|
|
278
269
|
'mcp__playwright__browser_hover',
|
|
279
270
|
'mcp__playwright__browser_drag',
|
|
@@ -291,11 +282,27 @@ function installSettings() {
|
|
|
291
282
|
'mcp__playwright__browser_tabs',
|
|
292
283
|
'mcp__playwright__browser_network_requests',
|
|
293
284
|
];
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
285
|
+
return [...greenrunTools, ...browserTools];
|
|
286
|
+
}
|
|
287
|
+
function installSettings() {
|
|
288
|
+
const settingsDir = join(process.cwd(), '.claude');
|
|
289
|
+
mkdirSync(settingsDir, { recursive: true });
|
|
290
|
+
const settingsPath = join(settingsDir, 'settings.local.json');
|
|
291
|
+
let existing = {};
|
|
292
|
+
if (existsSync(settingsPath)) {
|
|
293
|
+
try {
|
|
294
|
+
existing = JSON.parse(readFileSync(settingsPath, 'utf-8'));
|
|
295
|
+
}
|
|
296
|
+
catch {
|
|
297
|
+
// overwrite invalid JSON
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
const requiredTools = buildPermissionsList();
|
|
301
|
+
const permissions = (existing.permissions ?? {});
|
|
302
|
+
const currentAllow = (permissions.allow ?? []);
|
|
297
303
|
const merged = [...new Set([...currentAllow, ...requiredTools])];
|
|
298
|
-
|
|
304
|
+
permissions.allow = merged;
|
|
305
|
+
existing.permissions = permissions;
|
|
299
306
|
writeFileSync(settingsPath, JSON.stringify(existing, null, 2) + '\n');
|
|
300
307
|
console.log(' Updated .claude/settings.local.json with tool permissions');
|
|
301
308
|
}
|
package/dist/server.js
CHANGED
|
@@ -1,9 +1,46 @@
|
|
|
1
1
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
2
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
3
|
import { z } from 'zod';
|
|
4
|
-
import { writeFileSync, mkdirSync } from 'node:fs';
|
|
5
|
-
import { dirname } from 'node:path';
|
|
4
|
+
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
5
|
+
import { dirname, join } from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
6
7
|
import { ApiClient } from './api-client.js';
|
|
8
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
+
const __dirname = dirname(__filename);
|
|
10
|
+
/** Remove keys with null or undefined values from an object (shallow). */
|
|
11
|
+
function stripNulls(obj) {
|
|
12
|
+
const result = {};
|
|
13
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
14
|
+
if (value != null) {
|
|
15
|
+
result[key] = value;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return result;
|
|
19
|
+
}
|
|
20
|
+
/** Build a compact MCP text response from data, stripping nulls and using minimal JSON. */
|
|
21
|
+
function jsonResponse(data) {
|
|
22
|
+
const cleaned = data && typeof data === 'object' && !Array.isArray(data)
|
|
23
|
+
? stripNulls(data)
|
|
24
|
+
: data;
|
|
25
|
+
return { content: [{ type: 'text', text: JSON.stringify(cleaned) }] };
|
|
26
|
+
}
|
|
27
|
+
/** Read the package version from package.json. */
|
|
28
|
+
function readPackageVersion() {
|
|
29
|
+
try {
|
|
30
|
+
const pkgPath = join(__dirname, '..', 'package.json');
|
|
31
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
32
|
+
return pkg.version || '0.0.0';
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return '0.0.0';
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/** Shared zod schema for credential sets, used by create_project and update_project. */
|
|
39
|
+
const credentialSchema = z.object({
|
|
40
|
+
name: z.string().describe('Credential set name (e.g. "admin", "viewer")'),
|
|
41
|
+
email: z.string().describe('Login email'),
|
|
42
|
+
password: z.string().describe('Login password'),
|
|
43
|
+
});
|
|
7
44
|
export async function startServer() {
|
|
8
45
|
const GREENRUN_API_URL = process.env.GREENRUN_API_URL || 'https://app.greenrun.dev';
|
|
9
46
|
const GREENRUN_API_TOKEN = process.env.GREENRUN_API_TOKEN;
|
|
@@ -17,12 +54,12 @@ export async function startServer() {
|
|
|
17
54
|
});
|
|
18
55
|
const server = new McpServer({
|
|
19
56
|
name: 'greenrun',
|
|
20
|
-
version:
|
|
57
|
+
version: readPackageVersion(),
|
|
21
58
|
});
|
|
22
59
|
// --- Projects ---
|
|
23
60
|
server.tool('list_projects', 'List all projects', {}, async () => {
|
|
24
61
|
const result = await api.listProjects();
|
|
25
|
-
return
|
|
62
|
+
return jsonResponse(result);
|
|
26
63
|
});
|
|
27
64
|
server.tool('create_project', 'Create a new project', {
|
|
28
65
|
name: z.string().describe('Project name'),
|
|
@@ -33,18 +70,14 @@ export async function startServer() {
|
|
|
33
70
|
register_url: z.string().optional().describe('URL of registration page (for new_user auth mode)'),
|
|
34
71
|
login_instructions: z.string().optional().describe('Steps to log in with existing credentials'),
|
|
35
72
|
register_instructions: z.string().optional().describe('Steps to register a new user'),
|
|
36
|
-
credentials: z.array(
|
|
37
|
-
name: z.string().describe('Credential set name (e.g. "admin", "viewer")'),
|
|
38
|
-
email: z.string().describe('Login email'),
|
|
39
|
-
password: z.string().describe('Login password'),
|
|
40
|
-
})).optional().describe('Named credential sets for test authentication (max 20)'),
|
|
73
|
+
credentials: z.array(credentialSchema).optional().describe('Named credential sets for test authentication (max 20)'),
|
|
41
74
|
}, async (args) => {
|
|
42
75
|
const result = await api.createProject(args);
|
|
43
|
-
return
|
|
76
|
+
return jsonResponse(result);
|
|
44
77
|
});
|
|
45
78
|
server.tool('get_project', 'Get project details', { project_id: z.string().describe('Project UUID') }, async (args) => {
|
|
46
79
|
const result = await api.getProject(args.project_id);
|
|
47
|
-
return
|
|
80
|
+
return jsonResponse(result);
|
|
48
81
|
});
|
|
49
82
|
server.tool('update_project', 'Update project settings', {
|
|
50
83
|
project_id: z.string().describe('Project UUID'),
|
|
@@ -56,20 +89,16 @@ export async function startServer() {
|
|
|
56
89
|
register_url: z.string().optional().describe('URL of registration page (for new_user auth mode)'),
|
|
57
90
|
login_instructions: z.string().optional().describe('Steps to log in with existing credentials'),
|
|
58
91
|
register_instructions: z.string().optional().describe('Steps to register a new user'),
|
|
59
|
-
credentials: z.array(
|
|
60
|
-
name: z.string().describe('Credential set name (e.g. "admin", "viewer")'),
|
|
61
|
-
email: z.string().describe('Login email'),
|
|
62
|
-
password: z.string().describe('Login password'),
|
|
63
|
-
})).optional().describe('Named credential sets for test authentication (max 20)'),
|
|
92
|
+
credentials: z.array(credentialSchema).optional().describe('Named credential sets for test authentication (max 20)'),
|
|
64
93
|
}, async (args) => {
|
|
65
94
|
const { project_id, ...data } = args;
|
|
66
95
|
const result = await api.updateProject(project_id, data);
|
|
67
|
-
return
|
|
96
|
+
return jsonResponse(result);
|
|
68
97
|
});
|
|
69
98
|
// --- Pages ---
|
|
70
99
|
server.tool('list_pages', 'List pages in a project', { project_id: z.string().describe('Project UUID') }, async (args) => {
|
|
71
100
|
const result = await api.listPages(args.project_id);
|
|
72
|
-
return
|
|
101
|
+
return jsonResponse(result);
|
|
73
102
|
});
|
|
74
103
|
server.tool('create_page', 'Register a page URL in a project', {
|
|
75
104
|
project_id: z.string().describe('Project UUID'),
|
|
@@ -77,16 +106,16 @@ export async function startServer() {
|
|
|
77
106
|
name: z.string().optional().describe('Human-friendly page name'),
|
|
78
107
|
}, async (args) => {
|
|
79
108
|
const result = await api.createPage(args.project_id, { url: args.url, name: args.name });
|
|
80
|
-
return
|
|
109
|
+
return jsonResponse(result);
|
|
81
110
|
});
|
|
82
111
|
// --- Tests ---
|
|
83
112
|
server.tool('list_tests', 'List tests in a project (includes latest run status)', { project_id: z.string().describe('Project UUID') }, async (args) => {
|
|
84
113
|
const result = await api.listTests(args.project_id);
|
|
85
|
-
return
|
|
114
|
+
return jsonResponse(result);
|
|
86
115
|
});
|
|
87
116
|
server.tool('get_test', 'Get test details including instructions, pages, and recent runs', { test_id: z.string().describe('Test UUID') }, async (args) => {
|
|
88
117
|
const result = await api.getTest(args.test_id);
|
|
89
|
-
return
|
|
118
|
+
return jsonResponse(result);
|
|
90
119
|
});
|
|
91
120
|
server.tool('create_test', 'Store a new test case in a project', {
|
|
92
121
|
project_id: z.string().describe('Project UUID'),
|
|
@@ -105,7 +134,7 @@ export async function startServer() {
|
|
|
105
134
|
tags: args.tags,
|
|
106
135
|
credential_name: args.credential_name,
|
|
107
136
|
});
|
|
108
|
-
return
|
|
137
|
+
return jsonResponse(result);
|
|
109
138
|
});
|
|
110
139
|
server.tool('update_test', 'Update test instructions, name, status, or page associations', {
|
|
111
140
|
test_id: z.string().describe('Test UUID'),
|
|
@@ -119,8 +148,8 @@ export async function startServer() {
|
|
|
119
148
|
script_generated_at: z.string().optional().nullable().describe('ISO timestamp when the script was generated'),
|
|
120
149
|
}, async (args) => {
|
|
121
150
|
const { test_id, ...data } = args;
|
|
122
|
-
|
|
123
|
-
return {
|
|
151
|
+
await api.updateTest(test_id, data);
|
|
152
|
+
return jsonResponse({ success: true });
|
|
124
153
|
});
|
|
125
154
|
server.tool('export_test_script', 'Fetch a test\'s cached Playwright script and write it directly to a file. The script content is never returned — only a confirmation. Use this to export scripts without consuming context.', {
|
|
126
155
|
test_id: z.string().describe('Test UUID'),
|
|
@@ -135,6 +164,20 @@ export async function startServer() {
|
|
|
135
164
|
writeFileSync(args.file_path, script, 'utf-8');
|
|
136
165
|
return { content: [{ type: 'text', text: `Script written to ${args.file_path} (${script.length} chars)` }] };
|
|
137
166
|
});
|
|
167
|
+
server.tool('export_test_instructions', 'Fetch a test\'s instructions and write them to a local file. The instructions are never returned — only a confirmation. Use this so agents can read instructions from disk instead of receiving them through MCP context.', {
|
|
168
|
+
test_id: z.string().describe('Test UUID'),
|
|
169
|
+
file_path: z.string().describe('Absolute file path to write the instructions to (e.g. /tmp/greenrun-tests/{test_id}.instructions.md)'),
|
|
170
|
+
}, async (args) => {
|
|
171
|
+
const result = await api.getTest(args.test_id);
|
|
172
|
+
const instructions = result.test?.instructions;
|
|
173
|
+
if (!instructions) {
|
|
174
|
+
return { content: [{ type: 'text', text: `No instructions found for test ${args.test_id}` }] };
|
|
175
|
+
}
|
|
176
|
+
mkdirSync(dirname(args.file_path), { recursive: true });
|
|
177
|
+
const header = result.test.name ? `# ${result.test.name}\n\n` : '';
|
|
178
|
+
writeFileSync(args.file_path, header + instructions, 'utf-8');
|
|
179
|
+
return { content: [{ type: 'text', text: `Instructions written to ${args.file_path} (${instructions.length} chars)` }] };
|
|
180
|
+
});
|
|
138
181
|
// --- Sweep ---
|
|
139
182
|
server.tool('sweep', 'Find tests affected by specific pages (impact analysis). Use after making changes to determine which tests to re-run.', {
|
|
140
183
|
project_id: z.string().describe('Project UUID'),
|
|
@@ -145,7 +188,7 @@ export async function startServer() {
|
|
|
145
188
|
pages: args.pages,
|
|
146
189
|
url_pattern: args.url_pattern,
|
|
147
190
|
});
|
|
148
|
-
return
|
|
191
|
+
return jsonResponse(result);
|
|
149
192
|
});
|
|
150
193
|
// --- Batch ---
|
|
151
194
|
server.tool('prepare_test_batch', 'Prepare a batch of tests for execution: lists tests, filters, fetches full details, and starts runs — all in one call. Returns everything needed to execute tests.', {
|
|
@@ -155,35 +198,46 @@ export async function startServer() {
|
|
|
155
198
|
}, async (args) => {
|
|
156
199
|
try {
|
|
157
200
|
const result = await api.prepareTestBatch(args.project_id, args.filter, args.test_ids);
|
|
158
|
-
return
|
|
201
|
+
return jsonResponse(result);
|
|
159
202
|
}
|
|
160
203
|
catch (error) {
|
|
161
|
-
|
|
204
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
205
|
+
return { content: [{ type: 'text', text: `Error: ${message}` }], isError: true };
|
|
162
206
|
}
|
|
163
207
|
});
|
|
164
208
|
// --- Test Runs ---
|
|
165
209
|
server.tool('start_run', 'Start a test run (sets status to running)', { test_id: z.string().describe('Test UUID') }, async (args) => {
|
|
166
210
|
const result = await api.startRun(args.test_id);
|
|
167
|
-
return {
|
|
211
|
+
return jsonResponse({ run_id: result.run.id });
|
|
168
212
|
});
|
|
169
213
|
server.tool('complete_run', 'Record the result of a test run', {
|
|
170
214
|
run_id: z.string().describe('Run UUID'),
|
|
171
215
|
status: z.enum(['passed', 'failed', 'error']).describe('Run result status'),
|
|
172
216
|
result: z.string().optional().describe('Summary of what happened during the run'),
|
|
173
217
|
}, async (args) => {
|
|
174
|
-
|
|
218
|
+
await api.completeRun(args.run_id, {
|
|
175
219
|
status: args.status,
|
|
176
220
|
result: args.result,
|
|
177
221
|
});
|
|
178
|
-
return {
|
|
222
|
+
return jsonResponse({ success: true });
|
|
223
|
+
});
|
|
224
|
+
server.tool('batch_complete_runs', 'Record results for multiple test runs in a single call. More efficient than calling complete_run individually.', {
|
|
225
|
+
runs: z.array(z.object({
|
|
226
|
+
run_id: z.string().describe('Run UUID'),
|
|
227
|
+
status: z.enum(['passed', 'failed', 'error']).describe('Run result status'),
|
|
228
|
+
result: z.string().optional().describe('Summary of what happened'),
|
|
229
|
+
})).describe('Array of run results to complete'),
|
|
230
|
+
}, async (args) => {
|
|
231
|
+
const result = await api.batchCompleteRuns(args.runs);
|
|
232
|
+
return jsonResponse(result);
|
|
179
233
|
});
|
|
180
234
|
server.tool('get_run', 'Get details of a specific test run', { run_id: z.string().describe('Run UUID') }, async (args) => {
|
|
181
235
|
const result = await api.getRun(args.run_id);
|
|
182
|
-
return
|
|
236
|
+
return jsonResponse(result);
|
|
183
237
|
});
|
|
184
238
|
server.tool('list_runs', 'List run history for a test (newest first)', { test_id: z.string().describe('Test UUID') }, async (args) => {
|
|
185
239
|
const result = await api.listRuns(args.test_id);
|
|
186
|
-
return
|
|
240
|
+
return jsonResponse(result);
|
|
187
241
|
});
|
|
188
242
|
// Start server
|
|
189
243
|
const transport = new StdioServerTransport();
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "greenrun-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
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",
|
|
7
7
|
"bin": {
|
|
8
|
-
"greenrun": "dist/cli.js"
|
|
8
|
+
"greenrun": "dist/cli.js",
|
|
9
|
+
"greenrun-cli": "dist/cli.js"
|
|
9
10
|
},
|
|
10
11
|
"files": [
|
|
11
12
|
"dist",
|
package/templates/claude-md.md
CHANGED
|
@@ -12,9 +12,11 @@ The Greenrun MCP server provides these tools:
|
|
|
12
12
|
- **list_projects** / **get_project** / **create_project** - Manage projects (includes auth configuration)
|
|
13
13
|
- **list_pages** / **create_page** - Manage page URLs within a project
|
|
14
14
|
- **list_tests** / **get_test** / **create_test** / **update_test** - Manage test cases
|
|
15
|
-
- **start_run** / **complete_run** / **get_run** / **list_runs** - Execute and track test runs
|
|
15
|
+
- **start_run** / **complete_run** / **batch_complete_runs** / **get_run** / **list_runs** - Execute and track test runs
|
|
16
16
|
- **sweep** - Impact analysis: find tests affected by changed pages
|
|
17
17
|
- **prepare_test_batch** - Batch prepare tests for execution (lists, filters, fetches details, starts runs in one call)
|
|
18
|
+
- **export_test_script** - Write a test's cached Playwright script to a local file (keeps scripts out of context)
|
|
19
|
+
- **export_test_instructions** - Write a test's instructions to a local file (keeps instructions out of context)
|
|
18
20
|
|
|
19
21
|
### Running Tests
|
|
20
22
|
|
|
@@ -25,7 +25,7 @@ If auth fails (login form still visible after following instructions), report al
|
|
|
25
25
|
|
|
26
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`, `credential_name`, `pages`, `tags`, `has_script`).
|
|
27
27
|
|
|
28
|
-
Note: The batch does not include `instructions` or `script` content. Use `
|
|
28
|
+
Note: The batch does not include `instructions` or `script` content. Use `export_test_instructions(test_id, file_path)` to write instructions to disk — agents read from the file instead of receiving them through MCP context.
|
|
29
29
|
|
|
30
30
|
If `tests` is empty, tell the user no matching active tests were found and stop.
|
|
31
31
|
|
|
@@ -44,7 +44,10 @@ If all tests are scripted, skip to Step 4.
|
|
|
44
44
|
|
|
45
45
|
### Step 3: Generate scripts for unscripted tests
|
|
46
46
|
|
|
47
|
-
For each **unscripted** test
|
|
47
|
+
For each **unscripted** test:
|
|
48
|
+
|
|
49
|
+
1. Call `export_test_instructions(test_id, "/tmp/greenrun-tests/{test_id}.instructions.md")` to write instructions to disk
|
|
50
|
+
2. Launch a Task agent sequentially (one at a time, wait for each to complete before starting the next). This keeps browser snapshot data out of the parent context.
|
|
48
51
|
|
|
49
52
|
```
|
|
50
53
|
Task tool with:
|
|
@@ -68,7 +71,7 @@ Credentials: {credential_name} — email: {email}, password: {password}
|
|
|
68
71
|
|
|
69
72
|
## Task
|
|
70
73
|
|
|
71
|
-
1.
|
|
74
|
+
1. Read the test instructions from `/tmp/greenrun-tests/{test_id}.instructions.md` (exported before agent launch)
|
|
72
75
|
2. Authenticate: navigate to {login_url} and log in with the credential above using `browser_navigate`, `browser_snapshot`, `browser_click`, `browser_type`
|
|
73
76
|
3. Do a scouting pass — follow the test instructions step by step in the browser:
|
|
74
77
|
- Navigate to the test's starting page via `browser_navigate`
|
|
@@ -153,7 +156,7 @@ npx playwright test --config /tmp/greenrun-tests/playwright.config.ts
|
|
|
153
156
|
|
|
154
157
|
**3. Parse results**: Read `/tmp/greenrun-tests/results.json`. Map each result back to a run ID via the filename: `{test_id}.spec.ts` → test_id → find the matching run_id from the batch.
|
|
155
158
|
|
|
156
|
-
**4. Report results**: Call `
|
|
159
|
+
**4. Report results**: Call `batch_complete_runs` with all results at once. Map Playwright statuses: `passed` → `passed`, `failed`/`timedOut` → `failed`, other → `error`. Example: `batch_complete_runs({ runs: [{ run_id, status, result }] })`.
|
|
157
160
|
|
|
158
161
|
**5. Clean up**: Call `browser_close` to reset the MCP browser context.
|
|
159
162
|
|
|
@@ -170,7 +173,10 @@ After parsing all native results, walk through them in completion order. Track c
|
|
|
170
173
|
|
|
171
174
|
For tests that **failed** in native execution (and circuit breaker has not tripped), execute them one at a time via Task agents. This keeps snapshot data out of the parent context.
|
|
172
175
|
|
|
173
|
-
For each failed test
|
|
176
|
+
For each failed test:
|
|
177
|
+
|
|
178
|
+
1. Call `export_test_instructions(test_id, "/tmp/greenrun-tests/{test_id}.instructions.md")` to write instructions to disk
|
|
179
|
+
2. Launch a Task agent sequentially (wait for each to complete before the next):
|
|
174
180
|
|
|
175
181
|
```
|
|
176
182
|
Task tool with:
|
|
@@ -193,7 +199,7 @@ Native execution failed with: {failure_message}
|
|
|
193
199
|
|
|
194
200
|
## Task
|
|
195
201
|
|
|
196
|
-
1.
|
|
202
|
+
1. Read the test instructions from `/tmp/greenrun-tests/{test_id}.instructions.md` (exported before agent launch)
|
|
197
203
|
2. Start a new run: `start_run("{test_id}")` — note the run_id
|
|
198
204
|
3. Authenticate: navigate to {login_url} and log in with the credential above
|
|
199
205
|
4. Follow the test instructions step by step using Playwright MCP tools (`browser_navigate`, `browser_snapshot`, `browser_click`, `browser_type`)
|