99 ConfigDiffResult ,
1010} from '../config/config-diff.service' ;
1111import { AnalyzerService } from '../analyzer/analyzer.service' ;
12+ import { SkillRecommendationService } from '../skill/skill-recommendation.service' ;
13+ import type { RecommendSkillsResult } from '../skill/skill-recommendation.types' ;
1214
1315// Handler function type for MCP request handlers
1416type McpHandler = ( request : unknown ) => Promise < unknown > ;
@@ -139,6 +141,21 @@ const createMockAnalyzerService = (): Partial<AnalyzerService> => ({
139141 analyzeProject : vi . fn ( ) . mockResolvedValue ( mockAnalysis ) ,
140142} ) ;
141143
144+ const createMockSkillRecommendationService =
145+ ( ) : Partial < SkillRecommendationService > => ( {
146+ recommendSkills : vi . fn ( ) . mockReturnValue ( {
147+ recommendations : [
148+ {
149+ skillName : 'systematic-debugging' ,
150+ confidence : 'high' ,
151+ matchedPatterns : [ 'bug' , 'debug' ] ,
152+ description : 'Systematic approach to debugging' ,
153+ } ,
154+ ] ,
155+ originalPrompt : 'I have a bug in my code' ,
156+ } as RecommendSkillsResult ) ,
157+ } ) ;
158+
142159// Import after mocks
143160import { McpService } from './mcp.service' ;
144161
@@ -148,6 +165,7 @@ describe('McpService', () => {
148165 let mockConfigService : Partial < ConfigService > ;
149166 let mockConfigDiffService : Partial < ConfigDiffService > ;
150167 let mockAnalyzerService : Partial < AnalyzerService > ;
168+ let mockSkillRecommendationService : Partial < SkillRecommendationService > ;
151169
152170 const testConfig : CodingBuddyConfig = {
153171 language : 'ko' ,
@@ -167,13 +185,15 @@ describe('McpService', () => {
167185 mockConfigService = createMockConfigService ( testConfig ) ;
168186 mockConfigDiffService = createMockConfigDiffService ( ) ;
169187 mockAnalyzerService = createMockAnalyzerService ( ) ;
188+ mockSkillRecommendationService = createMockSkillRecommendationService ( ) ;
170189
171190 const mcpService = new McpService (
172191 mockRulesService as RulesService ,
173192 mockKeywordService as KeywordService ,
174193 mockConfigService as ConfigService ,
175194 mockConfigDiffService as ConfigDiffService ,
176195 mockAnalyzerService as AnalyzerService ,
196+ mockSkillRecommendationService as SkillRecommendationService ,
177197 ) ;
178198 mcpService . onModuleInit ( ) ;
179199 } ) ;
@@ -384,6 +404,7 @@ describe('McpService', () => {
384404 emptyConfigService as ConfigService ,
385405 mockConfigDiffService as ConfigDiffService ,
386406 mockAnalyzerService as AnalyzerService ,
407+ mockSkillRecommendationService as SkillRecommendationService ,
387408 ) ;
388409 serviceWithEmptyConfig . onModuleInit ( ) ;
389410
@@ -432,6 +453,7 @@ describe('McpService', () => {
432453 fullConfigService as ConfigService ,
433454 mockConfigDiffService as ConfigDiffService ,
434455 mockAnalyzerService as AnalyzerService ,
456+ mockSkillRecommendationService as SkillRecommendationService ,
435457 ) ;
436458 service . onModuleInit ( ) ;
437459
@@ -495,6 +517,7 @@ describe('McpService', () => {
495517 failingConfigService as ConfigService ,
496518 mockConfigDiffService as ConfigDiffService ,
497519 mockAnalyzerService as AnalyzerService ,
520+ mockSkillRecommendationService as SkillRecommendationService ,
498521 ) ;
499522 service . onModuleInit ( ) ;
500523
@@ -565,6 +588,7 @@ describe('McpService', () => {
565588 failingConfigService as ConfigService ,
566589 mockConfigDiffService as ConfigDiffService ,
567590 mockAnalyzerService as AnalyzerService ,
591+ mockSkillRecommendationService as SkillRecommendationService ,
568592 ) ;
569593 service . onModuleInit ( ) ;
570594
@@ -620,6 +644,7 @@ describe('McpService', () => {
620644 mockConfigService as ConfigService ,
621645 mockConfigDiffService as ConfigDiffService ,
622646 mockAnalyzerService as AnalyzerService ,
647+ mockSkillRecommendationService as SkillRecommendationService ,
623648 ) ;
624649
625650 // Should not throw
@@ -635,10 +660,276 @@ describe('McpService', () => {
635660 mockConfigService as ConfigService ,
636661 mockConfigDiffService as ConfigDiffService ,
637662 mockAnalyzerService as AnalyzerService ,
663+ mockSkillRecommendationService as SkillRecommendationService ,
638664 ) ;
639665
640666 const server = service . getServer ( ) ;
641667 expect ( server ) . toBeDefined ( ) ;
642668 } ) ;
643669 } ) ;
670+
671+ // ============================================================================
672+ // recommend_skills Tool Tests (RED phase - tests should FAIL)
673+ // ============================================================================
674+
675+ describe ( 'recommend_skills tool' , ( ) => {
676+ describe ( 'Tool Registration' , ( ) => {
677+ it ( 'should list recommend_skills tool' , async ( ) => {
678+ const handler = handlers . get ( 'tools/list' ) ;
679+ expect ( handler ) . toBeDefined ( ) ;
680+
681+ const result = ( await handler ! ( { } ) ) as {
682+ tools : { name : string ; description : string ; inputSchema : object } [ ] ;
683+ } ;
684+ const recommendTool = result . tools . find (
685+ t => t . name === 'recommend_skills' ,
686+ ) ;
687+
688+ expect ( recommendTool ) . toBeDefined ( ) ;
689+ expect ( recommendTool ! . description ) . toContain ( 'skill' ) ;
690+ } ) ;
691+
692+ it ( 'should have correct inputSchema for recommend_skills' , async ( ) => {
693+ const handler = handlers . get ( 'tools/list' ) ;
694+ expect ( handler ) . toBeDefined ( ) ;
695+
696+ const result = ( await handler ! ( { } ) ) as {
697+ tools : {
698+ name : string ;
699+ inputSchema : { properties : object ; required : string [ ] } ;
700+ } [ ] ;
701+ } ;
702+ const recommendTool = result . tools . find (
703+ t => t . name === 'recommend_skills' ,
704+ ) ;
705+
706+ expect ( recommendTool ) . toBeDefined ( ) ;
707+ expect ( recommendTool ! . inputSchema . properties ) . toHaveProperty ( 'prompt' ) ;
708+ expect ( recommendTool ! . inputSchema . required ) . toContain ( 'prompt' ) ;
709+ } ) ;
710+ } ) ;
711+
712+ describe ( 'Basic Functionality' , ( ) => {
713+ it ( 'should return recommendations for debugging prompt' , async ( ) => {
714+ const handler = handlers . get ( 'tools/call' ) ;
715+ expect ( handler ) . toBeDefined ( ) ;
716+
717+ const result = ( await handler ! ( {
718+ params : {
719+ name : 'recommend_skills' ,
720+ arguments : { prompt : 'I have a bug in my code' } ,
721+ } ,
722+ } ) ) as { content : { type : string ; text : string } [ ] } ;
723+
724+ expect ( result . content ) . toHaveLength ( 1 ) ;
725+ expect ( result . content [ 0 ] . type ) . toBe ( 'text' ) ;
726+
727+ const parsed = JSON . parse ( result . content [ 0 ] . text ) ;
728+ expect ( parsed . recommendations ) . toBeDefined ( ) ;
729+ expect ( parsed . recommendations . length ) . toBeGreaterThan ( 0 ) ;
730+ expect ( parsed . recommendations [ 0 ] . skillName ) . toBe (
731+ 'systematic-debugging' ,
732+ ) ;
733+ expect (
734+ mockSkillRecommendationService . recommendSkills ,
735+ ) . toHaveBeenCalledWith ( 'I have a bug in my code' ) ;
736+ } ) ;
737+
738+ it ( 'should return empty recommendations for unrelated prompt' , async ( ) => {
739+ vi . mocked (
740+ mockSkillRecommendationService . recommendSkills ! ,
741+ ) . mockReturnValue ( {
742+ recommendations : [ ] ,
743+ originalPrompt : 'What is the weather today?' ,
744+ } ) ;
745+
746+ const handler = handlers . get ( 'tools/call' ) ;
747+ expect ( handler ) . toBeDefined ( ) ;
748+
749+ const result = ( await handler ! ( {
750+ params : {
751+ name : 'recommend_skills' ,
752+ arguments : { prompt : 'What is the weather today?' } ,
753+ } ,
754+ } ) ) as { content : { type : string ; text : string } [ ] } ;
755+
756+ const parsed = JSON . parse ( result . content [ 0 ] . text ) ;
757+ expect ( parsed . recommendations ) . toEqual ( [ ] ) ;
758+ } ) ;
759+
760+ it ( 'should include originalPrompt in response' , async ( ) => {
761+ const handler = handlers . get ( 'tools/call' ) ;
762+ expect ( handler ) . toBeDefined ( ) ;
763+
764+ const result = ( await handler ! ( {
765+ params : {
766+ name : 'recommend_skills' ,
767+ arguments : { prompt : 'I have a bug in my code' } ,
768+ } ,
769+ } ) ) as { content : { type : string ; text : string } [ ] } ;
770+
771+ const parsed = JSON . parse ( result . content [ 0 ] . text ) ;
772+ expect ( parsed . originalPrompt ) . toBe ( 'I have a bug in my code' ) ;
773+ } ) ;
774+ } ) ;
775+
776+ describe ( 'Multi-language Support' , ( ) => {
777+ it ( 'should work with English prompt' , async ( ) => {
778+ vi . mocked (
779+ mockSkillRecommendationService . recommendSkills ! ,
780+ ) . mockReturnValue ( {
781+ recommendations : [
782+ {
783+ skillName : 'systematic-debugging' ,
784+ confidence : 'high' ,
785+ matchedPatterns : [ 'bug' ] ,
786+ description : 'Systematic approach to debugging' ,
787+ } ,
788+ ] ,
789+ originalPrompt : 'There is a bug in the login feature' ,
790+ } ) ;
791+
792+ const handler = handlers . get ( 'tools/call' ) ;
793+ expect ( handler ) . toBeDefined ( ) ;
794+
795+ const result = ( await handler ! ( {
796+ params : {
797+ name : 'recommend_skills' ,
798+ arguments : { prompt : 'There is a bug in the login feature' } ,
799+ } ,
800+ } ) ) as { content : { type : string ; text : string } [ ] } ;
801+
802+ const parsed = JSON . parse ( result . content [ 0 ] . text ) ;
803+ expect ( parsed . recommendations ) . toHaveLength ( 1 ) ;
804+ expect ( parsed . recommendations [ 0 ] . skillName ) . toBe (
805+ 'systematic-debugging' ,
806+ ) ;
807+ } ) ;
808+
809+ it ( 'should work with Korean prompt' , async ( ) => {
810+ vi . mocked (
811+ mockSkillRecommendationService . recommendSkills ! ,
812+ ) . mockReturnValue ( {
813+ recommendations : [
814+ {
815+ skillName : 'systematic-debugging' ,
816+ confidence : 'high' ,
817+ matchedPatterns : [ '버그' ] ,
818+ description : 'Systematic approach to debugging' ,
819+ } ,
820+ ] ,
821+ originalPrompt : '로그인에 버그가 있습니다' ,
822+ } ) ;
823+
824+ const handler = handlers . get ( 'tools/call' ) ;
825+ expect ( handler ) . toBeDefined ( ) ;
826+
827+ const result = ( await handler ! ( {
828+ params : {
829+ name : 'recommend_skills' ,
830+ arguments : { prompt : '로그인에 버그가 있습니다' } ,
831+ } ,
832+ } ) ) as { content : { type : string ; text : string } [ ] } ;
833+
834+ const parsed = JSON . parse ( result . content [ 0 ] . text ) ;
835+ expect ( parsed . recommendations ) . toHaveLength ( 1 ) ;
836+ expect (
837+ mockSkillRecommendationService . recommendSkills ,
838+ ) . toHaveBeenCalledWith ( '로그인에 버그가 있습니다' ) ;
839+ } ) ;
840+
841+ it ( 'should work with Japanese prompt' , async ( ) => {
842+ vi . mocked (
843+ mockSkillRecommendationService . recommendSkills ! ,
844+ ) . mockReturnValue ( {
845+ recommendations : [
846+ {
847+ skillName : 'systematic-debugging' ,
848+ confidence : 'high' ,
849+ matchedPatterns : [ 'バグ' ] ,
850+ description : 'Systematic approach to debugging' ,
851+ } ,
852+ ] ,
853+ originalPrompt : 'ログイン機能にバグがある' ,
854+ } ) ;
855+
856+ const handler = handlers . get ( 'tools/call' ) ;
857+ expect ( handler ) . toBeDefined ( ) ;
858+
859+ const result = ( await handler ! ( {
860+ params : {
861+ name : 'recommend_skills' ,
862+ arguments : { prompt : 'ログイン機能にバグがある' } ,
863+ } ,
864+ } ) ) as { content : { type : string ; text : string } [ ] } ;
865+
866+ const parsed = JSON . parse ( result . content [ 0 ] . text ) ;
867+ expect ( parsed . recommendations ) . toHaveLength ( 1 ) ;
868+ expect (
869+ mockSkillRecommendationService . recommendSkills ,
870+ ) . toHaveBeenCalledWith ( 'ログイン機能にバグがある' ) ;
871+ } ) ;
872+ } ) ;
873+
874+ describe ( 'Error Handling' , ( ) => {
875+ it ( 'should throw error when prompt is missing' , async ( ) => {
876+ const handler = handlers . get ( 'tools/call' ) ;
877+ expect ( handler ) . toBeDefined ( ) ;
878+
879+ const result = ( await handler ! ( {
880+ params : {
881+ name : 'recommend_skills' ,
882+ arguments : { } ,
883+ } ,
884+ } ) ) as { isError : boolean ; content : { text : string } [ ] } ;
885+
886+ expect ( result . isError ) . toBe ( true ) ;
887+ expect ( result . content [ 0 ] . text ) . toContain ( 'prompt' ) ;
888+ } ) ;
889+
890+ it ( 'should return empty recommendations for empty prompt' , async ( ) => {
891+ vi . mocked (
892+ mockSkillRecommendationService . recommendSkills ! ,
893+ ) . mockReturnValue ( {
894+ recommendations : [ ] ,
895+ originalPrompt : '' ,
896+ } ) ;
897+
898+ const handler = handlers . get ( 'tools/call' ) ;
899+ expect ( handler ) . toBeDefined ( ) ;
900+
901+ const result = ( await handler ! ( {
902+ params : {
903+ name : 'recommend_skills' ,
904+ arguments : { prompt : '' } ,
905+ } ,
906+ } ) ) as { content : { type : string ; text : string } [ ] } ;
907+
908+ const parsed = JSON . parse ( result . content [ 0 ] . text ) ;
909+ expect ( parsed . recommendations ) . toEqual ( [ ] ) ;
910+ expect ( parsed . originalPrompt ) . toBe ( '' ) ;
911+ } ) ;
912+
913+ it ( 'should return error when service throws' , async ( ) => {
914+ vi . mocked (
915+ mockSkillRecommendationService . recommendSkills ! ,
916+ ) . mockImplementation ( ( ) => {
917+ throw new Error ( 'Service error' ) ;
918+ } ) ;
919+
920+ const handler = handlers . get ( 'tools/call' ) ;
921+ expect ( handler ) . toBeDefined ( ) ;
922+
923+ const result = ( await handler ! ( {
924+ params : {
925+ name : 'recommend_skills' ,
926+ arguments : { prompt : 'test prompt' } ,
927+ } ,
928+ } ) ) as { isError : boolean ; content : { text : string } [ ] } ;
929+
930+ expect ( result . isError ) . toBe ( true ) ;
931+ expect ( result . content [ 0 ] . text ) . toContain ( 'Failed to recommend skills' ) ;
932+ } ) ;
933+ } ) ;
934+ } ) ;
644935} ) ;
0 commit comments