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