44package skills
55
66import (
7+ "bytes"
78 "context"
89 "encoding/json"
910 "fmt"
@@ -331,14 +332,7 @@ func TestPackager_Package_RejectsSymlinks(t *testing.T) {
331332 t .Parallel ()
332333
333334 dir := t .TempDir ()
334- skillMD := `---
335- name: test-skill
336- description: A test skill
337- version: 1.0.0
338- ---
339- # Test Skill
340- `
341- require .NoError (t , os .WriteFile (filepath .Join (dir , "SKILL.md" ), []byte (skillMD ), 0600 ))
335+ writeValidSkillMD (t , dir )
342336 require .NoError (t , os .Symlink ("/etc/passwd" , filepath .Join (dir , "evil_link" )))
343337
344338 store , err := NewStore (t .TempDir ())
@@ -356,14 +350,7 @@ func TestPackager_Package_RejectsSymlinkedDirectory(t *testing.T) {
356350 t .Parallel ()
357351
358352 dir := t .TempDir ()
359- skillMD := `---
360- name: test-skill
361- description: A test skill
362- version: 1.0.0
363- ---
364- # Test Skill
365- `
366- require .NoError (t , os .WriteFile (filepath .Join (dir , "SKILL.md" ), []byte (skillMD ), 0600 ))
353+ writeValidSkillMD (t , dir )
367354 require .NoError (t , os .Symlink ("/etc" , filepath .Join (dir , "evil_dir" )))
368355
369356 store , err := NewStore (t .TempDir ())
@@ -571,6 +558,187 @@ version: 1.0.0
571558 _ , err := collectSkillFiles (dir )
572559 require .Error (t , err )
573560 assert .Contains (t , err .Error (), "exceeds maximum" )
561+ assert .ErrorIs (t , err , ErrTooManyFiles )
562+ }
563+
564+ func TestPackager_Package_SentinelErrors (t * testing.T ) {
565+ t .Parallel ()
566+
567+ tests := []struct {
568+ name string
569+ setup func (t * testing.T ) string
570+ wantErr error
571+ }{
572+ {
573+ name : "missing skill directory" ,
574+ setup : func (t * testing.T ) string {
575+ t .Helper ()
576+ return filepath .Join (t .TempDir (), "does-not-exist" )
577+ },
578+ wantErr : ErrInvalidSkillDir ,
579+ },
580+ {
581+ name : "path is file not directory" ,
582+ setup : func (t * testing.T ) string {
583+ t .Helper ()
584+ f := filepath .Join (t .TempDir (), "not-a-dir" )
585+ require .NoError (t , os .WriteFile (f , []byte ("x" ), 0600 ))
586+ return f
587+ },
588+ wantErr : ErrInvalidSkillDir ,
589+ },
590+ {
591+ name : "path contains traversal" ,
592+ setup : func (_ * testing.T ) string {
593+ return "../no-such-skill-dir"
594+ },
595+ wantErr : ErrInvalidSkillDir ,
596+ },
597+ {
598+ name : "missing SKILL.md" ,
599+ setup : func (t * testing.T ) string {
600+ t .Helper ()
601+ return t .TempDir ()
602+ },
603+ wantErr : ErrSkillMDMissing ,
604+ },
605+ {
606+ name : "frontmatter missing opening delimiter" ,
607+ setup : func (t * testing.T ) string {
608+ t .Helper ()
609+ dir := t .TempDir ()
610+ require .NoError (t , os .WriteFile (
611+ filepath .Join (dir , "SKILL.md" ),
612+ []byte ("# no frontmatter\n " ),
613+ 0600 ,
614+ ))
615+ return dir
616+ },
617+ wantErr : ErrInvalidFrontmatter ,
618+ },
619+ {
620+ name : "frontmatter missing closing delimiter" ,
621+ setup : func (t * testing.T ) string {
622+ t .Helper ()
623+ dir := t .TempDir ()
624+ require .NoError (t , os .WriteFile (
625+ filepath .Join (dir , "SKILL.md" ),
626+ []byte ("---\n name: test\n # never closed" ),
627+ 0600 ,
628+ ))
629+ return dir
630+ },
631+ wantErr : ErrInvalidFrontmatter ,
632+ },
633+ {
634+ name : "frontmatter exceeds size limit" ,
635+ setup : func (t * testing.T ) string {
636+ t .Helper ()
637+ dir := t .TempDir ()
638+ var buf bytes.Buffer
639+ buf .WriteString ("---\n name: test\n filler: " )
640+ buf .Write (bytes .Repeat ([]byte ("a" ), maxFrontmatterSize + 1 ))
641+ buf .WriteString ("\n ---\n # body\n " )
642+ require .NoError (t , os .WriteFile (filepath .Join (dir , "SKILL.md" ), buf .Bytes (), 0600 ))
643+ return dir
644+ },
645+ wantErr : ErrInvalidFrontmatter ,
646+ },
647+ {
648+ name : "frontmatter invalid YAML" ,
649+ setup : func (t * testing.T ) string {
650+ t .Helper ()
651+ dir := t .TempDir ()
652+ require .NoError (t , os .WriteFile (
653+ filepath .Join (dir , "SKILL.md" ),
654+ []byte ("---\n name: [unclosed\n ---\n # body\n " ),
655+ 0600 ,
656+ ))
657+ return dir
658+ },
659+ wantErr : ErrInvalidFrontmatter ,
660+ },
661+ {
662+ name : "frontmatter missing name" ,
663+ setup : func (t * testing.T ) string {
664+ t .Helper ()
665+ dir := t .TempDir ()
666+ require .NoError (t , os .WriteFile (
667+ filepath .Join (dir , "SKILL.md" ),
668+ []byte ("---\n description: nameless skill\n ---\n # body\n " ),
669+ 0600 ,
670+ ))
671+ return dir
672+ },
673+ wantErr : ErrInvalidFrontmatter ,
674+ },
675+ {
676+ name : "symlinked file in skill directory" ,
677+ setup : func (t * testing.T ) string {
678+ t .Helper ()
679+ dir := t .TempDir ()
680+ writeValidSkillMD (t , dir )
681+ require .NoError (t , os .Symlink ("/etc/passwd" , filepath .Join (dir , "evil_link" )))
682+ return dir
683+ },
684+ wantErr : ErrInvalidSkillFile ,
685+ },
686+ {
687+ name : "symlinked directory in skill directory" ,
688+ setup : func (t * testing.T ) string {
689+ t .Helper ()
690+ dir := t .TempDir ()
691+ writeValidSkillMD (t , dir )
692+ require .NoError (t , os .Symlink ("/etc" , filepath .Join (dir , "evil_dir" )))
693+ return dir
694+ },
695+ wantErr : ErrInvalidSkillFile ,
696+ },
697+ }
698+
699+ for _ , tt := range tests {
700+ t .Run (tt .name , func (t * testing.T ) {
701+ t .Parallel ()
702+
703+ store , err := NewStore (t .TempDir ())
704+ require .NoError (t , err )
705+ packager := NewPackager (store )
706+ opts := PackageOptions {Epoch : time .Unix (0 , 0 ).UTC ()}
707+
708+ _ , err = packager .Package (context .Background (), tt .setup (t ), opts )
709+ require .Error (t , err )
710+ assert .ErrorIs (t , err , tt .wantErr )
711+ })
712+ }
713+ }
714+
715+ // TestCollectSkillFiles_ExceedsMaxSize verifies that the total-size limit
716+ // surfaces ErrSkillTooLarge. Kept separate from the table-driven test because
717+ // it writes >100 MiB to disk.
718+ func TestCollectSkillFiles_ExceedsMaxSize (t * testing.T ) {
719+ t .Parallel ()
720+
721+ dir := t .TempDir ()
722+ writeValidSkillMD (t , dir )
723+
724+ // Stream-write a single file just over maxSkillTotalSize using a 1 MiB
725+ // buffer so we don't hold the whole payload in memory at once.
726+ f , err := os .Create (filepath .Join (dir , "big.bin" )) //#nosec G304 -- t.TempDir
727+ require .NoError (t , err )
728+ const chunkSize = 1 << 20 // 1 MiB
729+ chunk := make ([]byte , chunkSize )
730+ written := int64 (0 )
731+ for written <= maxSkillTotalSize {
732+ n , werr := f .Write (chunk )
733+ require .NoError (t , werr )
734+ written += int64 (n )
735+ }
736+ require .NoError (t , f .Close ())
737+
738+ _ , err = collectSkillFiles (dir )
739+ require .Error (t , err )
740+ assert .Contains (t , err .Error (), "exceeds maximum total size" )
741+ assert .ErrorIs (t , err , ErrSkillTooLarge )
574742}
575743
576744// Helper functions
@@ -597,6 +765,19 @@ This is a test skill.
597765 return dir
598766}
599767
768+ func writeValidSkillMD (t * testing.T , dir string ) {
769+ t .Helper ()
770+
771+ skillMD := `---
772+ name: test-skill
773+ description: A test skill
774+ version: 1.0.0
775+ ---
776+ # Test Skill
777+ `
778+ require .NoError (t , os .WriteFile (filepath .Join (dir , "SKILL.md" ), []byte (skillMD ), 0600 ))
779+ }
780+
600781func createTestSkillDirWithScripts (t * testing.T ) string {
601782 t .Helper ()
602783
0 commit comments