@bbearai/mcp-server 0.5.0 → 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 +1365 -126
- package/package.json +1 -1
- package/src/index.ts +1504 -115
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();
|
|
@@ -435,6 +439,10 @@ const tools = [
|
|
|
435
439
|
items: { type: 'string' },
|
|
436
440
|
description: 'List of files that were modified to fix this bug',
|
|
437
441
|
},
|
|
442
|
+
notify_tester: {
|
|
443
|
+
type: 'boolean',
|
|
444
|
+
description: 'If true, notify the original tester about the fix with a message and verification task. Default: false (silent resolve).',
|
|
445
|
+
},
|
|
438
446
|
},
|
|
439
447
|
required: ['report_id', 'commit_sha'],
|
|
440
448
|
},
|
|
@@ -902,13 +910,366 @@ const tools = [
|
|
|
902
910
|
},
|
|
903
911
|
},
|
|
904
912
|
},
|
|
913
|
+
// === TESTER & ASSIGNMENT MANAGEMENT TOOLS ===
|
|
914
|
+
{
|
|
915
|
+
name: 'list_testers',
|
|
916
|
+
description: 'List all QA testers for the project with their status, platforms, and workload counts.',
|
|
917
|
+
inputSchema: {
|
|
918
|
+
type: 'object',
|
|
919
|
+
properties: {
|
|
920
|
+
status: {
|
|
921
|
+
type: 'string',
|
|
922
|
+
enum: ['active', 'inactive', 'invited'],
|
|
923
|
+
description: 'Filter by tester status (default: all)',
|
|
924
|
+
},
|
|
925
|
+
platform: {
|
|
926
|
+
type: 'string',
|
|
927
|
+
enum: ['ios', 'android', 'web'],
|
|
928
|
+
description: 'Filter by platform support',
|
|
929
|
+
},
|
|
930
|
+
},
|
|
931
|
+
},
|
|
932
|
+
},
|
|
933
|
+
{
|
|
934
|
+
name: 'list_test_runs',
|
|
935
|
+
description: 'List testing campaigns (test runs) for the project with pass/fail stats.',
|
|
936
|
+
inputSchema: {
|
|
937
|
+
type: 'object',
|
|
938
|
+
properties: {
|
|
939
|
+
status: {
|
|
940
|
+
type: 'string',
|
|
941
|
+
enum: ['draft', 'active', 'paused', 'completed', 'archived'],
|
|
942
|
+
description: 'Filter by test run status',
|
|
943
|
+
},
|
|
944
|
+
limit: {
|
|
945
|
+
type: 'number',
|
|
946
|
+
description: 'Maximum number of runs to return (default: 20)',
|
|
947
|
+
},
|
|
948
|
+
},
|
|
949
|
+
},
|
|
950
|
+
},
|
|
951
|
+
{
|
|
952
|
+
name: 'create_test_run',
|
|
953
|
+
description: 'Create a new testing campaign (test run). Tests can then be assigned to testers within this run.',
|
|
954
|
+
inputSchema: {
|
|
955
|
+
type: 'object',
|
|
956
|
+
properties: {
|
|
957
|
+
name: {
|
|
958
|
+
type: 'string',
|
|
959
|
+
description: 'Name for the test run (e.g. "v2.1 QA Pass", "Sprint 5 Testing")',
|
|
960
|
+
},
|
|
961
|
+
description: {
|
|
962
|
+
type: 'string',
|
|
963
|
+
description: 'Optional description of the test run scope and goals',
|
|
964
|
+
},
|
|
965
|
+
},
|
|
966
|
+
required: ['name'],
|
|
967
|
+
},
|
|
968
|
+
},
|
|
969
|
+
{
|
|
970
|
+
name: 'list_test_assignments',
|
|
971
|
+
description: 'List test assignments showing which tests are assigned to which testers, with status tracking. Joins test case and tester info.',
|
|
972
|
+
inputSchema: {
|
|
973
|
+
type: 'object',
|
|
974
|
+
properties: {
|
|
975
|
+
tester_id: {
|
|
976
|
+
type: 'string',
|
|
977
|
+
description: 'Filter by tester UUID',
|
|
978
|
+
},
|
|
979
|
+
test_run_id: {
|
|
980
|
+
type: 'string',
|
|
981
|
+
description: 'Filter by test run UUID',
|
|
982
|
+
},
|
|
983
|
+
status: {
|
|
984
|
+
type: 'string',
|
|
985
|
+
enum: ['pending', 'in_progress', 'passed', 'failed', 'blocked', 'skipped'],
|
|
986
|
+
description: 'Filter by assignment status',
|
|
987
|
+
},
|
|
988
|
+
limit: {
|
|
989
|
+
type: 'number',
|
|
990
|
+
description: 'Maximum number of assignments to return (default: 50, max: 200)',
|
|
991
|
+
},
|
|
992
|
+
},
|
|
993
|
+
},
|
|
994
|
+
},
|
|
995
|
+
{
|
|
996
|
+
name: 'assign_tests',
|
|
997
|
+
description: 'Assign one or more test cases to a tester. Optionally assign within a test run. Skips duplicates gracefully.',
|
|
998
|
+
inputSchema: {
|
|
999
|
+
type: 'object',
|
|
1000
|
+
properties: {
|
|
1001
|
+
tester_id: {
|
|
1002
|
+
type: 'string',
|
|
1003
|
+
description: 'UUID of the tester to assign tests to (required)',
|
|
1004
|
+
},
|
|
1005
|
+
test_case_ids: {
|
|
1006
|
+
type: 'array',
|
|
1007
|
+
items: { type: 'string' },
|
|
1008
|
+
description: 'Array of test case UUIDs to assign (required)',
|
|
1009
|
+
},
|
|
1010
|
+
test_run_id: {
|
|
1011
|
+
type: 'string',
|
|
1012
|
+
description: 'Optional test run UUID to group assignments under',
|
|
1013
|
+
},
|
|
1014
|
+
},
|
|
1015
|
+
required: ['tester_id', 'test_case_ids'],
|
|
1016
|
+
},
|
|
1017
|
+
},
|
|
1018
|
+
{
|
|
1019
|
+
name: 'get_tester_workload',
|
|
1020
|
+
description: 'View a specific tester\'s current workload — assignment counts by status and recent assignments.',
|
|
1021
|
+
inputSchema: {
|
|
1022
|
+
type: 'object',
|
|
1023
|
+
properties: {
|
|
1024
|
+
tester_id: {
|
|
1025
|
+
type: 'string',
|
|
1026
|
+
description: 'UUID of the tester (required)',
|
|
1027
|
+
},
|
|
1028
|
+
},
|
|
1029
|
+
required: ['tester_id'],
|
|
1030
|
+
},
|
|
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
|
+
},
|
|
905
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
|
+
}
|
|
906
1267
|
// Tool handlers
|
|
907
1268
|
async function listReports(args) {
|
|
908
1269
|
let query = supabase
|
|
909
1270
|
.from('reports')
|
|
910
|
-
.select('id, report_type, severity, status, description, app_context, created_at, reporter_name,
|
|
911
|
-
.eq('project_id',
|
|
1271
|
+
.select('id, report_type, severity, status, description, app_context, created_at, reporter_name, tester:testers(name)')
|
|
1272
|
+
.eq('project_id', currentProjectId)
|
|
912
1273
|
.order('created_at', { ascending: false })
|
|
913
1274
|
.limit(Math.min(args.limit || 10, 50));
|
|
914
1275
|
if (args.status)
|
|
@@ -944,7 +1305,7 @@ async function getReport(args) {
|
|
|
944
1305
|
.from('reports')
|
|
945
1306
|
.select('*, tester:testers(*), track:qa_tracks(*)')
|
|
946
1307
|
.eq('id', args.report_id)
|
|
947
|
-
.eq('project_id',
|
|
1308
|
+
.eq('project_id', currentProjectId) // Security: ensure report belongs to this project
|
|
948
1309
|
.single();
|
|
949
1310
|
if (error) {
|
|
950
1311
|
return { error: error.message };
|
|
@@ -963,10 +1324,8 @@ async function getReport(args) {
|
|
|
963
1324
|
created_at: data.created_at,
|
|
964
1325
|
reporter: data.tester ? {
|
|
965
1326
|
name: data.tester.name,
|
|
966
|
-
email: data.tester.email,
|
|
967
1327
|
} : (data.reporter_name ? {
|
|
968
1328
|
name: data.reporter_name,
|
|
969
|
-
email: data.reporter_email,
|
|
970
1329
|
} : null),
|
|
971
1330
|
track: data.track ? {
|
|
972
1331
|
name: data.track.name,
|
|
@@ -981,7 +1340,7 @@ async function searchReports(args) {
|
|
|
981
1340
|
let query = supabase
|
|
982
1341
|
.from('reports')
|
|
983
1342
|
.select('id, report_type, severity, status, description, app_context, created_at')
|
|
984
|
-
.eq('project_id',
|
|
1343
|
+
.eq('project_id', currentProjectId)
|
|
985
1344
|
.order('created_at', { ascending: false })
|
|
986
1345
|
.limit(20);
|
|
987
1346
|
if (sanitizedQuery) {
|
|
@@ -1024,7 +1383,7 @@ async function updateReportStatus(args) {
|
|
|
1024
1383
|
.from('reports')
|
|
1025
1384
|
.update(updates)
|
|
1026
1385
|
.eq('id', args.report_id)
|
|
1027
|
-
.eq('project_id',
|
|
1386
|
+
.eq('project_id', currentProjectId); // Security: ensure report belongs to this project
|
|
1028
1387
|
if (error) {
|
|
1029
1388
|
return { error: error.message };
|
|
1030
1389
|
}
|
|
@@ -1038,7 +1397,7 @@ async function getReportContext(args) {
|
|
|
1038
1397
|
.from('reports')
|
|
1039
1398
|
.select('app_context, device_info, navigation_history, enhanced_context')
|
|
1040
1399
|
.eq('id', args.report_id)
|
|
1041
|
-
.eq('project_id',
|
|
1400
|
+
.eq('project_id', currentProjectId) // Security: ensure report belongs to this project
|
|
1042
1401
|
.single();
|
|
1043
1402
|
if (error) {
|
|
1044
1403
|
return { error: error.message };
|
|
@@ -1057,7 +1416,7 @@ async function getProjectInfo() {
|
|
|
1057
1416
|
const { data: project, error: projectError } = await supabase
|
|
1058
1417
|
.from('projects')
|
|
1059
1418
|
.select('id, name, slug, is_qa_enabled')
|
|
1060
|
-
.eq('id',
|
|
1419
|
+
.eq('id', currentProjectId)
|
|
1061
1420
|
.single();
|
|
1062
1421
|
if (projectError) {
|
|
1063
1422
|
return { error: projectError.message };
|
|
@@ -1066,17 +1425,17 @@ async function getProjectInfo() {
|
|
|
1066
1425
|
const { data: tracks } = await supabase
|
|
1067
1426
|
.from('qa_tracks')
|
|
1068
1427
|
.select('id, name, icon, test_template')
|
|
1069
|
-
.eq('project_id',
|
|
1428
|
+
.eq('project_id', currentProjectId);
|
|
1070
1429
|
// Get test case count
|
|
1071
1430
|
const { count: testCaseCount } = await supabase
|
|
1072
1431
|
.from('test_cases')
|
|
1073
1432
|
.select('id', { count: 'exact', head: true })
|
|
1074
|
-
.eq('project_id',
|
|
1433
|
+
.eq('project_id', currentProjectId);
|
|
1075
1434
|
// Get open bug count
|
|
1076
1435
|
const { count: openBugCount } = await supabase
|
|
1077
1436
|
.from('reports')
|
|
1078
1437
|
.select('id', { count: 'exact', head: true })
|
|
1079
|
-
.eq('project_id',
|
|
1438
|
+
.eq('project_id', currentProjectId)
|
|
1080
1439
|
.eq('report_type', 'bug')
|
|
1081
1440
|
.in('status', ['new', 'confirmed', 'in_progress']);
|
|
1082
1441
|
return {
|
|
@@ -1103,7 +1462,7 @@ async function getQaTracks() {
|
|
|
1103
1462
|
const { data, error } = await supabase
|
|
1104
1463
|
.from('qa_tracks')
|
|
1105
1464
|
.select('*')
|
|
1106
|
-
.eq('project_id',
|
|
1465
|
+
.eq('project_id', currentProjectId)
|
|
1107
1466
|
.order('sort_order');
|
|
1108
1467
|
if (error) {
|
|
1109
1468
|
return { error: error.message };
|
|
@@ -1126,16 +1485,19 @@ async function createTestCase(args) {
|
|
|
1126
1485
|
// Find track ID if track name provided
|
|
1127
1486
|
let trackId = null;
|
|
1128
1487
|
if (args.track) {
|
|
1129
|
-
const
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1488
|
+
const sanitizedTrack = sanitizeSearchQuery(args.track);
|
|
1489
|
+
if (sanitizedTrack) {
|
|
1490
|
+
const { data: trackData } = await supabase
|
|
1491
|
+
.from('qa_tracks')
|
|
1492
|
+
.select('id')
|
|
1493
|
+
.eq('project_id', currentProjectId)
|
|
1494
|
+
.ilike('name', `%${sanitizedTrack}%`)
|
|
1495
|
+
.single();
|
|
1496
|
+
trackId = trackData?.id || null;
|
|
1497
|
+
}
|
|
1136
1498
|
}
|
|
1137
1499
|
const testCase = {
|
|
1138
|
-
project_id:
|
|
1500
|
+
project_id: currentProjectId,
|
|
1139
1501
|
test_key: args.test_key,
|
|
1140
1502
|
title: args.title,
|
|
1141
1503
|
description: args.description || '',
|
|
@@ -1175,7 +1537,7 @@ async function updateTestCase(args) {
|
|
|
1175
1537
|
const { data: existing } = await supabase
|
|
1176
1538
|
.from('test_cases')
|
|
1177
1539
|
.select('id')
|
|
1178
|
-
.eq('project_id',
|
|
1540
|
+
.eq('project_id', currentProjectId)
|
|
1179
1541
|
.eq('test_key', args.test_key)
|
|
1180
1542
|
.single();
|
|
1181
1543
|
if (!existing) {
|
|
@@ -1206,7 +1568,7 @@ async function updateTestCase(args) {
|
|
|
1206
1568
|
.from('test_cases')
|
|
1207
1569
|
.update(updates)
|
|
1208
1570
|
.eq('id', testCaseId)
|
|
1209
|
-
.eq('project_id',
|
|
1571
|
+
.eq('project_id', currentProjectId)
|
|
1210
1572
|
.select('id, test_key, title, target_route')
|
|
1211
1573
|
.single();
|
|
1212
1574
|
if (error) {
|
|
@@ -1251,7 +1613,7 @@ async function deleteTestCases(args) {
|
|
|
1251
1613
|
const { data: existing } = await supabase
|
|
1252
1614
|
.from('test_cases')
|
|
1253
1615
|
.select('id')
|
|
1254
|
-
.eq('project_id',
|
|
1616
|
+
.eq('project_id', currentProjectId)
|
|
1255
1617
|
.eq('test_key', args.test_key)
|
|
1256
1618
|
.single();
|
|
1257
1619
|
if (!existing) {
|
|
@@ -1284,7 +1646,7 @@ async function deleteTestCases(args) {
|
|
|
1284
1646
|
const { data: existing, error: lookupError } = await supabase
|
|
1285
1647
|
.from('test_cases')
|
|
1286
1648
|
.select('id, test_key')
|
|
1287
|
-
.eq('project_id',
|
|
1649
|
+
.eq('project_id', currentProjectId)
|
|
1288
1650
|
.in('test_key', args.test_keys);
|
|
1289
1651
|
if (lookupError) {
|
|
1290
1652
|
return { error: lookupError.message };
|
|
@@ -1300,7 +1662,7 @@ async function deleteTestCases(args) {
|
|
|
1300
1662
|
const { data: toDelete } = await supabase
|
|
1301
1663
|
.from('test_cases')
|
|
1302
1664
|
.select('id, test_key, title')
|
|
1303
|
-
.eq('project_id',
|
|
1665
|
+
.eq('project_id', currentProjectId)
|
|
1304
1666
|
.in('id', idsToDelete);
|
|
1305
1667
|
if (!toDelete || toDelete.length === 0) {
|
|
1306
1668
|
return { error: 'No matching test cases found in this project' };
|
|
@@ -1309,7 +1671,7 @@ async function deleteTestCases(args) {
|
|
|
1309
1671
|
const { error: deleteError } = await supabase
|
|
1310
1672
|
.from('test_cases')
|
|
1311
1673
|
.delete()
|
|
1312
|
-
.eq('project_id',
|
|
1674
|
+
.eq('project_id', currentProjectId)
|
|
1313
1675
|
.in('id', idsToDelete);
|
|
1314
1676
|
if (deleteError) {
|
|
1315
1677
|
return { error: deleteError.message };
|
|
@@ -1343,7 +1705,7 @@ async function listTestCases(args) {
|
|
|
1343
1705
|
steps,
|
|
1344
1706
|
track:qa_tracks(id, name, icon, color)
|
|
1345
1707
|
`)
|
|
1346
|
-
.eq('project_id',
|
|
1708
|
+
.eq('project_id', currentProjectId)
|
|
1347
1709
|
.order('test_key', { ascending: true });
|
|
1348
1710
|
// Apply filters
|
|
1349
1711
|
if (args.priority) {
|
|
@@ -1390,7 +1752,7 @@ async function getBugPatterns(args) {
|
|
|
1390
1752
|
let query = supabase
|
|
1391
1753
|
.from('reports')
|
|
1392
1754
|
.select('app_context, severity, status, created_at')
|
|
1393
|
-
.eq('project_id',
|
|
1755
|
+
.eq('project_id', currentProjectId)
|
|
1394
1756
|
.eq('report_type', 'bug')
|
|
1395
1757
|
.order('created_at', { ascending: false })
|
|
1396
1758
|
.limit(100);
|
|
@@ -1442,7 +1804,7 @@ async function suggestTestCases(args) {
|
|
|
1442
1804
|
const { data: existingTests } = await supabase
|
|
1443
1805
|
.from('test_cases')
|
|
1444
1806
|
.select('test_key, title')
|
|
1445
|
-
.eq('project_id',
|
|
1807
|
+
.eq('project_id', currentProjectId)
|
|
1446
1808
|
.order('test_key', { ascending: false })
|
|
1447
1809
|
.limit(1);
|
|
1448
1810
|
// Calculate next test key number
|
|
@@ -1477,7 +1839,7 @@ async function suggestTestCases(args) {
|
|
|
1477
1839
|
const { data: relatedBugs } = await supabase
|
|
1478
1840
|
.from('reports')
|
|
1479
1841
|
.select('id, description, severity')
|
|
1480
|
-
.eq('project_id',
|
|
1842
|
+
.eq('project_id', currentProjectId)
|
|
1481
1843
|
.eq('report_type', 'bug')
|
|
1482
1844
|
.limit(10);
|
|
1483
1845
|
const routeBugs = (relatedBugs || []).filter(bug => {
|
|
@@ -1511,12 +1873,16 @@ async function getTestPriorities(args) {
|
|
|
1511
1873
|
const minScore = args.min_score || 0;
|
|
1512
1874
|
const includeFactors = args.include_factors !== false;
|
|
1513
1875
|
// First, refresh the route stats
|
|
1514
|
-
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 });
|
|
1877
|
+
if (refreshError) {
|
|
1878
|
+
// Non-fatal: proceed with potentially stale data but warn
|
|
1879
|
+
console.warn('Failed to refresh route stats:', refreshError.message);
|
|
1880
|
+
}
|
|
1515
1881
|
// Get prioritized routes
|
|
1516
1882
|
const { data: routes, error } = await supabase
|
|
1517
1883
|
.from('route_test_stats')
|
|
1518
1884
|
.select('*')
|
|
1519
|
-
.eq('project_id',
|
|
1885
|
+
.eq('project_id', currentProjectId)
|
|
1520
1886
|
.gte('priority_score', minScore)
|
|
1521
1887
|
.order('priority_score', { ascending: false })
|
|
1522
1888
|
.limit(limit);
|
|
@@ -1633,7 +1999,7 @@ async function getCoverageGaps(args) {
|
|
|
1633
1999
|
const { data: routesFromReports } = await supabase
|
|
1634
2000
|
.from('reports')
|
|
1635
2001
|
.select('app_context')
|
|
1636
|
-
.eq('project_id',
|
|
2002
|
+
.eq('project_id', currentProjectId)
|
|
1637
2003
|
.not('app_context->currentRoute', 'is', null);
|
|
1638
2004
|
const allRoutes = new Set();
|
|
1639
2005
|
(routesFromReports || []).forEach(r => {
|
|
@@ -1645,7 +2011,7 @@ async function getCoverageGaps(args) {
|
|
|
1645
2011
|
const { data: testCases } = await supabase
|
|
1646
2012
|
.from('test_cases')
|
|
1647
2013
|
.select('target_route, category, track_id')
|
|
1648
|
-
.eq('project_id',
|
|
2014
|
+
.eq('project_id', currentProjectId);
|
|
1649
2015
|
const coveredRoutes = new Set();
|
|
1650
2016
|
const routeTrackCoverage = {};
|
|
1651
2017
|
(testCases || []).forEach(tc => {
|
|
@@ -1662,13 +2028,13 @@ async function getCoverageGaps(args) {
|
|
|
1662
2028
|
const { data: tracks } = await supabase
|
|
1663
2029
|
.from('qa_tracks')
|
|
1664
2030
|
.select('id, name')
|
|
1665
|
-
.eq('project_id',
|
|
2031
|
+
.eq('project_id', currentProjectId);
|
|
1666
2032
|
const trackMap = new Map((tracks || []).map(t => [t.id, t.name]));
|
|
1667
2033
|
// Get route stats for staleness
|
|
1668
2034
|
const { data: routeStats } = await supabase
|
|
1669
2035
|
.from('route_test_stats')
|
|
1670
2036
|
.select('route, last_tested_at, open_bugs, critical_bugs')
|
|
1671
|
-
.eq('project_id',
|
|
2037
|
+
.eq('project_id', currentProjectId);
|
|
1672
2038
|
const routeStatsMap = new Map((routeStats || []).map(r => [r.route, r]));
|
|
1673
2039
|
// Find untested routes
|
|
1674
2040
|
if (gapType === 'all' || gapType === 'untested_routes') {
|
|
@@ -1765,14 +2131,14 @@ async function getRegressions(args) {
|
|
|
1765
2131
|
const { data: resolvedBugs } = await supabase
|
|
1766
2132
|
.from('reports')
|
|
1767
2133
|
.select('id, description, severity, app_context, resolved_at')
|
|
1768
|
-
.eq('project_id',
|
|
2134
|
+
.eq('project_id', currentProjectId)
|
|
1769
2135
|
.eq('report_type', 'bug')
|
|
1770
2136
|
.in('status', ['resolved', 'fixed', 'verified', 'closed'])
|
|
1771
2137
|
.gte('resolved_at', new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString());
|
|
1772
2138
|
const { data: newBugs } = await supabase
|
|
1773
2139
|
.from('reports')
|
|
1774
2140
|
.select('id, description, severity, app_context, created_at')
|
|
1775
|
-
.eq('project_id',
|
|
2141
|
+
.eq('project_id', currentProjectId)
|
|
1776
2142
|
.eq('report_type', 'bug')
|
|
1777
2143
|
.in('status', ['new', 'triaging', 'confirmed', 'in_progress', 'reviewed'])
|
|
1778
2144
|
.gte('created_at', new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString());
|
|
@@ -1876,21 +2242,23 @@ async function getCoverageMatrix(args) {
|
|
|
1876
2242
|
const { data: tracks } = await supabase
|
|
1877
2243
|
.from('qa_tracks')
|
|
1878
2244
|
.select('id, name, icon, color')
|
|
1879
|
-
.eq('project_id',
|
|
2245
|
+
.eq('project_id', currentProjectId)
|
|
1880
2246
|
.order('sort_order');
|
|
1881
2247
|
// Get test cases with track info
|
|
1882
2248
|
const { data: testCases } = await supabase
|
|
1883
2249
|
.from('test_cases')
|
|
1884
2250
|
.select('id, target_route, category, track_id')
|
|
1885
|
-
.eq('project_id',
|
|
2251
|
+
.eq('project_id', currentProjectId);
|
|
1886
2252
|
// Get test assignments for execution data
|
|
1887
2253
|
let assignments = [];
|
|
1888
2254
|
if (includeExecution) {
|
|
1889
2255
|
const { data } = await supabase
|
|
1890
2256
|
.from('test_assignments')
|
|
1891
2257
|
.select('test_case_id, status, completed_at')
|
|
1892
|
-
.eq('project_id',
|
|
1893
|
-
.in('status', ['passed', 'failed'])
|
|
2258
|
+
.eq('project_id', currentProjectId)
|
|
2259
|
+
.in('status', ['passed', 'failed'])
|
|
2260
|
+
.order('completed_at', { ascending: false })
|
|
2261
|
+
.limit(2000);
|
|
1894
2262
|
assignments = data || [];
|
|
1895
2263
|
}
|
|
1896
2264
|
// Get route stats for bug counts
|
|
@@ -1899,7 +2267,7 @@ async function getCoverageMatrix(args) {
|
|
|
1899
2267
|
const { data } = await supabase
|
|
1900
2268
|
.from('route_test_stats')
|
|
1901
2269
|
.select('route, open_bugs, critical_bugs')
|
|
1902
|
-
.eq('project_id',
|
|
2270
|
+
.eq('project_id', currentProjectId);
|
|
1903
2271
|
routeStats = data || [];
|
|
1904
2272
|
}
|
|
1905
2273
|
const routeStatsMap = new Map(routeStats.map(r => [r.route, r]));
|
|
@@ -2038,12 +2406,16 @@ async function getStaleCoverage(args) {
|
|
|
2038
2406
|
const daysThreshold = args.days_threshold || 14;
|
|
2039
2407
|
const limit = args.limit || 20;
|
|
2040
2408
|
// Refresh stats first
|
|
2041
|
-
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 });
|
|
2410
|
+
if (refreshError) {
|
|
2411
|
+
// Non-fatal: proceed with potentially stale data but warn
|
|
2412
|
+
console.warn('Failed to refresh route stats:', refreshError.message);
|
|
2413
|
+
}
|
|
2042
2414
|
// Get routes ordered by staleness and risk
|
|
2043
2415
|
const { data: routes, error } = await supabase
|
|
2044
2416
|
.from('route_test_stats')
|
|
2045
2417
|
.select('route, last_tested_at, open_bugs, critical_bugs, test_case_count, priority_score')
|
|
2046
|
-
.eq('project_id',
|
|
2418
|
+
.eq('project_id', currentProjectId)
|
|
2047
2419
|
.order('last_tested_at', { ascending: true, nullsFirst: true })
|
|
2048
2420
|
.limit(limit * 2); // Get extra to filter
|
|
2049
2421
|
if (error) {
|
|
@@ -2123,17 +2495,34 @@ async function generateDeployChecklist(args) {
|
|
|
2123
2495
|
}
|
|
2124
2496
|
});
|
|
2125
2497
|
}
|
|
2126
|
-
//
|
|
2127
|
-
const
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2498
|
+
// Limit routes array to prevent query explosion
|
|
2499
|
+
const safeRoutes = routes.slice(0, 100);
|
|
2500
|
+
// Get test cases for these routes (use separate queries to avoid filter injection)
|
|
2501
|
+
const [{ data: byRoute }, { data: byCategory }] = await Promise.all([
|
|
2502
|
+
supabase
|
|
2503
|
+
.from('test_cases')
|
|
2504
|
+
.select('id, test_key, title, target_route, category, priority, track:qa_tracks(name)')
|
|
2505
|
+
.eq('project_id', currentProjectId)
|
|
2506
|
+
.in('target_route', safeRoutes),
|
|
2507
|
+
supabase
|
|
2508
|
+
.from('test_cases')
|
|
2509
|
+
.select('id, test_key, title, target_route, category, priority, track:qa_tracks(name)')
|
|
2510
|
+
.eq('project_id', currentProjectId)
|
|
2511
|
+
.in('category', safeRoutes),
|
|
2512
|
+
]);
|
|
2513
|
+
// Deduplicate by id
|
|
2514
|
+
const seenIds = new Set();
|
|
2515
|
+
const testCases = [...(byRoute || []), ...(byCategory || [])].filter(tc => {
|
|
2516
|
+
if (seenIds.has(tc.id))
|
|
2517
|
+
return false;
|
|
2518
|
+
seenIds.add(tc.id);
|
|
2519
|
+
return true;
|
|
2520
|
+
});
|
|
2132
2521
|
// Get route stats for risk assessment
|
|
2133
2522
|
const { data: routeStats } = await supabase
|
|
2134
2523
|
.from('route_test_stats')
|
|
2135
2524
|
.select('*')
|
|
2136
|
-
.eq('project_id',
|
|
2525
|
+
.eq('project_id', currentProjectId)
|
|
2137
2526
|
.in('route', Array.from(allRoutes));
|
|
2138
2527
|
const routeStatsMap = new Map((routeStats || []).map(r => [r.route, r]));
|
|
2139
2528
|
// Categorize tests
|
|
@@ -2232,30 +2621,30 @@ async function getQAHealth(args) {
|
|
|
2232
2621
|
const { data: currentTests } = await supabase
|
|
2233
2622
|
.from('test_assignments')
|
|
2234
2623
|
.select('id, status, completed_at')
|
|
2235
|
-
.eq('project_id',
|
|
2624
|
+
.eq('project_id', currentProjectId)
|
|
2236
2625
|
.gte('completed_at', periodStart.toISOString())
|
|
2237
2626
|
.in('status', ['passed', 'failed']);
|
|
2238
2627
|
const { data: currentBugs } = await supabase
|
|
2239
2628
|
.from('reports')
|
|
2240
2629
|
.select('id, severity, status, created_at')
|
|
2241
|
-
.eq('project_id',
|
|
2630
|
+
.eq('project_id', currentProjectId)
|
|
2242
2631
|
.eq('report_type', 'bug')
|
|
2243
2632
|
.gte('created_at', periodStart.toISOString());
|
|
2244
2633
|
const { data: resolvedBugs } = await supabase
|
|
2245
2634
|
.from('reports')
|
|
2246
2635
|
.select('id, created_at, resolved_at')
|
|
2247
|
-
.eq('project_id',
|
|
2636
|
+
.eq('project_id', currentProjectId)
|
|
2248
2637
|
.eq('report_type', 'bug')
|
|
2249
2638
|
.in('status', ['resolved', 'fixed', 'verified', 'closed'])
|
|
2250
2639
|
.gte('resolved_at', periodStart.toISOString());
|
|
2251
2640
|
const { data: testers } = await supabase
|
|
2252
2641
|
.from('testers')
|
|
2253
2642
|
.select('id, status')
|
|
2254
|
-
.eq('project_id',
|
|
2643
|
+
.eq('project_id', currentProjectId);
|
|
2255
2644
|
const { data: routeStats } = await supabase
|
|
2256
2645
|
.from('route_test_stats')
|
|
2257
2646
|
.select('route, test_case_count')
|
|
2258
|
-
.eq('project_id',
|
|
2647
|
+
.eq('project_id', currentProjectId);
|
|
2259
2648
|
// Get previous period data for comparison
|
|
2260
2649
|
let previousTests = [];
|
|
2261
2650
|
let previousBugs = [];
|
|
@@ -2264,7 +2653,7 @@ async function getQAHealth(args) {
|
|
|
2264
2653
|
const { data: pt } = await supabase
|
|
2265
2654
|
.from('test_assignments')
|
|
2266
2655
|
.select('id, status')
|
|
2267
|
-
.eq('project_id',
|
|
2656
|
+
.eq('project_id', currentProjectId)
|
|
2268
2657
|
.gte('completed_at', previousStart.toISOString())
|
|
2269
2658
|
.lt('completed_at', periodStart.toISOString())
|
|
2270
2659
|
.in('status', ['passed', 'failed']);
|
|
@@ -2272,7 +2661,7 @@ async function getQAHealth(args) {
|
|
|
2272
2661
|
const { data: pb } = await supabase
|
|
2273
2662
|
.from('reports')
|
|
2274
2663
|
.select('id, severity')
|
|
2275
|
-
.eq('project_id',
|
|
2664
|
+
.eq('project_id', currentProjectId)
|
|
2276
2665
|
.eq('report_type', 'bug')
|
|
2277
2666
|
.gte('created_at', previousStart.toISOString())
|
|
2278
2667
|
.lt('created_at', periodStart.toISOString());
|
|
@@ -2280,7 +2669,7 @@ async function getQAHealth(args) {
|
|
|
2280
2669
|
const { data: pr } = await supabase
|
|
2281
2670
|
.from('reports')
|
|
2282
2671
|
.select('id')
|
|
2283
|
-
.eq('project_id',
|
|
2672
|
+
.eq('project_id', currentProjectId)
|
|
2284
2673
|
.in('status', ['resolved', 'fixed', 'verified', 'closed'])
|
|
2285
2674
|
.gte('resolved_at', previousStart.toISOString())
|
|
2286
2675
|
.lt('resolved_at', periodStart.toISOString());
|
|
@@ -2434,7 +2823,7 @@ async function getQASessions(args) {
|
|
|
2434
2823
|
findings_count, bugs_filed, created_at,
|
|
2435
2824
|
tester:testers(id, name, email)
|
|
2436
2825
|
`)
|
|
2437
|
-
.eq('project_id',
|
|
2826
|
+
.eq('project_id', currentProjectId)
|
|
2438
2827
|
.order('started_at', { ascending: false })
|
|
2439
2828
|
.limit(limit);
|
|
2440
2829
|
if (status !== 'all') {
|
|
@@ -2484,12 +2873,12 @@ async function getQAAlerts(args) {
|
|
|
2484
2873
|
const status = args.status || 'active';
|
|
2485
2874
|
// Optionally refresh alerts
|
|
2486
2875
|
if (args.refresh) {
|
|
2487
|
-
await supabase.rpc('detect_all_alerts', { p_project_id:
|
|
2876
|
+
await supabase.rpc('detect_all_alerts', { p_project_id: currentProjectId });
|
|
2488
2877
|
}
|
|
2489
2878
|
let query = supabase
|
|
2490
2879
|
.from('qa_alerts')
|
|
2491
2880
|
.select('*')
|
|
2492
|
-
.eq('project_id',
|
|
2881
|
+
.eq('project_id', currentProjectId)
|
|
2493
2882
|
.order('severity', { ascending: true }) // critical first
|
|
2494
2883
|
.order('created_at', { ascending: false });
|
|
2495
2884
|
if (severity !== 'all') {
|
|
@@ -2542,7 +2931,7 @@ async function getDeploymentAnalysis(args) {
|
|
|
2542
2931
|
.from('deployments')
|
|
2543
2932
|
.select('*')
|
|
2544
2933
|
.eq('id', args.deployment_id)
|
|
2545
|
-
.eq('project_id',
|
|
2934
|
+
.eq('project_id', currentProjectId)
|
|
2546
2935
|
.single();
|
|
2547
2936
|
if (error) {
|
|
2548
2937
|
return { error: error.message };
|
|
@@ -2553,7 +2942,7 @@ async function getDeploymentAnalysis(args) {
|
|
|
2553
2942
|
let query = supabase
|
|
2554
2943
|
.from('deployments')
|
|
2555
2944
|
.select('*')
|
|
2556
|
-
.eq('project_id',
|
|
2945
|
+
.eq('project_id', currentProjectId)
|
|
2557
2946
|
.order('deployed_at', { ascending: false })
|
|
2558
2947
|
.limit(limit);
|
|
2559
2948
|
if (args.environment && args.environment !== 'all') {
|
|
@@ -2627,14 +3016,23 @@ async function analyzeCommitForTesting(args) {
|
|
|
2627
3016
|
const { data: mappings } = await supabase
|
|
2628
3017
|
.from('file_route_mapping')
|
|
2629
3018
|
.select('file_pattern, route, feature, confidence')
|
|
2630
|
-
.eq('project_id',
|
|
3019
|
+
.eq('project_id', currentProjectId);
|
|
2631
3020
|
const affectedRoutes = [];
|
|
2632
3021
|
for (const mapping of mappings || []) {
|
|
2633
3022
|
const matchedFiles = filesChanged.filter(file => {
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
.
|
|
2637
|
-
|
|
3023
|
+
try {
|
|
3024
|
+
// Validate pattern complexity to prevent ReDoS
|
|
3025
|
+
if (mapping.file_pattern.length > 200)
|
|
3026
|
+
return false;
|
|
3027
|
+
const pattern = mapping.file_pattern
|
|
3028
|
+
.replace(/[.+?^${}()|[\]\\]/g, '\\$&') // Escape regex special chars first
|
|
3029
|
+
.replace(/\\\*\\\*/g, '.*') // Then convert glob ** to .*
|
|
3030
|
+
.replace(/\\\*/g, '[^/]*'); // And glob * to [^/]*
|
|
3031
|
+
return new RegExp(`^${pattern}$`).test(file);
|
|
3032
|
+
}
|
|
3033
|
+
catch {
|
|
3034
|
+
return false; // Skip malformed patterns
|
|
3035
|
+
}
|
|
2638
3036
|
});
|
|
2639
3037
|
if (matchedFiles.length > 0) {
|
|
2640
3038
|
affectedRoutes.push({
|
|
@@ -2652,7 +3050,7 @@ async function analyzeCommitForTesting(args) {
|
|
|
2652
3050
|
const { data: bugs } = await supabase
|
|
2653
3051
|
.from('reports')
|
|
2654
3052
|
.select('id, severity, description, route, created_at')
|
|
2655
|
-
.eq('project_id',
|
|
3053
|
+
.eq('project_id', currentProjectId)
|
|
2656
3054
|
.eq('report_type', 'bug')
|
|
2657
3055
|
.in('route', routes)
|
|
2658
3056
|
.gte('created_at', new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString());
|
|
@@ -2684,7 +3082,7 @@ async function analyzeCommitForTesting(args) {
|
|
|
2684
3082
|
// Optionally record as deployment
|
|
2685
3083
|
if (args.record_deployment) {
|
|
2686
3084
|
await supabase.rpc('record_deployment', {
|
|
2687
|
-
p_project_id:
|
|
3085
|
+
p_project_id: currentProjectId,
|
|
2688
3086
|
p_environment: 'production',
|
|
2689
3087
|
p_commit_sha: args.commit_sha || null,
|
|
2690
3088
|
p_commit_message: args.commit_message || null,
|
|
@@ -2773,12 +3171,12 @@ async function analyzeChangesForTests(args) {
|
|
|
2773
3171
|
const { data: existingTests } = await supabase
|
|
2774
3172
|
.from('test_cases')
|
|
2775
3173
|
.select('test_key, title, target_route, description')
|
|
2776
|
-
.eq('project_id',
|
|
3174
|
+
.eq('project_id', currentProjectId);
|
|
2777
3175
|
// Get next test key
|
|
2778
3176
|
const { data: lastTest } = await supabase
|
|
2779
3177
|
.from('test_cases')
|
|
2780
3178
|
.select('test_key')
|
|
2781
|
-
.eq('project_id',
|
|
3179
|
+
.eq('project_id', currentProjectId)
|
|
2782
3180
|
.order('test_key', { ascending: false })
|
|
2783
3181
|
.limit(1);
|
|
2784
3182
|
const lastKey = lastTest?.[0]?.test_key || 'TC-000';
|
|
@@ -2790,7 +3188,7 @@ async function analyzeChangesForTests(args) {
|
|
|
2790
3188
|
const { data: bugs } = await supabase
|
|
2791
3189
|
.from('reports')
|
|
2792
3190
|
.select('id, description, severity, app_context')
|
|
2793
|
-
.eq('project_id',
|
|
3191
|
+
.eq('project_id', currentProjectId)
|
|
2794
3192
|
.eq('report_type', 'bug')
|
|
2795
3193
|
.limit(50);
|
|
2796
3194
|
relatedBugs = (bugs || []).filter(bug => {
|
|
@@ -3135,7 +3533,7 @@ async function createBugReport(args) {
|
|
|
3135
3533
|
const { data: project } = await supabase
|
|
3136
3534
|
.from('projects')
|
|
3137
3535
|
.select('owner_id')
|
|
3138
|
-
.eq('id',
|
|
3536
|
+
.eq('id', currentProjectId)
|
|
3139
3537
|
.single();
|
|
3140
3538
|
if (project?.owner_id) {
|
|
3141
3539
|
reporterId = project.owner_id;
|
|
@@ -3145,14 +3543,14 @@ async function createBugReport(args) {
|
|
|
3145
3543
|
const { data: testers } = await supabase
|
|
3146
3544
|
.from('testers')
|
|
3147
3545
|
.select('id')
|
|
3148
|
-
.eq('project_id',
|
|
3546
|
+
.eq('project_id', currentProjectId)
|
|
3149
3547
|
.limit(1);
|
|
3150
3548
|
if (testers && testers.length > 0) {
|
|
3151
3549
|
reporterId = testers[0].id;
|
|
3152
3550
|
}
|
|
3153
3551
|
}
|
|
3154
3552
|
const report = {
|
|
3155
|
-
project_id:
|
|
3553
|
+
project_id: currentProjectId,
|
|
3156
3554
|
report_type: 'bug',
|
|
3157
3555
|
title: args.title,
|
|
3158
3556
|
description: args.description,
|
|
@@ -3216,7 +3614,7 @@ async function getBugsForFile(args) {
|
|
|
3216
3614
|
let query = supabase
|
|
3217
3615
|
.from('reports')
|
|
3218
3616
|
.select('id, title, description, severity, status, created_at, code_context')
|
|
3219
|
-
.eq('project_id',
|
|
3617
|
+
.eq('project_id', currentProjectId)
|
|
3220
3618
|
.eq('report_type', 'bug');
|
|
3221
3619
|
if (!args.include_resolved) {
|
|
3222
3620
|
query = query.in('status', ['new', 'confirmed', 'in_progress', 'reviewed']);
|
|
@@ -3282,7 +3680,7 @@ async function markFixedWithCommit(args) {
|
|
|
3282
3680
|
.from('reports')
|
|
3283
3681
|
.select('code_context')
|
|
3284
3682
|
.eq('id', args.report_id)
|
|
3285
|
-
.eq('project_id',
|
|
3683
|
+
.eq('project_id', currentProjectId) // Security: ensure report belongs to this project
|
|
3286
3684
|
.single();
|
|
3287
3685
|
if (fetchError) {
|
|
3288
3686
|
return { error: fetchError.message };
|
|
@@ -3292,6 +3690,7 @@ async function markFixedWithCommit(args) {
|
|
|
3292
3690
|
status: 'resolved',
|
|
3293
3691
|
resolved_at: new Date().toISOString(),
|
|
3294
3692
|
resolution_notes: args.resolution_notes || `Fixed in commit ${args.commit_sha.slice(0, 7)}`,
|
|
3693
|
+
notify_tester: args.notify_tester === true, // Opt-in: only notify if explicitly requested
|
|
3295
3694
|
code_context: {
|
|
3296
3695
|
...existingContext,
|
|
3297
3696
|
fix: {
|
|
@@ -3307,15 +3706,19 @@ async function markFixedWithCommit(args) {
|
|
|
3307
3706
|
.from('reports')
|
|
3308
3707
|
.update(updates)
|
|
3309
3708
|
.eq('id', args.report_id)
|
|
3310
|
-
.eq('project_id',
|
|
3709
|
+
.eq('project_id', currentProjectId); // Security: ensure report belongs to this project
|
|
3311
3710
|
if (error) {
|
|
3312
3711
|
return { error: error.message };
|
|
3313
3712
|
}
|
|
3713
|
+
const notificationStatus = args.notify_tester
|
|
3714
|
+
? 'The original tester will be notified and assigned a verification task.'
|
|
3715
|
+
: 'No notification sent (silent resolve). A verification task was created.';
|
|
3314
3716
|
return {
|
|
3315
3717
|
success: true,
|
|
3316
|
-
message: `Bug marked as fixed in commit ${args.commit_sha.slice(0, 7)}`,
|
|
3718
|
+
message: `Bug marked as fixed in commit ${args.commit_sha.slice(0, 7)}. ${notificationStatus}`,
|
|
3317
3719
|
report_id: args.report_id,
|
|
3318
3720
|
commit: args.commit_sha,
|
|
3721
|
+
tester_notified: args.notify_tester === true,
|
|
3319
3722
|
next_steps: [
|
|
3320
3723
|
'Consider running create_regression_test to prevent this bug from recurring',
|
|
3321
3724
|
'Push your changes to trigger CI/CD',
|
|
@@ -3327,7 +3730,7 @@ async function getBugsAffectingCode(args) {
|
|
|
3327
3730
|
const { data, error } = await supabase
|
|
3328
3731
|
.from('reports')
|
|
3329
3732
|
.select('id, title, description, severity, status, code_context, app_context')
|
|
3330
|
-
.eq('project_id',
|
|
3733
|
+
.eq('project_id', currentProjectId)
|
|
3331
3734
|
.eq('report_type', 'bug')
|
|
3332
3735
|
.in('status', ['new', 'confirmed', 'in_progress', 'reviewed'])
|
|
3333
3736
|
.order('severity', { ascending: true });
|
|
@@ -3432,7 +3835,7 @@ async function linkBugToCode(args) {
|
|
|
3432
3835
|
.from('reports')
|
|
3433
3836
|
.select('code_context')
|
|
3434
3837
|
.eq('id', args.report_id)
|
|
3435
|
-
.eq('project_id',
|
|
3838
|
+
.eq('project_id', currentProjectId) // Security: ensure report belongs to this project
|
|
3436
3839
|
.single();
|
|
3437
3840
|
if (fetchError) {
|
|
3438
3841
|
return { error: fetchError.message };
|
|
@@ -3453,7 +3856,7 @@ async function linkBugToCode(args) {
|
|
|
3453
3856
|
.from('reports')
|
|
3454
3857
|
.update(updates)
|
|
3455
3858
|
.eq('id', args.report_id)
|
|
3456
|
-
.eq('project_id',
|
|
3859
|
+
.eq('project_id', currentProjectId); // Security: ensure report belongs to this project
|
|
3457
3860
|
if (error) {
|
|
3458
3861
|
return { error: error.message };
|
|
3459
3862
|
}
|
|
@@ -3472,7 +3875,7 @@ async function createRegressionTest(args) {
|
|
|
3472
3875
|
.from('reports')
|
|
3473
3876
|
.select('*')
|
|
3474
3877
|
.eq('id', args.report_id)
|
|
3475
|
-
.eq('project_id',
|
|
3878
|
+
.eq('project_id', currentProjectId) // Security: ensure report belongs to this project
|
|
3476
3879
|
.single();
|
|
3477
3880
|
if (fetchError) {
|
|
3478
3881
|
return { error: fetchError.message };
|
|
@@ -3489,7 +3892,7 @@ async function createRegressionTest(args) {
|
|
|
3489
3892
|
const { data: existingTests } = await supabase
|
|
3490
3893
|
.from('test_cases')
|
|
3491
3894
|
.select('test_key')
|
|
3492
|
-
.eq('project_id',
|
|
3895
|
+
.eq('project_id', currentProjectId)
|
|
3493
3896
|
.order('test_key', { ascending: false })
|
|
3494
3897
|
.limit(1);
|
|
3495
3898
|
const lastKey = existingTests?.[0]?.test_key || 'TC-000';
|
|
@@ -3500,7 +3903,7 @@ async function createRegressionTest(args) {
|
|
|
3500
3903
|
const targetRoute = appContext?.currentRoute;
|
|
3501
3904
|
// Generate test case from bug
|
|
3502
3905
|
const testCase = {
|
|
3503
|
-
project_id:
|
|
3906
|
+
project_id: currentProjectId,
|
|
3504
3907
|
test_key: newKey,
|
|
3505
3908
|
title: `Regression: ${report.title}`,
|
|
3506
3909
|
description: `Regression test to prevent recurrence of bug #${args.report_id.slice(0, 8)}\n\nOriginal bug: ${report.description}`,
|
|
@@ -3576,7 +3979,7 @@ async function getPendingFixes(args) {
|
|
|
3576
3979
|
created_at,
|
|
3577
3980
|
report:reports(id, title, severity, description)
|
|
3578
3981
|
`)
|
|
3579
|
-
.eq('project_id',
|
|
3982
|
+
.eq('project_id', currentProjectId)
|
|
3580
3983
|
.order('created_at', { ascending: true })
|
|
3581
3984
|
.limit(limit);
|
|
3582
3985
|
if (!args.include_claimed) {
|
|
@@ -3626,7 +4029,7 @@ async function claimFixRequest(args) {
|
|
|
3626
4029
|
.from('fix_requests')
|
|
3627
4030
|
.select('id, status, claimed_by, prompt, title')
|
|
3628
4031
|
.eq('id', args.fix_request_id)
|
|
3629
|
-
.eq('project_id',
|
|
4032
|
+
.eq('project_id', currentProjectId) // Security: ensure fix request belongs to this project
|
|
3630
4033
|
.single();
|
|
3631
4034
|
if (checkError) {
|
|
3632
4035
|
return { error: checkError.message };
|
|
@@ -3653,7 +4056,7 @@ async function claimFixRequest(args) {
|
|
|
3653
4056
|
claimed_by: claimedBy,
|
|
3654
4057
|
})
|
|
3655
4058
|
.eq('id', args.fix_request_id)
|
|
3656
|
-
.eq('project_id',
|
|
4059
|
+
.eq('project_id', currentProjectId) // Security: ensure fix request belongs to this project
|
|
3657
4060
|
.eq('status', 'pending'); // Only claim if still pending (race condition protection)
|
|
3658
4061
|
if (updateError) {
|
|
3659
4062
|
return { error: updateError.message };
|
|
@@ -3688,7 +4091,7 @@ async function completeFixRequest(args) {
|
|
|
3688
4091
|
.from('fix_requests')
|
|
3689
4092
|
.update(updates)
|
|
3690
4093
|
.eq('id', args.fix_request_id)
|
|
3691
|
-
.eq('project_id',
|
|
4094
|
+
.eq('project_id', currentProjectId); // Security: ensure fix request belongs to this project
|
|
3692
4095
|
if (error) {
|
|
3693
4096
|
return { error: error.message };
|
|
3694
4097
|
}
|
|
@@ -3771,7 +4174,7 @@ async function generatePromptContent(name, args) {
|
|
|
3771
4174
|
created_at,
|
|
3772
4175
|
report:reports(id, title, severity)
|
|
3773
4176
|
`)
|
|
3774
|
-
.eq('project_id',
|
|
4177
|
+
.eq('project_id', currentProjectId)
|
|
3775
4178
|
.eq('status', 'pending')
|
|
3776
4179
|
.order('created_at', { ascending: true })
|
|
3777
4180
|
.limit(5);
|
|
@@ -3779,7 +4182,7 @@ async function generatePromptContent(name, args) {
|
|
|
3779
4182
|
let query = supabase
|
|
3780
4183
|
.from('reports')
|
|
3781
4184
|
.select('id, title, description, severity, status, code_context, created_at')
|
|
3782
|
-
.eq('project_id',
|
|
4185
|
+
.eq('project_id', currentProjectId)
|
|
3783
4186
|
.eq('report_type', 'bug')
|
|
3784
4187
|
.in('status', ['new', 'confirmed', 'in_progress']);
|
|
3785
4188
|
if (severity !== 'all') {
|
|
@@ -3929,7 +4332,7 @@ Would you like me to generate test cases for these files?`;
|
|
|
3929
4332
|
const { data: resolvedBugs } = await supabase
|
|
3930
4333
|
.from('reports')
|
|
3931
4334
|
.select('id, title, description, severity, resolved_at, code_context')
|
|
3932
|
-
.eq('project_id',
|
|
4335
|
+
.eq('project_id', currentProjectId)
|
|
3933
4336
|
.eq('report_type', 'bug')
|
|
3934
4337
|
.eq('status', 'resolved')
|
|
3935
4338
|
.order('resolved_at', { ascending: false })
|
|
@@ -4036,28 +4439,809 @@ Which files or areas would you like me to analyze?`;
|
|
|
4036
4439
|
return 'Unknown prompt';
|
|
4037
4440
|
}
|
|
4038
4441
|
}
|
|
4039
|
-
//
|
|
4040
|
-
async function
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
name
|
|
4044
|
-
|
|
4045
|
-
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
|
|
4050
|
-
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
|
|
4442
|
+
// === TESTER & ASSIGNMENT MANAGEMENT HANDLERS ===
|
|
4443
|
+
async function listTesters(args) {
|
|
4444
|
+
let query = supabase
|
|
4445
|
+
.from('testers')
|
|
4446
|
+
.select('id, name, email, status, platforms, tier, assigned_count, completed_count, notes, created_at')
|
|
4447
|
+
.eq('project_id', currentProjectId)
|
|
4448
|
+
.order('name', { ascending: true });
|
|
4449
|
+
if (args.status) {
|
|
4450
|
+
query = query.eq('status', args.status);
|
|
4451
|
+
}
|
|
4452
|
+
const { data, error } = await query;
|
|
4453
|
+
if (error) {
|
|
4454
|
+
return { error: error.message };
|
|
4455
|
+
}
|
|
4456
|
+
let testers = data || [];
|
|
4457
|
+
// Filter by platform if specified (platforms is an array column)
|
|
4458
|
+
if (args.platform) {
|
|
4459
|
+
testers = testers.filter((t) => t.platforms && t.platforms.includes(args.platform));
|
|
4460
|
+
}
|
|
4461
|
+
return {
|
|
4462
|
+
count: testers.length,
|
|
4463
|
+
testers: testers.map((t) => ({
|
|
4464
|
+
id: t.id,
|
|
4465
|
+
name: t.name,
|
|
4466
|
+
email: t.email,
|
|
4467
|
+
status: t.status,
|
|
4468
|
+
platforms: t.platforms,
|
|
4469
|
+
tier: t.tier,
|
|
4470
|
+
assignedCount: t.assigned_count,
|
|
4471
|
+
completedCount: t.completed_count,
|
|
4472
|
+
notes: t.notes,
|
|
4473
|
+
})),
|
|
4474
|
+
};
|
|
4475
|
+
}
|
|
4476
|
+
async function listTestRuns(args) {
|
|
4477
|
+
const limit = Math.min(args.limit || 20, 50);
|
|
4478
|
+
let query = supabase
|
|
4479
|
+
.from('test_runs')
|
|
4480
|
+
.select('id, name, description, status, total_tests, passed_tests, failed_tests, started_at, completed_at, created_at')
|
|
4481
|
+
.eq('project_id', currentProjectId)
|
|
4482
|
+
.order('created_at', { ascending: false })
|
|
4483
|
+
.limit(limit);
|
|
4484
|
+
if (args.status) {
|
|
4485
|
+
query = query.eq('status', args.status);
|
|
4486
|
+
}
|
|
4487
|
+
const { data, error } = await query;
|
|
4488
|
+
if (error) {
|
|
4489
|
+
return { error: error.message };
|
|
4490
|
+
}
|
|
4491
|
+
return {
|
|
4492
|
+
count: (data || []).length,
|
|
4493
|
+
testRuns: (data || []).map((r) => ({
|
|
4494
|
+
id: r.id,
|
|
4495
|
+
name: r.name,
|
|
4496
|
+
description: r.description,
|
|
4497
|
+
status: r.status,
|
|
4498
|
+
totalTests: r.total_tests,
|
|
4499
|
+
passedTests: r.passed_tests,
|
|
4500
|
+
failedTests: r.failed_tests,
|
|
4501
|
+
passRate: r.total_tests > 0 ? Math.round((r.passed_tests / r.total_tests) * 100) : 0,
|
|
4502
|
+
startedAt: r.started_at,
|
|
4503
|
+
completedAt: r.completed_at,
|
|
4504
|
+
createdAt: r.created_at,
|
|
4505
|
+
})),
|
|
4506
|
+
};
|
|
4507
|
+
}
|
|
4508
|
+
async function createTestRun(args) {
|
|
4509
|
+
if (!args.name || args.name.trim().length === 0) {
|
|
4510
|
+
return { error: 'Test run name is required' };
|
|
4511
|
+
}
|
|
4512
|
+
const { data, error } = await supabase
|
|
4513
|
+
.from('test_runs')
|
|
4514
|
+
.insert({
|
|
4515
|
+
project_id: currentProjectId,
|
|
4516
|
+
name: args.name.trim(),
|
|
4517
|
+
description: args.description?.trim() || null,
|
|
4518
|
+
status: 'draft',
|
|
4519
|
+
})
|
|
4520
|
+
.select('id, name, description, status, created_at')
|
|
4521
|
+
.single();
|
|
4522
|
+
if (error) {
|
|
4523
|
+
return { error: error.message };
|
|
4524
|
+
}
|
|
4525
|
+
return {
|
|
4526
|
+
success: true,
|
|
4527
|
+
testRun: {
|
|
4528
|
+
id: data.id,
|
|
4529
|
+
name: data.name,
|
|
4530
|
+
description: data.description,
|
|
4531
|
+
status: data.status,
|
|
4532
|
+
createdAt: data.created_at,
|
|
4533
|
+
},
|
|
4534
|
+
message: `Test run "${data.name}" created. Use assign_tests to add test assignments.`,
|
|
4535
|
+
};
|
|
4536
|
+
}
|
|
4537
|
+
async function listTestAssignments(args) {
|
|
4538
|
+
const limit = Math.min(args.limit || 50, 200);
|
|
4539
|
+
if (args.tester_id && !isValidUUID(args.tester_id)) {
|
|
4540
|
+
return { error: 'Invalid tester_id format' };
|
|
4541
|
+
}
|
|
4542
|
+
if (args.test_run_id && !isValidUUID(args.test_run_id)) {
|
|
4543
|
+
return { error: 'Invalid test_run_id format' };
|
|
4544
|
+
}
|
|
4545
|
+
let query = supabase
|
|
4546
|
+
.from('test_assignments')
|
|
4547
|
+
.select(`
|
|
4548
|
+
id,
|
|
4549
|
+
status,
|
|
4550
|
+
assigned_at,
|
|
4551
|
+
started_at,
|
|
4552
|
+
completed_at,
|
|
4553
|
+
duration_seconds,
|
|
4554
|
+
is_verification,
|
|
4555
|
+
notes,
|
|
4556
|
+
test_case:test_cases(id, test_key, title, priority, target_route),
|
|
4557
|
+
tester:testers(id, name, email),
|
|
4558
|
+
test_run:test_runs(id, name)
|
|
4559
|
+
`)
|
|
4560
|
+
.eq('project_id', currentProjectId)
|
|
4561
|
+
.order('assigned_at', { ascending: false })
|
|
4562
|
+
.limit(limit);
|
|
4563
|
+
if (args.tester_id) {
|
|
4564
|
+
query = query.eq('tester_id', args.tester_id);
|
|
4565
|
+
}
|
|
4566
|
+
if (args.test_run_id) {
|
|
4567
|
+
query = query.eq('test_run_id', args.test_run_id);
|
|
4568
|
+
}
|
|
4569
|
+
if (args.status) {
|
|
4570
|
+
query = query.eq('status', args.status);
|
|
4571
|
+
}
|
|
4572
|
+
const { data, error } = await query;
|
|
4573
|
+
if (error) {
|
|
4574
|
+
return { error: error.message };
|
|
4575
|
+
}
|
|
4576
|
+
return {
|
|
4577
|
+
count: (data || []).length,
|
|
4578
|
+
assignments: (data || []).map((a) => ({
|
|
4579
|
+
id: a.id,
|
|
4580
|
+
status: a.status,
|
|
4581
|
+
assignedAt: a.assigned_at,
|
|
4582
|
+
startedAt: a.started_at,
|
|
4583
|
+
completedAt: a.completed_at,
|
|
4584
|
+
durationSeconds: a.duration_seconds,
|
|
4585
|
+
isVerification: a.is_verification,
|
|
4586
|
+
notes: a.notes,
|
|
4587
|
+
testCase: a.test_case ? {
|
|
4588
|
+
id: a.test_case.id,
|
|
4589
|
+
testKey: a.test_case.test_key,
|
|
4590
|
+
title: a.test_case.title,
|
|
4591
|
+
priority: a.test_case.priority,
|
|
4592
|
+
targetRoute: a.test_case.target_route,
|
|
4593
|
+
} : null,
|
|
4594
|
+
tester: a.tester ? {
|
|
4595
|
+
id: a.tester.id,
|
|
4596
|
+
name: a.tester.name,
|
|
4597
|
+
email: a.tester.email,
|
|
4598
|
+
} : null,
|
|
4599
|
+
testRun: a.test_run ? {
|
|
4600
|
+
id: a.test_run.id,
|
|
4601
|
+
name: a.test_run.name,
|
|
4602
|
+
} : null,
|
|
4603
|
+
})),
|
|
4604
|
+
};
|
|
4605
|
+
}
|
|
4606
|
+
async function assignTests(args) {
|
|
4607
|
+
// Validate inputs
|
|
4608
|
+
if (!isValidUUID(args.tester_id)) {
|
|
4609
|
+
return { error: 'Invalid tester_id format' };
|
|
4610
|
+
}
|
|
4611
|
+
if (!args.test_case_ids || args.test_case_ids.length === 0) {
|
|
4612
|
+
return { error: 'At least one test_case_id is required' };
|
|
4613
|
+
}
|
|
4614
|
+
if (args.test_case_ids.length > 50) {
|
|
4615
|
+
return { error: 'Maximum 50 test cases per assignment batch' };
|
|
4616
|
+
}
|
|
4617
|
+
for (const id of args.test_case_ids) {
|
|
4618
|
+
if (!isValidUUID(id)) {
|
|
4619
|
+
return { error: `Invalid test_case_id format: ${id}` };
|
|
4620
|
+
}
|
|
4621
|
+
}
|
|
4622
|
+
if (args.test_run_id && !isValidUUID(args.test_run_id)) {
|
|
4623
|
+
return { error: 'Invalid test_run_id format' };
|
|
4624
|
+
}
|
|
4625
|
+
// Verify tester exists and is active
|
|
4626
|
+
const { data: tester, error: testerErr } = await supabase
|
|
4627
|
+
.from('testers')
|
|
4628
|
+
.select('id, name, email, status')
|
|
4629
|
+
.eq('id', args.tester_id)
|
|
4630
|
+
.eq('project_id', currentProjectId)
|
|
4631
|
+
.single();
|
|
4632
|
+
if (testerErr || !tester) {
|
|
4633
|
+
return { error: 'Tester not found in this project' };
|
|
4634
|
+
}
|
|
4635
|
+
if (tester.status !== 'active') {
|
|
4636
|
+
return { error: `Tester "${tester.name}" is ${tester.status}, not active` };
|
|
4637
|
+
}
|
|
4638
|
+
// Verify test cases exist for this project
|
|
4639
|
+
const { data: testCases, error: tcErr } = await supabase
|
|
4640
|
+
.from('test_cases')
|
|
4641
|
+
.select('id, test_key, title')
|
|
4642
|
+
.eq('project_id', currentProjectId)
|
|
4643
|
+
.in('id', args.test_case_ids);
|
|
4644
|
+
if (tcErr) {
|
|
4645
|
+
return { error: tcErr.message };
|
|
4646
|
+
}
|
|
4647
|
+
const foundIds = new Set((testCases || []).map((tc) => tc.id));
|
|
4648
|
+
const missingIds = args.test_case_ids.filter(id => !foundIds.has(id));
|
|
4649
|
+
if (missingIds.length > 0) {
|
|
4650
|
+
return {
|
|
4651
|
+
error: `Test cases not found in this project: ${missingIds.join(', ')}`,
|
|
4652
|
+
};
|
|
4653
|
+
}
|
|
4654
|
+
// Verify test run exists if provided
|
|
4655
|
+
if (args.test_run_id) {
|
|
4656
|
+
const { data: run, error: runErr } = await supabase
|
|
4657
|
+
.from('test_runs')
|
|
4658
|
+
.select('id')
|
|
4659
|
+
.eq('id', args.test_run_id)
|
|
4660
|
+
.eq('project_id', currentProjectId)
|
|
4661
|
+
.single();
|
|
4662
|
+
if (runErr || !run) {
|
|
4663
|
+
return { error: 'Test run not found in this project' };
|
|
4664
|
+
}
|
|
4665
|
+
}
|
|
4666
|
+
// Build assignment rows
|
|
4667
|
+
const rows = args.test_case_ids.map(tcId => ({
|
|
4668
|
+
project_id: currentProjectId,
|
|
4669
|
+
test_case_id: tcId,
|
|
4670
|
+
tester_id: args.tester_id,
|
|
4671
|
+
test_run_id: args.test_run_id || null,
|
|
4672
|
+
status: 'pending',
|
|
4673
|
+
}));
|
|
4674
|
+
// Insert — use upsert-like approach: insert and handle conflicts
|
|
4675
|
+
const { data: inserted, error: insertErr } = await supabase
|
|
4676
|
+
.from('test_assignments')
|
|
4677
|
+
.insert(rows)
|
|
4678
|
+
.select('id, test_case_id');
|
|
4679
|
+
if (insertErr) {
|
|
4680
|
+
// Check if it's a unique constraint violation
|
|
4681
|
+
if (insertErr.message.includes('duplicate') || insertErr.message.includes('unique')) {
|
|
4682
|
+
// Try inserting one by one to find duplicates
|
|
4683
|
+
const created = [];
|
|
4684
|
+
const skipped = [];
|
|
4685
|
+
for (const row of rows) {
|
|
4686
|
+
const { data: single, error: singleErr } = await supabase
|
|
4687
|
+
.from('test_assignments')
|
|
4688
|
+
.insert(row)
|
|
4689
|
+
.select('id, test_case_id')
|
|
4690
|
+
.single();
|
|
4691
|
+
if (singleErr) {
|
|
4692
|
+
const tc = testCases?.find((t) => t.id === row.test_case_id);
|
|
4693
|
+
skipped.push(tc?.test_key || row.test_case_id);
|
|
4694
|
+
}
|
|
4695
|
+
else if (single) {
|
|
4696
|
+
created.push(single);
|
|
4697
|
+
}
|
|
4698
|
+
}
|
|
4699
|
+
return {
|
|
4700
|
+
success: true,
|
|
4701
|
+
created: created.length,
|
|
4702
|
+
skipped: skipped.length,
|
|
4703
|
+
skippedTests: skipped,
|
|
4704
|
+
tester: { id: tester.id, name: tester.name },
|
|
4705
|
+
message: `Assigned ${created.length} test(s) to ${tester.name}. ${skipped.length} skipped (already assigned).`,
|
|
4706
|
+
};
|
|
4707
|
+
}
|
|
4708
|
+
return { error: insertErr.message };
|
|
4709
|
+
}
|
|
4710
|
+
return {
|
|
4711
|
+
success: true,
|
|
4712
|
+
created: (inserted || []).length,
|
|
4713
|
+
skipped: 0,
|
|
4714
|
+
tester: { id: tester.id, name: tester.name },
|
|
4715
|
+
testRun: args.test_run_id || null,
|
|
4716
|
+
message: `Assigned ${(inserted || []).length} test(s) to ${tester.name}.`,
|
|
4717
|
+
};
|
|
4718
|
+
}
|
|
4719
|
+
async function getTesterWorkload(args) {
|
|
4720
|
+
if (!isValidUUID(args.tester_id)) {
|
|
4721
|
+
return { error: 'Invalid tester_id format' };
|
|
4722
|
+
}
|
|
4723
|
+
// Get tester info
|
|
4724
|
+
const { data: tester, error: testerErr } = await supabase
|
|
4725
|
+
.from('testers')
|
|
4726
|
+
.select('id, name, email, status, platforms, tier')
|
|
4727
|
+
.eq('id', args.tester_id)
|
|
4728
|
+
.eq('project_id', currentProjectId)
|
|
4729
|
+
.single();
|
|
4730
|
+
if (testerErr || !tester) {
|
|
4731
|
+
return { error: 'Tester not found in this project' };
|
|
4732
|
+
}
|
|
4733
|
+
// Get all assignments for this tester in this project
|
|
4734
|
+
const { data: assignments, error: assignErr } = await supabase
|
|
4735
|
+
.from('test_assignments')
|
|
4736
|
+
.select(`
|
|
4737
|
+
id,
|
|
4738
|
+
status,
|
|
4739
|
+
assigned_at,
|
|
4740
|
+
completed_at,
|
|
4741
|
+
test_case:test_cases(test_key, title, priority),
|
|
4742
|
+
test_run:test_runs(name)
|
|
4743
|
+
`)
|
|
4744
|
+
.eq('project_id', currentProjectId)
|
|
4745
|
+
.eq('tester_id', args.tester_id)
|
|
4746
|
+
.order('assigned_at', { ascending: false });
|
|
4747
|
+
if (assignErr) {
|
|
4748
|
+
return { error: assignErr.message };
|
|
4749
|
+
}
|
|
4750
|
+
const all = assignments || [];
|
|
4751
|
+
// Count by status
|
|
4752
|
+
const counts = {
|
|
4753
|
+
pending: 0,
|
|
4754
|
+
in_progress: 0,
|
|
4755
|
+
passed: 0,
|
|
4756
|
+
failed: 0,
|
|
4757
|
+
blocked: 0,
|
|
4758
|
+
skipped: 0,
|
|
4759
|
+
};
|
|
4760
|
+
for (const a of all) {
|
|
4761
|
+
counts[a.status] = (counts[a.status] || 0) + 1;
|
|
4762
|
+
}
|
|
4763
|
+
return {
|
|
4764
|
+
tester: {
|
|
4765
|
+
id: tester.id,
|
|
4766
|
+
name: tester.name,
|
|
4767
|
+
email: tester.email,
|
|
4768
|
+
status: tester.status,
|
|
4769
|
+
platforms: tester.platforms,
|
|
4770
|
+
tier: tester.tier,
|
|
4771
|
+
},
|
|
4772
|
+
totalAssignments: all.length,
|
|
4773
|
+
counts,
|
|
4774
|
+
activeLoad: counts.pending + counts.in_progress,
|
|
4775
|
+
recentAssignments: all.slice(0, 10).map((a) => ({
|
|
4776
|
+
id: a.id,
|
|
4777
|
+
status: a.status,
|
|
4778
|
+
assignedAt: a.assigned_at,
|
|
4779
|
+
completedAt: a.completed_at,
|
|
4780
|
+
testCase: a.test_case ? {
|
|
4781
|
+
testKey: a.test_case.test_key,
|
|
4782
|
+
title: a.test_case.title,
|
|
4783
|
+
priority: a.test_case.priority,
|
|
4784
|
+
} : null,
|
|
4785
|
+
testRun: a.test_run?.name || null,
|
|
4786
|
+
})),
|
|
4787
|
+
};
|
|
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
|
+
}
|
|
5218
|
+
// Main server setup
|
|
5219
|
+
async function main() {
|
|
5220
|
+
initSupabase();
|
|
5221
|
+
const server = new index_js_1.Server({
|
|
5222
|
+
name: 'bugbear-mcp',
|
|
5223
|
+
version: '0.1.0',
|
|
5224
|
+
}, {
|
|
5225
|
+
capabilities: {
|
|
5226
|
+
tools: {},
|
|
5227
|
+
resources: {},
|
|
5228
|
+
prompts: {},
|
|
5229
|
+
},
|
|
5230
|
+
});
|
|
5231
|
+
// Handle tool listing
|
|
5232
|
+
server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({
|
|
5233
|
+
tools,
|
|
4055
5234
|
}));
|
|
4056
5235
|
// Handle tool execution
|
|
4057
5236
|
server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
4058
5237
|
const { name, arguments: args } = request.params;
|
|
4059
5238
|
try {
|
|
4060
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
|
+
}
|
|
4061
5245
|
switch (name) {
|
|
4062
5246
|
case 'list_reports':
|
|
4063
5247
|
result = await listReports(args);
|
|
@@ -4171,6 +5355,57 @@ async function main() {
|
|
|
4171
5355
|
case 'get_testing_patterns':
|
|
4172
5356
|
result = await getTestingPatterns(args);
|
|
4173
5357
|
break;
|
|
5358
|
+
// === TESTER & ASSIGNMENT MANAGEMENT ===
|
|
5359
|
+
case 'list_testers':
|
|
5360
|
+
result = await listTesters(args);
|
|
5361
|
+
break;
|
|
5362
|
+
case 'list_test_runs':
|
|
5363
|
+
result = await listTestRuns(args);
|
|
5364
|
+
break;
|
|
5365
|
+
case 'create_test_run':
|
|
5366
|
+
result = await createTestRun(args);
|
|
5367
|
+
break;
|
|
5368
|
+
case 'list_test_assignments':
|
|
5369
|
+
result = await listTestAssignments(args);
|
|
5370
|
+
break;
|
|
5371
|
+
case 'assign_tests':
|
|
5372
|
+
result = await assignTests(args);
|
|
5373
|
+
break;
|
|
5374
|
+
case 'get_tester_workload':
|
|
5375
|
+
result = await getTesterWorkload(args);
|
|
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;
|
|
4174
5409
|
default:
|
|
4175
5410
|
return {
|
|
4176
5411
|
content: [{ type: 'text', text: `Unknown tool: ${name}` }],
|
|
@@ -4190,13 +5425,17 @@ async function main() {
|
|
|
4190
5425
|
});
|
|
4191
5426
|
// Handle resource listing (reports as resources)
|
|
4192
5427
|
server.setRequestHandler(types_js_1.ListResourcesRequestSchema, async () => {
|
|
4193
|
-
const { data } = await supabase
|
|
5428
|
+
const { data, error } = await supabase
|
|
4194
5429
|
.from('reports')
|
|
4195
5430
|
.select('id, description, report_type, severity')
|
|
4196
|
-
.eq('project_id',
|
|
5431
|
+
.eq('project_id', currentProjectId)
|
|
4197
5432
|
.eq('status', 'new')
|
|
4198
5433
|
.order('created_at', { ascending: false })
|
|
4199
5434
|
.limit(10);
|
|
5435
|
+
if (error) {
|
|
5436
|
+
console.error('Failed to list resources:', error.message);
|
|
5437
|
+
return { resources: [] };
|
|
5438
|
+
}
|
|
4200
5439
|
return {
|
|
4201
5440
|
resources: (data || []).map(r => ({
|
|
4202
5441
|
uri: `bugbear://reports/${r.id}`,
|