11import * as fs from "fs-extra" ;
2- import * as https from "https" ;
32import * as os from "os" ;
43import * as path from "path" ;
54import { SkillManager } from "../../lib/SkillManager" ;
@@ -8,7 +7,6 @@ import { EnvironmentSelector } from "../../lib/EnvironmentSelector";
87import { GlobalConfigManager } from "../../lib/GlobalConfig" ;
98import * as gitUtil from "../../util/git" ;
109import * as skillUtil from "../../util/skill" ;
11- import { EventEmitter } from "events" ;
1210
1311jest . mock ( "fs-extra" , ( ) => ( {
1412 pathExists : jest . fn ( ) ,
@@ -19,8 +17,8 @@ jest.mock("fs-extra", () => ({
1917 readdir : jest . fn ( ) ,
2018 realpath : jest . fn ( ) ,
2119 readJson : jest . fn ( ) ,
20+ writeJson : jest . fn ( ) ,
2221} ) ) ;
23- jest . mock ( "https" ) ;
2422jest . mock ( "../../lib/Config" ) ;
2523jest . mock ( "../../lib/EnvironmentSelector" ) ;
2624jest . mock ( "../../lib/GlobalConfig" ) ;
@@ -39,7 +37,6 @@ jest.mock("ora", () => {
3937} ) ;
4038
4139const mockedFs = fs as jest . Mocked < typeof fs > ;
42- const mockedHttps = https as jest . Mocked < typeof https > ;
4340const MockedConfigManager = ConfigManager as jest . MockedClass <
4441 typeof ConfigManager
4542> ;
@@ -52,6 +49,17 @@ const MockedGlobalConfigManager = GlobalConfigManager as jest.MockedClass<
5249const mockedGitUtil = gitUtil as jest . Mocked < typeof gitUtil > ;
5350const mockedSkillUtil = skillUtil as jest . Mocked < typeof skillUtil > ;
5451
52+ function mockFetch ( response : any ) {
53+ global . fetch = jest . fn ( ) . mockResolvedValue ( {
54+ ok : true ,
55+ json : ( ) => Promise . resolve ( response )
56+ } ) ;
57+ }
58+
59+ function mockFetchError ( error : Error ) {
60+ global . fetch = jest . fn ( ) . mockRejectedValue ( error ) ;
61+ }
62+
5563describe ( "SkillManager" , ( ) => {
5664 let skillManager : SkillManager ;
5765 let mockConfigManager : jest . Mocked < ConfigManager > ;
@@ -97,7 +105,7 @@ describe("SkillManager", () => {
97105 ) ;
98106
99107 beforeEach ( ( ) => {
100- mockHttpsGet ( {
108+ mockFetch ( {
101109 registries : {
102110 [ mockRegistryId ] : mockGitUrl ,
103111 } ,
@@ -127,33 +135,43 @@ describe("SkillManager", () => {
127135 expect ( mockedGitUtil . ensureGitInstalled ) . toHaveBeenCalled ( ) ;
128136 } ) ;
129137
130- it ( "should fetch registry from GitHub" , async ( ) => {
138+ it ( "should fetch registry using fetch API" , async ( ) => {
139+ const originalFetch = global . fetch ;
140+ global . fetch = jest . fn ( ) . mockResolvedValue ( {
141+ ok : true ,
142+ json : ( ) => Promise . resolve ( { registries : { [ mockRegistryId ] : mockGitUrl } } )
143+ } ) ;
144+
131145 await skillManager . addSkill ( mockRegistryId , mockSkillName ) ;
132146
133- expect ( mockedHttps . get ) . toHaveBeenCalled ( ) ;
134- const getCall = ( mockedHttps . get as jest . Mock ) . mock . calls [ 0 ] [ 0 ] ;
135- expect ( getCall ) . toContain ( "raw.githubusercontent.com" ) ;
136- expect ( getCall ) . toContain ( "registry.json" ) ;
147+ expect ( global . fetch ) . toHaveBeenCalledWith (
148+ expect . stringContaining ( "registry.json" )
149+ ) ;
150+
151+ global . fetch = originalFetch ;
137152 } ) ;
138153
139154 it ( "should throw error if registry ID not found" , async ( ) => {
140- mockHttpsGet ( {
141- registries : {
142- "other/repo" : "https://github.com/other/repo.git" ,
143- } ,
155+ const originalFetch = global . fetch ;
156+ global . fetch = jest . fn ( ) . mockResolvedValue ( {
157+ ok : true ,
158+ json : ( ) => Promise . resolve ( {
159+ registries : {
160+ "other/repo" : "https://github.com/other/repo.git" ,
161+ } ,
162+ } )
144163 } ) ;
145164
146165 ( mockedFs . pathExists as any ) . mockImplementation ( ( checkPath : string ) => {
147- if ( checkPath . includes ( mockRegistryId ) ) {
148- return Promise . resolve ( false ) ;
149- }
150-
166+ if ( checkPath . includes ( mockRegistryId ) ) return Promise . resolve ( false ) ;
151167 return Promise . resolve ( true ) ;
152168 } ) ;
153169
154170 await expect (
155171 skillManager . addSkill ( mockRegistryId , mockSkillName ) ,
156172 ) . rejects . toThrow ( `Registry "${ mockRegistryId } " not found` ) ;
173+
174+ global . fetch = originalFetch ;
157175 } ) ;
158176
159177 it ( "should prefer custom registry URL over default" , async ( ) => {
@@ -203,7 +221,7 @@ describe("SkillManager", () => {
203221 const realGlobalConfigManager = new RealGlobalConfigManager ( ) ;
204222
205223 mockGlobalConfigManager . getSkillRegistries . mockResolvedValue ( { } ) ;
206- mockHttpsGet ( { registries : { } } ) ;
224+ mockFetch ( { registries : { } } ) ;
207225
208226 ( mockedFs . pathExists as any ) . mockImplementation ( ( checkPath : string ) => {
209227 if ( checkPath . includes ( `${ path . sep } skills${ path . sep } ${ mockSkillName } ` ) ) {
@@ -245,7 +263,6 @@ describe("SkillManager", () => {
245263 } ) ;
246264
247265 it ( "should use cached registry when remote fetch fails" , async ( ) => {
248- mockHttpsGetError ( new Error ( "offline" ) ) ;
249266 mockGlobalConfigManager . getSkillRegistries . mockResolvedValue ( { } ) ;
250267
251268 ( mockedFs . pathExists as any ) . mockImplementation ( ( checkPath : string ) => {
@@ -827,33 +844,120 @@ describe("SkillManager", () => {
827844 expect ( result . failed ) . toBe ( 1 ) ;
828845 } ) ;
829846 } ) ;
830- } ) ;
831847
832- function mockHttpsGet ( responseData : any ) {
833- ( mockedHttps . get as jest . Mock ) . mockImplementation (
834- ( url : string , callback : any ) => {
835- const response = new EventEmitter ( ) as any ;
836- response . statusCode = 200 ;
848+ describe ( "findSkills" , ( ) => {
849+ const mockSkillIndex = {
850+ meta : {
851+ version : 1 ,
852+ createdAt : Date . now ( ) - 1000 ,
853+ updatedAt : Date . now ( ) - 1000 ,
854+ registriesHash : "repo1|repo2" ,
855+ registryHeads : {
856+ "anthropics/skills" : "abc123" ,
857+ "vercel-labs/agent-skills" : "def456" ,
858+ } ,
859+ } ,
860+ skills : [
861+ {
862+ name : "typescript-helper" ,
863+ registry : "anthropics/skills" ,
864+ path : "skills/typescript-helper" ,
865+ description : "TypeScript development utilities" ,
866+ lastIndexed : Date . now ( ) ,
867+ } ,
868+ {
869+ name : "react-components" ,
870+ registry : "vercel-labs/agent-skills" ,
871+ path : "skills/react-components" ,
872+ description : "Build React components with best practices" ,
873+ lastIndexed : Date . now ( ) ,
874+ } ,
875+ {
876+ name : "frontend-design" ,
877+ registry : "anthropics/skills" ,
878+ path : "skills/frontend-design" ,
879+ description : "Frontend design patterns and components" ,
880+ lastIndexed : Date . now ( ) ,
881+ } ,
882+ ] ,
883+ } ;
884+
885+ beforeEach ( ( ) => {
886+ mockGlobalConfigManager . getSkillRegistries . mockResolvedValue ( { } ) ;
837887
838- process . nextTick ( ( ) => {
839- callback ( response ) ;
840- response . emit ( "data" , JSON . stringify ( responseData ) ) ;
841- response . emit ( "end" ) ;
888+ mockedGitUtil . fetchGitHead . mockImplementation ( async ( url : string ) => {
889+ if ( url . includes ( 'anthropics' ) ) return 'abc123' ;
890+ if ( url . includes ( 'vercel' ) ) return 'def456' ;
891+ return '000000' ;
842892 } ) ;
893+ } ) ;
843894
844- return {
845- on : jest . fn ( ) ,
846- } ;
847- } ,
848- ) ;
849- }
895+ it ( "should throw error if keyword is empty" , async ( ) => {
896+ await expect ( skillManager . findSkills ( "" ) ) . rejects . toThrow ( "Keyword is required" ) ;
897+ await expect ( skillManager . findSkills ( " " ) ) . rejects . toThrow ( "Keyword is required" ) ;
898+ } ) ;
850899
851- function mockHttpsGetError ( error : Error ) {
852- ( mockedHttps . get as jest . Mock ) . mockImplementation ( ( ) => ( {
853- on : ( event : string , handler : ( err : Error ) => void ) => {
854- if ( event === "error" ) {
855- process . nextTick ( ( ) => handler ( error ) ) ;
856- }
857- } ,
858- } ) ) ;
859- }
900+ it ( "should load and use fresh index when available" , async ( ) => {
901+ ( mockedFs . pathExists as any ) . mockResolvedValue ( true ) ;
902+ ( mockedFs . readJson as any ) . mockResolvedValue ( mockSkillIndex ) ;
903+
904+ const results = await skillManager . findSkills ( "typescript" ) ;
905+
906+ expect ( mockedFs . readJson ) . toHaveBeenCalledWith (
907+ expect . stringContaining ( "skills.json" )
908+ ) ;
909+ expect ( results ) . toHaveLength ( 1 ) ;
910+ expect ( results [ 0 ] . name ) . toBe ( "typescript-helper" ) ;
911+ } ) ;
912+
913+ it ( "should search by skill name" , async ( ) => {
914+ ( mockedFs . pathExists as any ) . mockResolvedValue ( true ) ;
915+ ( mockedFs . readJson as any ) . mockResolvedValue ( mockSkillIndex ) ;
916+
917+ const results = await skillManager . findSkills ( "react" ) ;
918+
919+ expect ( results ) . toHaveLength ( 1 ) ;
920+ expect ( results [ 0 ] . name ) . toBe ( "react-components" ) ;
921+ } ) ;
922+
923+ it ( "should search by description" , async ( ) => {
924+ ( mockedFs . pathExists as any ) . mockResolvedValue ( true ) ;
925+ ( mockedFs . readJson as any ) . mockResolvedValue ( mockSkillIndex ) ;
926+
927+ const results = await skillManager . findSkills ( "design" ) ;
928+
929+ expect ( results ) . toHaveLength ( 1 ) ;
930+ expect ( results [ 0 ] . name ) . toBe ( "frontend-design" ) ;
931+ } ) ;
932+
933+ it ( "should be case-insensitive" , async ( ) => {
934+ ( mockedFs . pathExists as any ) . mockResolvedValue ( true ) ;
935+ ( mockedFs . readJson as any ) . mockResolvedValue ( mockSkillIndex ) ;
936+
937+ const results = await skillManager . findSkills ( "TYPESCRIPT" ) ;
938+
939+ expect ( results ) . toHaveLength ( 1 ) ;
940+ expect ( results [ 0 ] . name ) . toBe ( "typescript-helper" ) ;
941+ } ) ;
942+
943+ it ( "should return multiple matches" , async ( ) => {
944+ ( mockedFs . pathExists as any ) . mockResolvedValue ( true ) ;
945+ ( mockedFs . readJson as any ) . mockResolvedValue ( mockSkillIndex ) ;
946+
947+ const results = await skillManager . findSkills ( "component" ) ;
948+
949+ expect ( results ) . toHaveLength ( 2 ) ;
950+ expect ( results . map ( r => r . name ) ) . toContain ( "react-components" ) ;
951+ expect ( results . map ( r => r . name ) ) . toContain ( "frontend-design" ) ;
952+ } ) ;
953+
954+ it ( "should return empty array when no matches found" , async ( ) => {
955+ ( mockedFs . pathExists as any ) . mockResolvedValue ( true ) ;
956+ ( mockedFs . readJson as any ) . mockResolvedValue ( mockSkillIndex ) ;
957+
958+ const results = await skillManager . findSkills ( "nonexistent" ) ;
959+
960+ expect ( results ) . toEqual ( [ ] ) ;
961+ } ) ;
962+ } ) ;
963+ } ) ;
0 commit comments