@@ -606,4 +606,125 @@ public async Task SearchSkills_WithPorterStemming_MatchesStemmedTerms()
606606 results = await _fixture . Client . SearchSkillsAsync ( "closing" , ct : ct ) ;
607607 Assert . Contains ( results , s => s . Name == skillName ) ;
608608 }
609+
610+ [ Fact ]
611+ public async Task UploadSkillWithReferences_EndToEnd ( )
612+ {
613+ var ct = TestContext . Current . CancellationToken ;
614+ var skillName = $ "ref-test-{ Guid . NewGuid ( ) : N} "[ ..20 ] ;
615+
616+ var skillContent = $ """
617+ ---
618+ name: { skillName }
619+ description: Testing reference file uploads
620+ ---
621+
622+ # Reference Upload Test
623+
624+ See references/guide.md for details.
625+ """ ;
626+
627+ var referenceContent = """
628+ # Guide
629+
630+ This is a reference document with detailed examples.
631+
632+ ## Section One
633+ First section content.
634+
635+ ## Section Two
636+ Second section content.
637+ """ ;
638+
639+ var referenceContent2 = """
640+ # Patterns
641+
642+ Common usage patterns for this skill.
643+ """ ;
644+
645+ // Upload skill with two reference files
646+ using var content = new MultipartFormDataContent ( ) ;
647+ content . Add ( new StringContent ( skillName ) , "name" ) ;
648+ content . Add ( new StringContent ( "1.0.0" ) , "version" ) ;
649+
650+ var fileContent = new ByteArrayContent ( Encoding . UTF8 . GetBytes ( skillContent ) ) ;
651+ fileContent . Headers . ContentType = new MediaTypeHeaderValue ( "text/markdown" ) ;
652+ content . Add ( fileContent , "file" , "SKILL.md" ) ;
653+
654+ var refFileContent = new ByteArrayContent ( Encoding . UTF8 . GetBytes ( referenceContent ) ) ;
655+ refFileContent . Headers . ContentType = new MediaTypeHeaderValue ( "text/markdown" ) ;
656+ content . Add ( refFileContent , "references" , "guide.md" ) ;
657+
658+ var refFileContent2 = new ByteArrayContent ( Encoding . UTF8 . GetBytes ( referenceContent2 ) ) ;
659+ refFileContent2 . Headers . ContentType = new MediaTypeHeaderValue ( "text/markdown" ) ;
660+ content . Add ( refFileContent2 , "references" , "patterns.md" ) ;
661+
662+ var uploadResponse = await _fixture . AuthenticatedHttpClient . PostAsync ( "/skills" , content , ct ) ;
663+ Assert . Equal ( HttpStatusCode . Created , uploadResponse . StatusCode ) ;
664+
665+ // Verify the version shows file count (SKILL.md + 2 references)
666+ var version = await _fixture . Client . GetVersionAsync ( skillName , "1.0.0" , ct ) ;
667+ Assert . NotNull ( version ) ;
668+ Assert . Equal ( 2 , version . FileCount ) ;
669+
670+ // Download SKILL.md
671+ var downloadedSkill = await _fixture . Client . GetSkillFileAsStringAsync ( skillName , "1.0.0" , ct : ct ) ;
672+ Assert . Contains ( "# Reference Upload Test" , downloadedSkill ) ;
673+
674+ // Download reference files via resource endpoint
675+ var refResponse = await _fixture . HttpClient . GetAsync (
676+ $ "/skills/{ skillName } /1.0.0/references/guide.md", ct ) ;
677+ Assert . Equal ( HttpStatusCode . OK , refResponse . StatusCode ) ;
678+ var refBody = await refResponse . Content . ReadAsStringAsync ( ct ) ;
679+ Assert . Contains ( "# Guide" , refBody ) ;
680+ Assert . Contains ( "Section One" , refBody ) ;
681+
682+ var refResponse2 = await _fixture . HttpClient . GetAsync (
683+ $ "/skills/{ skillName } /1.0.0/references/patterns.md", ct ) ;
684+ Assert . Equal ( HttpStatusCode . OK , refResponse2 . StatusCode ) ;
685+ var refBody2 = await refResponse2 . Content . ReadAsStringAsync ( ct ) ;
686+ Assert . Contains ( "# Patterns" , refBody2 ) ;
687+
688+ // Verify resources appear in RFC index
689+ var index = await _fixture . Client . GetRfcIndexAsync ( ct ) ;
690+ Assert . NotNull ( index ) ;
691+ var indexSkill = index . Skills . FirstOrDefault ( s => s . Name == skillName ) ;
692+ Assert . NotNull ( indexSkill ) ;
693+ var resources = indexSkill ! . Resources ;
694+ Assert . NotNull ( resources ) ;
695+ Assert . Equal ( 2 , resources ! . Count ) ;
696+ Assert . Contains ( resources , r => r . Path == "references/guide.md" ) ;
697+ Assert . Contains ( resources , r => r . Path == "references/patterns.md" ) ;
698+ }
699+
700+ [ Fact ]
701+ public async Task UploadSkillWithReferences_WithoutReferences_StillWorks ( )
702+ {
703+ var ct = TestContext . Current . CancellationToken ;
704+ var skillName = $ "noref-{ Guid . NewGuid ( ) : N} "[ ..20 ] ;
705+
706+ var skillContent = $ """
707+ ---
708+ name: { skillName }
709+ description: Testing upload without references still works
710+ ---
711+
712+ # No References Test
713+ """ ;
714+
715+ using var content = new MultipartFormDataContent ( ) ;
716+ content . Add ( new StringContent ( skillName ) , "name" ) ;
717+ content . Add ( new StringContent ( "1.0.0" ) , "version" ) ;
718+
719+ var fileContent = new ByteArrayContent ( Encoding . UTF8 . GetBytes ( skillContent ) ) ;
720+ fileContent . Headers . ContentType = new MediaTypeHeaderValue ( "text/markdown" ) ;
721+ content . Add ( fileContent , "file" , "SKILL.md" ) ;
722+
723+ var uploadResponse = await _fixture . AuthenticatedHttpClient . PostAsync ( "/skills" , content , ct ) ;
724+ Assert . Equal ( HttpStatusCode . Created , uploadResponse . StatusCode ) ;
725+
726+ var version = await _fixture . Client . GetVersionAsync ( skillName , "1.0.0" , ct ) ;
727+ Assert . NotNull ( version ) ;
728+ Assert . Equal ( 0 , version . FileCount ) ;
729+ }
609730}
0 commit comments