@@ -278,6 +278,58 @@ Run these research agents:
278278 expect ( await exists ( path . join ( outputRoot , "extensions" , "compound-engineering-compat.ts" ) ) ) . toBe ( false )
279279 } )
280280
281+ test ( "preserves user-managed symlinks at managed skill and agent paths on reinstall" , async ( ) => {
282+ const tempRoot = await fs . mkdtemp ( path . join ( os . tmpdir ( ) , "pi-symlink-preserve-" ) )
283+ const outputRoot = path . join ( tempRoot , ".pi" )
284+ const bundle : PiBundle = {
285+ pluginName : "compound-engineering" ,
286+ prompts : [ ] ,
287+ skillDirs : [
288+ {
289+ name : "skill-one" ,
290+ sourceDir : path . join ( import . meta. dir , "fixtures" , "sample-plugin" , "skills" , "skill-one" ) ,
291+ } ,
292+ ] ,
293+ generatedSkills : [ { name : "generated-one" , content : "---\nname: generated-one\n---\n\nUpstream generated" } ] ,
294+ agents : [ { name : "agent-one" , content : "---\nname: agent-one\n---\n\nUpstream agent" } ] ,
295+ extensions : [ ] ,
296+ }
297+ await writePiBundle ( outputRoot , bundle )
298+
299+ // The user replaces the managed skill dirs and agent file with symlinks
300+ // pointing at a local fork — the override pattern from issue #1048.
301+ const forkRoot = path . join ( tempRoot , "fork" )
302+ const forkSkillDir = path . join ( forkRoot , "skill-one" )
303+ await fs . mkdir ( forkSkillDir , { recursive : true } )
304+ await fs . writeFile ( path . join ( forkSkillDir , "SKILL.md" ) , "user fork skill\n" )
305+ const forkGeneratedDir = path . join ( forkRoot , "generated-one" )
306+ await fs . mkdir ( forkGeneratedDir , { recursive : true } )
307+ await fs . writeFile ( path . join ( forkGeneratedDir , "SKILL.md" ) , "user fork generated\n" )
308+ const forkAgentFile = path . join ( forkRoot , "agent-one.md" )
309+ await fs . writeFile ( forkAgentFile , "user fork agent\n" )
310+
311+ const skillLink = path . join ( outputRoot , "skills" , "skill-one" )
312+ await fs . rm ( skillLink , { recursive : true , force : true } )
313+ await fs . symlink ( forkSkillDir , skillLink , "dir" )
314+ const generatedLink = path . join ( outputRoot , "skills" , "generated-one" )
315+ await fs . rm ( generatedLink , { recursive : true , force : true } )
316+ await fs . symlink ( forkGeneratedDir , generatedLink , "dir" )
317+ const agentLink = path . join ( outputRoot , "agents" , "agent-one.md" )
318+ await fs . rm ( agentLink , { force : true } )
319+ await fs . symlink ( forkAgentFile , agentLink , "file" )
320+
321+ await writePiBundle ( outputRoot , bundle )
322+
323+ // Symlinks survive the reinstall...
324+ expect ( ( await fs . lstat ( skillLink ) ) . isSymbolicLink ( ) ) . toBe ( true )
325+ expect ( ( await fs . lstat ( generatedLink ) ) . isSymbolicLink ( ) ) . toBe ( true )
326+ expect ( ( await fs . lstat ( agentLink ) ) . isSymbolicLink ( ) ) . toBe ( true )
327+ // ...and the install did not write through them into the fork.
328+ expect ( await fs . readFile ( path . join ( forkSkillDir , "SKILL.md" ) , "utf8" ) ) . toBe ( "user fork skill\n" )
329+ expect ( await fs . readFile ( path . join ( forkGeneratedDir , "SKILL.md" ) , "utf8" ) ) . toBe ( "user fork generated\n" )
330+ expect ( await fs . readFile ( forkAgentFile , "utf8" ) ) . toBe ( "user fork agent\n" )
331+ } )
332+
281333 test ( "namespaces managed install manifests per plugin so installs do not collide" , async ( ) => {
282334 const tempRoot = await fs . mkdtemp ( path . join ( os . tmpdir ( ) , "pi-multi-plugin-" ) )
283335 const outputRoot = path . join ( tempRoot , ".pi" )
0 commit comments