@@ -180,9 +180,11 @@ async function createTestEnv() {
180180 } , null , 2 ) ) ;
181181
182182 // ── Project-level MCP (in repo root) ──
183+ // 'test-server' exists in both global and project → tests MCP shadowing
183184 await writeFile ( join ( projectDir , '.mcp.json' ) , JSON . stringify ( {
184185 mcpServers : {
185186 'project-mcp' : { command : 'node' , args : [ 'local-server.js' ] } ,
187+ 'test-server' : { command : 'node' , args : [ 'project-server.js' ] } ,
186188 }
187189 } , null , 2 ) ) ;
188190
@@ -202,19 +204,23 @@ async function createTestEnv() {
202204 await writeFile ( join ( globalCmdsDir , 'deploy.md' ) , '---\nname: deploy\ndescription: Deploy to production\n---\n# Deploy\nStep 1: Build\nStep 2: Push' ) ;
203205
204206 // ── Project commands ──
207+ // 'deploy' exists in both global and project → tests command conflict
205208 const projectCmdsDir = join ( projectDir , '.claude' , 'commands' ) ;
206209 await mkdir ( projectCmdsDir , { recursive : true } ) ;
207210 await writeFile ( join ( projectCmdsDir , 'local-build.md' ) , '---\nname: local-build\ndescription: Build the project locally\n---\n# Local Build\nRun npm run build' ) ;
211+ await writeFile ( join ( projectCmdsDir , 'deploy.md' ) , '---\nname: deploy\ndescription: Deploy this project\n---\n# Deploy\nProject-specific deploy' ) ;
208212
209213 // ── Global agents ──
210214 const globalAgentsDir = join ( claudeDir , 'agents' ) ;
211215 await mkdir ( globalAgentsDir , { recursive : true } ) ;
212216 await writeFile ( join ( globalAgentsDir , 'code-reviewer.md' ) , '---\nname: code-reviewer\ndescription: Reviews code for bugs and quality\n---\n# Code Reviewer\nReview code carefully.' ) ;
213217
214218 // ── Project agents ──
219+ // 'code-reviewer' exists in both global and project → tests agent shadowing
215220 const projectAgentsDir = join ( projectDir , '.claude' , 'agents' ) ;
216221 await mkdir ( projectAgentsDir , { recursive : true } ) ;
217222 await writeFile ( join ( projectAgentsDir , 'test-runner.md' ) , '---\nname: test-runner\ndescription: Runs tests and reports results\n---\n# Test Runner\nRun all tests.' ) ;
223+ await writeFile ( join ( projectAgentsDir , 'code-reviewer.md' ) , '---\nname: code-reviewer\ndescription: Project-specific code reviewer\n---\n# Code Reviewer\nProject-specific review.' ) ;
218224
219225 // ── Project rules (project-scoped only, locked) ──
220226 const projectRulesDir = join ( projectDir , '.claude' , 'rules' ) ;
@@ -3078,3 +3084,199 @@ test.describe('Path Resolution', () => {
30783084 await rm ( tmpDir , { recursive : true , force : true } ) ;
30793085 } ) ;
30803086} ) ;
3087+
3088+ // ── Show Effective + Move Restrictions ──────────────────────────────
3089+
3090+ test . describe ( 'Show Effective — per-category rules' , ( ) => {
3091+ let env ;
3092+ test . beforeAll ( async ( ) => { env = await createTestEnv ( ) ; } ) ;
3093+ test . afterAll ( async ( ) => { await env . cleanup ( ) ; } ) ;
3094+
3095+ test ( 'Show Effective adds global items for participating categories only' , async ( { page } ) => {
3096+ await page . goto ( env . baseURL ) ;
3097+ await page . waitForSelector ( '#loading' , { state : 'hidden' } ) ;
3098+
3099+ // Select workspace project scope
3100+ await page . locator ( `.s-scope-hdr[data-scope-id="${ env . encodedProject } "]` ) . click ( ) ;
3101+ await page . waitForTimeout ( 300 ) ;
3102+
3103+ const beforeCount = await page . evaluate ( ( ) => document . querySelectorAll ( '.item' ) . length ) ;
3104+
3105+ // Click Show Effective
3106+ await page . click ( '#inheritToggleBtn' ) ;
3107+ await page . waitForTimeout ( 500 ) ;
3108+
3109+ const afterCount = await page . evaluate ( ( ) => document . querySelectorAll ( '.item' ) . length ) ;
3110+ expect ( afterCount ) . toBeGreaterThan ( beforeCount ) ;
3111+
3112+ // Global items should have "Global" badge
3113+ const globalBadges = await page . evaluate ( ( ) =>
3114+ document . querySelectorAll ( '.ib-global' ) . length
3115+ ) ;
3116+ expect ( globalBadges ) . toBeGreaterThan ( 0 ) ;
3117+ } ) ;
3118+
3119+ test ( 'MCP same-name items get Shadowed badge' , async ( { page } ) => {
3120+ await page . goto ( env . baseURL ) ;
3121+ await page . waitForSelector ( '#loading' , { state : 'hidden' } ) ;
3122+
3123+ await page . locator ( `.s-scope-hdr[data-scope-id="${ env . encodedProject } "]` ) . click ( ) ;
3124+ await page . waitForTimeout ( 300 ) ;
3125+ await page . click ( '#inheritToggleBtn' ) ;
3126+ await page . waitForTimeout ( 500 ) ;
3127+
3128+ // 'test-server' exists in both global and project — global one should be Shadowed
3129+ const shadowed = await page . evaluate ( ( ) =>
3130+ Array . from ( document . querySelectorAll ( '.ib-shadowed' ) ) . map ( el =>
3131+ el . closest ( '.item' ) ?. querySelector ( '.item-name' ) ?. textContent
3132+ ) . filter ( Boolean )
3133+ ) ;
3134+ expect ( shadowed ) . toContain ( 'test-server' ) ;
3135+ } ) ;
3136+
3137+ test ( 'Command same-name items get Conflict badge' , async ( { page } ) => {
3138+ await page . goto ( env . baseURL ) ;
3139+ await page . waitForSelector ( '#loading' , { state : 'hidden' } ) ;
3140+
3141+ await page . locator ( `.s-scope-hdr[data-scope-id="${ env . encodedProject } "]` ) . click ( ) ;
3142+ await page . waitForTimeout ( 300 ) ;
3143+ await page . click ( '#inheritToggleBtn' ) ;
3144+ await page . waitForTimeout ( 500 ) ;
3145+
3146+ // 'deploy' exists in both global and project — should have Conflict badge
3147+ const conflicts = await page . evaluate ( ( ) =>
3148+ Array . from ( document . querySelectorAll ( '.ib-conflict' ) ) . map ( el =>
3149+ el . closest ( '.item' ) ?. querySelector ( '.item-name' ) ?. textContent
3150+ ) . filter ( Boolean )
3151+ ) ;
3152+ expect ( conflicts ) . toContain ( 'deploy' ) ;
3153+ } ) ;
3154+
3155+ test ( 'Agent same-name items get Shadowed badge (project overrides user)' , async ( { page } ) => {
3156+ await page . goto ( env . baseURL ) ;
3157+ await page . waitForSelector ( '#loading' , { state : 'hidden' } ) ;
3158+
3159+ await page . locator ( `.s-scope-hdr[data-scope-id="${ env . encodedProject } "]` ) . click ( ) ;
3160+ await page . waitForTimeout ( 300 ) ;
3161+ await page . click ( '#inheritToggleBtn' ) ;
3162+ await page . waitForTimeout ( 500 ) ;
3163+
3164+ // 'code-reviewer' exists in both — global one should be Shadowed
3165+ const shadowed = await page . evaluate ( ( ) =>
3166+ Array . from ( document . querySelectorAll ( '.ib-shadowed' ) ) . map ( el =>
3167+ el . closest ( '.item' ) ?. querySelector ( '.item-name' ) ?. textContent
3168+ ) . filter ( Boolean )
3169+ ) ;
3170+ expect ( shadowed ) . toContain ( 'code-reviewer' ) ;
3171+ } ) ;
3172+
3173+ test ( 'Categories without effectiveRule are dimmed when Show Effective is on' , async ( { page } ) => {
3174+ await page . goto ( env . baseURL ) ;
3175+ await page . waitForSelector ( '#loading' , { state : 'hidden' } ) ;
3176+
3177+ await page . locator ( `.s-scope-hdr[data-scope-id="${ env . encodedProject } "]` ) . click ( ) ;
3178+ await page . waitForTimeout ( 300 ) ;
3179+ await page . click ( '#inheritToggleBtn' ) ;
3180+ await page . waitForTimeout ( 500 ) ;
3181+
3182+ // Plan and session pills should be dimmed (f-pill-dim class)
3183+ const dimmedPills = await page . evaluate ( ( ) =>
3184+ Array . from ( document . querySelectorAll ( '.f-pill.f-pill-dim' ) ) . map ( el => el . dataset . filter )
3185+ ) ;
3186+ // plan, rule, session have no effectiveRule
3187+ for ( const cat of [ 'plan' , 'rule' , 'session' ] ) {
3188+ if ( dimmedPills . includes ( cat ) ) {
3189+ expect ( dimmedPills ) . toContain ( cat ) ;
3190+ }
3191+ }
3192+ } ) ;
3193+
3194+ test ( 'detail panel shows "Why it applies" text' , async ( { page } ) => {
3195+ await page . goto ( env . baseURL ) ;
3196+ await page . waitForSelector ( '#loading' , { state : 'hidden' } ) ;
3197+
3198+ await page . locator ( `.s-scope-hdr[data-scope-id="${ env . encodedProject } "]` ) . click ( ) ;
3199+ await page . waitForTimeout ( 300 ) ;
3200+
3201+ // Click first item
3202+ await page . locator ( '.item' ) . first ( ) . click ( ) ;
3203+ await page . waitForTimeout ( 300 ) ;
3204+
3205+ const whyVisible = await page . evaluate ( ( ) => {
3206+ const el = document . getElementById ( 'detailEffective' ) ;
3207+ return el && ! el . classList . contains ( 'hidden' ) ;
3208+ } ) ;
3209+ expect ( whyVisible ) . toBe ( true ) ;
3210+
3211+ const whyText = await page . evaluate ( ( ) =>
3212+ document . getElementById ( 'detailEffectiveText' ) ?. textContent || ''
3213+ ) ;
3214+ expect ( whyText . length ) . toBeGreaterThan ( 10 ) ;
3215+ } ) ;
3216+ } ) ;
3217+
3218+ test . describe ( 'Move restrictions — per-category destinations' , ( ) => {
3219+ let env ;
3220+ test . beforeAll ( async ( ) => { env = await createTestEnv ( ) ; } ) ;
3221+ test . afterAll ( async ( ) => { await env . cleanup ( ) ; } ) ;
3222+
3223+ test ( 'plan items have no valid move destinations' , async ( ) => {
3224+ const { items } = await ( await fetch ( `${ env . baseURL } /api/scan` ) ) . json ( ) ;
3225+ const plan = items . find ( i => i . category === 'plan' ) ;
3226+ if ( ! plan ) return ; // skip if no plans
3227+
3228+ const res = await ( await fetch ( `${ env . baseURL } /api/destinations?path=${ encodeURIComponent ( plan . path ) } &category=plan&name=${ encodeURIComponent ( plan . name ) } ` ) ) . json ( ) ;
3229+ expect ( res . ok ) . toBe ( true ) ;
3230+ expect ( res . destinations ) . toHaveLength ( 0 ) ;
3231+ } ) ;
3232+
3233+ test ( 'rule items have no valid move destinations' , async ( ) => {
3234+ const { items } = await ( await fetch ( `${ env . baseURL } /api/scan` ) ) . json ( ) ;
3235+ const rule = items . find ( i => i . category === 'rule' ) ;
3236+ if ( ! rule ) return ;
3237+
3238+ const res = await ( await fetch ( `${ env . baseURL } /api/destinations?path=${ encodeURIComponent ( rule . path ) } &category=rule&name=${ encodeURIComponent ( rule . name ) } ` ) ) . json ( ) ;
3239+ expect ( res . ok ) . toBe ( true ) ;
3240+ expect ( res . destinations ) . toHaveLength ( 0 ) ;
3241+ } ) ;
3242+
3243+ test ( 'skill items have valid move destinations' , async ( ) => {
3244+ const { items } = await ( await fetch ( `${ env . baseURL } /api/scan` ) ) . json ( ) ;
3245+ const skill = items . find ( i => i . category === 'skill' ) ;
3246+ expect ( skill ) . toBeTruthy ( ) ;
3247+
3248+ const res = await ( await fetch ( `${ env . baseURL } /api/destinations?path=${ encodeURIComponent ( skill . path ) } &category=skill&name=${ encodeURIComponent ( skill . name ) } ` ) ) . json ( ) ;
3249+ expect ( res . ok ) . toBe ( true ) ;
3250+ expect ( res . destinations . length ) . toBeGreaterThan ( 0 ) ;
3251+ } ) ;
3252+
3253+ test ( 'mcp items have valid move destinations' , async ( ) => {
3254+ const { items } = await ( await fetch ( `${ env . baseURL } /api/scan` ) ) . json ( ) ;
3255+ const mcp = items . find ( i => i . category === 'mcp' ) ;
3256+ expect ( mcp ) . toBeTruthy ( ) ;
3257+
3258+ const res = await ( await fetch ( `${ env . baseURL } /api/destinations?path=${ encodeURIComponent ( mcp . path ) } &category=mcp&name=${ encodeURIComponent ( mcp . name ) } ` ) ) . json ( ) ;
3259+ expect ( res . ok ) . toBe ( true ) ;
3260+ expect ( res . destinations . length ) . toBeGreaterThan ( 0 ) ;
3261+ } ) ;
3262+
3263+ test ( 'command items have valid move destinations' , async ( ) => {
3264+ const { items } = await ( await fetch ( `${ env . baseURL } /api/scan` ) ) . json ( ) ;
3265+ const cmd = items . find ( i => i . category === 'command' ) ;
3266+ expect ( cmd ) . toBeTruthy ( ) ;
3267+
3268+ const res = await ( await fetch ( `${ env . baseURL } /api/destinations?path=${ encodeURIComponent ( cmd . path ) } &category=command&name=${ encodeURIComponent ( cmd . name ) } ` ) ) . json ( ) ;
3269+ expect ( res . ok ) . toBe ( true ) ;
3270+ expect ( res . destinations . length ) . toBeGreaterThan ( 0 ) ;
3271+ } ) ;
3272+
3273+ test ( 'agent items have valid move destinations' , async ( ) => {
3274+ const { items } = await ( await fetch ( `${ env . baseURL } /api/scan` ) ) . json ( ) ;
3275+ const agent = items . find ( i => i . category === 'agent' ) ;
3276+ expect ( agent ) . toBeTruthy ( ) ;
3277+
3278+ const res = await ( await fetch ( `${ env . baseURL } /api/destinations?path=${ encodeURIComponent ( agent . path ) } &category=agent&name=${ encodeURIComponent ( agent . name ) } ` ) ) . json ( ) ;
3279+ expect ( res . ok ) . toBe ( true ) ;
3280+ expect ( res . destinations . length ) . toBeGreaterThan ( 0 ) ;
3281+ } ) ;
3282+ } ) ;
0 commit comments