@@ -387,7 +387,7 @@ Updated prompt.`;
387387 it ( "无效 SKILL.md 应抛出异常" , async ( ) => {
388388 const { service } = createTestService ( ) ;
389389
390- await expect ( service . installSkill ( "not valid skill md" ) ) . rejects . toThrow ( "Invalid SKILL.md" ) ;
390+ await expect ( service . installSkill ( "not valid skill md" ) ) . rejects . toThrow ( "Invalid SKILL.cat. md" ) ;
391391 } ) ;
392392
393393 it ( "含无效 Skill Script 时应抛出异常" , async ( ) => {
@@ -511,4 +511,246 @@ return query;`;
511511 expect ( record . referenceNames ) . toEqual ( [ ] ) ;
512512 } ) ;
513513 } ) ;
514+
515+ describe ( "installSkill version 和 installUrl" , ( ) => {
516+ it ( "应正确保存 version 字段" , async ( ) => {
517+ const { service, mockSkillRepo } = createTestService ( ) ;
518+
519+ const skillMd = `---
520+ name: versioned
521+ description: test
522+ version: 1.2.3
523+ ---
524+ Prompt.` ;
525+
526+ const record = await ( service as any ) . skillService . installSkill ( skillMd ) ;
527+ expect ( record . version ) . toBe ( "1.2.3" ) ;
528+ expect ( mockSkillRepo . saveSkill . mock . calls [ 0 ] [ 0 ] . version ) . toBe ( "1.2.3" ) ;
529+ } ) ;
530+
531+ it ( "应正确保存 installUrl" , async ( ) => {
532+ const { service, mockSkillRepo } = createTestService ( ) ;
533+
534+ const skillMd = `---
535+ name: from-url
536+ description: test
537+ version: 1.0.0
538+ ---
539+ Prompt.` ;
540+ const url = "https://example.com/skills/test/SKILL.cat.md" ;
541+
542+ const record = await ( service as any ) . skillService . installSkill ( skillMd , undefined , undefined , url ) ;
543+ expect ( record . installUrl ) . toBe ( url ) ;
544+ expect ( mockSkillRepo . saveSkill . mock . calls [ 0 ] [ 0 ] . installUrl ) . toBe ( url ) ;
545+ } ) ;
546+
547+ it ( "无 version 时 record.version 应为 undefined" , async ( ) => {
548+ const { service } = createTestService ( ) ;
549+
550+ const skillMd = `---
551+ name: no-ver
552+ description: test
553+ ---
554+ Prompt.` ;
555+
556+ const record = await ( service as any ) . skillService . installSkill ( skillMd ) ;
557+ expect ( record . version ) . toBeUndefined ( ) ;
558+ } ) ;
559+ } ) ;
560+
561+ describe ( "installFromUrl" , ( ) => {
562+ it ( "应从 URL 获取 SKILL.cat.md 并安装" , async ( ) => {
563+ const { service, mockSkillRepo } = createTestService ( ) ;
564+
565+ const skillMd = `---
566+ name: remote-skill
567+ description: Remote skill
568+ version: 2.0.0
569+ scripts:
570+ - helper.js
571+ references:
572+ - docs.md
573+ ---
574+ Remote prompt.` ;
575+
576+ const scriptCode = VALID_SKILLSCRIPT_CODE ;
577+ const refContent = "# Docs\nSome docs." ;
578+
579+ const fetchSpy = vi . spyOn ( globalThis , "fetch" ) . mockImplementation ( async ( url ) => {
580+ const urlStr = typeof url === "string" ? url : url . toString ( ) ;
581+ if ( urlStr . endsWith ( "SKILL.cat.md" ) ) {
582+ return { ok : true , text : async ( ) => skillMd } as Response ;
583+ }
584+ if ( urlStr . includes ( "scripts/helper.js" ) ) {
585+ return { ok : true , text : async ( ) => scriptCode } as Response ;
586+ }
587+ if ( urlStr . includes ( "references/docs.md" ) ) {
588+ return { ok : true , text : async ( ) => refContent } as Response ;
589+ }
590+ return { ok : false , status : 404 , statusText : "Not Found" } as Response ;
591+ } ) ;
592+
593+ try {
594+ const record = await ( service as any ) . skillService . installFromUrl (
595+ "https://example.com/skills/test/SKILL.cat.md"
596+ ) ;
597+
598+ expect ( record . name ) . toBe ( "remote-skill" ) ;
599+ expect ( record . version ) . toBe ( "2.0.0" ) ;
600+ expect ( record . installUrl ) . toBe ( "https://example.com/skills/test/SKILL.cat.md" ) ;
601+ expect ( record . toolNames ) . toEqual ( [ "test-tool" ] ) ;
602+ expect ( record . referenceNames ) . toEqual ( [ "docs.md" ] ) ;
603+
604+ // 验证 fetch 调用了正确的相对路径
605+ expect ( fetchSpy ) . toHaveBeenCalledTimes ( 3 ) ;
606+ const urls = fetchSpy . mock . calls . map ( ( c ) => ( typeof c [ 0 ] === "string" ? c [ 0 ] : c [ 0 ] . toString ( ) ) ) ;
607+ expect ( urls ) . toContain ( "https://example.com/skills/test/SKILL.cat.md" ) ;
608+ expect ( urls ) . toContain ( "https://example.com/skills/test/scripts/helper.js" ) ;
609+ expect ( urls ) . toContain ( "https://example.com/skills/test/references/docs.md" ) ;
610+ } finally {
611+ fetchSpy . mockRestore ( ) ;
612+ }
613+ } ) ;
614+
615+ it ( "SKILL.cat.md 获取失败时应抛错" , async ( ) => {
616+ const { service } = createTestService ( ) ;
617+
618+ const fetchSpy = vi . spyOn ( globalThis , "fetch" ) . mockResolvedValue ( {
619+ ok : false ,
620+ status : 404 ,
621+ statusText : "Not Found" ,
622+ } as Response ) ;
623+
624+ try {
625+ await expect (
626+ ( service as any ) . skillService . installFromUrl ( "https://example.com/not-found.cat.md" )
627+ ) . rejects . toThrow ( "Failed to fetch" ) ;
628+ } finally {
629+ fetchSpy . mockRestore ( ) ;
630+ }
631+ } ) ;
632+ } ) ;
633+
634+ describe ( "checkForUpdates / updateSkill" , ( ) => {
635+ it ( "远程版本更高时应返回更新信息" , async ( ) => {
636+ const { service, mockSkillRepo } = createTestService ( ) ;
637+
638+ const skillList = [ { name : "updatable" , version : "1.0.0" , installUrl : "https://example.com/SKILL.cat.md" } ] ;
639+ mockSkillRepo . listSkills . mockResolvedValue ( skillList ) ;
640+
641+ const remoteMd = `---
642+ name: updatable
643+ description: test
644+ version: 2.0.0
645+ ---
646+ Updated prompt.` ;
647+
648+ const fetchSpy = vi . spyOn ( globalThis , "fetch" ) . mockResolvedValue ( {
649+ ok : true ,
650+ text : async ( ) => remoteMd ,
651+ } as Response ) ;
652+
653+ try {
654+ const updates = await ( service as any ) . skillService . checkForUpdates ( ) ;
655+ expect ( updates ) . toHaveLength ( 1 ) ;
656+ expect ( updates [ 0 ] . name ) . toBe ( "updatable" ) ;
657+ expect ( updates [ 0 ] . currentVersion ) . toBe ( "1.0.0" ) ;
658+ expect ( updates [ 0 ] . remoteVersion ) . toBe ( "2.0.0" ) ;
659+ } finally {
660+ fetchSpy . mockRestore ( ) ;
661+ }
662+ } ) ;
663+
664+ it ( "远程版本相同或更低时不返回更新" , async ( ) => {
665+ const { service, mockSkillRepo } = createTestService ( ) ;
666+
667+ mockSkillRepo . listSkills . mockResolvedValue ( [
668+ { name : "up-to-date" , version : "2.0.0" , installUrl : "https://example.com/SKILL.cat.md" } ,
669+ ] ) ;
670+
671+ const remoteMd = `---
672+ name: up-to-date
673+ description: test
674+ version: 2.0.0
675+ ---
676+ Same prompt.` ;
677+
678+ const fetchSpy = vi . spyOn ( globalThis , "fetch" ) . mockResolvedValue ( {
679+ ok : true ,
680+ text : async ( ) => remoteMd ,
681+ } as Response ) ;
682+
683+ try {
684+ const updates = await ( service as any ) . skillService . checkForUpdates ( ) ;
685+ expect ( updates ) . toHaveLength ( 0 ) ;
686+ } finally {
687+ fetchSpy . mockRestore ( ) ;
688+ }
689+ } ) ;
690+
691+ it ( "无 installUrl 的 Skill 不参与更新检查" , async ( ) => {
692+ const { service, mockSkillRepo } = createTestService ( ) ;
693+
694+ mockSkillRepo . listSkills . mockResolvedValue ( [
695+ { name : "local-only" , version : "1.0.0" } ,
696+ { name : "no-version" , installUrl : "https://example.com/SKILL.cat.md" } ,
697+ ] ) ;
698+
699+ const updates = await ( service as any ) . skillService . checkForUpdates ( ) ;
700+ expect ( updates ) . toHaveLength ( 0 ) ;
701+ } ) ;
702+
703+ it ( "网络错误时静默忽略" , async ( ) => {
704+ const { service, mockSkillRepo } = createTestService ( ) ;
705+
706+ mockSkillRepo . listSkills . mockResolvedValue ( [
707+ { name : "net-err" , version : "1.0.0" , installUrl : "https://example.com/SKILL.cat.md" } ,
708+ ] ) ;
709+
710+ const fetchSpy = vi . spyOn ( globalThis , "fetch" ) . mockRejectedValue ( new Error ( "Network error" ) ) ;
711+
712+ try {
713+ const updates = await ( service as any ) . skillService . checkForUpdates ( ) ;
714+ expect ( updates ) . toHaveLength ( 0 ) ;
715+ } finally {
716+ fetchSpy . mockRestore ( ) ;
717+ }
718+ } ) ;
719+
720+ it ( "updateSkill 应从 installUrl 重新安装" , async ( ) => {
721+ const { service, mockSkillRepo } = createTestService ( ) ;
722+
723+ const url = "https://example.com/skills/test/SKILL.cat.md" ;
724+ mockSkillRepo . listSkills . mockResolvedValue ( [ { name : "to-update" , version : "1.0.0" , installUrl : url } ] ) ;
725+
726+ const remoteMd = `---
727+ name: to-update
728+ description: updated
729+ version: 2.0.0
730+ ---
731+ Updated.` ;
732+
733+ const fetchSpy = vi . spyOn ( globalThis , "fetch" ) . mockResolvedValue ( {
734+ ok : true ,
735+ text : async ( ) => remoteMd ,
736+ } as Response ) ;
737+
738+ try {
739+ const record = await ( service as any ) . skillService . updateSkill ( "to-update" ) ;
740+ expect ( record . name ) . toBe ( "to-update" ) ;
741+ expect ( record . version ) . toBe ( "2.0.0" ) ;
742+ expect ( record . installUrl ) . toBe ( url ) ;
743+ } finally {
744+ fetchSpy . mockRestore ( ) ;
745+ }
746+ } ) ;
747+
748+ it ( "无 installUrl 的 Skill 调用 updateSkill 应抛错" , async ( ) => {
749+ const { service, mockSkillRepo } = createTestService ( ) ;
750+
751+ mockSkillRepo . listSkills . mockResolvedValue ( [ { name : "local-only" , version : "1.0.0" } ] ) ;
752+
753+ await expect ( ( service as any ) . skillService . updateSkill ( "local-only" ) ) . rejects . toThrow ( "no install URL" ) ;
754+ } ) ;
755+ } ) ;
514756} ) ;
0 commit comments