@@ -4,7 +4,13 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
44import { render } from 'vitest-browser-react'
55
66import { TooltipProvider } from '@/renderer/src/components/ui/tooltip'
7- import type { BulkDeleteResult , Skill , SkillName } from '@/shared/types'
7+ import type {
8+ AgentId ,
9+ BulkDeleteResult ,
10+ Skill ,
11+ SkillName ,
12+ SymlinkInfo ,
13+ } from '@/shared/types'
814import { repositoryId , tombstoneId } from '@/shared/types'
915
1016const mockGetAll = vi . fn ( )
@@ -184,6 +190,34 @@ function makeSourceSkill(name: string, source: string): Skill {
184190 }
185191}
186192
193+ /**
194+ * Build an agent-local skill (real folder under ~/.<agent>/skills/, no source
195+ * repo) so repo-filter tests can exercise the "N local skills hidden" caveat.
196+ * @param name - Visible skill name.
197+ * @param agentId - Agent whose slot holds the local skill.
198+ * @returns Skill row with one local symlink slot and no source metadata.
199+ */
200+ function makeAgentLocalSkill ( name : string , agentId : AgentId ) : Skill {
201+ return {
202+ name : name as SkillName ,
203+ description : '' ,
204+ path : `/home/user/.${ agentId } /skills/${ name } ` as never ,
205+ symlinkCount : 0 ,
206+ symlinks : [
207+ {
208+ agentId,
209+ agentName : agentId as SymlinkInfo [ 'agentName' ] ,
210+ linkPath : `/home/user/.${ agentId } /skills/${ name } ` as never ,
211+ targetPath : `/home/user/.${ agentId } /skills/${ name } ` as never ,
212+ status : 'valid' ,
213+ isLocal : true ,
214+ } ,
215+ ] ,
216+ isSource : false ,
217+ isOrphan : false ,
218+ }
219+ }
220+
187221describe ( 'MainContent bulk-select toggle button' , ( ) => {
188222 it ( 'shows "Select" and aria-pressed=false by default' , async ( ) => {
189223 const { screen } = await renderMainContent ( )
@@ -469,6 +503,7 @@ describe('MainContent handleConfirmBulk — uniform delete pipeline', () => {
469503 skillNames : [ 'brainstorming' as SkillName , 'local-skill' as SkillName ] ,
470504 agentId : null ,
471505 agentName : null ,
506+ sourceSummary : null ,
472507 } ) ,
473508 )
474509
@@ -706,14 +741,14 @@ describe('MainContent repo facet dropdown', () => {
706741 . getByRole ( 'button' , { name : / F i l t e r b y s o u r c e r e p o s i t o r y / i } )
707742 . click ( )
708743 await screen
709- . getByRole ( 'menuitemradio ' , {
744+ . getByRole ( 'menuitemcheckbox ' , {
710745 name : / p b a k a u s \/ i m p e c c a b l e , 1 s k i l l / i,
711746 } )
712747 . click ( )
713748
714- expect ( store . getState ( ) . ui . selectedSource ) . toBe (
749+ expect ( store . getState ( ) . ui . selectedSources ) . toEqual ( [
715750 repositoryId ( 'pbakaus/impeccable' ) ,
716- )
751+ ] )
717752 } )
718753} )
719754
@@ -725,31 +760,31 @@ describe('MainContent filter pills (Agent + Source orthogonal)', () => {
725760
726761 it ( 'renders the Source pill with repo name and clears state on click' , async ( ) => {
727762 const { screen, store } = await renderMainContent ( )
728- const { setSelectedSource } =
763+ const { setSelectedSources } =
729764 await import ( '@/renderer/src/redux/slices/uiSlice' )
730765
731766 // No source filter active: pill must not render.
732767 expect ( screen . getByTestId ( 'source-filter-pill' ) . query ( ) ) . toBeNull ( )
733768
734- store . dispatch ( setSelectedSource ( repositoryId ( 'vercel-labs/skills' ) ) )
769+ store . dispatch ( setSelectedSources ( [ repositoryId ( 'vercel-labs/skills' ) ] ) )
735770
736771 const pill = screen . getByTestId ( 'source-filter-pill' )
737772 await expect . element ( pill ) . toBeInTheDocument ( )
738- await expect . element ( pill ) . toHaveTextContent ( / S h o w i n g s k i l l s f r o m / )
773+ await expect . element ( pill ) . toHaveTextContent ( / f r o m / )
739774 await expect . element ( pill ) . toHaveTextContent ( 'vercel-labs/skills' )
740775
741776 // Clear button inside the pill resets the slice field.
742777 await pill . getByRole ( 'button' , { name : / C l e a r / i } ) . click ( )
743778
744- await expect . poll ( ( ) => store . getState ( ) . ui . selectedSource ) . toBeNull ( )
779+ await expect . poll ( ( ) => store . getState ( ) . ui . selectedSources ) . toEqual ( [ ] )
745780 expect ( screen . getByTestId ( 'source-filter-pill' ) . query ( ) ) . toBeNull ( )
746781 } )
747782
748783 it ( 'Agent + Source pills both render when both filters are active' , async ( ) => {
749784 const { screen, store } = await renderMainContent ( )
750785 const { fetchAgents } =
751786 await import ( '@/renderer/src/redux/slices/agentsSlice' )
752- const { selectAgent, setSelectedSource } =
787+ const { selectAgent, setSelectedSources } =
753788 await import ( '@/renderer/src/redux/slices/uiSlice' )
754789
755790 // Seed an agent fixture so MainContent's `agents.find(...)` resolves.
@@ -769,7 +804,7 @@ describe('MainContent filter pills (Agent + Source orthogonal)', () => {
769804 ) ,
770805 )
771806 store . dispatch ( selectAgent ( 'claude-code' ) )
772- store . dispatch ( setSelectedSource ( repositoryId ( 'vercel-labs/skills' ) ) )
807+ store . dispatch ( setSelectedSources ( [ repositoryId ( 'vercel-labs/skills' ) ] ) )
773808
774809 await expect
775810 . element ( screen . getByTestId ( 'agent-filter-pill' ) )
@@ -783,7 +818,7 @@ describe('MainContent filter pills (Agent + Source orthogonal)', () => {
783818 const { screen, store } = await renderMainContent ( )
784819 const { fetchAgents } =
785820 await import ( '@/renderer/src/redux/slices/agentsSlice' )
786- const { selectAgent, setSelectedSource } =
821+ const { selectAgent, setSelectedSources } =
787822 await import ( '@/renderer/src/redux/slices/uiSlice' )
788823
789824 store . dispatch (
@@ -802,21 +837,82 @@ describe('MainContent filter pills (Agent + Source orthogonal)', () => {
802837 ) ,
803838 )
804839 store . dispatch ( selectAgent ( 'claude-code' ) )
805- store . dispatch ( setSelectedSource ( repositoryId ( 'vercel-labs/skills' ) ) )
840+ store . dispatch ( setSelectedSources ( [ repositoryId ( 'vercel-labs/skills' ) ] ) )
806841
807842 // Clear ONLY the source pill.
808843 await screen
809844 . getByTestId ( 'source-filter-pill' )
810845 . getByRole ( 'button' , { name : / C l e a r / i } )
811846 . click ( )
812847
813- // Agent pill must still be rendered with its label intact; selectedSource
814- // must be null . This pins Issue 4 from the design review: source clear
848+ // Agent pill must still be rendered with its label intact; selectedSources
849+ // must be empty . This pins Issue 4 from the design review: source clear
815850 // does not bleed into agent state.
816- await expect . poll ( ( ) => store . getState ( ) . ui . selectedSource ) . toBeNull ( )
851+ await expect . poll ( ( ) => store . getState ( ) . ui . selectedSources ) . toEqual ( [ ] )
817852 expect ( store . getState ( ) . ui . selectedAgentId ) . toBe ( 'claude-code' )
818853 await expect
819854 . element ( screen . getByTestId ( 'agent-filter-pill' ) )
820855 . toHaveTextContent ( 'Claude Code' )
821856 } )
822857} )
858+
859+ describe ( 'MainContent hidden-locals caveat' , ( ) => {
860+ // The repo include-filter hides source-less local skills. This inline caveat
861+ // tells the user how many were dropped so an empty-looking list is explained.
862+
863+ it ( 'shows "N local skills hidden" when the repo filter suppresses source-less locals' , async ( ) => {
864+ // Arrange — cursor view with one repo skill and two source-less locals
865+ const { screen, store } = await renderMainContent ( )
866+ const { fetchSkills } =
867+ await import ( '@/renderer/src/redux/slices/skillsSlice' )
868+ const { selectAgent, setSelectedSources } =
869+ await import ( '@/renderer/src/redux/slices/uiSlice' )
870+ store . dispatch (
871+ fetchSkills . fulfilled (
872+ [
873+ makeSourceSkill ( 'repo-skill' , 'vercel-labs/skills' ) ,
874+ makeAgentLocalSkill ( 'local-one' , 'cursor' ) ,
875+ makeAgentLocalSkill ( 'local-two' , 'cursor' ) ,
876+ ] ,
877+ 'req-id' ,
878+ ) ,
879+ )
880+ store . dispatch ( selectAgent ( 'cursor' ) )
881+
882+ // Act — turn on the repo include-filter
883+ store . dispatch ( setSelectedSources ( [ repositoryId ( 'vercel-labs/skills' ) ] ) )
884+
885+ // Assert — both suppressed locals are reported, pluralized
886+ await expect
887+ . element ( screen . getByText ( / 2 l o c a l s k i l l s h i d d e n / ) )
888+ . toBeInTheDocument ( )
889+ } )
890+
891+ it ( 'omits the caveat when no repo filter is active' , async ( ) => {
892+ // Arrange — same source-less locals in the cursor view, filter left empty
893+ const { screen, store } = await renderMainContent ( )
894+ const { fetchSkills } =
895+ await import ( '@/renderer/src/redux/slices/skillsSlice' )
896+ const { selectAgent } = await import ( '@/renderer/src/redux/slices/uiSlice' )
897+ store . dispatch (
898+ fetchSkills . fulfilled (
899+ [
900+ makeAgentLocalSkill ( 'local-one' , 'cursor' ) ,
901+ makeAgentLocalSkill ( 'local-two' , 'cursor' ) ,
902+ ] ,
903+ 'req-id' ,
904+ ) ,
905+ )
906+ store . dispatch ( selectAgent ( 'cursor' ) )
907+
908+ // Anchor on the always-rendered trigger so the toolbar is known to be live…
909+ await expect
910+ . element (
911+ screen . getByRole ( 'button' , { name : / F i l t e r b y s o u r c e r e p o s i t o r y / i } ) ,
912+ )
913+ . toBeInTheDocument ( )
914+
915+ // Assert — …then confirm the caveat is absent with nothing filtered out
916+ expect ( screen . getByText ( / l o c a l s k i l l s h i d d e n / ) . query ( ) ) . toBeNull ( )
917+ } )
918+ } )
0 commit comments