@@ -194,6 +194,173 @@ describe('scanForIntents', () => {
194194 expect ( result . packages [ 0 ] ! . name ) . toBe ( 'my-lib' )
195195 } )
196196
197+ it ( 'discovers transitive skills of a skill-bearing direct dep under pnpm isolated linker (#153)' , ( ) => {
198+ // pnpm isolated layout: a store-only transitive dep (start-core) reached
199+ // only through its skill-bearing parent's (react-start) store dir.
200+ writeFileSync ( join ( root , 'pnpm-lock.yaml' ) , '' )
201+ writeJson ( join ( root , 'package.json' ) , {
202+ name : 'consumer' ,
203+ version : '1.0.0' ,
204+ dependencies : { '@scope/react-start' : '1.0.0' } ,
205+ } )
206+
207+ const pnpmDir = join ( root , 'node_modules' , '.pnpm' )
208+
209+ const startCoreStore = createDir (
210+ pnpmDir ,
211+ '@scope+start-core@1.0.0' ,
212+ 'node_modules' ,
213+ '@scope' ,
214+ 'start-core' ,
215+ )
216+ writeJson ( join ( startCoreStore , 'package.json' ) , {
217+ name : '@scope/start-core' ,
218+ version : '1.0.0' ,
219+ intent : { version : 1 , repo : 'scope/start-core' , docs : 'docs/' } ,
220+ } )
221+ writeSkillMd ( createDir ( startCoreStore , 'skills' , 'start-core' ) , {
222+ name : 'start-core' ,
223+ description : 'Start core skill' ,
224+ type : 'core' ,
225+ } )
226+
227+ const reactStartStore = createDir (
228+ pnpmDir ,
229+ '@scope+react-start@1.0.0' ,
230+ 'node_modules' ,
231+ '@scope' ,
232+ 'react-start' ,
233+ )
234+ writeJson ( join ( reactStartStore , 'package.json' ) , {
235+ name : '@scope/react-start' ,
236+ version : '1.0.0' ,
237+ intent : { version : 1 , repo : 'scope/react-start' , docs : 'docs/' } ,
238+ dependencies : { '@scope/start-core' : '1.0.0' } ,
239+ } )
240+ writeSkillMd ( createDir ( reactStartStore , 'skills' , 'react-start' ) , {
241+ name : 'react-start' ,
242+ description : 'React start skill' ,
243+ type : 'core' ,
244+ } )
245+
246+ // start-core symlinked as a sibling inside react-start's store dir only.
247+ createDir ( pnpmDir , '@scope+react-start@1.0.0' , 'node_modules' , '@scope' )
248+ symlinkSync (
249+ startCoreStore ,
250+ join (
251+ pnpmDir ,
252+ '@scope+react-start@1.0.0' ,
253+ 'node_modules' ,
254+ '@scope' ,
255+ 'start-core' ,
256+ ) ,
257+ )
258+
259+ // react-start hoisted to the top-level node_modules; start-core is not.
260+ createDir ( root , 'node_modules' , '@scope' )
261+ symlinkSync (
262+ reactStartStore ,
263+ join ( root , 'node_modules' , '@scope' , 'react-start' ) ,
264+ )
265+
266+ const result = scanForIntents ( root )
267+
268+ const names = result . packages . map ( ( p ) => p . name )
269+ expect ( names ) . toContain ( '@scope/react-start' )
270+ expect ( names ) . toContain ( '@scope/start-core' )
271+
272+ const startCore = result . packages . find (
273+ ( p ) => p . name === '@scope/start-core' ,
274+ )
275+ expect ( startCore ! . skills . map ( ( s ) => s . name ) ) . toContain ( 'start-core' )
276+
277+ // One installed version must not be reported as a version conflict.
278+ expect ( result . conflicts ) . toEqual ( [ ] )
279+ } )
280+
281+ it ( 'discovers transitive skills when the dep resolves through a second symlink hop (#153 residual risk)' , ( ) => {
282+ // The transitive dep is reached through two symlink hops; realpathSync must
283+ // collapse the whole chain, not just one hop.
284+ writeFileSync ( join ( root , 'pnpm-lock.yaml' ) , '' )
285+ writeJson ( join ( root , 'package.json' ) , {
286+ name : 'consumer' ,
287+ version : '1.0.0' ,
288+ dependencies : { '@scope/react-start' : '1.0.0' } ,
289+ } )
290+
291+ const pnpmDir = join ( root , 'node_modules' , '.pnpm' )
292+
293+ const startCoreReal = createDir (
294+ pnpmDir ,
295+ '@scope+start-core@1.0.0' ,
296+ 'node_modules' ,
297+ '@scope' ,
298+ 'start-core' ,
299+ )
300+ writeJson ( join ( startCoreReal , 'package.json' ) , {
301+ name : '@scope/start-core' ,
302+ version : '1.0.0' ,
303+ intent : { version : 1 , repo : 'scope/start-core' , docs : 'docs/' } ,
304+ } )
305+ writeSkillMd ( createDir ( startCoreReal , 'skills' , 'start-core' ) , {
306+ name : 'start-core' ,
307+ description : 'Start core skill' ,
308+ type : 'core' ,
309+ } )
310+
311+ // Intermediate symlink hop: a separate link that targets the real store dir.
312+ const intermediateScope = createDir ( root , '.intermediate' , '@scope' )
313+ const intermediateStartCore = join ( intermediateScope , 'start-core' )
314+ symlinkSync ( startCoreReal , intermediateStartCore )
315+
316+ const reactStartStore = createDir (
317+ pnpmDir ,
318+ '@scope+react-start@1.0.0' ,
319+ 'node_modules' ,
320+ '@scope' ,
321+ 'react-start' ,
322+ )
323+ writeJson ( join ( reactStartStore , 'package.json' ) , {
324+ name : '@scope/react-start' ,
325+ version : '1.0.0' ,
326+ intent : { version : 1 , repo : 'scope/react-start' , docs : 'docs/' } ,
327+ dependencies : { '@scope/start-core' : '1.0.0' } ,
328+ } )
329+ writeSkillMd ( createDir ( reactStartStore , 'skills' , 'react-start' ) , {
330+ name : 'react-start' ,
331+ description : 'React start skill' ,
332+ type : 'core' ,
333+ } )
334+
335+ // react-start's sibling link -> intermediate link -> real store dir.
336+ createDir ( pnpmDir , '@scope+react-start@1.0.0' , 'node_modules' , '@scope' )
337+ symlinkSync (
338+ intermediateStartCore ,
339+ join (
340+ pnpmDir ,
341+ '@scope+react-start@1.0.0' ,
342+ 'node_modules' ,
343+ '@scope' ,
344+ 'start-core' ,
345+ ) ,
346+ )
347+
348+ createDir ( root , 'node_modules' , '@scope' )
349+ symlinkSync (
350+ reactStartStore ,
351+ join ( root , 'node_modules' , '@scope' , 'react-start' ) ,
352+ )
353+
354+ const result = scanForIntents ( root )
355+
356+ const startCore = result . packages . find (
357+ ( p ) => p . name === '@scope/start-core' ,
358+ )
359+ expect ( startCore ) . toBeDefined ( )
360+ expect ( startCore ! . skills . map ( ( s ) => s . name ) ) . toContain ( 'start-core' )
361+ expect ( result . conflicts ) . toEqual ( [ ] )
362+ } )
363+
197364 it ( 'discovers sub-skills' , ( ) => {
198365 const pkgDir = createDir ( root , 'node_modules' , '@tanstack' , 'db' )
199366 writeJson ( join ( pkgDir , 'package.json' ) , {
0 commit comments