@bbearai/mcp-server 0.5.0 → 0.5.1
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 +560 -25
- package/package.json +1 -1
- package/src/index.ts +632 -25
package/dist/index.js
CHANGED
|
@@ -435,6 +435,10 @@ const tools = [
|
|
|
435
435
|
items: { type: 'string' },
|
|
436
436
|
description: 'List of files that were modified to fix this bug',
|
|
437
437
|
},
|
|
438
|
+
notify_tester: {
|
|
439
|
+
type: 'boolean',
|
|
440
|
+
description: 'If true, notify the original tester about the fix with a message and verification task. Default: false (silent resolve).',
|
|
441
|
+
},
|
|
438
442
|
},
|
|
439
443
|
required: ['report_id', 'commit_sha'],
|
|
440
444
|
},
|
|
@@ -902,12 +906,131 @@ const tools = [
|
|
|
902
906
|
},
|
|
903
907
|
},
|
|
904
908
|
},
|
|
909
|
+
// === TESTER & ASSIGNMENT MANAGEMENT TOOLS ===
|
|
910
|
+
{
|
|
911
|
+
name: 'list_testers',
|
|
912
|
+
description: 'List all QA testers for the project with their status, platforms, and workload counts.',
|
|
913
|
+
inputSchema: {
|
|
914
|
+
type: 'object',
|
|
915
|
+
properties: {
|
|
916
|
+
status: {
|
|
917
|
+
type: 'string',
|
|
918
|
+
enum: ['active', 'inactive', 'invited'],
|
|
919
|
+
description: 'Filter by tester status (default: all)',
|
|
920
|
+
},
|
|
921
|
+
platform: {
|
|
922
|
+
type: 'string',
|
|
923
|
+
enum: ['ios', 'android', 'web'],
|
|
924
|
+
description: 'Filter by platform support',
|
|
925
|
+
},
|
|
926
|
+
},
|
|
927
|
+
},
|
|
928
|
+
},
|
|
929
|
+
{
|
|
930
|
+
name: 'list_test_runs',
|
|
931
|
+
description: 'List testing campaigns (test runs) for the project with pass/fail stats.',
|
|
932
|
+
inputSchema: {
|
|
933
|
+
type: 'object',
|
|
934
|
+
properties: {
|
|
935
|
+
status: {
|
|
936
|
+
type: 'string',
|
|
937
|
+
enum: ['draft', 'active', 'paused', 'completed', 'archived'],
|
|
938
|
+
description: 'Filter by test run status',
|
|
939
|
+
},
|
|
940
|
+
limit: {
|
|
941
|
+
type: 'number',
|
|
942
|
+
description: 'Maximum number of runs to return (default: 20)',
|
|
943
|
+
},
|
|
944
|
+
},
|
|
945
|
+
},
|
|
946
|
+
},
|
|
947
|
+
{
|
|
948
|
+
name: 'create_test_run',
|
|
949
|
+
description: 'Create a new testing campaign (test run). Tests can then be assigned to testers within this run.',
|
|
950
|
+
inputSchema: {
|
|
951
|
+
type: 'object',
|
|
952
|
+
properties: {
|
|
953
|
+
name: {
|
|
954
|
+
type: 'string',
|
|
955
|
+
description: 'Name for the test run (e.g. "v2.1 QA Pass", "Sprint 5 Testing")',
|
|
956
|
+
},
|
|
957
|
+
description: {
|
|
958
|
+
type: 'string',
|
|
959
|
+
description: 'Optional description of the test run scope and goals',
|
|
960
|
+
},
|
|
961
|
+
},
|
|
962
|
+
required: ['name'],
|
|
963
|
+
},
|
|
964
|
+
},
|
|
965
|
+
{
|
|
966
|
+
name: 'list_test_assignments',
|
|
967
|
+
description: 'List test assignments showing which tests are assigned to which testers, with status tracking. Joins test case and tester info.',
|
|
968
|
+
inputSchema: {
|
|
969
|
+
type: 'object',
|
|
970
|
+
properties: {
|
|
971
|
+
tester_id: {
|
|
972
|
+
type: 'string',
|
|
973
|
+
description: 'Filter by tester UUID',
|
|
974
|
+
},
|
|
975
|
+
test_run_id: {
|
|
976
|
+
type: 'string',
|
|
977
|
+
description: 'Filter by test run UUID',
|
|
978
|
+
},
|
|
979
|
+
status: {
|
|
980
|
+
type: 'string',
|
|
981
|
+
enum: ['pending', 'in_progress', 'passed', 'failed', 'blocked', 'skipped'],
|
|
982
|
+
description: 'Filter by assignment status',
|
|
983
|
+
},
|
|
984
|
+
limit: {
|
|
985
|
+
type: 'number',
|
|
986
|
+
description: 'Maximum number of assignments to return (default: 50, max: 200)',
|
|
987
|
+
},
|
|
988
|
+
},
|
|
989
|
+
},
|
|
990
|
+
},
|
|
991
|
+
{
|
|
992
|
+
name: 'assign_tests',
|
|
993
|
+
description: 'Assign one or more test cases to a tester. Optionally assign within a test run. Skips duplicates gracefully.',
|
|
994
|
+
inputSchema: {
|
|
995
|
+
type: 'object',
|
|
996
|
+
properties: {
|
|
997
|
+
tester_id: {
|
|
998
|
+
type: 'string',
|
|
999
|
+
description: 'UUID of the tester to assign tests to (required)',
|
|
1000
|
+
},
|
|
1001
|
+
test_case_ids: {
|
|
1002
|
+
type: 'array',
|
|
1003
|
+
items: { type: 'string' },
|
|
1004
|
+
description: 'Array of test case UUIDs to assign (required)',
|
|
1005
|
+
},
|
|
1006
|
+
test_run_id: {
|
|
1007
|
+
type: 'string',
|
|
1008
|
+
description: 'Optional test run UUID to group assignments under',
|
|
1009
|
+
},
|
|
1010
|
+
},
|
|
1011
|
+
required: ['tester_id', 'test_case_ids'],
|
|
1012
|
+
},
|
|
1013
|
+
},
|
|
1014
|
+
{
|
|
1015
|
+
name: 'get_tester_workload',
|
|
1016
|
+
description: 'View a specific tester\'s current workload — assignment counts by status and recent assignments.',
|
|
1017
|
+
inputSchema: {
|
|
1018
|
+
type: 'object',
|
|
1019
|
+
properties: {
|
|
1020
|
+
tester_id: {
|
|
1021
|
+
type: 'string',
|
|
1022
|
+
description: 'UUID of the tester (required)',
|
|
1023
|
+
},
|
|
1024
|
+
},
|
|
1025
|
+
required: ['tester_id'],
|
|
1026
|
+
},
|
|
1027
|
+
},
|
|
905
1028
|
];
|
|
906
1029
|
// Tool handlers
|
|
907
1030
|
async function listReports(args) {
|
|
908
1031
|
let query = supabase
|
|
909
1032
|
.from('reports')
|
|
910
|
-
.select('id, report_type, severity, status, description, app_context, created_at, reporter_name,
|
|
1033
|
+
.select('id, report_type, severity, status, description, app_context, created_at, reporter_name, tester:testers(name)')
|
|
911
1034
|
.eq('project_id', PROJECT_ID)
|
|
912
1035
|
.order('created_at', { ascending: false })
|
|
913
1036
|
.limit(Math.min(args.limit || 10, 50));
|
|
@@ -963,10 +1086,8 @@ async function getReport(args) {
|
|
|
963
1086
|
created_at: data.created_at,
|
|
964
1087
|
reporter: data.tester ? {
|
|
965
1088
|
name: data.tester.name,
|
|
966
|
-
email: data.tester.email,
|
|
967
1089
|
} : (data.reporter_name ? {
|
|
968
1090
|
name: data.reporter_name,
|
|
969
|
-
email: data.reporter_email,
|
|
970
1091
|
} : null),
|
|
971
1092
|
track: data.track ? {
|
|
972
1093
|
name: data.track.name,
|
|
@@ -1126,13 +1247,16 @@ async function createTestCase(args) {
|
|
|
1126
1247
|
// Find track ID if track name provided
|
|
1127
1248
|
let trackId = null;
|
|
1128
1249
|
if (args.track) {
|
|
1129
|
-
const
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1250
|
+
const sanitizedTrack = sanitizeSearchQuery(args.track);
|
|
1251
|
+
if (sanitizedTrack) {
|
|
1252
|
+
const { data: trackData } = await supabase
|
|
1253
|
+
.from('qa_tracks')
|
|
1254
|
+
.select('id')
|
|
1255
|
+
.eq('project_id', PROJECT_ID)
|
|
1256
|
+
.ilike('name', `%${sanitizedTrack}%`)
|
|
1257
|
+
.single();
|
|
1258
|
+
trackId = trackData?.id || null;
|
|
1259
|
+
}
|
|
1136
1260
|
}
|
|
1137
1261
|
const testCase = {
|
|
1138
1262
|
project_id: PROJECT_ID,
|
|
@@ -1511,7 +1635,11 @@ async function getTestPriorities(args) {
|
|
|
1511
1635
|
const minScore = args.min_score || 0;
|
|
1512
1636
|
const includeFactors = args.include_factors !== false;
|
|
1513
1637
|
// First, refresh the route stats
|
|
1514
|
-
await supabase.rpc('refresh_route_test_stats', { p_project_id: PROJECT_ID });
|
|
1638
|
+
const { error: refreshError } = await supabase.rpc('refresh_route_test_stats', { p_project_id: PROJECT_ID });
|
|
1639
|
+
if (refreshError) {
|
|
1640
|
+
// Non-fatal: proceed with potentially stale data but warn
|
|
1641
|
+
console.warn('Failed to refresh route stats:', refreshError.message);
|
|
1642
|
+
}
|
|
1515
1643
|
// Get prioritized routes
|
|
1516
1644
|
const { data: routes, error } = await supabase
|
|
1517
1645
|
.from('route_test_stats')
|
|
@@ -1890,7 +2018,9 @@ async function getCoverageMatrix(args) {
|
|
|
1890
2018
|
.from('test_assignments')
|
|
1891
2019
|
.select('test_case_id, status, completed_at')
|
|
1892
2020
|
.eq('project_id', PROJECT_ID)
|
|
1893
|
-
.in('status', ['passed', 'failed'])
|
|
2021
|
+
.in('status', ['passed', 'failed'])
|
|
2022
|
+
.order('completed_at', { ascending: false })
|
|
2023
|
+
.limit(2000);
|
|
1894
2024
|
assignments = data || [];
|
|
1895
2025
|
}
|
|
1896
2026
|
// Get route stats for bug counts
|
|
@@ -2038,7 +2168,11 @@ async function getStaleCoverage(args) {
|
|
|
2038
2168
|
const daysThreshold = args.days_threshold || 14;
|
|
2039
2169
|
const limit = args.limit || 20;
|
|
2040
2170
|
// Refresh stats first
|
|
2041
|
-
await supabase.rpc('refresh_route_test_stats', { p_project_id: PROJECT_ID });
|
|
2171
|
+
const { error: refreshError } = await supabase.rpc('refresh_route_test_stats', { p_project_id: PROJECT_ID });
|
|
2172
|
+
if (refreshError) {
|
|
2173
|
+
// Non-fatal: proceed with potentially stale data but warn
|
|
2174
|
+
console.warn('Failed to refresh route stats:', refreshError.message);
|
|
2175
|
+
}
|
|
2042
2176
|
// Get routes ordered by staleness and risk
|
|
2043
2177
|
const { data: routes, error } = await supabase
|
|
2044
2178
|
.from('route_test_stats')
|
|
@@ -2123,12 +2257,29 @@ async function generateDeployChecklist(args) {
|
|
|
2123
2257
|
}
|
|
2124
2258
|
});
|
|
2125
2259
|
}
|
|
2126
|
-
//
|
|
2127
|
-
const
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2260
|
+
// Limit routes array to prevent query explosion
|
|
2261
|
+
const safeRoutes = routes.slice(0, 100);
|
|
2262
|
+
// Get test cases for these routes (use separate queries to avoid filter injection)
|
|
2263
|
+
const [{ data: byRoute }, { data: byCategory }] = await Promise.all([
|
|
2264
|
+
supabase
|
|
2265
|
+
.from('test_cases')
|
|
2266
|
+
.select('id, test_key, title, target_route, category, priority, track:qa_tracks(name)')
|
|
2267
|
+
.eq('project_id', PROJECT_ID)
|
|
2268
|
+
.in('target_route', safeRoutes),
|
|
2269
|
+
supabase
|
|
2270
|
+
.from('test_cases')
|
|
2271
|
+
.select('id, test_key, title, target_route, category, priority, track:qa_tracks(name)')
|
|
2272
|
+
.eq('project_id', PROJECT_ID)
|
|
2273
|
+
.in('category', safeRoutes),
|
|
2274
|
+
]);
|
|
2275
|
+
// Deduplicate by id
|
|
2276
|
+
const seenIds = new Set();
|
|
2277
|
+
const testCases = [...(byRoute || []), ...(byCategory || [])].filter(tc => {
|
|
2278
|
+
if (seenIds.has(tc.id))
|
|
2279
|
+
return false;
|
|
2280
|
+
seenIds.add(tc.id);
|
|
2281
|
+
return true;
|
|
2282
|
+
});
|
|
2132
2283
|
// Get route stats for risk assessment
|
|
2133
2284
|
const { data: routeStats } = await supabase
|
|
2134
2285
|
.from('route_test_stats')
|
|
@@ -2631,10 +2782,19 @@ async function analyzeCommitForTesting(args) {
|
|
|
2631
2782
|
const affectedRoutes = [];
|
|
2632
2783
|
for (const mapping of mappings || []) {
|
|
2633
2784
|
const matchedFiles = filesChanged.filter(file => {
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
.
|
|
2637
|
-
|
|
2785
|
+
try {
|
|
2786
|
+
// Validate pattern complexity to prevent ReDoS
|
|
2787
|
+
if (mapping.file_pattern.length > 200)
|
|
2788
|
+
return false;
|
|
2789
|
+
const pattern = mapping.file_pattern
|
|
2790
|
+
.replace(/[.+?^${}()|[\]\\]/g, '\\$&') // Escape regex special chars first
|
|
2791
|
+
.replace(/\\\*\\\*/g, '.*') // Then convert glob ** to .*
|
|
2792
|
+
.replace(/\\\*/g, '[^/]*'); // And glob * to [^/]*
|
|
2793
|
+
return new RegExp(`^${pattern}$`).test(file);
|
|
2794
|
+
}
|
|
2795
|
+
catch {
|
|
2796
|
+
return false; // Skip malformed patterns
|
|
2797
|
+
}
|
|
2638
2798
|
});
|
|
2639
2799
|
if (matchedFiles.length > 0) {
|
|
2640
2800
|
affectedRoutes.push({
|
|
@@ -3292,6 +3452,7 @@ async function markFixedWithCommit(args) {
|
|
|
3292
3452
|
status: 'resolved',
|
|
3293
3453
|
resolved_at: new Date().toISOString(),
|
|
3294
3454
|
resolution_notes: args.resolution_notes || `Fixed in commit ${args.commit_sha.slice(0, 7)}`,
|
|
3455
|
+
notify_tester: args.notify_tester === true, // Opt-in: only notify if explicitly requested
|
|
3295
3456
|
code_context: {
|
|
3296
3457
|
...existingContext,
|
|
3297
3458
|
fix: {
|
|
@@ -3311,11 +3472,15 @@ async function markFixedWithCommit(args) {
|
|
|
3311
3472
|
if (error) {
|
|
3312
3473
|
return { error: error.message };
|
|
3313
3474
|
}
|
|
3475
|
+
const notificationStatus = args.notify_tester
|
|
3476
|
+
? 'The original tester will be notified and assigned a verification task.'
|
|
3477
|
+
: 'No notification sent (silent resolve). A verification task was created.';
|
|
3314
3478
|
return {
|
|
3315
3479
|
success: true,
|
|
3316
|
-
message: `Bug marked as fixed in commit ${args.commit_sha.slice(0, 7)}`,
|
|
3480
|
+
message: `Bug marked as fixed in commit ${args.commit_sha.slice(0, 7)}. ${notificationStatus}`,
|
|
3317
3481
|
report_id: args.report_id,
|
|
3318
3482
|
commit: args.commit_sha,
|
|
3483
|
+
tester_notified: args.notify_tester === true,
|
|
3319
3484
|
next_steps: [
|
|
3320
3485
|
'Consider running create_regression_test to prevent this bug from recurring',
|
|
3321
3486
|
'Push your changes to trigger CI/CD',
|
|
@@ -4036,6 +4201,353 @@ Which files or areas would you like me to analyze?`;
|
|
|
4036
4201
|
return 'Unknown prompt';
|
|
4037
4202
|
}
|
|
4038
4203
|
}
|
|
4204
|
+
// === TESTER & ASSIGNMENT MANAGEMENT HANDLERS ===
|
|
4205
|
+
async function listTesters(args) {
|
|
4206
|
+
let query = supabase
|
|
4207
|
+
.from('testers')
|
|
4208
|
+
.select('id, name, email, status, platforms, tier, assigned_count, completed_count, notes, created_at')
|
|
4209
|
+
.eq('project_id', PROJECT_ID)
|
|
4210
|
+
.order('name', { ascending: true });
|
|
4211
|
+
if (args.status) {
|
|
4212
|
+
query = query.eq('status', args.status);
|
|
4213
|
+
}
|
|
4214
|
+
const { data, error } = await query;
|
|
4215
|
+
if (error) {
|
|
4216
|
+
return { error: error.message };
|
|
4217
|
+
}
|
|
4218
|
+
let testers = data || [];
|
|
4219
|
+
// Filter by platform if specified (platforms is an array column)
|
|
4220
|
+
if (args.platform) {
|
|
4221
|
+
testers = testers.filter((t) => t.platforms && t.platforms.includes(args.platform));
|
|
4222
|
+
}
|
|
4223
|
+
return {
|
|
4224
|
+
count: testers.length,
|
|
4225
|
+
testers: testers.map((t) => ({
|
|
4226
|
+
id: t.id,
|
|
4227
|
+
name: t.name,
|
|
4228
|
+
email: t.email,
|
|
4229
|
+
status: t.status,
|
|
4230
|
+
platforms: t.platforms,
|
|
4231
|
+
tier: t.tier,
|
|
4232
|
+
assignedCount: t.assigned_count,
|
|
4233
|
+
completedCount: t.completed_count,
|
|
4234
|
+
notes: t.notes,
|
|
4235
|
+
})),
|
|
4236
|
+
};
|
|
4237
|
+
}
|
|
4238
|
+
async function listTestRuns(args) {
|
|
4239
|
+
const limit = Math.min(args.limit || 20, 50);
|
|
4240
|
+
let query = supabase
|
|
4241
|
+
.from('test_runs')
|
|
4242
|
+
.select('id, name, description, status, total_tests, passed_tests, failed_tests, started_at, completed_at, created_at')
|
|
4243
|
+
.eq('project_id', PROJECT_ID)
|
|
4244
|
+
.order('created_at', { ascending: false })
|
|
4245
|
+
.limit(limit);
|
|
4246
|
+
if (args.status) {
|
|
4247
|
+
query = query.eq('status', args.status);
|
|
4248
|
+
}
|
|
4249
|
+
const { data, error } = await query;
|
|
4250
|
+
if (error) {
|
|
4251
|
+
return { error: error.message };
|
|
4252
|
+
}
|
|
4253
|
+
return {
|
|
4254
|
+
count: (data || []).length,
|
|
4255
|
+
testRuns: (data || []).map((r) => ({
|
|
4256
|
+
id: r.id,
|
|
4257
|
+
name: r.name,
|
|
4258
|
+
description: r.description,
|
|
4259
|
+
status: r.status,
|
|
4260
|
+
totalTests: r.total_tests,
|
|
4261
|
+
passedTests: r.passed_tests,
|
|
4262
|
+
failedTests: r.failed_tests,
|
|
4263
|
+
passRate: r.total_tests > 0 ? Math.round((r.passed_tests / r.total_tests) * 100) : 0,
|
|
4264
|
+
startedAt: r.started_at,
|
|
4265
|
+
completedAt: r.completed_at,
|
|
4266
|
+
createdAt: r.created_at,
|
|
4267
|
+
})),
|
|
4268
|
+
};
|
|
4269
|
+
}
|
|
4270
|
+
async function createTestRun(args) {
|
|
4271
|
+
if (!args.name || args.name.trim().length === 0) {
|
|
4272
|
+
return { error: 'Test run name is required' };
|
|
4273
|
+
}
|
|
4274
|
+
const { data, error } = await supabase
|
|
4275
|
+
.from('test_runs')
|
|
4276
|
+
.insert({
|
|
4277
|
+
project_id: PROJECT_ID,
|
|
4278
|
+
name: args.name.trim(),
|
|
4279
|
+
description: args.description?.trim() || null,
|
|
4280
|
+
status: 'draft',
|
|
4281
|
+
})
|
|
4282
|
+
.select('id, name, description, status, created_at')
|
|
4283
|
+
.single();
|
|
4284
|
+
if (error) {
|
|
4285
|
+
return { error: error.message };
|
|
4286
|
+
}
|
|
4287
|
+
return {
|
|
4288
|
+
success: true,
|
|
4289
|
+
testRun: {
|
|
4290
|
+
id: data.id,
|
|
4291
|
+
name: data.name,
|
|
4292
|
+
description: data.description,
|
|
4293
|
+
status: data.status,
|
|
4294
|
+
createdAt: data.created_at,
|
|
4295
|
+
},
|
|
4296
|
+
message: `Test run "${data.name}" created. Use assign_tests to add test assignments.`,
|
|
4297
|
+
};
|
|
4298
|
+
}
|
|
4299
|
+
async function listTestAssignments(args) {
|
|
4300
|
+
const limit = Math.min(args.limit || 50, 200);
|
|
4301
|
+
if (args.tester_id && !isValidUUID(args.tester_id)) {
|
|
4302
|
+
return { error: 'Invalid tester_id format' };
|
|
4303
|
+
}
|
|
4304
|
+
if (args.test_run_id && !isValidUUID(args.test_run_id)) {
|
|
4305
|
+
return { error: 'Invalid test_run_id format' };
|
|
4306
|
+
}
|
|
4307
|
+
let query = supabase
|
|
4308
|
+
.from('test_assignments')
|
|
4309
|
+
.select(`
|
|
4310
|
+
id,
|
|
4311
|
+
status,
|
|
4312
|
+
assigned_at,
|
|
4313
|
+
started_at,
|
|
4314
|
+
completed_at,
|
|
4315
|
+
duration_seconds,
|
|
4316
|
+
is_verification,
|
|
4317
|
+
notes,
|
|
4318
|
+
test_case:test_cases(id, test_key, title, priority, target_route),
|
|
4319
|
+
tester:testers(id, name, email),
|
|
4320
|
+
test_run:test_runs(id, name)
|
|
4321
|
+
`)
|
|
4322
|
+
.eq('project_id', PROJECT_ID)
|
|
4323
|
+
.order('assigned_at', { ascending: false })
|
|
4324
|
+
.limit(limit);
|
|
4325
|
+
if (args.tester_id) {
|
|
4326
|
+
query = query.eq('tester_id', args.tester_id);
|
|
4327
|
+
}
|
|
4328
|
+
if (args.test_run_id) {
|
|
4329
|
+
query = query.eq('test_run_id', args.test_run_id);
|
|
4330
|
+
}
|
|
4331
|
+
if (args.status) {
|
|
4332
|
+
query = query.eq('status', args.status);
|
|
4333
|
+
}
|
|
4334
|
+
const { data, error } = await query;
|
|
4335
|
+
if (error) {
|
|
4336
|
+
return { error: error.message };
|
|
4337
|
+
}
|
|
4338
|
+
return {
|
|
4339
|
+
count: (data || []).length,
|
|
4340
|
+
assignments: (data || []).map((a) => ({
|
|
4341
|
+
id: a.id,
|
|
4342
|
+
status: a.status,
|
|
4343
|
+
assignedAt: a.assigned_at,
|
|
4344
|
+
startedAt: a.started_at,
|
|
4345
|
+
completedAt: a.completed_at,
|
|
4346
|
+
durationSeconds: a.duration_seconds,
|
|
4347
|
+
isVerification: a.is_verification,
|
|
4348
|
+
notes: a.notes,
|
|
4349
|
+
testCase: a.test_case ? {
|
|
4350
|
+
id: a.test_case.id,
|
|
4351
|
+
testKey: a.test_case.test_key,
|
|
4352
|
+
title: a.test_case.title,
|
|
4353
|
+
priority: a.test_case.priority,
|
|
4354
|
+
targetRoute: a.test_case.target_route,
|
|
4355
|
+
} : null,
|
|
4356
|
+
tester: a.tester ? {
|
|
4357
|
+
id: a.tester.id,
|
|
4358
|
+
name: a.tester.name,
|
|
4359
|
+
email: a.tester.email,
|
|
4360
|
+
} : null,
|
|
4361
|
+
testRun: a.test_run ? {
|
|
4362
|
+
id: a.test_run.id,
|
|
4363
|
+
name: a.test_run.name,
|
|
4364
|
+
} : null,
|
|
4365
|
+
})),
|
|
4366
|
+
};
|
|
4367
|
+
}
|
|
4368
|
+
async function assignTests(args) {
|
|
4369
|
+
// Validate inputs
|
|
4370
|
+
if (!isValidUUID(args.tester_id)) {
|
|
4371
|
+
return { error: 'Invalid tester_id format' };
|
|
4372
|
+
}
|
|
4373
|
+
if (!args.test_case_ids || args.test_case_ids.length === 0) {
|
|
4374
|
+
return { error: 'At least one test_case_id is required' };
|
|
4375
|
+
}
|
|
4376
|
+
if (args.test_case_ids.length > 50) {
|
|
4377
|
+
return { error: 'Maximum 50 test cases per assignment batch' };
|
|
4378
|
+
}
|
|
4379
|
+
for (const id of args.test_case_ids) {
|
|
4380
|
+
if (!isValidUUID(id)) {
|
|
4381
|
+
return { error: `Invalid test_case_id format: ${id}` };
|
|
4382
|
+
}
|
|
4383
|
+
}
|
|
4384
|
+
if (args.test_run_id && !isValidUUID(args.test_run_id)) {
|
|
4385
|
+
return { error: 'Invalid test_run_id format' };
|
|
4386
|
+
}
|
|
4387
|
+
// Verify tester exists and is active
|
|
4388
|
+
const { data: tester, error: testerErr } = await supabase
|
|
4389
|
+
.from('testers')
|
|
4390
|
+
.select('id, name, email, status')
|
|
4391
|
+
.eq('id', args.tester_id)
|
|
4392
|
+
.eq('project_id', PROJECT_ID)
|
|
4393
|
+
.single();
|
|
4394
|
+
if (testerErr || !tester) {
|
|
4395
|
+
return { error: 'Tester not found in this project' };
|
|
4396
|
+
}
|
|
4397
|
+
if (tester.status !== 'active') {
|
|
4398
|
+
return { error: `Tester "${tester.name}" is ${tester.status}, not active` };
|
|
4399
|
+
}
|
|
4400
|
+
// Verify test cases exist for this project
|
|
4401
|
+
const { data: testCases, error: tcErr } = await supabase
|
|
4402
|
+
.from('test_cases')
|
|
4403
|
+
.select('id, test_key, title')
|
|
4404
|
+
.eq('project_id', PROJECT_ID)
|
|
4405
|
+
.in('id', args.test_case_ids);
|
|
4406
|
+
if (tcErr) {
|
|
4407
|
+
return { error: tcErr.message };
|
|
4408
|
+
}
|
|
4409
|
+
const foundIds = new Set((testCases || []).map((tc) => tc.id));
|
|
4410
|
+
const missingIds = args.test_case_ids.filter(id => !foundIds.has(id));
|
|
4411
|
+
if (missingIds.length > 0) {
|
|
4412
|
+
return {
|
|
4413
|
+
error: `Test cases not found in this project: ${missingIds.join(', ')}`,
|
|
4414
|
+
};
|
|
4415
|
+
}
|
|
4416
|
+
// Verify test run exists if provided
|
|
4417
|
+
if (args.test_run_id) {
|
|
4418
|
+
const { data: run, error: runErr } = await supabase
|
|
4419
|
+
.from('test_runs')
|
|
4420
|
+
.select('id')
|
|
4421
|
+
.eq('id', args.test_run_id)
|
|
4422
|
+
.eq('project_id', PROJECT_ID)
|
|
4423
|
+
.single();
|
|
4424
|
+
if (runErr || !run) {
|
|
4425
|
+
return { error: 'Test run not found in this project' };
|
|
4426
|
+
}
|
|
4427
|
+
}
|
|
4428
|
+
// Build assignment rows
|
|
4429
|
+
const rows = args.test_case_ids.map(tcId => ({
|
|
4430
|
+
project_id: PROJECT_ID,
|
|
4431
|
+
test_case_id: tcId,
|
|
4432
|
+
tester_id: args.tester_id,
|
|
4433
|
+
test_run_id: args.test_run_id || null,
|
|
4434
|
+
status: 'pending',
|
|
4435
|
+
}));
|
|
4436
|
+
// Insert — use upsert-like approach: insert and handle conflicts
|
|
4437
|
+
const { data: inserted, error: insertErr } = await supabase
|
|
4438
|
+
.from('test_assignments')
|
|
4439
|
+
.insert(rows)
|
|
4440
|
+
.select('id, test_case_id');
|
|
4441
|
+
if (insertErr) {
|
|
4442
|
+
// Check if it's a unique constraint violation
|
|
4443
|
+
if (insertErr.message.includes('duplicate') || insertErr.message.includes('unique')) {
|
|
4444
|
+
// Try inserting one by one to find duplicates
|
|
4445
|
+
const created = [];
|
|
4446
|
+
const skipped = [];
|
|
4447
|
+
for (const row of rows) {
|
|
4448
|
+
const { data: single, error: singleErr } = await supabase
|
|
4449
|
+
.from('test_assignments')
|
|
4450
|
+
.insert(row)
|
|
4451
|
+
.select('id, test_case_id')
|
|
4452
|
+
.single();
|
|
4453
|
+
if (singleErr) {
|
|
4454
|
+
const tc = testCases?.find((t) => t.id === row.test_case_id);
|
|
4455
|
+
skipped.push(tc?.test_key || row.test_case_id);
|
|
4456
|
+
}
|
|
4457
|
+
else if (single) {
|
|
4458
|
+
created.push(single);
|
|
4459
|
+
}
|
|
4460
|
+
}
|
|
4461
|
+
return {
|
|
4462
|
+
success: true,
|
|
4463
|
+
created: created.length,
|
|
4464
|
+
skipped: skipped.length,
|
|
4465
|
+
skippedTests: skipped,
|
|
4466
|
+
tester: { id: tester.id, name: tester.name },
|
|
4467
|
+
message: `Assigned ${created.length} test(s) to ${tester.name}. ${skipped.length} skipped (already assigned).`,
|
|
4468
|
+
};
|
|
4469
|
+
}
|
|
4470
|
+
return { error: insertErr.message };
|
|
4471
|
+
}
|
|
4472
|
+
return {
|
|
4473
|
+
success: true,
|
|
4474
|
+
created: (inserted || []).length,
|
|
4475
|
+
skipped: 0,
|
|
4476
|
+
tester: { id: tester.id, name: tester.name },
|
|
4477
|
+
testRun: args.test_run_id || null,
|
|
4478
|
+
message: `Assigned ${(inserted || []).length} test(s) to ${tester.name}.`,
|
|
4479
|
+
};
|
|
4480
|
+
}
|
|
4481
|
+
async function getTesterWorkload(args) {
|
|
4482
|
+
if (!isValidUUID(args.tester_id)) {
|
|
4483
|
+
return { error: 'Invalid tester_id format' };
|
|
4484
|
+
}
|
|
4485
|
+
// Get tester info
|
|
4486
|
+
const { data: tester, error: testerErr } = await supabase
|
|
4487
|
+
.from('testers')
|
|
4488
|
+
.select('id, name, email, status, platforms, tier')
|
|
4489
|
+
.eq('id', args.tester_id)
|
|
4490
|
+
.eq('project_id', PROJECT_ID)
|
|
4491
|
+
.single();
|
|
4492
|
+
if (testerErr || !tester) {
|
|
4493
|
+
return { error: 'Tester not found in this project' };
|
|
4494
|
+
}
|
|
4495
|
+
// Get all assignments for this tester in this project
|
|
4496
|
+
const { data: assignments, error: assignErr } = await supabase
|
|
4497
|
+
.from('test_assignments')
|
|
4498
|
+
.select(`
|
|
4499
|
+
id,
|
|
4500
|
+
status,
|
|
4501
|
+
assigned_at,
|
|
4502
|
+
completed_at,
|
|
4503
|
+
test_case:test_cases(test_key, title, priority),
|
|
4504
|
+
test_run:test_runs(name)
|
|
4505
|
+
`)
|
|
4506
|
+
.eq('project_id', PROJECT_ID)
|
|
4507
|
+
.eq('tester_id', args.tester_id)
|
|
4508
|
+
.order('assigned_at', { ascending: false });
|
|
4509
|
+
if (assignErr) {
|
|
4510
|
+
return { error: assignErr.message };
|
|
4511
|
+
}
|
|
4512
|
+
const all = assignments || [];
|
|
4513
|
+
// Count by status
|
|
4514
|
+
const counts = {
|
|
4515
|
+
pending: 0,
|
|
4516
|
+
in_progress: 0,
|
|
4517
|
+
passed: 0,
|
|
4518
|
+
failed: 0,
|
|
4519
|
+
blocked: 0,
|
|
4520
|
+
skipped: 0,
|
|
4521
|
+
};
|
|
4522
|
+
for (const a of all) {
|
|
4523
|
+
counts[a.status] = (counts[a.status] || 0) + 1;
|
|
4524
|
+
}
|
|
4525
|
+
return {
|
|
4526
|
+
tester: {
|
|
4527
|
+
id: tester.id,
|
|
4528
|
+
name: tester.name,
|
|
4529
|
+
email: tester.email,
|
|
4530
|
+
status: tester.status,
|
|
4531
|
+
platforms: tester.platforms,
|
|
4532
|
+
tier: tester.tier,
|
|
4533
|
+
},
|
|
4534
|
+
totalAssignments: all.length,
|
|
4535
|
+
counts,
|
|
4536
|
+
activeLoad: counts.pending + counts.in_progress,
|
|
4537
|
+
recentAssignments: all.slice(0, 10).map((a) => ({
|
|
4538
|
+
id: a.id,
|
|
4539
|
+
status: a.status,
|
|
4540
|
+
assignedAt: a.assigned_at,
|
|
4541
|
+
completedAt: a.completed_at,
|
|
4542
|
+
testCase: a.test_case ? {
|
|
4543
|
+
testKey: a.test_case.test_key,
|
|
4544
|
+
title: a.test_case.title,
|
|
4545
|
+
priority: a.test_case.priority,
|
|
4546
|
+
} : null,
|
|
4547
|
+
testRun: a.test_run?.name || null,
|
|
4548
|
+
})),
|
|
4549
|
+
};
|
|
4550
|
+
}
|
|
4039
4551
|
// Main server setup
|
|
4040
4552
|
async function main() {
|
|
4041
4553
|
initSupabase();
|
|
@@ -4171,6 +4683,25 @@ async function main() {
|
|
|
4171
4683
|
case 'get_testing_patterns':
|
|
4172
4684
|
result = await getTestingPatterns(args);
|
|
4173
4685
|
break;
|
|
4686
|
+
// === TESTER & ASSIGNMENT MANAGEMENT ===
|
|
4687
|
+
case 'list_testers':
|
|
4688
|
+
result = await listTesters(args);
|
|
4689
|
+
break;
|
|
4690
|
+
case 'list_test_runs':
|
|
4691
|
+
result = await listTestRuns(args);
|
|
4692
|
+
break;
|
|
4693
|
+
case 'create_test_run':
|
|
4694
|
+
result = await createTestRun(args);
|
|
4695
|
+
break;
|
|
4696
|
+
case 'list_test_assignments':
|
|
4697
|
+
result = await listTestAssignments(args);
|
|
4698
|
+
break;
|
|
4699
|
+
case 'assign_tests':
|
|
4700
|
+
result = await assignTests(args);
|
|
4701
|
+
break;
|
|
4702
|
+
case 'get_tester_workload':
|
|
4703
|
+
result = await getTesterWorkload(args);
|
|
4704
|
+
break;
|
|
4174
4705
|
default:
|
|
4175
4706
|
return {
|
|
4176
4707
|
content: [{ type: 'text', text: `Unknown tool: ${name}` }],
|
|
@@ -4190,13 +4721,17 @@ async function main() {
|
|
|
4190
4721
|
});
|
|
4191
4722
|
// Handle resource listing (reports as resources)
|
|
4192
4723
|
server.setRequestHandler(types_js_1.ListResourcesRequestSchema, async () => {
|
|
4193
|
-
const { data } = await supabase
|
|
4724
|
+
const { data, error } = await supabase
|
|
4194
4725
|
.from('reports')
|
|
4195
4726
|
.select('id, description, report_type, severity')
|
|
4196
4727
|
.eq('project_id', PROJECT_ID)
|
|
4197
4728
|
.eq('status', 'new')
|
|
4198
4729
|
.order('created_at', { ascending: false })
|
|
4199
4730
|
.limit(10);
|
|
4731
|
+
if (error) {
|
|
4732
|
+
console.error('Failed to list resources:', error.message);
|
|
4733
|
+
return { resources: [] };
|
|
4734
|
+
}
|
|
4200
4735
|
return {
|
|
4201
4736
|
resources: (data || []).map(r => ({
|
|
4202
4737
|
uri: `bugbear://reports/${r.id}`,
|