@bbearai/mcp-server 0.3.0 → 0.3.3
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 +543 -0
- package/package.json +1 -1
- package/src/index.ts +627 -4
package/dist/index.js
CHANGED
|
@@ -722,6 +722,159 @@ const tools = [
|
|
|
722
722
|
},
|
|
723
723
|
},
|
|
724
724
|
},
|
|
725
|
+
// === SPRINT 5: QA INTELLIGENCE EXPANSION ===
|
|
726
|
+
{
|
|
727
|
+
name: 'get_qa_sessions',
|
|
728
|
+
description: 'Get QA session history showing exploratory testing sessions. Sessions capture findings that may or may not become formal bugs. Use this to understand tester exploration patterns and unstructured discoveries.',
|
|
729
|
+
inputSchema: {
|
|
730
|
+
type: 'object',
|
|
731
|
+
properties: {
|
|
732
|
+
status: {
|
|
733
|
+
type: 'string',
|
|
734
|
+
enum: ['active', 'completed', 'all'],
|
|
735
|
+
description: 'Filter by session status (default: all)',
|
|
736
|
+
},
|
|
737
|
+
tester_id: {
|
|
738
|
+
type: 'string',
|
|
739
|
+
description: 'Filter by specific tester UUID',
|
|
740
|
+
},
|
|
741
|
+
limit: {
|
|
742
|
+
type: 'number',
|
|
743
|
+
description: 'Maximum sessions to return (default: 20)',
|
|
744
|
+
},
|
|
745
|
+
include_findings: {
|
|
746
|
+
type: 'boolean',
|
|
747
|
+
description: 'Include session findings in response (default: true)',
|
|
748
|
+
},
|
|
749
|
+
},
|
|
750
|
+
},
|
|
751
|
+
},
|
|
752
|
+
{
|
|
753
|
+
name: 'get_qa_alerts',
|
|
754
|
+
description: 'Get active QA alerts including hot spots (routes with many bugs), coverage gaps (untested areas), and track gaps. Alerts are proactively generated to highlight areas needing attention.',
|
|
755
|
+
inputSchema: {
|
|
756
|
+
type: 'object',
|
|
757
|
+
properties: {
|
|
758
|
+
severity: {
|
|
759
|
+
type: 'string',
|
|
760
|
+
enum: ['critical', 'warning', 'info', 'all'],
|
|
761
|
+
description: 'Filter by alert severity (default: all)',
|
|
762
|
+
},
|
|
763
|
+
type: {
|
|
764
|
+
type: 'string',
|
|
765
|
+
enum: ['hot_spot', 'coverage_gap', 'track_gap', 'regression', 'all'],
|
|
766
|
+
description: 'Filter by alert type (default: all)',
|
|
767
|
+
},
|
|
768
|
+
status: {
|
|
769
|
+
type: 'string',
|
|
770
|
+
enum: ['active', 'acknowledged', 'all'],
|
|
771
|
+
description: 'Filter by status (default: active)',
|
|
772
|
+
},
|
|
773
|
+
refresh: {
|
|
774
|
+
type: 'boolean',
|
|
775
|
+
description: 'Refresh alerts by running detection (default: false)',
|
|
776
|
+
},
|
|
777
|
+
},
|
|
778
|
+
},
|
|
779
|
+
},
|
|
780
|
+
{
|
|
781
|
+
name: 'get_deployment_analysis',
|
|
782
|
+
description: 'Get deployment history with risk analysis. Shows what files/routes were affected by each deploy and the calculated risk score. Use this to understand deployment impact on QA.',
|
|
783
|
+
inputSchema: {
|
|
784
|
+
type: 'object',
|
|
785
|
+
properties: {
|
|
786
|
+
deployment_id: {
|
|
787
|
+
type: 'string',
|
|
788
|
+
description: 'Get specific deployment by ID',
|
|
789
|
+
},
|
|
790
|
+
environment: {
|
|
791
|
+
type: 'string',
|
|
792
|
+
enum: ['production', 'preview', 'staging', 'all'],
|
|
793
|
+
description: 'Filter by environment (default: all)',
|
|
794
|
+
},
|
|
795
|
+
limit: {
|
|
796
|
+
type: 'number',
|
|
797
|
+
description: 'Maximum deployments to return (default: 10)',
|
|
798
|
+
},
|
|
799
|
+
include_testing_priority: {
|
|
800
|
+
type: 'boolean',
|
|
801
|
+
description: 'Include testing priority recommendations (default: true)',
|
|
802
|
+
},
|
|
803
|
+
},
|
|
804
|
+
},
|
|
805
|
+
},
|
|
806
|
+
{
|
|
807
|
+
name: 'get_tester_recommendations',
|
|
808
|
+
description: 'Get recommended testers for a test case based on their expertise, track strengths, and past performance. Use this for smart test assignment.',
|
|
809
|
+
inputSchema: {
|
|
810
|
+
type: 'object',
|
|
811
|
+
properties: {
|
|
812
|
+
test_case_id: {
|
|
813
|
+
type: 'string',
|
|
814
|
+
description: 'Test case UUID to get recommendations for (required)',
|
|
815
|
+
},
|
|
816
|
+
limit: {
|
|
817
|
+
type: 'number',
|
|
818
|
+
description: 'Maximum recommendations to return (default: 5)',
|
|
819
|
+
},
|
|
820
|
+
},
|
|
821
|
+
required: ['test_case_id'],
|
|
822
|
+
},
|
|
823
|
+
},
|
|
824
|
+
{
|
|
825
|
+
name: 'analyze_commit_for_testing',
|
|
826
|
+
description: 'Analyze a commit to suggest what should be tested. Maps changed files to routes/features and checks bug history to provide prioritized testing recommendations.',
|
|
827
|
+
inputSchema: {
|
|
828
|
+
type: 'object',
|
|
829
|
+
properties: {
|
|
830
|
+
commit_sha: {
|
|
831
|
+
type: 'string',
|
|
832
|
+
description: 'Git commit SHA to analyze',
|
|
833
|
+
},
|
|
834
|
+
files_changed: {
|
|
835
|
+
type: 'array',
|
|
836
|
+
items: { type: 'string' },
|
|
837
|
+
description: 'List of files changed in the commit',
|
|
838
|
+
},
|
|
839
|
+
commit_message: {
|
|
840
|
+
type: 'string',
|
|
841
|
+
description: 'Commit message for context',
|
|
842
|
+
},
|
|
843
|
+
record_deployment: {
|
|
844
|
+
type: 'boolean',
|
|
845
|
+
description: 'Record this as a deployment for tracking (default: false)',
|
|
846
|
+
},
|
|
847
|
+
},
|
|
848
|
+
required: ['files_changed'],
|
|
849
|
+
},
|
|
850
|
+
},
|
|
851
|
+
{
|
|
852
|
+
name: 'get_testing_patterns',
|
|
853
|
+
description: 'Get common testing patterns from the curated pattern library. Patterns are based on public knowledge (OWASP, WCAG, framework docs) NOT customer data. Use this to suggest tests for specific feature types.',
|
|
854
|
+
inputSchema: {
|
|
855
|
+
type: 'object',
|
|
856
|
+
properties: {
|
|
857
|
+
feature_type: {
|
|
858
|
+
type: 'string',
|
|
859
|
+
enum: ['form', 'auth', 'payment', 'file_upload', 'search', 'navigation', 'settings', 'dashboard', 'checkout', 'media', 'async', 'error', 'rendering'],
|
|
860
|
+
description: 'Type of feature to get patterns for',
|
|
861
|
+
},
|
|
862
|
+
framework: {
|
|
863
|
+
type: 'string',
|
|
864
|
+
description: 'Framework to filter patterns (react, nextjs, vue, etc.)',
|
|
865
|
+
},
|
|
866
|
+
tracks: {
|
|
867
|
+
type: 'array',
|
|
868
|
+
items: { type: 'string' },
|
|
869
|
+
description: 'QA tracks to focus on (functional, security, accessibility, performance)',
|
|
870
|
+
},
|
|
871
|
+
search: {
|
|
872
|
+
type: 'string',
|
|
873
|
+
description: 'Search patterns by keyword instead of category',
|
|
874
|
+
},
|
|
875
|
+
},
|
|
876
|
+
},
|
|
877
|
+
},
|
|
725
878
|
];
|
|
726
879
|
// Tool handlers
|
|
727
880
|
async function listReports(args) {
|
|
@@ -2134,6 +2287,353 @@ async function getQAHealth(args) {
|
|
|
2134
2287
|
},
|
|
2135
2288
|
};
|
|
2136
2289
|
}
|
|
2290
|
+
// === SPRINT 5: NEW HANDLER FUNCTIONS ===
|
|
2291
|
+
async function getQASessions(args) {
|
|
2292
|
+
const status = args.status || 'all';
|
|
2293
|
+
const limit = Math.min(args.limit || 20, 50);
|
|
2294
|
+
const includeFindings = args.include_findings !== false;
|
|
2295
|
+
let query = supabase
|
|
2296
|
+
.from('qa_sessions')
|
|
2297
|
+
.select(`
|
|
2298
|
+
id, focus_area, track, platform, started_at, ended_at,
|
|
2299
|
+
notes, routes_covered, status, duration_minutes,
|
|
2300
|
+
findings_count, bugs_filed, created_at,
|
|
2301
|
+
tester:testers(id, name, email)
|
|
2302
|
+
`)
|
|
2303
|
+
.eq('project_id', PROJECT_ID)
|
|
2304
|
+
.order('started_at', { ascending: false })
|
|
2305
|
+
.limit(limit);
|
|
2306
|
+
if (status !== 'all') {
|
|
2307
|
+
query = query.eq('status', status);
|
|
2308
|
+
}
|
|
2309
|
+
if (args.tester_id && isValidUUID(args.tester_id)) {
|
|
2310
|
+
query = query.eq('tester_id', args.tester_id);
|
|
2311
|
+
}
|
|
2312
|
+
const { data: sessions, error } = await query;
|
|
2313
|
+
if (error) {
|
|
2314
|
+
return { error: error.message };
|
|
2315
|
+
}
|
|
2316
|
+
// Optionally load findings for each session
|
|
2317
|
+
let sessionsWithFindings = sessions || [];
|
|
2318
|
+
if (includeFindings && sessions && sessions.length > 0) {
|
|
2319
|
+
const sessionIds = sessions.map(s => s.id);
|
|
2320
|
+
const { data: findings } = await supabase
|
|
2321
|
+
.from('qa_findings')
|
|
2322
|
+
.select('id, session_id, type, severity, title, description, route, converted_to_bug_id, dismissed')
|
|
2323
|
+
.in('session_id', sessionIds);
|
|
2324
|
+
const findingsList = findings || [];
|
|
2325
|
+
const findingsBySession = findingsList.reduce((acc, f) => {
|
|
2326
|
+
acc[f.session_id] = acc[f.session_id] || [];
|
|
2327
|
+
acc[f.session_id].push(f);
|
|
2328
|
+
return acc;
|
|
2329
|
+
}, {});
|
|
2330
|
+
sessionsWithFindings = sessions.map(s => ({
|
|
2331
|
+
...s,
|
|
2332
|
+
findings: findingsBySession[s.id] || [],
|
|
2333
|
+
}));
|
|
2334
|
+
}
|
|
2335
|
+
const summary = {
|
|
2336
|
+
total: sessionsWithFindings.length,
|
|
2337
|
+
active: sessionsWithFindings.filter(s => s.status === 'active').length,
|
|
2338
|
+
completed: sessionsWithFindings.filter(s => s.status === 'completed').length,
|
|
2339
|
+
totalFindings: sessionsWithFindings.reduce((sum, s) => sum + (s.findings_count || 0), 0),
|
|
2340
|
+
totalBugsFiled: sessionsWithFindings.reduce((sum, s) => sum + (s.bugs_filed || 0), 0),
|
|
2341
|
+
};
|
|
2342
|
+
return {
|
|
2343
|
+
sessions: sessionsWithFindings,
|
|
2344
|
+
summary,
|
|
2345
|
+
};
|
|
2346
|
+
}
|
|
2347
|
+
async function getQAAlerts(args) {
|
|
2348
|
+
const severity = args.severity || 'all';
|
|
2349
|
+
const type = args.type || 'all';
|
|
2350
|
+
const status = args.status || 'active';
|
|
2351
|
+
// Optionally refresh alerts
|
|
2352
|
+
if (args.refresh) {
|
|
2353
|
+
await supabase.rpc('detect_all_alerts', { p_project_id: PROJECT_ID });
|
|
2354
|
+
}
|
|
2355
|
+
let query = supabase
|
|
2356
|
+
.from('qa_alerts')
|
|
2357
|
+
.select('*')
|
|
2358
|
+
.eq('project_id', PROJECT_ID)
|
|
2359
|
+
.order('severity', { ascending: true }) // critical first
|
|
2360
|
+
.order('created_at', { ascending: false });
|
|
2361
|
+
if (severity !== 'all') {
|
|
2362
|
+
query = query.eq('severity', severity);
|
|
2363
|
+
}
|
|
2364
|
+
if (type !== 'all') {
|
|
2365
|
+
query = query.eq('type', type);
|
|
2366
|
+
}
|
|
2367
|
+
if (status !== 'all') {
|
|
2368
|
+
query = query.eq('status', status);
|
|
2369
|
+
}
|
|
2370
|
+
const { data: alerts, error } = await query;
|
|
2371
|
+
if (error) {
|
|
2372
|
+
return { error: error.message };
|
|
2373
|
+
}
|
|
2374
|
+
const summary = {
|
|
2375
|
+
total: alerts?.length || 0,
|
|
2376
|
+
critical: alerts?.filter(a => a.severity === 'critical').length || 0,
|
|
2377
|
+
warning: alerts?.filter(a => a.severity === 'warning').length || 0,
|
|
2378
|
+
info: alerts?.filter(a => a.severity === 'info').length || 0,
|
|
2379
|
+
byType: {
|
|
2380
|
+
hot_spot: alerts?.filter(a => a.type === 'hot_spot').length || 0,
|
|
2381
|
+
coverage_gap: alerts?.filter(a => a.type === 'coverage_gap').length || 0,
|
|
2382
|
+
track_gap: alerts?.filter(a => a.type === 'track_gap').length || 0,
|
|
2383
|
+
},
|
|
2384
|
+
};
|
|
2385
|
+
return {
|
|
2386
|
+
alerts: alerts?.map(a => ({
|
|
2387
|
+
id: a.id,
|
|
2388
|
+
type: a.type,
|
|
2389
|
+
severity: a.severity,
|
|
2390
|
+
title: a.title,
|
|
2391
|
+
description: a.description,
|
|
2392
|
+
route: a.trigger_route,
|
|
2393
|
+
track: a.trigger_track,
|
|
2394
|
+
recommendation: a.recommendation,
|
|
2395
|
+
action_type: a.action_type,
|
|
2396
|
+
status: a.status,
|
|
2397
|
+
created_at: a.created_at,
|
|
2398
|
+
})),
|
|
2399
|
+
summary,
|
|
2400
|
+
};
|
|
2401
|
+
}
|
|
2402
|
+
async function getDeploymentAnalysis(args) {
|
|
2403
|
+
const limit = Math.min(args.limit || 10, 50);
|
|
2404
|
+
const includeTestingPriority = args.include_testing_priority !== false;
|
|
2405
|
+
if (args.deployment_id && isValidUUID(args.deployment_id)) {
|
|
2406
|
+
// Get specific deployment
|
|
2407
|
+
const { data: deployment, error } = await supabase
|
|
2408
|
+
.from('deployments')
|
|
2409
|
+
.select('*')
|
|
2410
|
+
.eq('id', args.deployment_id)
|
|
2411
|
+
.eq('project_id', PROJECT_ID)
|
|
2412
|
+
.single();
|
|
2413
|
+
if (error) {
|
|
2414
|
+
return { error: error.message };
|
|
2415
|
+
}
|
|
2416
|
+
return { deployment };
|
|
2417
|
+
}
|
|
2418
|
+
// Get deployment history
|
|
2419
|
+
let query = supabase
|
|
2420
|
+
.from('deployments')
|
|
2421
|
+
.select('*')
|
|
2422
|
+
.eq('project_id', PROJECT_ID)
|
|
2423
|
+
.order('deployed_at', { ascending: false })
|
|
2424
|
+
.limit(limit);
|
|
2425
|
+
if (args.environment && args.environment !== 'all') {
|
|
2426
|
+
query = query.eq('environment', args.environment);
|
|
2427
|
+
}
|
|
2428
|
+
const { data: deployments, error } = await query;
|
|
2429
|
+
if (error) {
|
|
2430
|
+
return { error: error.message };
|
|
2431
|
+
}
|
|
2432
|
+
const summary = {
|
|
2433
|
+
total: deployments?.length || 0,
|
|
2434
|
+
avgRiskScore: deployments?.length
|
|
2435
|
+
? Math.round(deployments.reduce((sum, d) => sum + (d.risk_score || 0), 0) / deployments.length)
|
|
2436
|
+
: 0,
|
|
2437
|
+
highRisk: deployments?.filter(d => (d.risk_score || 0) >= 70).length || 0,
|
|
2438
|
+
verified: deployments?.filter(d => d.verified_at).length || 0,
|
|
2439
|
+
};
|
|
2440
|
+
return {
|
|
2441
|
+
deployments: deployments?.map(d => ({
|
|
2442
|
+
id: d.id,
|
|
2443
|
+
environment: d.environment,
|
|
2444
|
+
commit_sha: d.commit_sha,
|
|
2445
|
+
commit_message: d.commit_message,
|
|
2446
|
+
branch: d.branch,
|
|
2447
|
+
deployed_at: d.deployed_at,
|
|
2448
|
+
risk_score: d.risk_score,
|
|
2449
|
+
routes_affected: d.routes_affected,
|
|
2450
|
+
testing_priority: includeTestingPriority ? d.testing_priority : undefined,
|
|
2451
|
+
verified: !!d.verified_at,
|
|
2452
|
+
})),
|
|
2453
|
+
summary,
|
|
2454
|
+
};
|
|
2455
|
+
}
|
|
2456
|
+
async function getTesterRecommendations(args) {
|
|
2457
|
+
if (!isValidUUID(args.test_case_id)) {
|
|
2458
|
+
return { error: 'Invalid test_case_id format' };
|
|
2459
|
+
}
|
|
2460
|
+
const limit = Math.min(args.limit || 5, 10);
|
|
2461
|
+
const { data: recommendations, error } = await supabase.rpc('get_tester_recommendations', {
|
|
2462
|
+
p_test_case_id: args.test_case_id,
|
|
2463
|
+
p_limit: limit,
|
|
2464
|
+
});
|
|
2465
|
+
if (error) {
|
|
2466
|
+
return { error: error.message };
|
|
2467
|
+
}
|
|
2468
|
+
// Get test case info for context
|
|
2469
|
+
const { data: testCase } = await supabase
|
|
2470
|
+
.from('test_cases')
|
|
2471
|
+
.select('test_key, title, track:qa_tracks(name)')
|
|
2472
|
+
.eq('id', args.test_case_id)
|
|
2473
|
+
.single();
|
|
2474
|
+
return {
|
|
2475
|
+
test_case: testCase ? {
|
|
2476
|
+
id: args.test_case_id,
|
|
2477
|
+
test_key: testCase.test_key,
|
|
2478
|
+
title: testCase.title,
|
|
2479
|
+
track: testCase.track?.name,
|
|
2480
|
+
} : null,
|
|
2481
|
+
recommendations: recommendations?.map((r) => ({
|
|
2482
|
+
tester_id: r.tester_id,
|
|
2483
|
+
name: r.tester_name,
|
|
2484
|
+
email: r.tester_email,
|
|
2485
|
+
match_score: r.match_score,
|
|
2486
|
+
reasons: r.match_reasons,
|
|
2487
|
+
})) || [],
|
|
2488
|
+
};
|
|
2489
|
+
}
|
|
2490
|
+
async function analyzeCommitForTesting(args) {
|
|
2491
|
+
const filesChanged = args.files_changed || [];
|
|
2492
|
+
// Map files to routes using file_route_mapping
|
|
2493
|
+
const { data: mappings } = await supabase
|
|
2494
|
+
.from('file_route_mapping')
|
|
2495
|
+
.select('file_pattern, route, feature, confidence')
|
|
2496
|
+
.eq('project_id', PROJECT_ID);
|
|
2497
|
+
const affectedRoutes = [];
|
|
2498
|
+
for (const mapping of mappings || []) {
|
|
2499
|
+
const matchedFiles = filesChanged.filter(file => {
|
|
2500
|
+
const pattern = mapping.file_pattern
|
|
2501
|
+
.replace(/\*\*/g, '.*')
|
|
2502
|
+
.replace(/\*/g, '[^/]*');
|
|
2503
|
+
return new RegExp(pattern).test(file);
|
|
2504
|
+
});
|
|
2505
|
+
if (matchedFiles.length > 0) {
|
|
2506
|
+
affectedRoutes.push({
|
|
2507
|
+
route: mapping.route,
|
|
2508
|
+
feature: mapping.feature || undefined,
|
|
2509
|
+
confidence: mapping.confidence,
|
|
2510
|
+
matched_files: matchedFiles,
|
|
2511
|
+
});
|
|
2512
|
+
}
|
|
2513
|
+
}
|
|
2514
|
+
// Get bug history for affected routes
|
|
2515
|
+
const routes = affectedRoutes.map(r => r.route);
|
|
2516
|
+
let bugHistory = [];
|
|
2517
|
+
if (routes.length > 0) {
|
|
2518
|
+
const { data: bugs } = await supabase
|
|
2519
|
+
.from('reports')
|
|
2520
|
+
.select('id, severity, description, route, created_at')
|
|
2521
|
+
.eq('project_id', PROJECT_ID)
|
|
2522
|
+
.eq('report_type', 'bug')
|
|
2523
|
+
.in('route', routes)
|
|
2524
|
+
.gte('created_at', new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString());
|
|
2525
|
+
bugHistory = bugs || [];
|
|
2526
|
+
}
|
|
2527
|
+
// Calculate risk score
|
|
2528
|
+
let riskScore = 0;
|
|
2529
|
+
riskScore += Math.min(filesChanged.length * 2, 20); // File count
|
|
2530
|
+
riskScore += Math.min(affectedRoutes.length * 5, 30); // Route count
|
|
2531
|
+
riskScore += Math.min(bugHistory.length * 10, 50); // Bug history
|
|
2532
|
+
// Generate testing recommendations
|
|
2533
|
+
const recommendations = affectedRoutes.map(r => {
|
|
2534
|
+
const routeBugs = bugHistory.filter(b => b.route === r.route);
|
|
2535
|
+
const priority = routeBugs.length >= 3 ? 'critical' :
|
|
2536
|
+
routeBugs.length >= 1 ? 'high' : 'medium';
|
|
2537
|
+
return {
|
|
2538
|
+
route: r.route,
|
|
2539
|
+
feature: r.feature,
|
|
2540
|
+
priority,
|
|
2541
|
+
reason: routeBugs.length > 0
|
|
2542
|
+
? `${routeBugs.length} bug(s) in last 30 days`
|
|
2543
|
+
: 'Code changed in this area',
|
|
2544
|
+
recent_bugs: routeBugs.length,
|
|
2545
|
+
};
|
|
2546
|
+
}).sort((a, b) => {
|
|
2547
|
+
const order = { critical: 0, high: 1, medium: 2 };
|
|
2548
|
+
return order[a.priority] - order[b.priority];
|
|
2549
|
+
});
|
|
2550
|
+
// Optionally record as deployment
|
|
2551
|
+
if (args.record_deployment) {
|
|
2552
|
+
await supabase.rpc('record_deployment', {
|
|
2553
|
+
p_project_id: PROJECT_ID,
|
|
2554
|
+
p_environment: 'production',
|
|
2555
|
+
p_commit_sha: args.commit_sha || null,
|
|
2556
|
+
p_commit_message: args.commit_message || null,
|
|
2557
|
+
p_files_changed: filesChanged,
|
|
2558
|
+
p_webhook_source: 'mcp',
|
|
2559
|
+
});
|
|
2560
|
+
}
|
|
2561
|
+
return {
|
|
2562
|
+
commit_sha: args.commit_sha,
|
|
2563
|
+
files_analyzed: filesChanged.length,
|
|
2564
|
+
risk_score: Math.min(riskScore, 100),
|
|
2565
|
+
affected_routes: affectedRoutes,
|
|
2566
|
+
bug_history_summary: {
|
|
2567
|
+
total: bugHistory.length,
|
|
2568
|
+
critical: bugHistory.filter(b => b.severity === 'critical').length,
|
|
2569
|
+
high: bugHistory.filter(b => b.severity === 'high').length,
|
|
2570
|
+
},
|
|
2571
|
+
testing_recommendations: recommendations,
|
|
2572
|
+
deployment_recorded: args.record_deployment || false,
|
|
2573
|
+
};
|
|
2574
|
+
}
|
|
2575
|
+
async function getTestingPatterns(args) {
|
|
2576
|
+
let patterns = [];
|
|
2577
|
+
if (args.search) {
|
|
2578
|
+
// Search patterns by keyword
|
|
2579
|
+
const { data, error } = await supabase.rpc('search_patterns', {
|
|
2580
|
+
p_query: args.search,
|
|
2581
|
+
p_limit: 20,
|
|
2582
|
+
});
|
|
2583
|
+
if (!error)
|
|
2584
|
+
patterns = data || [];
|
|
2585
|
+
}
|
|
2586
|
+
else if (args.feature_type) {
|
|
2587
|
+
// Get patterns for feature type
|
|
2588
|
+
const { data, error } = await supabase.rpc('get_patterns_for_feature', {
|
|
2589
|
+
p_category: args.feature_type,
|
|
2590
|
+
p_framework: args.framework || null,
|
|
2591
|
+
p_tracks: args.tracks || null,
|
|
2592
|
+
});
|
|
2593
|
+
if (!error)
|
|
2594
|
+
patterns = data || [];
|
|
2595
|
+
}
|
|
2596
|
+
else {
|
|
2597
|
+
// Get all patterns (limited)
|
|
2598
|
+
const { data, error } = await supabase
|
|
2599
|
+
.from('qa_patterns')
|
|
2600
|
+
.select('*')
|
|
2601
|
+
.eq('is_active', true)
|
|
2602
|
+
.order('severity')
|
|
2603
|
+
.limit(30);
|
|
2604
|
+
if (!error)
|
|
2605
|
+
patterns = data || [];
|
|
2606
|
+
}
|
|
2607
|
+
const summary = {
|
|
2608
|
+
total: patterns.length,
|
|
2609
|
+
by_severity: {
|
|
2610
|
+
critical: patterns.filter(p => p.severity === 'critical').length,
|
|
2611
|
+
high: patterns.filter(p => p.severity === 'high').length,
|
|
2612
|
+
medium: patterns.filter(p => p.severity === 'medium').length,
|
|
2613
|
+
low: patterns.filter(p => p.severity === 'low').length,
|
|
2614
|
+
},
|
|
2615
|
+
by_track: patterns.reduce((acc, p) => {
|
|
2616
|
+
acc[p.track] = (acc[p.track] || 0) + 1;
|
|
2617
|
+
return acc;
|
|
2618
|
+
}, {}),
|
|
2619
|
+
};
|
|
2620
|
+
return {
|
|
2621
|
+
patterns: patterns.map(p => ({
|
|
2622
|
+
title: p.title,
|
|
2623
|
+
category: p.category,
|
|
2624
|
+
track: p.track,
|
|
2625
|
+
severity: p.severity,
|
|
2626
|
+
description: p.description,
|
|
2627
|
+
why_it_happens: p.why_it_happens,
|
|
2628
|
+
suggested_tests: p.suggested_tests,
|
|
2629
|
+
common_fix: p.common_fix,
|
|
2630
|
+
source: p.source,
|
|
2631
|
+
frameworks: p.frameworks,
|
|
2632
|
+
})),
|
|
2633
|
+
summary,
|
|
2634
|
+
note: 'Patterns are from public knowledge (OWASP, WCAG, framework docs), not customer data.',
|
|
2635
|
+
};
|
|
2636
|
+
}
|
|
2137
2637
|
async function analyzeChangesForTests(args) {
|
|
2138
2638
|
// Get existing tests to check coverage
|
|
2139
2639
|
const { data: existingTests } = await supabase
|
|
@@ -2496,6 +2996,27 @@ async function createBugReport(args) {
|
|
|
2496
2996
|
if (args.suggested_fix) {
|
|
2497
2997
|
codeContext.suggested_fix = args.suggested_fix;
|
|
2498
2998
|
}
|
|
2999
|
+
// Find a reporter_id: try to get the project owner or first tester
|
|
3000
|
+
let reporterId = null;
|
|
3001
|
+
const { data: project } = await supabase
|
|
3002
|
+
.from('projects')
|
|
3003
|
+
.select('owner_id')
|
|
3004
|
+
.eq('id', PROJECT_ID)
|
|
3005
|
+
.single();
|
|
3006
|
+
if (project?.owner_id) {
|
|
3007
|
+
reporterId = project.owner_id;
|
|
3008
|
+
}
|
|
3009
|
+
else {
|
|
3010
|
+
// Fallback: use the first tester for this project
|
|
3011
|
+
const { data: testers } = await supabase
|
|
3012
|
+
.from('testers')
|
|
3013
|
+
.select('id')
|
|
3014
|
+
.eq('project_id', PROJECT_ID)
|
|
3015
|
+
.limit(1);
|
|
3016
|
+
if (testers && testers.length > 0) {
|
|
3017
|
+
reporterId = testers[0].id;
|
|
3018
|
+
}
|
|
3019
|
+
}
|
|
2499
3020
|
const report = {
|
|
2500
3021
|
project_id: PROJECT_ID,
|
|
2501
3022
|
report_type: 'bug',
|
|
@@ -2514,6 +3035,9 @@ async function createBugReport(args) {
|
|
|
2514
3035
|
},
|
|
2515
3036
|
code_context: codeContext,
|
|
2516
3037
|
};
|
|
3038
|
+
if (reporterId) {
|
|
3039
|
+
report.reporter_id = reporterId;
|
|
3040
|
+
}
|
|
2517
3041
|
const { data, error } = await supabase
|
|
2518
3042
|
.from('reports')
|
|
2519
3043
|
.insert(report)
|
|
@@ -3491,6 +4015,25 @@ async function main() {
|
|
|
3491
4015
|
case 'complete_fix_request':
|
|
3492
4016
|
result = await completeFixRequest(args);
|
|
3493
4017
|
break;
|
|
4018
|
+
// === SPRINT 5: NEW MCP TOOLS ===
|
|
4019
|
+
case 'get_qa_sessions':
|
|
4020
|
+
result = await getQASessions(args);
|
|
4021
|
+
break;
|
|
4022
|
+
case 'get_qa_alerts':
|
|
4023
|
+
result = await getQAAlerts(args);
|
|
4024
|
+
break;
|
|
4025
|
+
case 'get_deployment_analysis':
|
|
4026
|
+
result = await getDeploymentAnalysis(args);
|
|
4027
|
+
break;
|
|
4028
|
+
case 'get_tester_recommendations':
|
|
4029
|
+
result = await getTesterRecommendations(args);
|
|
4030
|
+
break;
|
|
4031
|
+
case 'analyze_commit_for_testing':
|
|
4032
|
+
result = await analyzeCommitForTesting(args);
|
|
4033
|
+
break;
|
|
4034
|
+
case 'get_testing_patterns':
|
|
4035
|
+
result = await getTestingPatterns(args);
|
|
4036
|
+
break;
|
|
3494
4037
|
default:
|
|
3495
4038
|
return {
|
|
3496
4039
|
content: [{ type: 'text', text: `Unknown tool: ${name}` }],
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -739,6 +739,159 @@ const tools = [
|
|
|
739
739
|
},
|
|
740
740
|
},
|
|
741
741
|
},
|
|
742
|
+
// === SPRINT 5: QA INTELLIGENCE EXPANSION ===
|
|
743
|
+
{
|
|
744
|
+
name: 'get_qa_sessions',
|
|
745
|
+
description: 'Get QA session history showing exploratory testing sessions. Sessions capture findings that may or may not become formal bugs. Use this to understand tester exploration patterns and unstructured discoveries.',
|
|
746
|
+
inputSchema: {
|
|
747
|
+
type: 'object' as const,
|
|
748
|
+
properties: {
|
|
749
|
+
status: {
|
|
750
|
+
type: 'string',
|
|
751
|
+
enum: ['active', 'completed', 'all'],
|
|
752
|
+
description: 'Filter by session status (default: all)',
|
|
753
|
+
},
|
|
754
|
+
tester_id: {
|
|
755
|
+
type: 'string',
|
|
756
|
+
description: 'Filter by specific tester UUID',
|
|
757
|
+
},
|
|
758
|
+
limit: {
|
|
759
|
+
type: 'number',
|
|
760
|
+
description: 'Maximum sessions to return (default: 20)',
|
|
761
|
+
},
|
|
762
|
+
include_findings: {
|
|
763
|
+
type: 'boolean',
|
|
764
|
+
description: 'Include session findings in response (default: true)',
|
|
765
|
+
},
|
|
766
|
+
},
|
|
767
|
+
},
|
|
768
|
+
},
|
|
769
|
+
{
|
|
770
|
+
name: 'get_qa_alerts',
|
|
771
|
+
description: 'Get active QA alerts including hot spots (routes with many bugs), coverage gaps (untested areas), and track gaps. Alerts are proactively generated to highlight areas needing attention.',
|
|
772
|
+
inputSchema: {
|
|
773
|
+
type: 'object' as const,
|
|
774
|
+
properties: {
|
|
775
|
+
severity: {
|
|
776
|
+
type: 'string',
|
|
777
|
+
enum: ['critical', 'warning', 'info', 'all'],
|
|
778
|
+
description: 'Filter by alert severity (default: all)',
|
|
779
|
+
},
|
|
780
|
+
type: {
|
|
781
|
+
type: 'string',
|
|
782
|
+
enum: ['hot_spot', 'coverage_gap', 'track_gap', 'regression', 'all'],
|
|
783
|
+
description: 'Filter by alert type (default: all)',
|
|
784
|
+
},
|
|
785
|
+
status: {
|
|
786
|
+
type: 'string',
|
|
787
|
+
enum: ['active', 'acknowledged', 'all'],
|
|
788
|
+
description: 'Filter by status (default: active)',
|
|
789
|
+
},
|
|
790
|
+
refresh: {
|
|
791
|
+
type: 'boolean',
|
|
792
|
+
description: 'Refresh alerts by running detection (default: false)',
|
|
793
|
+
},
|
|
794
|
+
},
|
|
795
|
+
},
|
|
796
|
+
},
|
|
797
|
+
{
|
|
798
|
+
name: 'get_deployment_analysis',
|
|
799
|
+
description: 'Get deployment history with risk analysis. Shows what files/routes were affected by each deploy and the calculated risk score. Use this to understand deployment impact on QA.',
|
|
800
|
+
inputSchema: {
|
|
801
|
+
type: 'object' as const,
|
|
802
|
+
properties: {
|
|
803
|
+
deployment_id: {
|
|
804
|
+
type: 'string',
|
|
805
|
+
description: 'Get specific deployment by ID',
|
|
806
|
+
},
|
|
807
|
+
environment: {
|
|
808
|
+
type: 'string',
|
|
809
|
+
enum: ['production', 'preview', 'staging', 'all'],
|
|
810
|
+
description: 'Filter by environment (default: all)',
|
|
811
|
+
},
|
|
812
|
+
limit: {
|
|
813
|
+
type: 'number',
|
|
814
|
+
description: 'Maximum deployments to return (default: 10)',
|
|
815
|
+
},
|
|
816
|
+
include_testing_priority: {
|
|
817
|
+
type: 'boolean',
|
|
818
|
+
description: 'Include testing priority recommendations (default: true)',
|
|
819
|
+
},
|
|
820
|
+
},
|
|
821
|
+
},
|
|
822
|
+
},
|
|
823
|
+
{
|
|
824
|
+
name: 'get_tester_recommendations',
|
|
825
|
+
description: 'Get recommended testers for a test case based on their expertise, track strengths, and past performance. Use this for smart test assignment.',
|
|
826
|
+
inputSchema: {
|
|
827
|
+
type: 'object' as const,
|
|
828
|
+
properties: {
|
|
829
|
+
test_case_id: {
|
|
830
|
+
type: 'string',
|
|
831
|
+
description: 'Test case UUID to get recommendations for (required)',
|
|
832
|
+
},
|
|
833
|
+
limit: {
|
|
834
|
+
type: 'number',
|
|
835
|
+
description: 'Maximum recommendations to return (default: 5)',
|
|
836
|
+
},
|
|
837
|
+
},
|
|
838
|
+
required: ['test_case_id'],
|
|
839
|
+
},
|
|
840
|
+
},
|
|
841
|
+
{
|
|
842
|
+
name: 'analyze_commit_for_testing',
|
|
843
|
+
description: 'Analyze a commit to suggest what should be tested. Maps changed files to routes/features and checks bug history to provide prioritized testing recommendations.',
|
|
844
|
+
inputSchema: {
|
|
845
|
+
type: 'object' as const,
|
|
846
|
+
properties: {
|
|
847
|
+
commit_sha: {
|
|
848
|
+
type: 'string',
|
|
849
|
+
description: 'Git commit SHA to analyze',
|
|
850
|
+
},
|
|
851
|
+
files_changed: {
|
|
852
|
+
type: 'array',
|
|
853
|
+
items: { type: 'string' },
|
|
854
|
+
description: 'List of files changed in the commit',
|
|
855
|
+
},
|
|
856
|
+
commit_message: {
|
|
857
|
+
type: 'string',
|
|
858
|
+
description: 'Commit message for context',
|
|
859
|
+
},
|
|
860
|
+
record_deployment: {
|
|
861
|
+
type: 'boolean',
|
|
862
|
+
description: 'Record this as a deployment for tracking (default: false)',
|
|
863
|
+
},
|
|
864
|
+
},
|
|
865
|
+
required: ['files_changed'],
|
|
866
|
+
},
|
|
867
|
+
},
|
|
868
|
+
{
|
|
869
|
+
name: 'get_testing_patterns',
|
|
870
|
+
description: 'Get common testing patterns from the curated pattern library. Patterns are based on public knowledge (OWASP, WCAG, framework docs) NOT customer data. Use this to suggest tests for specific feature types.',
|
|
871
|
+
inputSchema: {
|
|
872
|
+
type: 'object' as const,
|
|
873
|
+
properties: {
|
|
874
|
+
feature_type: {
|
|
875
|
+
type: 'string',
|
|
876
|
+
enum: ['form', 'auth', 'payment', 'file_upload', 'search', 'navigation', 'settings', 'dashboard', 'checkout', 'media', 'async', 'error', 'rendering'],
|
|
877
|
+
description: 'Type of feature to get patterns for',
|
|
878
|
+
},
|
|
879
|
+
framework: {
|
|
880
|
+
type: 'string',
|
|
881
|
+
description: 'Framework to filter patterns (react, nextjs, vue, etc.)',
|
|
882
|
+
},
|
|
883
|
+
tracks: {
|
|
884
|
+
type: 'array',
|
|
885
|
+
items: { type: 'string' },
|
|
886
|
+
description: 'QA tracks to focus on (functional, security, accessibility, performance)',
|
|
887
|
+
},
|
|
888
|
+
search: {
|
|
889
|
+
type: 'string',
|
|
890
|
+
description: 'Search patterns by keyword instead of category',
|
|
891
|
+
},
|
|
892
|
+
},
|
|
893
|
+
},
|
|
894
|
+
},
|
|
742
895
|
];
|
|
743
896
|
|
|
744
897
|
// Tool handlers
|
|
@@ -750,7 +903,7 @@ async function listReports(args: {
|
|
|
750
903
|
}) {
|
|
751
904
|
let query = supabase
|
|
752
905
|
.from('reports')
|
|
753
|
-
.select('id, report_type, severity, status, description, app_context, created_at, tester:testers(name, email)')
|
|
906
|
+
.select('id, report_type, severity, status, description, app_context, created_at, reporter_name, reporter_email, tester:testers(name, email)')
|
|
754
907
|
.eq('project_id', PROJECT_ID)
|
|
755
908
|
.order('created_at', { ascending: false })
|
|
756
909
|
.limit(Math.min(args.limit || 10, 50));
|
|
@@ -773,7 +926,7 @@ async function listReports(args: {
|
|
|
773
926
|
status: r.status,
|
|
774
927
|
description: r.description,
|
|
775
928
|
route: (r.app_context as any)?.currentRoute,
|
|
776
|
-
reporter: (r.tester as any)?.name || 'Anonymous',
|
|
929
|
+
reporter: (r.tester as any)?.name || (r as any).reporter_name || 'Anonymous',
|
|
777
930
|
created_at: r.created_at,
|
|
778
931
|
})),
|
|
779
932
|
total: data?.length || 0,
|
|
@@ -812,7 +965,10 @@ async function getReport(args: { report_id: string }) {
|
|
|
812
965
|
reporter: data.tester ? {
|
|
813
966
|
name: data.tester.name,
|
|
814
967
|
email: data.tester.email,
|
|
815
|
-
} :
|
|
968
|
+
} : (data.reporter_name ? {
|
|
969
|
+
name: data.reporter_name,
|
|
970
|
+
email: data.reporter_email,
|
|
971
|
+
} : null),
|
|
816
972
|
track: data.track ? {
|
|
817
973
|
name: data.track.name,
|
|
818
974
|
icon: data.track.icon,
|
|
@@ -2409,6 +2565,429 @@ async function getQAHealth(args: {
|
|
|
2409
2565
|
};
|
|
2410
2566
|
}
|
|
2411
2567
|
|
|
2568
|
+
// === SPRINT 5: NEW HANDLER FUNCTIONS ===
|
|
2569
|
+
|
|
2570
|
+
async function getQASessions(args: {
|
|
2571
|
+
status?: 'active' | 'completed' | 'all';
|
|
2572
|
+
tester_id?: string;
|
|
2573
|
+
limit?: number;
|
|
2574
|
+
include_findings?: boolean;
|
|
2575
|
+
}) {
|
|
2576
|
+
const status = args.status || 'all';
|
|
2577
|
+
const limit = Math.min(args.limit || 20, 50);
|
|
2578
|
+
const includeFindings = args.include_findings !== false;
|
|
2579
|
+
|
|
2580
|
+
let query = supabase
|
|
2581
|
+
.from('qa_sessions')
|
|
2582
|
+
.select(`
|
|
2583
|
+
id, focus_area, track, platform, started_at, ended_at,
|
|
2584
|
+
notes, routes_covered, status, duration_minutes,
|
|
2585
|
+
findings_count, bugs_filed, created_at,
|
|
2586
|
+
tester:testers(id, name, email)
|
|
2587
|
+
`)
|
|
2588
|
+
.eq('project_id', PROJECT_ID)
|
|
2589
|
+
.order('started_at', { ascending: false })
|
|
2590
|
+
.limit(limit);
|
|
2591
|
+
|
|
2592
|
+
if (status !== 'all') {
|
|
2593
|
+
query = query.eq('status', status);
|
|
2594
|
+
}
|
|
2595
|
+
|
|
2596
|
+
if (args.tester_id && isValidUUID(args.tester_id)) {
|
|
2597
|
+
query = query.eq('tester_id', args.tester_id);
|
|
2598
|
+
}
|
|
2599
|
+
|
|
2600
|
+
const { data: sessions, error } = await query;
|
|
2601
|
+
|
|
2602
|
+
if (error) {
|
|
2603
|
+
return { error: error.message };
|
|
2604
|
+
}
|
|
2605
|
+
|
|
2606
|
+
// Optionally load findings for each session
|
|
2607
|
+
let sessionsWithFindings = sessions || [];
|
|
2608
|
+
if (includeFindings && sessions && sessions.length > 0) {
|
|
2609
|
+
const sessionIds = sessions.map(s => s.id);
|
|
2610
|
+
const { data: findings } = await supabase
|
|
2611
|
+
.from('qa_findings')
|
|
2612
|
+
.select('id, session_id, type, severity, title, description, route, converted_to_bug_id, dismissed')
|
|
2613
|
+
.in('session_id', sessionIds);
|
|
2614
|
+
|
|
2615
|
+
const findingsList = findings || [];
|
|
2616
|
+
const findingsBySession = findingsList.reduce((acc, f) => {
|
|
2617
|
+
acc[f.session_id] = acc[f.session_id] || [];
|
|
2618
|
+
acc[f.session_id].push(f);
|
|
2619
|
+
return acc;
|
|
2620
|
+
}, {} as Record<string, typeof findingsList>);
|
|
2621
|
+
|
|
2622
|
+
sessionsWithFindings = sessions.map(s => ({
|
|
2623
|
+
...s,
|
|
2624
|
+
findings: findingsBySession[s.id] || [],
|
|
2625
|
+
}));
|
|
2626
|
+
}
|
|
2627
|
+
|
|
2628
|
+
const summary = {
|
|
2629
|
+
total: sessionsWithFindings.length,
|
|
2630
|
+
active: sessionsWithFindings.filter(s => s.status === 'active').length,
|
|
2631
|
+
completed: sessionsWithFindings.filter(s => s.status === 'completed').length,
|
|
2632
|
+
totalFindings: sessionsWithFindings.reduce((sum, s) => sum + (s.findings_count || 0), 0),
|
|
2633
|
+
totalBugsFiled: sessionsWithFindings.reduce((sum, s) => sum + (s.bugs_filed || 0), 0),
|
|
2634
|
+
};
|
|
2635
|
+
|
|
2636
|
+
return {
|
|
2637
|
+
sessions: sessionsWithFindings,
|
|
2638
|
+
summary,
|
|
2639
|
+
};
|
|
2640
|
+
}
|
|
2641
|
+
|
|
2642
|
+
async function getQAAlerts(args: {
|
|
2643
|
+
severity?: 'critical' | 'warning' | 'info' | 'all';
|
|
2644
|
+
type?: 'hot_spot' | 'coverage_gap' | 'track_gap' | 'regression' | 'all';
|
|
2645
|
+
status?: 'active' | 'acknowledged' | 'all';
|
|
2646
|
+
refresh?: boolean;
|
|
2647
|
+
}) {
|
|
2648
|
+
const severity = args.severity || 'all';
|
|
2649
|
+
const type = args.type || 'all';
|
|
2650
|
+
const status = args.status || 'active';
|
|
2651
|
+
|
|
2652
|
+
// Optionally refresh alerts
|
|
2653
|
+
if (args.refresh) {
|
|
2654
|
+
await supabase.rpc('detect_all_alerts', { p_project_id: PROJECT_ID });
|
|
2655
|
+
}
|
|
2656
|
+
|
|
2657
|
+
let query = supabase
|
|
2658
|
+
.from('qa_alerts')
|
|
2659
|
+
.select('*')
|
|
2660
|
+
.eq('project_id', PROJECT_ID)
|
|
2661
|
+
.order('severity', { ascending: true }) // critical first
|
|
2662
|
+
.order('created_at', { ascending: false });
|
|
2663
|
+
|
|
2664
|
+
if (severity !== 'all') {
|
|
2665
|
+
query = query.eq('severity', severity);
|
|
2666
|
+
}
|
|
2667
|
+
if (type !== 'all') {
|
|
2668
|
+
query = query.eq('type', type);
|
|
2669
|
+
}
|
|
2670
|
+
if (status !== 'all') {
|
|
2671
|
+
query = query.eq('status', status);
|
|
2672
|
+
}
|
|
2673
|
+
|
|
2674
|
+
const { data: alerts, error } = await query;
|
|
2675
|
+
|
|
2676
|
+
if (error) {
|
|
2677
|
+
return { error: error.message };
|
|
2678
|
+
}
|
|
2679
|
+
|
|
2680
|
+
const summary = {
|
|
2681
|
+
total: alerts?.length || 0,
|
|
2682
|
+
critical: alerts?.filter(a => a.severity === 'critical').length || 0,
|
|
2683
|
+
warning: alerts?.filter(a => a.severity === 'warning').length || 0,
|
|
2684
|
+
info: alerts?.filter(a => a.severity === 'info').length || 0,
|
|
2685
|
+
byType: {
|
|
2686
|
+
hot_spot: alerts?.filter(a => a.type === 'hot_spot').length || 0,
|
|
2687
|
+
coverage_gap: alerts?.filter(a => a.type === 'coverage_gap').length || 0,
|
|
2688
|
+
track_gap: alerts?.filter(a => a.type === 'track_gap').length || 0,
|
|
2689
|
+
},
|
|
2690
|
+
};
|
|
2691
|
+
|
|
2692
|
+
return {
|
|
2693
|
+
alerts: alerts?.map(a => ({
|
|
2694
|
+
id: a.id,
|
|
2695
|
+
type: a.type,
|
|
2696
|
+
severity: a.severity,
|
|
2697
|
+
title: a.title,
|
|
2698
|
+
description: a.description,
|
|
2699
|
+
route: a.trigger_route,
|
|
2700
|
+
track: a.trigger_track,
|
|
2701
|
+
recommendation: a.recommendation,
|
|
2702
|
+
action_type: a.action_type,
|
|
2703
|
+
status: a.status,
|
|
2704
|
+
created_at: a.created_at,
|
|
2705
|
+
})),
|
|
2706
|
+
summary,
|
|
2707
|
+
};
|
|
2708
|
+
}
|
|
2709
|
+
|
|
2710
|
+
async function getDeploymentAnalysis(args: {
|
|
2711
|
+
deployment_id?: string;
|
|
2712
|
+
environment?: 'production' | 'preview' | 'staging' | 'all';
|
|
2713
|
+
limit?: number;
|
|
2714
|
+
include_testing_priority?: boolean;
|
|
2715
|
+
}) {
|
|
2716
|
+
const limit = Math.min(args.limit || 10, 50);
|
|
2717
|
+
const includeTestingPriority = args.include_testing_priority !== false;
|
|
2718
|
+
|
|
2719
|
+
if (args.deployment_id && isValidUUID(args.deployment_id)) {
|
|
2720
|
+
// Get specific deployment
|
|
2721
|
+
const { data: deployment, error } = await supabase
|
|
2722
|
+
.from('deployments')
|
|
2723
|
+
.select('*')
|
|
2724
|
+
.eq('id', args.deployment_id)
|
|
2725
|
+
.eq('project_id', PROJECT_ID)
|
|
2726
|
+
.single();
|
|
2727
|
+
|
|
2728
|
+
if (error) {
|
|
2729
|
+
return { error: error.message };
|
|
2730
|
+
}
|
|
2731
|
+
|
|
2732
|
+
return { deployment };
|
|
2733
|
+
}
|
|
2734
|
+
|
|
2735
|
+
// Get deployment history
|
|
2736
|
+
let query = supabase
|
|
2737
|
+
.from('deployments')
|
|
2738
|
+
.select('*')
|
|
2739
|
+
.eq('project_id', PROJECT_ID)
|
|
2740
|
+
.order('deployed_at', { ascending: false })
|
|
2741
|
+
.limit(limit);
|
|
2742
|
+
|
|
2743
|
+
if (args.environment && args.environment !== 'all') {
|
|
2744
|
+
query = query.eq('environment', args.environment);
|
|
2745
|
+
}
|
|
2746
|
+
|
|
2747
|
+
const { data: deployments, error } = await query;
|
|
2748
|
+
|
|
2749
|
+
if (error) {
|
|
2750
|
+
return { error: error.message };
|
|
2751
|
+
}
|
|
2752
|
+
|
|
2753
|
+
const summary = {
|
|
2754
|
+
total: deployments?.length || 0,
|
|
2755
|
+
avgRiskScore: deployments?.length
|
|
2756
|
+
? Math.round(deployments.reduce((sum, d) => sum + (d.risk_score || 0), 0) / deployments.length)
|
|
2757
|
+
: 0,
|
|
2758
|
+
highRisk: deployments?.filter(d => (d.risk_score || 0) >= 70).length || 0,
|
|
2759
|
+
verified: deployments?.filter(d => d.verified_at).length || 0,
|
|
2760
|
+
};
|
|
2761
|
+
|
|
2762
|
+
return {
|
|
2763
|
+
deployments: deployments?.map(d => ({
|
|
2764
|
+
id: d.id,
|
|
2765
|
+
environment: d.environment,
|
|
2766
|
+
commit_sha: d.commit_sha,
|
|
2767
|
+
commit_message: d.commit_message,
|
|
2768
|
+
branch: d.branch,
|
|
2769
|
+
deployed_at: d.deployed_at,
|
|
2770
|
+
risk_score: d.risk_score,
|
|
2771
|
+
routes_affected: d.routes_affected,
|
|
2772
|
+
testing_priority: includeTestingPriority ? d.testing_priority : undefined,
|
|
2773
|
+
verified: !!d.verified_at,
|
|
2774
|
+
})),
|
|
2775
|
+
summary,
|
|
2776
|
+
};
|
|
2777
|
+
}
|
|
2778
|
+
|
|
2779
|
+
async function getTesterRecommendations(args: {
|
|
2780
|
+
test_case_id: string;
|
|
2781
|
+
limit?: number;
|
|
2782
|
+
}) {
|
|
2783
|
+
if (!isValidUUID(args.test_case_id)) {
|
|
2784
|
+
return { error: 'Invalid test_case_id format' };
|
|
2785
|
+
}
|
|
2786
|
+
|
|
2787
|
+
const limit = Math.min(args.limit || 5, 10);
|
|
2788
|
+
|
|
2789
|
+
const { data: recommendations, error } = await supabase.rpc('get_tester_recommendations', {
|
|
2790
|
+
p_test_case_id: args.test_case_id,
|
|
2791
|
+
p_limit: limit,
|
|
2792
|
+
});
|
|
2793
|
+
|
|
2794
|
+
if (error) {
|
|
2795
|
+
return { error: error.message };
|
|
2796
|
+
}
|
|
2797
|
+
|
|
2798
|
+
// Get test case info for context
|
|
2799
|
+
const { data: testCase } = await supabase
|
|
2800
|
+
.from('test_cases')
|
|
2801
|
+
.select('test_key, title, track:qa_tracks(name)')
|
|
2802
|
+
.eq('id', args.test_case_id)
|
|
2803
|
+
.single();
|
|
2804
|
+
|
|
2805
|
+
return {
|
|
2806
|
+
test_case: testCase ? {
|
|
2807
|
+
id: args.test_case_id,
|
|
2808
|
+
test_key: testCase.test_key,
|
|
2809
|
+
title: testCase.title,
|
|
2810
|
+
track: (testCase.track as any)?.name,
|
|
2811
|
+
} : null,
|
|
2812
|
+
recommendations: recommendations?.map((r: any) => ({
|
|
2813
|
+
tester_id: r.tester_id,
|
|
2814
|
+
name: r.tester_name,
|
|
2815
|
+
email: r.tester_email,
|
|
2816
|
+
match_score: r.match_score,
|
|
2817
|
+
reasons: r.match_reasons,
|
|
2818
|
+
})) || [],
|
|
2819
|
+
};
|
|
2820
|
+
}
|
|
2821
|
+
|
|
2822
|
+
async function analyzeCommitForTesting(args: {
|
|
2823
|
+
commit_sha?: string;
|
|
2824
|
+
files_changed: string[];
|
|
2825
|
+
commit_message?: string;
|
|
2826
|
+
record_deployment?: boolean;
|
|
2827
|
+
}) {
|
|
2828
|
+
const filesChanged = args.files_changed || [];
|
|
2829
|
+
|
|
2830
|
+
// Map files to routes using file_route_mapping
|
|
2831
|
+
const { data: mappings } = await supabase
|
|
2832
|
+
.from('file_route_mapping')
|
|
2833
|
+
.select('file_pattern, route, feature, confidence')
|
|
2834
|
+
.eq('project_id', PROJECT_ID);
|
|
2835
|
+
|
|
2836
|
+
const affectedRoutes: Array<{ route: string; feature?: string; confidence: number; matched_files: string[] }> = [];
|
|
2837
|
+
|
|
2838
|
+
for (const mapping of mappings || []) {
|
|
2839
|
+
const matchedFiles = filesChanged.filter(file => {
|
|
2840
|
+
const pattern = mapping.file_pattern
|
|
2841
|
+
.replace(/\*\*/g, '.*')
|
|
2842
|
+
.replace(/\*/g, '[^/]*');
|
|
2843
|
+
return new RegExp(pattern).test(file);
|
|
2844
|
+
});
|
|
2845
|
+
|
|
2846
|
+
if (matchedFiles.length > 0) {
|
|
2847
|
+
affectedRoutes.push({
|
|
2848
|
+
route: mapping.route,
|
|
2849
|
+
feature: mapping.feature || undefined,
|
|
2850
|
+
confidence: mapping.confidence,
|
|
2851
|
+
matched_files: matchedFiles,
|
|
2852
|
+
});
|
|
2853
|
+
}
|
|
2854
|
+
}
|
|
2855
|
+
|
|
2856
|
+
// Get bug history for affected routes
|
|
2857
|
+
const routes = affectedRoutes.map(r => r.route);
|
|
2858
|
+
let bugHistory: any[] = [];
|
|
2859
|
+
|
|
2860
|
+
if (routes.length > 0) {
|
|
2861
|
+
const { data: bugs } = await supabase
|
|
2862
|
+
.from('reports')
|
|
2863
|
+
.select('id, severity, description, route, created_at')
|
|
2864
|
+
.eq('project_id', PROJECT_ID)
|
|
2865
|
+
.eq('report_type', 'bug')
|
|
2866
|
+
.in('route', routes)
|
|
2867
|
+
.gte('created_at', new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString());
|
|
2868
|
+
|
|
2869
|
+
bugHistory = bugs || [];
|
|
2870
|
+
}
|
|
2871
|
+
|
|
2872
|
+
// Calculate risk score
|
|
2873
|
+
let riskScore = 0;
|
|
2874
|
+
riskScore += Math.min(filesChanged.length * 2, 20); // File count
|
|
2875
|
+
riskScore += Math.min(affectedRoutes.length * 5, 30); // Route count
|
|
2876
|
+
riskScore += Math.min(bugHistory.length * 10, 50); // Bug history
|
|
2877
|
+
|
|
2878
|
+
// Generate testing recommendations
|
|
2879
|
+
const recommendations = affectedRoutes.map(r => {
|
|
2880
|
+
const routeBugs = bugHistory.filter(b => b.route === r.route);
|
|
2881
|
+
const priority = routeBugs.length >= 3 ? 'critical' :
|
|
2882
|
+
routeBugs.length >= 1 ? 'high' : 'medium';
|
|
2883
|
+
|
|
2884
|
+
return {
|
|
2885
|
+
route: r.route,
|
|
2886
|
+
feature: r.feature,
|
|
2887
|
+
priority,
|
|
2888
|
+
reason: routeBugs.length > 0
|
|
2889
|
+
? `${routeBugs.length} bug(s) in last 30 days`
|
|
2890
|
+
: 'Code changed in this area',
|
|
2891
|
+
recent_bugs: routeBugs.length,
|
|
2892
|
+
};
|
|
2893
|
+
}).sort((a, b) => {
|
|
2894
|
+
const order = { critical: 0, high: 1, medium: 2 };
|
|
2895
|
+
return order[a.priority as keyof typeof order] - order[b.priority as keyof typeof order];
|
|
2896
|
+
});
|
|
2897
|
+
|
|
2898
|
+
// Optionally record as deployment
|
|
2899
|
+
if (args.record_deployment) {
|
|
2900
|
+
await supabase.rpc('record_deployment', {
|
|
2901
|
+
p_project_id: PROJECT_ID,
|
|
2902
|
+
p_environment: 'production',
|
|
2903
|
+
p_commit_sha: args.commit_sha || null,
|
|
2904
|
+
p_commit_message: args.commit_message || null,
|
|
2905
|
+
p_files_changed: filesChanged,
|
|
2906
|
+
p_webhook_source: 'mcp',
|
|
2907
|
+
});
|
|
2908
|
+
}
|
|
2909
|
+
|
|
2910
|
+
return {
|
|
2911
|
+
commit_sha: args.commit_sha,
|
|
2912
|
+
files_analyzed: filesChanged.length,
|
|
2913
|
+
risk_score: Math.min(riskScore, 100),
|
|
2914
|
+
affected_routes: affectedRoutes,
|
|
2915
|
+
bug_history_summary: {
|
|
2916
|
+
total: bugHistory.length,
|
|
2917
|
+
critical: bugHistory.filter(b => b.severity === 'critical').length,
|
|
2918
|
+
high: bugHistory.filter(b => b.severity === 'high').length,
|
|
2919
|
+
},
|
|
2920
|
+
testing_recommendations: recommendations,
|
|
2921
|
+
deployment_recorded: args.record_deployment || false,
|
|
2922
|
+
};
|
|
2923
|
+
}
|
|
2924
|
+
|
|
2925
|
+
async function getTestingPatterns(args: {
|
|
2926
|
+
feature_type?: string;
|
|
2927
|
+
framework?: string;
|
|
2928
|
+
tracks?: string[];
|
|
2929
|
+
search?: string;
|
|
2930
|
+
}) {
|
|
2931
|
+
let patterns: any[] = [];
|
|
2932
|
+
|
|
2933
|
+
if (args.search) {
|
|
2934
|
+
// Search patterns by keyword
|
|
2935
|
+
const { data, error } = await supabase.rpc('search_patterns', {
|
|
2936
|
+
p_query: args.search,
|
|
2937
|
+
p_limit: 20,
|
|
2938
|
+
});
|
|
2939
|
+
if (!error) patterns = data || [];
|
|
2940
|
+
} else if (args.feature_type) {
|
|
2941
|
+
// Get patterns for feature type
|
|
2942
|
+
const { data, error } = await supabase.rpc('get_patterns_for_feature', {
|
|
2943
|
+
p_category: args.feature_type,
|
|
2944
|
+
p_framework: args.framework || null,
|
|
2945
|
+
p_tracks: args.tracks || null,
|
|
2946
|
+
});
|
|
2947
|
+
if (!error) patterns = data || [];
|
|
2948
|
+
} else {
|
|
2949
|
+
// Get all patterns (limited)
|
|
2950
|
+
const { data, error } = await supabase
|
|
2951
|
+
.from('qa_patterns')
|
|
2952
|
+
.select('*')
|
|
2953
|
+
.eq('is_active', true)
|
|
2954
|
+
.order('severity')
|
|
2955
|
+
.limit(30);
|
|
2956
|
+
if (!error) patterns = data || [];
|
|
2957
|
+
}
|
|
2958
|
+
|
|
2959
|
+
const summary = {
|
|
2960
|
+
total: patterns.length,
|
|
2961
|
+
by_severity: {
|
|
2962
|
+
critical: patterns.filter(p => p.severity === 'critical').length,
|
|
2963
|
+
high: patterns.filter(p => p.severity === 'high').length,
|
|
2964
|
+
medium: patterns.filter(p => p.severity === 'medium').length,
|
|
2965
|
+
low: patterns.filter(p => p.severity === 'low').length,
|
|
2966
|
+
},
|
|
2967
|
+
by_track: patterns.reduce((acc, p) => {
|
|
2968
|
+
acc[p.track] = (acc[p.track] || 0) + 1;
|
|
2969
|
+
return acc;
|
|
2970
|
+
}, {} as Record<string, number>),
|
|
2971
|
+
};
|
|
2972
|
+
|
|
2973
|
+
return {
|
|
2974
|
+
patterns: patterns.map(p => ({
|
|
2975
|
+
title: p.title,
|
|
2976
|
+
category: p.category,
|
|
2977
|
+
track: p.track,
|
|
2978
|
+
severity: p.severity,
|
|
2979
|
+
description: p.description,
|
|
2980
|
+
why_it_happens: p.why_it_happens,
|
|
2981
|
+
suggested_tests: p.suggested_tests,
|
|
2982
|
+
common_fix: p.common_fix,
|
|
2983
|
+
source: p.source,
|
|
2984
|
+
frameworks: p.frameworks,
|
|
2985
|
+
})),
|
|
2986
|
+
summary,
|
|
2987
|
+
note: 'Patterns are from public knowledge (OWASP, WCAG, framework docs), not customer data.',
|
|
2988
|
+
};
|
|
2989
|
+
}
|
|
2990
|
+
|
|
2412
2991
|
async function analyzeChangesForTests(args: {
|
|
2413
2992
|
changed_files: string[];
|
|
2414
2993
|
change_type: 'feature' | 'bugfix' | 'refactor' | 'ui_change' | 'api_change' | 'config';
|
|
@@ -2837,7 +3416,28 @@ async function createBugReport(args: {
|
|
|
2837
3416
|
codeContext.suggested_fix = args.suggested_fix;
|
|
2838
3417
|
}
|
|
2839
3418
|
|
|
2840
|
-
|
|
3419
|
+
// Find a reporter_id: try to get the project owner or first tester
|
|
3420
|
+
let reporterId: string | null = null;
|
|
3421
|
+
const { data: project } = await supabase
|
|
3422
|
+
.from('projects')
|
|
3423
|
+
.select('owner_id')
|
|
3424
|
+
.eq('id', PROJECT_ID)
|
|
3425
|
+
.single();
|
|
3426
|
+
if (project?.owner_id) {
|
|
3427
|
+
reporterId = project.owner_id;
|
|
3428
|
+
} else {
|
|
3429
|
+
// Fallback: use the first tester for this project
|
|
3430
|
+
const { data: testers } = await supabase
|
|
3431
|
+
.from('testers')
|
|
3432
|
+
.select('id')
|
|
3433
|
+
.eq('project_id', PROJECT_ID)
|
|
3434
|
+
.limit(1);
|
|
3435
|
+
if (testers && testers.length > 0) {
|
|
3436
|
+
reporterId = testers[0].id;
|
|
3437
|
+
}
|
|
3438
|
+
}
|
|
3439
|
+
|
|
3440
|
+
const report: Record<string, unknown> = {
|
|
2841
3441
|
project_id: PROJECT_ID,
|
|
2842
3442
|
report_type: 'bug',
|
|
2843
3443
|
title: args.title,
|
|
@@ -2856,6 +3456,10 @@ async function createBugReport(args: {
|
|
|
2856
3456
|
code_context: codeContext,
|
|
2857
3457
|
};
|
|
2858
3458
|
|
|
3459
|
+
if (reporterId) {
|
|
3460
|
+
report.reporter_id = reporterId;
|
|
3461
|
+
}
|
|
3462
|
+
|
|
2859
3463
|
const { data, error } = await supabase
|
|
2860
3464
|
.from('reports')
|
|
2861
3465
|
.insert(report)
|
|
@@ -3986,6 +4590,25 @@ async function main() {
|
|
|
3986
4590
|
case 'complete_fix_request':
|
|
3987
4591
|
result = await completeFixRequest(args as any);
|
|
3988
4592
|
break;
|
|
4593
|
+
// === SPRINT 5: NEW MCP TOOLS ===
|
|
4594
|
+
case 'get_qa_sessions':
|
|
4595
|
+
result = await getQASessions(args as any);
|
|
4596
|
+
break;
|
|
4597
|
+
case 'get_qa_alerts':
|
|
4598
|
+
result = await getQAAlerts(args as any);
|
|
4599
|
+
break;
|
|
4600
|
+
case 'get_deployment_analysis':
|
|
4601
|
+
result = await getDeploymentAnalysis(args as any);
|
|
4602
|
+
break;
|
|
4603
|
+
case 'get_tester_recommendations':
|
|
4604
|
+
result = await getTesterRecommendations(args as any);
|
|
4605
|
+
break;
|
|
4606
|
+
case 'analyze_commit_for_testing':
|
|
4607
|
+
result = await analyzeCommitForTesting(args as any);
|
|
4608
|
+
break;
|
|
4609
|
+
case 'get_testing_patterns':
|
|
4610
|
+
result = await getTestingPatterns(args as any);
|
|
4611
|
+
break;
|
|
3989
4612
|
default:
|
|
3990
4613
|
return {
|
|
3991
4614
|
content: [{ type: 'text', text: `Unknown tool: ${name}` }],
|