@debugg-ai/debugg-ai-mcp 2.6.1 → 2.8.1
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/CHANGELOG.md +15 -0
- package/dist/config/index.js +11 -1
- package/dist/handlers/createTestCaseHandler.js +46 -0
- package/dist/handlers/createTestSuiteHandler.js +31 -0
- package/dist/handlers/deleteTestCaseHandler.js +20 -0
- package/dist/handlers/deleteTestSuiteHandler.js +38 -0
- package/dist/handlers/getTestSuiteResultsHandler.js +38 -0
- package/dist/handlers/index.js +8 -0
- package/dist/handlers/probePageHandler.js +48 -41
- package/dist/handlers/runTestSuiteHandler.js +122 -0
- package/dist/handlers/searchTestSuitesHandler.js +36 -0
- package/dist/handlers/testPageChangesHandler.js +63 -57
- package/dist/handlers/triggerCrawlHandler.js +43 -37
- package/dist/handlers/updateTestCaseHandler.js +24 -0
- package/dist/services/index.js +145 -0
- package/dist/services/workflows.js +17 -1
- package/dist/tools/index.js +17 -0
- package/dist/tools/testSuiteTools.js +183 -0
- package/dist/types/index.js +51 -0
- package/dist/utils/resolveProject.js +27 -0
- package/package.json +1 -1
|
@@ -125,69 +125,75 @@ async function testPageChangesHandlerInner(input, context, rawProgressCallback)
|
|
|
125
125
|
return { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }], isError: true };
|
|
126
126
|
}
|
|
127
127
|
}
|
|
128
|
-
if (
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const reused = findExistingTunnel(ctx);
|
|
132
|
-
if (reused) {
|
|
133
|
-
ctx = reused;
|
|
134
|
-
logger.info(`Reusing tunnel: ${ctx.targetUrl} (id: ${ctx.tunnelId})`);
|
|
128
|
+
if (config.devMode) {
|
|
129
|
+
// Dev mode: local backend can reach localhost directly — no tunnel needed.
|
|
130
|
+
logger.info(`check_app_in_browser: dev mode — using localhost URL directly: ${ctx.originalUrl}`);
|
|
135
131
|
}
|
|
136
132
|
else {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
tunnel = await client.tunnels.provisionWithRetry();
|
|
140
|
-
}
|
|
141
|
-
catch (provisionError) {
|
|
142
|
-
const msg = provisionError instanceof Error ? provisionError.message : String(provisionError);
|
|
143
|
-
const diag = provisionError instanceof TunnelProvisionError ? ` ${provisionError.diagnosticSuffix()}` : '';
|
|
144
|
-
throw new Error(`Failed to provision tunnel for ${ctx.originalUrl}. ` +
|
|
145
|
-
`The remote browser needs a secure tunnel to reach your local dev server. ` +
|
|
146
|
-
`Make sure your dev server is running on the specified port and try again. ` +
|
|
147
|
-
`(Detail: ${msg})${diag}`);
|
|
133
|
+
if (progressCallback) {
|
|
134
|
+
await progressCallback({ progress: 1, total: TOTAL_STEPS, message: 'Provisioning secure tunnel for localhost...' });
|
|
148
135
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
ctx =
|
|
136
|
+
const reused = findExistingTunnel(ctx);
|
|
137
|
+
if (reused) {
|
|
138
|
+
ctx = reused;
|
|
139
|
+
logger.info(`Reusing tunnel: ${ctx.targetUrl} (id: ${ctx.tunnelId})`);
|
|
152
140
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
141
|
+
else {
|
|
142
|
+
let tunnel;
|
|
143
|
+
try {
|
|
144
|
+
tunnel = await client.tunnels.provisionWithRetry();
|
|
145
|
+
}
|
|
146
|
+
catch (provisionError) {
|
|
147
|
+
const msg = provisionError instanceof Error ? provisionError.message : String(provisionError);
|
|
148
|
+
const diag = provisionError instanceof TunnelProvisionError ? ` ${provisionError.diagnosticSuffix()}` : '';
|
|
149
|
+
throw new Error(`Failed to provision tunnel for ${ctx.originalUrl}. ` +
|
|
150
|
+
`The remote browser needs a secure tunnel to reach your local dev server. ` +
|
|
151
|
+
`Make sure your dev server is running on the specified port and try again. ` +
|
|
152
|
+
`(Detail: ${msg})${diag}`);
|
|
153
|
+
}
|
|
154
|
+
keyId = tunnel.keyId;
|
|
155
|
+
try {
|
|
156
|
+
ctx = await ensureTunnel(ctx, tunnel.tunnelKey, tunnel.tunnelId, tunnel.keyId, () => client.revokeNgrokKey(tunnel.keyId));
|
|
157
|
+
}
|
|
158
|
+
catch (tunnelError) {
|
|
159
|
+
const msg = tunnelError instanceof Error ? tunnelError.message : String(tunnelError);
|
|
160
|
+
throw new Error(`Tunnel creation failed for ${ctx.originalUrl}. ` +
|
|
161
|
+
`Could not establish a secure connection between the remote browser and your local port. ` +
|
|
162
|
+
`Verify your dev server is running and the port is accessible. ` +
|
|
163
|
+
`(Detail: ${msg})`);
|
|
164
|
+
}
|
|
165
|
+
logger.info(`Tunnel ready: ${ctx.targetUrl} (id: ${ctx.tunnelId})`);
|
|
159
166
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
167
|
+
// Bead 1om: verify traffic actually flows through the tunnel. The
|
|
168
|
+
// tunnel can be established (ngrok.connect returns OK) yet refuse
|
|
169
|
+
// to forward traffic — e.g., IPv4/IPv6 bind mismatch, or the dev
|
|
170
|
+
// server died between the pre-flight probe and here. Catch it now,
|
|
171
|
+
// in ~1s, not via a 5-minute browser-agent false-pass.
|
|
172
|
+
if (ctx.targetUrl) {
|
|
173
|
+
const health = await probeTunnelHealth(ctx.targetUrl);
|
|
174
|
+
if (!health.healthy) {
|
|
175
|
+
const payload = {
|
|
176
|
+
error: 'TunnelTrafficBlocked',
|
|
177
|
+
message: `Tunnel was established but traffic isn't reaching the dev server. ${health.detail ?? ''} Common causes: dev server binds to 0.0.0.0 or ::1 but not 127.0.0.1; dev server crashed; firewall.`,
|
|
178
|
+
detail: {
|
|
179
|
+
code: health.code,
|
|
180
|
+
status: health.status,
|
|
181
|
+
ngrokErrorCode: health.ngrokErrorCode,
|
|
182
|
+
elapsedMs: health.elapsedMs,
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
logger.warn(`Tunnel health probe failed for ${ctx.targetUrl}: ${health.code} ${health.ngrokErrorCode ?? ''} in ${health.elapsedMs}ms`);
|
|
186
|
+
// Tear down the broken tunnel so a subsequent call doesn't reuse it.
|
|
187
|
+
// stopTunnel handles both owned (ngrok disconnect + key revoke) and
|
|
188
|
+
// borrowed (just drops local ref) cases.
|
|
189
|
+
if (ctx.tunnelId) {
|
|
190
|
+
tunnelManager.stopTunnel(ctx.tunnelId).catch((err) => logger.warn(`Failed to stop broken tunnel ${ctx.tunnelId}: ${err}`));
|
|
191
|
+
}
|
|
192
|
+
// keyId is consumed by stopTunnel's revoke path; clear so the
|
|
193
|
+
// outer finally block doesn't double-revoke.
|
|
194
|
+
keyId = undefined;
|
|
195
|
+
return { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }], isError: true };
|
|
186
196
|
}
|
|
187
|
-
// keyId is consumed by stopTunnel's revoke path; clear so the
|
|
188
|
-
// outer finally block doesn't double-revoke.
|
|
189
|
-
keyId = undefined;
|
|
190
|
-
return { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }], isError: true };
|
|
191
197
|
}
|
|
192
198
|
}
|
|
193
199
|
}
|
|
@@ -82,48 +82,54 @@ export async function triggerCrawlHandler(input, context, rawProgressCallback) {
|
|
|
82
82
|
return { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }], isError: true };
|
|
83
83
|
}
|
|
84
84
|
}
|
|
85
|
-
if (
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const reused = findExistingTunnel(ctx);
|
|
89
|
-
if (reused) {
|
|
90
|
-
ctx = reused;
|
|
85
|
+
if (config.devMode) {
|
|
86
|
+
// Dev mode: local backend can reach localhost directly — no tunnel needed.
|
|
87
|
+
logger.info(`trigger_crawl: dev mode — using localhost URL directly: ${ctx.originalUrl}`);
|
|
91
88
|
}
|
|
92
89
|
else {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
tunnel = await client.tunnels.provisionWithRetry();
|
|
90
|
+
if (progressCallback) {
|
|
91
|
+
await progressCallback({ progress: 1, total: 4, message: 'Provisioning secure tunnel for localhost...' });
|
|
96
92
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
throw new Error(`Failed to provision tunnel for ${ctx.originalUrl}. ` +
|
|
101
|
-
`The remote browser needs a secure tunnel to reach your local dev server. ` +
|
|
102
|
-
`(Detail: ${msg})${diag}`);
|
|
93
|
+
const reused = findExistingTunnel(ctx);
|
|
94
|
+
if (reused) {
|
|
95
|
+
ctx = reused;
|
|
103
96
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
if (
|
|
123
|
-
|
|
97
|
+
else {
|
|
98
|
+
let tunnel;
|
|
99
|
+
try {
|
|
100
|
+
tunnel = await client.tunnels.provisionWithRetry();
|
|
101
|
+
}
|
|
102
|
+
catch (provisionError) {
|
|
103
|
+
const msg = provisionError instanceof Error ? provisionError.message : String(provisionError);
|
|
104
|
+
const diag = provisionError instanceof TunnelProvisionError ? ` ${provisionError.diagnosticSuffix()}` : '';
|
|
105
|
+
throw new Error(`Failed to provision tunnel for ${ctx.originalUrl}. ` +
|
|
106
|
+
`The remote browser needs a secure tunnel to reach your local dev server. ` +
|
|
107
|
+
`(Detail: ${msg})${diag}`);
|
|
108
|
+
}
|
|
109
|
+
keyId = tunnel.keyId;
|
|
110
|
+
ctx = await ensureTunnel(ctx, tunnel.tunnelKey, tunnel.tunnelId, tunnel.keyId, () => client.revokeNgrokKey(tunnel.keyId));
|
|
111
|
+
}
|
|
112
|
+
// Bead 1om: post-tunnel health check — verify traffic actually flows.
|
|
113
|
+
if (ctx.targetUrl) {
|
|
114
|
+
const health = await probeTunnelHealth(ctx.targetUrl);
|
|
115
|
+
if (!health.healthy) {
|
|
116
|
+
const payload = {
|
|
117
|
+
error: 'TunnelTrafficBlocked',
|
|
118
|
+
message: `Tunnel was established but traffic isn't reaching the dev server. ${health.detail ?? ''} Common causes: dev server binds to 0.0.0.0 or ::1 but not 127.0.0.1; dev server crashed; firewall.`,
|
|
119
|
+
detail: {
|
|
120
|
+
code: health.code,
|
|
121
|
+
status: health.status,
|
|
122
|
+
ngrokErrorCode: health.ngrokErrorCode,
|
|
123
|
+
elapsedMs: health.elapsedMs,
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
logger.warn(`Tunnel health probe failed for ${ctx.targetUrl}: ${health.code} ${health.ngrokErrorCode ?? ''} in ${health.elapsedMs}ms`);
|
|
127
|
+
if (ctx.tunnelId) {
|
|
128
|
+
tunnelManager.stopTunnel(ctx.tunnelId).catch((err) => logger.warn(`Failed to stop broken tunnel ${ctx.tunnelId}: ${err}`));
|
|
129
|
+
}
|
|
130
|
+
keyId = undefined;
|
|
131
|
+
return { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }], isError: true };
|
|
124
132
|
}
|
|
125
|
-
keyId = undefined;
|
|
126
|
-
return { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }], isError: true };
|
|
127
133
|
}
|
|
128
134
|
}
|
|
129
135
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Logger } from '../utils/logger.js';
|
|
2
|
+
import { handleExternalServiceError } from '../utils/errors.js';
|
|
3
|
+
import { DebuggAIServerClient } from '../services/index.js';
|
|
4
|
+
import { config } from '../config/index.js';
|
|
5
|
+
const logger = new Logger({ module: 'updateTestCaseHandler' });
|
|
6
|
+
export async function updateTestCaseHandler(input, _context) {
|
|
7
|
+
const start = Date.now();
|
|
8
|
+
logger.toolStart('update_test_case', input);
|
|
9
|
+
try {
|
|
10
|
+
const client = new DebuggAIServerClient(config.api.key);
|
|
11
|
+
await client.init();
|
|
12
|
+
const updated = await client.updateTestCase(input.testUuid, {
|
|
13
|
+
name: input.name,
|
|
14
|
+
description: input.description,
|
|
15
|
+
agentTaskDescription: input.agentTaskDescription,
|
|
16
|
+
});
|
|
17
|
+
logger.toolComplete('update_test_case', Date.now() - start);
|
|
18
|
+
return { content: [{ type: 'text', text: JSON.stringify(updated, null, 2) }] };
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
logger.toolError('update_test_case', error, Date.now() - start);
|
|
22
|
+
throw handleExternalServiceError(error, 'DebuggAI', 'update_test_case');
|
|
23
|
+
}
|
|
24
|
+
}
|
package/dist/services/index.js
CHANGED
|
@@ -430,6 +430,151 @@ export class DebuggAIServerClient {
|
|
|
430
430
|
throw new Error('Client not initialized — call init() first');
|
|
431
431
|
await this.tx.post('api/v1/ngrok/revoke/', { ngrokKeyId });
|
|
432
432
|
}
|
|
433
|
+
// ── E2E Suite Management ──────────────────────────────────────────────────
|
|
434
|
+
async createTestSuite(input) {
|
|
435
|
+
if (!this.tx)
|
|
436
|
+
throw new Error('Client not initialized — call init() first');
|
|
437
|
+
const s = await this.tx.post('api/v1/test-suites/', {
|
|
438
|
+
name: input.name,
|
|
439
|
+
description: input.description,
|
|
440
|
+
project: input.projectUuid,
|
|
441
|
+
});
|
|
442
|
+
return this.mapTestSuite(s);
|
|
443
|
+
}
|
|
444
|
+
async listTestSuites(params) {
|
|
445
|
+
if (!this.tx)
|
|
446
|
+
throw new Error('Client not initialized — call init() first');
|
|
447
|
+
const { makePageInfo } = await import('../utils/pagination.js');
|
|
448
|
+
const page = params.page ?? 1;
|
|
449
|
+
const pageSize = params.pageSize ?? 20;
|
|
450
|
+
const query = { project: params.projectUuid, page, pageSize };
|
|
451
|
+
if (params.search)
|
|
452
|
+
query.search = params.search;
|
|
453
|
+
const response = await this.tx.get('api/v1/test-suites/', query);
|
|
454
|
+
return {
|
|
455
|
+
pageInfo: makePageInfo(page, pageSize, response?.count ?? 0, response?.next),
|
|
456
|
+
suites: (response?.results ?? []).map((s) => ({
|
|
457
|
+
uuid: s.uuid,
|
|
458
|
+
name: s.name,
|
|
459
|
+
description: s.description ?? null,
|
|
460
|
+
runStatus: s.runStatus ?? s.run_status ?? 'NEVER_RUN',
|
|
461
|
+
testsCount: s.testsCount ?? s.tests_count ?? 0,
|
|
462
|
+
passRate: s.passRate ?? s.pass_rate ?? null,
|
|
463
|
+
lastRunAt: s.lastRunAt ?? s.last_run_at ?? null,
|
|
464
|
+
})),
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
async disableTestSuite(suiteUuid) {
|
|
468
|
+
if (!this.tx)
|
|
469
|
+
throw new Error('Client not initialized — call init() first');
|
|
470
|
+
await this.tx.post(`api/v1/test-suites/${suiteUuid}/disable/`, {});
|
|
471
|
+
return { uuid: suiteUuid, isDisabled: true };
|
|
472
|
+
}
|
|
473
|
+
async createTestCase(input) {
|
|
474
|
+
if (!this.tx)
|
|
475
|
+
throw new Error('Client not initialized — call init() first');
|
|
476
|
+
const body = {
|
|
477
|
+
name: input.name,
|
|
478
|
+
description: input.description,
|
|
479
|
+
agent_task_description: input.agentTaskDescription,
|
|
480
|
+
suite: input.suiteUuid,
|
|
481
|
+
project: input.projectUuid,
|
|
482
|
+
run: false,
|
|
483
|
+
};
|
|
484
|
+
if (input.relativeUrl)
|
|
485
|
+
body.relative_url = input.relativeUrl;
|
|
486
|
+
if (input.maxSteps)
|
|
487
|
+
body.max_steps = input.maxSteps;
|
|
488
|
+
const t = await this.tx.post('api/v1/e2e-tests/', body);
|
|
489
|
+
return {
|
|
490
|
+
uuid: t.uuid,
|
|
491
|
+
name: t.name,
|
|
492
|
+
description: t.description,
|
|
493
|
+
agentTaskDescription: t.agentTaskDescription ?? t.agent_task_description ?? '',
|
|
494
|
+
suite: t.suite ?? input.suiteUuid,
|
|
495
|
+
project: t.project ?? input.projectUuid,
|
|
496
|
+
runCount: t.runCount ?? t.run_count ?? 0,
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
async updateTestCase(testUuid, patch) {
|
|
500
|
+
if (!this.tx)
|
|
501
|
+
throw new Error('Client not initialized — call init() first');
|
|
502
|
+
const body = {};
|
|
503
|
+
if (patch.name !== undefined)
|
|
504
|
+
body.name = patch.name;
|
|
505
|
+
if (patch.description !== undefined)
|
|
506
|
+
body.description = patch.description;
|
|
507
|
+
if (patch.agentTaskDescription !== undefined)
|
|
508
|
+
body.agent_task_description = patch.agentTaskDescription;
|
|
509
|
+
const t = await this.tx.patch(`api/v1/e2e-tests/${testUuid}/`, body);
|
|
510
|
+
return {
|
|
511
|
+
uuid: t.uuid,
|
|
512
|
+
name: t.name,
|
|
513
|
+
description: t.description,
|
|
514
|
+
agentTaskDescription: t.agentTaskDescription ?? t.agent_task_description ?? '',
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
async disableTestCase(testUuid) {
|
|
518
|
+
if (!this.tx)
|
|
519
|
+
throw new Error('Client not initialized — call init() first');
|
|
520
|
+
await this.tx.post(`api/v1/e2e-tests/${testUuid}/disable/`, {});
|
|
521
|
+
return { uuid: testUuid, isDisabled: true };
|
|
522
|
+
}
|
|
523
|
+
async runTestSuite(suiteUuid, params) {
|
|
524
|
+
if (!this.tx)
|
|
525
|
+
throw new Error('Client not initialized — call init() first');
|
|
526
|
+
const body = {};
|
|
527
|
+
if (params.targetUrl)
|
|
528
|
+
body.target_url = params.targetUrl;
|
|
529
|
+
const s = await this.tx.post(`api/v1/test-suites/${suiteUuid}/run/`, body);
|
|
530
|
+
return {
|
|
531
|
+
suiteUuid,
|
|
532
|
+
runStatus: s?.runStatus ?? s?.run_status ?? 'PENDING',
|
|
533
|
+
testsTriggered: (s?.tests ?? []).length,
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
async getTestSuiteDetail(suiteUuid) {
|
|
537
|
+
if (!this.tx)
|
|
538
|
+
throw new Error('Client not initialized — call init() first');
|
|
539
|
+
const s = await this.tx.get(`api/v1/test-suites/${suiteUuid}/`);
|
|
540
|
+
const tests = s.tests ?? [];
|
|
541
|
+
return {
|
|
542
|
+
uuid: s.uuid,
|
|
543
|
+
name: s.name,
|
|
544
|
+
runStatus: s.runStatus ?? s.run_status ?? 'NEVER_RUN',
|
|
545
|
+
testsCount: tests.length,
|
|
546
|
+
passRate: s.passRate ?? s.pass_rate ?? null,
|
|
547
|
+
lastRunAt: s.lastRunAt ?? s.last_run_at ?? null,
|
|
548
|
+
tests: tests.map((t) => {
|
|
549
|
+
// Backend returns cur_run (latest run) per test in the suite detail view
|
|
550
|
+
const lastRun = t.curRun ?? t.cur_run ?? t.lastRun ?? t.last_run ?? null;
|
|
551
|
+
return {
|
|
552
|
+
uuid: t.uuid,
|
|
553
|
+
name: t.name,
|
|
554
|
+
runCount: t.runCount ?? t.run_count ?? 0,
|
|
555
|
+
passedRunsCount: t.passedRunsCount ?? t.passed_runs_count ?? 0,
|
|
556
|
+
failedRunsCount: t.failedRunsCount ?? t.failed_runs_count ?? 0,
|
|
557
|
+
passRate: t.passRate ?? t.pass_rate ?? null,
|
|
558
|
+
lastRun: lastRun ? {
|
|
559
|
+
uuid: lastRun.uuid,
|
|
560
|
+
status: lastRun.status,
|
|
561
|
+
outcome: lastRun.outcome,
|
|
562
|
+
executionTime: lastRun.executionTime ?? lastRun.execution_time ?? null,
|
|
563
|
+
timestamp: lastRun.timestamp,
|
|
564
|
+
} : null,
|
|
565
|
+
};
|
|
566
|
+
}),
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
mapTestSuite(s) {
|
|
570
|
+
return {
|
|
571
|
+
uuid: s.uuid,
|
|
572
|
+
name: s.name,
|
|
573
|
+
description: s.description ?? null,
|
|
574
|
+
runStatus: s.runStatus ?? s.run_status ?? 'NEVER_RUN',
|
|
575
|
+
testsCount: s.testsCount ?? s.tests_count ?? 0,
|
|
576
|
+
};
|
|
577
|
+
}
|
|
433
578
|
}
|
|
434
579
|
/**
|
|
435
580
|
* Create and initialize a service client
|
|
@@ -29,7 +29,23 @@ export const createWorkflowsService = (tx) => {
|
|
|
29
29
|
return match;
|
|
30
30
|
},
|
|
31
31
|
async findEvaluationTemplate() {
|
|
32
|
-
|
|
32
|
+
// Try keywords in priority order — allows prod ('app evaluation') and
|
|
33
|
+
// dev backends with different naming ('Browser Use Evaluation Workflow Template')
|
|
34
|
+
// to both resolve without config changes. 'evaluation workflow' is specific
|
|
35
|
+
// enough to exclude 'Browser Use Evaluation Brain'.
|
|
36
|
+
const envOverride = process.env.DEBUGGAI_EVAL_TEMPLATE;
|
|
37
|
+
const keywords = envOverride
|
|
38
|
+
? [envOverride]
|
|
39
|
+
: ['app evaluation', 'evaluation workflow'];
|
|
40
|
+
for (const keyword of keywords) {
|
|
41
|
+
try {
|
|
42
|
+
return await service.findTemplateByName(keyword);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// keyword not matched on this backend, try next
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
33
49
|
},
|
|
34
50
|
async executeWorkflow(workflowUuid, contextData, env) {
|
|
35
51
|
const body = { contextData };
|
package/dist/tools/index.js
CHANGED
|
@@ -10,6 +10,7 @@ import { buildDeleteEnvironmentTool, buildValidatedDeleteEnvironmentTool } from
|
|
|
10
10
|
import { buildUpdateProjectTool, buildValidatedUpdateProjectTool } from './updateProject.js';
|
|
11
11
|
import { buildDeleteProjectTool, buildValidatedDeleteProjectTool } from './deleteProject.js';
|
|
12
12
|
import { buildCreateProjectTool, buildValidatedCreateProjectTool } from './createProject.js';
|
|
13
|
+
import { buildCreateTestSuiteTool, buildValidatedCreateTestSuiteTool, buildSearchTestSuitesTool, buildValidatedSearchTestSuitesTool, buildDeleteTestSuiteTool, buildValidatedDeleteTestSuiteTool, buildCreateTestCaseTool, buildValidatedCreateTestCaseTool, buildUpdateTestCaseTool, buildValidatedUpdateTestCaseTool, buildDeleteTestCaseTool, buildValidatedDeleteTestCaseTool, buildRunTestSuiteTool, buildValidatedRunTestSuiteTool, buildGetTestSuiteResultsTool, buildValidatedGetTestSuiteResultsTool, } from './testSuiteTools.js';
|
|
13
14
|
let _tools = null;
|
|
14
15
|
let _validatedTools = null;
|
|
15
16
|
const toolRegistry = new Map();
|
|
@@ -30,6 +31,14 @@ export function initTools(ctx) {
|
|
|
30
31
|
buildDeleteProjectTool(),
|
|
31
32
|
buildSearchExecutionsTool(),
|
|
32
33
|
buildCreateProjectTool(),
|
|
34
|
+
buildCreateTestSuiteTool(),
|
|
35
|
+
buildSearchTestSuitesTool(),
|
|
36
|
+
buildDeleteTestSuiteTool(),
|
|
37
|
+
buildCreateTestCaseTool(),
|
|
38
|
+
buildUpdateTestCaseTool(),
|
|
39
|
+
buildDeleteTestCaseTool(),
|
|
40
|
+
buildRunTestSuiteTool(),
|
|
41
|
+
buildGetTestSuiteResultsTool(),
|
|
33
42
|
];
|
|
34
43
|
const validated = [
|
|
35
44
|
buildValidatedTestPageChangesTool(ctx),
|
|
@@ -44,6 +53,14 @@ export function initTools(ctx) {
|
|
|
44
53
|
buildValidatedDeleteProjectTool(),
|
|
45
54
|
buildValidatedSearchExecutionsTool(),
|
|
46
55
|
buildValidatedCreateProjectTool(),
|
|
56
|
+
buildValidatedCreateTestSuiteTool(),
|
|
57
|
+
buildValidatedSearchTestSuitesTool(),
|
|
58
|
+
buildValidatedDeleteTestSuiteTool(),
|
|
59
|
+
buildValidatedCreateTestCaseTool(),
|
|
60
|
+
buildValidatedUpdateTestCaseTool(),
|
|
61
|
+
buildValidatedDeleteTestCaseTool(),
|
|
62
|
+
buildValidatedRunTestSuiteTool(),
|
|
63
|
+
buildValidatedGetTestSuiteResultsTool(),
|
|
47
64
|
];
|
|
48
65
|
_tools = tools;
|
|
49
66
|
_validatedTools = validated;
|