@bbearai/mcp-server 0.5.1 → 0.6.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 +804 -100
  2. package/package.json +1 -1
  3. package/src/index.ts +883 -101
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();
@@ -1025,13 +1029,247 @@ const tools = [
1025
1029
  required: ['tester_id'],
1026
1030
  },
1027
1031
  },
1032
+ // === NEW TESTER & ANALYTICS TOOLS ===
1033
+ {
1034
+ name: 'create_tester',
1035
+ description: 'Add a new QA tester to the project without opening the dashboard.',
1036
+ inputSchema: {
1037
+ type: 'object',
1038
+ properties: {
1039
+ name: {
1040
+ type: 'string',
1041
+ description: 'Full name of the tester (required)',
1042
+ },
1043
+ email: {
1044
+ type: 'string',
1045
+ description: 'Email address of the tester (required, must be unique per project)',
1046
+ },
1047
+ platforms: {
1048
+ type: 'array',
1049
+ items: { type: 'string', enum: ['ios', 'android', 'web'] },
1050
+ description: 'Platforms the tester can test on (default: ["ios", "web"])',
1051
+ },
1052
+ tier: {
1053
+ type: 'number',
1054
+ description: 'Tester tier 1-3 (default: 1)',
1055
+ },
1056
+ notes: {
1057
+ type: 'string',
1058
+ description: 'Optional notes about the tester',
1059
+ },
1060
+ },
1061
+ required: ['name', 'email'],
1062
+ },
1063
+ },
1064
+ {
1065
+ name: 'update_tester',
1066
+ description: 'Update an existing tester\'s status, platforms, tier, or notes.',
1067
+ inputSchema: {
1068
+ type: 'object',
1069
+ properties: {
1070
+ tester_id: {
1071
+ type: 'string',
1072
+ description: 'UUID of the tester to update (required)',
1073
+ },
1074
+ status: {
1075
+ type: 'string',
1076
+ enum: ['active', 'inactive', 'invited'],
1077
+ description: 'New status for the tester',
1078
+ },
1079
+ platforms: {
1080
+ type: 'array',
1081
+ items: { type: 'string', enum: ['ios', 'android', 'web'] },
1082
+ description: 'Updated platforms array',
1083
+ },
1084
+ tier: {
1085
+ type: 'number',
1086
+ description: 'Updated tier (1-3)',
1087
+ },
1088
+ notes: {
1089
+ type: 'string',
1090
+ description: 'Updated notes',
1091
+ },
1092
+ name: {
1093
+ type: 'string',
1094
+ description: 'Updated name',
1095
+ },
1096
+ },
1097
+ required: ['tester_id'],
1098
+ },
1099
+ },
1100
+ {
1101
+ name: 'bulk_update_reports',
1102
+ description: 'Update the status of multiple bug reports at once. Useful after a fix session to close many bugs.',
1103
+ inputSchema: {
1104
+ type: 'object',
1105
+ properties: {
1106
+ report_ids: {
1107
+ type: 'array',
1108
+ items: { type: 'string' },
1109
+ description: 'Array of report UUIDs to update (required, max 50)',
1110
+ },
1111
+ status: {
1112
+ type: 'string',
1113
+ enum: ['new', 'triaging', 'confirmed', 'in_progress', 'fixed', 'resolved', 'verified', 'wont_fix', 'duplicate', 'closed'],
1114
+ description: 'New status for all reports (required)',
1115
+ },
1116
+ resolution_notes: {
1117
+ type: 'string',
1118
+ description: 'Optional resolution notes applied to all reports',
1119
+ },
1120
+ },
1121
+ required: ['report_ids', 'status'],
1122
+ },
1123
+ },
1124
+ {
1125
+ name: 'get_bug_trends',
1126
+ description: 'Get bug report trends over time — grouped by week, severity, category, or status. Useful for spotting patterns.',
1127
+ inputSchema: {
1128
+ type: 'object',
1129
+ properties: {
1130
+ group_by: {
1131
+ type: 'string',
1132
+ enum: ['week', 'severity', 'category', 'status'],
1133
+ description: 'How to group the trends (default: week)',
1134
+ },
1135
+ days: {
1136
+ type: 'number',
1137
+ description: 'Number of days to look back (default: 30, max: 180)',
1138
+ },
1139
+ },
1140
+ },
1141
+ },
1142
+ {
1143
+ name: 'get_tester_leaderboard',
1144
+ description: 'Rank testers by testing activity — bugs found, tests completed, pass rate, and average test duration.',
1145
+ inputSchema: {
1146
+ type: 'object',
1147
+ properties: {
1148
+ days: {
1149
+ type: 'number',
1150
+ description: 'Number of days to look back (default: 30, max: 180)',
1151
+ },
1152
+ sort_by: {
1153
+ type: 'string',
1154
+ enum: ['bugs_found', 'tests_completed', 'pass_rate'],
1155
+ description: 'Sort metric (default: tests_completed)',
1156
+ },
1157
+ },
1158
+ },
1159
+ },
1160
+ {
1161
+ name: 'export_test_results',
1162
+ description: 'Export test results for a specific test run as structured JSON — includes every assignment, tester, result, and duration.',
1163
+ inputSchema: {
1164
+ type: 'object',
1165
+ properties: {
1166
+ test_run_id: {
1167
+ type: 'string',
1168
+ description: 'UUID of the test run to export (required)',
1169
+ },
1170
+ },
1171
+ required: ['test_run_id'],
1172
+ },
1173
+ },
1174
+ {
1175
+ name: 'get_testing_velocity',
1176
+ description: 'Get a rolling average of test completions per day over the specified window. Shows daily completion counts and trend direction.',
1177
+ inputSchema: {
1178
+ type: 'object',
1179
+ properties: {
1180
+ days: {
1181
+ type: 'number',
1182
+ description: 'Number of days to analyze (default: 14, max: 90)',
1183
+ },
1184
+ },
1185
+ },
1186
+ },
1187
+ // === PROJECT MANAGEMENT TOOLS ===
1188
+ {
1189
+ name: 'list_projects',
1190
+ description: 'List all BugBear projects accessible with the current credentials. Use this to find project IDs for switch_project.',
1191
+ inputSchema: {
1192
+ type: 'object',
1193
+ properties: {},
1194
+ },
1195
+ },
1196
+ {
1197
+ name: 'switch_project',
1198
+ description: 'Switch the active project. All subsequent tool calls will use this project. Use list_projects first to find the project ID.',
1199
+ inputSchema: {
1200
+ type: 'object',
1201
+ properties: {
1202
+ project_id: {
1203
+ type: 'string',
1204
+ description: 'UUID of the project to switch to (required)',
1205
+ },
1206
+ },
1207
+ required: ['project_id'],
1208
+ },
1209
+ },
1210
+ {
1211
+ name: 'get_current_project',
1212
+ description: 'Show which project is currently active.',
1213
+ inputSchema: {
1214
+ type: 'object',
1215
+ properties: {},
1216
+ },
1217
+ },
1028
1218
  ];
1219
+ // === Project management handlers ===
1220
+ async function listProjects() {
1221
+ const { data, error } = await supabase
1222
+ .from('projects')
1223
+ .select('id, name, slug, is_qa_enabled, created_at')
1224
+ .order('name');
1225
+ if (error) {
1226
+ return { error: error.message };
1227
+ }
1228
+ return {
1229
+ currentProjectId: currentProjectId || null,
1230
+ projects: data?.map(p => ({
1231
+ id: p.id,
1232
+ name: p.name,
1233
+ slug: p.slug,
1234
+ isQAEnabled: p.is_qa_enabled,
1235
+ isActive: p.id === currentProjectId,
1236
+ createdAt: p.created_at,
1237
+ })) || [],
1238
+ };
1239
+ }
1240
+ async function switchProject(args) {
1241
+ if (!isValidUUID(args.project_id)) {
1242
+ return { error: 'Invalid project_id format — must be a UUID' };
1243
+ }
1244
+ // Verify the project exists and is accessible
1245
+ const { data, error } = await supabase
1246
+ .from('projects')
1247
+ .select('id, name, slug')
1248
+ .eq('id', args.project_id)
1249
+ .single();
1250
+ if (error || !data) {
1251
+ return { error: 'Project not found or not accessible' };
1252
+ }
1253
+ currentProjectId = data.id;
1254
+ return {
1255
+ success: true,
1256
+ message: `Switched to project "${data.name}" (${data.slug})`,
1257
+ projectId: data.id,
1258
+ projectName: data.name,
1259
+ };
1260
+ }
1261
+ function getCurrentProject() {
1262
+ if (!currentProjectId) {
1263
+ return { message: 'No project selected. Use list_projects to see available projects, then switch_project to select one.' };
1264
+ }
1265
+ return { projectId: currentProjectId };
1266
+ }
1029
1267
  // Tool handlers
