@bbearai/mcp-server 0.6.0 → 0.7.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/dist/index.js +930 -37
- package/package.json +2 -1
- package/src/index.ts +1023 -38
package/src/index.ts
CHANGED
|
@@ -145,7 +145,7 @@ const tools = [
|
|
|
145
145
|
},
|
|
146
146
|
status: {
|
|
147
147
|
type: 'string',
|
|
148
|
-
enum: ['new', 'triaging', 'confirmed', 'in_progress', 'fixed', '
|
|
148
|
+
enum: ['new', 'triaging', 'confirmed', 'in_progress', 'fixed', 'ready_to_test', 'verified', 'resolved', 'reviewed', 'closed', 'wont_fix', 'duplicate'],
|
|
149
149
|
description: 'The new status for the report',
|
|
150
150
|
},
|
|
151
151
|
resolution_notes: {
|
|
@@ -158,7 +158,47 @@ const tools = [
|
|
|
158
158
|
},
|
|
159
159
|
{
|
|
160
160
|
name: 'get_report_context',
|
|
161
|
-
description: 'Get the full debugging context for a report including console logs, network requests, and navigation history',
|
|
161
|
+
description: 'Get the full debugging context for a report including console logs, network requests, and navigation history. Use compact=true for app_context summary only (no console/network/navigation).',
|
|
162
|
+
inputSchema: {
|
|
163
|
+
type: 'object' as const,
|
|
164
|
+
properties: {
|
|
165
|
+
report_id: {
|
|
166
|
+
type: 'string',
|
|
167
|
+
description: 'The UUID of the report',
|
|
168
|
+
},
|
|
169
|
+
compact: {
|
|
170
|
+
type: 'boolean',
|
|
171
|
+
description: 'Compact mode: returns app_context only, skips console logs, network requests, and navigation history. (default: false)',
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
required: ['report_id'],
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
name: 'add_report_comment',
|
|
179
|
+
description: 'Add a comment/note to a bug report thread without changing its status. Use this for follow-up questions, investigation notes, or developer-tester communication.',
|
|
180
|
+
inputSchema: {
|
|
181
|
+
type: 'object' as const,
|
|
182
|
+
properties: {
|
|
183
|
+
report_id: {
|
|
184
|
+
type: 'string',
|
|
185
|
+
description: 'The UUID of the report to comment on',
|
|
186
|
+
},
|
|
187
|
+
message: {
|
|
188
|
+
type: 'string',
|
|
189
|
+
description: 'The comment/note content',
|
|
190
|
+
},
|
|
191
|
+
author: {
|
|
192
|
+
type: 'string',
|
|
193
|
+
description: 'Optional author name (defaults to "Claude Code")',
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
required: ['report_id', 'message'],
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
name: 'get_report_comments',
|
|
201
|
+
description: 'Get all comments/notes on a bug report in chronological order. Returns the full discussion thread.',
|
|
162
202
|
inputSchema: {
|
|
163
203
|
type: 'object' as const,
|
|
164
204
|
properties: {
|
|
@@ -325,7 +365,7 @@ const tools = [
|
|
|
325
365
|
},
|
|
326
366
|
{
|
|
327
367
|
name: 'list_test_cases',
|
|
328
|
-
description: 'List all test cases in the project. Returns test_key, title, target_route, and other metadata. Use this to see existing tests before updating them.',
|
|
368
|
+
description: 'List all test cases in the project. Returns test_key, title, target_route, and other metadata. Use this to see existing tests before updating them. Use compact=true for id, test_key, title, and priority only (saves tokens).',
|
|
329
369
|
inputSchema: {
|
|
330
370
|
type: 'object' as const,
|
|
331
371
|
properties: {
|
|
@@ -350,6 +390,10 @@ const tools = [
|
|
|
350
390
|
type: 'number',
|
|
351
391
|
description: 'Offset for pagination (default 0)',
|
|
352
392
|
},
|
|
393
|
+
compact: {
|
|
394
|
+
type: 'boolean',
|
|
395
|
+
description: 'Compact mode: returns id, test_key, title, and priority only. (default: false)',
|
|
396
|
+
},
|
|
353
397
|
},
|
|
354
398
|
},
|
|
355
399
|
},
|
|
@@ -458,7 +502,7 @@ const tools = [
|
|
|
458
502
|
},
|
|
459
503
|
notify_tester: {
|
|
460
504
|
type: 'boolean',
|
|
461
|
-
description: '
|
|
505
|
+
description: 'Notify the original tester about the fix with a message and verification task. Default: true. Set to false for silent resolve.',
|
|
462
506
|
},
|
|
463
507
|
},
|
|
464
508
|
required: ['report_id', 'commit_sha'],
|
|
@@ -700,17 +744,17 @@ const tools = [
|
|
|
700
744
|
},
|
|
701
745
|
{
|
|
702
746
|
name: 'get_coverage_matrix',
|
|
703
|
-
description: 'Get a comprehensive Route × Track coverage matrix showing test counts, pass rates, and execution data. Use this for a complete view of test coverage.',
|
|
747
|
+
description: 'Get a comprehensive Route × Track coverage matrix showing test counts, pass rates, and execution data. Use this for a complete view of test coverage. Execution data and bug counts are opt-in to save tokens.',
|
|
704
748
|
inputSchema: {
|
|
705
749
|
type: 'object' as const,
|
|
706
750
|
properties: {
|
|
707
751
|
include_execution_data: {
|
|
708
752
|
type: 'boolean',
|
|
709
|
-
description: 'Include pass/fail rates and last execution times (default: true
|
|
753
|
+
description: 'Include pass/fail rates and last execution times (default: false). Set true when you need execution history.',
|
|
710
754
|
},
|
|
711
755
|
include_bug_counts: {
|
|
712
756
|
type: 'boolean',
|
|
713
|
-
description: 'Include open/critical bug counts per route (default: true
|
|
757
|
+
description: 'Include open/critical bug counts per route (default: false). Set true when you need bug context.',
|
|
714
758
|
},
|
|
715
759
|
},
|
|
716
760
|
},
|
|
@@ -944,6 +988,11 @@ const tools = [
|
|
|
944
988
|
enum: ['ios', 'android', 'web'],
|
|
945
989
|
description: 'Filter by platform support',
|
|
946
990
|
},
|
|
991
|
+
role: {
|
|
992
|
+
type: 'string',
|
|
993
|
+
enum: ['tester', 'feedback'],
|
|
994
|
+
description: 'Filter by role: "tester" for QA testers, "feedback" for feedback-only users (default: all)',
|
|
995
|
+
},
|
|
947
996
|
},
|
|
948
997
|
},
|
|
949
998
|
},
|
|
@@ -1032,6 +1081,21 @@ const tools = [
|
|
|
1032
1081
|
required: ['tester_id', 'test_case_ids'],
|
|
1033
1082
|
},
|
|
1034
1083
|
},
|
|
1084
|
+
{
|
|
1085
|
+
name: 'unassign_tests',
|
|
1086
|
+
description: 'Remove one or more test assignments by assignment ID. Preserves the test case and its history — only the assignment link is deleted. Use list_test_assignments first to find assignment IDs. Max 50 per call.',
|
|
1087
|
+
inputSchema: {
|
|
1088
|
+
type: 'object' as const,
|
|
1089
|
+
properties: {
|
|
1090
|
+
assignment_ids: {
|
|
1091
|
+
type: 'array',
|
|
1092
|
+
items: { type: 'string' },
|
|
1093
|
+
description: 'Array of test assignment UUIDs to remove (required, max 50)',
|
|
1094
|
+
},
|
|
1095
|
+
},
|
|
1096
|
+
required: ['assignment_ids'],
|
|
1097
|
+
},
|
|
1098
|
+
},
|
|
1035
1099
|
{
|
|
1036
1100
|
name: 'get_tester_workload',
|
|
1037
1101
|
description: 'View a specific tester\'s current workload — assignment counts by status and recent assignments.',
|
|
@@ -1074,6 +1138,11 @@ const tools = [
|
|
|
1074
1138
|
type: 'string',
|
|
1075
1139
|
description: 'Optional notes about the tester',
|
|
1076
1140
|
},
|
|
1141
|
+
role: {
|
|
1142
|
+
type: 'string',
|
|
1143
|
+
enum: ['tester', 'feedback'],
|
|
1144
|
+
description: 'Role: "tester" for QA testers (default), "feedback" for feedback-only users',
|
|
1145
|
+
},
|
|
1077
1146
|
},
|
|
1078
1147
|
required: ['name', 'email'],
|
|
1079
1148
|
},
|
|
@@ -1176,7 +1245,7 @@ const tools = [
|
|
|
1176
1245
|
},
|
|
1177
1246
|
{
|
|
1178
1247
|
name: 'export_test_results',
|
|
1179
|
-
description: 'Export test results for a specific test run as structured JSON — includes every assignment, tester, result, and duration.',
|
|
1248
|
+
description: 'Export test results for a specific test run as structured JSON — includes every assignment, tester, result, and duration. Use compact=true for summary only (no assignments array). Use limit to cap assignments returned.',
|
|
1180
1249
|
inputSchema: {
|
|
1181
1250
|
type: 'object' as const,
|
|
1182
1251
|
properties: {
|
|
@@ -1184,6 +1253,14 @@ const tools = [
|
|
|
1184
1253
|
type: 'string',
|
|
1185
1254
|
description: 'UUID of the test run to export (required)',
|
|
1186
1255
|
},
|
|
1256
|
+
compact: {
|
|
1257
|
+
type: 'boolean',
|
|
1258
|
+
description: 'Compact mode: returns test run info + summary only, no assignments array. (default: false)',
|
|
1259
|
+
},
|
|
1260
|
+
limit: {
|
|
1261
|
+
type: 'number',
|
|
1262
|
+
description: 'Max assignments to return in full mode (default: 100, max: 500). Ignored when compact=true.',
|
|
1263
|
+
},
|
|
1187
1264
|
},
|
|
1188
1265
|
required: ['test_run_id'],
|
|
1189
1266
|
},
|
|
@@ -1232,8 +1309,677 @@ const tools = [
|
|
|
1232
1309
|
properties: {},
|
|
1233
1310
|
},
|
|
1234
1311
|
},
|
|
1312
|
+
// === TEST EXECUTION INTELLIGENCE ===
|
|
1313
|
+
{
|
|
1314
|
+
name: 'get_test_impact',
|
|
1315
|
+
description: 'Given changed files, identify which test cases are affected by mapping file paths to test case target routes.',
|
|
1316
|
+
inputSchema: {
|
|
1317
|
+
type: 'object' as const,
|
|
1318
|
+
properties: {
|
|
1319
|
+
changed_files: {
|
|
1320
|
+
type: 'array',
|
|
1321
|
+
items: { type: 'string' },
|
|
1322
|
+
description: 'List of changed file paths (relative to project root)',
|
|
1323
|
+
},
|
|
1324
|
+
},
|
|
1325
|
+
required: ['changed_files'],
|
|
1326
|
+
},
|
|
1327
|
+
},
|
|
1328
|
+
{
|
|
1329
|
+
name: 'get_flaky_tests',
|
|
1330
|
+
description: 'Analyze test run history to identify tests with intermittent failure rates above a threshold.',
|
|
1331
|
+
inputSchema: {
|
|
1332
|
+
type: 'object' as const,
|
|
1333
|
+
properties: {
|
|
1334
|
+
threshold: {
|
|
1335
|
+
type: 'number',
|
|
1336
|
+
description: 'Minimum flakiness rate to report (0-100, default: 5)',
|
|
1337
|
+
},
|
|
1338
|
+
limit: {
|
|
1339
|
+
type: 'number',
|
|
1340
|
+
description: 'Maximum results to return (default: 20)',
|
|
1341
|
+
},
|
|
1342
|
+
},
|
|
1343
|
+
},
|
|
1344
|
+
},
|
|
1345
|
+
{
|
|
1346
|
+
name: 'assess_test_quality',
|
|
1347
|
+
description: 'Analyze test case steps for weak patterns: vague assertions, missing edge cases, no negative testing, generic descriptions.',
|
|
1348
|
+
inputSchema: {
|
|
1349
|
+
type: 'object' as const,
|
|
1350
|
+
properties: {
|
|
1351
|
+
test_case_ids: {
|
|
1352
|
+
type: 'array',
|
|
1353
|
+
items: { type: 'string' },
|
|
1354
|
+
description: 'Specific test case IDs to assess. If omitted, assesses recent test cases.',
|
|
1355
|
+
},
|
|
1356
|
+
limit: {
|
|
1357
|
+
type: 'number',
|
|
1358
|
+
description: 'Maximum test cases to assess (default: 20)',
|
|
1359
|
+
},
|
|
1360
|
+
},
|
|
1361
|
+
},
|
|
1362
|
+
},
|
|
1363
|
+
{
|
|
1364
|
+
name: 'get_test_execution_summary',
|
|
1365
|
+
description: 'Aggregate test execution metrics: pass rate, completion rate, most-failed tests, fastest/slowest tests.',
|
|
1366
|
+
inputSchema: {
|
|
1367
|
+
type: 'object' as const,
|
|
1368
|
+
properties: {
|
|
1369
|
+
days: {
|
|
1370
|
+
type: 'number',
|
|
1371
|
+
description: 'Number of days to analyze (default: 30)',
|
|
1372
|
+
},
|
|
1373
|
+
},
|
|
1374
|
+
},
|
|
1375
|
+
},
|
|
1376
|
+
{
|
|
1377
|
+
name: 'check_test_freshness',
|
|
1378
|
+
description: 'Identify test cases that have not been updated since their target code was modified.',
|
|
1379
|
+
inputSchema: {
|
|
1380
|
+
type: 'object' as const,
|
|
1381
|
+
properties: {
|
|
1382
|
+
limit: {
|
|
1383
|
+
type: 'number',
|
|
1384
|
+
description: 'Maximum results to return (default: 20)',
|
|
1385
|
+
},
|
|
1386
|
+
},
|
|
1387
|
+
},
|
|
1388
|
+
},
|
|
1389
|
+
{
|
|
1390
|
+
name: 'get_untested_changes',
|
|
1391
|
+
description: 'Given recent commits or changed files, find code changes with no corresponding test coverage in BugBear.',
|
|
1392
|
+
inputSchema: {
|
|
1393
|
+
type: 'object' as const,
|
|
1394
|
+
properties: {
|
|
1395
|
+
changed_files: {
|
|
1396
|
+
type: 'array',
|
|
1397
|
+
items: { type: 'string' },
|
|
1398
|
+
description: 'List of changed file paths. If omitted, uses git diff against main.',
|
|
1399
|
+
},
|
|
1400
|
+
},
|
|
1401
|
+
},
|
|
1402
|
+
},
|
|
1403
|
+
// === AUTO-MONITORING TOOLS ===
|
|
1404
|
+
{
|
|
1405
|
+
name: 'get_auto_detected_issues',
|
|
1406
|
+
description: 'Get auto-detected monitoring issues grouped by error fingerprint. Shows recurring crashes, API failures, and rage clicks with frequency and user impact.',
|
|
1407
|
+
inputSchema: {
|
|
1408
|
+
type: 'object' as const,
|
|
1409
|
+
properties: {
|
|
1410
|
+
source: {
|
|
1411
|
+
type: 'string',
|
|
1412
|
+
enum: ['auto_crash', 'auto_api', 'auto_rage_click'],
|
|
1413
|
+
description: 'Filter by source type',
|
|
1414
|
+
},
|
|
1415
|
+
min_occurrences: {
|
|
1416
|
+
type: 'number',
|
|
1417
|
+
description: 'Min occurrence count (default: 1)',
|
|
1418
|
+
},
|
|
1419
|
+
since: {
|
|
1420
|
+
type: 'string',
|
|
1421
|
+
description: 'ISO date — only issues after this date (default: 7 days ago)',
|
|
1422
|
+
},
|
|
1423
|
+
limit: {
|
|
1424
|
+
type: 'number',
|
|
1425
|
+
description: 'Max results (default: 20)',
|
|
1426
|
+
},
|
|
1427
|
+
compact: {
|
|
1428
|
+
type: 'boolean',
|
|
1429
|
+
description: 'Compact mode: fingerprint, source, count only',
|
|
1430
|
+
},
|
|
1431
|
+
},
|
|
1432
|
+
},
|
|
1433
|
+
},
|
|
1434
|
+
{
|
|
1435
|
+
name: 'generate_tests_from_errors',
|
|
1436
|
+
description: 'Suggest QA test cases from auto-detected error patterns. Returns structured suggestions — does NOT auto-create test cases.',
|
|
1437
|
+
inputSchema: {
|
|
1438
|
+
type: 'object' as const,
|
|
1439
|
+
properties: {
|
|
1440
|
+
report_ids: {
|
|
1441
|
+
type: 'array',
|
|
1442
|
+
items: { type: 'string' },
|
|
1443
|
+
description: 'Specific report IDs. If omitted, uses top uncovered errors.',
|
|
1444
|
+
},
|
|
1445
|
+
limit: {
|
|
1446
|
+
type: 'number',
|
|
1447
|
+
description: 'Max suggestions (default: 5)',
|
|
1448
|
+
},
|
|
1449
|
+
},
|
|
1450
|
+
},
|
|
1451
|
+
},
|
|
1235
1452
|
];
|
|
1236
1453
|
|
|
1454
|
+
// === TEST EXECUTION INTELLIGENCE ===
|
|
1455
|
+
|
|
1456
|
+
async function getTestImpact(args: { changed_files: string[] }) {
|
|
1457
|
+
const projectId = requireProject();
|
|
1458
|
+
const changedFiles = args.changed_files || [];
|
|
1459
|
+
|
|
1460
|
+
if (changedFiles.length === 0) {
|
|
1461
|
+
return { affectedTests: [], message: 'No changed files provided.' };
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
// Get all test cases for the project with their target routes
|
|
1465
|
+
const { data: testCases, error } = await supabase
|
|
1466
|
+
.from('test_cases')
|
|
1467
|
+
.select('id, title, target_route, qa_track, priority')
|
|
1468
|
+
.eq('project_id', projectId);
|
|
1469
|
+
|
|
1470
|
+
if (error) return { error: error.message };
|
|
1471
|
+
if (!testCases || testCases.length === 0) {
|
|
1472
|
+
return { affectedTests: [], message: 'No test cases found for this project.' };
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
// Map changed files to affected test cases
|
|
1476
|
+
const affected: Array<{ testId: string; title: string; targetRoute: string; matchedFiles: string[]; qaTrack: string }> = [];
|
|
1477
|
+
|
|
1478
|
+
for (const tc of testCases) {
|
|
1479
|
+
const route = tc.target_route || '';
|
|
1480
|
+
const matchedFiles = changedFiles.filter(f => {
|
|
1481
|
+
// Match file path to route (e.g., src/app/api/tasks/route.ts -> /api/tasks)
|
|
1482
|
+
const normalized = f.replace(/\\/g, '/');
|
|
1483
|
+
const routeParts = route.split('/').filter(Boolean);
|
|
1484
|
+
return routeParts.some((part: string) => normalized.includes(part)) || normalized.includes(route.replace(/\//g, '/'));
|
|
1485
|
+
});
|
|
1486
|
+
|
|
1487
|
+
if (matchedFiles.length > 0) {
|
|
1488
|
+
affected.push({
|
|
1489
|
+
testId: tc.id,
|
|
1490
|
+
title: tc.title,
|
|
1491
|
+
targetRoute: route,
|
|
1492
|
+
matchedFiles,
|
|
1493
|
+
qaTrack: tc.qa_track,
|
|
1494
|
+
});
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
return {
|
|
1499
|
+
affectedTests: affected,
|
|
1500
|
+
totalTestCases: testCases.length,
|
|
1501
|
+
affectedCount: affected.length,
|
|
1502
|
+
changedFileCount: changedFiles.length,
|
|
1503
|
+
};
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
async function getFlakyTests(args: { threshold?: number; limit?: number }) {
|
|
1507
|
+
const projectId = requireProject();
|
|
1508
|
+
const threshold = args.threshold || 5;
|
|
1509
|
+
const limit = args.limit || 20;
|
|
1510
|
+
|
|
1511
|
+
// Get test results grouped by test case
|
|
1512
|
+
const { data: results, error } = await supabase
|
|
1513
|
+
.from('test_results')
|
|
1514
|
+
.select('test_case_id, status, test_cases!inner(title, target_route, qa_track)')
|
|
1515
|
+
.eq('test_cases.project_id', projectId)
|
|
1516
|
+
.order('created_at', { ascending: false })
|
|
1517
|
+
.limit(5000);
|
|
1518
|
+
|
|
1519
|
+
if (error) return { error: error.message };
|
|
1520
|
+
if (!results || results.length === 0) {
|
|
1521
|
+
return { flakyTests: [], message: 'No test results found.' };
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
// Group by test case and calculate flakiness
|
|
1525
|
+
const testStats: Record<string, { passes: number; fails: number; total: number; title: string; route: string; track: string }> = {};
|
|
1526
|
+
|
|
1527
|
+
for (const r of results) {
|
|
1528
|
+
const id = r.test_case_id;
|
|
1529
|
+
if (!testStats[id]) {
|
|
1530
|
+
const tc = r.test_cases as any;
|
|
1531
|
+
testStats[id] = { passes: 0, fails: 0, total: 0, title: tc?.title || '', route: tc?.target_route || '', track: tc?.qa_track || '' };
|
|
1532
|
+
}
|
|
1533
|
+
testStats[id].total++;
|
|
1534
|
+
if (r.status === 'pass') testStats[id].passes++;
|
|
1535
|
+
else if (r.status === 'fail') testStats[id].fails++;
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
// Find flaky tests (have both passes and fails, with fail rate above threshold)
|
|
1539
|
+
const flaky = Object.entries(testStats)
|
|
1540
|
+
.filter(([, stats]) => {
|
|
1541
|
+
if (stats.total < 3) return false; // Need enough data
|
|
1542
|
+
const failRate = (stats.fails / stats.total) * 100;
|
|
1543
|
+
const passRate = (stats.passes / stats.total) * 100;
|
|
1544
|
+
return failRate >= threshold && passRate > 0; // Has both passes and fails
|
|
1545
|
+
})
|
|
1546
|
+
.map(([id, stats]) => ({
|
|
1547
|
+
testCaseId: id,
|
|
1548
|
+
title: stats.title,
|
|
1549
|
+
targetRoute: stats.route,
|
|
1550
|
+
qaTrack: stats.track,
|
|
1551
|
+
totalRuns: stats.total,
|
|
1552
|
+
failRate: Math.round((stats.fails / stats.total) * 100),
|
|
1553
|
+
passRate: Math.round((stats.passes / stats.total) * 100),
|
|
1554
|
+
}))
|
|
1555
|
+
.sort((a, b) => b.failRate - a.failRate)
|
|
1556
|
+
.slice(0, limit);
|
|
1557
|
+
|
|
1558
|
+
return {
|
|
1559
|
+
flakyTests: flaky,
|
|
1560
|
+
totalAnalyzed: Object.keys(testStats).length,
|
|
1561
|
+
flakyCount: flaky.length,
|
|
1562
|
+
threshold,
|
|
1563
|
+
};
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
async function assessTestQuality(args: { test_case_ids?: string[]; limit?: number }) {
|
|
1567
|
+
const projectId = requireProject();
|
|
1568
|
+
const limit = args.limit || 20;
|
|
1569
|
+
|
|
1570
|
+
let query = supabase
|
|
1571
|
+
.from('test_cases')
|
|
1572
|
+
.select('id, title, steps, target_route, qa_track, priority')
|
|
1573
|
+
.eq('project_id', projectId)
|
|
1574
|
+
.limit(limit);
|
|
1575
|
+
|
|
1576
|
+
if (args.test_case_ids && args.test_case_ids.length > 0) {
|
|
1577
|
+
query = query.in('id', args.test_case_ids);
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
const { data: testCases, error } = await query;
|
|
1581
|
+
if (error) return { error: error.message };
|
|
1582
|
+
if (!testCases || testCases.length === 0) {
|
|
1583
|
+
return { assessments: [], message: 'No test cases found.' };
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
const assessments = testCases.map(tc => {
|
|
1587
|
+
const issues: string[] = [];
|
|
1588
|
+
const steps = tc.steps || [];
|
|
1589
|
+
|
|
1590
|
+
// Check for weak patterns
|
|
1591
|
+
if (steps.length < 2) {
|
|
1592
|
+
issues.push('Too few steps — test may not cover the full flow');
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
const allStepsText = steps.map((s: any) => (typeof s === 'string' ? s : s.action || s.description || '')).join(' ');
|
|
1596
|
+
|
|
1597
|
+
// Vague assertions
|
|
1598
|
+
if (/should work|looks good|is correct|verify it works/i.test(allStepsText)) {
|
|
1599
|
+
issues.push('Vague assertions detected — use specific expected outcomes');
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
// Missing edge cases
|
|
1603
|
+
if (!/error|invalid|empty|missing|unauthorized|forbidden|404|500/i.test(allStepsText)) {
|
|
1604
|
+
issues.push('No negative/error test cases — add edge case testing');
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
// Generic descriptions
|
|
1608
|
+
if (/test the|check the|verify the/i.test(tc.title) && tc.title.length < 30) {
|
|
1609
|
+
issues.push('Generic test title — be more specific about what is being tested');
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
// No specific UI elements referenced
|
|
1613
|
+
if (!/button|input|form|modal|dropdown|select|click|type|enter|submit/i.test(allStepsText)) {
|
|
1614
|
+
issues.push('No specific UI elements referenced — steps may be too abstract');
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
const quality = issues.length === 0 ? 'good' : issues.length <= 2 ? 'needs-improvement' : 'poor';
|
|
1618
|
+
|
|
1619
|
+
return {
|
|
1620
|
+
testCaseId: tc.id,
|
|
1621
|
+
title: tc.title,
|
|
1622
|
+
targetRoute: tc.target_route,
|
|
1623
|
+
stepCount: steps.length,
|
|
1624
|
+
quality,
|
|
1625
|
+
issues,
|
|
1626
|
+
};
|
|
1627
|
+
});
|
|
1628
|
+
|
|
1629
|
+
const qualityCounts = {
|
|
1630
|
+
good: assessments.filter(a => a.quality === 'good').length,
|
|
1631
|
+
needsImprovement: assessments.filter(a => a.quality === 'needs-improvement').length,
|
|
1632
|
+
poor: assessments.filter(a => a.quality === 'poor').length,
|
|
1633
|
+
};
|
|
1634
|
+
|
|
1635
|
+
return {
|
|
1636
|
+
assessments,
|
|
1637
|
+
summary: qualityCounts,
|
|
1638
|
+
totalAssessed: assessments.length,
|
|
1639
|
+
};
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
async function getTestExecutionSummary(args: { days?: number }) {
|
|
1643
|
+
const projectId = requireProject();
|
|
1644
|
+
const days = args.days || 30;
|
|
1645
|
+
const since = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString();
|
|
1646
|
+
|
|
1647
|
+
// Get test results
|
|
1648
|
+
const { data: results, error } = await supabase
|
|
1649
|
+
.from('test_results')
|
|
1650
|
+
.select('test_case_id, status, duration_ms, created_at, test_cases!inner(title, target_route)')
|
|
1651
|
+
.eq('test_cases.project_id', projectId)
|
|
1652
|
+
.gte('created_at', since)
|
|
1653
|
+
.order('created_at', { ascending: false });
|
|
1654
|
+
|
|
1655
|
+
if (error) return { error: error.message };
|
|
1656
|
+
if (!results || results.length === 0) {
|
|
1657
|
+
return { message: `No test results found in the last ${days} days.` };
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
const totalRuns = results.length;
|
|
1661
|
+
const passed = results.filter(r => r.status === 'pass').length;
|
|
1662
|
+
const failed = results.filter(r => r.status === 'fail').length;
|
|
1663
|
+
const blocked = results.filter(r => r.status === 'blocked').length;
|
|
1664
|
+
|
|
1665
|
+
// Most failed tests
|
|
1666
|
+
const failCounts: Record<string, { count: number; title: string; route: string }> = {};
|
|
1667
|
+
for (const r of results.filter(r => r.status === 'fail')) {
|
|
1668
|
+
const id = r.test_case_id;
|
|
1669
|
+
const tc = r.test_cases as any;
|
|
1670
|
+
if (!failCounts[id]) {
|
|
1671
|
+
failCounts[id] = { count: 0, title: tc?.title || '', route: tc?.target_route || '' };
|
|
1672
|
+
}
|
|
1673
|
+
failCounts[id].count++;
|
|
1674
|
+
}
|
|
1675
|
+
const mostFailed = Object.entries(failCounts)
|
|
1676
|
+
.sort((a, b) => b[1].count - a[1].count)
|
|
1677
|
+
.slice(0, 5)
|
|
1678
|
+
.map(([id, data]) => ({ testCaseId: id, ...data }));
|
|
1679
|
+
|
|
1680
|
+
// Duration stats
|
|
1681
|
+
const durations = results.filter(r => r.duration_ms).map(r => r.duration_ms as number);
|
|
1682
|
+
const avgDuration = durations.length > 0 ? Math.round(durations.reduce((a, b) => a + b, 0) / durations.length) : 0;
|
|
1683
|
+
const maxDuration = durations.length > 0 ? Math.max(...durations) : 0;
|
|
1684
|
+
|
|
1685
|
+
return {
|
|
1686
|
+
period: `${days} days`,
|
|
1687
|
+
totalRuns,
|
|
1688
|
+
passRate: Math.round((passed / totalRuns) * 100),
|
|
1689
|
+
failRate: Math.round((failed / totalRuns) * 100),
|
|
1690
|
+
blockedCount: blocked,
|
|
1691
|
+
averageDurationMs: avgDuration,
|
|
1692
|
+
maxDurationMs: maxDuration,
|
|
1693
|
+
mostFailed,
|
|
1694
|
+
uniqueTestsCovered: new Set(results.map(r => r.test_case_id)).size,
|
|
1695
|
+
};
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
async function checkTestFreshness(args: { limit?: number }) {
|
|
1699
|
+
const projectId = requireProject();
|
|
1700
|
+
const limit = args.limit || 20;
|
|
1701
|
+
|
|
1702
|
+
// Get test cases with their last update and last result
|
|
1703
|
+
const { data: testCases, error } = await supabase
|
|
1704
|
+
.from('test_cases')
|
|
1705
|
+
.select('id, title, target_route, updated_at, created_at')
|
|
1706
|
+
.eq('project_id', projectId)
|
|
1707
|
+
.order('updated_at', { ascending: true })
|
|
1708
|
+
.limit(limit);
|
|
1709
|
+
|
|
1710
|
+
if (error) return { error: error.message };
|
|
1711
|
+
if (!testCases || testCases.length === 0) {
|
|
1712
|
+
return { staleTests: [], message: 'No test cases found.' };
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString();
|
|
1716
|
+
|
|
1717
|
+
const stale = testCases
|
|
1718
|
+
.filter(tc => tc.updated_at < thirtyDaysAgo)
|
|
1719
|
+
.map(tc => ({
|
|
1720
|
+
testCaseId: tc.id,
|
|
1721
|
+
title: tc.title,
|
|
1722
|
+
targetRoute: tc.target_route,
|
|
1723
|
+
lastUpdated: tc.updated_at,
|
|
1724
|
+
daysSinceUpdate: Math.round((Date.now() - new Date(tc.updated_at).getTime()) / (24 * 60 * 60 * 1000)),
|
|
1725
|
+
}));
|
|
1726
|
+
|
|
1727
|
+
return {
|
|
1728
|
+
staleTests: stale,
|
|
1729
|
+
totalTestCases: testCases.length,
|
|
1730
|
+
staleCount: stale.length,
|
|
1731
|
+
stalenessThreshold: '30 days',
|
|
1732
|
+
};
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
async function getUntestedChanges(args: { changed_files?: string[] }) {
|
|
1736
|
+
const projectId = requireProject();
|
|
1737
|
+
|
|
1738
|
+
// Get all test cases to understand what's covered
|
|
1739
|
+
const { data: testCases, error } = await supabase
|
|
1740
|
+
.from('test_cases')
|
|
1741
|
+
.select('id, title, target_route')
|
|
1742
|
+
.eq('project_id', projectId);
|
|
1743
|
+
|
|
1744
|
+
if (error) return { error: error.message };
|
|
1745
|
+
|
|
1746
|
+
const coveredRoutes = new Set((testCases || []).map(tc => tc.target_route).filter(Boolean));
|
|
1747
|
+
|
|
1748
|
+
// If changed_files provided, check coverage
|
|
1749
|
+
const changedFiles = args.changed_files || [];
|
|
1750
|
+
|
|
1751
|
+
if (changedFiles.length === 0) {
|
|
1752
|
+
return {
|
|
1753
|
+
message: 'No changed files provided. Pass changed_files to check coverage.',
|
|
1754
|
+
totalCoveredRoutes: coveredRoutes.size,
|
|
1755
|
+
};
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
// Map changed files to routes and check coverage
|
|
1759
|
+
const untested: Array<{ file: string; inferredRoute: string; reason: string }> = [];
|
|
1760
|
+
|
|
1761
|
+
for (const file of changedFiles) {
|
|
1762
|
+
const normalized = file.replace(/\\/g, '/');
|
|
1763
|
+
|
|
1764
|
+
// Extract route-like path from file
|
|
1765
|
+
let inferredRoute = '';
|
|
1766
|
+
|
|
1767
|
+
// Next.js app router: app/api/tasks/route.ts -> /api/tasks
|
|
1768
|
+
const appRouterMatch = normalized.match(/app\/(api\/[^/]+(?:\/[^/]+)*?)\/route\.\w+$/);
|
|
1769
|
+
if (appRouterMatch) {
|
|
1770
|
+
inferredRoute = '/' + appRouterMatch[1];
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
// Pages router: pages/api/tasks.ts -> /api/tasks
|
|
1774
|
+
const pagesMatch = normalized.match(/pages\/(api\/[^.]+)\.\w+$/);
|
|
1775
|
+
if (!inferredRoute && pagesMatch) {
|
|
1776
|
+
inferredRoute = '/' + pagesMatch[1];
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
// Component files
|
|
1780
|
+
const componentMatch = normalized.match(/(?:components|screens|pages)\/([^.]+)\.\w+$/);
|
|
1781
|
+
if (!inferredRoute && componentMatch) {
|
|
1782
|
+
inferredRoute = '/' + componentMatch[1].replace(/\\/g, '/');
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1785
|
+
if (inferredRoute && !coveredRoutes.has(inferredRoute)) {
|
|
1786
|
+
untested.push({
|
|
1787
|
+
file,
|
|
1788
|
+
inferredRoute,
|
|
1789
|
+
reason: 'No test cases cover this route',
|
|
1790
|
+
});
|
|
1791
|
+
} else if (!inferredRoute) {
|
|
1792
|
+
// Can't map to a route — flag as potentially untested
|
|
1793
|
+
untested.push({
|
|
1794
|
+
file,
|
|
1795
|
+
inferredRoute: 'unknown',
|
|
1796
|
+
reason: 'Could not map file to a testable route',
|
|
1797
|
+
});
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
return {
|
|
1802
|
+
untestedChanges: untested,
|
|
1803
|
+
changedFileCount: changedFiles.length,
|
|
1804
|
+
untestedCount: untested.length,
|
|
1805
|
+
coveredRoutes: coveredRoutes.size,
|
|
1806
|
+
};
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
// === AUTO-MONITORING HANDLERS ===
|
|
1810
|
+
|
|
1811
|
+
async function getAutoDetectedIssues(args: {
|
|
1812
|
+
source?: string;
|
|
1813
|
+
min_occurrences?: number;
|
|
1814
|
+
since?: string;
|
|
1815
|
+
limit?: number;
|
|
1816
|
+
compact?: boolean;
|
|
1817
|
+
}) {
|
|
1818
|
+
const projectId = requireProject();
|
|
1819
|
+
const since = args.since || new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString();
|
|
1820
|
+
const limit = args.limit || 20;
|
|
1821
|
+
|
|
1822
|
+
let query = supabase
|
|
1823
|
+
.from('reports')
|
|
1824
|
+
.select('id, error_fingerprint, report_source, title, severity, reporter_id, sentry_event_id, created_at, app_context')
|
|
1825
|
+
.eq('project_id', projectId)
|
|
1826
|
+
.neq('report_source', 'manual')
|
|
1827
|
+
.not('error_fingerprint', 'is', null)
|
|
1828
|
+
.gte('created_at', since)
|
|
1829
|
+
.order('created_at', { ascending: false });
|
|
1830
|
+
|
|
1831
|
+
if (args.source) {
|
|
1832
|
+
query = query.eq('report_source', args.source);
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1835
|
+
const { data, error } = await query;
|
|
1836
|
+
if (error) return { error: error.message };
|
|
1837
|
+
if (!data || data.length === 0) return { issues: [], total: 0 };
|
|
1838
|
+
|
|
1839
|
+
// Group by fingerprint
|
|
1840
|
+
const grouped = new Map<string, typeof data>();
|
|
1841
|
+
for (const report of data) {
|
|
1842
|
+
const fp = report.error_fingerprint!;
|
|
1843
|
+
if (!grouped.has(fp)) grouped.set(fp, []);
|
|
1844
|
+
grouped.get(fp)!.push(report);
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
// Build issue summaries
|
|
1848
|
+
const issues = Array.from(grouped.entries())
|
|
1849
|
+
.map(([fingerprint, reports]) => {
|
|
1850
|
+
const uniqueReporters = new Set(reports.map(r => r.reporter_id));
|
|
1851
|
+
const sorted = reports.sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime());
|
|
1852
|
+
const first = sorted[0];
|
|
1853
|
+
const last = sorted[sorted.length - 1];
|
|
1854
|
+
const route = (first.app_context as any)?.currentRoute || 'unknown';
|
|
1855
|
+
|
|
1856
|
+
return {
|
|
1857
|
+
fingerprint,
|
|
1858
|
+
source: first.report_source,
|
|
1859
|
+
message: first.title,
|
|
1860
|
+
route,
|
|
1861
|
+
occurrence_count: reports.length,
|
|
1862
|
+
affected_users: uniqueReporters.size,
|
|
1863
|
+
first_seen: first.created_at,
|
|
1864
|
+
last_seen: last.created_at,
|
|
1865
|
+
severity: first.severity,
|
|
1866
|
+
has_sentry_link: reports.some(r => r.sentry_event_id != null),
|
|
1867
|
+
sample_report_id: first.id,
|
|
1868
|
+
};
|
|
1869
|
+
})
|
|
1870
|
+
.filter(issue => issue.occurrence_count >= (args.min_occurrences || 1))
|
|
1871
|
+
.sort((a, b) => b.occurrence_count - a.occurrence_count)
|
|
1872
|
+
.slice(0, limit);
|
|
1873
|
+
|
|
1874
|
+
if (args.compact) {
|
|
1875
|
+
return {
|
|
1876
|
+
issues: issues.map(i => ({
|
|
1877
|
+
fingerprint: i.fingerprint,
|
|
1878
|
+
source: i.source,
|
|
1879
|
+
count: i.occurrence_count,
|
|
1880
|
+
users: i.affected_users,
|
|
1881
|
+
severity: i.severity,
|
|
1882
|
+
})),
|
|
1883
|
+
total: issues.length,
|
|
1884
|
+
};
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1887
|
+
return { issues, total: issues.length };
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1890
|
+
async function generateTestsFromErrors(args: {
|
|
1891
|
+
report_ids?: string[];
|
|
1892
|
+
limit?: number;
|
|
1893
|
+
}) {
|
|
1894
|
+
const projectId = requireProject();
|
|
1895
|
+
const limit = args.limit || 5;
|
|
1896
|
+
|
|
1897
|
+
let reports;
|
|
1898
|
+
if (args.report_ids?.length) {
|
|
1899
|
+
// Validate all UUIDs
|
|
1900
|
+
for (const id of args.report_ids) {
|
|
1901
|
+
if (!isValidUUID(id)) {
|
|
1902
|
+
return { error: `Invalid report_id format: ${id}` };
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1906
|
+
const { data, error } = await supabase
|
|
1907
|
+
.from('reports')
|
|
1908
|
+
.select('id, title, report_source, severity, app_context, error_fingerprint, description')
|
|
1909
|
+
.eq('project_id', projectId)
|
|
1910
|
+
.in('id', args.report_ids);
|
|
1911
|
+
if (error) return { error: error.message };
|
|
1912
|
+
reports = data;
|
|
1913
|
+
} else {
|
|
1914
|
+
// Get top uncovered auto-detected errors
|
|
1915
|
+
const { data, error } = await supabase
|
|
1916
|
+
.from('reports')
|
|
1917
|
+
.select('id, title, report_source, severity, app_context, error_fingerprint, description')
|
|
1918
|
+
.eq('project_id', projectId)
|
|
1919
|
+
.neq('report_source', 'manual')
|
|
1920
|
+
.not('error_fingerprint', 'is', null)
|
|
1921
|
+
.order('created_at', { ascending: false })
|
|
1922
|
+
.limit(50);
|
|
1923
|
+
if (error) return { error: error.message };
|
|
1924
|
+
|
|
1925
|
+
// Deduplicate by fingerprint, keep first occurrence
|
|
1926
|
+
const seen = new Set<string>();
|
|
1927
|
+
reports = (data || []).filter(r => {
|
|
1928
|
+
if (!r.error_fingerprint || seen.has(r.error_fingerprint)) return false;
|
|
1929
|
+
seen.add(r.error_fingerprint);
|
|
1930
|
+
return true;
|
|
1931
|
+
}).slice(0, limit);
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1934
|
+
if (!reports?.length) return { suggestions: [] };
|
|
1935
|
+
|
|
1936
|
+
const suggestions = reports.map(report => {
|
|
1937
|
+
const route = (report.app_context as any)?.currentRoute || '/unknown';
|
|
1938
|
+
const source = report.report_source;
|
|
1939
|
+
const priority = report.severity === 'critical' ? 'P1' : report.severity === 'high' ? 'P1' : 'P2';
|
|
1940
|
+
|
|
1941
|
+
let suggestedSteps: string[];
|
|
1942
|
+
if (source === 'auto_crash') {
|
|
1943
|
+
suggestedSteps = [
|
|
1944
|
+
`Navigate to ${route}`,
|
|
1945
|
+
'Reproduce the action that triggered the crash',
|
|
1946
|
+
'Verify the page does not throw an unhandled error',
|
|
1947
|
+
'Verify error boundary displays a user-friendly message if error occurs',
|
|
1948
|
+
];
|
|
1949
|
+
} else if (source === 'auto_api') {
|
|
1950
|
+
const statusCode = (report.app_context as any)?.custom?.statusCode || 'error';
|
|
1951
|
+
const method = (report.app_context as any)?.custom?.requestMethod || 'API';
|
|
1952
|
+
suggestedSteps = [
|
|
1953
|
+
`Navigate to ${route}`,
|
|
1954
|
+
`Trigger the ${method} request that returned ${statusCode}`,
|
|
1955
|
+
'Verify the request succeeds or displays an appropriate error message',
|
|
1956
|
+
'Verify no data corruption occurs on failure',
|
|
1957
|
+
];
|
|
1958
|
+
} else {
|
|
1959
|
+
// rage_click or sentry_sync
|
|
1960
|
+
const target = (report.app_context as any)?.custom?.targetSelector || 'the element';
|
|
1961
|
+
suggestedSteps = [
|
|
1962
|
+
`Navigate to ${route}`,
|
|
1963
|
+
`Click on ${target}`,
|
|
1964
|
+
'Verify the element responds to interaction',
|
|
1965
|
+
'Verify loading state is shown if action takes time',
|
|
1966
|
+
];
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1969
|
+
return {
|
|
1970
|
+
title: `Test: ${report.title?.replace('[Auto] ', '') || 'Auto-detected issue'}`,
|
|
1971
|
+
track: source === 'auto_crash' ? 'Stability' : source === 'auto_api' ? 'API' : 'UX',
|
|
1972
|
+
priority,
|
|
1973
|
+
rationale: `Auto-detected ${source?.replace('auto_', '')} on ${route}`,
|
|
1974
|
+
suggested_steps: suggestedSteps,
|
|
1975
|
+
source_report_id: report.id,
|
|
1976
|
+
route,
|
|
1977
|
+
};
|
|
1978
|
+
});
|
|
1979
|
+
|
|
1980
|
+
return { suggestions };
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1237
1983
|
// === Project management handlers ===
|
|
1238
1984
|
|
|
1239
1985
|
async function listProjects() {
|
|
@@ -1358,7 +2104,7 @@ async function getReport(args: { report_id: string }) {
|
|
|
1358
2104
|
app_context: data.app_context,
|
|
1359
2105
|
device_info: data.device_info,
|
|
1360
2106
|
navigation_history: data.navigation_history,
|
|
1361
|
-
|
|
2107
|
+
screenshot_urls: data.screenshot_urls,
|
|
1362
2108
|
created_at: data.created_at,
|
|
1363
2109
|
reporter: data.tester ? {
|
|
1364
2110
|
name: data.tester.name,
|
|
@@ -1444,14 +2190,14 @@ async function updateReportStatus(args: {
|
|
|
1444
2190
|
return { success: true, message: `Report status updated to ${args.status}` };
|
|
1445
2191
|
}
|
|
1446
2192
|
|
|
1447
|
-
async function getReportContext(args: { report_id: string }) {
|
|
2193
|
+
async function getReportContext(args: { report_id: string; compact?: boolean }) {
|
|
1448
2194
|
if (!isValidUUID(args.report_id)) {
|
|
1449
2195
|
return { error: 'Invalid report_id format' };
|
|
1450
2196
|
}
|
|
1451
2197
|
|
|
1452
2198
|
const { data, error } = await supabase
|
|
1453
2199
|
.from('reports')
|
|
1454
|
-
.select('app_context, device_info, navigation_history, enhanced_context')
|
|
2200
|
+
.select('app_context, device_info, navigation_history, enhanced_context, screenshot_urls')
|
|
1455
2201
|
.eq('id', args.report_id)
|
|
1456
2202
|
.eq('project_id', currentProjectId) // Security: ensure report belongs to this project
|
|
1457
2203
|
.single();
|
|
@@ -1460,16 +2206,83 @@ async function getReportContext(args: { report_id: string }) {
|
|
|
1460
2206
|
return { error: error.message };
|
|
1461
2207
|
}
|
|
1462
2208
|
|
|
2209
|
+
// Compact: return app_context only (skip console/network/navigation)
|
|
2210
|
+
if (args.compact === true) {
|
|
2211
|
+
return {
|
|
2212
|
+
context: {
|
|
2213
|
+
app_context: data.app_context,
|
|
2214
|
+
screenshot_urls: data.screenshot_urls,
|
|
2215
|
+
},
|
|
2216
|
+
};
|
|
2217
|
+
}
|
|
2218
|
+
|
|
1463
2219
|
return {
|
|
1464
2220
|
context: {
|
|
1465
2221
|
app_context: data.app_context,
|
|
1466
2222
|
device_info: data.device_info,
|
|
1467
2223
|
navigation_history: data.navigation_history,
|
|
1468
2224
|
enhanced_context: data.enhanced_context || {},
|
|
2225
|
+
screenshot_urls: data.screenshot_urls,
|
|
1469
2226
|
},
|
|
1470
2227
|
};
|
|
1471
2228
|
}
|
|
1472
2229
|
|
|
2230
|
+
async function addReportComment(args: { report_id: string; message: string; author?: string }) {
|
|
2231
|
+
if (!isValidUUID(args.report_id)) return { error: 'Invalid report_id format' };
|
|
2232
|
+
if (!args.message?.trim()) return { error: 'Message is required' };
|
|
2233
|
+
|
|
2234
|
+
// Verify report exists
|
|
2235
|
+
const { data: report } = await supabase
|
|
2236
|
+
.from('reports').select('id').eq('id', args.report_id).eq('project_id', currentProjectId).single();
|
|
2237
|
+
if (!report) return { error: 'Report not found' };
|
|
2238
|
+
|
|
2239
|
+
// Find or create a discussion thread for this report
|
|
2240
|
+
const { data: existingThread } = await supabase
|
|
2241
|
+
.from('discussion_threads').select('id')
|
|
2242
|
+
.eq('project_id', currentProjectId).eq('report_id', args.report_id).eq('thread_type', 'report')
|
|
2243
|
+
.limit(1).single();
|
|
2244
|
+
|
|
2245
|
+
let threadId: string;
|
|
2246
|
+
if (existingThread) {
|
|
2247
|
+
threadId = existingThread.id;
|
|
2248
|
+
} else {
|
|
2249
|
+
const newId = crypto.randomUUID();
|
|
2250
|
+
const { error: threadErr } = await supabase
|
|
2251
|
+
.from('discussion_threads').insert({
|
|
2252
|
+
id: newId, project_id: currentProjectId, report_id: args.report_id,
|
|
2253
|
+
thread_type: 'report', subject: 'Bug Report Discussion', audience: 'all',
|
|
2254
|
+
priority: 'normal', created_by_admin: true, last_message_at: new Date().toISOString(),
|
|
2255
|
+
});
|
|
2256
|
+
if (threadErr) return { error: `Failed to create thread: ${threadErr.message}` };
|
|
2257
|
+
threadId = newId;
|
|
2258
|
+
}
|
|
2259
|
+
|
|
2260
|
+
const { data: msg, error: msgErr } = await supabase
|
|
2261
|
+
.from('discussion_messages').insert({
|
|
2262
|
+
thread_id: threadId, sender_type: 'admin', sender_name: args.author || 'Claude Code', content: args.message.trim(), content_type: 'text',
|
|
2263
|
+
}).select('id, content, created_at').single();
|
|
2264
|
+
|
|
2265
|
+
if (msgErr) return { error: `Failed to add comment: ${msgErr.message}` };
|
|
2266
|
+
return { success: true, comment: { id: msg.id, thread_id: threadId, content: msg.content, author: args.author || 'Claude Code', created_at: msg.created_at }, message: 'Comment added to report' };
|
|
2267
|
+
}
|
|
2268
|
+
|
|
2269
|
+
async function getReportComments(args: { report_id: string }) {
|
|
2270
|
+
if (!isValidUUID(args.report_id)) return { error: 'Invalid report_id format' };
|
|
2271
|
+
|
|
2272
|
+
const { data: threads } = await supabase
|
|
2273
|
+
.from('discussion_threads').select('id')
|
|
2274
|
+
.eq('project_id', currentProjectId).eq('report_id', args.report_id).order('created_at', { ascending: true });
|
|
2275
|
+
|
|
2276
|
+
if (!threads || threads.length === 0) return { comments: [], total: 0, message: 'No comments on this report' };
|
|
2277
|
+
|
|
2278
|
+
const { data: messages, error } = await supabase
|
|
2279
|
+
.from('discussion_messages').select('id, thread_id, sender_type, content, content_type, created_at, attachments')
|
|
2280
|
+
.in('thread_id', threads.map(t => t.id)).order('created_at', { ascending: true });
|
|
2281
|
+
|
|
2282
|
+
if (error) return { error: error.message };
|
|
2283
|
+
return { comments: (messages || []).map(m => ({ id: m.id, sender_type: m.sender_type, content: m.content, created_at: m.created_at, attachments: m.attachments })), total: (messages || []).length };
|
|
2284
|
+
}
|
|
2285
|
+
|
|
1473
2286
|
async function getProjectInfo() {
|
|
1474
2287
|
// Get project details
|
|
1475
2288
|
const { data: project, error: projectError } = await supabase
|
|
@@ -1810,6 +2623,7 @@ async function listTestCases(args: {
|
|
|
1810
2623
|
missing_target_route?: boolean;
|
|
1811
2624
|
limit?: number;
|
|
1812
2625
|
offset?: number;
|
|
2626
|
+
compact?: boolean;
|
|
1813
2627
|
}) {
|
|
1814
2628
|
let query = supabase
|
|
1815
2629
|
.from('test_cases')
|
|
@@ -1856,6 +2670,20 @@ async function listTestCases(args: {
|
|
|
1856
2670
|
);
|
|
1857
2671
|
}
|
|
1858
2672
|
|
|
2673
|
+
// Compact: return minimal fields only
|
|
2674
|
+
if (args.compact === true) {
|
|
2675
|
+
return {
|
|
2676
|
+
count: testCases.length,
|
|
2677
|
+
testCases: testCases.map((tc: any) => ({
|
|
2678
|
+
id: tc.id,
|
|
2679
|
+
testKey: tc.test_key,
|
|
2680
|
+
title: tc.title,
|
|
2681
|
+
priority: tc.priority,
|
|
2682
|
+
})),
|
|
2683
|
+
pagination: { limit, offset, hasMore: testCases.length === limit },
|
|
2684
|
+
};
|
|
2685
|
+
}
|
|
2686
|
+
|
|
1859
2687
|
return {
|
|
1860
2688
|
count: testCases.length,
|
|
1861
2689
|
testCases: testCases.map((tc: any) => ({
|
|
@@ -2467,8 +3295,8 @@ async function getCoverageMatrix(args: {
|
|
|
2467
3295
|
include_execution_data?: boolean;
|
|
2468
3296
|
include_bug_counts?: boolean;
|
|
2469
3297
|
}) {
|
|
2470
|
-
const includeExecution = args.include_execution_data
|
|
2471
|
-
const includeBugs = args.include_bug_counts
|
|
3298
|
+
const includeExecution = args.include_execution_data === true;
|
|
3299
|
+
const includeBugs = args.include_bug_counts === true;
|
|
2472
3300
|
|
|
2473
3301
|
// Get tracks
|
|
2474
3302
|
const { data: tracks } = await supabase
|
|
@@ -4167,7 +4995,7 @@ async function markFixedWithCommit(args: {
|
|
|
4167
4995
|
status: 'resolved',
|
|
4168
4996
|
resolved_at: new Date().toISOString(),
|
|
4169
4997
|
resolution_notes: args.resolution_notes || `Fixed in commit ${args.commit_sha.slice(0, 7)}`,
|
|
4170
|
-
notify_tester: args.notify_tester
|
|
4998
|
+
notify_tester: args.notify_tester !== false, // Default: notify tester. Pass false to silently resolve.
|
|
4171
4999
|
code_context: {
|
|
4172
5000
|
...existingContext,
|
|
4173
5001
|
fix: {
|
|
@@ -4190,7 +5018,8 @@ async function markFixedWithCommit(args: {
|
|
|
4190
5018
|
return { error: error.message };
|
|
4191
5019
|
}
|
|
4192
5020
|
|
|
4193
|
-
const
|
|
5021
|
+
const notifyTester = args.notify_tester !== false;
|
|
5022
|
+
const notificationStatus = notifyTester
|
|
4194
5023
|
? 'The original tester will be notified and assigned a verification task.'
|
|
4195
5024
|
: 'No notification sent (silent resolve). A verification task was created.';
|
|
4196
5025
|
|
|
@@ -4199,7 +5028,7 @@ async function markFixedWithCommit(args: {
|
|
|
4199
5028
|
message: `Bug marked as fixed in commit ${args.commit_sha.slice(0, 7)}. ${notificationStatus}`,
|
|
4200
5029
|
report_id: args.report_id,
|
|
4201
5030
|
commit: args.commit_sha,
|
|
4202
|
-
tester_notified:
|
|
5031
|
+
tester_notified: notifyTester,
|
|
4203
5032
|
next_steps: [
|
|
4204
5033
|
'Consider running create_regression_test to prevent this bug from recurring',
|
|
4205
5034
|
'Push your changes to trigger CI/CD',
|
|
@@ -5039,16 +5868,20 @@ Which files or areas would you like me to analyze?`;
|
|
|
5039
5868
|
async function listTesters(args: {
|
|
5040
5869
|
status?: string;
|
|
5041
5870
|
platform?: string;
|
|
5871
|
+
role?: string;
|
|
5042
5872
|
}) {
|
|
5043
5873
|
let query = supabase
|
|
5044
5874
|
.from('testers')
|
|
5045
|
-
.select('id, name, email, status, platforms, tier, assigned_count, completed_count, notes, created_at')
|
|
5875
|
+
.select('id, name, email, status, platforms, tier, assigned_count, completed_count, notes, role, created_at')
|
|
5046
5876
|
.eq('project_id', currentProjectId)
|
|
5047
5877
|
.order('name', { ascending: true });
|
|
5048
5878
|
|
|
5049
5879
|
if (args.status) {
|
|
5050
5880
|
query = query.eq('status', args.status);
|
|
5051
5881
|
}
|
|
5882
|
+
if (args.role) {
|
|
5883
|
+
query = query.eq('role', args.role);
|
|
5884
|
+
}
|
|
5052
5885
|
|
|
5053
5886
|
const { data, error } = await query;
|
|
5054
5887
|
|
|
@@ -5077,6 +5910,7 @@ async function listTesters(args: {
|
|
|
5077
5910
|
assignedCount: t.assigned_count,
|
|
5078
5911
|
completedCount: t.completed_count,
|
|
5079
5912
|
notes: t.notes,
|
|
5913
|
+
role: t.role,
|
|
5080
5914
|
})),
|
|
5081
5915
|
};
|
|
5082
5916
|
}
|
|
@@ -5321,6 +6155,22 @@ async function assignTests(args: {
|
|
|
5321
6155
|
status: 'pending',
|
|
5322
6156
|
}));
|
|
5323
6157
|
|
|
6158
|
+
// Helper: after assignments change, sync the test run's total_tests counter
|
|
6159
|
+
async function syncRunCounter() {
|
|
6160
|
+
if (!args.test_run_id) return;
|
|
6161
|
+
const { count } = await supabase
|
|
6162
|
+
.from('test_assignments')
|
|
6163
|
+
.select('id', { count: 'exact', head: true })
|
|
6164
|
+
.eq('test_run_id', args.test_run_id)
|
|
6165
|
+
.eq('project_id', currentProjectId);
|
|
6166
|
+
if (count !== null) {
|
|
6167
|
+
await supabase
|
|
6168
|
+
.from('test_runs')
|
|
6169
|
+
.update({ total_tests: count })
|
|
6170
|
+
.eq('id', args.test_run_id);
|
|
6171
|
+
}
|
|
6172
|
+
}
|
|
6173
|
+
|
|
5324
6174
|
// Insert — use upsert-like approach: insert and handle conflicts
|
|
5325
6175
|
const { data: inserted, error: insertErr } = await supabase
|
|
5326
6176
|
.from('test_assignments')
|
|
@@ -5349,6 +6199,8 @@ async function assignTests(args: {
|
|
|
5349
6199
|
}
|
|
5350
6200
|
}
|
|
5351
6201
|
|
|
6202
|
+
await syncRunCounter();
|
|
6203
|
+
|
|
5352
6204
|
return {
|
|
5353
6205
|
success: true,
|
|
5354
6206
|
created: created.length,
|
|
@@ -5361,6 +6213,8 @@ async function assignTests(args: {
|
|
|
5361
6213
|
return { error: insertErr.message };
|
|
5362
6214
|
}
|
|
5363
6215
|
|
|
6216
|
+
await syncRunCounter();
|
|
6217
|
+
|
|
5364
6218
|
return {
|
|
5365
6219
|
success: true,
|
|
5366
6220
|
created: (inserted || []).length,
|
|
@@ -5371,6 +6225,82 @@ async function assignTests(args: {
|
|
|
5371
6225
|
};
|
|
5372
6226
|
}
|
|
5373
6227
|
|
|
6228
|
+
async function unassignTests(args: {
|
|
6229
|
+
assignment_ids: string[];
|
|
6230
|
+
}) {
|
|
6231
|
+
if (!args.assignment_ids || args.assignment_ids.length === 0) {
|
|
6232
|
+
return { error: 'At least one assignment_id is required' };
|
|
6233
|
+
}
|
|
6234
|
+
if (args.assignment_ids.length > 50) {
|
|
6235
|
+
return { error: 'Maximum 50 assignments per unassign batch' };
|
|
6236
|
+
}
|
|
6237
|
+
const invalidIds = args.assignment_ids.filter(id => !isValidUUID(id));
|
|
6238
|
+
if (invalidIds.length > 0) {
|
|
6239
|
+
return { error: `Invalid UUID(s): ${invalidIds.join(', ')}` };
|
|
6240
|
+
}
|
|
6241
|
+
|
|
6242
|
+
// Verify assignments exist and belong to this project
|
|
6243
|
+
const { data: existing, error: lookupErr } = await supabase
|
|
6244
|
+
.from('test_assignments')
|
|
6245
|
+
.select('id, test_run_id, test_case:test_cases(test_key, title), tester:testers(name)')
|
|
6246
|
+
.eq('project_id', currentProjectId)
|
|
6247
|
+
.in('id', args.assignment_ids);
|
|
6248
|
+
|
|
6249
|
+
if (lookupErr) return { error: lookupErr.message };
|
|
6250
|
+
|
|
6251
|
+
if (!existing || existing.length === 0) {
|
|
6252
|
+
return { error: 'No matching assignments found in this project' };
|
|
6253
|
+
}
|
|
6254
|
+
|
|
6255
|
+
const foundIds = new Set(existing.map((a: any) => a.id));
|
|
6256
|
+
const notFound = args.assignment_ids.filter(id => !foundIds.has(id));
|
|
6257
|
+
|
|
6258
|
+
// Delete the assignments
|
|
6259
|
+
const { error: deleteErr } = await supabase
|
|
6260
|
+
.from('test_assignments')
|
|
6261
|
+
.delete()
|
|
6262
|
+
.eq('project_id', currentProjectId)
|
|
6263
|
+
.in('id', args.assignment_ids);
|
|
6264
|
+
|
|
6265
|
+
if (deleteErr) return { error: deleteErr.message };
|
|
6266
|
+
|
|
6267
|
+
// Sync run counters for any affected test runs
|
|
6268
|
+
const affectedRunIds = [...new Set(existing.filter((a: any) => a.test_run_id).map((a: any) => a.test_run_id))];
|
|
6269
|
+
for (const runId of affectedRunIds) {
|
|
6270
|
+
const { count } = await supabase
|
|
6271
|
+
.from('test_assignments')
|
|
6272
|
+
.select('id', { count: 'exact', head: true })
|
|
6273
|
+
.eq('test_run_id', runId)
|
|
6274
|
+
.eq('project_id', currentProjectId);
|
|
6275
|
+
if (count !== null) {
|
|
6276
|
+
await supabase.from('test_runs').update({ total_tests: count }).eq('id', runId);
|
|
6277
|
+
}
|
|
6278
|
+
}
|
|
6279
|
+
|
|
6280
|
+
const deleted = existing.map((a: Record<string, unknown>) => {
|
|
6281
|
+
const tc = a.test_case as Record<string, string> | null;
|
|
6282
|
+
const tester = a.tester as Record<string, string> | null;
|
|
6283
|
+
return {
|
|
6284
|
+
id: a.id as string,
|
|
6285
|
+
testKey: tc?.test_key || null,
|
|
6286
|
+
testTitle: tc?.title || null,
|
|
6287
|
+
testerName: tester?.name || null,
|
|
6288
|
+
};
|
|
6289
|
+
});
|
|
6290
|
+
|
|
6291
|
+
const firstKey = deleted[0]?.testKey;
|
|
6292
|
+
|
|
6293
|
+
return {
|
|
6294
|
+
success: true,
|
|
6295
|
+
deletedCount: existing.length,
|
|
6296
|
+
deleted,
|
|
6297
|
+
notFound: notFound.length > 0 ? notFound : undefined,
|
|
6298
|
+
message: existing.length === 1
|
|
6299
|
+
? `Removed 1 assignment${firstKey ? ` (${firstKey})` : ''}`
|
|
6300
|
+
: `Removed ${existing.length} assignment(s)`,
|
|
6301
|
+
};
|
|
6302
|
+
}
|
|
6303
|
+
|
|
5374
6304
|
async function getTesterWorkload(args: {
|
|
5375
6305
|
tester_id: string;
|
|
5376
6306
|
}) {
|
|
@@ -5459,6 +6389,7 @@ async function createTester(args: {
|
|
|
5459
6389
|
platforms?: string[];
|
|
5460
6390
|
tier?: number;
|
|
5461
6391
|
notes?: string;
|
|
6392
|
+
role?: string;
|
|
5462
6393
|
}) {
|
|
5463
6394
|
if (!args.name || args.name.trim().length === 0) {
|
|
5464
6395
|
return { error: 'Tester name is required' };
|
|
@@ -5489,8 +6420,9 @@ async function createTester(args: {
|
|
|
5489
6420
|
tier: args.tier ?? 1,
|
|
5490
6421
|
notes: args.notes?.trim() || null,
|
|
5491
6422
|
status: 'active',
|
|
6423
|
+
role: args.role || 'tester',
|
|
5492
6424
|
})
|
|
5493
|
-
.select('id, name, email, status, platforms, tier, notes, created_at')
|
|
6425
|
+
.select('id, name, email, status, platforms, tier, notes, role, created_at')
|
|
5494
6426
|
.single();
|
|
5495
6427
|
|
|
5496
6428
|
if (error) {
|
|
@@ -5510,6 +6442,7 @@ async function createTester(args: {
|
|
|
5510
6442
|
platforms: data.platforms,
|
|
5511
6443
|
tier: data.tier,
|
|
5512
6444
|
notes: data.notes,
|
|
6445
|
+
role: data.role,
|
|
5513
6446
|
createdAt: data.created_at,
|
|
5514
6447
|
},
|
|
5515
6448
|
message: `Tester "${data.name}" added to the project. Use assign_tests to give them test cases.`,
|
|
@@ -5796,6 +6729,8 @@ async function getTesterLeaderboard(args: {
|
|
|
5796
6729
|
|
|
5797
6730
|
async function exportTestResults(args: {
|
|
5798
6731
|
test_run_id: string;
|
|
6732
|
+
compact?: boolean;
|
|
6733
|
+
limit?: number;
|
|
5799
6734
|
}) {
|
|
5800
6735
|
if (!isValidUUID(args.test_run_id)) {
|
|
5801
6736
|
return { error: 'Invalid test_run_id format' };
|
|
@@ -5844,27 +6779,42 @@ async function exportTestResults(args: {
|
|
|
5844
6779
|
const passCount = all.filter(a => a.status === 'passed').length;
|
|
5845
6780
|
const failCount = all.filter(a => a.status === 'failed').length;
|
|
5846
6781
|
|
|
6782
|
+
const testRunInfo = {
|
|
6783
|
+
id: run.id,
|
|
6784
|
+
name: run.name,
|
|
6785
|
+
description: run.description,
|
|
6786
|
+
status: run.status,
|
|
6787
|
+
startedAt: run.started_at,
|
|
6788
|
+
completedAt: run.completed_at,
|
|
6789
|
+
createdAt: run.created_at,
|
|
6790
|
+
};
|
|
6791
|
+
|
|
6792
|
+
const summaryInfo = {
|
|
6793
|
+
totalAssignments: all.length,
|
|
6794
|
+
passed: passCount,
|
|
6795
|
+
failed: failCount,
|
|
6796
|
+
blocked: all.filter(a => a.status === 'blocked').length,
|
|
6797
|
+
skipped: all.filter(a => a.status === 'skipped').length,
|
|
6798
|
+
pending: all.filter(a => a.status === 'pending').length,
|
|
6799
|
+
inProgress: all.filter(a => a.status === 'in_progress').length,
|
|
6800
|
+
passRate: all.length > 0 ? Math.round((passCount / all.length) * 100) : 0,
|
|
6801
|
+
};
|
|
6802
|
+
|
|
6803
|
+
// Compact: return test run info + summary only, no assignments array
|
|
6804
|
+
if (args.compact === true) {
|
|
6805
|
+
return { testRun: testRunInfo, summary: summaryInfo };
|
|
6806
|
+
}
|
|
6807
|
+
|
|
6808
|
+
// Apply limit (default: 100, max: 500)
|
|
6809
|
+
const assignmentLimit = Math.min(Math.max(args.limit ?? 100, 1), 500);
|
|
6810
|
+
const limitedAssignments = all.slice(0, assignmentLimit);
|
|
6811
|
+
|
|
5847
6812
|
return {
|
|
5848
|
-
testRun:
|
|
5849
|
-
|
|
5850
|
-
|
|
5851
|
-
|
|
5852
|
-
|
|
5853
|
-
startedAt: run.started_at,
|
|
5854
|
-
completedAt: run.completed_at,
|
|
5855
|
-
createdAt: run.created_at,
|
|
5856
|
-
},
|
|
5857
|
-
summary: {
|
|
5858
|
-
totalAssignments: all.length,
|
|
5859
|
-
passed: passCount,
|
|
5860
|
-
failed: failCount,
|
|
5861
|
-
blocked: all.filter(a => a.status === 'blocked').length,
|
|
5862
|
-
skipped: all.filter(a => a.status === 'skipped').length,
|
|
5863
|
-
pending: all.filter(a => a.status === 'pending').length,
|
|
5864
|
-
inProgress: all.filter(a => a.status === 'in_progress').length,
|
|
5865
|
-
passRate: all.length > 0 ? Math.round((passCount / all.length) * 100) : 0,
|
|
5866
|
-
},
|
|
5867
|
-
assignments: all.map((a: any) => ({
|
|
6813
|
+
testRun: testRunInfo,
|
|
6814
|
+
summary: summaryInfo,
|
|
6815
|
+
assignmentsReturned: limitedAssignments.length,
|
|
6816
|
+
assignmentsTotal: all.length,
|
|
6817
|
+
assignments: limitedAssignments.map((a: any) => ({
|
|
5868
6818
|
id: a.id,
|
|
5869
6819
|
status: a.status,
|
|
5870
6820
|
assignedAt: a.assigned_at,
|
|
@@ -5999,6 +6949,12 @@ async function main() {
|
|
|
5999
6949
|
case 'get_report_context':
|
|
6000
6950
|
result = await getReportContext(args as any);
|
|
6001
6951
|
break;
|
|
6952
|
+
case 'add_report_comment':
|
|
6953
|
+
result = await addReportComment(args as any);
|
|
6954
|
+
break;
|
|
6955
|
+
case 'get_report_comments':
|
|
6956
|
+
result = await getReportComments(args as any);
|
|
6957
|
+
break;
|
|
6002
6958
|
case 'get_project_info':
|
|
6003
6959
|
result = await getProjectInfo();
|
|
6004
6960
|
break;
|
|
@@ -6112,6 +7068,9 @@ async function main() {
|
|
|
6112
7068
|
case 'assign_tests':
|
|
6113
7069
|
result = await assignTests(args as any);
|
|
6114
7070
|
break;
|
|
7071
|
+
case 'unassign_tests':
|
|
7072
|
+
result = await unassignTests(args as any);
|
|
7073
|
+
break;
|
|
6115
7074
|
case 'get_tester_workload':
|
|
6116
7075
|
result = await getTesterWorkload(args as any);
|
|
6117
7076
|
break;
|
|
@@ -6147,6 +7106,32 @@ async function main() {
|
|
|
6147
7106
|
case 'get_current_project':
|
|
6148
7107
|
result = getCurrentProject();
|
|
6149
7108
|
break;
|
|
7109
|
+
// === TEST EXECUTION INTELLIGENCE ===
|
|
7110
|
+
case 'get_test_impact':
|
|
7111
|
+
result = await getTestImpact(args as any);
|
|
7112
|
+
break;
|
|
7113
|
+
case 'get_flaky_tests':
|
|
7114
|
+
result = await getFlakyTests(args as any);
|
|
7115
|
+
break;
|
|
7116
|
+
case 'assess_test_quality':
|
|
7117
|
+
result = await assessTestQuality(args as any);
|
|
7118
|
+
break;
|
|
7119
|
+
case 'get_test_execution_summary':
|
|
7120
|
+
result = await getTestExecutionSummary(args as any);
|
|
7121
|
+
break;
|
|
7122
|
+
case 'check_test_freshness':
|
|
7123
|
+
result = await checkTestFreshness(args as any);
|
|
7124
|
+
break;
|
|
7125
|
+
case 'get_untested_changes':
|
|
7126
|
+
result = await getUntestedChanges(args as any);
|
|
7127
|
+
break;
|
|
7128
|
+
// === AUTO-MONITORING TOOLS ===
|
|
7129
|
+
case 'get_auto_detected_issues':
|
|
7130
|
+
result = await getAutoDetectedIssues(args as any);
|
|
7131
|
+
break;
|
|
7132
|
+
case 'generate_tests_from_errors':
|
|
7133
|
+
result = await generateTestsFromErrors(args as any);
|
|
7134
|
+
break;
|
|
6150
7135
|
default:
|
|
6151
7136
|
return {
|
|
6152
7137
|
content: [{ type: 'text', text: `Unknown tool: ${name}` }],
|