@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/src/index.ts
CHANGED
|
@@ -452,6 +452,10 @@ const tools = [
|
|
|
452
452
|
items: { type: 'string' },
|
|
453
453
|
description: 'List of files that were modified to fix this bug',
|
|
454
454
|
},
|
|
455
|
+
notify_tester: {
|
|
456
|
+
type: 'boolean',
|
|
457
|
+
description: 'If true, notify the original tester about the fix with a message and verification task. Default: false (silent resolve).',
|
|
458
|
+
},
|
|
455
459
|
},
|
|
456
460
|
required: ['report_id', 'commit_sha'],
|
|
457
461
|
},
|
|
@@ -919,6 +923,125 @@ const tools = [
|
|
|
919
923
|
},
|
|
920
924
|
},
|
|
921
925
|
},
|
|
926
|
+
// === TESTER & ASSIGNMENT MANAGEMENT TOOLS ===
|
|
927
|
+
{
|
|
928
|
+
name: 'list_testers',
|
|
929
|
+
description: 'List all QA testers for the project with their status, platforms, and workload counts.',
|
|
930
|
+
inputSchema: {
|
|
931
|
+
type: 'object' as const,
|
|
932
|
+
properties: {
|
|
933
|
+
status: {
|
|
934
|
+
type: 'string',
|
|
935
|
+
enum: ['active', 'inactive', 'invited'],
|
|
936
|
+
description: 'Filter by tester status (default: all)',
|
|
937
|
+
},
|
|
938
|
+
platform: {
|
|
939
|
+
type: 'string',
|
|
940
|
+
enum: ['ios', 'android', 'web'],
|
|
941
|
+
description: 'Filter by platform support',
|
|
942
|
+
},
|
|
943
|
+
},
|
|
944
|
+
},
|
|
945
|
+
},
|
|
946
|
+
{
|
|
947
|
+
name: 'list_test_runs',
|
|
948
|
+
description: 'List testing campaigns (test runs) for the project with pass/fail stats.',
|
|
949
|
+
inputSchema: {
|
|
950
|
+
type: 'object' as const,
|
|
951
|
+
properties: {
|
|
952
|
+
status: {
|
|
953
|
+
type: 'string',
|
|
954
|
+
enum: ['draft', 'active', 'paused', 'completed', 'archived'],
|
|
955
|
+
description: 'Filter by test run status',
|
|
956
|
+
},
|
|
957
|
+
limit: {
|
|
958
|
+
type: 'number',
|
|
959
|
+
description: 'Maximum number of runs to return (default: 20)',
|
|
960
|
+
},
|
|
961
|
+
},
|
|
962
|
+
},
|
|
963
|
+
},
|
|
964
|
+
{
|
|
965
|
+
name: 'create_test_run',
|
|
966
|
+
description: 'Create a new testing campaign (test run). Tests can then be assigned to testers within this run.',
|
|
967
|
+
inputSchema: {
|
|
968
|
+
type: 'object' as const,
|
|
969
|
+
properties: {
|
|
970
|
+
name: {
|
|
971
|
+
type: 'string',
|
|
972
|
+
description: 'Name for the test run (e.g. "v2.1 QA Pass", "Sprint 5 Testing")',
|
|
973
|
+
},
|
|
974
|
+
description: {
|
|
975
|
+
type: 'string',
|
|
976
|
+
description: 'Optional description of the test run scope and goals',
|
|
977
|
+
},
|
|
978
|
+
},
|
|
979
|
+
required: ['name'],
|
|
980
|
+
},
|
|
981
|
+
},
|
|
982
|
+
{
|
|
983
|
+
name: 'list_test_assignments',
|
|
984
|
+
description: 'List test assignments showing which tests are assigned to which testers, with status tracking. Joins test case and tester info.',
|
|
985
|
+
inputSchema: {
|
|
986
|
+
type: 'object' as const,
|
|
987
|
+
properties: {
|
|
988
|
+
tester_id: {
|
|
989
|
+
type: 'string',
|
|
990
|
+
description: 'Filter by tester UUID',
|
|
991
|
+
},
|
|
992
|
+
test_run_id: {
|
|
993
|
+
type: 'string',
|
|
994
|
+
description: 'Filter by test run UUID',
|
|
995
|
+
},
|
|
996
|
+
status: {
|
|
997
|
+
type: 'string',
|
|
998
|
+
enum: ['pending', 'in_progress', 'passed', 'failed', 'blocked', 'skipped'],
|
|
999
|
+
description: 'Filter by assignment status',
|
|
1000
|
+
},
|
|
1001
|
+
limit: {
|
|
1002
|
+
type: 'number',
|
|
1003
|
+
description: 'Maximum number of assignments to return (default: 50, max: 200)',
|
|
1004
|
+
},
|
|
1005
|
+
},
|
|
1006
|
+
},
|
|
1007
|
+
},
|
|
1008
|
+
{
|
|
1009
|
+
name: 'assign_tests',
|
|
1010
|
+
description: 'Assign one or more test cases to a tester. Optionally assign within a test run. Skips duplicates gracefully.',
|
|
1011
|
+
inputSchema: {
|
|
1012
|
+
type: 'object' as const,
|
|
1013
|
+
properties: {
|
|
1014
|
+
tester_id: {
|
|
1015
|
+
type: 'string',
|
|
1016
|
+
description: 'UUID of the tester to assign tests to (required)',
|
|
1017
|
+
},
|
|
1018
|
+
test_case_ids: {
|
|
1019
|
+
type: 'array',
|
|
1020
|
+
items: { type: 'string' },
|
|
1021
|
+
description: 'Array of test case UUIDs to assign (required)',
|
|
1022
|
+
},
|
|
1023
|
+
test_run_id: {
|
|
1024
|
+
type: 'string',
|
|
1025
|
+
description: 'Optional test run UUID to group assignments under',
|
|
1026
|
+
},
|
|
1027
|
+
},
|
|
1028
|
+
required: ['tester_id', 'test_case_ids'],
|
|
1029
|
+
},
|
|
1030
|
+
},
|
|
1031
|
+
{
|
|
1032
|
+
name: 'get_tester_workload',
|
|
1033
|
+
description: 'View a specific tester\'s current workload — assignment counts by status and recent assignments.',
|
|
1034
|
+
inputSchema: {
|
|
1035
|
+
type: 'object' as const,
|
|
1036
|
+
properties: {
|
|
1037
|
+
tester_id: {
|
|
1038
|
+
type: 'string',
|
|
1039
|
+
description: 'UUID of the tester (required)',
|
|
1040
|
+
},
|
|
1041
|
+
},
|
|
1042
|
+
required: ['tester_id'],
|
|
1043
|
+
},
|
|
1044
|
+
},
|
|
922
1045
|
];
|
|
923
1046
|
|
|
924
1047
|
// Tool handlers
|
|
@@ -930,7 +1053,7 @@ async function listReports(args: {
|
|
|
930
1053
|
}) {
|
|
931
1054
|
let query = supabase
|
|
932
1055
|
.from('reports')
|
|
933
|
-
.select('id, report_type, severity, status, description, app_context, created_at, reporter_name,
|
|
1056
|
+
.select('id, report_type, severity, status, description, app_context, created_at, reporter_name, tester:testers(name)')
|
|
934
1057
|
.eq('project_id', PROJECT_ID)
|
|
935
1058
|
.order('created_at', { ascending: false })
|
|
936
1059
|
.limit(Math.min(args.limit || 10, 50));
|
|
@@ -991,10 +1114,8 @@ async function getReport(args: { report_id: string }) {
|
|
|
991
1114
|
created_at: data.created_at,
|
|
992
1115
|
reporter: data.tester ? {
|
|
993
1116
|
name: data.tester.name,
|
|
994
|
-
email: data.tester.email,
|
|
995
1117
|
} : (data.reporter_name ? {
|
|
996
1118
|
name: data.reporter_name,
|
|
997
|
-
email: data.reporter_email,
|
|
998
1119
|
} : null),
|
|
999
1120
|
track: data.track ? {
|
|
1000
1121
|
name: data.track.name,
|
|
@@ -1194,13 +1315,16 @@ async function createTestCase(args: {
|
|
|
1194
1315
|
// Find track ID if track name provided
|
|
1195
1316
|
let trackId: string | null = null;
|
|
1196
1317
|
if (args.track) {
|
|
1197
|
-
const
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1318
|
+
const sanitizedTrack = sanitizeSearchQuery(args.track);
|
|
1319
|
+
if (sanitizedTrack) {
|
|
1320
|
+
const { data: trackData } = await supabase
|
|
1321
|
+
.from('qa_tracks')
|
|
1322
|
+
.select('id')
|
|
1323
|
+
.eq('project_id', PROJECT_ID)
|
|
1324
|
+
.ilike('name', `%${sanitizedTrack}%`)
|
|
1325
|
+
.single();
|
|
1326
|
+
trackId = trackData?.id || null;
|
|
1327
|
+
}
|
|
1204
1328
|
}
|
|
1205
1329
|
|
|
1206
1330
|
const testCase = {
|
|
@@ -1674,7 +1798,11 @@ async function getTestPriorities(args: {
|
|
|
1674
1798
|
const includeFactors = args.include_factors !== false;
|
|
1675
1799
|
|
|
1676
1800
|
// First, refresh the route stats
|
|
1677
|
-
await supabase.rpc('refresh_route_test_stats', { p_project_id: PROJECT_ID });
|
|
1801
|
+
const { error: refreshError } = await supabase.rpc('refresh_route_test_stats', { p_project_id: PROJECT_ID });
|
|
1802
|
+
if (refreshError) {
|
|
1803
|
+
// Non-fatal: proceed with potentially stale data but warn
|
|
1804
|
+
console.warn('Failed to refresh route stats:', refreshError.message);
|
|
1805
|
+
}
|
|
1678
1806
|
|
|
1679
1807
|
// Get prioritized routes
|
|
1680
1808
|
const { data: routes, error } = await supabase
|
|
@@ -2114,7 +2242,9 @@ async function getCoverageMatrix(args: {
|
|
|
2114
2242
|
.from('test_assignments')
|
|
2115
2243
|
.select('test_case_id, status, completed_at')
|
|
2116
2244
|
.eq('project_id', PROJECT_ID)
|
|
2117
|
-
.in('status', ['passed', 'failed'])
|
|
2245
|
+
.in('status', ['passed', 'failed'])
|
|
2246
|
+
.order('completed_at', { ascending: false })
|
|
2247
|
+
.limit(2000);
|
|
2118
2248
|
assignments = data || [];
|
|
2119
2249
|
}
|
|
2120
2250
|
|
|
@@ -2286,7 +2416,11 @@ async function getStaleCoverage(args: {
|
|
|
2286
2416
|
const limit = args.limit || 20;
|
|
2287
2417
|
|
|
2288
2418
|
// Refresh stats first
|
|
2289
|
-
await supabase.rpc('refresh_route_test_stats', { p_project_id: PROJECT_ID });
|
|
2419
|
+
const { error: refreshError } = await supabase.rpc('refresh_route_test_stats', { p_project_id: PROJECT_ID });
|
|
2420
|
+
if (refreshError) {
|
|
2421
|
+
// Non-fatal: proceed with potentially stale data but warn
|
|
2422
|
+
console.warn('Failed to refresh route stats:', refreshError.message);
|
|
2423
|
+
}
|
|
2290
2424
|
|
|
2291
2425
|
// Get routes ordered by staleness and risk
|
|
2292
2426
|
const { data: routes, error } = await supabase
|
|
@@ -2382,12 +2516,30 @@ async function generateDeployChecklist(args: {
|
|
|
2382
2516
|
});
|
|
2383
2517
|
}
|
|
2384
2518
|
|
|
2385
|
-
//
|
|
2386
|
-
const
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2519
|
+
// Limit routes array to prevent query explosion
|
|
2520
|
+
const safeRoutes = routes.slice(0, 100);
|
|
2521
|
+
|
|
2522
|
+
// Get test cases for these routes (use separate queries to avoid filter injection)
|
|
2523
|
+
const [{ data: byRoute }, { data: byCategory }] = await Promise.all([
|
|
2524
|
+
supabase
|
|
2525
|
+
.from('test_cases')
|
|
2526
|
+
.select('id, test_key, title, target_route, category, priority, track:qa_tracks(name)')
|
|
2527
|
+
.eq('project_id', PROJECT_ID)
|
|
2528
|
+
.in('target_route', safeRoutes),
|
|
2529
|
+
supabase
|
|
2530
|
+
.from('test_cases')
|
|
2531
|
+
.select('id, test_key, title, target_route, category, priority, track:qa_tracks(name)')
|
|
2532
|
+
.eq('project_id', PROJECT_ID)
|
|
2533
|
+
.in('category', safeRoutes),
|
|
2534
|
+
]);
|
|
2535
|
+
|
|
2536
|
+
// Deduplicate by id
|
|
2537
|
+
const seenIds = new Set<string>();
|
|
2538
|
+
const testCases = [...(byRoute || []), ...(byCategory || [])].filter(tc => {
|
|
2539
|
+
if (seenIds.has(tc.id)) return false;
|
|
2540
|
+
seenIds.add(tc.id);
|
|
2541
|
+
return true;
|
|
2542
|
+
});
|
|
2391
2543
|
|
|
2392
2544
|
// Get route stats for risk assessment
|
|
2393
2545
|
const { data: routeStats } = await supabase
|
|
@@ -2988,10 +3140,17 @@ async function analyzeCommitForTesting(args: {
|
|
|
2988
3140
|
|
|
2989
3141
|
for (const mapping of mappings || []) {
|
|
2990
3142
|
const matchedFiles = filesChanged.filter(file => {
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
.
|
|
2994
|
-
|
|
3143
|
+
try {
|
|
3144
|
+
// Validate pattern complexity to prevent ReDoS
|
|
3145
|
+
if (mapping.file_pattern.length > 200) return false;
|
|
3146
|
+
const pattern = mapping.file_pattern
|
|
3147
|
+
.replace(/[.+?^${}()|[\]\\]/g, '\\$&') // Escape regex special chars first
|
|
3148
|
+
.replace(/\\\*\\\*/g, '.*') // Then convert glob ** to .*
|
|
3149
|
+
.replace(/\\\*/g, '[^/]*'); // And glob * to [^/]*
|
|
3150
|
+
return new RegExp(`^${pattern}$`).test(file);
|
|
3151
|
+
} catch {
|
|
3152
|
+
return false; // Skip malformed patterns
|
|
3153
|
+
}
|
|
2995
3154
|
});
|
|
2996
3155
|
|
|
2997
3156
|
if (matchedFiles.length > 0) {
|
|
@@ -3736,6 +3895,7 @@ async function markFixedWithCommit(args: {
|
|
|
3736
3895
|
commit_message?: string;
|
|
3737
3896
|
resolution_notes?: string;
|
|
3738
3897
|
files_changed?: string[];
|
|
3898
|
+
notify_tester?: boolean;
|
|
3739
3899
|
}) {
|
|
3740
3900
|
if (!isValidUUID(args.report_id)) {
|
|
3741
3901
|
return { error: 'Invalid report_id format' };
|
|
@@ -3759,6 +3919,7 @@ async function markFixedWithCommit(args: {
|
|
|
3759
3919
|
status: 'resolved',
|
|
3760
3920
|
resolved_at: new Date().toISOString(),
|
|
3761
3921
|
resolution_notes: args.resolution_notes || `Fixed in commit ${args.commit_sha.slice(0, 7)}`,
|
|
3922
|
+
notify_tester: args.notify_tester === true, // Opt-in: only notify if explicitly requested
|
|
3762
3923
|
code_context: {
|
|
3763
3924
|
...existingContext,
|
|
3764
3925
|
fix: {
|
|
@@ -3781,11 +3942,16 @@ async function markFixedWithCommit(args: {
|
|
|
3781
3942
|
return { error: error.message };
|
|
3782
3943
|
}
|
|
3783
3944
|
|
|
3945
|
+
const notificationStatus = args.notify_tester
|
|
3946
|
+
? 'The original tester will be notified and assigned a verification task.'
|
|
3947
|
+
: 'No notification sent (silent resolve). A verification task was created.';
|
|
3948
|
+
|
|
3784
3949
|
return {
|
|
3785
3950
|
success: true,
|
|
3786
|
-
message: `Bug marked as fixed in commit ${args.commit_sha.slice(0, 7)}`,
|
|
3951
|
+
message: `Bug marked as fixed in commit ${args.commit_sha.slice(0, 7)}. ${notificationStatus}`,
|
|
3787
3952
|
report_id: args.report_id,
|
|
3788
3953
|
commit: args.commit_sha,
|
|
3954
|
+
tester_notified: args.notify_tester === true,
|
|
3789
3955
|
next_steps: [
|
|
3790
3956
|
'Consider running create_regression_test to prevent this bug from recurring',
|
|
3791
3957
|
'Push your changes to trigger CI/CD',
|
|
@@ -4620,6 +4786,423 @@ Which files or areas would you like me to analyze?`;
|
|
|
4620
4786
|
}
|
|
4621
4787
|
}
|
|
4622
4788
|
|
|
4789
|
+
// === TESTER & ASSIGNMENT MANAGEMENT HANDLERS ===
|
|
4790
|
+
|
|
4791
|
+
async function listTesters(args: {
|
|
4792
|
+
status?: string;
|
|
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 });
|
|
4800
|
+
|
|
4801
|
+
if (args.status) {
|
|
4802
|
+
query = query.eq('status', args.status);
|
|
4803
|
+
}
|
|
4804
|
+
|
|
4805
|
+
const { data, error } = await query;
|
|
4806
|
+
|
|
4807
|
+
if (error) {
|
|
4808
|
+
return { error: error.message };
|
|
4809
|
+
}
|
|
4810
|
+
|
|
4811
|
+
let testers = data || [];
|
|
4812
|
+
|
|
4813
|
+
// Filter by platform if specified (platforms is an array column)
|
|
4814
|
+
if (args.platform) {
|
|
4815
|
+
testers = testers.filter((t: any) =>
|
|
4816
|
+
t.platforms && t.platforms.includes(args.platform)
|
|
4817
|
+
);
|
|
4818
|
+
}
|
|
4819
|
+
|
|
4820
|
+
return {
|
|
4821
|
+
count: testers.length,
|
|
4822
|
+
testers: testers.map((t: any) => ({
|
|
4823
|
+
id: t.id,
|
|
4824
|
+
name: t.name,
|
|
4825
|
+
email: t.email,
|
|
4826
|
+
status: t.status,
|
|
4827
|
+
platforms: t.platforms,
|
|
4828
|
+
tier: t.tier,
|
|
4829
|
+
assignedCount: t.assigned_count,
|
|
4830
|
+
completedCount: t.completed_count,
|
|
4831
|
+
notes: t.notes,
|
|
4832
|
+
})),
|
|
4833
|
+
};
|
|
4834
|
+
}
|
|
4835
|
+
|
|
4836
|
+
async function listTestRuns(args: {
|
|
4837
|
+
status?: string;
|
|
4838
|
+
limit?: number;
|
|
4839
|
+
}) {
|
|
4840
|
+
const limit = Math.min(args.limit || 20, 50);
|
|
4841
|
+
|
|
4842
|
+
let query = supabase
|
|
4843
|
+
.from('test_runs')
|
|
4844
|
+
.select('id, name, description, status, total_tests, passed_tests, failed_tests, started_at, completed_at, created_at')
|
|
4845
|
+
.eq('project_id', PROJECT_ID)
|
|
4846
|
+
.order('created_at', { ascending: false })
|
|
4847
|
+
.limit(limit);
|
|
4848
|
+
|
|
4849
|
+
if (args.status) {
|
|
4850
|
+
query = query.eq('status', args.status);
|
|
4851
|
+
}
|
|
4852
|
+
|
|
4853
|
+
const { data, error } = await query;
|
|
4854
|
+
|
|
4855
|
+
if (error) {
|
|
4856
|
+
return { error: error.message };
|
|
4857
|
+
}
|
|
4858
|
+
|
|
4859
|
+
return {
|
|
4860
|
+
count: (data || []).length,
|
|
4861
|
+
testRuns: (data || []).map((r: any) => ({
|
|
4862
|
+
id: r.id,
|
|
4863
|
+
name: r.name,
|
|
4864
|
+
description: r.description,
|
|
4865
|
+
status: r.status,
|
|
4866
|
+
totalTests: r.total_tests,
|
|
4867
|
+
passedTests: r.passed_tests,
|
|
4868
|
+
failedTests: r.failed_tests,
|
|
4869
|
+
passRate: r.total_tests > 0 ? Math.round((r.passed_tests / r.total_tests) * 100) : 0,
|
|
4870
|
+
startedAt: r.started_at,
|
|
4871
|
+
completedAt: r.completed_at,
|
|
4872
|
+
createdAt: r.created_at,
|
|
4873
|
+
})),
|
|
4874
|
+
};
|
|
4875
|
+
}
|
|
4876
|
+
|
|
4877
|
+
async function createTestRun(args: {
|
|
4878
|
+
name: string;
|
|
4879
|
+
description?: string;
|
|
4880
|
+
}) {
|
|
4881
|
+
if (!args.name || args.name.trim().length === 0) {
|
|
4882
|
+
return { error: 'Test run name is required' };
|
|
4883
|
+
}
|
|
4884
|
+
|
|
4885
|
+
const { data, error } = await supabase
|
|
4886
|
+
.from('test_runs')
|
|
4887
|
+
.insert({
|
|
4888
|
+
project_id: PROJECT_ID,
|
|
4889
|
+
name: args.name.trim(),
|
|
4890
|
+
description: args.description?.trim() || null,
|
|
4891
|
+
status: 'draft',
|
|
4892
|
+
})
|
|
4893
|
+
.select('id, name, description, status, created_at')
|
|
4894
|
+
.single();
|
|
4895
|
+
|
|
4896
|
+
if (error) {
|
|
4897
|
+
return { error: error.message };
|
|
4898
|
+
}
|
|
4899
|
+
|
|
4900
|
+
return {
|
|
4901
|
+
success: true,
|
|
4902
|
+
testRun: {
|
|
4903
|
+
id: data.id,
|
|
4904
|
+
name: data.name,
|
|
4905
|
+
description: data.description,
|
|
4906
|
+
status: data.status,
|
|
4907
|
+
createdAt: data.created_at,
|
|
4908
|
+
},
|
|
4909
|
+
message: `Test run "${data.name}" created. Use assign_tests to add test assignments.`,
|
|
4910
|
+
};
|
|
4911
|
+
}
|
|
4912
|
+
|
|
4913
|
+
async function listTestAssignments(args: {
|
|
4914
|
+
tester_id?: string;
|
|
4915
|
+
test_run_id?: string;
|
|
4916
|
+
status?: string;
|
|
4917
|
+
limit?: number;
|
|
4918
|
+
}) {
|
|
4919
|
+
const limit = Math.min(args.limit || 50, 200);
|
|
4920
|
+
|
|
4921
|
+
if (args.tester_id && !isValidUUID(args.tester_id)) {
|
|
4922
|
+
return { error: 'Invalid tester_id format' };
|
|
4923
|
+
}
|
|
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
|
+
|
|
4947
|
+
if (args.tester_id) {
|
|
4948
|
+
query = query.eq('tester_id', args.tester_id);
|
|
4949
|
+
}
|
|
4950
|
+
if (args.test_run_id) {
|
|
4951
|
+
query = query.eq('test_run_id', args.test_run_id);
|
|
4952
|
+
}
|
|
4953
|
+
if (args.status) {
|
|
4954
|
+
query = query.eq('status', args.status);
|
|
4955
|
+
}
|
|
4956
|
+
|
|
4957
|
+
const { data, error } = await query;
|
|
4958
|
+
|
|
4959
|
+
if (error) {
|
|
4960
|
+
return { error: error.message };
|
|
4961
|
+
}
|
|
4962
|
+
|
|
4963
|
+
return {
|
|
4964
|
+
count: (data || []).length,
|
|
4965
|
+
assignments: (data || []).map((a: any) => ({
|
|
4966
|
+
id: a.id,
|
|
4967
|
+
status: a.status,
|
|
4968
|
+
assignedAt: a.assigned_at,
|
|
4969
|
+
startedAt: a.started_at,
|
|
4970
|
+
completedAt: a.completed_at,
|
|
4971
|
+
durationSeconds: a.duration_seconds,
|
|
4972
|
+
isVerification: a.is_verification,
|
|
4973
|
+
notes: a.notes,
|
|
4974
|
+
testCase: a.test_case ? {
|
|
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
|
+
})),
|
|
4991
|
+
};
|
|
4992
|
+
}
|
|
4993
|
+
|
|
4994
|
+
async function assignTests(args: {
|
|
4995
|
+
tester_id: string;
|
|
4996
|
+
test_case_ids: string[];
|
|
4997
|
+
test_run_id?: string;
|
|
4998
|
+
}) {
|
|
4999
|
+
// Validate inputs
|
|
5000
|
+
if (!isValidUUID(args.tester_id)) {
|
|
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' };
|
|
5005
|
+
}
|
|
5006
|
+
if (args.test_case_ids.length > 50) {
|
|
5007
|
+
return { error: 'Maximum 50 test cases per assignment batch' };
|
|
5008
|
+
}
|
|
5009
|
+
for (const id of args.test_case_ids) {
|
|
5010
|
+
if (!isValidUUID(id)) {
|
|
5011
|
+
return { error: `Invalid test_case_id format: ${id}` };
|
|
5012
|
+
}
|
|
5013
|
+
}
|
|
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
|
+
|
|
5026
|
+
if (testerErr || !tester) {
|
|
5027
|
+
return { error: 'Tester not found in this project' };
|
|
5028
|
+
}
|
|
5029
|
+
if (tester.status !== 'active') {
|
|
5030
|
+
return { error: `Tester "${tester.name}" is ${tester.status}, not active` };
|
|
5031
|
+
}
|
|
5032
|
+
|
|
5033
|
+
// Verify test cases exist for this project
|
|
5034
|
+
const { data: testCases, error: tcErr } = await supabase
|
|
5035
|
+
.from('test_cases')
|
|
5036
|
+
.select('id, test_key, title')
|
|
5037
|
+
.eq('project_id', PROJECT_ID)
|
|
5038
|
+
.in('id', args.test_case_ids);
|
|
5039
|
+
|
|
5040
|
+
if (tcErr) {
|
|
5041
|
+
return { error: tcErr.message };
|
|
5042
|
+
}
|
|
5043
|
+
|
|
5044
|
+
const foundIds = new Set((testCases || []).map((tc: any) => tc.id));
|
|
5045
|
+
const missingIds = args.test_case_ids.filter(id => !foundIds.has(id));
|
|
5046
|
+
|
|
5047
|
+
if (missingIds.length > 0) {
|
|
5048
|
+
return {
|
|
5049
|
+
error: `Test cases not found in this project: ${missingIds.join(', ')}`,
|
|
5050
|
+
};
|
|
5051
|
+
}
|
|
5052
|
+
|
|
5053
|
+
// Verify test run exists if provided
|
|
5054
|
+
if (args.test_run_id) {
|
|
5055
|
+
const { data: run, error: runErr } = await supabase
|
|
5056
|
+
.from('test_runs')
|
|
5057
|
+
.select('id')
|
|
5058
|
+
.eq('id', args.test_run_id)
|
|
5059
|
+
.eq('project_id', PROJECT_ID)
|
|
5060
|
+
.single();
|
|
5061
|
+
|
|
5062
|
+
if (runErr || !run) {
|
|
5063
|
+
return { error: 'Test run not found in this project' };
|
|
5064
|
+
}
|
|
5065
|
+
}
|
|
5066
|
+
|
|
5067
|
+
// Build assignment rows
|
|
5068
|
+
const rows = args.test_case_ids.map(tcId => ({
|
|
5069
|
+
project_id: PROJECT_ID,
|
|
5070
|
+
test_case_id: tcId,
|
|
5071
|
+
tester_id: args.tester_id,
|
|
5072
|
+
test_run_id: args.test_run_id || null,
|
|
5073
|
+
status: 'pending',
|
|
5074
|
+
}));
|
|
5075
|
+
|
|
5076
|
+
// Insert — use upsert-like approach: insert and handle conflicts
|
|
5077
|
+
const { data: inserted, error: insertErr } = await supabase
|
|
5078
|
+
.from('test_assignments')
|
|
5079
|
+
.insert(rows)
|
|
5080
|
+
.select('id, test_case_id');
|
|
5081
|
+
|
|
5082
|
+
if (insertErr) {
|
|
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[] = [];
|
|
5088
|
+
|
|
5089
|
+
for (const row of rows) {
|
|
5090
|
+
const { data: single, error: singleErr } = await supabase
|
|
5091
|
+
.from('test_assignments')
|
|
5092
|
+
.insert(row)
|
|
5093
|
+
.select('id, test_case_id')
|
|
5094
|
+
.single();
|
|
5095
|
+
|
|
5096
|
+
if (singleErr) {
|
|
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
|
+
}
|
|
5103
|
+
|
|
5104
|
+
return {
|
|
5105
|
+
success: true,
|
|
5106
|
+
created: created.length,
|
|
5107
|
+
skipped: skipped.length,
|
|
5108
|
+
skippedTests: skipped,
|
|
5109
|
+
tester: { id: tester.id, name: tester.name },
|
|
5110
|
+
message: `Assigned ${created.length} test(s) to ${tester.name}. ${skipped.length} skipped (already assigned).`,
|
|
5111
|
+
};
|
|
5112
|
+
}
|
|
5113
|
+
return { error: insertErr.message };
|
|
5114
|
+
}
|
|
5115
|
+
|
|
5116
|
+
return {
|
|
5117
|
+
success: true,
|
|
5118
|
+
created: (inserted || []).length,
|
|
5119
|
+
skipped: 0,
|
|
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}.`,
|
|
5123
|
+
};
|
|
5124
|
+
}
|
|
5125
|
+
|
|
5126
|
+
async function getTesterWorkload(args: {
|
|
5127
|
+
tester_id: string;
|
|
5128
|
+
}) {
|
|
5129
|
+
if (!isValidUUID(args.tester_id)) {
|
|
5130
|
+
return { error: 'Invalid tester_id format' };
|
|
5131
|
+
}
|
|
5132
|
+
|
|
5133
|
+
// Get tester info
|
|
5134
|
+
const { data: tester, error: testerErr } = await supabase
|
|
5135
|
+
.from('testers')
|
|
5136
|
+
.select('id, name, email, status, platforms, tier')
|
|
5137
|
+
.eq('id', args.tester_id)
|
|
5138
|
+
.eq('project_id', PROJECT_ID)
|
|
5139
|
+
.single();
|
|
5140
|
+
|
|
5141
|
+
if (testerErr || !tester) {
|
|
5142
|
+
return { error: 'Tester not found in this project' };
|
|
5143
|
+
}
|
|
5144
|
+
|
|
5145
|
+
// Get all assignments for this tester in this project
|
|
5146
|
+
const { data: assignments, error: assignErr } = await supabase
|
|
5147
|
+
.from('test_assignments')
|
|
5148
|
+
.select(`
|
|
5149
|
+
id,
|
|
5150
|
+
status,
|
|
5151
|
+
assigned_at,
|
|
5152
|
+
completed_at,
|
|
5153
|
+
test_case:test_cases(test_key, title, priority),
|
|
5154
|
+
test_run:test_runs(name)
|
|
5155
|
+
`)
|
|
5156
|
+
.eq('project_id', PROJECT_ID)
|
|
5157
|
+
.eq('tester_id', args.tester_id)
|
|
5158
|
+
.order('assigned_at', { ascending: false });
|
|
5159
|
+
|
|
5160
|
+
if (assignErr) {
|
|
5161
|
+
return { error: assignErr.message };
|
|
5162
|
+
}
|
|
5163
|
+
|
|
5164
|
+
const all = assignments || [];
|
|
5165
|
+
|
|
5166
|
+
// Count by status
|
|
5167
|
+
const counts: Record<string, number> = {
|
|
5168
|
+
pending: 0,
|
|
5169
|
+
in_progress: 0,
|
|
5170
|
+
passed: 0,
|
|
5171
|
+
failed: 0,
|
|
5172
|
+
blocked: 0,
|
|
5173
|
+
skipped: 0,
|
|
5174
|
+
};
|
|
5175
|
+
for (const a of all) {
|
|
5176
|
+
counts[a.status] = (counts[a.status] || 0) + 1;
|
|
5177
|
+
}
|
|
5178
|
+
|
|
5179
|
+
return {
|
|
5180
|
+
tester: {
|
|
5181
|
+
id: tester.id,
|
|
5182
|
+
name: tester.name,
|
|
5183
|
+
email: tester.email,
|
|
5184
|
+
status: tester.status,
|
|
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) => ({
|
|
5192
|
+
id: a.id,
|
|
5193
|
+
status: a.status,
|
|
5194
|
+
assignedAt: a.assigned_at,
|
|
5195
|
+
completedAt: a.completed_at,
|
|
5196
|
+
testCase: a.test_case ? {
|
|
5197
|
+
testKey: a.test_case.test_key,
|
|
5198
|
+
title: a.test_case.title,
|
|
5199
|
+
priority: a.test_case.priority,
|
|
5200
|
+
} : null,
|
|
5201
|
+
testRun: a.test_run?.name || null,
|
|
5202
|
+
})),
|
|
5203
|
+
};
|
|
5204
|
+
}
|
|
5205
|
+
|
|
4623
5206
|
// Main server setup
|
|
4624
5207
|
async function main() {
|
|
4625
5208
|
initSupabase();
|
|
@@ -4763,6 +5346,25 @@ async function main() {
|
|
|
4763
5346
|
case 'get_testing_patterns':
|
|
4764
5347
|
result = await getTestingPatterns(args as any);
|
|
4765
5348
|
break;
|
|
5349
|
+
// === TESTER & ASSIGNMENT MANAGEMENT ===
|
|
5350
|
+
case 'list_testers':
|
|
5351
|
+
result = await listTesters(args as any);
|
|
5352
|
+
break;
|
|
5353
|
+
case 'list_test_runs':
|
|
5354
|
+
result = await listTestRuns(args as any);
|
|
5355
|
+
break;
|
|
5356
|
+
case 'create_test_run':
|
|
5357
|
+
result = await createTestRun(args as any);
|
|
5358
|
+
break;
|
|
5359
|
+
case 'list_test_assignments':
|
|
5360
|
+
result = await listTestAssignments(args as any);
|
|
5361
|
+
break;
|
|
5362
|
+
case 'assign_tests':
|
|
5363
|
+
result = await assignTests(args as any);
|
|
5364
|
+
break;
|
|
5365
|
+
case 'get_tester_workload':
|
|
5366
|
+
result = await getTesterWorkload(args as any);
|
|
5367
|
+
break;
|
|
4766
5368
|
default:
|
|
4767
5369
|
return {
|
|
4768
5370
|
content: [{ type: 'text', text: `Unknown tool: ${name}` }],
|
|
@@ -4783,7 +5385,7 @@ async function main() {
|
|
|
4783
5385
|
|
|
4784
5386
|
// Handle resource listing (reports as resources)
|
|
4785
5387
|
server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
4786
|
-
const { data } = await supabase
|
|
5388
|
+
const { data, error } = await supabase
|
|
4787
5389
|
.from('reports')
|
|
4788
5390
|
.select('id, description, report_type, severity')
|
|
4789
5391
|
.eq('project_id', PROJECT_ID)
|
|
@@ -4791,6 +5393,11 @@ async function main() {
|
|
|
4791
5393
|
.order('created_at', { ascending: false })
|
|
4792
5394
|
.limit(10);
|
|
4793
5395
|
|
|
5396
|
+
if (error) {
|
|
5397
|
+
console.error('Failed to list resources:', error.message);
|
|
5398
|
+
return { resources: [] };
|
|
5399
|
+
}
|
|
5400
|
+
|
|
4794
5401
|
return {
|
|
4795
5402
|
resources: (data || []).map(r => ({
|
|
4796
5403
|
uri: `bugbear://reports/${r.id}`,
|