@bbearai/mcp-server 0.3.0 → 0.3.4
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-api.js +2 -2
- package/dist/index.js +554 -8
- package/package.json +1 -1
- package/src/index-api.ts +2 -2
- package/src/index.ts +633 -10
package/src/index.ts
CHANGED
|
@@ -144,7 +144,7 @@ const tools = [
|
|
|
144
144
|
enum: ['new', 'triaging', 'confirmed', 'in_progress', 'fixed', 'resolved', 'verified', 'wont_fix', 'duplicate'],
|
|
145
145
|
description: 'The new status for the report',
|
|
146
146
|
},
|
|
147
|
-
|
|
147
|
+
resolution_notes: {
|
|
148
148
|
type: 'string',
|
|
149
149
|
description: 'Optional resolution notes when marking as resolved',
|
|
150
150
|
},
|
|
@@ -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,
|
|
@@ -868,15 +1024,15 @@ async function searchReports(args: { query?: string; route?: string }) {
|
|
|
868
1024
|
async function updateReportStatus(args: {
|
|
869
1025
|
report_id: string;
|
|
870
1026
|
status: string;
|
|
871
|
-
|
|
1027
|
+
resolution_notes?: string;
|
|
872
1028
|
}) {
|
|
873
1029
|
if (!isValidUUID(args.report_id)) {
|
|
874
1030
|
return { error: 'Invalid report_id format' };
|
|
875
1031
|
}
|
|
876
1032
|
|
|
877
1033
|
const updates: Record<string, unknown> = { status: args.status };
|
|
878
|
-
if (args.
|
|
879
|
-
updates.
|
|
1034
|
+
if (args.resolution_notes) {
|
|
1035
|
+
updates.resolution_notes = args.resolution_notes;
|
|
880
1036
|
}
|
|
881
1037
|
|
|
882
1038
|
const { error } = await supabase
|
|
@@ -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)
|
|
@@ -3003,7 +3607,7 @@ async function markFixedWithCommit(args: {
|
|
|
3003
3607
|
const updates = {
|
|
3004
3608
|
status: 'resolved',
|
|
3005
3609
|
resolved_at: new Date().toISOString(),
|
|
3006
|
-
|
|
3610
|
+
resolution_notes: args.resolution_notes || `Fixed in commit ${args.commit_sha.slice(0, 7)}`,
|
|
3007
3611
|
code_context: {
|
|
3008
3612
|
...existingContext,
|
|
3009
3613
|
fix: {
|
|
@@ -3285,7 +3889,7 @@ async function createRegressionTest(args: {
|
|
|
3285
3889
|
{
|
|
3286
3890
|
stepNumber: 3,
|
|
3287
3891
|
action: 'Verify the fix is working',
|
|
3288
|
-
expectedResult: report.
|
|
3892
|
+
expectedResult: report.resolution_notes || 'Feature works as expected',
|
|
3289
3893
|
},
|
|
3290
3894
|
],
|
|
3291
3895
|
expected_result: `The bug "${report.title}" should not recur`,
|
|
@@ -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}` }],
|