@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/src/index.ts CHANGED
@@ -19,32 +19,36 @@ import { createClient, SupabaseClient } from '@supabase/supabase-js';
19
19
  // Configuration from environment
20
20
  const SUPABASE_URL = process.env.SUPABASE_URL || 'https://kyxgzjnqgvapvlnvqawz.supabase.co';
21
21
  const SUPABASE_ANON_KEY = process.env.SUPABASE_ANON_KEY || '';
22
- const PROJECT_ID = process.env.BUGBEAR_PROJECT_ID || '';
22
+
23
+ // Active project — set from env var, switchable via switch_project tool
24
+ let currentProjectId = process.env.BUGBEAR_currentProjectId || '';
23
25
 
24
26
  // Initialize Supabase client
25
27
  let supabase: SupabaseClient;
26
28
 
27
29
  function validateConfig() {
28
- const errors: string[] = [];
29
-
30
30
  if (!SUPABASE_ANON_KEY) {
31
- errors.push('SUPABASE_ANON_KEY environment variable is required');
31
+ console.error('BugBear MCP Server: SUPABASE_ANON_KEY environment variable is required');
32
+ process.exit(1);
32
33
  }
33
34
 
34
- if (!PROJECT_ID) {
35
- errors.push('BUGBEAR_PROJECT_ID environment variable is required');
35
+ // Validate project ID format if provided
36
+ 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)) {
37
+ console.error('BugBear MCP Server: BUGBEAR_currentProjectId must be a valid UUID');
38
+ process.exit(1);
36
39
  }
37
40
 
38
- // Basic UUID format validation for project ID
39
- 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)) {
40
- errors.push('BUGBEAR_PROJECT_ID must be a valid UUID');
41
+ if (!currentProjectId) {
42
+ console.error('BugBear MCP Server: No BUGBEAR_currentProjectId set. Use list_projects + switch_project to select one.');
41
43
  }
44
+ }
42
45
 
