@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.
@@ -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 (progressCallback) {
129
- await progressCallback({ progress: 1, total: TOTAL_STEPS, message: 'Provisioning secure tunnel for localhost...' });
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
- let tunnel;
138
- try {
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
- keyId = tunnel.keyId;
150
- try {
151
- ctx = await ensureTunnel(ctx, tunnel.tunnelKey, tunnel.tunnelId, tunnel.keyId, () => client.revokeNgrokKey(tunnel.keyId));
136
+ const reused = findExistingTunnel(ctx);
137
+ if (reused) {
138
+ ctx = reused;
139
+ logger.info(`Reusing tunnel: ${ctx.targetUrl} (id: ${ctx.tunnelId})`);
152
140
  }
153
- catch (tunnelError) {
154
- const msg = tunnelError instanceof Error ? tunnelError.message : String(tunnelError);
155
- throw new Error(`Tunnel creation failed for ${ctx.originalUrl}. ` +
156
- `Could not establish a secure connection between the remote browser and your local port. ` +
157
- `Verify your dev server is running and the port is accessible. ` +
158
- `(Detail: ${msg})`);
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
- logger.info(`Tunnel ready: ${ctx.targetUrl} (id: ${ctx.tunnelId})`);
161
- }
162
- // Bead 1om: verify traffic actually flows through the tunnel. The
163
- // tunnel can be established (ngrok.connect returns OK) yet refuse
164
- // to forward traffic e.g., IPv4/IPv6 bind mismatch, or the dev
165
- // server died between the pre-flight probe and here. Catch it now,
166
- // in ~1s, not via a 5-minute browser-agent false-pass.
167
- if (ctx.targetUrl) {
168
- const health = await probeTunnelHealth(ctx.targetUrl);
169
- if (!health.healthy) {
170
- const payload = {
171
- error: 'TunnelTrafficBlocked',
172
- 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.`,
173
- detail: {
174
- code: health.code,
175
- status: health.status,
176
- ngrokErrorCode: health.ngrokErrorCode,
177
- elapsedMs: health.elapsedMs,
178
- },
179
- };
180
- logger.warn(`Tunnel health probe failed for ${ctx.targetUrl}: ${health.code} ${health.ngrokErrorCode ?? ''} in ${health.elapsedMs}ms`);
181
- // Tear down the broken tunnel so a subsequent call doesn't reuse it.
182
- // stopTunnel handles both owned (ngrok disconnect + key revoke) and
183
- // borrowed (just drops local ref) cases.
184
- if (ctx.tunnelId) {
185
- tunnelManager.stopTunnel(ctx.tunnelId).catch((err) => logger.warn(`Failed to stop broken tunnel ${ctx.tunnelId}: ${err}`));
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 (progressCallback) {
86
- await progressCallback({ progress: 1, total: 4, message: 'Provisioning secure tunnel for localhost...' });
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
- let tunnel;
94
- try {
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
- catch (provisionError) {
98
- const msg = provisionError instanceof Error ? provisionError.message : String(provisionError);
99
- const diag = provisionError instanceof TunnelProvisionError ? ` ${provisionError.diagnosticSuffix()}` : '';
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
- keyId = tunnel.keyId;
105
- ctx = await ensureTunnel(ctx, tunnel.tunnelKey, tunnel.tunnelId, tunnel.keyId, () => client.revokeNgrokKey(tunnel.keyId));
106
- }
107
- // Bead 1om: post-tunnel health check — verify traffic actually flows.
108
- if (ctx.targetUrl) {
109
- const health = await probeTunnelHealth(ctx.targetUrl);
110
- if (!health.healthy) {
111
- const payload = {
112
- error: 'TunnelTrafficBlocked',
113
- 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.`,
114
- detail: {
115
- code: health.code,
116
- status: health.status,
117
- ngrokErrorCode: health.ngrokErrorCode,
118
- elapsedMs: health.elapsedMs,
119
- },
120
- };
121
- logger.warn(`Tunnel health probe failed for ${ctx.targetUrl}: ${health.code} ${health.ngrokErrorCode ?? ''} in ${health.elapsedMs}ms`);
122
- if (ctx.tunnelId) {
123
- tunnelManager.stopTunnel(ctx.tunnelId).catch((err) => logger.warn(`Failed to stop broken tunnel ${ctx.tunnelId}: ${err}`));
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
+ }
@@ -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
- return service.findTemplateByName('app evaluation');
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 };
@@ -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;