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