43
- if (errors.length > 0) {
44
- console.error('BugBear MCP Server configuration errors:');
45
- errors.forEach(e => console.error(` - ${e}`));
46
- process.exit(1);
46
+ /** Guard for tools that require a project to be selected */
47
+ function requireProject(): string {
48
+ if (!currentProjectId) {
49
+ throw new Error('No project selected. Use list_projects to see available projects, then switch_project to select one.');
47
50
  }
51
+ return currentProjectId;
48
52
  }
49
53
 
50
54
  function initSupabase() {
@@ -141,7 +145,7 @@ const tools = [
141
145
  },
142
146
  status: {
143
147
  type: 'string',
144
- enum: ['new', 'triaging', 'confirmed', 'in_progress', 'fixed', 'resolved', 'verified', 'wont_fix', 'duplicate'],
148
+ enum: ['new', 'triaging', 'confirmed', 'in_progress', 'fixed', 'ready_to_test', 'verified', 'resolved', 'reviewed', 'closed', 'wont_fix', 'duplicate'],
145
149
  description: 'The new status for the report',
146
150
  },
147
151
  resolution_notes: {
@@ -154,7 +158,47 @@ const tools = [
154
158
  },
155
159
  {
156
160
  name: 'get_report_context',
157
- description: 'Get the full debugging context for a report including console logs, network requests, and navigation history',
161
+ description: 'Get the full debugging context for a report including console logs, network requests, and navigation history. Use compact=true for app_context summary only (no console/network/navigation).',
162
+ inputSchema: {
163
+ type: 'object' as const,
164
+ properties: {
165
+ report_id: {
166
+ type: 'string',
167
+ description: 'The UUID of the report',
168
+ },
169
+ compact: {
170
+ type: 'boolean',
171
+ description: 'Compact mode: returns app_context only, skips console logs, network requests, and navigation history. (default: false)',
172
+ },
173
+ },
174
+ required: ['report_id'],
175
+ },
176
+ },
177
+ {
178
+ name: 'add_report_comment',
179
+ description: 'Add a comment/note to a bug report thread without changing its status. Use this for follow-up questions, investigation notes, or developer-tester communication.',
180
+ inputSchema: {
181
+ type: 'object' as const,
182
+ properties: {
183
+ report_id: {
184
+ type: 'string',
185
+ description: 'The UUID of the report to comment on',
186
+ },
187
+ message: {
188
+ type: 'string',
189
+ description: 'The comment/note content',
190
+ },
191
+ author: {
192
+ type: 'string',
193
+ description: 'Optional author name (defaults to "Claude Code")',
194
+ },
195
+ },
196
+ required: ['report_id', 'message'],
197
+ },
198
+ },
199
+ {
200
+ name: 'get_report_comments',
201
+ description: 'Get all comments/notes on a bug report in chronological order. Returns the full discussion thread.',
158
202
  inputSchema: {
159
203
  type: 'object' as const,
160
204
  properties: {
@@ -321,7 +365,7 @@ const tools = [
321
365
  },
322
366
  {
323
367
  name: 'list_test_cases',
324
- description: 'List all test cases in the project. Returns test_key, title, target_route, and other metadata. Use this to see existing tests before updating them.',
368
+ description: 'List all test cases in the project. Returns test_key, title, target_route, and other metadata. Use this to see existing tests before updating them. Use compact=true for id, test_key, title, and priority only (saves tokens).',
325
369
  inputSchema: {
326
370
  type: 'object' as const,
327
371
  properties: {
@@ -346,6 +390,10 @@ const tools = [
346
390
  type: 'number',
347
391
  description: 'Offset for pagination (default 0)',
348
392
  },
393
+ compact: {
394
+ type: 'boolean',
395
+ description: 'Compact mode: returns id, test_key, title, and priority only. (default: false)',
396
+ },
349
397
  },
350
398
  },
351
399
  },
@@ -454,7 +502,7 @@ const tools = [
454
502
  },
455
503
  notify_tester: {
456
504
  type: 'boolean',
457
- description: 'If true, notify the original tester about the fix with a message and verification task. Default: false (silent resolve).',
505
+ description: 'Notify the original tester about the fix with a message and verification task. Default: true. Set to false for silent resolve.',
458
506
  },
459
507
  },
460
508
  required: ['report_id', 'commit_sha'],
@@ -696,17 +744,17 @@ const tools = [
696
744
  },
697
745
  {
698
746
  name: 'get_coverage_matrix',
699
- description: 'Get a comprehensive Route × Track coverage matrix showing test counts, pass rates, and execution data. Use this for a complete view of test coverage.',
747
+ description: 'Get a comprehensive Route × Track coverage matrix showing test counts, pass rates, and execution data. Use this for a complete view of test coverage. Execution data and bug counts are opt-in to save tokens.',
700
748
  inputSchema: {
701
749
  type: 'object' as const,
702
750
  properties: {
703
751
  include_execution_data: {
704
752
  type: 'boolean',
705
- description: 'Include pass/fail rates and last execution times (default: true)',
753
+ description: 'Include pass/fail rates and last execution times (default: false). Set true when you need execution history.',
706
754
  },
707
755
  include_bug_counts: {
708
756
  type: 'boolean',
709
- description: 'Include open/critical bug counts per route (default: true)',
757
+ description: 'Include open/critical bug counts per route (default: false). Set true when you need bug context.',
710
758
  },
711
759
  },
712
760
  },
@@ -940,6 +988,11 @@ const tools = [
940
988
  enum: ['ios', 'android', 'web'],
941
989
  description: 'Filter by platform support',
942
990
  },
991
+ role: {
992
+ type: 'string',
993
+ enum: ['tester', 'feedback'],
994
+ description: 'Filter by role: "tester" for QA testers, "feedback" for feedback-only users (default: all)',
995
+ },
943
996
  },
944
997
  },
945
998
  },
@@ -1028,6 +1081,21 @@ const tools = [
1028
1081
  required: ['tester_id', 'test_case_ids'],
1029
1082
  },
1030
1083
  },
1084
+ {
1085
+ name: 'unassign_tests',
1086
+ description: 'Remove one or more test assignments by assignment ID. Preserves the test case and its history — only the assignment link is deleted. Use list_test_assignments first to find assignment IDs. Max 50 per call.',
1087
+ inputSchema: {
1088
+ type: 'object' as const,
1089
+ properties: {
1090
+ assignment_ids: {
1091
+ type: 'array',
1092
+ items: { type: 'string' },
1093
+ description: 'Array of test assignment UUIDs to remove (required, max 50)',
1094
+ },
1095
+ },
1096
+ required: ['assignment_ids'],
1097
+ },
1098
+ },
1031
1099
  {
1032
1100
  name: 'get_tester_workload',
1033
1101
  description: 'View a specific tester\'s current workload — assignment counts by status and recent assignments.',
@@ -1042,215 +1110,1208 @@ const tools = [
1042
1110
  required: ['tester_id'],
1043
1111
  },
1044
1112
  },
1045
- ];
1046
-
1047
- // Tool handlers
1048
- async function listReports(args: {
1049
- limit?: number;
1050
- status?: string;
1051
- severity?: string;
1052
- type?: string;
1053
- }) {
1054
- let query = supabase
1055
- .from('reports')
1056
- .select('id, report_type, severity, status, description, app_context, created_at, reporter_name, tester:testers(name)')
1057
- .eq('project_id', PROJECT_ID)
1058
- .order('created_at', { ascending: false })
1059
- .limit(Math.min(args.limit || 10, 50));
1060
-
1061
- if (args.status) query = query.eq('status', args.status);
1062
- if (args.severity) query = query.eq('severity', args.severity);
1063
- if (args.type) query = query.eq('report_type', args.type);
1064
-
1065
- const { data, error } = await query;
1066
-
1067
- if (error) {
1068
- return { error: error.message };
1069
- }
1070
-
1071
- return {
1072
- reports: data?.map(r => ({
1073
- id: r.id,
1074
- type: r.report_type,
1075
- severity: r.severity,
1076
- status: r.status,
1077
- description: r.description,
1078
- route: (r.app_context as any)?.currentRoute,
1079
- reporter: (r.tester as any)?.name || (r as any).reporter_name || 'Anonymous',
1080
- created_at: r.created_at,
1081
- })),
1082
- total: data?.length || 0,
1083
- };
1084
- }
1085
-
1086
- async function getReport(args: { report_id: string }) {
1087
- // Validate UUID format to prevent injection
1088
- if (!args.report_id || !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(args.report_id)) {
1089
- return { error: 'Invalid report_id format' };
1090
- }
1091
-
1092
- const { data, error } = await supabase
1093
- .from('reports')
1094
- .select('*, tester:testers(*), track:qa_tracks(*)')
1095
- .eq('id', args.report_id)
1096
- .eq('project_id', PROJECT_ID) // Security: ensure report belongs to this project
1097
- .single();
1098
-
1099
- if (error) {
1100
- return { error: error.message };
1101
- }
1102
-
1103
- return {
1104
- report: {
1105
- id: data.id,
1106
- type: data.report_type,
1107
- severity: data.severity,
1108
- status: data.status,
1109
- description: data.description,
1110
- app_context: data.app_context,
1111
- device_info: data.device_info,
1112
- navigation_history: data.navigation_history,
1113
- screenshots: data.screenshots,
1114
- created_at: data.created_at,
1115
- reporter: data.tester ? {
1116
- name: data.tester.name,
1117
- } : (data.reporter_name ? {
1118
- name: data.reporter_name,
1119
- } : null),
1120
- track: data.track ? {
1121
- name: data.track.name,
1122
- icon: data.track.icon,
1123
- } : null,
1124
- },
1125
- };
1126
- }
1127
-
1128
- async function searchReports(args: { query?: string; route?: string }) {
1129
- const sanitizedQuery = sanitizeSearchQuery(args.query);
1130
- const sanitizedRoute = sanitizeSearchQuery(args.route);
1131
-
1132
- let query = supabase
1133
- .from('reports')
1134
- .select('id, report_type, severity, status, description, app_context, created_at')
1135
- .eq('project_id', PROJECT_ID)
1136
- .order('created_at', { ascending: false })
1137
- .limit(20);
1138
-
1139
- if (sanitizedQuery) {
1140
- query = query.ilike('description', `%${sanitizedQuery}%`);
1141
- }
1142
-
1143
- const { data, error } = await query;
1144
-
1145
- if (error) {
1146
- return { error: error.message };
1147
- }
1148
-
1149
- // Filter by route if provided
1150
- let results = data || [];
1151
- if (sanitizedRoute) {
1152
- results = results.filter(r => {
1153
- const route = (r.app_context as any)?.currentRoute;
1154
- return route && route.includes(sanitizedRoute);
1155
- });
1156
- }
1157
-
1158
- return {
1159
- reports: results.map(r => ({
1160
- id: r.id,
1161
- type: r.report_type,
1162
- severity: r.severity,
1163
- status: r.status,
1164
- description: r.description,
1165
- route: (r.app_context as any)?.currentRoute,
1166
- created_at: r.created_at,
1167
- })),
1168
- total: results.length,
1169
- };
1170
- }
1171
-
1172
- async function updateReportStatus(args: {
1173
- report_id: string;
1174
- status: string;
1175
- resolution_notes?: string;
1176
- }) {
1177
- if (!isValidUUID(args.report_id)) {
1178
- return { error: 'Invalid report_id format' };
1179
- }
1180
-
1181
- const updates: Record<string, unknown> = { status: args.status };
1182
- if (args.resolution_notes) {
1183
- updates.resolution_notes = args.resolution_notes;
1184
- }
1185
-
1186
- const { error } = await supabase
1187
- .from('reports')
1188
- .update(updates)
1189
- .eq('id', args.report_id)
1190
- .eq('project_id', PROJECT_ID); // Security: ensure report belongs to this project
1191
-
1192
- if (error) {
1193
- return { error: error.message };
1194
- }
1195
-
1196
- return { success: true, message: `Report status updated to ${args.status}` };
1197
- }
1198
-
1199
- async function getReportContext(args: { report_id: string }) {
1200
- if (!isValidUUID(args.report_id)) {
1201
- return { error: 'Invalid report_id format' };
1202
- }
1203
-
1204
- const { data, error } = await supabase
1205
- .from('reports')
1206
- .select('app_context, device_info, navigation_history, enhanced_context')
1207
- .eq('id', args.report_id)
1208
- .eq('project_id', PROJECT_ID) // Security: ensure report belongs to this project
1209
- .single();
1210
-
1211
- if (error) {
1212
- return { error: error.message };
1213
- }
1214
-
1215
- return {
1216
- context: {
1217
- app_context: data.app_context,
1218
- device_info: data.device_info,
1219
- navigation_history: data.navigation_history,
1220
- enhanced_context: data.enhanced_context || {},
1113
+ // === NEW TESTER & ANALYTICS TOOLS ===
1114
+ {
1115
+ name: 'create_tester',
1116
+ description: 'Add a new QA tester to the project without opening the dashboard.',
1117
+ inputSchema: {
1118
+ type: 'object' as const,
1119
+ properties: {
1120
+ name: {
1121
+ type: 'string',
1122
+ description: 'Full name of the tester (required)',
1123
+ },
1124
+ email: {
1125
+ type: 'string',
1126
+ description: 'Email address of the tester (required, must be unique per project)',
1127
+ },
1128
+ platforms: {
1129
+ type: 'array',
1130
+ items: { type: 'string', enum: ['ios', 'android', 'web'] },
1131
+ description: 'Platforms the tester can test on (default: ["ios", "web"])',
1132
+ },
1133
+ tier: {
1134
+ type: 'number',
1135
+ description: 'Tester tier 1-3 (default: 1)',
1136
+ },
1137
+ notes: {
1138
+ type: 'string',
1139
+ description: 'Optional notes about the tester',
1140
+ },
1141
+ role: {
1142
+ type: 'string',
1143
+ enum: ['tester', 'feedback'],
1144
+ description: 'Role: "tester" for QA testers (default), "feedback" for feedback-only users',
1145
+ },
1146
+ },
1147
+ required: ['name', 'email'],
1221
1148
  },
1222
- };
1223
- }
1224
-
1225
- async function getProjectInfo() {
1226
- // Get project details
1227
- const { data: project, error: projectError } = await supabase
1228
- .from('projects')
1229
- .select('id, name, slug, is_qa_enabled')
1230
- .eq('id', PROJECT_ID)
1231
- .single();
1232
-
1233
- if (projectError) {
1234
- return { error: projectError.message };
1235
- }
1236
-
1237
- // Get track counts
1238
- const { data: tracks } = await supabase
1239
- .from('qa_tracks')
1240
- .select('id, name, icon, test_template')
1241
- .eq('project_id', PROJECT_ID);
1242
-
1243
- // Get test case count
1244
- const { count: testCaseCount } = await supabase
1149
+ },
1150
+ {
1151
+ name: 'update_tester',
1152
+ description: 'Update an existing tester\'s status, platforms, tier, or notes.',
1153
+ inputSchema: {
1154
+ type: 'object' as const,
1155
+ properties: {
1156
+ tester_id: {
1157
+ type: 'string',
1158
+ description: 'UUID of the tester to update (required)',
1159
+ },
1160
+ status: {
1161
+ type: 'string',
1162
+ enum: ['active', 'inactive', 'invited'],
1163
+ description: 'New status for the tester',
1164
+ },
1165
+ platforms: {
1166
+ type: 'array',
1167
+ items: { type: 'string', enum: ['ios', 'android', 'web'] },
1168
+ description: 'Updated platforms array',
1169
+ },
1170
+ tier: {
1171
+ type: 'number',
1172
+ description: 'Updated tier (1-3)',
1173
+ },
1174
+ notes: {
1175
+ type: 'string',
1176
+ description: 'Updated notes',
1177
+ },
1178
+ name: {
1179
+ type: 'string',
1180
+ description: 'Updated name',
1181
+ },
1182
+ },
1183
+ required: ['tester_id'],
1184
+ },
1185
+ },
1186
+ {
1187
+ name: 'bulk_update_reports',
1188
+ description: 'Update the status of multiple bug reports at once. Useful after a fix session to close many bugs.',
1189
+ inputSchema: {
1190
+ type: 'object' as const,
1191
+ properties: {
1192
+ report_ids: {
1193
+ type: 'array',
1194
+ items: { type: 'string' },
1195
+ description: 'Array of report UUIDs to update (required, max 50)',
1196
+ },
1197
+ status: {
1198
+ type: 'string',
1199
+ enum: ['new', 'triaging', 'confirmed', 'in_progress', 'fixed', 'resolved', 'verified', 'wont_fix', 'duplicate', 'closed'],
1200
+ description: 'New status for all reports (required)',
1201
+ },
1202
+ resolution_notes: {
1203
+ type: 'string',
1204
+ description: 'Optional resolution notes applied to all reports',
1205
+ },
1206
+ },
1207
+ required: ['report_ids', 'status'],
1208
+ },
1209
+ },
1210
+ {
1211
+ name: 'get_bug_trends',
1212
+ description: 'Get bug report trends over time — grouped by week, severity, category, or status. Useful for spotting patterns.',
1213
+ inputSchema: {
1214
+ type: 'object' as const,
1215
+ properties: {
1216
+ group_by: {
1217
+ type: 'string',
1218
+ enum: ['week', 'severity', 'category', 'status'],
1219
+ description: 'How to group the trends (default: week)',
1220
+ },
1221
+ days: {
1222
+ type: 'number',
1223
+ description: 'Number of days to look back (default: 30, max: 180)',
1224
+ },
1225
+ },
1226
+ },
1227
+ },
1228
+ {
1229
+ name: 'get_tester_leaderboard',
1230
+ description: 'Rank testers by testing activity — bugs found, tests completed, pass rate, and average test duration.',
1231
+ inputSchema: {
1232
+ type: 'object' as const,
1233
+ properties: {
1234
+ days: {
1235
+ type: 'number',
1236
+ description: 'Number of days to look back (default: 30, max: 180)',
1237
+ },
1238
+ sort_by: {
1239
+ type: 'string',
1240
+ enum: ['bugs_found', 'tests_completed', 'pass_rate'],
1241
+ description: 'Sort metric (default: tests_completed)',
1242
+ },
1243
+ },
1244
+ },
1245
+ },
1246
+ {
1247
+ name: 'export_test_results',
1248
+ description: 'Export test results for a specific test run as structured JSON — includes every assignment, tester, result, and duration. Use compact=true for summary only (no assignments array). Use limit to cap assignments returned.',
1249
+ inputSchema: {
1250
+ type: 'object' as const,
1251
+ properties: {
1252
+ test_run_id: {
1253
+ type: 'string',
1254
+ description: 'UUID of the test run to export (required)',
1255
+ },
1256
+ compact: {
1257
+ type: 'boolean',
1258
+ description: 'Compact mode: returns test run info + summary only, no assignments array. (default: false)',
1259
+ },
1260
+ limit: {
1261
+ type: 'number',
1262
+ description: 'Max assignments to return in full mode (default: 100, max: 500). Ignored when compact=true.',
1263
+ },
1264
+ },
1265
+ required: ['test_run_id'],
1266
+ },
1267
+ },
1268
+ {
1269
+ name: 'get_testing_velocity',
1270
+ description: 'Get a rolling average of test completions per day over the specified window. Shows daily completion counts and trend direction.',
1271
+ inputSchema: {
1272
+ type: 'object' as const,
1273
+ properties: {
1274
+ days: {
1275
+ type: 'number',
1276
+ description: 'Number of days to analyze (default: 14, max: 90)',
1277
+ },
1278
+ },
1279
+ },
1280
+ },
1281
+ // === PROJECT MANAGEMENT TOOLS ===
1282
+ {
1283
+ name: 'list_projects',
1284
+ description: 'List all BugBear projects accessible with the current credentials. Use this to find project IDs for switch_project.',
1285
+ inputSchema: {
1286
+ type: 'object' as const,
1287
+ properties: {},
1288
+ },
1289
+ },
1290
+ {
1291
+ name: 'switch_project',
1292
+ description: 'Switch the active project. All subsequent tool calls will use this project. Use list_projects first to find the project ID.',
1293
+ inputSchema: {
1294
+ type: 'object' as const,
1295
+ properties: {
1296
+ project_id: {
1297
+ type: 'string',
1298
+ description: 'UUID of the project to switch to (required)',
1299
+ },
1300
+ },
1301
+ required: ['project_id'],
1302
+ },
1303
+ },
1304
+ {
1305
+ name: 'get_current_project',
1306
+ description: 'Show which project is currently active.',
1307
+ inputSchema: {
1308
+ type: 'object' as const,
1309
+ properties: {},
1310
+ },
1311
+ },
1312
+ // === TEST EXECUTION INTELLIGENCE ===
1313
+ {
1314
+ name: 'get_test_impact',
1315
+ description: 'Given changed files, identify which test cases are affected by mapping file paths to test case target routes.',
1316
+ inputSchema: {
1317
+ type: 'object' as const,
1318
+ properties: {
1319
+ changed_files: {
1320
+ type: 'array',
1321
+ items: { type: 'string' },
1322
+ description: 'List of changed file paths (relative to project root)',
1323
+ },
1324
+ },
1325
+ required: ['changed_files'],
1326
+ },
1327
+ },
1328
+ {
1329
+ name: 'get_flaky_tests',
1330
+ description: 'Analyze test run history to identify tests with intermittent failure rates above a threshold.',
1331
+ inputSchema: {
1332
+ type: 'object' as const,
1333
+ properties: {
1334
+ threshold: {
1335
+ type: 'number',
1336
+ description: 'Minimum flakiness rate to report (0-100, default: 5)',
1337
+ },
1338
+ limit: {
1339
+ type: 'number',
1340
+ description: 'Maximum results to return (default: 20)',
1341
+ },
1342
+ },
1343
+ },
1344
+ },
1345
+ {
1346
+ name: 'assess_test_quality',
1347
+ description: 'Analyze test case steps for weak patterns: vague assertions, missing edge cases, no negative testing, generic descriptions.',
1348
+ inputSchema: {
1349
+ type: 'object' as const,
1350
+ properties: {
1351
+ test_case_ids: {
1352
+ type: 'array',
1353
+ items: { type: 'string' },
1354
+ description: 'Specific test case IDs to assess. If omitted, assesses recent test cases.',
1355
+ },
1356
+ limit: {
1357
+ type: 'number',
1358
+ description: 'Maximum test cases to assess (default: 20)',
1359
+ },
1360
+ },
1361
+ },
1362
+ },
1363
+ {
1364
+ name: 'get_test_execution_summary',
1365
+ description: 'Aggregate test execution metrics: pass rate, completion rate, most-failed tests, fastest/slowest tests.',
1366
+ inputSchema: {
1367
+ type: 'object' as const,
1368
+ properties: {
1369
+ days: {
1370
+ type: 'number',
1371
+ description: 'Number of days to analyze (default: 30)',
1372
+ },
1373
+ },
1374
+ },
1375
+ },
1376
+ {
1377
+ name: 'check_test_freshness',
1378
+ description: 'Identify test cases that have not been updated since their target code was modified.',
1379
+ inputSchema: {
1380
+ type: 'object' as const,
1381
+ properties: {
1382
+ limit: {
1383
+ type: 'number',
1384
+ description: 'Maximum results to return (default: 20)',
1385
+ },
1386
+ },
1387
+ },
1388
+ },
1389
+ {
1390
+ name: 'get_untested_changes',
1391
+ description: 'Given recent commits or changed files, find code changes with no corresponding test coverage in BugBear.',
1392
+ inputSchema: {
1393
+ type: 'object' as const,
1394
+ properties: {
1395
+ changed_files: {
1396
+ type: 'array',
1397
+ items: { type: 'string' },
1398
+ description: 'List of changed file paths. If omitted, uses git diff against main.',
1399
+ },
1400
+ },
1401
+ },
1402
+ },
1403
+ // === AUTO-MONITORING TOOLS ===
1404
+ {
1405
+ name: 'get_auto_detected_issues',
1406
+ description: 'Get auto-detected monitoring issues grouped by error fingerprint. Shows recurring crashes, API failures, and rage clicks with frequency and user impact.',
1407
+ inputSchema: {
1408
+ type: 'object' as const,
1409
+ properties: {
1410
+ source: {
1411
+ type: 'string',
1412
+ enum: ['auto_crash', 'auto_api', 'auto_rage_click'],
1413
+ description: 'Filter by source type',
1414
+ },
1415
+ min_occurrences: {
1416
+ type: 'number',
1417
+ description: 'Min occurrence count (default: 1)',
1418
+ },
1419
+ since: {
1420
+ type: 'string',
1421
+ description: 'ISO date — only issues after this date (default: 7 days ago)',
1422
+ },
1423
+ limit: {
1424
+ type: 'number',
1425
+ description: 'Max results (default: 20)',
1426
+ },
1427
+ compact: {
1428
+ type: 'boolean',
1429
+ description: 'Compact mode: fingerprint, source, count only',
1430
+ },
1431
+ },
1432
+ },
1433
+ },
1434
+ {
1435
+ name: 'generate_tests_from_errors',
1436
+ description: 'Suggest QA test cases from auto-detected error patterns. Returns structured suggestions — does NOT auto-create test cases.',
1437
+ inputSchema: {
1438
+ type: 'object' as const,
1439
+ properties: {
1440
+ report_ids: {
1441
+ type: 'array',
1442
+ items: { type: 'string' },
1443
+ description: 'Specific report IDs. If omitted, uses top uncovered errors.',
1444
+ },
1445
+ limit: {
1446
+ type: 'number',
1447
+ description: 'Max suggestions (default: 5)',
1448
+ },
1449
+ },
1450
+ },
1451
+ },
1452
+ ];
1453
+
1454
+ // === TEST EXECUTION INTELLIGENCE ===
1455
+
1456
+ async function getTestImpact(args: { changed_files: string[] }) {
1457
+ const projectId = requireProject();
1458
+ const changedFiles = args.changed_files || [];
1459
+
1460
+ if (changedFiles.length === 0) {
1461
+ return { affectedTests: [], message: 'No changed files provided.' };
1462
+ }
1463
+
1464
+ // Get all test cases for the project with their target routes
1465
+ const { data: testCases, error } = await supabase
1466
+ .from('test_cases')
1467
+ .select('id, title, target_route, qa_track, priority')
1468
+ .eq('project_id', projectId);
1469
+
1470
+ if (error) return { error: error.message };
1471
+ if (!testCases || testCases.length === 0) {
1472
+ return { affectedTests: [], message: 'No test cases found for this project.' };
1473
+ }
1474
+
1475
+ // Map changed files to affected test cases
1476
+ const affected: Array<{ testId: string; title: string; targetRoute: string; matchedFiles: string[]; qaTrack: string }> = [];
1477
+
1478
+ for (const tc of testCases) {
1479
+ const route = tc.target_route || '';
1480
+ const matchedFiles = changedFiles.filter(f => {
1481
+ // Match file path to route (e.g., src/app/api/tasks/route.ts -> /api/tasks)
1482
+ const normalized = f.replace(/\\/g, '/');
1483
+ const routeParts = route.split('/').filter(Boolean);
1484
+ return routeParts.some((part: string) => normalized.includes(part)) || normalized.includes(route.replace(/\//g, '/'));
1485
+ });
1486
+
1487
+ if (matchedFiles.length > 0) {
1488
+ affected.push({
1489
+ testId: tc.id,
1490
+ title: tc.title,
1491
+ targetRoute: route,
1492
+ matchedFiles,
1493
+ qaTrack: tc.qa_track,
1494
+ });
1495
+ }
1496
+ }
1497
+
1498
+ return {
1499
+ affectedTests: affected,
1500
+ totalTestCases: testCases.length,
1501
+ affectedCount: affected.length,
1502
+ changedFileCount: changedFiles.length,
1503
+ };
1504
+ }
1505
+
1506
+ async function getFlakyTests(args: { threshold?: number; limit?: number }) {
1507
+ const projectId = requireProject();
1508
+ const threshold = args.threshold || 5;
1509
+ const limit = args.limit || 20;
1510
+
1511
+ // Get test results grouped by test case
1512
+ const { data: results, error } = await supabase
1513
+ .from('test_results')
1514
+ .select('test_case_id, status, test_cases!inner(title, target_route, qa_track)')
1515
+ .eq('test_cases.project_id', projectId)
1516
+ .order('created_at', { ascending: false })
1517
+ .limit(5000);
1518
+
1519
+ if (error) return { error: error.message };
1520
+ if (!results || results.length === 0) {
1521
+ return { flakyTests: [], message: 'No test results found.' };
1522
+ }
1523
+
1524
+ // Group by test case and calculate flakiness
1525
+ const testStats: Record<string, { passes: number; fails: number; total: number; title: string; route: string; track: string }> = {};
1526
+
1527
+ for (const r of results) {
1528
+ const id = r.test_case_id;
1529
+ if (!testStats[id]) {
1530
+ const tc = r.test_cases as any;
1531
+ testStats[id] = { passes: 0, fails: 0, total: 0, title: tc?.title || '', route: tc?.target_route || '', track: tc?.qa_track || '' };
1532
+ }
1533
+ testStats[id].total++;
1534
+ if (r.status === 'pass') testStats[id].passes++;
1535
+ else if (r.status === 'fail') testStats[id].fails++;
1536
+ }
1537
+
1538
+ // Find flaky tests (have both passes and fails, with fail rate above threshold)
1539
+ const flaky = Object.entries(testStats)
1540
+ .filter(([, stats]) => {
1541
+ if (stats.total < 3) return false; // Need enough data
1542
+ const failRate = (stats.fails / stats.total) * 100;
1543
+ const passRate = (stats.passes / stats.total) * 100;
1544
+ return failRate >= threshold && passRate > 0; // Has both passes and fails
1545
+ })
1546
+ .map(([id, stats]) => ({
1547
+ testCaseId: id,
1548
+ title: stats.title,
1549
+ targetRoute: stats.route,
1550
+ qaTrack: stats.track,
1551
+ totalRuns: stats.total,
1552
+ failRate: Math.round((stats.fails / stats.total) * 100),
1553
+ passRate: Math.round((stats.passes / stats.total) * 100),
1554
+ }))
1555
+ .sort((a, b) => b.failRate - a.failRate)
1556
+ .slice(0, limit);
1557
+
1558
+ return {
1559
+ flakyTests: flaky,
1560
+ totalAnalyzed: Object.keys(testStats).length,
1561
+ flakyCount: flaky.length,
1562
+ threshold,
1563
+ };
1564
+ }
1565
+
1566
+ async function assessTestQuality(args: { test_case_ids?: string[]; limit?: number }) {
1567
+ const projectId = requireProject();
1568
+ const limit = args.limit || 20;
1569
+
1570
+ let query = supabase
1571
+ .from('test_cases')
1572
+ .select('id, title, steps, target_route, qa_track, priority')
1573
+ .eq('project_id', projectId)
1574
+ .limit(limit);
1575
+
1576
+ if (args.test_case_ids && args.test_case_ids.length > 0) {
1577
+ query = query.in('id', args.test_case_ids);
1578
+ }
1579
+
1580
+ const { data: testCases, error } = await query;
1581
+ if (error) return { error: error.message };
1582
+ if (!testCases || testCases.length === 0) {
1583
+ return { assessments: [], message: 'No test cases found.' };
1584
+ }
1585
+
1586
+ const assessments = testCases.map(tc => {
1587
+ const issues: string[] = [];
1588
+ const steps = tc.steps || [];
1589
+
1590
+ // Check for weak patterns
1591
+ if (steps.length < 2) {
1592
+ issues.push('Too few steps — test may not cover the full flow');
1593
+ }
1594
+
1595
+ const allStepsText = steps.map((s: any) => (typeof s === 'string' ? s : s.action || s.description || '')).join(' ');
1596
+
1597
+ // Vague assertions
1598
+ if (/should work|looks good|is correct|verify it works/i.test(allStepsText)) {
1599
+ issues.push('Vague assertions detected — use specific expected outcomes');
1600
+ }
1601
+
1602
+ // Missing edge cases
1603
+ if (!/error|invalid|empty|missing|unauthorized|forbidden|404|500/i.test(allStepsText)) {
1604
+ issues.push('No negative/error test cases — add edge case testing');
1605
+ }
1606
+
1607
+ // Generic descriptions
1608
+ if (/test the|check the|verify the/i.test(tc.title) && tc.title.length < 30) {
1609
+ issues.push('Generic test title — be more specific about what is being tested');
1610
+ }
1611
+
1612
+ // No specific UI elements referenced
1613
+ if (!/button|input|form|modal|dropdown|select|click|type|enter|submit/i.test(allStepsText)) {
1614
+ issues.push('No specific UI elements referenced — steps may be too abstract');
1615
+ }
1616
+
1617
+ const quality = issues.length === 0 ? 'good' : issues.length <= 2 ? 'needs-improvement' : 'poor';
1618
+
1619
+ return {
1620
+ testCaseId: tc.id,
1621
+ title: tc.title,
1622
+ targetRoute: tc.target_route,
1623
+ stepCount: steps.length,
1624
+ quality,
1625
+ issues,
1626
+ };
1627
+ });
1628
+
1629
+ const qualityCounts = {
1630
+ good: assessments.filter(a => a.quality === 'good').length,
1631
+ needsImprovement: assessments.filter(a => a.quality === 'needs-improvement').length,
1632
+ poor: assessments.filter(a => a.quality === 'poor').length,
1633
+ };
1634
+
1635
+ return {
1636
+ assessments,
1637
+ summary: qualityCounts,
1638
+ totalAssessed: assessments.length,
1639
+ };
1640
+ }
1641
+
1642
+ async function getTestExecutionSummary(args: { days?: number }) {
1643
+ const projectId = requireProject();
1644
+ const days = args.days || 30;
1645
+ const since = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString();
1646
+
1647
+ // Get test results
1648
+ const { data: results, error } = await supabase
1649
+ .from('test_results')
1650
+ .select('test_case_id, status, duration_ms, created_at, test_cases!inner(title, target_route)')
1651
+ .eq('test_cases.project_id', projectId)
1652
+ .gte('created_at', since)
1653
+ .order('created_at', { ascending: false });
1654
+
1655
+ if (error) return { error: error.message };
1656
+ if (!results || results.length === 0) {
1657
+ return { message: `No test results found in the last ${days} days.` };
1658
+ }
1659
+
1660
+ const totalRuns = results.length;
1661
+ const passed = results.filter(r => r.status === 'pass').length;
1662
+ const failed = results.filter(r => r.status === 'fail').length;
1663
+ const blocked = results.filter(r => r.status === 'blocked').length;
1664
+
1665
+ // Most failed tests
1666
+ const failCounts: Record<string, { count: number; title: string; route: string }> = {};
1667
+ for (const r of results.filter(r => r.status === 'fail')) {
1668
+ const id = r.test_case_id;
1669
+ const tc = r.test_cases as any;
1670
+ if (!failCounts[id]) {
1671
+ failCounts[id] = { count: 0, title: tc?.title || '', route: tc?.target_route || '' };
1672
+ }
1673
+ failCounts[id].count++;
1674
+ }
1675
+ const mostFailed = Object.entries(failCounts)
1676
+ .sort((a, b) => b[1].count - a[1].count)
1677
+ .slice(0, 5)
1678
+ .map(([id, data]) => ({ testCaseId: id, ...data }));
1679
+
1680
+ // Duration stats
1681
+ const durations = results.filter(r => r.duration_ms).map(r => r.duration_ms as number);
1682
+ const avgDuration = durations.length > 0 ? Math.round(durations.reduce((a, b) => a + b, 0) / durations.length) : 0;
1683
+ const maxDuration = durations.length > 0 ? Math.max(...durations) : 0;
1684
+
1685
+ return {
1686
+ period: `${days} days`,
1687
+ totalRuns,
1688
+ passRate: Math.round((passed / totalRuns) * 100),
1689
+ failRate: Math.round((failed / totalRuns) * 100),
1690
+ blockedCount: blocked,
1691
+ averageDurationMs: avgDuration,
1692
+ maxDurationMs: maxDuration,
1693
+ mostFailed,
1694
+ uniqueTestsCovered: new Set(results.map(r => r.test_case_id)).size,
1695
+ };
1696
+ }
1697
+
1698
+ async function checkTestFreshness(args: { limit?: number }) {
1699
+ const projectId = requireProject();
1700
+ const limit = args.limit || 20;
1701
+
1702
+ // Get test cases with their last update and last result
1703
+ const { data: testCases, error } = await supabase
1704
+ .from('test_cases')
1705
+ .select('id, title, target_route, updated_at, created_at')
1706
+ .eq('project_id', projectId)
1707
+ .order('updated_at', { ascending: true })
1708
+ .limit(limit);
1709
+
1710
+ if (error) return { error: error.message };
1711
+ if (!testCases || testCases.length === 0) {
1712
+ return { staleTests: [], message: 'No test cases found.' };
1713
+ }
1714
+
1715
+ const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString();
1716
+
1717
+ const stale = testCases
1718
+ .filter(tc => tc.updated_at < thirtyDaysAgo)
1719
+ .map(tc => ({
1720
+ testCaseId: tc.id,
1721
+ title: tc.title,
1722
+ targetRoute: tc.target_route,
1723
+ lastUpdated: tc.updated_at,
1724
+ daysSinceUpdate: Math.round((Date.now() - new Date(tc.updated_at).getTime()) / (24 * 60 * 60 * 1000)),
1725
+ }));
1726
+
1727
+ return {
1728
+ staleTests: stale,
1729
+ totalTestCases: testCases.length,
1730
+ staleCount: stale.length,
1731
+ stalenessThreshold: '30 days',
1732
+ };
1733
+ }
1734
+
1735
+ async function getUntestedChanges(args: { changed_files?: string[] }) {
1736
+ const projectId = requireProject();
1737
+
1738
+ // Get all test cases to understand what's covered
1739
+ const { data: testCases, error } = await supabase
1740
+ .from('test_cases')
1741
+ .select('id, title, target_route')
1742
+ .eq('project_id', projectId);
1743
+
1744
+ if (error) return { error: error.message };
1745
+
1746
+ const coveredRoutes = new Set((testCases || []).map(tc => tc.target_route).filter(Boolean));
1747
+
1748
+ // If changed_files provided, check coverage
1749
+ const changedFiles = args.changed_files || [];
1750
+
1751
+ if (changedFiles.length === 0) {
1752
+ return {
1753
+ message: 'No changed files provided. Pass changed_files to check coverage.',
1754
+ totalCoveredRoutes: coveredRoutes.size,
1755
+ };
1756
+ }
1757
+
1758
+ // Map changed files to routes and check coverage
1759
+ const untested: Array<{ file: string; inferredRoute: string; reason: string }> = [];
1760
+
1761
+ for (const file of changedFiles) {
1762
+ const normalized = file.replace(/\\/g, '/');
1763
+
1764
+ // Extract route-like path from file
1765
+ let inferredRoute = '';
1766
+
1767
+ // Next.js app router: app/api/tasks/route.ts -> /api/tasks
1768
+ const appRouterMatch = normalized.match(/app\/(api\/[^/]+(?:\/[^/]+)*?)\/route\.\w+$/);
1769
+ if (appRouterMatch) {
1770
+ inferredRoute = '/' + appRouterMatch[1];
1771
+ }
1772
+
1773
+ // Pages router: pages/api/tasks.ts -> /api/tasks
1774
+ const pagesMatch = normalized.match(/pages\/(api\/[^.]+)\.\w+$/);
1775
+ if (!inferredRoute && pagesMatch) {
1776
+ inferredRoute = '/' + pagesMatch[1];
1777
+ }
1778
+
1779
+ // Component files
1780
+ const componentMatch = normalized.match(/(?:components|screens|pages)\/([^.]+)\.\w+$/);
1781
+ if (!inferredRoute && componentMatch) {
1782
+ inferredRoute = '/' + componentMatch[1].replace(/\\/g, '/');
1783
+ }
1784
+
1785
+ if (inferredRoute && !coveredRoutes.has(inferredRoute)) {
1786
+ untested.push({
1787
+ file,
1788
+ inferredRoute,
1789
+ reason: 'No test cases cover this route',
1790
+ });
1791
+ } else if (!inferredRoute) {
1792
+ // Can't map to a route — flag as potentially untested
1793
+ untested.push({
1794
+ file,
1795
+ inferredRoute: 'unknown',
1796
+ reason: 'Could not map file to a testable route',
1797
+ });
1798
+ }
1799
+ }
1800
+
1801
+ return {
1802
+ untestedChanges: untested,
1803
+ changedFileCount: changedFiles.length,
1804
+ untestedCount: untested.length,
1805
+ coveredRoutes: coveredRoutes.size,
1806
+ };
1807
+ }
1808
+
1809
+ // === AUTO-MONITORING HANDLERS ===
1810
+
1811
+ async function getAutoDetectedIssues(args: {
1812
+ source?: string;
1813
+ min_occurrences?: number;
1814
+ since?: string;
1815
+ limit?: number;
1816
+ compact?: boolean;
1817
+ }) {
1818
+ const projectId = requireProject();
1819
+ const since = args.since || new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString();
1820
+ const limit = args.limit || 20;
1821
+
1822
+ let query = supabase
1823
+ .from('reports')
1824
+ .select('id, error_fingerprint, report_source, title, severity, reporter_id, sentry_event_id, created_at, app_context')
1825
+ .eq('project_id', projectId)
1826
+ .neq('report_source', 'manual')
1827
+ .not('error_fingerprint', 'is', null)
1828
+ .gte('created_at', since)
1829
+ .order('created_at', { ascending: false });
1830
+
1831
+ if (args.source) {
1832
+ query = query.eq('report_source', args.source);
1833
+ }
1834
+
1835
+ const { data, error } = await query;
1836
+ if (error) return { error: error.message };
1837
+ if (!data || data.length === 0) return { issues: [], total: 0 };
1838
+
1839
+ // Group by fingerprint
1840
+ const grouped = new Map<string, typeof data>();
1841
+ for (const report of data) {
1842
+ const fp = report.error_fingerprint!;
1843
+ if (!grouped.has(fp)) grouped.set(fp, []);
1844
+ grouped.get(fp)!.push(report);
1845
+ }
1846
+
1847
+ // Build issue summaries
1848
+ const issues = Array.from(grouped.entries())
1849
+ .map(([fingerprint, reports]) => {
1850
+ const uniqueReporters = new Set(reports.map(r => r.reporter_id));
1851
+ const sorted = reports.sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime());
1852
+ const first = sorted[0];
1853
+ const last = sorted[sorted.length - 1];
1854
+ const route = (first.app_context as any)?.currentRoute || 'unknown';
1855
+
1856
+ return {
1857
+ fingerprint,
1858
+ source: first.report_source,
1859
+ message: first.title,
1860
+ route,
1861
+ occurrence_count: reports.length,
1862
+ affected_users: uniqueReporters.size,
1863
+ first_seen: first.created_at,
1864
+ last_seen: last.created_at,
1865
+ severity: first.severity,
1866
+ has_sentry_link: reports.some(r => r.sentry_event_id != null),
1867
+ sample_report_id: first.id,
1868
+ };
1869
+ })
1870
+ .filter(issue => issue.occurrence_count >= (args.min_occurrences || 1))
1871
+ .sort((a, b) => b.occurrence_count - a.occurrence_count)
1872
+ .slice(0, limit);
1873
+
1874
+ if (args.compact) {
1875
+ return {
1876
+ issues: issues.map(i => ({
1877
+ fingerprint: i.fingerprint,
1878
+ source: i.source,
1879
+ count: i.occurrence_count,
1880
+ users: i.affected_users,
1881
+ severity: i.severity,
1882
+ })),
1883
+ total: issues.length,
1884
+ };
1885
+ }
1886
+
1887
+ return { issues, total: issues.length };
1888
+ }
1889
+
1890
+ async function generateTestsFromErrors(args: {
1891
+ report_ids?: string[];
1892
+ limit?: number;
1893
+ }) {
1894
+ const projectId = requireProject();
1895
+ const limit = args.limit || 5;
1896
+
1897
+ let reports;
1898
+ if (args.report_ids?.length) {
1899
+ // Validate all UUIDs
1900
+ for (const id of args.report_ids) {
1901
+ if (!isValidUUID(id)) {
1902
+ return { error: `Invalid report_id format: ${id}` };
1903
+ }
1904
+ }
1905
+
1906
+ const { data, error } = await supabase
1907
+ .from('reports')
1908
+ .select('id, title, report_source, severity, app_context, error_fingerprint, description')
1909
+ .eq('project_id', projectId)
1910
+ .in('id', args.report_ids);
1911
+ if (error) return { error: error.message };
1912
+ reports = data;
1913
+ } else {
1914
+ // Get top uncovered auto-detected errors
1915
+ const { data, error } = await supabase
1916
+ .from('reports')
1917
+ .select('id, title, report_source, severity, app_context, error_fingerprint, description')
1918
+ .eq('project_id', projectId)
1919
+ .neq('report_source', 'manual')
1920
+ .not('error_fingerprint', 'is', null)
1921
+ .order('created_at', { ascending: false })
1922
+ .limit(50);
1923
+ if (error) return { error: error.message };
1924
+
1925
+ // Deduplicate by fingerprint, keep first occurrence
1926
+ const seen = new Set<string>();
1927
+ reports = (data || []).filter(r => {
1928
+ if (!r.error_fingerprint || seen.has(r.error_fingerprint)) return false;
1929
+ seen.add(r.error_fingerprint);
1930
+ return true;
1931
+ }).slice(0, limit);
1932
+ }
1933
+
1934
+ if (!reports?.length) return { suggestions: [] };
1935
+
1936
+ const suggestions = reports.map(report => {
1937
+ const route = (report.app_context as any)?.currentRoute || '/unknown';
1938
+ const source = report.report_source;
1939
+ const priority = report.severity === 'critical' ? 'P1' : report.severity === 'high' ? 'P1' : 'P2';
1940
+
1941
+ let suggestedSteps: string[];
1942
+ if (source === 'auto_crash') {
1943
+ suggestedSteps = [
1944
+ `Navigate to ${route}`,
1945
+ 'Reproduce the action that triggered the crash',
1946
+ 'Verify the page does not throw an unhandled error',
1947
+ 'Verify error boundary displays a user-friendly message if error occurs',
1948
+ ];
1949
+ } else if (source === 'auto_api') {
1950
+ const statusCode = (report.app_context as any)?.custom?.statusCode || 'error';
1951
+ const method = (report.app_context as any)?.custom?.requestMethod || 'API';
1952
+ suggestedSteps = [
1953
+ `Navigate to ${route}`,
1954
+ `Trigger the ${method} request that returned ${statusCode}`,
1955
+ 'Verify the request succeeds or displays an appropriate error message',
1956
+ 'Verify no data corruption occurs on failure',
1957
+ ];
1958
+ } else {
1959
+ // rage_click or sentry_sync
1960
+ const target = (report.app_context as any)?.custom?.targetSelector || 'the element';
1961
+ suggestedSteps = [
1962
+ `Navigate to ${route}`,
1963
+ `Click on ${target}`,
1964
+ 'Verify the element responds to interaction',
1965
+ 'Verify loading state is shown if action takes time',
1966
+ ];
1967
+ }
1968
+
1969
+ return {
1970
+ title: `Test: ${report.title?.replace('[Auto] ', '') || 'Auto-detected issue'}`,
1971
+ track: source === 'auto_crash' ? 'Stability' : source === 'auto_api' ? 'API' : 'UX',
1972
+ priority,
1973
+ rationale: `Auto-detected ${source?.replace('auto_', '')} on ${route}`,
1974
+ suggested_steps: suggestedSteps,
1975
+ source_report_id: report.id,
1976
+ route,
1977
+ };
1978
+ });
1979
+
1980
+ return { suggestions };
1981
+ }
1982
+
1983
+ // === Project management handlers ===
1984
+
1985
+ async function listProjects() {
1986
+ const { data, error } = await supabase
1987
+ .from('projects')
1988
+ .select('id, name, slug, is_qa_enabled, created_at')
1989
+ .order('name');
1990
+
1991
+ if (error) {
1992
+ return { error: error.message };
1993
+ }
1994
+
1995
+ return {
1996
+ currentProjectId: currentProjectId || null,
1997
+ projects: data?.map(p => ({
1998
+ id: p.id,
1999
+ name: p.name,
2000
+ slug: p.slug,
2001
+ isQAEnabled: p.is_qa_enabled,
2002
+ isActive: p.id === currentProjectId,
2003
+ createdAt: p.created_at,
2004
+ })) || [],
2005
+ };
2006
+ }
2007
+
2008
+ async function switchProject(args: { project_id: string }) {
2009
+ if (!isValidUUID(args.project_id)) {
2010
+ return { error: 'Invalid project_id format — must be a UUID' };
2011
+ }
2012
+
2013
+ // Verify the project exists and is accessible
2014
+ const { data, error } = await supabase
2015
+ .from('projects')
2016
+ .select('id, name, slug')
2017
+ .eq('id', args.project_id)
2018
+ .single();
2019
+
2020
+ if (error || !data) {
2021
+ return { error: 'Project not found or not accessible' };
2022
+ }
2023
+
2024
+ currentProjectId = data.id;
2025
+
2026
+ return {
2027
+ success: true,
2028
+ message: `Switched to project "${data.name}" (${data.slug})`,
2029
+ projectId: data.id,
2030
+ projectName: data.name,
2031
+ };
2032
+ }
2033
+
2034
+ function getCurrentProject() {
2035
+ if (!currentProjectId) {
2036
+ return { message: 'No project selected. Use list_projects to see available projects, then switch_project to select one.' };
2037
+ }
2038
+ return { projectId: currentProjectId };
2039
+ }
2040
+
2041
+ // Tool handlers
2042
+ async function listReports(args: {
2043
+ limit?: number;
2044
+ status?: string;
2045
+ severity?: string;
2046
+ type?: string;
2047
+ }) {
2048
+ let query = supabase
2049
+ .from('reports')
2050
+ .select('id, report_type, severity, status, description, app_context, created_at, reporter_name, tester:testers(name)')
2051
+ .eq('project_id', currentProjectId)
2052
+ .order('created_at', { ascending: false })
2053
+ .limit(Math.min(args.limit || 10, 50));
2054
+
2055
+ if (args.status) query = query.eq('status', args.status);
2056
+ if (args.severity) query = query.eq('severity', args.severity);
2057
+ if (args.type) query = query.eq('report_type', args.type);
2058
+
2059
+ const { data, error } = await query;
2060
+
2061
+ if (error) {
2062
+ return { error: error.message };
2063
+ }
2064
+
2065
+ return {
2066
+ reports: data?.map(r => ({
2067
+ id: r.id,
2068
+ type: r.report_type,
2069
+ severity: r.severity,
2070
+ status: r.status,
2071
+ description: r.description,
2072
+ route: (r.app_context as any)?.currentRoute,
2073
+ reporter: (r.tester as any)?.name || (r as any).reporter_name || 'Anonymous',
2074
+ created_at: r.created_at,
2075
+ })),
2076
+ total: data?.length || 0,
2077
+ };
2078
+ }
2079
+
2080
+ async function getReport(args: { report_id: string }) {
2081
+ // Validate UUID format to prevent injection
2082
+ if (!args.report_id || !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(args.report_id)) {
2083
+ return { error: 'Invalid report_id format' };
2084
+ }
2085
+
2086
+ const { data, error } = await supabase
2087
+ .from('reports')
2088
+ .select('*, tester:testers(*), track:qa_tracks(*)')
2089
+ .eq('id', args.report_id)
2090
+ .eq('project_id', currentProjectId) // Security: ensure report belongs to this project
2091
+ .single();
2092
+
2093
+ if (error) {
2094
+ return { error: error.message };
2095
+ }
2096
+
2097
+ return {
2098
+ report: {
2099
+ id: data.id,
2100
+ type: data.report_type,
2101
+ severity: data.severity,
2102
+ status: data.status,
2103
+ description: data.description,
2104
+ app_context: data.app_context,
2105
+ device_info: data.device_info,
2106
+ navigation_history: data.navigation_history,
2107
+ screenshot_urls: data.screenshot_urls,
2108
+ created_at: data.created_at,
2109
+ reporter: data.tester ? {
2110
+ name: data.tester.name,
2111
+ } : (data.reporter_name ? {
2112
+ name: data.reporter_name,
2113
+ } : null),
2114
+ track: data.track ? {
2115
+ name: data.track.name,
2116
+ icon: data.track.icon,
2117
+ } : null,
2118
+ },
2119
+ };
2120
+ }
2121
+
2122
+ async function searchReports(args: { query?: string; route?: string }) {
2123
+ const sanitizedQuery = sanitizeSearchQuery(args.query);
2124
+ const sanitizedRoute = sanitizeSearchQuery(args.route);
2125
+
2126
+ let query = supabase
2127
+ .from('reports')
2128
+ .select('id, report_type, severity, status, description, app_context, created_at')
2129
+ .eq('project_id', currentProjectId)
2130
+ .order('created_at', { ascending: false })
2131
+ .limit(20);
2132
+
2133
+ if (sanitizedQuery) {
2134
+ query = query.ilike('description', `%${sanitizedQuery}%`);
2135
+ }
2136
+
2137
+ const { data, error } = await query;
2138
+
2139
+ if (error) {
2140
+ return { error: error.message };
2141
+ }
2142
+
2143
+ // Filter by route if provided
2144
+ let results = data || [];
2145
+ if (sanitizedRoute) {
2146
+ results = results.filter(r => {
2147
+ const route = (r.app_context as any)?.currentRoute;
2148
+ return route && route.includes(sanitizedRoute);
2149
+ });
2150
+ }
2151
+
2152
+ return {
2153
+ reports: results.map(r => ({
2154
+ id: r.id,
2155
+ type: r.report_type,
2156
+ severity: r.severity,
2157
+ status: r.status,
2158
+ description: r.description,
2159
+ route: (r.app_context as any)?.currentRoute,
2160
+ created_at: r.created_at,
2161
+ })),
2162
+ total: results.length,
2163
+ };
2164
+ }
2165
+
2166
+ async function updateReportStatus(args: {
2167
+ report_id: string;
2168
+ status: string;
2169
+ resolution_notes?: string;
2170
+ }) {
2171
+ if (!isValidUUID(args.report_id)) {
2172
+ return { error: 'Invalid report_id format' };
2173
+ }
2174
+
2175
+ const updates: Record<string, unknown> = { status: args.status };
2176
+ if (args.resolution_notes) {
2177
+ updates.resolution_notes = args.resolution_notes;
2178
+ }
2179
+
2180
+ const { error } = await supabase
2181
+ .from('reports')
2182
+ .update(updates)
2183
+ .eq('id', args.report_id)
2184
+ .eq('project_id', currentProjectId); // Security: ensure report belongs to this project
2185
+
2186
+ if (error) {
2187
+ return { error: error.message };
2188
+ }
2189
+
2190
+ return { success: true, message: `Report status updated to ${args.status}` };
2191
+ }
2192
+
2193
+ async function getReportContext(args: { report_id: string; compact?: boolean }) {
2194
+ if (!isValidUUID(args.report_id)) {
2195
+ return { error: 'Invalid report_id format' };
2196
+ }
2197
+
2198
+ const { data, error } = await supabase
2199
+ .from('reports')
2200
+ .select('app_context, device_info, navigation_history, enhanced_context, screenshot_urls')
2201
+ .eq('id', args.report_id)
2202
+ .eq('project_id', currentProjectId) // Security: ensure report belongs to this project
2203
+ .single();
2204
+
2205
+ if (error) {
2206
+ return { error: error.message };
2207
+ }
2208
+
2209
+ // Compact: return app_context only (skip console/network/navigation)
2210
+ if (args.compact === true) {
2211
+ return {
2212
+ context: {
2213
+ app_context: data.app_context,
2214
+ screenshot_urls: data.screenshot_urls,
2215
+ },
2216
+ };
2217
+ }
2218
+
2219
+ return {
2220
+ context: {
2221
+ app_context: data.app_context,
2222
+ device_info: data.device_info,
2223
+ navigation_history: data.navigation_history,
2224
+ enhanced_context: data.enhanced_context || {},
2225
+ screenshot_urls: data.screenshot_urls,
2226
+ },
2227
+ };
2228
+ }
2229
+
2230
+ async function addReportComment(args: { report_id: string; message: string; author?: string }) {
2231
+ if (!isValidUUID(args.report_id)) return { error: 'Invalid report_id format' };
2232
+ if (!args.message?.trim()) return { error: 'Message is required' };
2233
+
2234
+ // Verify report exists
2235
+ const { data: report } = await supabase
2236
+ .from('reports').select('id').eq('id', args.report_id).eq('project_id', currentProjectId).single();
2237
+ if (!report) return { error: 'Report not found' };
2238
+
2239
+ // Find or create a discussion thread for this report
2240
+ const { data: existingThread } = await supabase
2241
+ .from('discussion_threads').select('id')
2242
+ .eq('project_id', currentProjectId).eq('report_id', args.report_id).eq('thread_type', 'report')
2243
+ .limit(1).single();
2244
+
2245
+ let threadId: string;
2246
+ if (existingThread) {
2247
+ threadId = existingThread.id;
2248
+ } else {
2249
+ const newId = crypto.randomUUID();
2250
+ const { error: threadErr } = await supabase
2251
+ .from('discussion_threads').insert({
2252
+ id: newId, project_id: currentProjectId, report_id: args.report_id,
2253
+ thread_type: 'report', subject: 'Bug Report Discussion', audience: 'all',
2254
+ priority: 'normal', created_by_admin: true, last_message_at: new Date().toISOString(),
2255
+ });
2256
+ if (threadErr) return { error: `Failed to create thread: ${threadErr.message}` };
2257
+ threadId = newId;
2258
+ }
2259
+
2260
+ const { data: msg, error: msgErr } = await supabase
2261
+ .from('discussion_messages').insert({
2262
+ thread_id: threadId, sender_type: 'admin', sender_name: args.author || 'Claude Code', content: args.message.trim(), content_type: 'text',
2263
+ }).select('id, content, created_at').single();
2264
+
2265
+ if (msgErr) return { error: `Failed to add comment: ${msgErr.message}` };
2266
+ return { success: true, comment: { id: msg.id, thread_id: threadId, content: msg.content, author: args.author || 'Claude Code', created_at: msg.created_at }, message: 'Comment added to report' };
2267
+ }
2268
+
2269
+ async function getReportComments(args: { report_id: string }) {
2270
+ if (!isValidUUID(args.report_id)) return { error: 'Invalid report_id format' };
2271
+
2272
+ const { data: threads } = await supabase
2273
+ .from('discussion_threads').select('id')
2274
+ .eq('project_id', currentProjectId).eq('report_id', args.report_id).order('created_at', { ascending: true });
2275
+
2276
+ if (!threads || threads.length === 0) return { comments: [], total: 0, message: 'No comments on this report' };
2277
+
2278
+ const { data: messages, error } = await supabase
2279
+ .from('discussion_messages').select('id, thread_id, sender_type, content, content_type, created_at, attachments')
2280
+ .in('thread_id', threads.map(t => t.id)).order('created_at', { ascending: true });
2281
+
2282
+ if (error) return { error: error.message };
2283
+ return { comments: (messages || []).map(m => ({ id: m.id, sender_type: m.sender_type, content: m.content, created_at: m.created_at, attachments: m.attachments })), total: (messages || []).length };
2284
+ }
2285
+
2286
+ async function getProjectInfo() {
2287
+ // Get project details
2288
+ const { data: project, error: projectError } = await supabase
2289
+ .from('projects')
2290
+ .select('id, name, slug, is_qa_enabled')
2291
+ .eq('id', currentProjectId)
2292
+ .single();
2293
+
2294
+ if (projectError) {
2295
+ return { error: projectError.message };
2296
+ }
2297
+
2298
+ // Get track counts
2299
+ const { data: tracks } = await supabase
2300
+ .from('qa_tracks')
2301
+ .select('id, name, icon, test_template')
2302
+ .eq('project_id', currentProjectId);
2303
+
2304
+ // Get test case count
2305
+ const { count: testCaseCount } = await supabase
1245
2306
  .from('test_cases')
1246
2307
  .select('id', { count: 'exact', head: true })
1247
- .eq('project_id', PROJECT_ID);
2308
+ .eq('project_id', currentProjectId);
1248
2309
 
1249
2310
  // Get open bug count
1250
2311
  const { count: openBugCount } = await supabase
1251
2312
  .from('reports')
1252
2313
  .select('id', { count: 'exact', head: true })
1253
- .eq('project_id', PROJECT_ID)
2314
+ .eq('project_id', currentProjectId)
1254
2315
  .eq('report_type', 'bug')
1255
2316
  .in('status', ['new', 'confirmed', 'in_progress']);
1256
2317
 
@@ -1279,7 +2340,7 @@ async function getQaTracks() {
1279
2340
  const { data, error } = await supabase
1280
2341
  .from('qa_tracks')
1281
2342
  .select('*')
1282
- .eq('project_id', PROJECT_ID)
2343
+ .eq('project_id', currentProjectId)
1283
2344
  .order('sort_order');
1284
2345
 
1285
2346
  if (error) {
@@ -1320,7 +2381,7 @@ async function createTestCase(args: {
1320
2381
  const { data: trackData } = await supabase
1321
2382
  .from('qa_tracks')
1322
2383
  .select('id')
1323
- .eq('project_id', PROJECT_ID)
2384
+ .eq('project_id', currentProjectId)
1324
2385
  .ilike('name', `%${sanitizedTrack}%`)
1325
2386
  .single();
1326
2387
  trackId = trackData?.id || null;
@@ -1328,7 +2389,7 @@ async function createTestCase(args: {
1328
2389
  }
1329
2390
 
1330
2391
  const testCase = {
1331
- project_id: PROJECT_ID,
2392
+ project_id: currentProjectId,
1332
2393
  test_key: args.test_key,
1333
2394
  title: args.title,
1334
2395
  description: args.description || '',
@@ -1383,7 +2444,7 @@ async function updateTestCase(args: {
1383
2444
  const { data: existing } = await supabase
1384
2445
  .from('test_cases')
1385
2446
  .select('id')
1386
- .eq('project_id', PROJECT_ID)
2447
+ .eq('project_id', currentProjectId)
1387
2448
  .eq('test_key', args.test_key)
1388
2449
  .single();
1389
2450
 
@@ -1411,7 +2472,7 @@ async function updateTestCase(args: {
1411
2472
  .from('test_cases')
1412
2473
  .update(updates)
1413
2474
  .eq('id', testCaseId)
1414
- .eq('project_id', PROJECT_ID)
2475
+ .eq('project_id', currentProjectId)
1415
2476
  .select('id, test_key, title, target_route')
1416
2477
  .single();
1417
2478
 
@@ -1468,7 +2529,7 @@ async function deleteTestCases(args: {
1468
2529
  const { data: existing } = await supabase
1469
2530
  .from('test_cases')
1470
2531
  .select('id')
1471
- .eq('project_id', PROJECT_ID)
2532
+ .eq('project_id', currentProjectId)
1472
2533
  .eq('test_key', args.test_key)
1473
2534
  .single();
1474
2535
 
@@ -1504,7 +2565,7 @@ async function deleteTestCases(args: {
1504
2565
  const { data: existing, error: lookupError } = await supabase
1505
2566
  .from('test_cases')
1506
2567
  .select('id, test_key')
1507
- .eq('project_id', PROJECT_ID)
2568
+ .eq('project_id', currentProjectId)
1508
2569
  .in('test_key', args.test_keys);
1509
2570
 
1510
2571
  if (lookupError) {
@@ -1523,7 +2584,7 @@ async function deleteTestCases(args: {
1523
2584
  const { data: toDelete } = await supabase
1524
2585
  .from('test_cases')
1525
2586
  .select('id, test_key, title')
1526
- .eq('project_id', PROJECT_ID)
2587
+ .eq('project_id', currentProjectId)
1527
2588
  .in('id', idsToDelete);
1528
2589
 
1529
2590
  if (!toDelete || toDelete.length === 0) {
@@ -1534,7 +2595,7 @@ async function deleteTestCases(args: {
1534
2595
  const { error: deleteError } = await supabase
1535
2596
  .from('test_cases')
1536
2597
  .delete()
1537
- .eq('project_id', PROJECT_ID)
2598
+ .eq('project_id', currentProjectId)
1538
2599
  .in('id', idsToDelete);
1539
2600
 
1540
2601
  if (deleteError) {
@@ -1562,6 +2623,7 @@ async function listTestCases(args: {
1562
2623
  missing_target_route?: boolean;
1563
2624
  limit?: number;
1564
2625
  offset?: number;
2626
+ compact?: boolean;
1565
2627
  }) {
1566
2628
  let query = supabase
1567
2629
  .from('test_cases')
@@ -1577,7 +2639,7 @@ async function listTestCases(args: {
1577
2639
  steps,
1578
2640
  track:qa_tracks(id, name, icon, color)
1579
2641
  `)
1580
- .eq('project_id', PROJECT_ID)
2642
+ .eq('project_id', currentProjectId)
1581
2643
  .order('test_key', { ascending: true });
1582
2644
 
1583
2645
  // Apply filters
@@ -1608,6 +2670,20 @@ async function listTestCases(args: {
1608
2670
  );
1609
2671
  }
1610
2672
 
2673
+ // Compact: return minimal fields only
2674
+ if (args.compact === true) {
2675
+ return {
2676
+ count: testCases.length,
2677
+ testCases: testCases.map((tc: any) => ({
2678
+ id: tc.id,
2679
+ testKey: tc.test_key,
2680
+ title: tc.title,
2681
+ priority: tc.priority,
2682
+ })),
2683
+ pagination: { limit, offset, hasMore: testCases.length === limit },
2684
+ };
2685
+ }
2686
+
1611
2687
  return {
1612
2688
  count: testCases.length,
1613
2689
  testCases: testCases.map((tc: any) => ({
@@ -1634,7 +2710,7 @@ async function getBugPatterns(args: { route?: string }) {
1634
2710
  let query = supabase
1635
2711
  .from('reports')
1636
2712
  .select('app_context, severity, status, created_at')
1637
- .eq('project_id', PROJECT_ID)
2713
+ .eq('project_id', currentProjectId)
1638
2714
  .eq('report_type', 'bug')
1639
2715
  .order('created_at', { ascending: false })
1640
2716
  .limit(100);
@@ -1704,7 +2780,7 @@ async function suggestTestCases(args: {
1704
2780
  const { data: existingTests } = await supabase
1705
2781
  .from('test_cases')
1706
2782
  .select('test_key, title')
1707
- .eq('project_id', PROJECT_ID)
2783
+ .eq('project_id', currentProjectId)
1708
2784
  .order('test_key', { ascending: false })
1709
2785
  .limit(1);
1710
2786
 
@@ -1755,7 +2831,7 @@ async function suggestTestCases(args: {
1755
2831
  const { data: relatedBugs } = await supabase
1756
2832
  .from('reports')
1757
2833
  .select('id, description, severity')
1758
- .eq('project_id', PROJECT_ID)
2834
+ .eq('project_id', currentProjectId)
1759
2835
  .eq('report_type', 'bug')
1760
2836
  .limit(10);
1761
2837
 
@@ -1798,7 +2874,7 @@ async function getTestPriorities(args: {
1798
2874
  const includeFactors = args.include_factors !== false;
1799
2875
 
1800
2876
  // First, refresh the route stats
1801
- const { error: refreshError } = await supabase.rpc('refresh_route_test_stats', { p_project_id: PROJECT_ID });
2877
+ const { error: refreshError } = await supabase.rpc('refresh_route_test_stats', { p_project_id: currentProjectId });
1802
2878
  if (refreshError) {
1803
2879
  // Non-fatal: proceed with potentially stale data but warn
1804
2880
  console.warn('Failed to refresh route stats:', refreshError.message);
@@ -1808,7 +2884,7 @@ async function getTestPriorities(args: {
1808
2884
  const { data: routes, error } = await supabase
1809
2885
  .from('route_test_stats')
1810
2886
  .select('*')
1811
- .eq('project_id', PROJECT_ID)
2887
+ .eq('project_id', currentProjectId)
1812
2888
  .gte('priority_score', minScore)
1813
2889
  .order('priority_score', { ascending: false })
1814
2890
  .limit(limit);
@@ -1942,7 +3018,7 @@ async function getCoverageGaps(args: {
1942
3018
  const { data: routesFromReports } = await supabase
1943
3019
  .from('reports')
1944
3020
  .select('app_context')
1945
- .eq('project_id', PROJECT_ID)
3021
+ .eq('project_id', currentProjectId)
1946
3022
  .not('app_context->currentRoute', 'is', null);
1947
3023
 
1948
3024
  const allRoutes = new Set<string>();
@@ -1955,7 +3031,7 @@ async function getCoverageGaps(args: {
1955
3031
  const { data: testCases } = await supabase
1956
3032
  .from('test_cases')
1957
3033
  .select('target_route, category, track_id')
1958
- .eq('project_id', PROJECT_ID);
3034
+ .eq('project_id', currentProjectId);
1959
3035
 
1960
3036
  const coveredRoutes = new Set<string>();
1961
3037
  const routeTrackCoverage: Record<string, Set<string>> = {};
@@ -1973,7 +3049,7 @@ async function getCoverageGaps(args: {
1973
3049
  const { data: tracks } = await supabase
1974
3050
  .from('qa_tracks')
1975
3051
  .select('id, name')
1976
- .eq('project_id', PROJECT_ID);
3052
+ .eq('project_id', currentProjectId);
1977
3053
 
1978
3054
  const trackMap = new Map((tracks || []).map(t => [t.id, t.name]));
1979
3055
 
@@ -1981,7 +3057,7 @@ async function getCoverageGaps(args: {
1981
3057
  const { data: routeStats } = await supabase
1982
3058
  .from('route_test_stats')
1983
3059
  .select('route, last_tested_at, open_bugs, critical_bugs')
1984
- .eq('project_id', PROJECT_ID);
3060
+ .eq('project_id', currentProjectId);
1985
3061
 
1986
3062
  const routeStatsMap = new Map((routeStats || []).map(r => [r.route, r]));
1987
3063
 
@@ -2092,7 +3168,7 @@ async function getRegressions(args: {
2092
3168
  const { data: resolvedBugs } = await supabase
2093
3169
  .from('reports')
2094
3170
  .select('id, description, severity, app_context, resolved_at')
2095
- .eq('project_id', PROJECT_ID)
3171
+ .eq('project_id', currentProjectId)
2096
3172
  .eq('report_type', 'bug')
2097
3173
  .in('status', ['resolved', 'fixed', 'verified', 'closed'])
2098
3174
  .gte('resolved_at', new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString());
@@ -2100,7 +3176,7 @@ async function getRegressions(args: {
2100
3176
  const { data: newBugs } = await supabase
2101
3177
  .from('reports')
2102
3178
  .select('id, description, severity, app_context, created_at')
2103
- .eq('project_id', PROJECT_ID)
3179
+ .eq('project_id', currentProjectId)
2104
3180
  .eq('report_type', 'bug')
2105
3181
  .in('status', ['new', 'triaging', 'confirmed', 'in_progress', 'reviewed'])
2106
3182
  .gte('created_at', new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString());
@@ -2219,21 +3295,21 @@ async function getCoverageMatrix(args: {
2219
3295
  include_execution_data?: boolean;
2220
3296
  include_bug_counts?: boolean;
2221
3297
  }) {
2222
- const includeExecution = args.include_execution_data !== false;
2223
- const includeBugs = args.include_bug_counts !== false;
3298
+ const includeExecution = args.include_execution_data === true;
3299
+ const includeBugs = args.include_bug_counts === true;
2224
3300
 
2225
3301
  // Get tracks
2226
3302
  const { data: tracks } = await supabase
2227
3303
  .from('qa_tracks')
2228
3304
  .select('id, name, icon, color')
2229
- .eq('project_id', PROJECT_ID)
3305
+ .eq('project_id', currentProjectId)
2230
3306
  .order('sort_order');
2231
3307
 
2232
3308
  // Get test cases with track info
2233
3309
  const { data: testCases } = await supabase
2234
3310
  .from('test_cases')
2235
3311
  .select('id, target_route, category, track_id')
2236
- .eq('project_id', PROJECT_ID);
3312
+ .eq('project_id', currentProjectId);
2237
3313
 
2238
3314
  // Get test assignments for execution data
2239
3315
  let assignments: any[] = [];
@@ -2241,7 +3317,7 @@ async function getCoverageMatrix(args: {
2241
3317
  const { data } = await supabase
2242
3318
  .from('test_assignments')
2243
3319
  .select('test_case_id, status, completed_at')
2244
- .eq('project_id', PROJECT_ID)
3320
+ .eq('project_id', currentProjectId)
2245
3321
  .in('status', ['passed', 'failed'])
2246
3322
  .order('completed_at', { ascending: false })
2247
3323
  .limit(2000);
@@ -2254,7 +3330,7 @@ async function getCoverageMatrix(args: {
2254
3330
  const { data } = await supabase
2255
3331
  .from('route_test_stats')
2256
3332
  .select('route, open_bugs, critical_bugs')
2257
- .eq('project_id', PROJECT_ID);
3333
+ .eq('project_id', currentProjectId);
2258
3334
  routeStats = data || [];
2259
3335
  }
2260
3336
  const routeStatsMap = new Map(routeStats.map(r => [r.route, r]));
@@ -2416,7 +3492,7 @@ async function getStaleCoverage(args: {
2416
3492
  const limit = args.limit || 20;
2417
3493
 
2418
3494
  // Refresh stats first
2419
- const { error: refreshError } = await supabase.rpc('refresh_route_test_stats', { p_project_id: PROJECT_ID });
3495
+ const { error: refreshError } = await supabase.rpc('refresh_route_test_stats', { p_project_id: currentProjectId });
2420
3496
  if (refreshError) {
2421
3497
  // Non-fatal: proceed with potentially stale data but warn
2422
3498
  console.warn('Failed to refresh route stats:', refreshError.message);
@@ -2426,7 +3502,7 @@ async function getStaleCoverage(args: {
2426
3502
  const { data: routes, error } = await supabase
2427
3503
  .from('route_test_stats')
2428
3504
  .select('route, last_tested_at, open_bugs, critical_bugs, test_case_count, priority_score')
2429
- .eq('project_id', PROJECT_ID)
3505
+ .eq('project_id', currentProjectId)
2430
3506
  .order('last_tested_at', { ascending: true, nullsFirst: true })
2431
3507
  .limit(limit * 2); // Get extra to filter
2432
3508
 
@@ -2524,12 +3600,12 @@ async function generateDeployChecklist(args: {
2524
3600
  supabase
2525
3601
  .from('test_cases')
2526
3602
  .select('id, test_key, title, target_route, category, priority, track:qa_tracks(name)')
2527
- .eq('project_id', PROJECT_ID)
3603
+ .eq('project_id', currentProjectId)
2528
3604
  .in('target_route', safeRoutes),
2529
3605
  supabase
2530
3606
  .from('test_cases')
2531
3607
  .select('id, test_key, title, target_route, category, priority, track:qa_tracks(name)')
2532
- .eq('project_id', PROJECT_ID)
3608
+ .eq('project_id', currentProjectId)
2533
3609
  .in('category', safeRoutes),
2534
3610
  ]);
2535
3611
 
@@ -2545,7 +3621,7 @@ async function generateDeployChecklist(args: {
2545
3621
  const { data: routeStats } = await supabase
2546
3622
  .from('route_test_stats')
2547
3623
  .select('*')
2548
- .eq('project_id', PROJECT_ID)
3624
+ .eq('project_id', currentProjectId)
2549
3625
  .in('route', Array.from(allRoutes));
2550
3626
 
2551
3627
  const routeStatsMap = new Map((routeStats || []).map(r => [r.route, r]));
@@ -2661,21 +3737,21 @@ async function getQAHealth(args: {
2661
3737
  const { data: currentTests } = await supabase
2662
3738
  .from('test_assignments')
2663
3739
  .select('id, status, completed_at')
2664
- .eq('project_id', PROJECT_ID)
3740
+ .eq('project_id', currentProjectId)
2665
3741
  .gte('completed_at', periodStart.toISOString())
2666
3742
  .in('status', ['passed', 'failed']);
2667
3743
 
2668
3744
  const { data: currentBugs } = await supabase
2669
3745
  .from('reports')
2670
3746
  .select('id, severity, status, created_at')
2671
- .eq('project_id', PROJECT_ID)
3747
+ .eq('project_id', currentProjectId)
2672
3748
  .eq('report_type', 'bug')
2673
3749
  .gte('created_at', periodStart.toISOString());
2674
3750
 
2675
3751
  const { data: resolvedBugs } = await supabase
2676
3752
  .from('reports')
2677
3753
  .select('id, created_at, resolved_at')
2678
- .eq('project_id', PROJECT_ID)
3754
+ .eq('project_id', currentProjectId)
2679
3755
  .eq('report_type', 'bug')
2680
3756
  .in('status', ['resolved', 'fixed', 'verified', 'closed'])
2681
3757
  .gte('resolved_at', periodStart.toISOString());
@@ -2683,12 +3759,12 @@ async function getQAHealth(args: {
2683
3759
  const { data: testers } = await supabase
2684
3760
  .from('testers')
2685
3761
  .select('id, status')
2686
- .eq('project_id', PROJECT_ID);
3762
+ .eq('project_id', currentProjectId);
2687
3763
 
2688
3764
  const { data: routeStats } = await supabase
2689
3765
  .from('route_test_stats')
2690
3766
  .select('route, test_case_count')
2691
- .eq('project_id', PROJECT_ID);
3767
+ .eq('project_id', currentProjectId);
2692
3768
 
2693
3769
  // Get previous period data for comparison
2694
3770
  let previousTests: any[] = [];
@@ -2699,7 +3775,7 @@ async function getQAHealth(args: {
2699
3775
  const { data: pt } = await supabase
2700
3776
  .from('test_assignments')
2701
3777
  .select('id, status')
2702
- .eq('project_id', PROJECT_ID)
3778
+ .eq('project_id', currentProjectId)
2703
3779
  .gte('completed_at', previousStart.toISOString())
2704
3780
  .lt('completed_at', periodStart.toISOString())
2705
3781
  .in('status', ['passed', 'failed']);
@@ -2708,7 +3784,7 @@ async function getQAHealth(args: {
2708
3784
  const { data: pb } = await supabase
2709
3785
  .from('reports')
2710
3786
  .select('id, severity')
2711
- .eq('project_id', PROJECT_ID)
3787
+ .eq('project_id', currentProjectId)
2712
3788
  .eq('report_type', 'bug')
2713
3789
  .gte('created_at', previousStart.toISOString())
2714
3790
  .lt('created_at', periodStart.toISOString());
@@ -2717,7 +3793,7 @@ async function getQAHealth(args: {
2717
3793
  const { data: pr } = await supabase
2718
3794
  .from('reports')
2719
3795
  .select('id')
2720
- .eq('project_id', PROJECT_ID)
3796
+ .eq('project_id', currentProjectId)
2721
3797
  .in('status', ['resolved', 'fixed', 'verified', 'closed'])
2722
3798
  .gte('resolved_at', previousStart.toISOString())
2723
3799
  .lt('resolved_at', periodStart.toISOString());
@@ -2888,7 +3964,7 @@ async function getQASessions(args: {
2888
3964
  findings_count, bugs_filed, created_at,
2889
3965
  tester:testers(id, name, email)
2890
3966
  `)
2891
- .eq('project_id', PROJECT_ID)
3967
+ .eq('project_id', currentProjectId)
2892
3968
  .order('started_at', { ascending: false })
2893
3969
  .limit(limit);
2894
3970
 
@@ -2954,13 +4030,13 @@ async function getQAAlerts(args: {
2954
4030
 
2955
4031
  // Optionally refresh alerts
2956
4032
  if (args.refresh) {
2957
- await supabase.rpc('detect_all_alerts', { p_project_id: PROJECT_ID });
4033
+ await supabase.rpc('detect_all_alerts', { p_project_id: currentProjectId });
2958
4034
  }
2959
4035
 
2960
4036
  let query = supabase
2961
4037
  .from('qa_alerts')
2962
4038
  .select('*')
2963
- .eq('project_id', PROJECT_ID)
4039
+ .eq('project_id', currentProjectId)
2964
4040
  .order('severity', { ascending: true }) // critical first
2965
4041
  .order('created_at', { ascending: false });
2966
4042
 
@@ -3025,7 +4101,7 @@ async function getDeploymentAnalysis(args: {
3025
4101
  .from('deployments')
3026
4102
  .select('*')
3027
4103
  .eq('id', args.deployment_id)
3028
- .eq('project_id', PROJECT_ID)
4104
+ .eq('project_id', currentProjectId)
3029
4105
  .single();
3030
4106
 
3031
4107
  if (error) {
@@ -3039,7 +4115,7 @@ async function getDeploymentAnalysis(args: {
3039
4115
  let query = supabase
3040
4116
  .from('deployments')
3041
4117
  .select('*')
3042
- .eq('project_id', PROJECT_ID)
4118
+ .eq('project_id', currentProjectId)
3043
4119
  .order('deployed_at', { ascending: false })
3044
4120
  .limit(limit);
3045
4121
 
@@ -3134,7 +4210,7 @@ async function analyzeCommitForTesting(args: {
3134
4210
  const { data: mappings } = await supabase
3135
4211
  .from('file_route_mapping')
3136
4212
  .select('file_pattern, route, feature, confidence')
3137
- .eq('project_id', PROJECT_ID);
4213
+ .eq('project_id', currentProjectId);
3138
4214
 
3139
4215
  const affectedRoutes: Array<{ route: string; feature?: string; confidence: number; matched_files: string[] }> = [];
3140
4216
 
@@ -3171,7 +4247,7 @@ async function analyzeCommitForTesting(args: {
3171
4247
  const { data: bugs } = await supabase
3172
4248
  .from('reports')
3173
4249
  .select('id, severity, description, route, created_at')
3174
- .eq('project_id', PROJECT_ID)
4250
+ .eq('project_id', currentProjectId)
3175
4251
  .eq('report_type', 'bug')
3176
4252
  .in('route', routes)
3177
4253
  .gte('created_at', new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString());
@@ -3208,7 +4284,7 @@ async function analyzeCommitForTesting(args: {
3208
4284
  // Optionally record as deployment
3209
4285
  if (args.record_deployment) {
3210
4286
  await supabase.rpc('record_deployment', {
3211
- p_project_id: PROJECT_ID,
4287
+ p_project_id: currentProjectId,
3212
4288
  p_environment: 'production',
3213
4289
  p_commit_sha: args.commit_sha || null,
3214
4290
  p_commit_message: args.commit_message || null,
@@ -3308,13 +4384,13 @@ async function analyzeChangesForTests(args: {
3308
4384
  const { data: existingTests } = await supabase
3309
4385
  .from('test_cases')
3310
4386
  .select('test_key, title, target_route, description')
3311
- .eq('project_id', PROJECT_ID);
4387
+ .eq('project_id', currentProjectId);
3312
4388
 
3313
4389
  // Get next test key
3314
4390
  const { data: lastTest } = await supabase
3315
4391
  .from('test_cases')
3316
4392
  .select('test_key')
3317
- .eq('project_id', PROJECT_ID)
4393
+ .eq('project_id', currentProjectId)
3318
4394
  .order('test_key', { ascending: false })
3319
4395
  .limit(1);
3320
4396
 
@@ -3329,7 +4405,7 @@ async function analyzeChangesForTests(args: {
3329
4405
  const { data: bugs } = await supabase
3330
4406
  .from('reports')
3331
4407
  .select('id, description, severity, app_context')
3332
- .eq('project_id', PROJECT_ID)
4408
+ .eq('project_id', currentProjectId)
3333
4409
  .eq('report_type', 'bug')
3334
4410
  .limit(50);
3335
4411
 
@@ -3731,7 +4807,7 @@ async function createBugReport(args: {
3731
4807
  const { data: project } = await supabase
3732
4808
  .from('projects')
3733
4809
  .select('owner_id')
3734
- .eq('id', PROJECT_ID)
4810
+ .eq('id', currentProjectId)
3735
4811
  .single();
3736
4812
  if (project?.owner_id) {
3737
4813
  reporterId = project.owner_id;
@@ -3740,7 +4816,7 @@ async function createBugReport(args: {
3740
4816
  const { data: testers } = await supabase
3741
4817
  .from('testers')
3742
4818
  .select('id')
3743
- .eq('project_id', PROJECT_ID)
4819
+ .eq('project_id', currentProjectId)
3744
4820
  .limit(1);
3745
4821
  if (testers && testers.length > 0) {
3746
4822
  reporterId = testers[0].id;
@@ -3748,7 +4824,7 @@ async function createBugReport(args: {
3748
4824
  }
3749
4825
 
3750
4826
  const report: Record<string, unknown> = {
3751
- project_id: PROJECT_ID,
4827
+ project_id: currentProjectId,
3752
4828
  report_type: 'bug',
3753
4829
  title: args.title,
3754
4830
  description: args.description,
@@ -3823,7 +4899,7 @@ async function getBugsForFile(args: {
3823
4899
  let query = supabase
3824
4900
  .from('reports')
3825
4901
  .select('id, title, description, severity, status, created_at, code_context')
3826
- .eq('project_id', PROJECT_ID)
4902
+ .eq('project_id', currentProjectId)
3827
4903
  .eq('report_type', 'bug');
3828
4904
 
3829
4905
  if (!args.include_resolved) {
@@ -3906,7 +4982,7 @@ async function markFixedWithCommit(args: {
3906
4982
  .from('reports')
3907
4983
  .select('code_context')
3908
4984
  .eq('id', args.report_id)
3909
- .eq('project_id', PROJECT_ID) // Security: ensure report belongs to this project
4985
+ .eq('project_id', currentProjectId) // Security: ensure report belongs to this project
3910
4986
  .single();
3911
4987
 
3912
4988
  if (fetchError) {
@@ -3919,7 +4995,7 @@ async function markFixedWithCommit(args: {
3919
4995
  status: 'resolved',
3920
4996
  resolved_at: new Date().toISOString(),
3921
4997
  resolution_notes: args.resolution_notes || `Fixed in commit ${args.commit_sha.slice(0, 7)}`,
3922
- notify_tester: args.notify_tester === true, // Opt-in: only notify if explicitly requested
4998
+ notify_tester: args.notify_tester !== false, // Default: notify tester. Pass false to silently resolve.
3923
4999
  code_context: {
3924
5000
  ...existingContext,
3925
5001
  fix: {
@@ -3936,13 +5012,14 @@ async function markFixedWithCommit(args: {
3936
5012
  .from('reports')
3937
5013
  .update(updates)
3938
5014
  .eq('id', args.report_id)
3939
- .eq('project_id', PROJECT_ID); // Security: ensure report belongs to this project
5015
+ .eq('project_id', currentProjectId); // Security: ensure report belongs to this project
3940
5016
 
3941
5017
  if (error) {
3942
5018
  return { error: error.message };
3943
5019
  }
3944
5020
 
3945
- const notificationStatus = args.notify_tester
5021
+ const notifyTester = args.notify_tester !== false;
5022
+ const notificationStatus = notifyTester
3946
5023
  ? 'The original tester will be notified and assigned a verification task.'
3947
5024
  : 'No notification sent (silent resolve). A verification task was created.';
3948
5025
 
@@ -3951,7 +5028,7 @@ async function markFixedWithCommit(args: {
3951
5028
  message: `Bug marked as fixed in commit ${args.commit_sha.slice(0, 7)}. ${notificationStatus}`,
3952
5029
  report_id: args.report_id,
3953
5030
  commit: args.commit_sha,
3954
- tester_notified: args.notify_tester === true,
5031
+ tester_notified: notifyTester,
3955
5032
  next_steps: [
3956
5033
  'Consider running create_regression_test to prevent this bug from recurring',
3957
5034
  'Push your changes to trigger CI/CD',
@@ -3968,7 +5045,7 @@ async function getBugsAffectingCode(args: {
3968
5045
  const { data, error } = await supabase
3969
5046
  .from('reports')
3970
5047
  .select('id, title, description, severity, status, code_context, app_context')
3971
- .eq('project_id', PROJECT_ID)
5048
+ .eq('project_id', currentProjectId)
3972
5049
  .eq('report_type', 'bug')
3973
5050
  .in('status', ['new', 'confirmed', 'in_progress', 'reviewed'])
3974
5051
  .order('severity', { ascending: true });
@@ -4099,7 +5176,7 @@ async function linkBugToCode(args: {
4099
5176
  .from('reports')
4100
5177
  .select('code_context')
4101
5178
  .eq('id', args.report_id)
4102
- .eq('project_id', PROJECT_ID) // Security: ensure report belongs to this project
5179
+ .eq('project_id', currentProjectId) // Security: ensure report belongs to this project
4103
5180
  .single();
4104
5181
 
4105
5182
  if (fetchError) {
@@ -4124,7 +5201,7 @@ async function linkBugToCode(args: {
4124
5201
  .from('reports')
4125
5202
  .update(updates)
4126
5203
  .eq('id', args.report_id)
4127
- .eq('project_id', PROJECT_ID); // Security: ensure report belongs to this project
5204
+ .eq('project_id', currentProjectId); // Security: ensure report belongs to this project
4128
5205
 
4129
5206
  if (error) {
4130
5207
  return { error: error.message };
@@ -4150,7 +5227,7 @@ async function createRegressionTest(args: {
4150
5227
  .from('reports')
4151
5228
  .select('*')
4152
5229
  .eq('id', args.report_id)
4153
- .eq('project_id', PROJECT_ID) // Security: ensure report belongs to this project
5230
+ .eq('project_id', currentProjectId) // Security: ensure report belongs to this project
4154
5231
  .single();
4155
5232
 
4156
5233
  if (fetchError) {
@@ -4171,7 +5248,7 @@ async function createRegressionTest(args: {
4171
5248
  const { data: existingTests } = await supabase
4172
5249
  .from('test_cases')
4173
5250
  .select('test_key')
4174
- .eq('project_id', PROJECT_ID)
5251
+ .eq('project_id', currentProjectId)
4175
5252
  .order('test_key', { ascending: false })
4176
5253
  .limit(1);
4177
5254
 
@@ -4185,7 +5262,7 @@ async function createRegressionTest(args: {
4185
5262
 
4186
5263
  // Generate test case from bug
4187
5264
  const testCase = {
4188
- project_id: PROJECT_ID,
5265
+ project_id: currentProjectId,
4189
5266
  test_key: newKey,
4190
5267
  title: `Regression: ${report.title}`,
4191
5268
  description: `Regression test to prevent recurrence of bug #${args.report_id.slice(0, 8)}\n\nOriginal bug: ${report.description}`,
@@ -4270,7 +5347,7 @@ async function getPendingFixes(args: {
4270
5347
  created_at,
4271
5348
  report:reports(id, title, severity, description)
4272
5349
  `)
4273
- .eq('project_id', PROJECT_ID)
5350
+ .eq('project_id', currentProjectId)
4274
5351
  .order('created_at', { ascending: true })
4275
5352
  .limit(limit);
4276
5353
 
@@ -4329,7 +5406,7 @@ async function claimFixRequest(args: {
4329
5406
  .from('fix_requests')
4330
5407
  .select('id, status, claimed_by, prompt, title')
4331
5408
  .eq('id', args.fix_request_id)
4332
- .eq('project_id', PROJECT_ID) // Security: ensure fix request belongs to this project
5409
+ .eq('project_id', currentProjectId) // Security: ensure fix request belongs to this project
4333
5410
  .single();
4334
5411
 
4335
5412
  if (checkError) {
@@ -4360,7 +5437,7 @@ async function claimFixRequest(args: {
4360
5437
  claimed_by: claimedBy,
4361
5438
  })
4362
5439
  .eq('id', args.fix_request_id)
4363
- .eq('project_id', PROJECT_ID) // Security: ensure fix request belongs to this project
5440
+ .eq('project_id', currentProjectId) // Security: ensure fix request belongs to this project
4364
5441
  .eq('status', 'pending'); // Only claim if still pending (race condition protection)
4365
5442
 
4366
5443
  if (updateError) {
@@ -4405,7 +5482,7 @@ async function completeFixRequest(args: {
4405
5482
  .from('fix_requests')
4406
5483
  .update(updates)
4407
5484
  .eq('id', args.fix_request_id)
4408
- .eq('project_id', PROJECT_ID); // Security: ensure fix request belongs to this project
5485
+ .eq('project_id', currentProjectId); // Security: ensure fix request belongs to this project
4409
5486
 
4410
5487
  if (error) {
4411
5488
  return { error: error.message };
@@ -4493,7 +5570,7 @@ async function generatePromptContent(name: string, args: Record<string, string>)
4493
5570
  created_at,
4494
5571
  report:reports(id, title, severity)
4495
5572
  `)
4496
- .eq('project_id', PROJECT_ID)
5573
+ .eq('project_id', currentProjectId)
4497
5574
  .eq('status', 'pending')
4498
5575
  .order('created_at', { ascending: true })
4499
5576
  .limit(5);
@@ -4502,7 +5579,7 @@ async function generatePromptContent(name: string, args: Record<string, string>)
4502
5579
  let query = supabase
4503
5580
  .from('reports')
4504
5581
  .select('id, title, description, severity, status, code_context, created_at')
4505
- .eq('project_id', PROJECT_ID)
5582
+ .eq('project_id', currentProjectId)
4506
5583
  .eq('report_type', 'bug')
4507
5584
  .in('status', ['new', 'confirmed', 'in_progress']);
4508
5585
 
@@ -4667,542 +5744,1159 @@ Would you like me to generate test cases for these files?`;
4667
5744
  const { data: resolvedBugs } = await supabase
4668
5745
  .from('reports')
4669
5746
  .select('id, title, description, severity, resolved_at, code_context')
4670
- .eq('project_id', PROJECT_ID)
5747
+ .eq('project_id', currentProjectId)
4671
5748
  .eq('report_type', 'bug')
4672
5749
  .eq('status', 'resolved')
4673
5750
  .order('resolved_at', { ascending: false })
4674
5751
  .limit(limit);
4675
5752
 
4676
- if (!resolvedBugs || resolvedBugs.length === 0) {
4677
- return `# Regression Test Generation
5753
+ if (!resolvedBugs || resolvedBugs.length === 0) {
5754
+ return `# Regression Test Generation
5755
+
5756
+ No recently resolved bugs found.
5757
+
5758
+ To create regression tests:
5759
+ 1. First fix some bugs and mark them as resolved using \`mark_fixed_with_commit\`
5760
+ 2. Then come back here to generate regression tests
5761
+
5762
+ Alternatively, use \`suggest_test_cases\` to generate general test cases.`;
5763
+ }
5764
+
5765
+ // Check which ones already have regression tests
5766
+ const bugList = resolvedBugs.map((b, i) => {
5767
+ const ctx = b.code_context as Record<string, unknown> | null;
5768
+ const hasCommit = ctx?.fix && (ctx.fix as any).commit_sha;
5769
+
5770
+ return `
5771
+ ## ${i + 1}. ${b.title}
5772
+ - **Bug ID:** \`${b.id}\`
5773
+ - **Severity:** ${b.severity}
5774
+ - **Resolved:** ${b.resolved_at ? new Date(b.resolved_at).toLocaleDateString() : 'Unknown'}
5775
+ - **Commit:** ${hasCommit ? (ctx!.fix as any).commit_sha.slice(0, 7) : 'Not linked'}
5776
+ `;
5777
+ }).join('\n');
5778
+
5779
+ return `# Regression Test Generation
5780
+
5781
+ Found **${resolvedBugs.length}** recently resolved bug(s) that need regression tests:
5782
+
5783
+ ${bugList}
5784
+
5785
+ ---
5786
+
5787
+ ## Generate Tests
5788
+
5789
+ For each bug, I can create a regression test to prevent it from recurring.
5790
+
5791
+ Use \`create_regression_test\` with the bug ID to generate a test case.
5792
+
5793
+ Example:
5794
+ \`\`\`
5795
+ create_regression_test(report_id: "${resolvedBugs[0]?.id}", test_type: "integration")
5796
+ \`\`\`
5797
+
5798
+ Would you like me to generate regression tests for all of these bugs?`;
5799
+ }
5800
+
5801
+ case 'bug_hunt': {
5802
+ const focus = args.focus || 'general';
5803
+
5804
+ const focusDescriptions: Record<string, string> = {
5805
+ error_handling: 'missing try/catch blocks, unhandled promise rejections, missing error boundaries',
5806
+ null_checks: 'potential null/undefined access, optional chaining opportunities, nullish coalescing',
5807
+ async_issues: 'race conditions, missing await, unhandled async errors, memory leaks',
5808
+ type_safety: 'type assertions, any types, missing type guards, unsafe casts',
5809
+ security: 'XSS vulnerabilities, injection risks, exposed secrets, insecure data handling',
5810
+ general: 'common patterns that have caused bugs in this project',
5811
+ };
5812
+
5813
+ // Get bug patterns for context
5814
+ const patterns = await getBugPatterns({});
5815
+
5816
+ return `# Bug Hunt Mode: ${focus.charAt(0).toUpperCase() + focus.slice(1)}
5817
+
5818
+ I'm looking for: **${focusDescriptions[focus] || focusDescriptions.general}**
5819
+
5820
+ ## Historical Bug Hotspots
5821
+
5822
+ ${(patterns as any).hotspots?.slice(0, 5).map((h: any) =>
5823
+ `- **${h.route}**: ${h.total} bugs (${h.open} open, ${h.critical} critical)`
5824
+ ).join('\n') || 'No bug patterns found yet.'}
5825
+
5826
+ ## Bug Hunt Checklist
5827
+
5828
+ Based on past bugs in this project, here's what to look for:
5829
+
5830
+ ${focus === 'error_handling' || focus === 'general' ? `
5831
+ ### Error Handling
5832
+ - [ ] All async functions have try/catch or .catch()
5833
+ - [ ] API calls handle network errors
5834
+ - [ ] User-facing error messages are helpful
5835
+ - [ ] Errors are logged appropriately
5836
+ ` : ''}
5837
+
5838
+ ${focus === 'null_checks' || focus === 'general' ? `
5839
+ ### Null Safety
5840
+ - [ ] Optional chaining (?.) used for potentially undefined values
5841
+ - [ ] Nullish coalescing (??) for default values
5842
+ - [ ] Array/object access is guarded
5843
+ - [ ] Props have appropriate defaults
5844
+ ` : ''}
5845
+
5846
+ ${focus === 'async_issues' || focus === 'general' ? `
5847
+ ### Async Issues
5848
+ - [ ] All Promises are awaited or have .then()
5849
+ - [ ] Cleanup functions in useEffect
5850
+ - [ ] Race conditions prevented with AbortController
5851
+ - [ ] Loading states are handled
5852
+ ` : ''}
5853
+
5854
+ ---
5855
+
5856
+ **To report bugs you find:** Use \`create_bug_report\` with the file, line number, and code snippet.
5857
+
5858
+ Which files or areas would you like me to analyze?`;
5859
+ }
5860
+
5861
+ default:
5862
+ return 'Unknown prompt';
5863
+ }
5864
+ }
5865
+
5866
+ // === TESTER & ASSIGNMENT MANAGEMENT HANDLERS ===
5867
+
5868
+ async function listTesters(args: {
5869
+ status?: string;
5870
+ platform?: string;
5871
+ role?: string;
5872
+ }) {
5873
+ let query = supabase
5874
+ .from('testers')
5875
+ .select('id, name, email, status, platforms, tier, assigned_count, completed_count, notes, role, created_at')
5876
+ .eq('project_id', currentProjectId)
5877
+ .order('name', { ascending: true });
5878
+
5879
+ if (args.status) {
5880
+ query = query.eq('status', args.status);
5881
+ }
5882
+ if (args.role) {
5883
+ query = query.eq('role', args.role);
5884
+ }
5885
+
5886
+ const { data, error } = await query;
5887
+
5888
+ if (error) {
5889
+ return { error: error.message };
5890
+ }
5891
+
5892
+ let testers = data || [];
5893
+
5894
+ // Filter by platform if specified (platforms is an array column)
5895
+ if (args.platform) {
5896
+ testers = testers.filter((t: any) =>
5897
+ t.platforms && t.platforms.includes(args.platform)
5898
+ );
5899
+ }
5900
+
5901
+ return {
5902
+ count: testers.length,
5903
+ testers: testers.map((t: any) => ({
5904
+ id: t.id,
5905
+ name: t.name,
5906
+ email: t.email,
5907
+ status: t.status,
5908
+ platforms: t.platforms,
5909
+ tier: t.tier,
5910
+ assignedCount: t.assigned_count,
5911
+ completedCount: t.completed_count,
5912
+ notes: t.notes,
5913
+ role: t.role,
5914
+ })),
5915
+ };
5916
+ }
5917
+
5918
+ async function listTestRuns(args: {
5919
+ status?: string;
5920
+ limit?: number;
5921
+ }) {
5922
+ const limit = Math.min(args.limit || 20, 50);
5923
+
5924
+ let query = supabase
5925
+ .from('test_runs')
5926
+ .select('id, name, description, status, total_tests, passed_tests, failed_tests, started_at, completed_at, created_at')
5927
+ .eq('project_id', currentProjectId)
5928
+ .order('created_at', { ascending: false })
5929
+ .limit(limit);
5930
+
5931
+ if (args.status) {
5932
+ query = query.eq('status', args.status);
5933
+ }
5934
+
5935
+ const { data, error } = await query;
5936
+
5937
+ if (error) {
5938
+ return { error: error.message };
5939
+ }
5940
+
5941
+ return {
5942
+ count: (data || []).length,
5943
+ testRuns: (data || []).map((r: any) => ({
5944
+ id: r.id,
5945
+ name: r.name,
5946
+ description: r.description,
5947
+ status: r.status,
5948
+ totalTests: r.total_tests,
5949
+ passedTests: r.passed_tests,
5950
+ failedTests: r.failed_tests,
5951
+ passRate: r.total_tests > 0 ? Math.round((r.passed_tests / r.total_tests) * 100) : 0,
5952
+ startedAt: r.started_at,
5953
+ completedAt: r.completed_at,
5954
+ createdAt: r.created_at,
5955
+ })),
5956
+ };
5957
+ }
5958
+
5959
+ async function createTestRun(args: {
5960
+ name: string;
5961
+ description?: string;
5962
+ }) {
5963
+ if (!args.name || args.name.trim().length === 0) {
5964
+ return { error: 'Test run name is required' };
5965
+ }
5966
+
5967
+ const { data, error } = await supabase
5968
+ .from('test_runs')
5969
+ .insert({
5970
+ project_id: currentProjectId,
5971
+ name: args.name.trim(),
5972
+ description: args.description?.trim() || null,
5973
+ status: 'draft',
5974
+ })
5975
+ .select('id, name, description, status, created_at')
5976
+ .single();
5977
+
5978
+ if (error) {
5979
+ return { error: error.message };
5980
+ }
5981
+
5982
+ return {
5983
+ success: true,
5984
+ testRun: {
5985
+ id: data.id,
5986
+ name: data.name,
5987
+ description: data.description,
5988
+ status: data.status,
5989
+ createdAt: data.created_at,
5990
+ },
5991
+ message: `Test run "${data.name}" created. Use assign_tests to add test assignments.`,
5992
+ };
5993
+ }
5994
+
5995
+ async function listTestAssignments(args: {
5996
+ tester_id?: string;
5997
+ test_run_id?: string;
5998
+ status?: string;
5999
+ limit?: number;
6000
+ }) {
6001
+ const limit = Math.min(args.limit || 50, 200);
6002
+
6003
+ if (args.tester_id && !isValidUUID(args.tester_id)) {
6004
+ return { error: 'Invalid tester_id format' };
6005
+ }
6006
+ if (args.test_run_id && !isValidUUID(args.test_run_id)) {
6007
+ return { error: 'Invalid test_run_id format' };
6008
+ }
4678
6009
 
4679
- No recently resolved bugs found.
6010
+ let query = supabase
6011
+ .from('test_assignments')
6012
+ .select(`
6013
+ id,
6014
+ status,
6015
+ assigned_at,
6016
+ started_at,
6017
+ completed_at,
6018
+ duration_seconds,
6019
+ is_verification,
6020
+ notes,
6021
+ test_case:test_cases(id, test_key, title, priority, target_route),
6022
+ tester:testers(id, name, email),
6023
+ test_run:test_runs(id, name)
6024
+ `)
6025
+ .eq('project_id', currentProjectId)
6026
+ .order('assigned_at', { ascending: false })
6027
+ .limit(limit);
4680
6028
 
4681
- To create regression tests:
4682
- 1. First fix some bugs and mark them as resolved using \`mark_fixed_with_commit\`
4683
- 2. Then come back here to generate regression tests
6029
+ if (args.tester_id) {
6030
+ query = query.eq('tester_id', args.tester_id);
6031
+ }
6032
+ if (args.test_run_id) {
6033
+ query = query.eq('test_run_id', args.test_run_id);
6034
+ }
6035
+ if (args.status) {
6036
+ query = query.eq('status', args.status);
6037
+ }
4684
6038
 
4685
- Alternatively, use \`suggest_test_cases\` to generate general test cases.`;
4686
- }
6039
+ const { data, error } = await query;
4687
6040
 
4688
- // Check which ones already have regression tests
4689
- const bugList = resolvedBugs.map((b, i) => {
4690
- const ctx = b.code_context as Record<string, unknown> | null;
4691
- const hasCommit = ctx?.fix && (ctx.fix as any).commit_sha;
6041
+ if (error) {
6042
+ return { error: error.message };
6043
+ }
4692
6044
 
4693
- return `
4694
- ## ${i + 1}. ${b.title}
4695
- - **Bug ID:** \`${b.id}\`
4696
- - **Severity:** ${b.severity}
4697
- - **Resolved:** ${b.resolved_at ? new Date(b.resolved_at).toLocaleDateString() : 'Unknown'}
4698
- - **Commit:** ${hasCommit ? (ctx!.fix as any).commit_sha.slice(0, 7) : 'Not linked'}
4699
- `;
4700
- }).join('\n');
6045
+ return {
6046
+ count: (data || []).length,
6047
+ assignments: (data || []).map((a: any) => ({
6048
+ id: a.id,
6049
+ status: a.status,
6050
+ assignedAt: a.assigned_at,
6051
+ startedAt: a.started_at,
6052
+ completedAt: a.completed_at,
6053
+ durationSeconds: a.duration_seconds,
6054
+ isVerification: a.is_verification,
6055
+ notes: a.notes,
6056
+ testCase: a.test_case ? {
6057
+ id: a.test_case.id,
6058
+ testKey: a.test_case.test_key,
6059
+ title: a.test_case.title,
6060
+ priority: a.test_case.priority,
6061
+ targetRoute: a.test_case.target_route,
6062
+ } : null,
6063
+ tester: a.tester ? {
6064
+ id: a.tester.id,
6065
+ name: a.tester.name,
6066
+ email: a.tester.email,
6067
+ } : null,
6068
+ testRun: a.test_run ? {
6069
+ id: a.test_run.id,
6070
+ name: a.test_run.name,
6071
+ } : null,
6072
+ })),
6073
+ };
6074
+ }
4701
6075
 
4702
- return `# Regression Test Generation
6076
+ async function assignTests(args: {
6077
+ tester_id: string;
6078
+ test_case_ids: string[];
6079
+ test_run_id?: string;
6080
+ }) {
6081
+ // Validate inputs
6082
+ if (!isValidUUID(args.tester_id)) {
6083
+ return { error: 'Invalid tester_id format' };
6084
+ }
6085
+ if (!args.test_case_ids || args.test_case_ids.length === 0) {
6086
+ return { error: 'At least one test_case_id is required' };
6087
+ }
6088
+ if (args.test_case_ids.length > 50) {
6089
+ return { error: 'Maximum 50 test cases per assignment batch' };
6090
+ }
6091
+ for (const id of args.test_case_ids) {
6092
+ if (!isValidUUID(id)) {
6093
+ return { error: `Invalid test_case_id format: ${id}` };
6094
+ }
6095
+ }
6096
+ if (args.test_run_id && !isValidUUID(args.test_run_id)) {
6097
+ return { error: 'Invalid test_run_id format' };
6098
+ }
4703
6099
 
4704
- Found **${resolvedBugs.length}** recently resolved bug(s) that need regression tests:
6100
+ // Verify tester exists and is active
6101
+ const { data: tester, error: testerErr } = await supabase
6102
+ .from('testers')
6103
+ .select('id, name, email, status')
6104
+ .eq('id', args.tester_id)
6105
+ .eq('project_id', currentProjectId)
6106
+ .single();
4705
6107
 
4706
- ${bugList}
6108
+ if (testerErr || !tester) {
6109
+ return { error: 'Tester not found in this project' };
6110
+ }
6111
+ if (tester.status !== 'active') {
6112
+ return { error: `Tester "${tester.name}" is ${tester.status}, not active` };
6113
+ }
4707
6114
 
4708
- ---
6115
+ // Verify test cases exist for this project
6116
+ const { data: testCases, error: tcErr } = await supabase
6117
+ .from('test_cases')
6118
+ .select('id, test_key, title')
6119
+ .eq('project_id', currentProjectId)
6120
+ .in('id', args.test_case_ids);
4709
6121
 
4710
- ## Generate Tests
6122
+ if (tcErr) {
6123
+ return { error: tcErr.message };
6124
+ }
4711
6125
 
4712
- For each bug, I can create a regression test to prevent it from recurring.
6126
+ const foundIds = new Set((testCases || []).map((tc: any) => tc.id));
6127
+ const missingIds = args.test_case_ids.filter(id => !foundIds.has(id));
4713
6128
 
4714
- Use \`create_regression_test\` with the bug ID to generate a test case.
6129
+ if (missingIds.length > 0) {
6130
+ return {
6131
+ error: `Test cases not found in this project: ${missingIds.join(', ')}`,
6132
+ };
6133
+ }
4715
6134
 
4716
- Example:
4717
- \`\`\`
4718
- create_regression_test(report_id: "${resolvedBugs[0]?.id}", test_type: "integration")
4719
- \`\`\`
6135
+ // Verify test run exists if provided
6136
+ if (args.test_run_id) {
6137
+ const { data: run, error: runErr } = await supabase
6138
+ .from('test_runs')
6139
+ .select('id')
6140
+ .eq('id', args.test_run_id)
6141
+ .eq('project_id', currentProjectId)
6142
+ .single();
4720
6143
 
4721
- Would you like me to generate regression tests for all of these bugs?`;
6144
+ if (runErr || !run) {
6145
+ return { error: 'Test run not found in this project' };
4722
6146
  }
6147
+ }
4723
6148
 
4724
- case 'bug_hunt': {
4725
- const focus = args.focus || 'general';
4726
-
4727
- const focusDescriptions: Record<string, string> = {
4728
- error_handling: 'missing try/catch blocks, unhandled promise rejections, missing error boundaries',
4729
- null_checks: 'potential null/undefined access, optional chaining opportunities, nullish coalescing',
4730
- async_issues: 'race conditions, missing await, unhandled async errors, memory leaks',
4731
- type_safety: 'type assertions, any types, missing type guards, unsafe casts',
4732
- security: 'XSS vulnerabilities, injection risks, exposed secrets, insecure data handling',
4733
- general: 'common patterns that have caused bugs in this project',
4734
- };
4735
-
4736
- // Get bug patterns for context
4737
- const patterns = await getBugPatterns({});
6149
+ // Build assignment rows
6150
+ const rows = args.test_case_ids.map(tcId => ({
6151
+ project_id: currentProjectId,
6152
+ test_case_id: tcId,
6153
+ tester_id: args.tester_id,
6154
+ test_run_id: args.test_run_id || null,
6155
+ status: 'pending',
6156
+ }));
4738
6157
 
4739
- return `# Bug Hunt Mode: ${focus.charAt(0).toUpperCase() + focus.slice(1)}
6158
+ // Helper: after assignments change, sync the test run's total_tests counter
6159
+ async function syncRunCounter() {
6160
+ if (!args.test_run_id) return;
6161
+ const { count } = await supabase
6162
+ .from('test_assignments')
6163
+ .select('id', { count: 'exact', head: true })
6164
+ .eq('test_run_id', args.test_run_id)
6165
+ .eq('project_id', currentProjectId);
6166
+ if (count !== null) {
6167
+ await supabase
6168
+ .from('test_runs')
6169
+ .update({ total_tests: count })
6170
+ .eq('id', args.test_run_id);
6171
+ }
6172
+ }
4740
6173
 
4741
- I'm looking for: **${focusDescriptions[focus] || focusDescriptions.general}**
6174
+ // Insert — use upsert-like approach: insert and handle conflicts
6175
+ const { data: inserted, error: insertErr } = await supabase
6176
+ .from('test_assignments')
6177
+ .insert(rows)
6178
+ .select('id, test_case_id');
4742
6179
 
4743
- ## Historical Bug Hotspots
6180
+ if (insertErr) {
6181
+ // Check if it's a unique constraint violation
6182
+ if (insertErr.message.includes('duplicate') || insertErr.message.includes('unique')) {
6183
+ // Try inserting one by one to find duplicates
6184
+ const created: any[] = [];
6185
+ const skipped: string[] = [];
4744
6186
 
4745
- ${(patterns as any).hotspots?.slice(0, 5).map((h: any) =>
4746
- `- **${h.route}**: ${h.total} bugs (${h.open} open, ${h.critical} critical)`
4747
- ).join('\n') || 'No bug patterns found yet.'}
6187
+ for (const row of rows) {
6188
+ const { data: single, error: singleErr } = await supabase
6189
+ .from('test_assignments')
6190
+ .insert(row)
6191
+ .select('id, test_case_id')
6192
+ .single();
4748
6193
 
4749
- ## Bug Hunt Checklist
6194
+ if (singleErr) {
6195
+ const tc = testCases?.find((t: any) => t.id === row.test_case_id);
6196
+ skipped.push(tc?.test_key || row.test_case_id);
6197
+ } else if (single) {
6198
+ created.push(single);
6199
+ }
6200
+ }
4750
6201
 
4751
- Based on past bugs in this project, here's what to look for:
6202
+ await syncRunCounter();
4752
6203
 
4753
- ${focus === 'error_handling' || focus === 'general' ? `
4754
- ### Error Handling
4755
- - [ ] All async functions have try/catch or .catch()
4756
- - [ ] API calls handle network errors
4757
- - [ ] User-facing error messages are helpful
4758
- - [ ] Errors are logged appropriately
4759
- ` : ''}
6204
+ return {
6205
+ success: true,
6206
+ created: created.length,
6207
+ skipped: skipped.length,
6208
+ skippedTests: skipped,
6209
+ tester: { id: tester.id, name: tester.name },
6210
+ message: `Assigned ${created.length} test(s) to ${tester.name}. ${skipped.length} skipped (already assigned).`,
6211
+ };
6212
+ }
6213
+ return { error: insertErr.message };
6214
+ }
4760
6215
 
4761
- ${focus === 'null_checks' || focus === 'general' ? `
4762
- ### Null Safety
4763
- - [ ] Optional chaining (?.) used for potentially undefined values
4764
- - [ ] Nullish coalescing (??) for default values
4765
- - [ ] Array/object access is guarded
4766
- - [ ] Props have appropriate defaults
4767
- ` : ''}
6216
+ await syncRunCounter();
4768
6217
 
4769
- ${focus === 'async_issues' || focus === 'general' ? `
4770
- ### Async Issues
4771
- - [ ] All Promises are awaited or have .then()
4772
- - [ ] Cleanup functions in useEffect
4773
- - [ ] Race conditions prevented with AbortController
4774
- - [ ] Loading states are handled
4775
- ` : ''}
6218
+ return {
6219
+ success: true,
6220
+ created: (inserted || []).length,
6221
+ skipped: 0,
6222
+ tester: { id: tester.id, name: tester.name },
6223
+ testRun: args.test_run_id || null,
6224
+ message: `Assigned ${(inserted || []).length} test(s) to ${tester.name}.`,
6225
+ };
6226
+ }
4776
6227
 
4777
- ---
6228
+ async function unassignTests(args: {
6229
+ assignment_ids: string[];
6230
+ }) {
6231
+ if (!args.assignment_ids || args.assignment_ids.length === 0) {
6232
+ return { error: 'At least one assignment_id is required' };
6233
+ }
6234
+ if (args.assignment_ids.length > 50) {
6235
+ return { error: 'Maximum 50 assignments per unassign batch' };
6236
+ }
6237
+ const invalidIds = args.assignment_ids.filter(id => !isValidUUID(id));
6238
+ if (invalidIds.length > 0) {
6239
+ return { error: `Invalid UUID(s): ${invalidIds.join(', ')}` };
6240
+ }
4778
6241
 
4779
- **To report bugs you find:** Use \`create_bug_report\` with the file, line number, and code snippet.
6242
+ // Verify assignments exist and belong to this project
6243
+ const { data: existing, error: lookupErr } = await supabase
6244
+ .from('test_assignments')
6245
+ .select('id, test_run_id, test_case:test_cases(test_key, title), tester:testers(name)')
6246
+ .eq('project_id', currentProjectId)
6247
+ .in('id', args.assignment_ids);
4780
6248
 
4781
- Which files or areas would you like me to analyze?`;
4782
- }
6249
+ if (lookupErr) return { error: lookupErr.message };
4783
6250
 
4784
- default:
4785
- return 'Unknown prompt';
6251
+ if (!existing || existing.length === 0) {
6252
+ return { error: 'No matching assignments found in this project' };
4786
6253
  }
4787
- }
4788
-
4789
- // === TESTER & ASSIGNMENT MANAGEMENT HANDLERS ===
4790
6254
 
4791
- async function listTesters(args: {
4792
- status?: string;
4793
- platform?: string;
4794
- }) {
4795
- let query = supabase
4796
- .from('testers')
4797
- .select('id, name, email, status, platforms, tier, assigned_count, completed_count, notes, created_at')
4798
- .eq('project_id', PROJECT_ID)
4799
- .order('name', { ascending: true });
6255
+ const foundIds = new Set(existing.map((a: any) => a.id));
6256
+ const notFound = args.assignment_ids.filter(id => !foundIds.has(id));
4800
6257
 
4801
- if (args.status) {
4802
- query = query.eq('status', args.status);
4803
- }
6258
+ // Delete the assignments
6259
+ const { error: deleteErr } = await supabase
6260
+ .from('test_assignments')
6261
+ .delete()
6262
+ .eq('project_id', currentProjectId)
6263
+ .in('id', args.assignment_ids);
4804
6264
 
4805
- const { data, error } = await query;
6265
+ if (deleteErr) return { error: deleteErr.message };
4806
6266
 
4807
- if (error) {
4808
- return { error: error.message };
6267
+ // Sync run counters for any affected test runs
6268
+ const affectedRunIds = [...new Set(existing.filter((a: any) => a.test_run_id).map((a: any) => a.test_run_id))];
6269
+ for (const runId of affectedRunIds) {
6270
+ const { count } = await supabase
6271
+ .from('test_assignments')
6272
+ .select('id', { count: 'exact', head: true })
6273
+ .eq('test_run_id', runId)
6274
+ .eq('project_id', currentProjectId);
6275
+ if (count !== null) {
6276
+ await supabase.from('test_runs').update({ total_tests: count }).eq('id', runId);
6277
+ }
4809
6278
  }
4810
6279
 
4811
- let testers = data || [];
6280
+ const deleted = existing.map((a: Record<string, unknown>) => {
6281
+ const tc = a.test_case as Record<string, string> | null;
6282
+ const tester = a.tester as Record<string, string> | null;
6283
+ return {
6284
+ id: a.id as string,
6285
+ testKey: tc?.test_key || null,
6286
+ testTitle: tc?.title || null,
6287
+ testerName: tester?.name || null,
6288
+ };
6289
+ });
4812
6290
 
4813
- // Filter by platform if specified (platforms is an array column)
4814
- if (args.platform) {
4815
- testers = testers.filter((t: any) =>
4816
- t.platforms && t.platforms.includes(args.platform)
4817
- );
4818
- }
6291
+ const firstKey = deleted[0]?.testKey;
4819
6292
 
4820
6293
  return {
4821
- count: testers.length,
4822
- testers: testers.map((t: any) => ({
4823
- id: t.id,
4824
- name: t.name,
4825
- email: t.email,
4826
- status: t.status,
4827
- platforms: t.platforms,
4828
- tier: t.tier,
4829
- assignedCount: t.assigned_count,
4830
- completedCount: t.completed_count,
4831
- notes: t.notes,
4832
- })),
6294
+ success: true,
6295
+ deletedCount: existing.length,
6296
+ deleted,
6297
+ notFound: notFound.length > 0 ? notFound : undefined,
6298
+ message: existing.length === 1
6299
+ ? `Removed 1 assignment${firstKey ? ` (${firstKey})` : ''}`
6300
+ : `Removed ${existing.length} assignment(s)`,
4833
6301
  };
4834
6302
  }
4835
6303
 
4836
- async function listTestRuns(args: {
4837
- status?: string;
4838
- limit?: number;
6304
+ async function getTesterWorkload(args: {
6305
+ tester_id: string;
4839
6306
  }) {
4840
- const limit = Math.min(args.limit || 20, 50);
6307
+ if (!isValidUUID(args.tester_id)) {
6308
+ return { error: 'Invalid tester_id format' };
6309
+ }
4841
6310
 
4842
- let query = supabase
4843
- .from('test_runs')
4844
- .select('id, name, description, status, total_tests, passed_tests, failed_tests, started_at, completed_at, created_at')
4845
- .eq('project_id', PROJECT_ID)
4846
- .order('created_at', { ascending: false })
4847
- .limit(limit);
6311
+ // Get tester info
6312
+ const { data: tester, error: testerErr } = await supabase
6313
+ .from('testers')
6314
+ .select('id, name, email, status, platforms, tier')
6315
+ .eq('id', args.tester_id)
6316
+ .eq('project_id', currentProjectId)
6317
+ .single();
4848
6318
 
4849
- if (args.status) {
4850
- query = query.eq('status', args.status);
6319
+ if (testerErr || !tester) {
6320
+ return { error: 'Tester not found in this project' };
4851
6321
  }
4852
6322
 
4853
- const { data, error } = await query;
6323
+ // Get all assignments for this tester in this project
6324
+ const { data: assignments, error: assignErr } = await supabase
6325
+ .from('test_assignments')
6326
+ .select(`
6327
+ id,
6328
+ status,
6329
+ assigned_at,
6330
+ completed_at,
6331
+ test_case:test_cases(test_key, title, priority),
6332
+ test_run:test_runs(name)
6333
+ `)
6334
+ .eq('project_id', currentProjectId)
6335
+ .eq('tester_id', args.tester_id)
6336
+ .order('assigned_at', { ascending: false });
4854
6337
 
4855
- if (error) {
4856
- return { error: error.message };
6338
+ if (assignErr) {
6339
+ return { error: assignErr.message };
6340
+ }
6341
+
6342
+ const all = assignments || [];
6343
+
6344
+ // Count by status
6345
+ const counts: Record<string, number> = {
6346
+ pending: 0,
6347
+ in_progress: 0,
6348
+ passed: 0,
6349
+ failed: 0,
6350
+ blocked: 0,
6351
+ skipped: 0,
6352
+ };
6353
+ for (const a of all) {
6354
+ counts[a.status] = (counts[a.status] || 0) + 1;
4857
6355
  }
4858
6356
 
4859
6357
  return {
4860
- count: (data || []).length,
4861
- testRuns: (data || []).map((r: any) => ({
4862
- id: r.id,
4863
- name: r.name,
4864
- description: r.description,
4865
- status: r.status,
4866
- totalTests: r.total_tests,
4867
- passedTests: r.passed_tests,
4868
- failedTests: r.failed_tests,
4869
- passRate: r.total_tests > 0 ? Math.round((r.passed_tests / r.total_tests) * 100) : 0,
4870
- startedAt: r.started_at,
4871
- completedAt: r.completed_at,
4872
- createdAt: r.created_at,
6358
+ tester: {
6359
+ id: tester.id,
6360
+ name: tester.name,
6361
+ email: tester.email,
6362
+ status: tester.status,
6363
+ platforms: tester.platforms,
6364
+ tier: tester.tier,
6365
+ },
6366
+ totalAssignments: all.length,
6367
+ counts,
6368
+ activeLoad: counts.pending + counts.in_progress,
6369
+ recentAssignments: all.slice(0, 10).map((a: any) => ({
6370
+ id: a.id,
6371
+ status: a.status,
6372
+ assignedAt: a.assigned_at,
6373
+ completedAt: a.completed_at,
6374
+ testCase: a.test_case ? {
6375
+ testKey: a.test_case.test_key,
6376
+ title: a.test_case.title,
6377
+ priority: a.test_case.priority,
6378
+ } : null,
6379
+ testRun: a.test_run?.name || null,
4873
6380
  })),
4874
6381
  };
4875
6382
  }
4876
6383
 
4877
- async function createTestRun(args: {
6384
+ // === NEW TESTER & ANALYTICS HANDLERS ===
6385
+
6386
+ async function createTester(args: {
4878
6387
  name: string;
4879
- description?: string;
6388
+ email: string;
6389
+ platforms?: string[];
6390
+ tier?: number;
6391
+ notes?: string;
6392
+ role?: string;
4880
6393
  }) {
4881
6394
  if (!args.name || args.name.trim().length === 0) {
4882
- return { error: 'Test run name is required' };
6395
+ return { error: 'Tester name is required' };
6396
+ }
6397
+ if (!args.email || !args.email.includes('@')) {
6398
+ return { error: 'A valid email address is required' };
6399
+ }
6400
+ if (args.tier !== undefined && (args.tier < 1 || args.tier > 3)) {
6401
+ return { error: 'Tier must be 1, 2, or 3' };
6402
+ }
6403
+
6404
+ const validPlatforms = ['ios', 'android', 'web'];
6405
+ if (args.platforms) {
6406
+ for (const p of args.platforms) {
6407
+ if (!validPlatforms.includes(p)) {
6408
+ return { error: `Invalid platform "${p}". Must be one of: ${validPlatforms.join(', ')}` };
6409
+ }
6410
+ }
4883
6411
  }
4884
6412
 
4885
6413
  const { data, error } = await supabase
4886
- .from('test_runs')
6414
+ .from('testers')
4887
6415
  .insert({
4888
- project_id: PROJECT_ID,
6416
+ project_id: currentProjectId,
4889
6417
  name: args.name.trim(),
4890
- description: args.description?.trim() || null,
4891
- status: 'draft',
6418
+ email: args.email.trim().toLowerCase(),
6419
+ platforms: args.platforms || ['ios', 'web'],
6420
+ tier: args.tier ?? 1,
6421
+ notes: args.notes?.trim() || null,
6422
+ status: 'active',
6423
+ role: args.role || 'tester',
4892
6424
  })
4893
- .select('id, name, description, status, created_at')
6425
+ .select('id, name, email, status, platforms, tier, notes, role, created_at')
4894
6426
  .single();
4895
6427
 
4896
6428
  if (error) {
6429
+ if (error.message.includes('duplicate') || error.message.includes('unique')) {
6430
+ return { error: `A tester with email "${args.email}" already exists in this project` };
6431
+ }
4897
6432
  return { error: error.message };
4898
6433
  }
4899
6434
 
4900
6435
  return {
4901
6436
  success: true,
4902
- testRun: {
6437
+ tester: {
4903
6438
  id: data.id,
4904
6439
  name: data.name,
4905
- description: data.description,
6440
+ email: data.email,
4906
6441
  status: data.status,
6442
+ platforms: data.platforms,
6443
+ tier: data.tier,
6444
+ notes: data.notes,
6445
+ role: data.role,
4907
6446
  createdAt: data.created_at,
4908
6447
  },
4909
- message: `Test run "${data.name}" created. Use assign_tests to add test assignments.`,
6448
+ message: `Tester "${data.name}" added to the project. Use assign_tests to give them test cases.`,
4910
6449
  };
4911
6450
  }
4912
6451
 
4913
- async function listTestAssignments(args: {
4914
- tester_id?: string;
4915
- test_run_id?: string;
6452
+ async function updateTester(args: {
6453
+ tester_id: string;
4916
6454
  status?: string;
4917
- limit?: number;
6455
+ platforms?: string[];
6456
+ tier?: number;
6457
+ notes?: string;
6458
+ name?: string;
4918
6459
  }) {
4919
- const limit = Math.min(args.limit || 50, 200);
4920
-
4921
- if (args.tester_id && !isValidUUID(args.tester_id)) {
6460
+ if (!isValidUUID(args.tester_id)) {
4922
6461
  return { error: 'Invalid tester_id format' };
4923
6462
  }
4924
- if (args.test_run_id && !isValidUUID(args.test_run_id)) {
4925
- return { error: 'Invalid test_run_id format' };
4926
- }
4927
-
4928
- let query = supabase
4929
- .from('test_assignments')
4930
- .select(`
4931
- id,
4932
- status,
4933
- assigned_at,
4934
- started_at,
4935
- completed_at,
4936
- duration_seconds,
4937
- is_verification,
4938
- notes,
4939
- test_case:test_cases(id, test_key, title, priority, target_route),
4940
- tester:testers(id, name, email),
4941
- test_run:test_runs(id, name)
4942
- `)
4943
- .eq('project_id', PROJECT_ID)
4944
- .order('assigned_at', { ascending: false })
4945
- .limit(limit);
4946
6463
 
4947
- if (args.tester_id) {
4948
- query = query.eq('tester_id', args.tester_id);
4949
- }
4950
- if (args.test_run_id) {
4951
- query = query.eq('test_run_id', args.test_run_id);
6464
+ const updates: Record<string, unknown> = {};
6465
+ if (args.status) updates.status = args.status;
6466
+ if (args.platforms) updates.platforms = args.platforms;
6467
+ if (args.tier !== undefined) {
6468
+ if (args.tier < 1 || args.tier > 3) {
6469
+ return { error: 'Tier must be 1, 2, or 3' };
6470
+ }
6471
+ updates.tier = args.tier;
4952
6472
  }
4953
- if (args.status) {
4954
- query = query.eq('status', args.status);
6473
+ if (args.notes !== undefined) updates.notes = args.notes.trim() || null;
6474
+ if (args.name) updates.name = args.name.trim();
6475
+
6476
+ if (Object.keys(updates).length === 0) {
6477
+ return { error: 'No fields to update. Provide at least one of: status, platforms, tier, notes, name' };
4955
6478
  }
4956
6479
 
4957
- const { data, error } = await query;
6480
+ const { data, error } = await supabase
6481
+ .from('testers')
6482
+ .update(updates)
6483
+ .eq('id', args.tester_id)
6484
+ .eq('project_id', currentProjectId)
6485
+ .select('id, name, email, status, platforms, tier, notes')
6486
+ .single();
4958
6487
 
4959
6488
  if (error) {
4960
6489
  return { error: error.message };
4961
6490
  }
4962
6491
 
6492
+ if (!data) {
6493
+ return { error: 'Tester not found in this project' };
6494
+ }
6495
+
4963
6496
  return {
4964
- count: (data || []).length,
4965
- assignments: (data || []).map((a: any) => ({
4966
- id: a.id,
4967
- status: a.status,
4968
- assignedAt: a.assigned_at,
4969
- startedAt: a.started_at,
4970
- completedAt: a.completed_at,
4971
- durationSeconds: a.duration_seconds,
4972
- isVerification: a.is_verification,
4973
- notes: a.notes,
4974
- testCase: a.test_case ? {
4975
- id: a.test_case.id,
4976
- testKey: a.test_case.test_key,
4977
- title: a.test_case.title,
4978
- priority: a.test_case.priority,
4979
- targetRoute: a.test_case.target_route,
4980
- } : null,
4981
- tester: a.tester ? {
4982
- id: a.tester.id,
4983
- name: a.tester.name,
4984
- email: a.tester.email,
4985
- } : null,
4986
- testRun: a.test_run ? {
4987
- id: a.test_run.id,
4988
- name: a.test_run.name,
4989
- } : null,
4990
- })),
6497
+ success: true,
6498
+ tester: {
6499
+ id: data.id,
6500
+ name: data.name,
6501
+ email: data.email,
6502
+ status: data.status,
6503
+ platforms: data.platforms,
6504
+ tier: data.tier,
6505
+ notes: data.notes,
6506
+ },
6507
+ updatedFields: Object.keys(updates),
4991
6508
  };
4992
6509
  }
4993
6510
 
4994
- async function assignTests(args: {
4995
- tester_id: string;
4996
- test_case_ids: string[];
4997
- test_run_id?: string;
6511
+ async function bulkUpdateReports(args: {
6512
+ report_ids: string[];
6513
+ status: string;
6514
+ resolution_notes?: string;
4998
6515
  }) {
4999
- // Validate inputs
5000
- if (!isValidUUID(args.tester_id)) {
5001
- return { error: 'Invalid tester_id format' };
5002
- }
5003
- if (!args.test_case_ids || args.test_case_ids.length === 0) {
5004
- return { error: 'At least one test_case_id is required' };
6516
+ if (!args.report_ids || args.report_ids.length === 0) {
6517
+ return { error: 'At least one report_id is required' };
5005
6518
  }
5006
- if (args.test_case_ids.length > 50) {
5007
- return { error: 'Maximum 50 test cases per assignment batch' };
6519
+ if (args.report_ids.length > 50) {
6520
+ return { error: 'Maximum 50 reports per bulk update' };
5008
6521
  }
5009
- for (const id of args.test_case_ids) {
6522
+ for (const id of args.report_ids) {
5010
6523
  if (!isValidUUID(id)) {
5011
- return { error: `Invalid test_case_id format: ${id}` };
6524
+ return { error: `Invalid report_id format: ${id}` };
5012
6525
  }
5013
6526
  }
5014
- if (args.test_run_id && !isValidUUID(args.test_run_id)) {
5015
- return { error: 'Invalid test_run_id format' };
5016
- }
5017
-
5018
- // Verify tester exists and is active
5019
- const { data: tester, error: testerErr } = await supabase
5020
- .from('testers')
5021
- .select('id, name, email, status')
5022
- .eq('id', args.tester_id)
5023
- .eq('project_id', PROJECT_ID)
5024
- .single();
5025
6527
 
5026
- if (testerErr || !tester) {
5027
- return { error: 'Tester not found in this project' };
6528
+ const updates: Record<string, unknown> = { status: args.status };
6529
+ if (args.resolution_notes) {
6530
+ updates.resolution_notes = args.resolution_notes;
5028
6531
  }
5029
- if (tester.status !== 'active') {
5030
- return { error: `Tester "${tester.name}" is ${tester.status}, not active` };
6532
+ // Set resolved_at timestamp for terminal statuses
6533
+ if (['fixed', 'resolved', 'verified', 'wont_fix', 'duplicate', 'closed'].includes(args.status)) {
6534
+ updates.resolved_at = new Date().toISOString();
5031
6535
  }
5032
6536
 
5033
- // Verify test cases exist for this project
5034
- const { data: testCases, error: tcErr } = await supabase
5035
- .from('test_cases')
5036
- .select('id, test_key, title')
5037
- .eq('project_id', PROJECT_ID)
5038
- .in('id', args.test_case_ids);
6537
+ const { data, error } = await supabase
6538
+ .from('reports')
6539
+ .update(updates)
6540
+ .eq('project_id', currentProjectId)
6541
+ .in('id', args.report_ids)
6542
+ .select('id, status, description');
5039
6543
 
5040
- if (tcErr) {
5041
- return { error: tcErr.message };
6544
+ if (error) {
6545
+ return { error: error.message };
5042
6546
  }
5043
6547
 
5044
- const foundIds = new Set((testCases || []).map((tc: any) => tc.id));
5045
- const missingIds = args.test_case_ids.filter(id => !foundIds.has(id));
6548
+ const updated = data || [];
6549
+ const updatedIds = new Set(updated.map((r: any) => r.id));
6550
+ const notFound = args.report_ids.filter(id => !updatedIds.has(id));
5046
6551
 
5047
- if (missingIds.length > 0) {
6552
+ return {
6553
+ success: true,
6554
+ updatedCount: updated.length,
6555
+ requestedCount: args.report_ids.length,
6556
+ notFound: notFound.length > 0 ? notFound : undefined,
6557
+ status: args.status,
6558
+ reports: updated.map((r: any) => ({
6559
+ id: r.id,
6560
+ status: r.status,
6561
+ description: r.description?.slice(0, 80),
6562
+ })),
6563
+ message: `Updated ${updated.length} report(s) to "${args.status}".${notFound.length > 0 ? ` ${notFound.length} report(s) not found.` : ''}`,
6564
+ };
6565
+ }
6566
+
6567
+ async function getBugTrends(args: {
6568
+ group_by?: string;
6569
+ days?: number;
6570
+ }) {
6571
+ const days = Math.min(args.days || 30, 180);
6572
+ const groupBy = args.group_by || 'week';
6573
+ const since = new Date(Date.now() - days * 86400000).toISOString();
6574
+
6575
+ const { data, error } = await supabase
6576
+ .from('reports')
6577
+ .select('id, severity, category, status, report_type, created_at')
6578
+ .eq('project_id', currentProjectId)
6579
+ .gte('created_at', since)
6580
+ .order('created_at', { ascending: true });
6581
+
6582
+ if (error) {
6583
+ return { error: error.message };
6584
+ }
6585
+
6586
+ const reports = data || [];
6587
+
6588
+ if (groupBy === 'week') {
6589
+ const weeks: Record<string, { count: number; critical: number; high: number; medium: number; low: number }> = {};
6590
+ for (const r of reports) {
6591
+ const d = new Date(r.created_at);
6592
+ // Get Monday of that week
6593
+ const day = d.getDay();
6594
+ const diff = d.getDate() - day + (day === 0 ? -6 : 1);
6595
+ const monday = new Date(d.setDate(diff));
6596
+ const weekKey = monday.toISOString().slice(0, 10);
6597
+ if (!weeks[weekKey]) weeks[weekKey] = { count: 0, critical: 0, high: 0, medium: 0, low: 0 };
6598
+ weeks[weekKey].count++;
6599
+ const sev = (r.severity || 'low') as 'critical' | 'high' | 'medium' | 'low';
6600
+ weeks[weekKey][sev]++;
6601
+ }
5048
6602
  return {
5049
- error: `Test cases not found in this project: ${missingIds.join(', ')}`,
6603
+ period: `${days} days`,
6604
+ groupBy: 'week',
6605
+ totalReports: reports.length,
6606
+ weeks: Object.entries(weeks).map(([week, data]) => ({ week, ...data })),
5050
6607
  };
5051
6608
  }
5052
6609
 
5053
- // Verify test run exists if provided
5054
- if (args.test_run_id) {
5055
- const { data: run, error: runErr } = await supabase
5056
- .from('test_runs')
5057
- .select('id')
5058
- .eq('id', args.test_run_id)
5059
- .eq('project_id', PROJECT_ID)
5060
- .single();
6610
+ if (groupBy === 'severity') {
6611
+ const groups: Record<string, number> = { critical: 0, high: 0, medium: 0, low: 0 };
6612
+ for (const r of reports) groups[r.severity || 'low']++;
6613
+ return { period: `${days} days`, groupBy: 'severity', totalReports: reports.length, breakdown: groups };
6614
+ }
5061
6615
 
5062
- if (runErr || !run) {
5063
- return { error: 'Test run not found in this project' };
6616
+ if (groupBy === 'category') {
6617
+ const groups: Record<string, number> = {};
6618
+ for (const r of reports) {
6619
+ const cat = r.category || 'uncategorized';
6620
+ groups[cat] = (groups[cat] || 0) + 1;
5064
6621
  }
6622
+ return { period: `${days} days`, groupBy: 'category', totalReports: reports.length, breakdown: groups };
5065
6623
  }
5066
6624
 
5067
- // Build assignment rows
5068
- const rows = args.test_case_ids.map(tcId => ({
5069
- project_id: PROJECT_ID,
5070
- test_case_id: tcId,
5071
- tester_id: args.tester_id,
5072
- test_run_id: args.test_run_id || null,
5073
- status: 'pending',
5074
- }));
6625
+ if (groupBy === 'status') {
6626
+ const groups: Record<string, number> = {};
6627
+ for (const r of reports) {
6628
+ groups[r.status] = (groups[r.status] || 0) + 1;
6629
+ }
6630
+ return { period: `${days} days`, groupBy: 'status', totalReports: reports.length, breakdown: groups };
6631
+ }
5075
6632
 
5076
- // Insert use upsert-like approach: insert and handle conflicts
5077
- const { data: inserted, error: insertErr } = await supabase
6633
+ return { error: `Invalid group_by: ${groupBy}. Must be one of: week, severity, category, status` };
6634
+ }
6635
+
6636
+ async function getTesterLeaderboard(args: {
6637
+ days?: number;
6638
+ sort_by?: string;
6639
+ }) {
6640
+ const days = Math.min(args.days || 30, 180);
6641
+ const sortBy = args.sort_by || 'tests_completed';
6642
+ const since = new Date(Date.now() - days * 86400000).toISOString();
6643
+
6644
+ // Get all testers for the project
6645
+ const { data: testers, error: testerErr } = await supabase
6646
+ .from('testers')
6647
+ .select('id, name, email, status, platforms, tier')
6648
+ .eq('project_id', currentProjectId)
6649
+ .eq('status', 'active');
6650
+
6651
+ if (testerErr) return { error: testerErr.message };
6652
+
6653
+ // Get completed assignments in the period
6654
+ const { data: assignments, error: assignErr } = await supabase
5078
6655
  .from('test_assignments')
5079
- .insert(rows)
5080
- .select('id, test_case_id');
6656
+ .select('tester_id, status, completed_at, duration_seconds')
6657
+ .eq('project_id', currentProjectId)
6658
+ .gte('completed_at', since)
6659
+ .in('status', ['passed', 'failed']);
5081
6660
 
5082
- if (insertErr) {
5083
- // Check if it's a unique constraint violation
5084
- if (insertErr.message.includes('duplicate') || insertErr.message.includes('unique')) {
5085
- // Try inserting one by one to find duplicates
5086
- const created: any[] = [];
5087
- const skipped: string[] = [];
6661
+ if (assignErr) return { error: assignErr.message };
5088
6662
 
5089
- for (const row of rows) {
5090
- const { data: single, error: singleErr } = await supabase
5091
- .from('test_assignments')
5092
- .insert(row)
5093
- .select('id, test_case_id')
5094
- .single();
6663
+ // Get bugs filed in the period
6664
+ const { data: bugs, error: bugErr } = await supabase
6665
+ .from('reports')
6666
+ .select('tester_id, severity')
6667
+ .eq('project_id', currentProjectId)
6668
+ .gte('created_at', since)
6669
+ .not('tester_id', 'is', null);
5095
6670
 
5096
- if (singleErr) {
5097
- const tc = testCases?.find((t: any) => t.id === row.test_case_id);
5098
- skipped.push(tc?.test_key || row.test_case_id);
5099
- } else if (single) {
5100
- created.push(single);
5101
- }
5102
- }
6671
+ if (bugErr) return { error: bugErr.message };
5103
6672
 
5104
- return {
5105
- success: true,
5106
- created: created.length,
5107
- skipped: skipped.length,
5108
- skippedTests: skipped,
5109
- tester: { id: tester.id, name: tester.name },
5110
- message: `Assigned ${created.length} test(s) to ${tester.name}. ${skipped.length} skipped (already assigned).`,
5111
- };
5112
- }
5113
- return { error: insertErr.message };
6673
+ // Aggregate per tester
6674
+ const testerMap = new Map<string, any>();
6675
+ for (const t of testers || []) {
6676
+ testerMap.set(t.id, {
6677
+ id: t.id,
6678
+ name: t.name,
6679
+ email: t.email,
6680
+ tier: t.tier,
6681
+ testsCompleted: 0,
6682
+ testsPassed: 0,
6683
+ testsFailed: 0,
6684
+ bugsFound: 0,
6685
+ criticalBugs: 0,
6686
+ avgDurationSeconds: 0,
6687
+ totalDuration: 0,
6688
+ });
6689
+ }
6690
+
6691
+ for (const a of assignments || []) {
6692
+ const entry = testerMap.get(a.tester_id);
6693
+ if (!entry) continue;
6694
+ entry.testsCompleted++;
6695
+ if (a.status === 'passed') entry.testsPassed++;
6696
+ if (a.status === 'failed') entry.testsFailed++;
6697
+ if (a.duration_seconds) entry.totalDuration += a.duration_seconds;
6698
+ }
6699
+
6700
+ for (const b of bugs || []) {
6701
+ const entry = testerMap.get(b.tester_id);
6702
+ if (!entry) continue;
6703
+ entry.bugsFound++;
6704
+ if (b.severity === 'critical') entry.criticalBugs++;
6705
+ }
6706
+
6707
+ let leaderboard = Array.from(testerMap.values()).map(t => ({
6708
+ ...t,
6709
+ passRate: t.testsCompleted > 0 ? Math.round((t.testsPassed / t.testsCompleted) * 100) : 0,
6710
+ avgDurationSeconds: t.testsCompleted > 0 ? Math.round(t.totalDuration / t.testsCompleted) : 0,
6711
+ totalDuration: undefined,
6712
+ }));
6713
+
6714
+ // Sort
6715
+ if (sortBy === 'bugs_found') {
6716
+ leaderboard.sort((a, b) => b.bugsFound - a.bugsFound);
6717
+ } else if (sortBy === 'pass_rate') {
6718
+ leaderboard.sort((a, b) => b.passRate - a.passRate);
6719
+ } else {
6720
+ leaderboard.sort((a, b) => b.testsCompleted - a.testsCompleted);
5114
6721
  }
5115
6722
 
5116
6723
  return {
5117
- success: true,
5118
- created: (inserted || []).length,
5119
- skipped: 0,
5120
- tester: { id: tester.id, name: tester.name },
5121
- testRun: args.test_run_id || null,
5122
- message: `Assigned ${(inserted || []).length} test(s) to ${tester.name}.`,
6724
+ period: `${days} days`,
6725
+ sortedBy: sortBy,
6726
+ leaderboard,
5123
6727
  };
5124
6728
  }
5125
6729
 
5126
- async function getTesterWorkload(args: {
5127
- tester_id: string;
6730
+ async function exportTestResults(args: {
6731
+ test_run_id: string;
6732
+ compact?: boolean;
6733
+ limit?: number;
5128
6734
  }) {
5129
- if (!isValidUUID(args.tester_id)) {
5130
- return { error: 'Invalid tester_id format' };
6735
+ if (!isValidUUID(args.test_run_id)) {
6736
+ return { error: 'Invalid test_run_id format' };
5131
6737
  }
5132
6738
 
5133
- // Get tester info
5134
- const { data: tester, error: testerErr } = await supabase
5135
- .from('testers')
5136
- .select('id, name, email, status, platforms, tier')
5137
- .eq('id', args.tester_id)
5138
- .eq('project_id', PROJECT_ID)
6739
+ // Get the test run
6740
+ const { data: run, error: runErr } = await supabase
6741
+ .from('test_runs')
6742
+ .select('id, name, description, status, total_tests, passed_tests, failed_tests, started_at, completed_at, created_at')
6743
+ .eq('id', args.test_run_id)
6744
+ .eq('project_id', currentProjectId)
5139
6745
  .single();
5140
6746
 
5141
- if (testerErr || !tester) {
5142
- return { error: 'Tester not found in this project' };
6747
+ if (runErr || !run) {
6748
+ return { error: 'Test run not found in this project' };
5143
6749
  }
5144
6750
 
5145
- // Get all assignments for this tester in this project
6751
+ // Get all assignments for this run
5146
6752
  const { data: assignments, error: assignErr } = await supabase
5147
6753
  .from('test_assignments')
5148
6754
  .select(`
5149
6755
  id,
5150
6756
  status,
5151
6757
  assigned_at,
6758
+ started_at,
5152
6759
  completed_at,
5153
- test_case:test_cases(test_key, title, priority),
5154
- test_run:test_runs(name)
6760
+ duration_seconds,
6761
+ is_verification,
6762
+ notes,
6763
+ skip_reason,
6764
+ test_result,
6765
+ feedback_rating,
6766
+ feedback_note,
6767
+ test_case:test_cases(id, test_key, title, priority, description, target_route),
6768
+ tester:testers(id, name, email)
5155
6769
  `)
5156
- .eq('project_id', PROJECT_ID)
5157
- .eq('tester_id', args.tester_id)
5158
- .order('assigned_at', { ascending: false });
6770
+ .eq('test_run_id', args.test_run_id)
6771
+ .eq('project_id', currentProjectId)
6772
+ .order('assigned_at', { ascending: true });
5159
6773
 
5160
6774
  if (assignErr) {
5161
6775
  return { error: assignErr.message };
5162
6776
  }
5163
6777
 
5164
6778
  const all = assignments || [];
6779
+ const passCount = all.filter(a => a.status === 'passed').length;
6780
+ const failCount = all.filter(a => a.status === 'failed').length;
6781
+
6782
+ const testRunInfo = {
6783
+ id: run.id,
6784
+ name: run.name,
6785
+ description: run.description,
6786
+ status: run.status,
6787
+ startedAt: run.started_at,
6788
+ completedAt: run.completed_at,
6789
+ createdAt: run.created_at,
6790
+ };
5165
6791
 
5166
- // Count by status
5167
- const counts: Record<string, number> = {
5168
- pending: 0,
5169
- in_progress: 0,
5170
- passed: 0,
5171
- failed: 0,
5172
- blocked: 0,
5173
- skipped: 0,
6792
+ const summaryInfo = {
6793
+ totalAssignments: all.length,
6794
+ passed: passCount,
6795
+ failed: failCount,
6796
+ blocked: all.filter(a => a.status === 'blocked').length,
6797
+ skipped: all.filter(a => a.status === 'skipped').length,
6798
+ pending: all.filter(a => a.status === 'pending').length,
6799
+ inProgress: all.filter(a => a.status === 'in_progress').length,
6800
+ passRate: all.length > 0 ? Math.round((passCount / all.length) * 100) : 0,
5174
6801
  };
5175
- for (const a of all) {
5176
- counts[a.status] = (counts[a.status] || 0) + 1;
6802
+
6803
+ // Compact: return test run info + summary only, no assignments array
6804
+ if (args.compact === true) {
6805
+ return { testRun: testRunInfo, summary: summaryInfo };
5177
6806
  }
5178
6807
 
6808
+ // Apply limit (default: 100, max: 500)
6809
+ const assignmentLimit = Math.min(Math.max(args.limit ?? 100, 1), 500);
6810
+ const limitedAssignments = all.slice(0, assignmentLimit);
6811
+
5179
6812
  return {
5180
- tester: {
5181
- id: tester.id,
5182
- name: tester.name,
5183
- email: tester.email,
5184
- status: tester.status,
5185
- platforms: tester.platforms,
5186
- tier: tester.tier,
5187
- },
5188
- totalAssignments: all.length,
5189
- counts,
5190
- activeLoad: counts.pending + counts.in_progress,
5191
- recentAssignments: all.slice(0, 10).map((a: any) => ({
6813
+ testRun: testRunInfo,
6814
+ summary: summaryInfo,
6815
+ assignmentsReturned: limitedAssignments.length,
6816
+ assignmentsTotal: all.length,
6817
+ assignments: limitedAssignments.map((a: any) => ({
5192
6818
  id: a.id,
5193
6819
  status: a.status,
5194
6820
  assignedAt: a.assigned_at,
6821
+ startedAt: a.started_at,
5195
6822
  completedAt: a.completed_at,
6823
+ durationSeconds: a.duration_seconds,
6824
+ isVerification: a.is_verification,
6825
+ notes: a.notes,
6826
+ skipReason: a.skip_reason,
6827
+ testResult: a.test_result,
6828
+ feedbackRating: a.feedback_rating,
6829
+ feedbackNote: a.feedback_note,
5196
6830
  testCase: a.test_case ? {
6831
+ id: a.test_case.id,
5197
6832
  testKey: a.test_case.test_key,
5198
6833
  title: a.test_case.title,
5199
6834
  priority: a.test_case.priority,
6835
+ description: a.test_case.description,
6836
+ targetRoute: a.test_case.target_route,
6837
+ } : null,
6838
+ tester: a.tester ? {
6839
+ id: a.tester.id,
6840
+ name: a.tester.name,
6841
+ email: a.tester.email,
5200
6842
  } : null,
5201
- testRun: a.test_run?.name || null,
5202
6843
  })),
5203
6844
  };
5204
6845
  }
5205
6846
 
6847
+ async function getTestingVelocity(args: {
6848
+ days?: number;
6849
+ }) {
6850
+ const days = Math.min(args.days || 14, 90);
6851
+ const since = new Date(Date.now() - days * 86400000).toISOString();
6852
+
6853
+ const { data, error } = await supabase
6854
+ .from('test_assignments')
6855
+ .select('completed_at, status')
6856
+ .eq('project_id', currentProjectId)
6857
+ .gte('completed_at', since)
6858
+ .in('status', ['passed', 'failed'])
6859
+ .order('completed_at', { ascending: true });
6860
+
6861
+ if (error) {
6862
+ return { error: error.message };
6863
+ }
6864
+
6865
+ const completions = data || [];
6866
+
6867
+ // Group by day
6868
+ const dailyCounts: Record<string, number> = {};
6869
+ for (let i = 0; i < days; i++) {
6870
+ const d = new Date(Date.now() - (days - 1 - i) * 86400000);
6871
+ dailyCounts[d.toISOString().slice(0, 10)] = 0;
6872
+ }
6873
+
6874
+ for (const c of completions) {
6875
+ const day = new Date(c.completed_at).toISOString().slice(0, 10);
6876
+ if (dailyCounts[day] !== undefined) {
6877
+ dailyCounts[day]++;
6878
+ }
6879
+ }
6880
+
6881
+ const dailyArray = Object.entries(dailyCounts).map(([date, count]) => ({ date, count }));
6882
+ const totalCompleted = completions.length;
6883
+ const avgPerDay = days > 0 ? Math.round((totalCompleted / days) * 10) / 10 : 0;
6884
+
6885
+ // Trend: compare first half to second half
6886
+ const mid = Math.floor(dailyArray.length / 2);
6887
+ const firstHalf = dailyArray.slice(0, mid).reduce((sum, d) => sum + d.count, 0);
6888
+ const secondHalf = dailyArray.slice(mid).reduce((sum, d) => sum + d.count, 0);
6889
+ const trend = secondHalf > firstHalf ? 'increasing' : secondHalf < firstHalf ? 'decreasing' : 'stable';
6890
+
6891
+ return {
6892
+ period: `${days} days`,
6893
+ totalCompleted,
6894
+ averagePerDay: avgPerDay,
6895
+ trend,
6896
+ daily: dailyArray,
6897
+ };
6898
+ }
6899
+
5206
6900
  // Main server setup
5207
6901
  async function main() {
5208
6902
  initSupabase();
@@ -5233,6 +6927,12 @@ async function main() {
5233
6927
  try {
5234
6928
  let result: unknown;
5235
6929
 
6930
+ // Project management tools don't require a project to be selected
6931
+ const projectFreeTools = ['list_projects', 'switch_project', 'get_current_project'];
6932
+ if (!projectFreeTools.includes(name)) {
6933
+ requireProject();
6934
+ }
6935
+
5236
6936
  switch (name) {
5237
6937
  case 'list_reports':
5238
6938
  result = await listReports(args as any);
@@ -5249,6 +6949,12 @@ async function main() {
5249
6949
  case 'get_report_context':
5250
6950
  result = await getReportContext(args as any);
5251
6951
  break;
6952
+ case 'add_report_comment':
6953
+ result = await addReportComment(args as any);
6954
+ break;
6955
+ case 'get_report_comments':
6956
+ result = await getReportComments(args as any);
6957
+ break;
5252
6958
  case 'get_project_info':
5253
6959
  result = await getProjectInfo();
5254
6960
  break;
@@ -5362,9 +7068,70 @@ async function main() {
5362
7068
  case 'assign_tests':
5363
7069
  result = await assignTests(args as any);
5364
7070
  break;
7071
+ case 'unassign_tests':
7072
+ result = await unassignTests(args as any);
7073
+ break;
5365
7074
  case 'get_tester_workload':
5366
7075
  result = await getTesterWorkload(args as any);
5367
7076
  break;
7077
+ // === NEW TESTER & ANALYTICS TOOLS ===
7078
+ case 'create_tester':
7079
+ result = await createTester(args as any);
7080
+ break;
7081
+ case 'update_tester':
7082
+ result = await updateTester(args as any);
7083
+ break;
7084
+ case 'bulk_update_reports':
7085
+ result = await bulkUpdateReports(args as any);
7086
+ break;
7087
+ case 'get_bug_trends':
7088
+ result = await getBugTrends(args as any);
7089
+ break;
7090
+ case 'get_tester_leaderboard':
7091
+ result = await getTesterLeaderboard(args as any);
7092
+ break;
7093
+ case 'export_test_results':
7094
+ result = await exportTestResults(args as any);
7095
+ break;
7096
+ case 'get_testing_velocity':
7097
+ result = await getTestingVelocity(args as any);
7098
+ break;
7099
+ // === PROJECT MANAGEMENT ===
7100
+ case 'list_projects':
7101
+ result = await listProjects();
7102
+ break;
7103
+ case 'switch_project':
7104
+ result = await switchProject(args as any);
7105
+ break;
7106
+ case 'get_current_project':
7107
+ result = getCurrentProject();
7108
+ break;
7109
+ // === TEST EXECUTION INTELLIGENCE ===
7110
+ case 'get_test_impact':
7111
+ result = await getTestImpact(args as any);
7112
+ break;
7113
+ case 'get_flaky_tests':
7114
+ result = await getFlakyTests(args as any);
7115
+ break;
7116
+ case 'assess_test_quality':
7117
+ result = await assessTestQuality(args as any);
7118
+ break;
7119
+ case 'get_test_execution_summary':
7120
+ result = await getTestExecutionSummary(args as any);
7121
+ break;
7122
+ case 'check_test_freshness':
7123
+ result = await checkTestFreshness(args as any);
7124
+ break;
7125
+ case 'get_untested_changes':
7126
+ result = await getUntestedChanges(args as any);
7127
+ break;
7128
+ // === AUTO-MONITORING TOOLS ===
7129
+ case 'get_auto_detected_issues':
7130
+ result = await getAutoDetectedIssues(args as any);
7131
+ break;
7132
+ case 'generate_tests_from_errors':
7133
+ result = await generateTestsFromErrors(args as any);
7134
+ break;
5368
7135
  default:
5369
7136
  return {
5370
7137
  content: [{ type: 'text', text: `Unknown tool: ${name}` }],
@@ -5388,7 +7155,7 @@ async function main() {
5388
7155
  const { data, error } = await supabase
5389
7156
  .from('reports')
5390
7157
  .select('id, description, report_type, severity')
5391
- .eq('project_id', PROJECT_ID)
7158
+ .eq('project_id', currentProjectId)
5392
7159
  .eq('status', 'new')
5393
7160
  .order('created_at', { ascending: false })
5394
7161
  .limit(10);