@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.
Files changed (3) hide show
  1. package/dist/index.js +930 -37
  2. package/package.json +2 -1
  3. 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', 'resolved', 'verified', 'wont_fix', 'duplicate'],
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: 'If true, notify the original tester about the fix with a message and verification task. Default: false (silent resolve).',
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
- screenshots: data.screenshots,
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 !== false;
2471
- const includeBugs = args.include_bug_counts !== false;
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 === true, // Opt-in: only notify if explicitly requested
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 notificationStatus = args.notify_tester
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: args.notify_tester === true,
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
- id: run.id,
5850
- name: run.name,
5851
- description: run.description,
5852
- status: run.status,
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}` }],