1030
1268
  async function listReports(args) {
1031
1269
  let query = supabase
1032
1270
  .from('reports')
1033
1271
  .select('id, report_type, severity, status, description, app_context, created_at, reporter_name, tester:testers(name)')
1034
- .eq('project_id', PROJECT_ID)
1272
+ .eq('project_id', currentProjectId)
1035
1273
  .order('created_at', { ascending: false })
1036
1274
  .limit(Math.min(args.limit || 10, 50));
1037
1275
  if (args.status)
@@ -1067,7 +1305,7 @@ async function getReport(args) {
1067
1305
  .from('reports')
1068
1306
  .select('*, tester:testers(*), track:qa_tracks(*)')
1069
1307
  .eq('id', args.report_id)
1070
- .eq('project_id', PROJECT_ID) // Security: ensure report belongs to this project
1308
+ .eq('project_id', currentProjectId) // Security: ensure report belongs to this project
1071
1309
  .single();
1072
1310
  if (error) {
1073
1311
  return { error: error.message };
@@ -1102,7 +1340,7 @@ async function searchReports(args) {
1102
1340
  let query = supabase
1103
1341
  .from('reports')
1104
1342
  .select('id, report_type, severity, status, description, app_context, created_at')
1105
- .eq('project_id', PROJECT_ID)
1343
+ .eq('project_id', currentProjectId)
1106
1344
  .order('created_at', { ascending: false })
1107
1345
  .limit(20);
1108
1346
  if (sanitizedQuery) {
@@ -1145,7 +1383,7 @@ async function updateReportStatus(args) {
1145
1383
  .from('reports')
1146
1384
  .update(updates)
1147
1385
  .eq('id', args.report_id)
1148
- .eq('project_id', PROJECT_ID); // Security: ensure report belongs to this project
1386
+ .eq('project_id', currentProjectId); // Security: ensure report belongs to this project
1149
1387
  if (error) {
1150
1388
  return { error: error.message };
1151
1389
  }
@@ -1159,7 +1397,7 @@ async function getReportContext(args) {
1159
1397
  .from('reports')
1160
1398
  .select('app_context, device_info, navigation_history, enhanced_context')
1161
1399
  .eq('id', args.report_id)
1162
- .eq('project_id', PROJECT_ID) // Security: ensure report belongs to this project
1400
+ .eq('project_id', currentProjectId) // Security: ensure report belongs to this project
1163
1401
  .single();
1164
1402
  if (error) {
1165
1403
  return { error: error.message };
@@ -1178,7 +1416,7 @@ async function getProjectInfo() {
1178
1416
  const { data: project, error: projectError } = await supabase
1179
1417
  .from('projects')
1180
1418
  .select('id, name, slug, is_qa_enabled')
1181
- .eq('id', PROJECT_ID)
1419
+ .eq('id', currentProjectId)
1182
1420
  .single();
1183
1421
  if (projectError) {
1184
1422
  return { error: projectError.message };
@@ -1187,17 +1425,17 @@ async function getProjectInfo() {
1187
1425
  const { data: tracks } = await supabase
1188
1426
  .from('qa_tracks')
1189
1427
  .select('id, name, icon, test_template')
1190
- .eq('project_id', PROJECT_ID);
1428
+ .eq('project_id', currentProjectId);
1191
1429
  // Get test case count
1192
1430
  const { count: testCaseCount } = await supabase
1193
1431
  .from('test_cases')
1194
1432
  .select('id', { count: 'exact', head: true })
1195
- .eq('project_id', PROJECT_ID);
1433
+ .eq('project_id', currentProjectId);
1196
1434
  // Get open bug count
1197
1435
  const { count: openBugCount } = await supabase
1198
1436
  .from('reports')
1199
1437
  .select('id', { count: 'exact', head: true })
1200
- .eq('project_id', PROJECT_ID)
1438
+ .eq('project_id', currentProjectId)
1201
1439
  .eq('report_type', 'bug')
1202
1440
  .in('status', ['new', 'confirmed', 'in_progress']);
1203
1441
  return {
@@ -1224,7 +1462,7 @@ async function getQaTracks() {
1224
1462
  const { data, error } = await supabase
1225
1463
  .from('qa_tracks')
1226
1464
  .select('*')
1227
- .eq('project_id', PROJECT_ID)
1465
+ .eq('project_id', currentProjectId)
1228
1466
  .order('sort_order');
1229
1467
  if (error) {
1230
1468
  return { error: error.message };
@@ -1252,14 +1490,14 @@ async function createTestCase(args) {
1252
1490
  const { data: trackData } = await supabase
1253
1491
  .from('qa_tracks')
1254
1492
  .select('id')
1255
- .eq('project_id', PROJECT_ID)
1493
+ .eq('project_id', currentProjectId)
1256
1494
  .ilike('name', `%${sanitizedTrack}%`)
1257
1495
  .single();
1258
1496
  trackId = trackData?.id || null;
1259
1497
  }
1260
1498
  }
1261
1499
  const testCase = {
1262
- project_id: PROJECT_ID,
1500
+ project_id: currentProjectId,
1263
1501
  test_key: args.test_key,
1264
1502
  title: args.title,
1265
1503
  description: args.description || '',
@@ -1299,7 +1537,7 @@ async function updateTestCase(args) {
1299
1537
  const { data: existing } = await supabase
1300
1538
  .from('test_cases')
1301
1539
  .select('id')
1302
- .eq('project_id', PROJECT_ID)
1540
+ .eq('project_id', currentProjectId)
1303
1541
  .eq('test_key', args.test_key)
1304
1542
  .single();
1305
1543
  if (!existing) {
@@ -1330,7 +1568,7 @@ async function updateTestCase(args) {
1330
1568
  .from('test_cases')
1331
1569
  .update(updates)
1332
1570
  .eq('id', testCaseId)
1333
- .eq('project_id', PROJECT_ID)
1571
+ .eq('project_id', currentProjectId)
1334
1572
  .select('id, test_key, title, target_route')
1335
1573
  .single();
1336
1574
  if (error) {
@@ -1375,7 +1613,7 @@ async function deleteTestCases(args) {
1375
1613
  const { data: existing } = await supabase
1376
1614
  .from('test_cases')
1377
1615
  .select('id')
1378
- .eq('project_id', PROJECT_ID)
1616
+ .eq('project_id', currentProjectId)
1379
1617
  .eq('test_key', args.test_key)
1380
1618
  .single();
1381
1619
  if (!existing) {
@@ -1408,7 +1646,7 @@ async function deleteTestCases(args) {
1408
1646
  const { data: existing, error: lookupError } = await supabase
1409
1647
  .from('test_cases')
1410
1648
  .select('id, test_key')
1411
- .eq('project_id', PROJECT_ID)
1649
+ .eq('project_id', currentProjectId)
1412
1650
  .in('test_key', args.test_keys);
1413
1651
  if (lookupError) {
1414
1652
  return { error: lookupError.message };
@@ -1424,7 +1662,7 @@ async function deleteTestCases(args) {
1424
1662
  const { data: toDelete } = await supabase
1425
1663
  .from('test_cases')
1426
1664
  .select('id, test_key, title')
1427
- .eq('project_id', PROJECT_ID)
1665
+ .eq('project_id', currentProjectId)
1428
1666
  .in('id', idsToDelete);
1429
1667
  if (!toDelete || toDelete.length === 0) {
1430
1668
  return { error: 'No matching test cases found in this project' };
@@ -1433,7 +1671,7 @@ async function deleteTestCases(args) {
1433
1671
  const { error: deleteError } = await supabase
1434
1672
  .from('test_cases')
1435
1673
  .delete()
1436
- .eq('project_id', PROJECT_ID)
1674
+ .eq('project_id', currentProjectId)
1437
1675
  .in('id', idsToDelete);
1438
1676
  if (deleteError) {
1439
1677
  return { error: deleteError.message };
@@ -1467,7 +1705,7 @@ async function listTestCases(args) {
1467
1705
  steps,
1468
1706
  track:qa_tracks(id, name, icon, color)
1469
1707
  `)
1470
- .eq('project_id', PROJECT_ID)
1708
+ .eq('project_id', currentProjectId)
1471
1709
  .order('test_key', { ascending: true });
1472
1710
  // Apply filters
1473
1711
  if (args.priority) {
@@ -1514,7 +1752,7 @@ async function getBugPatterns(args) {
1514
1752
  let query = supabase
1515
1753
  .from('reports')
1516
1754
  .select('app_context, severity, status, created_at')
1517
- .eq('project_id', PROJECT_ID)
1755
+ .eq('project_id', currentProjectId)
1518
1756
  .eq('report_type', 'bug')
1519
1757
  .order('created_at', { ascending: false })
1520
1758
  .limit(100);
@@ -1566,7 +1804,7 @@ async function suggestTestCases(args) {
1566
1804
  const { data: existingTests } = await supabase
1567
1805
  .from('test_cases')
1568
1806
  .select('test_key, title')
1569
- .eq('project_id', PROJECT_ID)
1807
+ .eq('project_id', currentProjectId)
1570
1808
  .order('test_key', { ascending: false })
1571
1809
  .limit(1);
1572
1810
  // Calculate next test key number
@@ -1601,7 +1839,7 @@ async function suggestTestCases(args) {
1601
1839
  const { data: relatedBugs } = await supabase
1602
1840
  .from('reports')
1603
1841
  .select('id, description, severity')
1604
- .eq('project_id', PROJECT_ID)
1842
+ .eq('project_id', currentProjectId)
1605
1843
  .eq('report_type', 'bug')
1606
1844
  .limit(10);
1607
1845
  const routeBugs = (relatedBugs || []).filter(bug => {
@@ -1635,7 +1873,7 @@ async function getTestPriorities(args) {
1635
1873
  const minScore = args.min_score || 0;
1636
1874
  const includeFactors = args.include_factors !== false;
1637
1875
  // First, refresh the route stats
1638
- const { error: refreshError } = await supabase.rpc('refresh_route_test_stats', { p_project_id: PROJECT_ID });
1876
+ const { error: refreshError } = await supabase.rpc('refresh_route_test_stats', { p_project_id: currentProjectId });
1639
1877
  if (refreshError) {
1640
1878
  // Non-fatal: proceed with potentially stale data but warn
1641
1879
  console.warn('Failed to refresh route stats:', refreshError.message);
@@ -1644,7 +1882,7 @@ async function getTestPriorities(args) {
1644
1882
  const { data: routes, error } = await supabase
1645
1883
  .from('route_test_stats')
1646
1884
  .select('*')
1647
- .eq('project_id', PROJECT_ID)
1885
+ .eq('project_id', currentProjectId)
1648
1886
  .gte('priority_score', minScore)
1649
1887
  .order('priority_score', { ascending: false })
1650
1888
  .limit(limit);
@@ -1761,7 +1999,7 @@ async function getCoverageGaps(args) {
1761
1999
  const { data: routesFromReports } = await supabase
1762
2000
  .from('reports')
1763
2001
  .select('app_context')
1764
- .eq('project_id', PROJECT_ID)
2002
+ .eq('project_id', currentProjectId)
1765
2003
  .not('app_context->currentRoute', 'is', null);
1766
2004
  const allRoutes = new Set();
1767
2005
  (routesFromReports || []).forEach(r => {
@@ -1773,7 +2011,7 @@ async function getCoverageGaps(args) {
1773
2011
  const { data: testCases } = await supabase
1774
2012
  .from('test_cases')
1775
2013
  .select('target_route, category, track_id')
1776
- .eq('project_id', PROJECT_ID);
2014
+ .eq('project_id', currentProjectId);
1777
2015
  const coveredRoutes = new Set();
1778
2016
  const routeTrackCoverage = {};
1779
2017
  (testCases || []).forEach(tc => {
@@ -1790,13 +2028,13 @@ async function getCoverageGaps(args) {
1790
2028
  const { data: tracks } = await supabase
1791
2029
  .from('qa_tracks')
1792
2030
  .select('id, name')
1793
- .eq('project_id', PROJECT_ID);
2031
+ .eq('project_id', currentProjectId);
1794
2032
  const trackMap = new Map((tracks || []).map(t => [t.id, t.name]));
1795
2033
  // Get route stats for staleness
1796
2034
  const { data: routeStats } = await supabase
1797
2035
  .from('route_test_stats')
1798
2036
  .select('route, last_tested_at, open_bugs, critical_bugs')
1799
- .eq('project_id', PROJECT_ID);
2037
+ .eq('project_id', currentProjectId);
1800
2038
  const routeStatsMap = new Map((routeStats || []).map(r => [r.route, r]));
1801
2039
  // Find untested routes
1802
2040
  if (gapType === 'all' || gapType === 'untested_routes') {
@@ -1893,14 +2131,14 @@ async function getRegressions(args) {
1893
2131
  const { data: resolvedBugs } = await supabase
1894
2132
  .from('reports')
1895
2133
  .select('id, description, severity, app_context, resolved_at')
1896
- .eq('project_id', PROJECT_ID)
2134
+ .eq('project_id', currentProjectId)
1897
2135
  .eq('report_type', 'bug')
1898
2136
  .in('status', ['resolved', 'fixed', 'verified', 'closed'])
1899
2137
  .gte('resolved_at', new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString());
1900
2138
  const { data: newBugs } = await supabase
1901
2139
  .from('reports')
1902
2140
  .select('id, description, severity, app_context, created_at')
1903
- .eq('project_id', PROJECT_ID)
2141
+ .eq('project_id', currentProjectId)
1904
2142
  .eq('report_type', 'bug')
1905
2143
  .in('status', ['new', 'triaging', 'confirmed', 'in_progress', 'reviewed'])
1906
2144
  .gte('created_at', new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString());
@@ -2004,20 +2242,20 @@ async function getCoverageMatrix(args) {
2004
2242
  const { data: tracks } = await supabase
2005
2243
  .from('qa_tracks')
2006
2244
  .select('id, name, icon, color')
2007
- .eq('project_id', PROJECT_ID)
2245
+ .eq('project_id', currentProjectId)
2008
2246
  .order('sort_order');
2009
2247
  // Get test cases with track info
2010
2248
  const { data: testCases } = await supabase
2011
2249
  .from('test_cases')
2012
2250
  .select('id, target_route, category, track_id')
2013
- .eq('project_id', PROJECT_ID);
2251
+ .eq('project_id', currentProjectId);
2014
2252
  // Get test assignments for execution data
2015
2253
  let assignments = [];
2016
2254
  if (includeExecution) {
2017
2255
  const { data } = await supabase
2018
2256
  .from('test_assignments')
2019
2257
  .select('test_case_id, status, completed_at')
2020
- .eq('project_id', PROJECT_ID)
2258
+ .eq('project_id', currentProjectId)
2021
2259
  .in('status', ['passed', 'failed'])
2022
2260
  .order('completed_at', { ascending: false })
2023
2261
  .limit(2000);
@@ -2029,7 +2267,7 @@ async function getCoverageMatrix(args) {
2029
2267
  const { data } = await supabase
2030
2268
  .from('route_test_stats')
2031
2269
  .select('route, open_bugs, critical_bugs')
2032
- .eq('project_id', PROJECT_ID);
2270
+ .eq('project_id', currentProjectId);
2033
2271
  routeStats = data || [];
2034
2272
  }
2035
2273
  const routeStatsMap = new Map(routeStats.map(r => [r.route, r]));
@@ -2168,7 +2406,7 @@ async function getStaleCoverage(args) {
2168
2406
  const daysThreshold = args.days_threshold || 14;
2169
2407
  const limit = args.limit || 20;
2170
2408
  // Refresh stats first
2171
- const { error: refreshError } = await supabase.rpc('refresh_route_test_stats', { p_project_id: PROJECT_ID });
2409
+ const { error: refreshError } = await supabase.rpc('refresh_route_test_stats', { p_project_id: currentProjectId });
2172
2410
  if (refreshError) {
2173
2411
  // Non-fatal: proceed with potentially stale data but warn
2174
2412
  console.warn('Failed to refresh route stats:', refreshError.message);
@@ -2177,7 +2415,7 @@ async function getStaleCoverage(args) {
2177
2415
  const { data: routes, error } = await supabase
2178
2416
  .from('route_test_stats')
2179
2417
  .select('route, last_tested_at, open_bugs, critical_bugs, test_case_count, priority_score')
2180
- .eq('project_id', PROJECT_ID)
2418
+ .eq('project_id', currentProjectId)
2181
2419
  .order('last_tested_at', { ascending: true, nullsFirst: true })
2182
2420
  .limit(limit * 2); // Get extra to filter
2183
2421
  if (error) {
@@ -2264,12 +2502,12 @@ async function generateDeployChecklist(args) {
2264
2502
  supabase
2265
2503
  .from('test_cases')
2266
2504
  .select('id, test_key, title, target_route, category, priority, track:qa_tracks(name)')
2267
- .eq('project_id', PROJECT_ID)
2505
+ .eq('project_id', currentProjectId)
2268
2506
  .in('target_route', safeRoutes),
2269
2507
  supabase
2270
2508
  .from('test_cases')
2271
2509
  .select('id, test_key, title, target_route, category, priority, track:qa_tracks(name)')
2272
- .eq('project_id', PROJECT_ID)
2510
+ .eq('project_id', currentProjectId)
2273
2511
  .in('category', safeRoutes),
2274
2512
  ]);
2275
2513
  // Deduplicate by id
@@ -2284,7 +2522,7 @@ async function generateDeployChecklist(args) {
2284
2522
  const { data: routeStats } = await supabase
2285
2523
  .from('route_test_stats')
2286
2524
  .select('*')
2287
- .eq('project_id', PROJECT_ID)
2525
+ .eq('project_id', currentProjectId)
2288
2526
  .in('route', Array.from(allRoutes));
2289
2527
  const routeStatsMap = new Map((routeStats || []).map(r => [r.route, r]));
2290
2528
  // Categorize tests
@@ -2383,30 +2621,30 @@ async function getQAHealth(args) {
2383
2621
  const { data: currentTests } = await supabase
2384
2622
  .from('test_assignments')
2385
2623
  .select('id, status, completed_at')
2386
- .eq('project_id', PROJECT_ID)
2624
+ .eq('project_id', currentProjectId)
2387
2625
  .gte('completed_at', periodStart.toISOString())
2388
2626
  .in('status', ['passed', 'failed']);
2389
2627
  const { data: currentBugs } = await supabase
2390
2628
  .from('reports')
2391
2629
  .select('id, severity, status, created_at')
2392
- .eq('project_id', PROJECT_ID)
2630
+ .eq('project_id', currentProjectId)
2393
2631
  .eq('report_type', 'bug')
2394
2632
  .gte('created_at', periodStart.toISOString());
2395
2633
  const { data: resolvedBugs } = await supabase
2396
2634
  .from('reports')
2397
2635
  .select('id, created_at, resolved_at')
2398
- .eq('project_id', PROJECT_ID)
2636
+ .eq('project_id', currentProjectId)
2399
2637
  .eq('report_type', 'bug')
2400
2638
  .in('status', ['resolved', 'fixed', 'verified', 'closed'])
2401
2639
  .gte('resolved_at', periodStart.toISOString());
2402
2640
  const { data: testers } = await supabase
2403
2641
  .from('testers')
2404
2642
  .select('id, status')
2405
- .eq('project_id', PROJECT_ID);
2643
+ .eq('project_id', currentProjectId);
2406
2644
  const { data: routeStats } = await supabase
2407
2645
  .from('route_test_stats')
2408
2646
  .select('route, test_case_count')
2409
- .eq('project_id', PROJECT_ID);
2647
+ .eq('project_id', currentProjectId);
2410
2648
  // Get previous period data for comparison
2411
2649
  let previousTests = [];
2412
2650
  let previousBugs = [];
@@ -2415,7 +2653,7 @@ async function getQAHealth(args) {
2415
2653
  const { data: pt } = await supabase
2416
2654
  .from('test_assignments')
2417
2655
  .select('id, status')
2418
- .eq('project_id', PROJECT_ID)
2656
+ .eq('project_id', currentProjectId)
2419
2657
  .gte('completed_at', previousStart.toISOString())
2420
2658
  .lt('completed_at', periodStart.toISOString())
2421
2659
  .in('status', ['passed', 'failed']);
@@ -2423,7 +2661,7 @@ async function getQAHealth(args) {
2423
2661
  const { data: pb } = await supabase
2424
2662
  .from('reports')
2425
2663
  .select('id, severity')
2426
- .eq('project_id', PROJECT_ID)
2664
+ .eq('project_id', currentProjectId)
2427
2665
  .eq('report_type', 'bug')
2428
2666
  .gte('created_at', previousStart.toISOString())
2429
2667
  .lt('created_at', periodStart.toISOString());
@@ -2431,7 +2669,7 @@ async function getQAHealth(args) {
2431
2669
  const { data: pr } = await supabase
2432
2670
  .from('reports')
2433
2671
  .select('id')
2434
- .eq('project_id', PROJECT_ID)
2672
+ .eq('project_id', currentProjectId)
2435
2673
  .in('status', ['resolved', 'fixed', 'verified', 'closed'])
2436
2674
  .gte('resolved_at', previousStart.toISOString())
2437
2675
  .lt('resolved_at', periodStart.toISOString());
@@ -2585,7 +2823,7 @@ async function getQASessions(args) {
2585
2823
  findings_count, bugs_filed, created_at,
2586
2824
  tester:testers(id, name, email)
2587
2825
  `)
2588
- .eq('project_id', PROJECT_ID)
2826
+ .eq('project_id', currentProjectId)
2589
2827
  .order('started_at', { ascending: false })
2590
2828
  .limit(limit);
2591
2829
  if (status !== 'all') {
@@ -2635,12 +2873,12 @@ async function getQAAlerts(args) {
2635
2873
  const status = args.status || 'active';
2636
2874
  // Optionally refresh alerts
2637
2875
  if (args.refresh) {
2638
- await supabase.rpc('detect_all_alerts', { p_project_id: PROJECT_ID });
2876
+ await supabase.rpc('detect_all_alerts', { p_project_id: currentProjectId });
2639
2877
  }
2640
2878
  let query = supabase
2641
2879
  .from('qa_alerts')
2642
2880
  .select('*')
2643
- .eq('project_id', PROJECT_ID)
2881
+ .eq('project_id', currentProjectId)
2644
2882
  .order('severity', { ascending: true }) // critical first
2645
2883
  .order('created_at', { ascending: false });
2646
2884
  if (severity !== 'all') {
@@ -2693,7 +2931,7 @@ async function getDeploymentAnalysis(args) {
2693
2931
  .from('deployments')
2694
2932
  .select('*')
2695
2933
  .eq('id', args.deployment_id)
2696
- .eq('project_id', PROJECT_ID)
2934
+ .eq('project_id', currentProjectId)
2697
2935
  .single();
2698
2936
  if (error) {
2699
2937
  return { error: error.message };
@@ -2704,7 +2942,7 @@ async function getDeploymentAnalysis(args) {
2704
2942
  let query = supabase
2705
2943
  .from('deployments')
2706
2944
  .select('*')
2707
- .eq('project_id', PROJECT_ID)
2945
+ .eq('project_id', currentProjectId)
2708
2946
  .order('deployed_at', { ascending: false })
2709
2947
  .limit(limit);
2710
2948
  if (args.environment && args.environment !== 'all') {
@@ -2778,7 +3016,7 @@ async function analyzeCommitForTesting(args) {
2778
3016
  const { data: mappings } = await supabase
2779
3017
  .from('file_route_mapping')
2780
3018
  .select('file_pattern, route, feature, confidence')
2781
- .eq('project_id', PROJECT_ID);
3019
+ .eq('project_id', currentProjectId);
2782
3020
  const affectedRoutes = [];
2783
3021
  for (const mapping of mappings || []) {
2784
3022
  const matchedFiles = filesChanged.filter(file => {
@@ -2812,7 +3050,7 @@ async function analyzeCommitForTesting(args) {
2812
3050
  const { data: bugs } = await supabase
2813
3051
  .from('reports')
2814
3052
  .select('id, severity, description, route, created_at')
2815
- .eq('project_id', PROJECT_ID)
3053
+ .eq('project_id', currentProjectId)
2816
3054
  .eq('report_type', 'bug')
2817
3055
  .in('route', routes)
2818
3056
  .gte('created_at', new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString());
@@ -2844,7 +3082,7 @@ async function analyzeCommitForTesting(args) {
2844
3082
  // Optionally record as deployment
2845
3083
  if (args.record_deployment) {
2846
3084
  await supabase.rpc('record_deployment', {
2847
- p_project_id: PROJECT_ID,
3085
+ p_project_id: currentProjectId,
2848
3086
  p_environment: 'production',
2849
3087
  p_commit_sha: args.commit_sha || null,
2850
3088
  p_commit_message: args.commit_message || null,
@@ -2933,12 +3171,12 @@ async function analyzeChangesForTests(args) {
2933
3171
  const { data: existingTests } = await supabase
2934
3172
  .from('test_cases')
2935
3173
  .select('test_key, title, target_route, description')
2936
- .eq('project_id', PROJECT_ID);
3174
+ .eq('project_id', currentProjectId);
2937
3175
  // Get next test key
2938
3176
  const { data: lastTest } = await supabase
2939
3177
  .from('test_cases')
2940
3178
  .select('test_key')
2941
- .eq('project_id', PROJECT_ID)
3179
+ .eq('project_id', currentProjectId)
2942
3180
  .order('test_key', { ascending: false })
2943
3181
  .limit(1);
2944
3182
  const lastKey = lastTest?.[0]?.test_key || 'TC-000';
@@ -2950,7 +3188,7 @@ async function analyzeChangesForTests(args) {
2950
3188
  const { data: bugs } = await supabase
2951
3189
  .from('reports')
2952
3190
  .select('id, description, severity, app_context')
2953
- .eq('project_id', PROJECT_ID)
3191
+ .eq('project_id', currentProjectId)
2954
3192
  .eq('report_type', 'bug')
2955
3193
  .limit(50);
2956
3194
  relatedBugs = (bugs || []).filter(bug => {
@@ -3295,7 +3533,7 @@ async function createBugReport(args) {
3295
3533
  const { data: project } = await supabase
3296
3534
  .from('projects')
3297
3535
  .select('owner_id')
3298
- .eq('id', PROJECT_ID)
3536
+ .eq('id', currentProjectId)
3299
3537
  .single();
3300
3538
  if (project?.owner_id) {
3301
3539
  reporterId = project.owner_id;
@@ -3305,14 +3543,14 @@ async function createBugReport(args) {
3305
3543
  const { data: testers } = await supabase
3306
3544
  .from('testers')
3307
3545
  .select('id')
3308
- .eq('project_id', PROJECT_ID)
3546
+ .eq('project_id', currentProjectId)
3309
3547
  .limit(1);
3310
3548
  if (testers && testers.length > 0) {
3311
3549
  reporterId = testers[0].id;
3312
3550
  }
3313
3551
  }
3314
3552
  const report = {
3315
- project_id: PROJECT_ID,
3553
+ project_id: currentProjectId,
3316
3554
  report_type: 'bug',
3317
3555
  title: args.title,
3318
3556
  description: args.description,
@@ -3376,7 +3614,7 @@ async function getBugsForFile(args) {
3376
3614
  let query = supabase
3377
3615
  .from('reports')
3378
3616
  .select('id, title, description, severity, status, created_at, code_context')
3379
- .eq('project_id', PROJECT_ID)
3617
+ .eq('project_id', currentProjectId)
3380
3618
  .eq('report_type', 'bug');
3381
3619
  if (!args.include_resolved) {
3382
3620
  query = query.in('status', ['new', 'confirmed', 'in_progress', 'reviewed']);
@@ -3442,7 +3680,7 @@ async function markFixedWithCommit(args) {
3442
3680
  .from('reports')
3443
3681
  .select('code_context')
3444
3682
  .eq('id', args.report_id)
3445
- .eq('project_id', PROJECT_ID) // Security: ensure report belongs to this project
3683
+ .eq('project_id', currentProjectId) // Security: ensure report belongs to this project
3446
3684
  .single();
3447
3685
  if (fetchError) {
3448
3686
  return { error: fetchError.message };
@@ -3468,7 +3706,7 @@ async function markFixedWithCommit(args) {
3468
3706
  .from('reports')
3469
3707
  .update(updates)
3470
3708
  .eq('id', args.report_id)
3471
- .eq('project_id', PROJECT_ID); // Security: ensure report belongs to this project
3709
+ .eq('project_id', currentProjectId); // Security: ensure report belongs to this project
3472
3710
  if (error) {
3473
3711
  return { error: error.message };
3474
3712
  }
@@ -3492,7 +3730,7 @@ async function getBugsAffectingCode(args) {
3492
3730
  const { data, error } = await supabase
3493
3731
  .from('reports')
3494
3732
  .select('id, title, description, severity, status, code_context, app_context')
3495
- .eq('project_id', PROJECT_ID)
3733
+ .eq('project_id', currentProjectId)
3496
3734
  .eq('report_type', 'bug')
3497
3735
  .in('status', ['new', 'confirmed', 'in_progress', 'reviewed'])
3498
3736
  .order('severity', { ascending: true });
@@ -3597,7 +3835,7 @@ async function linkBugToCode(args) {
3597
3835
  .from('reports')
3598
3836
  .select('code_context')
3599
3837
  .eq('id', args.report_id)
3600
- .eq('project_id', PROJECT_ID) // Security: ensure report belongs to this project
3838
+ .eq('project_id', currentProjectId) // Security: ensure report belongs to this project
3601
3839
  .single();
3602
3840
  if (fetchError) {
3603
3841
  return { error: fetchError.message };
@@ -3618,7 +3856,7 @@ async function linkBugToCode(args) {
3618
3856
  .from('reports')
3619
3857
  .update(updates)
3620
3858
  .eq('id', args.report_id)
3621
- .eq('project_id', PROJECT_ID); // Security: ensure report belongs to this project
3859
+ .eq('project_id', currentProjectId); // Security: ensure report belongs to this project
3622
3860
  if (error) {
3623
3861
  return { error: error.message };
3624
3862
  }
@@ -3637,7 +3875,7 @@ async function createRegressionTest(args) {
3637
3875
  .from('reports')
3638
3876
  .select('*')
3639
3877
  .eq('id', args.report_id)
3640
- .eq('project_id', PROJECT_ID) // Security: ensure report belongs to this project
3878
+ .eq('project_id', currentProjectId) // Security: ensure report belongs to this project
3641
3879
  .single();
3642
3880
  if (fetchError) {
3643
3881
  return { error: fetchError.message };
@@ -3654,7 +3892,7 @@ async function createRegressionTest(args) {
3654
3892
  const { data: existingTests } = await supabase
3655
3893
  .from('test_cases')
3656
3894
  .select('test_key')
3657
- .eq('project_id', PROJECT_ID)
3895
+ .eq('project_id', currentProjectId)
3658
3896
  .order('test_key', { ascending: false })
3659
3897
  .limit(1);
3660
3898
  const lastKey = existingTests?.[0]?.test_key || 'TC-000';
@@ -3665,7 +3903,7 @@ async function createRegressionTest(args) {
3665
3903
  const targetRoute = appContext?.currentRoute;
3666
3904
  // Generate test case from bug
3667
3905
  const testCase = {
3668
- project_id: PROJECT_ID,
3906
+ project_id: currentProjectId,
3669
3907
  test_key: newKey,
3670
3908
  title: `Regression: ${report.title}`,
3671
3909
  description: `Regression test to prevent recurrence of bug #${args.report_id.slice(0, 8)}\n\nOriginal bug: ${report.description}`,
@@ -3741,7 +3979,7 @@ async function getPendingFixes(args) {
3741
3979
  created_at,
3742
3980
  report:reports(id, title, severity, description)
3743
3981
  `)
3744
- .eq('project_id', PROJECT_ID)
3982
+ .eq('project_id', currentProjectId)
3745
3983
  .order('created_at', { ascending: true })
3746
3984
  .limit(limit);
3747
3985
  if (!args.include_claimed) {
@@ -3791,7 +4029,7 @@ async function claimFixRequest(args) {
3791
4029
  .from('fix_requests')
3792
4030
  .select('id, status, claimed_by, prompt, title')
3793
4031
  .eq('id', args.fix_request_id)
3794
- .eq('project_id', PROJECT_ID) // Security: ensure fix request belongs to this project
4032
+ .eq('project_id', currentProjectId) // Security: ensure fix request belongs to this project
3795
4033
  .single();
3796
4034
  if (checkError) {
3797
4035
  return { error: checkError.message };
@@ -3818,7 +4056,7 @@ async function claimFixRequest(args) {
3818
4056
  claimed_by: claimedBy,
3819
4057
  })
3820
4058
  .eq('id', args.fix_request_id)
3821
- .eq('project_id', PROJECT_ID) // Security: ensure fix request belongs to this project
4059
+ .eq('project_id', currentProjectId) // Security: ensure fix request belongs to this project
3822
4060
  .eq('status', 'pending'); // Only claim if still pending (race condition protection)
3823
4061
  if (updateError) {
3824
4062
  return { error: updateError.message };
@@ -3853,7 +4091,7 @@ async function completeFixRequest(args) {
3853
4091
  .from('fix_requests')
3854
4092
  .update(updates)
3855
4093
  .eq('id', args.fix_request_id)
3856
- .eq('project_id', PROJECT_ID); // Security: ensure fix request belongs to this project
4094
+ .eq('project_id', currentProjectId); // Security: ensure fix request belongs to this project
3857
4095
  if (error) {
3858
4096
  return { error: error.message };
3859
4097
  }
@@ -3936,7 +4174,7 @@ async function generatePromptContent(name, args) {
3936
4174
  created_at,
3937
4175
  report:reports(id, title, severity)
3938
4176
  `)
3939
- .eq('project_id', PROJECT_ID)
4177
+ .eq('project_id', currentProjectId)
3940
4178
  .eq('status', 'pending')
3941
4179
  .order('created_at', { ascending: true })
3942
4180
  .limit(5);
@@ -3944,7 +4182,7 @@ async function generatePromptContent(name, args) {
3944
4182
  let query = supabase
3945
4183
  .from('reports')
3946
4184
  .select('id, title, description, severity, status, code_context, created_at')
3947
- .eq('project_id', PROJECT_ID)
4185
+ .eq('project_id', currentProjectId)
3948
4186
  .eq('report_type', 'bug')
3949
4187
  .in('status', ['new', 'confirmed', 'in_progress']);
3950
4188
  if (severity !== 'all') {
@@ -4094,7 +4332,7 @@ Would you like me to generate test cases for these files?`;
4094
4332
  const { data: resolvedBugs } = await supabase
4095
4333
  .from('reports')
4096
4334
  .select('id, title, description, severity, resolved_at, code_context')
4097
- .eq('project_id', PROJECT_ID)
4335
+ .eq('project_id', currentProjectId)
4098
4336
  .eq('report_type', 'bug')
4099
4337
  .eq('status', 'resolved')
4100
4338
  .order('resolved_at', { ascending: false })
@@ -4206,7 +4444,7 @@ async function listTesters(args) {
4206
4444
  let query = supabase
4207
4445
  .from('testers')
4208
4446
  .select('id, name, email, status, platforms, tier, assigned_count, completed_count, notes, created_at')
4209
- .eq('project_id', PROJECT_ID)
4447
+ .eq('project_id', currentProjectId)
4210
4448
  .order('name', { ascending: true });
4211
4449
  if (args.status) {
4212
4450
  query = query.eq('status', args.status);
@@ -4240,7 +4478,7 @@ async function listTestRuns(args) {
4240
4478
  let query = supabase
4241
4479
  .from('test_runs')
4242
4480
  .select('id, name, description, status, total_tests, passed_tests, failed_tests, started_at, completed_at, created_at')
4243
- .eq('project_id', PROJECT_ID)
4481
+ .eq('project_id', currentProjectId)
4244
4482
  .order('created_at', { ascending: false })
4245
4483
  .limit(limit);
4246
4484
  if (args.status) {
@@ -4274,7 +4512,7 @@ async function createTestRun(args) {
4274
4512
  const { data, error } = await supabase
4275
4513
  .from('test_runs')
4276
4514
  .insert({
4277
- project_id: PROJECT_ID,
4515
+ project_id: currentProjectId,
4278
4516
  name: args.name.trim(),
4279
4517
  description: args.description?.trim() || null,
4280
4518
  status: 'draft',
@@ -4319,7 +4557,7 @@ async function listTestAssignments(args) {
4319
4557
  tester:testers(id, name, email),
4320
4558
  test_run:test_runs(id, name)
4321
4559
  `)
4322
- .eq('project_id', PROJECT_ID)
4560
+ .eq('project_id', currentProjectId)
4323
4561
  .order('assigned_at', { ascending: false })
4324
4562
  .limit(limit);
4325
4563
  if (args.tester_id) {
@@ -4389,7 +4627,7 @@ async function assignTests(args) {
4389
4627
  .from('testers')
4390
4628
  .select('id, name, email, status')
4391
4629
  .eq('id', args.tester_id)
4392
- .eq('project_id', PROJECT_ID)
4630
+ .eq('project_id', currentProjectId)
4393
4631
  .single();
4394
4632
  if (testerErr || !tester) {
4395
4633
  return { error: 'Tester not found in this project' };
@@ -4401,7 +4639,7 @@ async function assignTests(args) {
4401
4639
  const { data: testCases, error: tcErr } = await supabase
4402
4640
  .from('test_cases')
4403
4641
  .select('id, test_key, title')
4404
- .eq('project_id', PROJECT_ID)
4642
+ .eq('project_id', currentProjectId)
4405
4643
  .in('id', args.test_case_ids);
4406
4644
  if (tcErr) {
4407
4645
  return { error: tcErr.message };
@@ -4419,7 +4657,7 @@ async function assignTests(args) {
4419
4657
  .from('test_runs')
4420
4658
  .select('id')
4421
4659
  .eq('id', args.test_run_id)
4422
- .eq('project_id', PROJECT_ID)
4660
+ .eq('project_id', currentProjectId)
4423
4661
  .single();
4424
4662
  if (runErr || !run) {
4425
4663
  return { error: 'Test run not found in this project' };
@@ -4427,7 +4665,7 @@ async function assignTests(args) {
4427
4665
  }
4428
4666
  // Build assignment rows
4429
4667
  const rows = args.test_case_ids.map(tcId => ({
4430
- project_id: PROJECT_ID,
4668
+ project_id: currentProjectId,
4431
4669
  test_case_id: tcId,
4432
4670
  tester_id: args.tester_id,
4433
4671
  test_run_id: args.test_run_id || null,
@@ -4487,7 +4725,7 @@ async function getTesterWorkload(args) {
4487
4725
  .from('testers')
4488
4726
  .select('id, name, email, status, platforms, tier')
4489
4727
  .eq('id', args.tester_id)
4490
- .eq('project_id', PROJECT_ID)
4728
+ .eq('project_id', currentProjectId)
4491
4729
  .single();
4492
4730
  if (testerErr || !tester) {
4493
4731
  return { error: 'Tester not found in this project' };
@@ -4503,7 +4741,7 @@ async function getTesterWorkload(args) {
4503
4741
  test_case:test_cases(test_key, title, priority),
4504
4742
  test_run:test_runs(name)
4505
4743
  `)
4506
- .eq('project_id', PROJECT_ID)
4744
+ .eq('project_id', currentProjectId)
4507
4745
  .eq('tester_id', args.tester_id)
4508
4746
  .order('assigned_at', { ascending: false });
4509
4747
  if (assignErr) {
@@ -4548,6 +4786,435 @@ async function getTesterWorkload(args) {
4548
4786
  })),
4549
4787
  };
4550
4788
  }
4789
+ // === NEW TESTER & ANALYTICS HANDLERS ===
4790
+ async function createTester(args) {
4791
+ if (!args.name || args.name.trim().length === 0) {
4792
+ return { error: 'Tester name is required' };
4793
+ }
4794
+ if (!args.email || !args.email.includes('@')) {
4795
+ return { error: 'A valid email address is required' };
4796
+ }
4797
+ if (args.tier !== undefined && (args.tier < 1 || args.tier > 3)) {
4798
+ return { error: 'Tier must be 1, 2, or 3' };
4799
+ }
4800
+ const validPlatforms = ['ios', 'android', 'web'];
4801
+ if (args.platforms) {
4802
+ for (const p of args.platforms) {
4803
+ if (!validPlatforms.includes(p)) {
4804
+ return { error: `Invalid platform "${p}". Must be one of: ${validPlatforms.join(', ')}` };
4805
+ }
4806
+ }
4807
+ }
4808
+ const { data, error } = await supabase
4809
+ .from('testers')
4810
+ .insert({
4811
+ project_id: currentProjectId,
4812
+ name: args.name.trim(),
4813
+ email: args.email.trim().toLowerCase(),
4814
+ platforms: args.platforms || ['ios', 'web'],
4815
+ tier: args.tier ?? 1,
4816
+ notes: args.notes?.trim() || null,
4817
+ status: 'active',
4818
+ })
4819
+ .select('id, name, email, status, platforms, tier, notes, created_at')
4820
+ .single();
4821
+ if (error) {
4822
+ if (error.message.includes('duplicate') || error.message.includes('unique')) {
4823
+ return { error: `A tester with email "${args.email}" already exists in this project` };
4824
+ }
4825
+ return { error: error.message };
4826
+ }
4827
+ return {
4828
+ success: true,
4829
+ tester: {
4830
+ id: data.id,
4831
+ name: data.name,
4832
+ email: data.email,
4833
+ status: data.status,
4834
+ platforms: data.platforms,
4835
+ tier: data.tier,
4836
+ notes: data.notes,
4837
+ createdAt: data.created_at,
4838
+ },
4839
+ message: `Tester "${data.name}" added to the project. Use assign_tests to give them test cases.`,
4840
+ };
4841
+ }
4842
+ async function updateTester(args) {
4843
+ if (!isValidUUID(args.tester_id)) {
4844
+ return { error: 'Invalid tester_id format' };
4845
+ }
4846
+ const updates = {};
4847
+ if (args.status)
4848
+ updates.status = args.status;
4849
+ if (args.platforms)
4850
+ updates.platforms = args.platforms;
4851
+ if (args.tier !== undefined) {
4852
+ if (args.tier < 1 || args.tier > 3) {
4853
+ return { error: 'Tier must be 1, 2, or 3' };
4854
+ }
4855
+ updates.tier = args.tier;
4856
+ }
4857
+ if (args.notes !== undefined)
4858
+ updates.notes = args.notes.trim() || null;
4859
+ if (args.name)
4860
+ updates.name = args.name.trim();
4861
+ if (Object.keys(updates).length === 0) {
4862
+ return { error: 'No fields to update. Provide at least one of: status, platforms, tier, notes, name' };
4863
+ }
4864
+ const { data, error } = await supabase
4865
+ .from('testers')
4866
+ .update(updates)
4867
+ .eq('id', args.tester_id)
4868
+ .eq('project_id', currentProjectId)
4869
+ .select('id, name, email, status, platforms, tier, notes')
4870
+ .single();
4871
+ if (error) {
4872
+ return { error: error.message };
4873
+ }
4874
+ if (!data) {
4875
+ return { error: 'Tester not found in this project' };
4876
+ }
4877
+ return {
4878
+ success: true,
4879
+ tester: {
4880
+ id: data.id,
4881
+ name: data.name,
4882
+ email: data.email,
4883
+ status: data.status,
4884
+ platforms: data.platforms,
4885
+ tier: data.tier,
4886
+ notes: data.notes,
4887
+ },
4888
+ updatedFields: Object.keys(updates),
4889
+ };
4890
+ }
4891
+ async function bulkUpdateReports(args) {
4892
+ if (!args.report_ids || args.report_ids.length === 0) {
4893
+ return { error: 'At least one report_id is required' };
4894
+ }
4895
+ if (args.report_ids.length > 50) {
4896
+ return { error: 'Maximum 50 reports per bulk update' };
4897
+ }
4898
+ for (const id of args.report_ids) {
4899
+ if (!isValidUUID(id)) {
4900
+ return { error: `Invalid report_id format: ${id}` };
4901
+ }
4902
+ }
4903
+ const updates = { status: args.status };
4904
+ if (args.resolution_notes) {
4905
+ updates.resolution_notes = args.resolution_notes;
4906
+ }
4907
+ // Set resolved_at timestamp for terminal statuses
4908
+ if (['fixed', 'resolved', 'verified', 'wont_fix', 'duplicate', 'closed'].includes(args.status)) {
4909
+ updates.resolved_at = new Date().toISOString();
4910
+ }
4911
+ const { data, error } = await supabase
4912
+ .from('reports')
4913
+ .update(updates)
4914
+ .eq('project_id', currentProjectId)
4915
+ .in('id', args.report_ids)
4916
+ .select('id, status, description');
4917
+ if (error) {
4918
+ return { error: error.message };
4919
+ }
4920
+ const updated = data || [];
4921
+ const updatedIds = new Set(updated.map((r) => r.id));
4922
+ const notFound = args.report_ids.filter(id => !updatedIds.has(id));
4923
+ return {
4924
+ success: true,
4925
+ updatedCount: updated.length,
4926
+ requestedCount: args.report_ids.length,
4927
+ notFound: notFound.length > 0 ? notFound : undefined,
4928
+ status: args.status,
4929
+ reports: updated.map((r) => ({
4930
+ id: r.id,
4931
+ status: r.status,
4932
+ description: r.description?.slice(0, 80),
4933
+ })),
4934
+ message: `Updated ${updated.length} report(s) to "${args.status}".${notFound.length > 0 ? ` ${notFound.length} report(s) not found.` : ''}`,
4935
+ };
4936
+ }
4937
+ async function getBugTrends(args) {
4938
+ const days = Math.min(args.days || 30, 180);
4939
+ const groupBy = args.group_by || 'week';
4940
+ const since = new Date(Date.now() - days * 86400000).toISOString();
4941
+ const { data, error } = await supabase
4942
+ .from('reports')
4943
+ .select('id, severity, category, status, report_type, created_at')
4944
+ .eq('project_id', currentProjectId)
4945
+ .gte('created_at', since)
4946
+ .order('created_at', { ascending: true });
4947
+ if (error) {
4948
+ return { error: error.message };
4949
+ }
4950
+ const reports = data || [];
4951
+ if (groupBy === 'week') {
4952
+ const weeks = {};
4953
+ for (const r of reports) {
4954
+ const d = new Date(r.created_at);
4955
+ // Get Monday of that week
4956
+ const day = d.getDay();
4957
+ const diff = d.getDate() - day + (day === 0 ? -6 : 1);
4958
+ const monday = new Date(d.setDate(diff));
4959
+ const weekKey = monday.toISOString().slice(0, 10);
4960
+ if (!weeks[weekKey])
4961
+ weeks[weekKey] = { count: 0, critical: 0, high: 0, medium: 0, low: 0 };
4962
+ weeks[weekKey].count++;
4963
+ const sev = (r.severity || 'low');
4964
+ weeks[weekKey][sev]++;
4965
+ }
4966
+ return {
4967
+ period: `${days} days`,
4968
+ groupBy: 'week',
4969
+ totalReports: reports.length,
4970
+ weeks: Object.entries(weeks).map(([week, data]) => ({ week, ...data })),
4971
+ };
4972
+ }
4973
+ if (groupBy === 'severity') {
4974
+ const groups = { critical: 0, high: 0, medium: 0, low: 0 };
4975
+ for (const r of reports)
4976
+ groups[r.severity || 'low']++;
4977
+ return { period: `${days} days`, groupBy: 'severity', totalReports: reports.length, breakdown: groups };
4978
+ }
4979
+ if (groupBy === 'category') {
4980
+ const groups = {};
4981
+ for (const r of reports) {
4982
+ const cat = r.category || 'uncategorized';
4983
+ groups[cat] = (groups[cat] || 0) + 1;
4984
+ }
4985
+ return { period: `${days} days`, groupBy: 'category', totalReports: reports.length, breakdown: groups };
4986
+ }
4987
+ if (groupBy === 'status') {
4988
+ const groups = {};
4989
+ for (const r of reports) {
4990
+ groups[r.status] = (groups[r.status] || 0) + 1;
4991
+ }
4992
+ return { period: `${days} days`, groupBy: 'status', totalReports: reports.length, breakdown: groups };
4993
+ }
4994
+ return { error: `Invalid group_by: ${groupBy}. Must be one of: week, severity, category, status` };
4995
+ }
4996
+ async function getTesterLeaderboard(args) {
4997
+ const days = Math.min(args.days || 30, 180);
4998
+ const sortBy = args.sort_by || 'tests_completed';
4999
+ const since = new Date(Date.now() - days * 86400000).toISOString();
5000
+ // Get all testers for the project
5001
+ const { data: testers, error: testerErr } = await supabase
5002
+ .from('testers')
5003
+ .select('id, name, email, status, platforms, tier')
5004
+ .eq('project_id', currentProjectId)
5005
+ .eq('status', 'active');
5006
+ if (testerErr)
5007
+ return { error: testerErr.message };
5008
+ // Get completed assignments in the period
5009
+ const { data: assignments, error: assignErr } = await supabase
5010
+ .from('test_assignments')
5011
+ .select('tester_id, status, completed_at, duration_seconds')
5012
+ .eq('project_id', currentProjectId)
5013
+ .gte('completed_at', since)
5014
+ .in('status', ['passed', 'failed']);
5015
+ if (assignErr)
5016
+ return { error: assignErr.message };
5017
+ // Get bugs filed in the period
5018
+ const { data: bugs, error: bugErr } = await supabase
5019
+ .from('reports')
5020
+ .select('tester_id, severity')
5021
+ .eq('project_id', currentProjectId)
5022
+ .gte('created_at', since)
5023
+ .not('tester_id', 'is', null);
5024
+ if (bugErr)
5025
+ return { error: bugErr.message };
5026
+ // Aggregate per tester
5027
+ const testerMap = new Map();
5028
+ for (const t of testers || []) {
5029
+ testerMap.set(t.id, {
5030
+ id: t.id,
5031
+ name: t.name,
5032
+ email: t.email,
5033
+ tier: t.tier,
5034
+ testsCompleted: 0,
5035
+ testsPassed: 0,
5036
+ testsFailed: 0,
5037
+ bugsFound: 0,
5038
+ criticalBugs: 0,
5039
+ avgDurationSeconds: 0,
5040
+ totalDuration: 0,
5041
+ });
5042
+ }
5043
+ for (const a of assignments || []) {
5044
+ const entry = testerMap.get(a.tester_id);
5045
+ if (!entry)
5046
+ continue;
5047
+ entry.testsCompleted++;
5048
+ if (a.status === 'passed')
5049
+ entry.testsPassed++;
5050
+ if (a.status === 'failed')
5051
+ entry.testsFailed++;
5052
+ if (a.duration_seconds)
5053
+ entry.totalDuration += a.duration_seconds;
5054
+ }
5055
+ for (const b of bugs || []) {
5056
+ const entry = testerMap.get(b.tester_id);
5057
+ if (!entry)
5058
+ continue;
5059
+ entry.bugsFound++;
5060
+ if (b.severity === 'critical')
5061
+ entry.criticalBugs++;
5062
+ }
5063
+ let leaderboard = Array.from(testerMap.values()).map(t => ({
5064
+ ...t,
5065
+ passRate: t.testsCompleted > 0 ? Math.round((t.testsPassed / t.testsCompleted) * 100) : 0,
5066
+ avgDurationSeconds: t.testsCompleted > 0 ? Math.round(t.totalDuration / t.testsCompleted) : 0,
5067
+ totalDuration: undefined,
5068
+ }));
5069
+ // Sort
5070
+ if (sortBy === 'bugs_found') {
5071
+ leaderboard.sort((a, b) => b.bugsFound - a.bugsFound);
5072
+ }
5073
+ else if (sortBy === 'pass_rate') {
5074
+ leaderboard.sort((a, b) => b.passRate - a.passRate);
5075
+ }
5076
+ else {
5077
+ leaderboard.sort((a, b) => b.testsCompleted - a.testsCompleted);
5078
+ }
5079
+ return {
5080
+ period: `${days} days`,
5081
+ sortedBy: sortBy,
5082
+ leaderboard,
5083
+ };
5084
+ }
5085
+ async function exportTestResults(args) {
5086
+ if (!isValidUUID(args.test_run_id)) {
5087
+ return { error: 'Invalid test_run_id format' };
5088
+ }
5089
+ // Get the test run
5090
+ const { data: run, error: runErr } = await supabase
5091
+ .from('test_runs')
5092
+ .select('id, name, description, status, total_tests, passed_tests, failed_tests, started_at, completed_at, created_at')
5093
+ .eq('id', args.test_run_id)
5094
+ .eq('project_id', currentProjectId)
5095
+ .single();
5096
+ if (runErr || !run) {
5097
+ return { error: 'Test run not found in this project' };
5098
+ }
5099
+ // Get all assignments for this run
5100
+ const { data: assignments, error: assignErr } = await supabase
5101
+ .from('test_assignments')
5102
+ .select(`
5103
+ id,
5104
+ status,
5105
+ assigned_at,
5106
+ started_at,
5107
+ completed_at,
5108
+ duration_seconds,
5109
+ is_verification,
5110
+ notes,
5111
+ skip_reason,
5112
+ test_result,
5113
+ feedback_rating,
5114
+ feedback_note,
5115
+ test_case:test_cases(id, test_key, title, priority, description, target_route),
5116
+ tester:testers(id, name, email)
5117
+ `)
5118
+ .eq('test_run_id', args.test_run_id)
5119
+ .eq('project_id', currentProjectId)
5120
+ .order('assigned_at', { ascending: true });
5121
+ if (assignErr) {
5122
+ return { error: assignErr.message };
5123
+ }
5124
+ const all = assignments || [];
5125
+ const passCount = all.filter(a => a.status === 'passed').length;
5126
+ const failCount = all.filter(a => a.status === 'failed').length;
5127
+ return {
5128
+ testRun: {
5129
+ id: run.id,
5130
+ name: run.name,
5131
+ description: run.description,
5132
+ status: run.status,
5133
+ startedAt: run.started_at,
5134
+ completedAt: run.completed_at,
5135
+ createdAt: run.created_at,
5136
+ },
5137
+ summary: {
5138
+ totalAssignments: all.length,
5139
+ passed: passCount,
5140
+ failed: failCount,
5141
+ blocked: all.filter(a => a.status === 'blocked').length,
5142
+ skipped: all.filter(a => a.status === 'skipped').length,
5143
+ pending: all.filter(a => a.status === 'pending').length,
5144
+ inProgress: all.filter(a => a.status === 'in_progress').length,
5145
+ passRate: all.length > 0 ? Math.round((passCount / all.length) * 100) : 0,
5146
+ },
5147
+ assignments: all.map((a) => ({
5148
+ id: a.id,
5149
+ status: a.status,
5150
+ assignedAt: a.assigned_at,
5151
+ startedAt: a.started_at,
5152
+ completedAt: a.completed_at,
5153
+ durationSeconds: a.duration_seconds,
5154
+ isVerification: a.is_verification,
5155
+ notes: a.notes,
5156
+ skipReason: a.skip_reason,
5157
+ testResult: a.test_result,
5158
+ feedbackRating: a.feedback_rating,
5159
+ feedbackNote: a.feedback_note,
5160
+ testCase: a.test_case ? {
5161
+ id: a.test_case.id,
5162
+ testKey: a.test_case.test_key,
5163
+ title: a.test_case.title,
5164
+ priority: a.test_case.priority,
5165
+ description: a.test_case.description,
5166
+ targetRoute: a.test_case.target_route,
5167
+ } : null,
5168
+ tester: a.tester ? {
5169
+ id: a.tester.id,
5170
+ name: a.tester.name,
5171
+ email: a.tester.email,
5172
+ } : null,
5173
+ })),
5174
+ };
5175
+ }
5176
+ async function getTestingVelocity(args) {
5177
+ const days = Math.min(args.days || 14, 90);
5178
+ const since = new Date(Date.now() - days * 86400000).toISOString();
5179
+ const { data, error } = await supabase
5180
+ .from('test_assignments')
5181
+ .select('completed_at, status')
5182
+ .eq('project_id', currentProjectId)
5183
+ .gte('completed_at', since)
5184
+ .in('status', ['passed', 'failed'])
5185
+ .order('completed_at', { ascending: true });
5186
+ if (error) {
5187
+ return { error: error.message };
5188
+ }
5189
+ const completions = data || [];
5190
+ // Group by day
5191
+ const dailyCounts = {};
5192
+ for (let i = 0; i < days; i++) {
5193
+ const d = new Date(Date.now() - (days - 1 - i) * 86400000);
5194
+ dailyCounts[d.toISOString().slice(0, 10)] = 0;
5195
+ }
5196
+ for (const c of completions) {
5197
+ const day = new Date(c.completed_at).toISOString().slice(0, 10);
5198
+ if (dailyCounts[day] !== undefined) {
5199
+ dailyCounts[day]++;
5200
+ }
5201
+ }
5202
+ const dailyArray = Object.entries(dailyCounts).map(([date, count]) => ({ date, count }));
5203
+ const totalCompleted = completions.length;
5204
+ const avgPerDay = days > 0 ? Math.round((totalCompleted / days) * 10) / 10 : 0;
5205
+ // Trend: compare first half to second half
5206
+ const mid = Math.floor(dailyArray.length / 2);
5207
+ const firstHalf = dailyArray.slice(0, mid).reduce((sum, d) => sum + d.count, 0);
5208
+ const secondHalf = dailyArray.slice(mid).reduce((sum, d) => sum + d.count, 0);
5209
+ const trend = secondHalf > firstHalf ? 'increasing' : secondHalf < firstHalf ? 'decreasing' : 'stable';
5210
+ return {
5211
+ period: `${days} days`,
5212
+ totalCompleted,
5213
+ averagePerDay: avgPerDay,
5214
+ trend,
5215
+ daily: dailyArray,
5216
+ };
5217
+ }
4551
5218
  // Main server setup
4552
5219
  async function main() {
4553
5220
  initSupabase();
@@ -4570,6 +5237,11 @@ async function main() {
4570
5237
  const { name, arguments: args } = request.params;
4571
5238
  try {
4572
5239
  let result;
5240
+ // Project management tools don't require a project to be selected
5241
+ const projectFreeTools = ['list_projects', 'switch_project', 'get_current_project'];
5242
+ if (!projectFreeTools.includes(name)) {
5243
+ requireProject();
5244
+ }
4573
5245
  switch (name) {
4574
5246
  case 'list_reports':
4575
5247
  result = await listReports(args);
@@ -4702,6 +5374,38 @@ async function main() {
4702
5374
  case 'get_tester_workload':
4703
5375
  result = await getTesterWorkload(args);
4704
5376
  break;
5377
+ // === NEW TESTER & ANALYTICS TOOLS ===
5378
+ case 'create_tester':
5379
+ result = await createTester(args);
5380
+ break;
5381
+ case 'update_tester':
5382
+ result = await updateTester(args);
5383
+ break;
5384
+ case 'bulk_update_reports':
5385
+ result = await bulkUpdateReports(args);
5386
+ break;
5387
+ case 'get_bug_trends':
5388
+ result = await getBugTrends(args);
5389
+ break;
5390
+ case 'get_tester_leaderboard':
5391
+ result = await getTesterLeaderboard(args);
5392
+ break;
5393
+ case 'export_test_results':
5394
+ result = await exportTestResults(args);
5395
+ break;
5396
+ case 'get_testing_velocity':
5397
+ result = await getTestingVelocity(args);
5398
+ break;
5399
+ // === PROJECT MANAGEMENT ===
5400
+ case 'list_projects':
5401
+ result = await listProjects();
5402
+ break;
5403
+ case 'switch_project':
5404
+ result = await switchProject(args);
5405
+ break;
5406
+ case 'get_current_project':
5407
+ result = getCurrentProject();
5408
+ break;
4705
5409
  default:
4706
5410
  return {
4707
5411
  content: [{ type: 'text', text: `Unknown tool: ${name}` }],
@@ -4724,7 +5428,7 @@ async function main() {
4724
5428
  const { data, error } = await supabase
4725
5429
  .from('reports')
4726
5430
  .select('id, description, report_type, severity')
4727
- .eq('project_id', PROJECT_ID)
5431
+ .eq('project_id', currentProjectId)
4728
5432
  .eq('status', 'new')
4729
5433
  .order('created_at', { ascending: false })
4730
5434
  .limit(10);