@@ -734,6 +734,79 @@ func TestInstallWithExtraction(t *testing.T) {
734734 assert .Equal (t , http .StatusConflict , httperr .Code (err ))
735735 assert .Contains (t , err .Error (), "not managed by ToolHive" )
736736 })
737+
738+ t .Run ("legacy row with explicit client is not a no-op" , func (t * testing.T ) {
739+ t .Parallel ()
740+ ctrl := gomock .NewController (t )
741+ store := storemocks .NewMockSkillStore (ctrl )
742+ pr := skillsmocks .NewMockPathResolver (ctrl )
743+ inst := skillsmocks .NewMockInstaller (ctrl )
744+
745+ dirB := filepath .Join (t .TempDir (), "b" , "my-skill" )
746+ existing := skills.InstalledSkill {
747+ Metadata : skills.SkillMetadata {Name : "my-skill" },
748+ Digest : "sha256:abc" ,
749+ Clients : []string {},
750+ }
751+ pr .EXPECT ().GetSkillPath ("opencode" , "my-skill" , skills .ScopeUser , "" ).Return (dirB , nil )
752+ store .EXPECT ().Get (gomock .Any (), "my-skill" , skills .ScopeUser , "" ).Return (existing , nil )
753+ inst .EXPECT ().Extract (layerData , dirB , false ).
754+ Return (& skills.ExtractResult {SkillDir : dirB , Files : 1 }, nil )
755+ store .EXPECT ().Update (gomock .Any (), gomock .Any ()).DoAndReturn (
756+ func (_ context.Context , sk skills.InstalledSkill ) error {
757+ assert .Contains (t , sk .Clients , "opencode" )
758+ return nil
759+ })
760+
761+ svc := New (store , WithPathResolver (pr ), WithInstaller (inst ))
762+ result , err := svc .Install (t .Context (), skills.InstallOptions {
763+ Name : "my-skill" ,
764+ LayerData : layerData ,
765+ Digest : "sha256:abc" ,
766+ Clients : []string {"opencode" },
767+ })
768+ require .NoError (t , err )
769+ assert .Equal (t , "my-skill" , result .Skill .Metadata .Name )
770+ })
771+
772+ t .Run ("upgrade extracts to all existing clients not just requested" , func (t * testing.T ) {
773+ t .Parallel ()
774+ ctrl := gomock .NewController (t )
775+ store := storemocks .NewMockSkillStore (ctrl )
776+ pr := skillsmocks .NewMockPathResolver (ctrl )
777+ inst := skillsmocks .NewMockInstaller (ctrl )
778+
779+ dirA := filepath .Join (t .TempDir (), "a" , "my-skill" )
780+ dirB := filepath .Join (t .TempDir (), "b" , "my-skill" )
781+ existing := skills.InstalledSkill {
782+ Metadata : skills.SkillMetadata {Name : "my-skill" },
783+ Digest : "sha256:old" ,
784+ Clients : []string {"claude-code" },
785+ }
786+ pr .EXPECT ().GetSkillPath ("opencode" , "my-skill" , skills .ScopeUser , "" ).Return (dirB , nil )
787+ pr .EXPECT ().GetSkillPath ("claude-code" , "my-skill" , skills .ScopeUser , "" ).Return (dirA , nil )
788+ store .EXPECT ().Get (gomock .Any (), "my-skill" , skills .ScopeUser , "" ).Return (existing , nil )
789+ inst .EXPECT ().Extract (layerData , dirB , true ).
790+ Return (& skills.ExtractResult {SkillDir : dirB , Files : 1 }, nil )
791+ inst .EXPECT ().Extract (layerData , dirA , true ).
792+ Return (& skills.ExtractResult {SkillDir : dirA , Files : 1 }, nil )
793+ store .EXPECT ().Update (gomock .Any (), gomock .Any ()).DoAndReturn (
794+ func (_ context.Context , sk skills.InstalledSkill ) error {
795+ assert .ElementsMatch (t , []string {"opencode" , "claude-code" }, sk .Clients )
796+ assert .Equal (t , "sha256:new" , sk .Digest )
797+ return nil
798+ })
799+
800+ svc := New (store , WithPathResolver (pr ), WithInstaller (inst ))
801+ result , err := svc .Install (t .Context (), skills.InstallOptions {
802+ Name : "my-skill" ,
803+ LayerData : layerData ,
804+ Digest : "sha256:new" ,
805+ Clients : []string {"opencode" },
806+ })
807+ require .NoError (t , err )
808+ assert .ElementsMatch (t , []string {"opencode" , "claude-code" }, result .Skill .Clients )
809+ })
737810}
738811
739812// buildTestArtifact creates a real OCI skill artifact in the store and returns
0 commit comments