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