@@ -2,10 +2,12 @@ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
22import { execFileSync } from "child_process" ;
33import {
44 existsSync ,
5+ lstatSync ,
56 mkdtempSync ,
67 mkdirSync ,
78 readFileSync ,
89 rmSync ,
10+ symlinkSync ,
911 writeFileSync ,
1012} from "fs" ;
1113import { dirname , isAbsolute , join } from "path" ;
@@ -121,6 +123,48 @@ describe("restoreConfigFromBase", () => {
121123 }
122124 } ) ;
123125
126+ test ( "restores symlinked CLAUDE.md paths from the PR base branch" , ( ) => {
127+ setupSymlinkedMainBranch ( ) ;
128+
129+ git ( [ "checkout" , "pr" ] ) ;
130+ writeRepoFile (
131+ ".claude/settings.json" ,
132+ `${ JSON . stringify ( { source : "pr-with-symlinks" } ) } \n` ,
133+ ) ;
134+ git ( [ "add" , ".claude/settings.json" ] ) ;
135+ git ( [ "commit" , "-m" , "pr updates settings" ] ) ;
136+
137+ restoreConfigFromBase ( "main" ) ;
138+
139+ expect ( lstatRepoFile ( "CLAUDE.md" ) . isSymbolicLink ( ) ) . toBe ( true ) ;
140+ expect ( lstatRepoFile ( ".claude/CLAUDE.md" ) . isSymbolicLink ( ) ) . toBe ( true ) ;
141+ expect ( readRepoFile ( "CLAUDE.md" ) . trim ( ) ) . toBe ( "shared agent instructions" ) ;
142+ expect ( readRepoFile ( ".claude/CLAUDE.md" ) . trim ( ) ) . toBe (
143+ "shared agent instructions" ,
144+ ) ;
145+ expect ( readRepoFile ( ".claude/settings.json" ) ) . toBe (
146+ `${ JSON . stringify ( { source : "base" } ) } \n` ,
147+ ) ;
148+ } ) ;
149+
150+ test ( "snapshots symlinked sensitive paths even when the PR head target is missing" , ( ) => {
151+ setupSymlinkedMainBranch ( ) ;
152+
153+ git ( [ "checkout" , "pr" ] ) ;
154+ rmSync ( join ( repoDir , "AGENTS.md" ) , { force : true } ) ;
155+ git ( [ "add" , "-A" ] ) ;
156+ git ( [ "commit" , "-m" , "pr deletes agents file" ] ) ;
157+
158+ restoreConfigFromBase ( "main" ) ;
159+
160+ expect ( lstatRepoFile ( ".claude-pr/.claude/CLAUDE.md" ) . isSymbolicLink ( ) ) . toBe (
161+ true ,
162+ ) ;
163+ expect ( readRepoFile ( ".claude/settings.json" ) ) . toBe (
164+ `${ JSON . stringify ( { source : "base" } ) } \n` ,
165+ ) ;
166+ } ) ;
167+
124168 test ( "does not modify an existing .gitignore" , ( ) => {
125169 writeRepoFile ( ".gitignore" , "node_modules\n" ) ;
126170 git ( [ "add" , ".gitignore" ] ) ;
@@ -156,6 +200,29 @@ describe("restoreConfigFromBase", () => {
156200 return existsSync ( join ( repoDir , path ) ) ;
157201 }
158202
203+ function symlinkRepoFile ( path : string , target : string ) : void {
204+ const fullPath = join ( repoDir , path ) ;
205+ mkdirSync ( dirname ( fullPath ) , { recursive : true } ) ;
206+ symlinkSync ( target , fullPath ) ;
207+ }
208+
209+ function lstatRepoFile ( path : string ) {
210+ return lstatSync ( join ( repoDir , path ) ) ;
211+ }
212+
213+ function setupSymlinkedMainBranch ( ) : void {
214+ git ( [ "checkout" , "main" ] ) ;
215+ rmSync ( join ( repoDir , "CLAUDE.md" ) , { force : true } ) ;
216+ writeRepoFile ( "AGENTS.md" , "shared agent instructions\n" ) ;
217+ symlinkRepoFile ( "CLAUDE.md" , "AGENTS.md" ) ;
218+ symlinkRepoFile ( ".claude/CLAUDE.md" , "../AGENTS.md" ) ;
219+ git ( [ "add" , "AGENTS.md" , "CLAUDE.md" , ".claude/CLAUDE.md" ] ) ;
220+ git ( [ "commit" , "-m" , "add symlinked claude files" ] ) ;
221+ git ( [ "push" , "origin" , "main" ] ) ;
222+ git ( [ "branch" , "-D" , "pr" ] ) ;
223+ git ( [ "checkout" , "-b" , "pr" ] ) ;
224+ }
225+
159226 function countClaudePrExcludeEntries ( ) : number {
160227 return readFileSync ( getExcludePath ( ) , "utf8" )
161228 . split ( / \r ? \n / )
0 commit comments