@bbearai/mcp-server 0.5.1 → 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 +1712 -115
  2. package/package.json +2 -1
  3. package/src/index.ts +2422 -655
package/dist/index.js CHANGED
@@ -12,26 +12,30 @@ const supabase_js_1 = require("@supabase/supabase-js");
12
12
  // Configuration from environment
13
13
  const SUPABASE_URL = process.env.SUPABASE_URL || 'https://kyxgzjnqgvapvlnvqawz.supabase.co';
14
14
  const SUPABASE_ANON_KEY = process.env.SUPABASE_ANON_KEY || '';
15
- const PROJECT_ID = process.env.BUGBEAR_PROJECT_ID || '';
15
+ // Active project — set from env var, switchable via switch_project tool
16
+ let currentProjectId = process.env.BUGBEAR_currentProjectId || '';
16
17
  // Initialize Supabase client
17
18
  let supabase;
18
19
  function validateConfig() {
19
- const errors = [];
20
20
  if (!SUPABASE_ANON_KEY) {
21
- errors.push('SUPABASE_ANON_KEY environment variable is required');
21
+ console.error('BugBear MCP Server: SUPABASE_ANON_KEY environment variable is required');
22
+ process.exit(1);
22
23
  }
23
- if (!PROJECT_ID) {
24
- errors.push('BUGBEAR_PROJECT_ID environment variable is required');
24
+ // Validate project ID format if provided
25
+ if (currentProjectId && !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(currentProjectId)) {
26
+ console.error('BugBear MCP Server: BUGBEAR_currentProjectId must be a valid UUID');
27
+ process.exit(1);
25
28
  }
26
- // Basic UUID format validation for project ID
27
- if (PROJECT_ID && !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(PROJECT_ID)) {
28
- errors.push('BUGBEAR_PROJECT_ID must be a valid UUID');
29
+ if (!currentProjectId) {
30
+ console.error('BugBear MCP Server: No BUGBEAR_currentProjectId set. Use list_projects + switch_project to select one.');
29
31
  }
30
- if (errors.length > 0) {
31
- console.error('BugBear MCP Server configuration errors:');
32
- errors.forEach(e => console.error(` - ${e}`));
33
- process.exit(1);
32
+ }
33
+ /** Guard for tools that require a project to be selected */
34
+ function requireProject() {
35
+ if (!currentProjectId) {
36
+ throw new Error('No project selected. Use list_projects to see available projects, then switch_project to select one.');
34
37
  }
38
+ return currentProjectId;
35
39
  }
36
40
  function initSupabase() {
37
41
  validateConfig();
@@ -124,7 +128,7 @@ const tools = [
124
128
  },
125
129
  status: {
126
130
  type: 'string',
127
- enum: ['new', 'triaging', 'confirmed', 'in_progress', 'fixed', 'resolved', 'verified', 'wont_fix', 'duplicate'],
131
+ enum: ['new', 'triaging', 'confirmed', 'in_progress', 'fixed', 'ready_to_test', 'verified', 'resolved', 'reviewed', 'closed', 'wont_fix', 'duplicate'],
128
132
  description: 'The new status for the report',
129
133
  },
130
134
  resolution_notes: {
@@ -137,7 +141,47 @@ const tools = [
137
141
  },
138
142
  {
139
143
  name: 'get_report_context',
140
- description: 'Get the full debugging context for a report including console logs, network requests, and navigation history',
144
+ 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).',
145
+ inputSchema: {
146
+ type: 'object',
147
+ properties: {
148
+ report_id: {
149
+ type: 'string',
150
+ description: 'The UUID of the report',
151
+ },
152
+ compact: {
153
+ type: 'boolean',
154
+ description: 'Compact mode: returns app_context only, skips console logs, network requests, and navigation history. (default: false)',
155
+ },
156
+ },
157
+ required: ['report_id'],
158
+ },
159
+ },
160
+ {
161
+ name: 'add_report_comment',
162
+ 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.',
163
+ inputSchema: {
164
+ type: 'object',
165
+ properties: {
166
+ report_id: {
167
+ type: 'string',
168
+ description: 'The UUID of the report to comment on',
169
+ },
170
+ message: {
171
+ type: 'string',
172
+ description: 'The comment/note content',
173
+ },
174
+ author: {
175
+ type: 'string',
176
+ description: 'Optional author name (defaults to "Claude Code")',
177
+ },
178
+ },
179
+ required: ['report_id', 'message'],
180
+ },
181
+ },
182
+ {
183
+ name: 'get_report_comments',
184
+ description: 'Get all comments/notes on a bug report in chronological order. Returns the full discussion thread.',
141
185
  inputSchema: {
142
186
  type: 'object',
143
187
  properties: {
@@ -304,7 +348,7 @@ const tools = [
304
348
  },
305
349
  {
306
350
  name: 'list_test_cases',
307
- 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.',
351
+ 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).',
308
352
  inputSchema: {
309
353
  type: 'object',
310
354
  properties: {
@@ -329,6 +373,10 @@ const tools = [
329
373
  type: 'number',
330
374
  description: 'Offset for pagination (default 0)',
331
375
  },
376
+ compact: {
377
+ type: 'boolean',
378
+ description: 'Compact mode: returns id, test_key, title, and priority only. (default: false)',
379
+ },
332
380
  },
333
381
  },
334
382
  },
@@ -437,7 +485,7 @@ const tools = [
437
485
  },
438
486
  notify_tester: {
439
487
  type: 'boolean',
440
- description: 'If true, notify the original tester about the fix with a message and verification task. Default: false (silent resolve).',
488
+ description: 'Notify the original tester about the fix with a message and verification task. Default: true. Set to false for silent resolve.',
441
489
  },
442
490
  },
443
491
  required: ['report_id', 'commit_sha'],
@@ -679,17 +727,17 @@ const tools = [
679
727
  },
680
728
  {
681
729
  name: 'get_coverage_matrix',
682
- 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.',
730
+ 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.',
683
731
  inputSchema: {
684
732
  type: 'object',
685
733
  properties: {
686
734
  include_execution_data: {
687
735
  type: 'boolean',
688
- description: 'Include pass/fail rates and last execution times (default: true)',
736
+ description: 'Include pass/fail rates and last execution times (default: false). Set true when you need execution history.',
689
737
  },
690
738
  include_bug_counts: {
691
739
  type: 'boolean',
692
- description: 'Include open/critical bug counts per route (default: true)',
740
+ description: 'Include open/critical bug counts per route (default: false). Set true when you need bug context.',
693
741
  },
694
742
  },
695
743
  },
@@ -923,6 +971,11 @@ const tools = [
923
971
  enum: ['ios', 'android', 'web'],
924
972
  description: 'Filter by platform support',
925
973
  },
974
+ role: {
975
+ type: 'string',
976
+ enum: ['tester', 'feedback'],
977
+ description: 'Filter by role: "tester" for QA testers, "feedback" for feedback-only users (default: all)',
978
+ },
926
979
  },
927
980
  },
928
981
  },
@@ -1011,6 +1064,21 @@ const tools = [
1011
1064
  required: ['tester_id', 'test_case_ids'],
1012
1065
  },
1013
1066
  },
1067
+ {
1068
+ name: 'unassign_tests',
1069
+ 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.',
1070
+ inputSchema: {
1071
+ type: 'object',
1072
+ properties: {
1073
+ assignment_ids: {
1074
+ type: 'array',
1075
+ items: { type: 'string' },
1076
+ description: 'Array of test assignment UUIDs to remove (required, max 50)',
1077
+ },
1078
+ },
1079
+ required: ['assignment_ids'],
1080
+ },
1081
+ },
1014
1082
  {
1015
1083
  name: 'get_tester_workload',
1016
1084
  description: 'View a specific tester\'s current workload — assignment counts by status and recent assignments.',
@@ -1025,13 +1093,863 @@ const tools = [
1025
1093
  required: ['tester_id'],
1026
1094
  },
1027
1095
  },
1096
+ // === NEW TESTER & ANALYTICS TOOLS ===
1097
+ {
1098
+ name: 'create_tester',
1099
+ description: 'Add a new QA tester to the project without opening the dashboard.',
1100
+ inputSchema: {
1101
+ type: 'object',
1102
+ properties: {
1103
+ name: {
1104
+ type: 'string',
1105
+ description: 'Full name of the tester (required)',
1106
+ },
1107
+ email: {
1108
+ type: 'string',
1109
+ description: 'Email address of the tester (required, must be unique per project)',
1110
+ },
1111
+ platforms: {
1112
+ type: 'array',
1113
+ items: { type: 'string', enum: ['ios', 'android', 'web'] },
1114
+ description: 'Platforms the tester can test on (default: ["ios", "web"])',
1115
+ },
1116
+ tier: {
1117
+ type: 'number',
1118
+ description: 'Tester tier 1-3 (default: 1)',
1119
+ },
1120
+ notes: {
1121
+ type: 'string',
1122
+ description: 'Optional notes about the tester',
1123
+ },
1124
+ role: {
1125
+ type: 'string',
1126
+ enum: ['tester', 'feedback'],
1127
+ description: 'Role: "tester" for QA testers (default), "feedback" for feedback-only users',
1128
+ },
1129
+ },
1130
+ required: ['name', 'email'],
1131
+ },
1132
+ },
1133
+ {
1134
+ name: 'update_tester',
1135
+ description: 'Update an existing tester\'s status, platforms, tier, or notes.',
1136
+ inputSchema: {
1137
+ type: 'object',
1138
+ properties: {
1139
+ tester_id: {
1140
+ type: 'string',
1141
+ description: 'UUID of the tester to update (required)',
1142
+ },
1143
+ status: {
1144
+ type: 'string',
1145
+ enum: ['active', 'inactive', 'invited'],
1146
+ description: 'New status for the tester',
1147
+ },
1148
+ platforms: {
1149
+ type: 'array',
1150
+ items: { type: 'string', enum: ['ios', 'android', 'web'] },
1151
+ description: 'Updated platforms array',
1152
+ },
1153
+ tier: {
1154
+ type: 'number',
1155
+ description: 'Updated tier (1-3)',
1156
+ },
1157
+ notes: {
1158
+ type: 'string',
1159
+ description: 'Updated notes',
1160
+ },
1161
+ name: {
1162
+ type: 'string',
1163
+ description: 'Updated name',
1164
+ },
1165
+ },
1166
+ required: ['tester_id'],
1167
+ },
1168
+ },
1169
+ {
1170
+ name: 'bulk_update_reports',
1171
+ description: 'Update the status of multiple bug reports at once. Useful after a fix session to close many bugs.',
1172
+ inputSchema: {
1173
+ type: 'object',
1174
+ properties: {
1175
+ report_ids: {
1176
+ type: 'array',
1177
+ items: { type: 'string' },
1178
+ description: 'Array of report UUIDs to update (required, max 50)',
1179
+ },
1180
+ status: {
1181
+ type: 'string',
1182
+ enum: ['new', 'triaging', 'confirmed', 'in_progress', 'fixed', 'resolved', 'verified', 'wont_fix', 'duplicate', 'closed'],
1183
+ description: 'New status for all reports (required)',
1184
+ },
1185
+ resolution_notes: {
1186
+ type: 'string',
1187
+ description: 'Optional resolution notes applied to all reports',
1188
+ },
1189
+ },
1190
+ required: ['report_ids', 'status'],
1191
+ },
1192
+ },
1193
+ {
1194
+ name: 'get_bug_trends',
1195
+ description: 'Get bug report trends over time — grouped by week, severity, category, or status. Useful for spotting patterns.',
1196
+ inputSchema: {
1197
+ type: 'object',
1198
+ properties: {
1199
+ group_by: {
1200
+ type: 'string',
1201
+ enum: ['week', 'severity', 'category', 'status'],
1202
+ description: 'How to group the trends (default: week)',
1203
+ },
1204
+ days: {
1205
+ type: 'number',
1206
+ description: 'Number of days to look back (default: 30, max: 180)',
1207
+ },
1208
+ },
1209
+ },
1210
+ },
1211
+ {
1212
+ name: 'get_tester_leaderboard',
1213
+ description: 'Rank testers by testing activity — bugs found, tests completed, pass rate, and average test duration.',
1214
+ inputSchema: {
1215
+ type: 'object',
1216
+ properties: {
1217
+ days: {
1218
+ type: 'number',
1219
+ description: 'Number of days to look back (default: 30, max: 180)',
1220
+ },
1221
+ sort_by: {
1222
+ type: 'string',
1223
+ enum: ['bugs_found', 'tests_completed', 'pass_rate'],
1224
+ description: 'Sort metric (default: tests_completed)',
1225
+ },
1226
+ },
1227
+ },
1228
+ },
1229
+ {
1230
+ name: 'export_test_results',
1231
+ 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.',
1232
+ inputSchema: {
1233
+ type: 'object',
1234
+ properties: {
1235
+ test_run_id: {
1236
+ type: 'string',
1237
+ description: 'UUID of the test run to export (required)',
1238
+ },
1239
+ compact: {
1240
+ type: 'boolean',
1241
+ description: 'Compact mode: returns test run info + summary only, no assignments array. (default: false)',
1242
+ },
1243
+ limit: {
1244
+ type: 'number',
1245
+ description: 'Max assignments to return in full mode (default: 100, max: 500). Ignored when compact=true.',
1246
+ },
1247
+ },
1248
+ required: ['test_run_id'],
1249
+ },
1250
+ },
1251
+ {
1252
+ name: 'get_testing_velocity',
1253
+ description: 'Get a rolling average of test completions per day over the specified window. Shows daily completion counts and trend direction.',
1254
+ inputSchema: {
1255
+ type: 'object',
1256
+ properties: {
1257
+ days: {
1258
+ type: 'number',
1259
+ description: 'Number of days to analyze (default: 14, max: 90)',
1260
+ },
1261
+ },
1262
+ },
1263
+ },
1264
+ // === PROJECT MANAGEMENT TOOLS ===
1265
+ {
1266
+ name: 'list_projects',
1267
+ description: 'List all BugBear projects accessible with the current credentials. Use this to find project IDs for switch_project.',
1268
+ inputSchema: {
1269
+ type: 'object',
1270
+ properties: {},
1271
+ },
1272
+ },
1273
+ {
1274
+ name: 'switch_project',
1275
+ description: 'Switch the active project. All subsequent tool calls will use this project. Use list_projects first to find the project ID.',
1276
+ inputSchema: {
1277
+ type: 'object',
1278
+ properties: {
1279
+ project_id: {
1280
+ type: 'string',
1281
+ description: 'UUID of the project to switch to (required)',
1282
+ },
1283
+ },
1284
+ required: ['project_id'],
1285
+ },
1286
+ },
1287
+ {
1288
+ name: 'get_current_project',
1289
+ description: 'Show which project is currently active.',
1290
+ inputSchema: {
1291
+ type: 'object',
1292
+ properties: {},
1293
+ },
1294
+ },
1295
+ // === TEST EXECUTION INTELLIGENCE ===
1296
+ {
1297
+ name: 'get_test_impact',
1298
+ description: 'Given changed files, identify which test cases are affected by mapping file paths to test case target routes.',
1299
+ inputSchema: {
1300
+ type: 'object',
1301
+ properties: {
1302
+ changed_files: {
1303
+ type: 'array',
1304
+ items: { type: 'string' },
1305
+ description: 'List of changed file paths (relative to project root)',
1306
+ },
1307
+ },
1308
+ required: ['changed_files'],
1309
+ },
1310
+ },
1311
+ {
1312
+ name: 'get_flaky_tests',
1313
+ description: 'Analyze test run history to identify tests with intermittent failure rates above a threshold.',
1314
+ inputSchema: {
1315
+ type: 'object',
1316
+ properties: {
1317
+ threshold: {
1318
+ type: 'number',
1319
+ description: 'Minimum flakiness rate to report (0-100, default: 5)',
1320
+ },
1321
+ limit: {
1322
+ type: 'number',
1323
+ description: 'Maximum results to return (default: 20)',
1324
+ },
1325
+ },
1326
+ },
1327
+ },
1328
+ {
1329
+ name: 'assess_test_quality',
1330
+ description: 'Analyze test case steps for weak patterns: vague assertions, missing edge cases, no negative testing, generic descriptions.',
1331
+ inputSchema: {
1332
+ type: 'object',
1333
+ properties: {
1334
+ test_case_ids: {
1335
+ type: 'array',
1336
+ items: { type: 'string' },
1337
+ description: 'Specific test case IDs to assess. If omitted, assesses recent test cases.',
1338
+ },
1339
+ limit: {
1340
+ type: 'number',
1341
+ description: 'Maximum test cases to assess (default: 20)',
1342
+ },
1343
+ },
1344
+ },
1345
+ },
1346
+ {
1347
+ name: 'get_test_execution_summary',
1348
+ description: 'Aggregate test execution metrics: pass rate, completion rate, most-failed tests, fastest/slowest tests.',
1349
+ inputSchema: {
1350
+ type: 'object',
1351
+ properties: {
1352
+ days: {
1353
+ type: 'number',
1354
+ description: 'Number of days to analyze (default: 30)',
1355
+ },
1356
+ },
1357
+ },
1358
+ },
1359
+ {
1360
+ name: 'check_test_freshness',
1361
+ description: 'Identify test cases that have not been updated since their target code was modified.',
1362
+ inputSchema: {
1363
+ type: 'object',
1364
+ properties: {
1365
+ limit: {
1366
+ type: 'number',
1367
+ description: 'Maximum results to return (default: 20)',
1368
+ },
1369
+ },
1370
+ },
1371
+ },
1372
+ {
1373
+ name: 'get_untested_changes',
1374
+ description: 'Given recent commits or changed files, find code changes with no corresponding test coverage in BugBear.',
1375
+ inputSchema: {
1376
+ type: 'object',
1377
+ properties: {
1378
+ changed_files: {
1379
+ type: 'array',
1380
+ items: { type: 'string' },
1381
+ description: 'List of changed file paths. If omitted, uses git diff against main.',
1382
+ },
1383
+ },
1384
+ },
1385
+ },
1386
+ // === AUTO-MONITORING TOOLS ===
1387
+ {
1388
+ name: 'get_auto_detected_issues',
1389
+ description: 'Get auto-detected monitoring issues grouped by error fingerprint. Shows recurring crashes, API failures, and rage clicks with frequency and user impact.',
1390
+ inputSchema: {
1391
+ type: 'object',
1392
+ properties: {
1393
+ source: {
1394
+ type: 'string',
1395
+ enum: ['auto_crash', 'auto_api', 'auto_rage_click'],
1396
+ description: 'Filter by source type',
1397
+ },
1398
+ min_occurrences: {
1399
+ type: 'number',
1400
+ description: 'Min occurrence count (default: 1)',
1401
+ },
1402
+ since: {
1403
+ type: 'string',
1404
+ description: 'ISO date — only issues after this date (default: 7 days ago)',
1405
+ },
1406
+ limit: {
1407
+ type: 'number',
1408
+ description: 'Max results (default: 20)',
1409
+ },
1410
+ compact: {
1411
+ type: 'boolean',
1412
+ description: 'Compact mode: fingerprint, source, count only',
1413
+ },
1414
+ },
1415
+ },
1416
+ },
1417
+ {
1418
+ name: 'generate_tests_from_errors',
1419
+ description: 'Suggest QA test cases from auto-detected error patterns. Returns structured suggestions — does NOT auto-create test cases.',
1420
+ inputSchema: {
1421
+ type: 'object',
1422
+ properties: {
1423
+ report_ids: {
1424
+ type: 'array',
1425
+ items: { type: 'string' },
1426
+ description: 'Specific report IDs. If omitted, uses top uncovered errors.',
1427
+ },
1428
+ limit: {
1429
+ type: 'number',
1430
+ description: 'Max suggestions (default: 5)',
1431
+ },
1432
+ },
1433
+ },
1434
+ },
1028
1435
  ];
1436
+ // === TEST EXECUTION INTELLIGENCE ===
1437
+ async function getTestImpact(args) {
1438
+ const projectId = requireProject();
1439
+ const changedFiles = args.changed_files || [];
1440
+ if (changedFiles.length === 0) {
1441
+ return { affectedTests: [], message: 'No changed files provided.' };
1442
+ }
1443
+ // Get all test cases for the project with their target routes
1444
+ const { data: testCases, error } = await supabase
1445
+ .from('test_cases')
1446
+ .select('id, title, target_route, qa_track, priority')
1447
+ .eq('project_id', projectId);
1448
+ if (error)
1449
+ return { error: error.message };
1450
+ if (!testCases || testCases.length === 0) {
1451
+ return { affectedTests: [], message: 'No test cases found for this project.' };
1452
+ }
1453
+ // Map changed files to affected test cases
1454
+ const affected = [];
1455
+ for (const tc of testCases) {
1456
+ const route = tc.target_route || '';
1457
+ const matchedFiles = changedFiles.filter(f => {
1458
+ // Match file path to route (e.g., src/app/api/tasks/route.ts -> /api/tasks)
1459
+ const normalized = f.replace(/\\/g, '/');
1460
+ const routeParts = route.split('/').filter(Boolean);
1461
+ return routeParts.some((part) => normalized.includes(part)) || normalized.includes(route.replace(/\//g, '/'));
1462
+ });
1463
+ if (matchedFiles.length > 0) {
1464
+ affected.push({
1465
+ testId: tc.id,
1466
+ title: tc.title,
1467
+ targetRoute: route,
1468
+ matchedFiles,
1469
+ qaTrack: tc.qa_track,
1470
+ });
1471
+ }
1472
+ }
1473
+ return {
1474
+ affectedTests: affected,
1475
+ totalTestCases: testCases.length,
1476
+ affectedCount: affected.length,
1477
+ changedFileCount: changedFiles.length,
1478
+ };
1479
+ }
1480
+ async function getFlakyTests(args) {
1481
+ const projectId = requireProject();
1482
+ const threshold = args.threshold || 5;
1483
+ const limit = args.limit || 20;
1484
+ // Get test results grouped by test case
1485
+ const { data: results, error } = await supabase
1486
+ .from('test_results')
1487
+ .select('test_case_id, status, test_cases!inner(title, target_route, qa_track)')
1488
+ .eq('test_cases.project_id', projectId)
1489
+ .order('created_at', { ascending: false })
1490
+ .limit(5000);
1491
+ if (error)
1492
+ return { error: error.message };
1493
+ if (!results || results.length === 0) {
1494
+ return { flakyTests: [], message: 'No test results found.' };
1495
+ }
1496
+ // Group by test case and calculate flakiness
1497
+ const testStats = {};
1498
+ for (const r of results) {
1499
+ const id = r.test_case_id;
1500
+ if (!testStats[id]) {
1501
+ const tc = r.test_cases;
1502
+ testStats[id] = { passes: 0, fails: 0, total: 0, title: tc?.title || '', route: tc?.target_route || '', track: tc?.qa_track || '' };
1503
+ }
1504
+ testStats[id].total++;
1505
+ if (r.status === 'pass')
1506
+ testStats[id].passes++;
1507
+ else if (r.status === 'fail')
1508
+ testStats[id].fails++;
1509
+ }
1510
+ // Find flaky tests (have both passes and fails, with fail rate above threshold)
1511
+ const flaky = Object.entries(testStats)
1512
+ .filter(([, stats]) => {
1513
+ if (stats.total < 3)
1514
+ return false; // Need enough data
1515
+ const failRate = (stats.fails / stats.total) * 100;
1516
+ const passRate = (stats.passes / stats.total) * 100;
1517
+ return failRate >= threshold && passRate > 0; // Has both passes and fails
1518
+ })
1519
+ .map(([id, stats]) => ({
1520
+ testCaseId: id,
1521
+ title: stats.title,
1522
+ targetRoute: stats.route,
1523
+ qaTrack: stats.track,
1524
+ totalRuns: stats.total,
1525
+ failRate: Math.round((stats.fails / stats.total) * 100),
1526
+ passRate: Math.round((stats.passes / stats.total) * 100),
1527
+ }))
1528
+ .sort((a, b) => b.failRate - a.failRate)
1529
+ .slice(0, limit);
1530
+ return {
1531
+ flakyTests: flaky,
1532
+ totalAnalyzed: Object.keys(testStats).length,
1533
+ flakyCount: flaky.length,
1534
+ threshold,
1535
+ };
1536
+ }
1537
+ async function assessTestQuality(args) {
1538
+ const projectId = requireProject();
1539
+ const limit = args.limit || 20;
1540
+ let query = supabase
1541
+ .from('test_cases')
1542
+ .select('id, title, steps, target_route, qa_track, priority')
1543
+ .eq('project_id', projectId)
1544
+ .limit(limit);
1545
+ if (args.test_case_ids && args.test_case_ids.length > 0) {
1546
+ query = query.in('id', args.test_case_ids);
1547
+ }
1548
+ const { data: testCases, error } = await query;
1549
+ if (error)
1550
+ return { error: error.message };
1551
+ if (!testCases || testCases.length === 0) {
1552
+ return { assessments: [], message: 'No test cases found.' };
1553
+ }
1554
+ const assessments = testCases.map(tc => {
1555
+ const issues = [];
1556
+ const steps = tc.steps || [];
1557
+ // Check for weak patterns
1558
+ if (steps.length < 2) {
1559
+ issues.push('Too few steps — test may not cover the full flow');
1560
+ }
1561
+ const allStepsText = steps.map((s) => (typeof s === 'string' ? s : s.action || s.description || '')).join(' ');
1562
+ // Vague assertions
1563
+ if (/should work|looks good|is correct|verify it works/i.test(allStepsText)) {
1564
+ issues.push('Vague assertions detected — use specific expected outcomes');
1565
+ }
1566
+ // Missing edge cases
1567
+ if (!/error|invalid|empty|missing|unauthorized|forbidden|404|500/i.test(allStepsText)) {
1568
+ issues.push('No negative/error test cases — add edge case testing');
1569
+ }
1570
+ // Generic descriptions
1571
+ if (/test the|check the|verify the/i.test(tc.title) && tc.title.length < 30) {
1572
+ issues.push('Generic test title — be more specific about what is being tested');
1573
+ }
1574
+ // No specific UI elements referenced
1575
+ if (!/button|input|form|modal|dropdown|select|click|type|enter|submit/i.test(allStepsText)) {
1576
+ issues.push('No specific UI elements referenced — steps may be too abstract');
1577
+ }
1578
+ const quality = issues.length === 0 ? 'good' : issues.length <= 2 ? 'needs-improvement' : 'poor';
1579
+ return {
1580
+ testCaseId: tc.id,
1581
+ title: tc.title,
1582
+ targetRoute: tc.target_route,
1583
+ stepCount: steps.length,
1584
+ quality,
1585
+ issues,
1586
+ };
1587
+ });
1588
+ const qualityCounts = {
1589
+ good: assessments.filter(a => a.quality === 'good').length,
1590
+ needsImprovement: assessments.filter(a => a.quality === 'needs-improvement').length,
1591
+ poor: assessments.filter(a => a.quality === 'poor').length,
1592
+ };
1593
+ return {
1594
+ assessments,
1595
+ summary: qualityCounts,
1596
+ totalAssessed: assessments.length,
1597
+ };
1598
+ }
1599
+ async function getTestExecutionSummary(args) {
1600
+ const projectId = requireProject();
1601
+ const days = args.days || 30;
1602
+ const since = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString();
1603
+ // Get test results
1604
+ const { data: results, error } = await supabase
1605
+ .from('test_results')
1606
+ .select('test_case_id, status, duration_ms, created_at, test_cases!inner(title, target_route)')
1607
+ .eq('test_cases.project_id', projectId)
1608
+ .gte('created_at', since)
1609
+ .order('created_at', { ascending: false });
1610
+ if (error)
1611
+ return { error: error.message };
1612
+ if (!results || results.length === 0) {
1613
+ return { message: `No test results found in the last ${days} days.` };
1614
+ }
1615
+ const totalRuns = results.length;
1616
+ const passed = results.filter(r => r.status === 'pass').length;
1617
+ const failed = results.filter(r => r.status === 'fail').length;
1618
+ const blocked = results.filter(r => r.status === 'blocked').length;
1619
+ // Most failed tests
1620
+ const failCounts = {};
1621
+ for (const r of results.filter(r => r.status === 'fail')) {
1622
+ const id = r.test_case_id;
1623
+ const tc = r.test_cases;
1624
+ if (!failCounts[id]) {
1625
+ failCounts[id] = { count: 0, title: tc?.title || '', route: tc?.target_route || '' };
1626
+ }
1627
+ failCounts[id].count++;
1628
+ }
1629
+ const mostFailed = Object.entries(failCounts)
1630
+ .sort((a, b) => b[1].count - a[1].count)
1631
+ .slice(0, 5)
1632
+ .map(([id, data]) => ({ testCaseId: id, ...data }));
1633
+ // Duration stats
1634
+ const durations = results.filter(r => r.duration_ms).map(r => r.duration_ms);
1635
+ const avgDuration = durations.length > 0 ? Math.round(durations.reduce((a, b) => a + b, 0) / durations.length) : 0;
1636
+ const maxDuration = durations.length > 0 ? Math.max(...durations) : 0;
1637
+ return {
1638
+ period: `${days} days`,
1639
+ totalRuns,
1640
+ passRate: Math.round((passed / totalRuns) * 100),
1641
+ failRate: Math.round((failed / totalRuns) * 100),
1642
+ blockedCount: blocked,
1643
+ averageDurationMs: avgDuration,
1644
+ maxDurationMs: maxDuration,
1645
+ mostFailed,
1646
+ uniqueTestsCovered: new Set(results.map(r => r.test_case_id)).size,
1647
+ };
1648
+ }
1649
+ async function checkTestFreshness(args) {
1650
+ const projectId = requireProject();
1651
+ const limit = args.limit || 20;
1652
+ // Get test cases with their last update and last result
1653
+ const { data: testCases, error } = await supabase
1654
+ .from('test_cases')
1655
+ .select('id, title, target_route, updated_at, created_at')
1656
+ .eq('project_id', projectId)
1657
+ .order('updated_at', { ascending: true })
1658
+ .limit(limit);
1659
+ if (error)
1660
+ return { error: error.message };
1661
+ if (!testCases || testCases.length === 0) {
1662
+ return { staleTests: [], message: 'No test cases found.' };
1663
+ }
1664
+ const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString();
1665
+ const stale = testCases
1666
+ .filter(tc => tc.updated_at < thirtyDaysAgo)
1667
+ .map(tc => ({
1668
+ testCaseId: tc.id,
1669
+ title: tc.title,
1670
+ targetRoute: tc.target_route,
1671
+ lastUpdated: tc.updated_at,
1672
+ daysSinceUpdate: Math.round((Date.now() - new Date(tc.updated_at).getTime()) / (24 * 60 * 60 * 1000)),
1673
+ }));
1674
+ return {
1675
+ staleTests: stale,
1676
+ totalTestCases: testCases.length,
1677
+ staleCount: stale.length,
1678
+ stalenessThreshold: '30 days',
1679
+ };
1680
+ }
1681
+ async function getUntestedChanges(args) {
1682
+ const projectId = requireProject();
1683
+ // Get all test cases to understand what's covered
1684
+ const { data: testCases, error } = await supabase
1685
+ .from('test_cases')
1686
+ .select('id, title, target_route')
1687
+ .eq('project_id', projectId);
1688
+ if (error)
1689
+ return { error: error.message };
1690
+ const coveredRoutes = new Set((testCases || []).map(tc => tc.target_route).filter(Boolean));
1691
+ // If changed_files provided, check coverage
1692
+ const changedFiles = args.changed_files || [];
1693
+ if (changedFiles.length === 0) {
1694
+ return {
1695
+ message: 'No changed files provided. Pass changed_files to check coverage.',
1696
+ totalCoveredRoutes: coveredRoutes.size,
1697
+ };
1698
+ }
1699
+ // Map changed files to routes and check coverage
1700
+ const untested = [];
1701
+ for (const file of changedFiles) {
1702
+ const normalized = file.replace(/\\/g, '/');
1703
+ // Extract route-like path from file
1704
+ let inferredRoute = '';
1705
+ // Next.js app router: app/api/tasks/route.ts -> /api/tasks
1706
+ const appRouterMatch = normalized.match(/app\/(api\/[^/]+(?:\/[^/]+)*?)\/route\.\w+$/);
1707
+ if (appRouterMatch) {
1708
+ inferredRoute = '/' + appRouterMatch[1];
1709
+ }
1710
+ // Pages router: pages/api/tasks.ts -> /api/tasks
1711
+ const pagesMatch = normalized.match(/pages\/(api\/[^.]+)\.\w+$/);
1712
+ if (!inferredRoute && pagesMatch) {
1713
+ inferredRoute = '/' + pagesMatch[1];
1714
+ }
1715
+ // Component files
1716
+ const componentMatch = normalized.match(/(?:components|screens|pages)\/([^.]+)\.\w+$/);
1717
+ if (!inferredRoute && componentMatch) {
1718
+ inferredRoute = '/' + componentMatch[1].replace(/\\/g, '/');
1719
+ }
1720
+ if (inferredRoute && !coveredRoutes.has(inferredRoute)) {
1721
+ untested.push({
1722
+ file,
1723
+ inferredRoute,
1724
+ reason: 'No test cases cover this route',
1725
+ });
1726
+ }
1727
+ else if (!inferredRoute) {
1728
+ // Can't map to a route — flag as potentially untested
1729
+ untested.push({
1730
+ file,
1731
+ inferredRoute: 'unknown',
1732
+ reason: 'Could not map file to a testable route',
1733
+ });
1734
+ }
1735
+ }
1736
+ return {
1737
+ untestedChanges: untested,
1738
+ changedFileCount: changedFiles.length,
1739
+ untestedCount: untested.length,
1740
+ coveredRoutes: coveredRoutes.size,
1741
+ };
1742
+ }
1743
+ // === AUTO-MONITORING HANDLERS ===
1744
+ async function getAutoDetectedIssues(args) {
1745
+ const projectId = requireProject();
1746
+ const since = args.since || new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString();
1747
+ const limit = args.limit || 20;
1748
+ let query = supabase
1749
+ .from('reports')
1750
+ .select('id, error_fingerprint, report_source, title, severity, reporter_id, sentry_event_id, created_at, app_context')
1751
+ .eq('project_id', projectId)
1752
+ .neq('report_source', 'manual')
1753
+ .not('error_fingerprint', 'is', null)
1754
+ .gte('created_at', since)
1755
+ .order('created_at', { ascending: false });
1756
+ if (args.source) {
1757
+ query = query.eq('report_source', args.source);
1758
+ }
1759
+ const { data, error } = await query;
1760
+ if (error)
1761
+ return { error: error.message };
1762
+ if (!data || data.length === 0)
1763
+ return { issues: [], total: 0 };
1764
+ // Group by fingerprint
1765
+ const grouped = new Map();
1766
+ for (const report of data) {
1767
+ const fp = report.error_fingerprint;
1768
+ if (!grouped.has(fp))
1769
+ grouped.set(fp, []);
1770
+ grouped.get(fp).push(report);
1771
+ }
1772
+ // Build issue summaries
1773
+ const issues = Array.from(grouped.entries())
1774
+ .map(([fingerprint, reports]) => {
1775
+ const uniqueReporters = new Set(reports.map(r => r.reporter_id));
1776
+ const sorted = reports.sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime());
1777
+ const first = sorted[0];
1778
+ const last = sorted[sorted.length - 1];
1779
+ const route = first.app_context?.currentRoute || 'unknown';
1780
+ return {
1781
+ fingerprint,
1782
+ source: first.report_source,
1783
+ message: first.title,
1784
+ route,
1785
+ occurrence_count: reports.length,
1786
+ affected_users: uniqueReporters.size,
1787
+ first_seen: first.created_at,
1788
+ last_seen: last.created_at,
1789
+ severity: first.severity,
1790
+ has_sentry_link: reports.some(r => r.sentry_event_id != null),
1791
+ sample_report_id: first.id,
1792
+ };
1793
+ })
1794
+ .filter(issue => issue.occurrence_count >= (args.min_occurrences || 1))
1795
+ .sort((a, b) => b.occurrence_count - a.occurrence_count)
1796
+ .slice(0, limit);
1797
+ if (args.compact) {
1798
+ return {
1799
+ issues: issues.map(i => ({
1800
+ fingerprint: i.fingerprint,
1801
+ source: i.source,
1802
+ count: i.occurrence_count,
1803
+ users: i.affected_users,
1804
+ severity: i.severity,
1805
+ })),
1806
+ total: issues.length,
1807
+ };
1808
+ }
1809
+ return { issues, total: issues.length };
1810
+ }
1811
+ async function generateTestsFromErrors(args) {
1812
+ const projectId = requireProject();
1813
+ const limit = args.limit || 5;
1814
+ let reports;
1815
+ if (args.report_ids?.length) {
1816
+ // Validate all UUIDs
1817
+ for (const id of args.report_ids) {
1818
+ if (!isValidUUID(id)) {
1819
+ return { error: `Invalid report_id format: ${id}` };
1820
+ }
1821
+ }
1822
+ const { data, error } = await supabase
1823
+ .from('reports')
1824
+ .select('id, title, report_source, severity, app_context, error_fingerprint, description')
1825
+ .eq('project_id', projectId)
1826
+ .in('id', args.report_ids);
1827
+ if (error)
1828
+ return { error: error.message };
1829
+ reports = data;
1830
+ }
1831
+ else {
1832
+ // Get top uncovered auto-detected errors
1833
+ const { data, error } = await supabase
1834
+ .from('reports')
1835
+ .select('id, title, report_source, severity, app_context, error_fingerprint, description')
1836
+ .eq('project_id', projectId)
1837
+ .neq('report_source', 'manual')
1838
+ .not('error_fingerprint', 'is', null)
1839
+ .order('created_at', { ascending: false })
1840
+ .limit(50);
1841
+ if (error)
1842
+ return { error: error.message };
1843
+ // Deduplicate by fingerprint, keep first occurrence
1844
+ const seen = new Set();
1845
+ reports = (data || []).filter(r => {
1846
+ if (!r.error_fingerprint || seen.has(r.error_fingerprint))
1847
+ return false;
1848
+ seen.add(r.error_fingerprint);
1849
+ return true;
1850
+ }).slice(0, limit);
1851
+ }
1852
+ if (!reports?.length)
1853
+ return { suggestions: [] };
1854
+ const suggestions = reports.map(report => {
1855
+ const route = report.app_context?.currentRoute || '/unknown';
1856
+ const source = report.report_source;
1857
+ const priority = report.severity === 'critical' ? 'P1' : report.severity === 'high' ? 'P1' : 'P2';
1858
+ let suggestedSteps;
1859
+ if (source === 'auto_crash') {
1860
+ suggestedSteps = [
1861
+ `Navigate to ${route}`,
1862
+ 'Reproduce the action that triggered the crash',
1863
+ 'Verify the page does not throw an unhandled error',
1864
+ 'Verify error boundary displays a user-friendly message if error occurs',
1865
+ ];
1866
+ }
1867
+ else if (source === 'auto_api') {
1868
+ const statusCode = report.app_context?.custom?.statusCode || 'error';
1869
+ const method = report.app_context?.custom?.requestMethod || 'API';
1870
+ suggestedSteps = [
1871
+ `Navigate to ${route}`,
1872
+ `Trigger the ${method} request that returned ${statusCode}`,
1873
+ 'Verify the request succeeds or displays an appropriate error message',
1874
+ 'Verify no data corruption occurs on failure',
1875
+ ];
1876
+ }
1877
+ else {
1878
+ // rage_click or sentry_sync
1879
+ const target = report.app_context?.custom?.targetSelector || 'the element';
1880
+ suggestedSteps = [
1881
+ `Navigate to ${route}`,
1882
+ `Click on ${target}`,
1883
+ 'Verify the element responds to interaction',
1884
+ 'Verify loading state is shown if action takes time',
1885
+ ];
1886
+ }
1887
+ return {
1888
+ title: `Test: ${report.title?.replace('[Auto] ', '') || 'Auto-detected issue'}`,
1889
+ track: source === 'auto_crash' ? 'Stability' : source === 'auto_api' ? 'API' : 'UX',
1890
+ priority,
1891
+ rationale: `Auto-detected ${source?.replace('auto_', '')} on ${route}`,
1892
+ suggested_steps: suggestedSteps,
1893
+ source_report_id: report.id,
1894
+ route,
1895
+ };
1896
+ });
1897
+ return { suggestions };
1898
+ }
1899
+ // === Project management handlers ===
1900
+ async function listProjects() {
1901
+ const { data, error } = await supabase
1902
+ .from('projects')
1903
+ .select('id, name, slug, is_qa_enabled, created_at')
1904
+ .order('name');
1905
+ if (error) {
1906
+ return { error: error.message };
1907
+ }
1908
+ return {
1909
+ currentProjectId: currentProjectId || null,
1910
+ projects: data?.map(p => ({
1911
+ id: p.id,
1912
+ name: p.name,
1913
+ slug: p.slug,
1914
+ isQAEnabled: p.is_qa_enabled,
1915
+ isActive: p.id === currentProjectId,
1916
+ createdAt: p.created_at,
1917
+ })) || [],
1918
+ };
1919
+ }
1920
+ async function switchProject(args) {
1921
+ if (!isValidUUID(args.project_id)) {
1922
+ return { error: 'Invalid project_id format — must be a UUID' };
1923
+ }
1924
+ // Verify the project exists and is accessible
1925
+ const { data, error } = await supabase
1926
+ .from('projects')
1927
+ .select('id, name, slug')
1928
+ .eq('id', args.project_id)
1929
+ .single();
1930
+ if (error || !data) {
1931
+ return { error: 'Project not found or not accessible' };
1932
+ }
1933
+ currentProjectId = data.id;
1934
+ return {
1935
+ success: true,
1936
+ message: `Switched to project "${data.name}" (${data.slug})`,
1937
+ projectId: data.id,
1938
+ projectName: data.name,
1939
+ };
1940
+ }
1941
+ function getCurrentProject() {
1942
+ if (!currentProjectId) {
1943
+ return { message: 'No project selected. Use list_projects to see available projects, then switch_project to select one.' };
1944
+ }
1945
+ return { projectId: currentProjectId };
1946
+ }
1029
1947
  // Tool handlers
1030
1948
  async function listReports(args) {
1031
1949
  let query = supabase
1032
1950
  .from('reports')
1033
1951
  .select('id, report_type, severity, status, description, app_context, created_at, reporter_name, tester:testers(name)')
1034
- .eq('project_id', PROJECT_ID)
1952
+ .eq('project_id', currentProjectId)
1035
1953
  .order('created_at', { ascending: false })
1036
1954
  .limit(Math.min(args.limit || 10, 50));
1037
1955
  if (args.status)
@@ -1067,7 +1985,7 @@ async function getReport(args) {
1067
1985
  .from('reports')
1068
1986
  .select('*, tester:testers(*), track:qa_tracks(*)')
1069
1987
  .eq('id', args.report_id)
1070
- .eq('project_id', PROJECT_ID) // Security: ensure report belongs to this project
1988
+ .eq('project_id', currentProjectId) // Security: ensure report belongs to this project
1071
1989
  .single();
1072
1990
  if (error) {
1073
1991
  return { error: error.message };
@@ -1082,7 +2000,7 @@ async function getReport(args) {
1082
2000
  app_context: data.app_context,
1083
2001
  device_info: data.device_info,
1084
2002
  navigation_history: data.navigation_history,
1085
- screenshots: data.screenshots,
2003
+ screenshot_urls: data.screenshot_urls,
1086
2004
  created_at: data.created_at,
1087
2005
  reporter: data.tester ? {
1088
2006
  name: data.tester.name,
@@ -1102,7 +2020,7 @@ async function searchReports(args) {
1102
2020
  let query = supabase
1103
2021
  .from('reports')
1104
2022
  .select('id, report_type, severity, status, description, app_context, created_at')
1105
- .eq('project_id', PROJECT_ID)
2023
+ .eq('project_id', currentProjectId)
1106
2024
  .order('created_at', { ascending: false })
1107
2025
  .limit(20);
1108
2026
  if (sanitizedQuery) {
@@ -1145,7 +2063,7 @@ async function updateReportStatus(args) {
1145
2063
  .from('reports')
1146
2064
  .update(updates)
1147
2065
  .eq('id', args.report_id)
1148
- .eq('project_id', PROJECT_ID); // Security: ensure report belongs to this project
2066
+ .eq('project_id', currentProjectId); // Security: ensure report belongs to this project
1149
2067
  if (error) {
1150
2068
  return { error: error.message };
1151
2069
  }
@@ -1157,28 +2075,92 @@ async function getReportContext(args) {
1157
2075
  }
1158
2076
  const { data, error } = await supabase
1159
2077
  .from('reports')
1160
- .select('app_context, device_info, navigation_history, enhanced_context')
2078
+ .select('app_context, device_info, navigation_history, enhanced_context, screenshot_urls')
1161
2079
  .eq('id', args.report_id)
1162
- .eq('project_id', PROJECT_ID) // Security: ensure report belongs to this project
2080
+ .eq('project_id', currentProjectId) // Security: ensure report belongs to this project
1163
2081
  .single();
1164
2082
  if (error) {
1165
2083
  return { error: error.message };
1166
2084
  }
2085
+ // Compact: return app_context only (skip console/network/navigation)
2086
+ if (args.compact === true) {
2087
+ return {
2088
+ context: {
2089
+ app_context: data.app_context,
2090
+ screenshot_urls: data.screenshot_urls,
2091
+ },
2092
+ };
2093
+ }
1167
2094
  return {
1168
2095
  context: {
1169
2096
  app_context: data.app_context,
1170
2097
  device_info: data.device_info,
1171
2098
  navigation_history: data.navigation_history,
1172
2099
  enhanced_context: data.enhanced_context || {},
2100
+ screenshot_urls: data.screenshot_urls,
1173
2101
  },
1174
2102
  };
1175
2103
  }
2104
+ async function addReportComment(args) {
2105
+ if (!isValidUUID(args.report_id))
2106
+ return { error: 'Invalid report_id format' };
2107
+ if (!args.message?.trim())
2108
+ return { error: 'Message is required' };
2109
+ // Verify report exists
2110
+ const { data: report } = await supabase
2111
+ .from('reports').select('id').eq('id', args.report_id).eq('project_id', currentProjectId).single();
2112
+ if (!report)
2113
+ return { error: 'Report not found' };
2114
+ // Find or create a discussion thread for this report
2115
+ const { data: existingThread } = await supabase
2116
+ .from('discussion_threads').select('id')
2117
+ .eq('project_id', currentProjectId).eq('report_id', args.report_id).eq('thread_type', 'report')
2118
+ .limit(1).single();
2119
+ let threadId;
2120
+ if (existingThread) {
2121
+ threadId = existingThread.id;
2122
+ }
2123
+ else {
2124
+ const newId = crypto.randomUUID();
2125
+ const { error: threadErr } = await supabase
2126
+ .from('discussion_threads').insert({
2127
+ id: newId, project_id: currentProjectId, report_id: args.report_id,
2128
+ thread_type: 'report', subject: 'Bug Report Discussion', audience: 'all',
2129
+ priority: 'normal', created_by_admin: true, last_message_at: new Date().toISOString(),
2130
+ });
2131
+ if (threadErr)
2132
+ return { error: `Failed to create thread: ${threadErr.message}` };
2133
+ threadId = newId;
2134
+ }
2135
+ const { data: msg, error: msgErr } = await supabase
2136
+ .from('discussion_messages').insert({
2137
+ thread_id: threadId, sender_type: 'admin', sender_name: args.author || 'Claude Code', content: args.message.trim(), content_type: 'text',
2138
+ }).select('id, content, created_at').single();
2139
+ if (msgErr)
2140
+ return { error: `Failed to add comment: ${msgErr.message}` };
2141
+ 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' };
2142
+ }
2143
+ async function getReportComments(args) {
2144
+ if (!isValidUUID(args.report_id))
2145
+ return { error: 'Invalid report_id format' };
2146
+ const { data: threads } = await supabase
2147
+ .from('discussion_threads').select('id')
2148
+ .eq('project_id', currentProjectId).eq('report_id', args.report_id).order('created_at', { ascending: true });
2149
+ if (!threads || threads.length === 0)
2150
+ return { comments: [], total: 0, message: 'No comments on this report' };
2151
+ const { data: messages, error } = await supabase
2152
+ .from('discussion_messages').select('id, thread_id, sender_type, content, content_type, created_at, attachments')
2153
+ .in('thread_id', threads.map(t => t.id)).order('created_at', { ascending: true });
2154
+ if (error)
2155
+ return { error: error.message };
2156
+ 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 };
2157
+ }
1176
2158
  async function getProjectInfo() {
1177
2159
  // Get project details
1178
2160
  const { data: project, error: projectError } = await supabase
1179
2161
  .from('projects')
1180
2162
  .select('id, name, slug, is_qa_enabled')
1181
- .eq('id', PROJECT_ID)
2163
+ .eq('id', currentProjectId)
1182
2164
  .single();
1183
2165
  if (projectError) {
1184
2166
  return { error: projectError.message };
@@ -1187,17 +2169,17 @@ async function getProjectInfo() {
1187
2169
  const { data: tracks } = await supabase
1188
2170
  .from('qa_tracks')
1189
2171
  .select('id, name, icon, test_template')
1190
- .eq('project_id', PROJECT_ID);
2172
+ .eq('project_id', currentProjectId);
1191
2173
  // Get test case count
1192
2174
  const { count: testCaseCount } = await supabase
1193
2175
  .from('test_cases')
1194
2176
  .select('id', { count: 'exact', head: true })
1195
- .eq('project_id', PROJECT_ID);
2177
+ .eq('project_id', currentProjectId);
1196
2178
  // Get open bug count
1197
2179
  const { count: openBugCount } = await supabase
1198
2180
  .from('reports')
1199
2181
  .select('id', { count: 'exact', head: true })
1200
- .eq('project_id', PROJECT_ID)
2182
+ .eq('project_id', currentProjectId)
1201
2183
  .eq('report_type', 'bug')
1202
2184
  .in('status', ['new', 'confirmed', 'in_progress']);
1203
2185
  return {
@@ -1224,7 +2206,7 @@ async function getQaTracks() {
1224
2206
  const { data, error } = await supabase
1225
2207
  .from('qa_tracks')
1226
2208
  .select('*')
1227
- .eq('project_id', PROJECT_ID)
2209
+ .eq('project_id', currentProjectId)
1228
2210
  .order('sort_order');
1229
2211
  if (error) {
1230
2212
  return { error: error.message };
@@ -1252,14 +2234,14 @@ async function createTestCase(args) {
1252
2234
  const { data: trackData } = await supabase
1253
2235
  .from('qa_tracks')
1254
2236
  .select('id')
1255
- .eq('project_id', PROJECT_ID)
2237
+ .eq('project_id', currentProjectId)
1256
2238
  .ilike('name', `%${sanitizedTrack}%`)
1257
2239
  .single();
1258
2240
  trackId = trackData?.id || null;
1259
2241
  }
1260
2242
  }
1261
2243
  const testCase = {
1262
- project_id: PROJECT_ID,
2244
+ project_id: currentProjectId,
1263
2245
  test_key: args.test_key,
1264
2246
  title: args.title,
1265
2247
  description: args.description || '',
@@ -1299,7 +2281,7 @@ async function updateTestCase(args) {
1299
2281
  const { data: existing } = await supabase
1300
2282
  .from('test_cases')
1301
2283
  .select('id')
1302
- .eq('project_id', PROJECT_ID)
2284
+ .eq('project_id', currentProjectId)
1303
2285
  .eq('test_key', args.test_key)
1304
2286
  .single();
1305
2287
  if (!existing) {
@@ -1330,7 +2312,7 @@ async function updateTestCase(args) {
1330
2312
  .from('test_cases')
1331
2313
  .update(updates)
1332
2314
  .eq('id', testCaseId)
1333
- .eq('project_id', PROJECT_ID)
2315
+ .eq('project_id', currentProjectId)
1334
2316
  .select('id, test_key, title, target_route')
1335
2317
  .single();
1336
2318
  if (error) {
@@ -1375,7 +2357,7 @@ async function deleteTestCases(args) {
1375
2357
  const { data: existing } = await supabase
1376
2358
  .from('test_cases')
1377
2359
  .select('id')
1378
- .eq('project_id', PROJECT_ID)
2360
+ .eq('project_id', currentProjectId)
1379
2361
  .eq('test_key', args.test_key)
1380
2362
  .single();
1381
2363
  if (!existing) {
@@ -1408,7 +2390,7 @@ async function deleteTestCases(args) {
1408
2390
  const { data: existing, error: lookupError } = await supabase
1409
2391
  .from('test_cases')
1410
2392
  .select('id, test_key')
1411
- .eq('project_id', PROJECT_ID)
2393
+ .eq('project_id', currentProjectId)
1412
2394
  .in('test_key', args.test_keys);
1413
2395
  if (lookupError) {
1414
2396
  return { error: lookupError.message };
@@ -1424,7 +2406,7 @@ async function deleteTestCases(args) {
1424
2406
  const { data: toDelete } = await supabase
1425
2407
  .from('test_cases')
1426
2408
  .select('id, test_key, title')
1427
- .eq('project_id', PROJECT_ID)
2409
+ .eq('project_id', currentProjectId)
1428
2410
  .in('id', idsToDelete);
1429
2411
  if (!toDelete || toDelete.length === 0) {
1430
2412
  return { error: 'No matching test cases found in this project' };
@@ -1433,7 +2415,7 @@ async function deleteTestCases(args) {
1433
2415
  const { error: deleteError } = await supabase
1434
2416
  .from('test_cases')
1435
2417
  .delete()
1436
- .eq('project_id', PROJECT_ID)
2418
+ .eq('project_id', currentProjectId)
1437
2419
  .in('id', idsToDelete);
1438
2420
  if (deleteError) {
1439
2421
  return { error: deleteError.message };
@@ -1467,7 +2449,7 @@ async function listTestCases(args) {
1467
2449
  steps,
1468
2450
  track:qa_tracks(id, name, icon, color)
1469
2451
  `)
1470
- .eq('project_id', PROJECT_ID)
2452
+ .eq('project_id', currentProjectId)
1471
2453
  .order('test_key', { ascending: true });
1472
2454
  // Apply filters
1473
2455
  if (args.priority) {
@@ -1489,6 +2471,19 @@ async function listTestCases(args) {
1489
2471
  if (args.track) {
1490
2472
  testCases = testCases.filter((tc) => tc.track?.name?.toLowerCase().includes(args.track.toLowerCase()));
1491
2473
  }
2474
+ // Compact: return minimal fields only
2475
+ if (args.compact === true) {
2476
+ return {
2477
+ count: testCases.length,
2478
+ testCases: testCases.map((tc) => ({
2479
+ id: tc.id,
2480
+ testKey: tc.test_key,
2481
+ title: tc.title,
2482
+ priority: tc.priority,
2483
+ })),
2484
+ pagination: { limit, offset, hasMore: testCases.length === limit },
2485
+ };
2486
+ }
1492
2487
  return {
1493
2488
  count: testCases.length,
1494
2489
  testCases: testCases.map((tc) => ({
@@ -1514,7 +2509,7 @@ async function getBugPatterns(args) {
1514
2509
  let query = supabase
1515
2510
  .from('reports')
1516
2511
  .select('app_context, severity, status, created_at')
1517
- .eq('project_id', PROJECT_ID)
2512
+ .eq('project_id', currentProjectId)
1518
2513
  .eq('report_type', 'bug')
1519
2514
  .order('created_at', { ascending: false })
1520
2515
  .limit(100);
@@ -1566,7 +2561,7 @@ async function suggestTestCases(args) {
1566
2561
  const { data: existingTests } = await supabase
1567
2562
  .from('test_cases')
1568
2563
  .select('test_key, title')
1569
- .eq('project_id', PROJECT_ID)
2564
+ .eq('project_id', currentProjectId)
1570
2565
  .order('test_key', { ascending: false })
1571
2566
  .limit(1);
1572
2567
  // Calculate next test key number
@@ -1601,7 +2596,7 @@ async function suggestTestCases(args) {
1601
2596
  const { data: relatedBugs } = await supabase
1602
2597
  .from('reports')
1603
2598
  .select('id, description, severity')
1604
- .eq('project_id', PROJECT_ID)
2599
+ .eq('project_id', currentProjectId)
1605
2600
  .eq('report_type', 'bug')
1606
2601
  .limit(10);
1607
2602
  const routeBugs = (relatedBugs || []).filter(bug => {
@@ -1635,7 +2630,7 @@ async function getTestPriorities(args) {
1635
2630
  const minScore = args.min_score || 0;
1636
2631
  const includeFactors = args.include_factors !== false;
1637
2632
  // First, refresh the route stats
1638
- const { error: refreshError } = await supabase.rpc('refresh_route_test_stats', { p_project_id: PROJECT_ID });
2633
+ const { error: refreshError } = await supabase.rpc('refresh_route_test_stats', { p_project_id: currentProjectId });
1639
2634
  if (refreshError) {
1640
2635
  // Non-fatal: proceed with potentially stale data but warn
1641
2636
  console.warn('Failed to refresh route stats:', refreshError.message);
@@ -1644,7 +2639,7 @@ async function getTestPriorities(args) {
1644
2639
  const { data: routes, error } = await supabase
1645
2640
  .from('route_test_stats')
1646
2641
  .select('*')
1647
- .eq('project_id', PROJECT_ID)
2642
+ .eq('project_id', currentProjectId)
1648
2643
  .gte('priority_score', minScore)
1649
2644
  .order('priority_score', { ascending: false })
1650
2645
  .limit(limit);
@@ -1761,7 +2756,7 @@ async function getCoverageGaps(args) {
1761
2756
  const { data: routesFromReports } = await supabase
1762
2757
  .from('reports')
1763
2758
  .select('app_context')
1764
- .eq('project_id', PROJECT_ID)
2759
+ .eq('project_id', currentProjectId)
1765
2760
  .not('app_context->currentRoute', 'is', null);
1766
2761
  const allRoutes = new Set();
1767
2762
  (routesFromReports || []).forEach(r => {
@@ -1773,7 +2768,7 @@ async function getCoverageGaps(args) {
1773
2768
  const { data: testCases } = await supabase
1774
2769
  .from('test_cases')
1775
2770
  .select('target_route, category, track_id')
1776
- .eq('project_id', PROJECT_ID);
2771
+ .eq('project_id', currentProjectId);
1777
2772
  const coveredRoutes = new Set();
1778
2773
  const routeTrackCoverage = {};
1779
2774
  (testCases || []).forEach(tc => {
@@ -1790,13 +2785,13 @@ async function getCoverageGaps(args) {
1790
2785
  const { data: tracks } = await supabase
1791
2786
  .from('qa_tracks')
1792
2787
  .select('id, name')
1793
- .eq('project_id', PROJECT_ID);
2788
+ .eq('project_id', currentProjectId);
1794
2789
  const trackMap = new Map((tracks || []).map(t => [t.id, t.name]));
1795
2790
  // Get route stats for staleness
1796
2791
  const { data: routeStats } = await supabase
1797
2792
  .from('route_test_stats')
1798
2793
  .select('route, last_tested_at, open_bugs, critical_bugs')
1799
- .eq('project_id', PROJECT_ID);
2794
+ .eq('project_id', currentProjectId);
1800
2795
  const routeStatsMap = new Map((routeStats || []).map(r => [r.route, r]));
1801
2796
  // Find untested routes
1802
2797
  if (gapType === 'all' || gapType === 'untested_routes') {
@@ -1893,14 +2888,14 @@ async function getRegressions(args) {
1893
2888
  const { data: resolvedBugs } = await supabase
1894
2889
  .from('reports')
1895
2890
  .select('id, description, severity, app_context, resolved_at')
1896
- .eq('project_id', PROJECT_ID)
2891
+ .eq('project_id', currentProjectId)
1897
2892
  .eq('report_type', 'bug')
1898
2893
  .in('status', ['resolved', 'fixed', 'verified', 'closed'])
1899
2894
  .gte('resolved_at', new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString());
1900
2895
  const { data: newBugs } = await supabase
1901
2896
  .from('reports')
1902
2897
  .select('id, description, severity, app_context, created_at')
1903
- .eq('project_id', PROJECT_ID)
2898
+ .eq('project_id', currentProjectId)
1904
2899
  .eq('report_type', 'bug')
1905
2900
  .in('status', ['new', 'triaging', 'confirmed', 'in_progress', 'reviewed'])
1906
2901
  .gte('created_at', new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString());
@@ -1998,26 +2993,26 @@ async function getRegressions(args) {
1998
2993
  };
1999
2994
  }
2000
2995
  async function getCoverageMatrix(args) {
2001
- const includeExecution = args.include_execution_data !== false;
2002
- const includeBugs = args.include_bug_counts !== false;
2996
+ const includeExecution = args.include_execution_data === true;
2997
+ const includeBugs = args.include_bug_counts === true;
2003
2998
  // Get tracks
2004
2999
  const { data: tracks } = await supabase
2005
3000
  .from('qa_tracks')
2006
3001
  .select('id, name, icon, color')
2007
- .eq('project_id', PROJECT_ID)
3002
+ .eq('project_id', currentProjectId)
2008
3003
  .order('sort_order');
2009
3004
  // Get test cases with track info
2010
3005
  const { data: testCases } = await supabase
2011
3006
  .from('test_cases')
2012
3007
  .select('id, target_route, category, track_id')
2013
- .eq('project_id', PROJECT_ID);
3008
+ .eq('project_id', currentProjectId);
2014
3009
  // Get test assignments for execution data
2015
3010
  let assignments = [];
2016
3011
  if (includeExecution) {
2017
3012
  const { data } = await supabase
2018
3013
  .from('test_assignments')
2019
3014
  .select('test_case_id, status, completed_at')
2020
- .eq('project_id', PROJECT_ID)
3015
+ .eq('project_id', currentProjectId)
2021
3016
  .in('status', ['passed', 'failed'])
2022
3017
  .order('completed_at', { ascending: false })
2023
3018
  .limit(2000);
@@ -2029,7 +3024,7 @@ async function getCoverageMatrix(args) {
2029
3024
  const { data } = await supabase
2030
3025
  .from('route_test_stats')
2031
3026
  .select('route, open_bugs, critical_bugs')
2032
- .eq('project_id', PROJECT_ID);
3027
+ .eq('project_id', currentProjectId);
2033
3028
  routeStats = data || [];
2034
3029
  }
2035
3030
  const routeStatsMap = new Map(routeStats.map(r => [r.route, r]));
@@ -2168,7 +3163,7 @@ async function getStaleCoverage(args) {
2168
3163
  const daysThreshold = args.days_threshold || 14;
2169
3164
  const limit = args.limit || 20;
2170
3165
  // Refresh stats first
2171
- const { error: refreshError } = await supabase.rpc('refresh_route_test_stats', { p_project_id: PROJECT_ID });
3166
+ const { error: refreshError } = await supabase.rpc('refresh_route_test_stats', { p_project_id: currentProjectId });
2172
3167
  if (refreshError) {
2173
3168
  // Non-fatal: proceed with potentially stale data but warn
2174
3169
  console.warn('Failed to refresh route stats:', refreshError.message);
@@ -2177,7 +3172,7 @@ async function getStaleCoverage(args) {
2177
3172
  const { data: routes, error } = await supabase
2178
3173
  .from('route_test_stats')
2179
3174
  .select('route, last_tested_at, open_bugs, critical_bugs, test_case_count, priority_score')
2180
- .eq('project_id', PROJECT_ID)
3175
+ .eq('project_id', currentProjectId)
2181
3176
  .order('last_tested_at', { ascending: true, nullsFirst: true })
2182
3177
  .limit(limit * 2); // Get extra to filter
2183
3178
  if (error) {
@@ -2264,12 +3259,12 @@ async function generateDeployChecklist(args) {
2264
3259
  supabase
2265
3260
  .from('test_cases')
2266
3261
  .select('id, test_key, title, target_route, category, priority, track:qa_tracks(name)')
2267
- .eq('project_id', PROJECT_ID)
3262
+ .eq('project_id', currentProjectId)
2268
3263
  .in('target_route', safeRoutes),
2269
3264
  supabase
2270
3265
  .from('test_cases')
2271
3266
  .select('id, test_key, title, target_route, category, priority, track:qa_tracks(name)')
2272
- .eq('project_id', PROJECT_ID)
3267
+ .eq('project_id', currentProjectId)
2273
3268
  .in('category', safeRoutes),
2274
3269
  ]);
2275
3270
  // Deduplicate by id
@@ -2284,7 +3279,7 @@ async function generateDeployChecklist(args) {
2284
3279
  const { data: routeStats } = await supabase
2285
3280
  .from('route_test_stats')
2286
3281
  .select('*')
2287
- .eq('project_id', PROJECT_ID)
3282
+ .eq('project_id', currentProjectId)
2288
3283
  .in('route', Array.from(allRoutes));
2289
3284
  const routeStatsMap = new Map((routeStats || []).map(r => [r.route, r]));
2290
3285
  // Categorize tests
@@ -2383,30 +3378,30 @@ async function getQAHealth(args) {
2383
3378
  const { data: currentTests } = await supabase
2384
3379
  .from('test_assignments')
2385
3380
  .select('id, status, completed_at')
2386
- .eq('project_id', PROJECT_ID)
3381
+ .eq('project_id', currentProjectId)
2387
3382
  .gte('completed_at', periodStart.toISOString())
2388
3383
  .in('status', ['passed', 'failed']);
2389
3384
  const { data: currentBugs } = await supabase
2390
3385
  .from('reports')
2391
3386
  .select('id, severity, status, created_at')
2392
- .eq('project_id', PROJECT_ID)
3387
+ .eq('project_id', currentProjectId)
2393
3388
  .eq('report_type', 'bug')
2394
3389
  .gte('created_at', periodStart.toISOString());
2395
3390
  const { data: resolvedBugs } = await supabase
2396
3391
  .from('reports')
2397
3392
  .select('id, created_at, resolved_at')
2398
- .eq('project_id', PROJECT_ID)
3393
+ .eq('project_id', currentProjectId)
2399
3394
  .eq('report_type', 'bug')
2400
3395
  .in('status', ['resolved', 'fixed', 'verified', 'closed'])
2401
3396
  .gte('resolved_at', periodStart.toISOString());
2402
3397
  const { data: testers } = await supabase
2403
3398
  .from('testers')
2404
3399
  .select('id, status')
2405
- .eq('project_id', PROJECT_ID);
3400
+ .eq('project_id', currentProjectId);
2406
3401
  const { data: routeStats } = await supabase
2407
3402
  .from('route_test_stats')
2408
3403
  .select('route, test_case_count')
2409
- .eq('project_id', PROJECT_ID);
3404
+ .eq('project_id', currentProjectId);
2410
3405
  // Get previous period data for comparison
2411
3406
  let previousTests = [];
2412
3407
  let previousBugs = [];
@@ -2415,7 +3410,7 @@ async function getQAHealth(args) {
2415
3410
  const { data: pt } = await supabase
2416
3411
  .from('test_assignments')
2417
3412
  .select('id, status')
2418
- .eq('project_id', PROJECT_ID)
3413
+ .eq('project_id', currentProjectId)
2419
3414
  .gte('completed_at', previousStart.toISOString())
2420
3415
  .lt('completed_at', periodStart.toISOString())
2421
3416
  .in('status', ['passed', 'failed']);
@@ -2423,7 +3418,7 @@ async function getQAHealth(args) {
2423
3418
  const { data: pb } = await supabase
2424
3419
  .from('reports')
2425
3420
  .select('id, severity')
2426
- .eq('project_id', PROJECT_ID)
3421
+ .eq('project_id', currentProjectId)
2427
3422
  .eq('report_type', 'bug')
2428
3423
  .gte('created_at', previousStart.toISOString())
2429
3424
  .lt('created_at', periodStart.toISOString());
@@ -2431,7 +3426,7 @@ async function getQAHealth(args) {
2431
3426
  const { data: pr } = await supabase
2432
3427
  .from('reports')
2433
3428
  .select('id')
2434
- .eq('project_id', PROJECT_ID)
3429
+ .eq('project_id', currentProjectId)
2435
3430
  .in('status', ['resolved', 'fixed', 'verified', 'closed'])
2436
3431
  .gte('resolved_at', previousStart.toISOString())
2437
3432
  .lt('resolved_at', periodStart.toISOString());
@@ -2585,7 +3580,7 @@ async function getQASessions(args) {
2585
3580
  findings_count, bugs_filed, created_at,
2586
3581
  tester:testers(id, name, email)
2587
3582
  `)
2588
- .eq('project_id', PROJECT_ID)
3583
+ .eq('project_id', currentProjectId)
2589
3584
  .order('started_at', { ascending: false })
2590
3585
  .limit(limit);
2591
3586
  if (status !== 'all') {
@@ -2635,12 +3630,12 @@ async function getQAAlerts(args) {
2635
3630
  const status = args.status || 'active';
2636
3631
  // Optionally refresh alerts
2637
3632
  if (args.refresh) {
2638
- await supabase.rpc('detect_all_alerts', { p_project_id: PROJECT_ID });
3633
+ await supabase.rpc('detect_all_alerts', { p_project_id: currentProjectId });
2639
3634
  }
2640
3635
  let query = supabase
2641
3636
  .from('qa_alerts')
2642
3637
  .select('*')
2643
- .eq('project_id', PROJECT_ID)
3638
+ .eq('project_id', currentProjectId)
2644
3639
  .order('severity', { ascending: true }) // critical first
2645
3640
  .order('created_at', { ascending: false });
2646
3641
  if (severity !== 'all') {
@@ -2693,7 +3688,7 @@ async function getDeploymentAnalysis(args) {
2693
3688
  .from('deployments')
2694
3689
  .select('*')
2695
3690
  .eq('id', args.deployment_id)
2696
- .eq('project_id', PROJECT_ID)
3691
+ .eq('project_id', currentProjectId)
2697
3692
  .single();
2698
3693
  if (error) {
2699
3694
  return { error: error.message };
@@ -2704,7 +3699,7 @@ async function getDeploymentAnalysis(args) {
2704
3699
  let query = supabase
2705
3700
  .from('deployments')
2706
3701
  .select('*')
2707
- .eq('project_id', PROJECT_ID)
3702
+ .eq('project_id', currentProjectId)
2708
3703
  .order('deployed_at', { ascending: false })
2709
3704
  .limit(limit);
2710
3705
  if (args.environment && args.environment !== 'all') {
@@ -2778,7 +3773,7 @@ async function analyzeCommitForTesting(args) {
2778
3773
  const { data: mappings } = await supabase
2779
3774
  .from('file_route_mapping')
2780
3775
  .select('file_pattern, route, feature, confidence')
2781
- .eq('project_id', PROJECT_ID);
3776
+ .eq('project_id', currentProjectId);
2782
3777
  const affectedRoutes = [];
2783
3778
  for (const mapping of mappings || []) {
2784
3779
  const matchedFiles = filesChanged.filter(file => {
@@ -2812,7 +3807,7 @@ async function analyzeCommitForTesting(args) {
2812
3807
  const { data: bugs } = await supabase
2813
3808
  .from('reports')
2814
3809
  .select('id, severity, description, route, created_at')
2815
- .eq('project_id', PROJECT_ID)
3810
+ .eq('project_id', currentProjectId)
2816
3811
  .eq('report_type', 'bug')
2817
3812
  .in('route', routes)
2818
3813
  .gte('created_at', new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString());
@@ -2844,7 +3839,7 @@ async function analyzeCommitForTesting(args) {
2844
3839
  // Optionally record as deployment
2845
3840
  if (args.record_deployment) {
2846
3841
  await supabase.rpc('record_deployment', {
2847
- p_project_id: PROJECT_ID,
3842
+ p_project_id: currentProjectId,
2848
3843
  p_environment: 'production',
2849
3844
  p_commit_sha: args.commit_sha || null,
2850
3845
  p_commit_message: args.commit_message || null,
@@ -2933,12 +3928,12 @@ async function analyzeChangesForTests(args) {
2933
3928
  const { data: existingTests } = await supabase
2934
3929
  .from('test_cases')
2935
3930
  .select('test_key, title, target_route, description')
2936
- .eq('project_id', PROJECT_ID);
3931
+ .eq('project_id', currentProjectId);
2937
3932
  // Get next test key
2938
3933
  const { data: lastTest } = await supabase
2939
3934
  .from('test_cases')
2940
3935
  .select('test_key')
2941
- .eq('project_id', PROJECT_ID)
3936
+ .eq('project_id', currentProjectId)
2942
3937
  .order('test_key', { ascending: false })
2943
3938
  .limit(1);
2944
3939
  const lastKey = lastTest?.[0]?.test_key || 'TC-000';
@@ -2950,7 +3945,7 @@ async function analyzeChangesForTests(args) {
2950
3945
  const { data: bugs } = await supabase
2951
3946
  .from('reports')
2952
3947
  .select('id, description, severity, app_context')
2953
- .eq('project_id', PROJECT_ID)
3948
+ .eq('project_id', currentProjectId)
2954
3949
  .eq('report_type', 'bug')
2955
3950
  .limit(50);
2956
3951
  relatedBugs = (bugs || []).filter(bug => {
@@ -3295,7 +4290,7 @@ async function createBugReport(args) {
3295
4290
  const { data: project } = await supabase
3296
4291
  .from('projects')
3297
4292
  .select('owner_id')
3298
- .eq('id', PROJECT_ID)
4293
+ .eq('id', currentProjectId)
3299
4294
  .single();
3300
4295
  if (project?.owner_id) {
3301
4296
  reporterId = project.owner_id;
@@ -3305,14 +4300,14 @@ async function createBugReport(args) {
3305
4300
  const { data: testers } = await supabase
3306
4301
  .from('testers')
3307
4302
  .select('id')
3308
- .eq('project_id', PROJECT_ID)
4303
+ .eq('project_id', currentProjectId)
3309
4304
  .limit(1);
3310
4305
  if (testers && testers.length > 0) {
3311
4306
  reporterId = testers[0].id;
3312
4307
  }
3313
4308
  }
3314
4309
  const report = {
3315
- project_id: PROJECT_ID,
4310
+ project_id: currentProjectId,
3316
4311
  report_type: 'bug',
3317
4312
  title: args.title,
3318
4313
  description: args.description,
@@ -3376,7 +4371,7 @@ async function getBugsForFile(args) {
3376
4371
  let query = supabase
3377
4372
  .from('reports')
3378
4373
  .select('id, title, description, severity, status, created_at, code_context')
3379
- .eq('project_id', PROJECT_ID)
4374
+ .eq('project_id', currentProjectId)
3380
4375
  .eq('report_type', 'bug');
3381
4376
  if (!args.include_resolved) {
3382
4377
  query = query.in('status', ['new', 'confirmed', 'in_progress', 'reviewed']);
@@ -3442,7 +4437,7 @@ async function markFixedWithCommit(args) {
3442
4437
  .from('reports')
3443
4438
  .select('code_context')
3444
4439
  .eq('id', args.report_id)
3445
- .eq('project_id', PROJECT_ID) // Security: ensure report belongs to this project
4440
+ .eq('project_id', currentProjectId) // Security: ensure report belongs to this project
3446
4441
  .single();
3447
4442
  if (fetchError) {
3448
4443
  return { error: fetchError.message };
@@ -3452,7 +4447,7 @@ async function markFixedWithCommit(args) {
3452
4447
  status: 'resolved',
3453
4448
  resolved_at: new Date().toISOString(),
3454
4449
  resolution_notes: args.resolution_notes || `Fixed in commit ${args.commit_sha.slice(0, 7)}`,
3455
- notify_tester: args.notify_tester === true, // Opt-in: only notify if explicitly requested
4450
+ notify_tester: args.notify_tester !== false, // Default: notify tester. Pass false to silently resolve.
3456
4451
  code_context: {
3457
4452
  ...existingContext,
3458
4453
  fix: {
@@ -3468,11 +4463,12 @@ async function markFixedWithCommit(args) {
3468
4463
  .from('reports')
3469
4464
  .update(updates)
3470
4465
  .eq('id', args.report_id)
3471
- .eq('project_id', PROJECT_ID); // Security: ensure report belongs to this project
4466
+ .eq('project_id', currentProjectId); // Security: ensure report belongs to this project
3472
4467
  if (error) {
3473
4468
  return { error: error.message };
3474
4469
  }
3475
- const notificationStatus = args.notify_tester
4470
+ const notifyTester = args.notify_tester !== false;
4471
+ const notificationStatus = notifyTester
3476
4472
  ? 'The original tester will be notified and assigned a verification task.'
3477
4473
  : 'No notification sent (silent resolve). A verification task was created.';
3478
4474
  return {
@@ -3480,7 +4476,7 @@ async function markFixedWithCommit(args) {
3480
4476
  message: `Bug marked as fixed in commit ${args.commit_sha.slice(0, 7)}. ${notificationStatus}`,
3481
4477
  report_id: args.report_id,
3482
4478
  commit: args.commit_sha,
3483
- tester_notified: args.notify_tester === true,
4479
+ tester_notified: notifyTester,
3484
4480
  next_steps: [
3485
4481
  'Consider running create_regression_test to prevent this bug from recurring',
3486
4482
  'Push your changes to trigger CI/CD',
@@ -3492,7 +4488,7 @@ async function getBugsAffectingCode(args) {
3492
4488
  const { data, error } = await supabase
3493
4489
  .from('reports')
3494
4490
  .select('id, title, description, severity, status, code_context, app_context')
3495
- .eq('project_id', PROJECT_ID)
4491
+ .eq('project_id', currentProjectId)
3496
4492
  .eq('report_type', 'bug')
3497
4493
  .in('status', ['new', 'confirmed', 'in_progress', 'reviewed'])
3498
4494
  .order('severity', { ascending: true });
@@ -3597,7 +4593,7 @@ async function linkBugToCode(args) {
3597
4593
  .from('reports')
3598
4594
  .select('code_context')
3599
4595
  .eq('id', args.report_id)
3600
- .eq('project_id', PROJECT_ID) // Security: ensure report belongs to this project
4596
+ .eq('project_id', currentProjectId) // Security: ensure report belongs to this project
3601
4597
  .single();
3602
4598
  if (fetchError) {
3603
4599
  return { error: fetchError.message };
@@ -3618,7 +4614,7 @@ async function linkBugToCode(args) {
3618
4614
  .from('reports')
3619
4615
  .update(updates)
3620
4616
  .eq('id', args.report_id)
3621
- .eq('project_id', PROJECT_ID); // Security: ensure report belongs to this project
4617
+ .eq('project_id', currentProjectId); // Security: ensure report belongs to this project
3622
4618
  if (error) {
3623
4619
  return { error: error.message };
3624
4620
  }
@@ -3637,7 +4633,7 @@ async function createRegressionTest(args) {
3637
4633
  .from('reports')
3638
4634
  .select('*')
3639
4635
  .eq('id', args.report_id)
3640
- .eq('project_id', PROJECT_ID) // Security: ensure report belongs to this project
4636
+ .eq('project_id', currentProjectId) // Security: ensure report belongs to this project
3641
4637
  .single();
3642
4638
  if (fetchError) {
3643
4639
  return { error: fetchError.message };
@@ -3654,7 +4650,7 @@ async function createRegressionTest(args) {
3654
4650
  const { data: existingTests } = await supabase
3655
4651
  .from('test_cases')
3656
4652
  .select('test_key')
3657
- .eq('project_id', PROJECT_ID)
4653
+ .eq('project_id', currentProjectId)
3658
4654
  .order('test_key', { ascending: false })
3659
4655
  .limit(1);
3660
4656
  const lastKey = existingTests?.[0]?.test_key || 'TC-000';
@@ -3665,7 +4661,7 @@ async function createRegressionTest(args) {
3665
4661
  const targetRoute = appContext?.currentRoute;
3666
4662
  // Generate test case from bug
3667
4663
  const testCase = {
3668
- project_id: PROJECT_ID,
4664
+ project_id: currentProjectId,
3669
4665
  test_key: newKey,
3670
4666
  title: `Regression: ${report.title}`,
3671
4667
  description: `Regression test to prevent recurrence of bug #${args.report_id.slice(0, 8)}\n\nOriginal bug: ${report.description}`,
@@ -3741,7 +4737,7 @@ async function getPendingFixes(args) {
3741
4737
  created_at,
3742
4738
  report:reports(id, title, severity, description)
3743
4739
  `)
3744
- .eq('project_id', PROJECT_ID)
4740
+ .eq('project_id', currentProjectId)
3745
4741
  .order('created_at', { ascending: true })
3746
4742
  .limit(limit);
3747
4743
  if (!args.include_claimed) {
@@ -3791,7 +4787,7 @@ async function claimFixRequest(args) {
3791
4787
  .from('fix_requests')
3792
4788
  .select('id, status, claimed_by, prompt, title')
3793
4789
  .eq('id', args.fix_request_id)
3794
- .eq('project_id', PROJECT_ID) // Security: ensure fix request belongs to this project
4790
+ .eq('project_id', currentProjectId) // Security: ensure fix request belongs to this project
3795
4791
  .single();
3796
4792
  if (checkError) {
3797
4793
  return { error: checkError.message };
@@ -3818,7 +4814,7 @@ async function claimFixRequest(args) {
3818
4814
  claimed_by: claimedBy,
3819
4815
  })
3820
4816
  .eq('id', args.fix_request_id)
3821
- .eq('project_id', PROJECT_ID) // Security: ensure fix request belongs to this project
4817
+ .eq('project_id', currentProjectId) // Security: ensure fix request belongs to this project
3822
4818
  .eq('status', 'pending'); // Only claim if still pending (race condition protection)
3823
4819
  if (updateError) {
3824
4820
  return { error: updateError.message };
@@ -3853,7 +4849,7 @@ async function completeFixRequest(args) {
3853
4849
  .from('fix_requests')
3854
4850
  .update(updates)
3855
4851
  .eq('id', args.fix_request_id)
3856
- .eq('project_id', PROJECT_ID); // Security: ensure fix request belongs to this project
4852
+ .eq('project_id', currentProjectId); // Security: ensure fix request belongs to this project
3857
4853
  if (error) {
3858
4854
  return { error: error.message };
3859
4855
  }
@@ -3936,7 +4932,7 @@ async function generatePromptContent(name, args) {
3936
4932
  created_at,
3937
4933
  report:reports(id, title, severity)
3938
4934
  `)
3939
- .eq('project_id', PROJECT_ID)
4935
+ .eq('project_id', currentProjectId)
3940
4936
  .eq('status', 'pending')
3941
4937
  .order('created_at', { ascending: true })
3942
4938
  .limit(5);
@@ -3944,7 +4940,7 @@ async function generatePromptContent(name, args) {
3944
4940
  let query = supabase
3945
4941
  .from('reports')
3946
4942
  .select('id, title, description, severity, status, code_context, created_at')
3947
- .eq('project_id', PROJECT_ID)
4943
+ .eq('project_id', currentProjectId)
3948
4944
  .eq('report_type', 'bug')
3949
4945
  .in('status', ['new', 'confirmed', 'in_progress']);
3950
4946
  if (severity !== 'all') {
@@ -4094,7 +5090,7 @@ Would you like me to generate test cases for these files?`;
4094
5090
  const { data: resolvedBugs } = await supabase
4095
5091
  .from('reports')
4096
5092
  .select('id, title, description, severity, resolved_at, code_context')
4097
- .eq('project_id', PROJECT_ID)
5093
+ .eq('project_id', currentProjectId)
4098
5094
  .eq('report_type', 'bug')
4099
5095
  .eq('status', 'resolved')
4100
5096
  .order('resolved_at', { ascending: false })
@@ -4205,12 +5201,15 @@ Which files or areas would you like me to analyze?`;
4205
5201
  async function listTesters(args) {
4206
5202
  let query = supabase
4207
5203
  .from('testers')
4208
- .select('id, name, email, status, platforms, tier, assigned_count, completed_count, notes, created_at')
4209
- .eq('project_id', PROJECT_ID)
5204
+ .select('id, name, email, status, platforms, tier, assigned_count, completed_count, notes, role, created_at')
5205
+ .eq('project_id', currentProjectId)
4210
5206
  .order('name', { ascending: true });
4211
5207
  if (args.status) {
4212
5208
  query = query.eq('status', args.status);
4213
5209
  }
5210
+ if (args.role) {
5211
+ query = query.eq('role', args.role);
5212
+ }
4214
5213
  const { data, error } = await query;
4215
5214
  if (error) {
4216
5215
  return { error: error.message };
@@ -4232,6 +5231,7 @@ async function listTesters(args) {
4232
5231
  assignedCount: t.assigned_count,
4233
5232
  completedCount: t.completed_count,
4234
5233
  notes: t.notes,
5234
+ role: t.role,
4235
5235
  })),
4236
5236
  };
4237
5237
  }
@@ -4240,7 +5240,7 @@ async function listTestRuns(args) {
4240
5240
  let query = supabase
4241
5241
  .from('test_runs')
4242
5242
  .select('id, name, description, status, total_tests, passed_tests, failed_tests, started_at, completed_at, created_at')
4243
- .eq('project_id', PROJECT_ID)
5243
+ .eq('project_id', currentProjectId)
4244
5244
  .order('created_at', { ascending: false })
4245
5245
  .limit(limit);
4246
5246
  if (args.status) {
@@ -4274,7 +5274,7 @@ async function createTestRun(args) {
4274
5274
  const { data, error } = await supabase
4275
5275
  .from('test_runs')
4276
5276
  .insert({
4277
- project_id: PROJECT_ID,
5277
+ project_id: currentProjectId,
4278
5278
  name: args.name.trim(),
4279
5279
  description: args.description?.trim() || null,
4280
5280
  status: 'draft',
@@ -4319,7 +5319,7 @@ async function listTestAssignments(args) {
4319
5319
  tester:testers(id, name, email),
4320
5320
  test_run:test_runs(id, name)
4321
5321
  `)
4322
- .eq('project_id', PROJECT_ID)
5322
+ .eq('project_id', currentProjectId)
4323
5323
  .order('assigned_at', { ascending: false })
4324
5324
  .limit(limit);
4325
5325
  if (args.tester_id) {
@@ -4389,7 +5389,7 @@ async function assignTests(args) {
4389
5389
  .from('testers')
4390
5390
  .select('id, name, email, status')
4391
5391
  .eq('id', args.tester_id)
4392
- .eq('project_id', PROJECT_ID)
5392
+ .eq('project_id', currentProjectId)
4393
5393
  .single();
4394
5394
  if (testerErr || !tester) {
4395
5395
  return { error: 'Tester not found in this project' };
@@ -4401,7 +5401,7 @@ async function assignTests(args) {
4401
5401
  const { data: testCases, error: tcErr } = await supabase
4402
5402
  .from('test_cases')
4403
5403
  .select('id, test_key, title')
4404
- .eq('project_id', PROJECT_ID)
5404
+ .eq('project_id', currentProjectId)
4405
5405
  .in('id', args.test_case_ids);
4406
5406
  if (tcErr) {
4407
5407
  return { error: tcErr.message };
@@ -4419,7 +5419,7 @@ async function assignTests(args) {
4419
5419
  .from('test_runs')
4420
5420
  .select('id')
4421
5421
  .eq('id', args.test_run_id)
4422
- .eq('project_id', PROJECT_ID)
5422
+ .eq('project_id', currentProjectId)
4423
5423
  .single();
4424
5424
  if (runErr || !run) {
4425
5425
  return { error: 'Test run not found in this project' };
@@ -4427,12 +5427,28 @@ async function assignTests(args) {
4427
5427
  }
4428
5428
  // Build assignment rows
4429
5429
  const rows = args.test_case_ids.map(tcId => ({
4430
- project_id: PROJECT_ID,
5430
+ project_id: currentProjectId,
4431
5431
  test_case_id: tcId,
4432
5432
  tester_id: args.tester_id,
4433
5433
  test_run_id: args.test_run_id || null,
4434
5434
  status: 'pending',
4435
5435
  }));
5436
+ // Helper: after assignments change, sync the test run's total_tests counter
5437
+ async function syncRunCounter() {
5438
+ if (!args.test_run_id)
5439
+ return;
5440
+ const { count } = await supabase
5441
+ .from('test_assignments')
5442
+ .select('id', { count: 'exact', head: true })
5443
+ .eq('test_run_id', args.test_run_id)
5444
+ .eq('project_id', currentProjectId);
5445
+ if (count !== null) {
5446
+ await supabase
5447
+ .from('test_runs')
5448
+ .update({ total_tests: count })
5449
+ .eq('id', args.test_run_id);
5450
+ }
5451
+ }
4436
5452
  // Insert — use upsert-like approach: insert and handle conflicts
4437
5453
  const { data: inserted, error: insertErr } = await supabase
4438
5454
  .from('test_assignments')
@@ -4458,6 +5474,7 @@ async function assignTests(args) {
4458
5474
  created.push(single);
4459
5475
  }
4460
5476
  }
5477
+ await syncRunCounter();
4461
5478
  return {
4462
5479
  success: true,
4463
5480
  created: created.length,
@@ -4469,6 +5486,7 @@ async function assignTests(args) {
4469
5486
  }
4470
5487
  return { error: insertErr.message };
4471
5488
  }
5489
+ await syncRunCounter();
4472
5490
  return {
4473
5491
  success: true,
4474
5492
  created: (inserted || []).length,
@@ -4478,6 +5496,71 @@ async function assignTests(args) {
4478
5496
  message: `Assigned ${(inserted || []).length} test(s) to ${tester.name}.`,
4479
5497
  };
4480
5498
  }
5499
+ async function unassignTests(args) {
5500
+ if (!args.assignment_ids || args.assignment_ids.length === 0) {
5501
+ return { error: 'At least one assignment_id is required' };
5502
+ }
5503
+ if (args.assignment_ids.length > 50) {
5504
+ return { error: 'Maximum 50 assignments per unassign batch' };
5505
+ }
5506
+ const invalidIds = args.assignment_ids.filter(id => !isValidUUID(id));
5507
+ if (invalidIds.length > 0) {
5508
+ return { error: `Invalid UUID(s): ${invalidIds.join(', ')}` };
5509
+ }
5510
+ // Verify assignments exist and belong to this project
5511
+ const { data: existing, error: lookupErr } = await supabase
5512
+ .from('test_assignments')
5513
+ .select('id, test_run_id, test_case:test_cases(test_key, title), tester:testers(name)')
5514
+ .eq('project_id', currentProjectId)
5515
+ .in('id', args.assignment_ids);
5516
+ if (lookupErr)
5517
+ return { error: lookupErr.message };
5518
+ if (!existing || existing.length === 0) {
5519
+ return { error: 'No matching assignments found in this project' };
5520
+ }
5521
+ const foundIds = new Set(existing.map((a) => a.id));
5522
+ const notFound = args.assignment_ids.filter(id => !foundIds.has(id));
5523
+ // Delete the assignments
5524
+ const { error: deleteErr } = await supabase
5525
+ .from('test_assignments')
5526
+ .delete()
5527
+ .eq('project_id', currentProjectId)
5528
+ .in('id', args.assignment_ids);
5529
+ if (deleteErr)
5530
+ return { error: deleteErr.message };
5531
+ // Sync run counters for any affected test runs
5532
+ const affectedRunIds = [...new Set(existing.filter((a) => a.test_run_id).map((a) => a.test_run_id))];
5533
+ for (const runId of affectedRunIds) {
5534
+ const { count } = await supabase
5535
+ .from('test_assignments')
5536
+ .select('id', { count: 'exact', head: true })
5537
+ .eq('test_run_id', runId)
5538
+ .eq('project_id', currentProjectId);
5539
+ if (count !== null) {
5540
+ await supabase.from('test_runs').update({ total_tests: count }).eq('id', runId);
5541
+ }
5542
+ }
5543
+ const deleted = existing.map((a) => {
5544
+ const tc = a.test_case;
5545
+ const tester = a.tester;
5546
+ return {
5547
+ id: a.id,
5548
+ testKey: tc?.test_key || null,
5549
+ testTitle: tc?.title || null,
5550
+ testerName: tester?.name || null,
5551
+ };
5552
+ });
5553
+ const firstKey = deleted[0]?.testKey;
5554
+ return {
5555
+ success: true,
5556
+ deletedCount: existing.length,
5557
+ deleted,
5558
+ notFound: notFound.length > 0 ? notFound : undefined,
5559
+ message: existing.length === 1
5560
+ ? `Removed 1 assignment${firstKey ? ` (${firstKey})` : ''}`
5561
+ : `Removed ${existing.length} assignment(s)`,
5562
+ };
5563
+ }
4481
5564
  async function getTesterWorkload(args) {
4482
5565
  if (!isValidUUID(args.tester_id)) {
4483
5566
  return { error: 'Invalid tester_id format' };
@@ -4487,7 +5570,7 @@ async function getTesterWorkload(args) {
4487
5570
  .from('testers')
4488
5571
  .select('id, name, email, status, platforms, tier')
4489
5572
  .eq('id', args.tester_id)
4490
- .eq('project_id', PROJECT_ID)
5573
+ .eq('project_id', currentProjectId)
4491
5574
  .single();
4492
5575
  if (testerErr || !tester) {
4493
5576
  return { error: 'Tester not found in this project' };
@@ -4503,7 +5586,7 @@ async function getTesterWorkload(args) {
4503
5586
  test_case:test_cases(test_key, title, priority),
4504
5587
  test_run:test_runs(name)
4505
5588
  `)
4506
- .eq('project_id', PROJECT_ID)
5589
+ .eq('project_id', currentProjectId)
4507
5590
  .eq('tester_id', args.tester_id)
4508
5591
  .order('assigned_at', { ascending: false });
4509
5592
  if (assignErr) {
@@ -4548,6 +5631,448 @@ async function getTesterWorkload(args) {
4548
5631
  })),
4549
5632
  };
4550
5633
  }
5634
+ // === NEW TESTER & ANALYTICS HANDLERS ===
5635
+ async function createTester(args) {
5636
+ if (!args.name || args.name.trim().length === 0) {
5637
+ return { error: 'Tester name is required' };
5638
+ }
5639
+ if (!args.email || !args.email.includes('@')) {
5640
+ return { error: 'A valid email address is required' };
5641
+ }
5642
+ if (args.tier !== undefined && (args.tier < 1 || args.tier > 3)) {
5643
+ return { error: 'Tier must be 1, 2, or 3' };
5644
+ }
5645
+ const validPlatforms = ['ios', 'android', 'web'];
5646
+ if (args.platforms) {
5647
+ for (const p of args.platforms) {
5648
+ if (!validPlatforms.includes(p)) {
5649
+ return { error: `Invalid platform "${p}". Must be one of: ${validPlatforms.join(', ')}` };
5650
+ }
5651
+ }
5652
+ }
5653
+ const { data, error } = await supabase
5654
+ .from('testers')
5655
+ .insert({
5656
+ project_id: currentProjectId,
5657
+ name: args.name.trim(),
5658
+ email: args.email.trim().toLowerCase(),
5659
+ platforms: args.platforms || ['ios', 'web'],
5660
+ tier: args.tier ?? 1,
5661
+ notes: args.notes?.trim() || null,
5662
+ status: 'active',
5663
+ role: args.role || 'tester',
5664
+ })
5665
+ .select('id, name, email, status, platforms, tier, notes, role, created_at')
5666
+ .single();
5667
+ if (error) {
5668
+ if (error.message.includes('duplicate') || error.message.includes('unique')) {
5669
+ return { error: `A tester with email "${args.email}" already exists in this project` };
5670
+ }
5671
+ return { error: error.message };
5672
+ }
5673
+ return {
5674
+ success: true,
5675
+ tester: {
5676
+ id: data.id,
5677
+ name: data.name,
5678
+ email: data.email,
5679
+ status: data.status,
5680
+ platforms: data.platforms,
5681
+ tier: data.tier,
5682
+ notes: data.notes,
5683
+ role: data.role,
5684
+ createdAt: data.created_at,
5685
+ },
5686
+ message: `Tester "${data.name}" added to the project. Use assign_tests to give them test cases.`,
5687
+ };
5688
+ }
5689
+ async function updateTester(args) {
5690
+ if (!isValidUUID(args.tester_id)) {
5691
+ return { error: 'Invalid tester_id format' };
5692
+ }
5693
+ const updates = {};
5694
+ if (args.status)
5695
+ updates.status = args.status;
5696
+ if (args.platforms)
5697
+ updates.platforms = args.platforms;
5698
+ if (args.tier !== undefined) {
5699
+ if (args.tier < 1 || args.tier > 3) {
5700
+ return { error: 'Tier must be 1, 2, or 3' };
5701
+ }
5702
+ updates.tier = args.tier;
5703
+ }
5704
+ if (args.notes !== undefined)
5705
+ updates.notes = args.notes.trim() || null;
5706
+ if (args.name)
5707
+ updates.name = args.name.trim();
5708
+ if (Object.keys(updates).length === 0) {
5709
+ return { error: 'No fields to update. Provide at least one of: status, platforms, tier, notes, name' };
5710
+ }
5711
+ const { data, error } = await supabase
5712
+ .from('testers')
5713
+ .update(updates)
5714
+ .eq('id', args.tester_id)
5715
+ .eq('project_id', currentProjectId)
5716
+ .select('id, name, email, status, platforms, tier, notes')
5717
+ .single();
5718
+ if (error) {
5719
+ return { error: error.message };
5720
+ }
5721
+ if (!data) {
5722
+ return { error: 'Tester not found in this project' };
5723
+ }
5724
+ return {
5725
+ success: true,
5726
+ tester: {
5727
+ id: data.id,
5728
+ name: data.name,
5729
+ email: data.email,
5730
+ status: data.status,
5731
+ platforms: data.platforms,
5732
+ tier: data.tier,
5733
+ notes: data.notes,
5734
+ },
5735
+ updatedFields: Object.keys(updates),
5736
+ };
5737
+ }
5738
+ async function bulkUpdateReports(args) {
5739
+ if (!args.report_ids || args.report_ids.length === 0) {
5740
+ return { error: 'At least one report_id is required' };
5741
+ }
5742
+ if (args.report_ids.length > 50) {
5743
+ return { error: 'Maximum 50 reports per bulk update' };
5744
+ }
5745
+ for (const id of args.report_ids) {
5746
+ if (!isValidUUID(id)) {
5747
+ return { error: `Invalid report_id format: ${id}` };
5748
+ }
5749
+ }
5750
+ const updates = { status: args.status };
5751
+ if (args.resolution_notes) {
5752
+ updates.resolution_notes = args.resolution_notes;
5753
+ }
5754
+ // Set resolved_at timestamp for terminal statuses
5755
+ if (['fixed', 'resolved', 'verified', 'wont_fix', 'duplicate', 'closed'].includes(args.status)) {
5756
+ updates.resolved_at = new Date().toISOString();
5757
+ }
5758
+ const { data, error } = await supabase
5759
+ .from('reports')
5760
+ .update(updates)
5761
+ .eq('project_id', currentProjectId)
5762
+ .in('id', args.report_ids)
5763
+ .select('id, status, description');
5764
+ if (error) {
5765
+ return { error: error.message };
5766
+ }
5767
+ const updated = data || [];
5768
+ const updatedIds = new Set(updated.map((r) => r.id));
5769
+ const notFound = args.report_ids.filter(id => !updatedIds.has(id));
5770
+ return {
5771
+ success: true,
5772
+ updatedCount: updated.length,
5773
+ requestedCount: args.report_ids.length,
5774
+ notFound: notFound.length > 0 ? notFound : undefined,
5775
+ status: args.status,
5776
+ reports: updated.map((r) => ({
5777
+ id: r.id,
5778
+ status: r.status,
5779
+ description: r.description?.slice(0, 80),
5780
+ })),
5781
+ message: `Updated ${updated.length} report(s) to "${args.status}".${notFound.length > 0 ? ` ${notFound.length} report(s) not found.` : ''}`,
5782
+ };
5783
+ }
5784
+ async function getBugTrends(args) {
5785
+ const days = Math.min(args.days || 30, 180);
5786
+ const groupBy = args.group_by || 'week';
5787
+ const since = new Date(Date.now() - days * 86400000).toISOString();
5788
+ const { data, error } = await supabase
5789
+ .from('reports')
5790
+ .select('id, severity, category, status, report_type, created_at')
5791
+ .eq('project_id', currentProjectId)
5792
+ .gte('created_at', since)
5793
+ .order('created_at', { ascending: true });
5794
+ if (error) {
5795
+ return { error: error.message };
5796
+ }
5797
+ const reports = data || [];
5798
+ if (groupBy === 'week') {
5799
+ const weeks = {};
5800
+ for (const r of reports) {
5801
+ const d = new Date(r.created_at);
5802
+ // Get Monday of that week
5803
+ const day = d.getDay();
5804
+ const diff = d.getDate() - day + (day === 0 ? -6 : 1);
5805
+ const monday = new Date(d.setDate(diff));
5806
+ const weekKey = monday.toISOString().slice(0, 10);
5807
+ if (!weeks[weekKey])
5808
+ weeks[weekKey] = { count: 0, critical: 0, high: 0, medium: 0, low: 0 };
5809
+ weeks[weekKey].count++;
5810
+ const sev = (r.severity || 'low');
5811
+ weeks[weekKey][sev]++;
5812
+ }
5813
+ return {
5814
+ period: `${days} days`,
5815
+ groupBy: 'week',
5816
+ totalReports: reports.length,
5817
+ weeks: Object.entries(weeks).map(([week, data]) => ({ week, ...data })),
5818
+ };
5819
+ }
5820
+ if (groupBy === 'severity') {
5821
+ const groups = { critical: 0, high: 0, medium: 0, low: 0 };
5822
+ for (const r of reports)
5823
+ groups[r.severity || 'low']++;
5824
+ return { period: `${days} days`, groupBy: 'severity', totalReports: reports.length, breakdown: groups };
5825
+ }
5826
+ if (groupBy === 'category') {
5827
+ const groups = {};
5828
+ for (const r of reports) {
5829
+ const cat = r.category || 'uncategorized';
5830
+ groups[cat] = (groups[cat] || 0) + 1;
5831
+ }
5832
+ return { period: `${days} days`, groupBy: 'category', totalReports: reports.length, breakdown: groups };
5833
+ }
5834
+ if (groupBy === 'status') {
5835
+ const groups = {};
5836
+ for (const r of reports) {
5837
+ groups[r.status] = (groups[r.status] || 0) + 1;
5838
+ }
5839
+ return { period: `${days} days`, groupBy: 'status', totalReports: reports.length, breakdown: groups };
5840
+ }
5841
+ return { error: `Invalid group_by: ${groupBy}. Must be one of: week, severity, category, status` };
5842
+ }
5843
+ async function getTesterLeaderboard(args) {
5844
+ const days = Math.min(args.days || 30, 180);
5845
+ const sortBy = args.sort_by || 'tests_completed';
5846
+ const since = new Date(Date.now() - days * 86400000).toISOString();
5847
+ // Get all testers for the project
5848
+ const { data: testers, error: testerErr } = await supabase
5849
+ .from('testers')
5850
+ .select('id, name, email, status, platforms, tier')
5851
+ .eq('project_id', currentProjectId)
5852
+ .eq('status', 'active');
5853
+ if (testerErr)
5854
+ return { error: testerErr.message };
5855
+ // Get completed assignments in the period
5856
+ const { data: assignments, error: assignErr } = await supabase
5857
+ .from('test_assignments')
5858
+ .select('tester_id, status, completed_at, duration_seconds')
5859
+ .eq('project_id', currentProjectId)
5860
+ .gte('completed_at', since)
5861
+ .in('status', ['passed', 'failed']);
5862
+ if (assignErr)
5863
+ return { error: assignErr.message };
5864
+ // Get bugs filed in the period
5865
+ const { data: bugs, error: bugErr } = await supabase
5866
+ .from('reports')
5867
+ .select('tester_id, severity')
5868
+ .eq('project_id', currentProjectId)
5869
+ .gte('created_at', since)
5870
+ .not('tester_id', 'is', null);
5871
+ if (bugErr)
5872
+ return { error: bugErr.message };
5873
+ // Aggregate per tester
5874
+ const testerMap = new Map();
5875
+ for (const t of testers || []) {
5876
+ testerMap.set(t.id, {
5877
+ id: t.id,
5878
+ name: t.name,
5879
+ email: t.email,
5880
+ tier: t.tier,
5881
+ testsCompleted: 0,
5882
+ testsPassed: 0,
5883
+ testsFailed: 0,
5884
+ bugsFound: 0,
5885
+ criticalBugs: 0,
5886
+ avgDurationSeconds: 0,
5887
+ totalDuration: 0,
5888
+ });
5889
+ }
5890
+ for (const a of assignments || []) {
5891
+ const entry = testerMap.get(a.tester_id);
5892
+ if (!entry)
5893
+ continue;
5894
+ entry.testsCompleted++;
5895
+ if (a.status === 'passed')
5896
+ entry.testsPassed++;
5897
+ if (a.status === 'failed')
5898
+ entry.testsFailed++;
5899
+ if (a.duration_seconds)
5900
+ entry.totalDuration += a.duration_seconds;
5901
+ }
5902
+ for (const b of bugs || []) {
5903
+ const entry = testerMap.get(b.tester_id);
5904
+ if (!entry)
5905
+ continue;
5906
+ entry.bugsFound++;
5907
+ if (b.severity === 'critical')
5908
+ entry.criticalBugs++;
5909
+ }
5910
+ let leaderboard = Array.from(testerMap.values()).map(t => ({
5911
+ ...t,
5912
+ passRate: t.testsCompleted > 0 ? Math.round((t.testsPassed / t.testsCompleted) * 100) : 0,
5913
+ avgDurationSeconds: t.testsCompleted > 0 ? Math.round(t.totalDuration / t.testsCompleted) : 0,
5914
+ totalDuration: undefined,
5915
+ }));
5916
+ // Sort
5917
+ if (sortBy === 'bugs_found') {
5918
+ leaderboard.sort((a, b) => b.bugsFound - a.bugsFound);
5919
+ }
5920
+ else if (sortBy === 'pass_rate') {
5921
+ leaderboard.sort((a, b) => b.passRate - a.passRate);
5922
+ }
5923
+ else {
5924
+ leaderboard.sort((a, b) => b.testsCompleted - a.testsCompleted);
5925
+ }
5926
+ return {
5927
+ period: `${days} days`,
5928
+ sortedBy: sortBy,
5929
+ leaderboard,
5930
+ };
5931
+ }
5932
+ async function exportTestResults(args) {
5933
+ if (!isValidUUID(args.test_run_id)) {
5934
+ return { error: 'Invalid test_run_id format' };
5935
+ }
5936
+ // Get the test run
5937
+ const { data: run, error: runErr } = await supabase
5938
+ .from('test_runs')
5939
+ .select('id, name, description, status, total_tests, passed_tests, failed_tests, started_at, completed_at, created_at')
5940
+ .eq('id', args.test_run_id)
5941
+ .eq('project_id', currentProjectId)
5942
+ .single();
5943
+ if (runErr || !run) {
5944
+ return { error: 'Test run not found in this project' };
5945
+ }
5946
+ // Get all assignments for this run
5947
+ const { data: assignments, error: assignErr } = await supabase
5948
+ .from('test_assignments')
5949
+ .select(`
5950
+ id,
5951
+ status,
5952
+ assigned_at,
5953
+ started_at,
5954
+ completed_at,
5955
+ duration_seconds,
5956
+ is_verification,
5957
+ notes,
5958
+ skip_reason,
5959
+ test_result,
5960
+ feedback_rating,
5961
+ feedback_note,
5962
+ test_case:test_cases(id, test_key, title, priority, description, target_route),
5963
+ tester:testers(id, name, email)
5964
+ `)
5965
+ .eq('test_run_id', args.test_run_id)
5966
+ .eq('project_id', currentProjectId)
5967
+ .order('assigned_at', { ascending: true });
5968
+ if (assignErr) {
5969
+ return { error: assignErr.message };
5970
+ }
5971
+ const all = assignments || [];
5972
+ const passCount = all.filter(a => a.status === 'passed').length;
5973
+ const failCount = all.filter(a => a.status === 'failed').length;
5974
+ const testRunInfo = {
5975
+ id: run.id,
5976
+ name: run.name,
5977
+ description: run.description,
5978
+ status: run.status,
5979
+ startedAt: run.started_at,
5980
+ completedAt: run.completed_at,
5981
+ createdAt: run.created_at,
5982
+ };
5983
+ const summaryInfo = {
5984
+ totalAssignments: all.length,
5985
+ passed: passCount,
5986
+ failed: failCount,
5987
+ blocked: all.filter(a => a.status === 'blocked').length,
5988
+ skipped: all.filter(a => a.status === 'skipped').length,
5989
+ pending: all.filter(a => a.status === 'pending').length,
5990
+ inProgress: all.filter(a => a.status === 'in_progress').length,
5991
+ passRate: all.length > 0 ? Math.round((passCount / all.length) * 100) : 0,
5992
+ };
5993
+ // Compact: return test run info + summary only, no assignments array
5994
+ if (args.compact === true) {
5995
+ return { testRun: testRunInfo, summary: summaryInfo };
5996
+ }
5997
+ // Apply limit (default: 100, max: 500)
5998
+ const assignmentLimit = Math.min(Math.max(args.limit ?? 100, 1), 500);
5999
+ const limitedAssignments = all.slice(0, assignmentLimit);
6000
+ return {
6001
+ testRun: testRunInfo,
6002
+ summary: summaryInfo,
6003
+ assignmentsReturned: limitedAssignments.length,
6004
+ assignmentsTotal: all.length,
6005
+ assignments: limitedAssignments.map((a) => ({
6006
+ id: a.id,
6007
+ status: a.status,
6008
+ assignedAt: a.assigned_at,
6009
+ startedAt: a.started_at,
6010
+ completedAt: a.completed_at,
6011
+ durationSeconds: a.duration_seconds,
6012
+ isVerification: a.is_verification,
6013
+ notes: a.notes,
6014
+ skipReason: a.skip_reason,
6015
+ testResult: a.test_result,
6016
+ feedbackRating: a.feedback_rating,
6017
+ feedbackNote: a.feedback_note,
6018
+ testCase: a.test_case ? {
6019
+ id: a.test_case.id,
6020
+ testKey: a.test_case.test_key,
6021
+ title: a.test_case.title,
6022
+ priority: a.test_case.priority,
6023
+ description: a.test_case.description,
6024
+ targetRoute: a.test_case.target_route,
6025
+ } : null,
6026
+ tester: a.tester ? {
6027
+ id: a.tester.id,
6028
+ name: a.tester.name,
6029
+ email: a.tester.email,
6030
+ } : null,
6031
+ })),
6032
+ };
6033
+ }
6034
+ async function getTestingVelocity(args) {
6035
+ const days = Math.min(args.days || 14, 90);
6036
+ const since = new Date(Date.now() - days * 86400000).toISOString();
6037
+ const { data, error } = await supabase
6038
+ .from('test_assignments')
6039
+ .select('completed_at, status')
6040
+ .eq('project_id', currentProjectId)
6041
+ .gte('completed_at', since)
6042
+ .in('status', ['passed', 'failed'])
6043
+ .order('completed_at', { ascending: true });
6044
+ if (error) {
6045
+ return { error: error.message };
6046
+ }
6047
+ const completions = data || [];
6048
+ // Group by day
6049
+ const dailyCounts = {};
6050
+ for (let i = 0; i < days; i++) {
6051
+ const d = new Date(Date.now() - (days - 1 - i) * 86400000);
6052
+ dailyCounts[d.toISOString().slice(0, 10)] = 0;
6053
+ }
6054
+ for (const c of completions) {
6055
+ const day = new Date(c.completed_at).toISOString().slice(0, 10);
6056
+ if (dailyCounts[day] !== undefined) {
6057
+ dailyCounts[day]++;
6058
+ }
6059
+ }
6060
+ const dailyArray = Object.entries(dailyCounts).map(([date, count]) => ({ date, count }));
6061
+ const totalCompleted = completions.length;
6062
+ const avgPerDay = days > 0 ? Math.round((totalCompleted / days) * 10) / 10 : 0;
6063
+ // Trend: compare first half to second half
6064
+ const mid = Math.floor(dailyArray.length / 2);
6065
+ const firstHalf = dailyArray.slice(0, mid).reduce((sum, d) => sum + d.count, 0);
6066
+ const secondHalf = dailyArray.slice(mid).reduce((sum, d) => sum + d.count, 0);
6067
+ const trend = secondHalf > firstHalf ? 'increasing' : secondHalf < firstHalf ? 'decreasing' : 'stable';
6068
+ return {
6069
+ period: `${days} days`,
6070
+ totalCompleted,
6071
+ averagePerDay: avgPerDay,
6072
+ trend,
6073
+ daily: dailyArray,
6074
+ };
6075
+ }
4551
6076
  // Main server setup
4552
6077
  async function main() {
4553
6078
  initSupabase();
@@ -4570,6 +6095,11 @@ async function main() {
4570
6095
  const { name, arguments: args } = request.params;
4571
6096
  try {
4572
6097
  let result;
6098
+ // Project management tools don't require a project to be selected
6099
+ const projectFreeTools = ['list_projects', 'switch_project', 'get_current_project'];
6100
+ if (!projectFreeTools.includes(name)) {
6101
+ requireProject();
6102
+ }
4573
6103
  switch (name) {
4574
6104
  case 'list_reports':
4575
6105
  result = await listReports(args);
@@ -4586,6 +6116,12 @@ async function main() {
4586
6116
  case 'get_report_context':
4587
6117
  result = await getReportContext(args);
4588
6118
  break;
6119
+ case 'add_report_comment':
6120
+ result = await addReportComment(args);
6121
+ break;
6122
+ case 'get_report_comments':
6123
+ result = await getReportComments(args);
6124
+ break;
4589
6125
  case 'get_project_info':
4590
6126
  result = await getProjectInfo();
4591
6127
  break;
@@ -4699,9 +6235,70 @@ async function main() {
4699
6235
  case 'assign_tests':
4700
6236
  result = await assignTests(args);
4701
6237
  break;
6238
+ case 'unassign_tests':
6239
+ result = await unassignTests(args);
6240
+ break;
4702
6241
  case 'get_tester_workload':
4703
6242
  result = await getTesterWorkload(args);
4704
6243
  break;
6244
+ // === NEW TESTER & ANALYTICS TOOLS ===
6245
+ case 'create_tester':
6246
+ result = await createTester(args);
6247
+ break;
6248
+ case 'update_tester':
6249
+ result = await updateTester(args);
6250
+ break;
6251
+ case 'bulk_update_reports':
6252
+ result = await bulkUpdateReports(args);
6253
+ break;
6254
+ case 'get_bug_trends':
6255
+ result = await getBugTrends(args);
6256
+ break;
6257
+ case 'get_tester_leaderboard':
6258
+ result = await getTesterLeaderboard(args);
6259
+ break;
6260
+ case 'export_test_results':
6261
+ result = await exportTestResults(args);
6262
+ break;
6263
+ case 'get_testing_velocity':
6264
+ result = await getTestingVelocity(args);
6265
+ break;
6266
+ // === PROJECT MANAGEMENT ===
6267
+ case 'list_projects':
6268
+ result = await listProjects();
6269
+ break;
6270
+ case 'switch_project':
6271
+ result = await switchProject(args);
6272
+ break;
6273
+ case 'get_current_project':
6274
+ result = getCurrentProject();
6275
+ break;
6276
+ // === TEST EXECUTION INTELLIGENCE ===
6277
+ case 'get_test_impact':
6278
+ result = await getTestImpact(args);
6279
+ break;
6280
+ case 'get_flaky_tests':
6281
+ result = await getFlakyTests(args);
6282
+ break;
6283
+ case 'assess_test_quality':
6284
+ result = await assessTestQuality(args);
6285
+ break;
6286
+ case 'get_test_execution_summary':
6287
+ result = await getTestExecutionSummary(args);
6288
+ break;
6289
+ case 'check_test_freshness':
6290
+ result = await checkTestFreshness(args);
6291
+ break;
6292
+ case 'get_untested_changes':
6293
+ result = await getUntestedChanges(args);
6294
+ break;
6295
+ // === AUTO-MONITORING TOOLS ===
6296
+ case 'get_auto_detected_issues':
6297
+ result = await getAutoDetectedIssues(args);
6298
+ break;
6299
+ case 'generate_tests_from_errors':
6300
+ result = await generateTestsFromErrors(args);
6301
+ break;
4705
6302
  default:
4706
6303
  return {
4707
6304
  content: [{ type: 'text', text: `Unknown tool: ${name}` }],
@@ -4724,7 +6321,7 @@ async function main() {
4724
6321
  const { data, error } = await supabase
4725
6322
  .from('reports')
4726
6323
  .select('id, description, report_type, severity')
4727
- .eq('project_id', PROJECT_ID)
6324
+ .eq('project_id', currentProjectId)
4728
6325
  .eq('status', 'new')
4729
6326
  .order('created_at', { ascending: false })
4730
6327
  .limit(10);