1- import { lstat , readlink } from 'node:fs/promises' ;
1+ import { lstat , readlink , realpath , symlink } from 'node:fs/promises' ;
22import { fileURLToPath } from 'node:url' ;
33import { describe , it , expect } from 'vitest' ;
44import { createFixture , createMocks } from '../test-utils/index.ts' ;
5- import { copySkills , findParentPackage , updateAgentsMd } from './sync.ts' ;
5+ import { copySkills , findParentPackage , resolveSkillsSource , updateAgentsMd } from './sync.ts' ;
66
77describe ( 'copySkills' , ( ) => {
88 it ( 'symlinks each skill into the destination' , async ( ) => {
@@ -82,6 +82,34 @@ describe('copySkills', () => {
8282 ] ) ,
8383 ) ;
8484 } ) ;
85+
86+ it ( 'prunes stale links that target the real path of the source' , async ( ) => {
87+ const fixture = await createFixture ( {
88+ store : {
89+ skills : {
90+ build : { 'SKILL.md' : '---\nname: build\ndescription: Build.\n---\n' } ,
91+ } ,
92+ } ,
93+ project : {
94+ tools : ( { symlink } ) => symlink ( '../store' ) ,
95+ skills : { } ,
96+ } ,
97+ } ) ;
98+ // Left behind by an older sync that linked through the real path.
99+ await symlink (
100+ `${ await realpath ( fileURLToPath ( new URL ( 'store/skills/' , fixture . root ) ) ) } /removed` ,
101+ fileURLToPath ( new URL ( 'project/skills/removed' , fixture . root ) ) ,
102+ ) ;
103+
104+ await copySkills ( {
105+ source : new URL ( 'project/tools/skills/' , fixture . root ) ,
106+ dest : new URL ( 'project/skills/' , fixture . root ) ,
107+ } ) ;
108+
109+ await expect (
110+ lstat ( fileURLToPath ( new URL ( 'project/skills/removed' , fixture . root ) ) ) ,
111+ ) . rejects . toThrow ( ) ;
112+ } ) ;
85113} ) ;
86114
87115describe ( 'updateAgentsMd' , ( ) => {
@@ -104,6 +132,30 @@ describe('updateAgentsMd', () => {
104132 } ) ;
105133} ) ;
106134
135+ describe ( 'resolveSkillsSource' , ( ) => {
136+ it ( 'prefers the project node_modules path over the real store path' , async ( ) => {
137+ const fixture = await createFixture ( {
138+ store : { tools : { skills : { build : { 'SKILL.md' : '' } } } } ,
139+ project : {
140+ 'node_modules/@bomb.sh/tools' : ( { symlink } ) => symlink ( '../../../store/tools' ) ,
141+ } ,
142+ } ) ;
143+ const root = new URL ( 'project/' , fixture . root ) ;
144+ const fallback = new URL ( 'store/tools/skills/' , fixture . root ) ;
145+
146+ expect ( ( await resolveSkillsSource ( root , fallback ) ) . href ) . toBe (
147+ new URL ( 'node_modules/@bomb.sh/tools/skills/' , root ) . href ,
148+ ) ;
149+ } ) ;
150+
151+ it ( 'falls back when the project has no linked @bomb.sh/tools' , async ( ) => {
152+ const fixture = await createFixture ( { project : { } } ) ;
153+ const fallback = new URL ( 'store/tools/skills/' , fixture . root ) ;
154+
155+ expect ( await resolveSkillsSource ( new URL ( 'project/' , fixture . root ) , fallback ) ) . toBe ( fallback ) ;
156+ } ) ;
157+ } ) ;
158+
107159describe ( 'findParentPackage' , ( ) => {
108160 it ( 'resolves the project root from INIT_CWD, not from this package' , async ( ) => {
109161 const fixture = await createFixture ( {
0 commit comments