@@ -3,6 +3,7 @@ import * as CommandExecutor from "@effect/platform/CommandExecutor"
33import * as FileSystem from "@effect/platform/FileSystem"
44import * as Path from "@effect/platform/Path"
55import { NodeContext } from "@effect/platform-node"
6+ import { claudeOauthTokenFileMode } from "@prover-coder-ai/docker-git-auth-oauth/claude-oauth-token"
67import { describe , expect , it } from "@effect/vitest"
78import { Effect , Logger } from "effect"
89import * as Inspectable from "effect/Inspectable"
@@ -211,6 +212,57 @@ describe("authClaudeLogin", () => {
211212 )
212213 ) . pipe ( Effect . provide ( NodeContext . layer ) ) )
213214
215+ it . effect ( "replaces an existing token symlink without writing the secret to the symlink target" , ( ) =>
216+ withTempDir ( ( root ) =>
217+ withPatchedEnv (
218+ { HOME : root , DOCKER_GIT_STATE_AUTO_SYNC : "0" , DOCKER_GIT_PROJECTS_ROOT : undefined } ,
219+ Effect . gen ( function * ( _ ) {
220+ const fs = yield * _ ( FileSystem . FileSystem )
221+ const path = yield * _ ( Path . Path )
222+ const claudeAuthPath = path . join ( root , ".docker-git/.orch/auth/claude" )
223+ const accountPath = path . join ( claudeAuthPath , "default" )
224+ const tokenPath = path . join ( accountPath , ".oauth-token" )
225+ const outsidePath = path . join ( root , "outside-token-target" )
226+ yield * _ ( fs . makeDirectory ( accountPath , { recursive : true } ) )
227+ yield * _ ( fs . writeFileString ( outsidePath , "outside-sentinel\n" ) )
228+ yield * _ ( fs . symlink ( outsidePath , tokenPath ) )
229+ let finalTokenWrites = 0
230+ const guardedFs : FileSystem . FileSystem = {
231+ ...fs ,
232+ writeFileString : ( targetPath , data , options ) =>
233+ ( targetPath === tokenPath
234+ ? Effect . sync ( ( ) => {
235+ finalTokenWrites += 1
236+ } )
237+ : Effect . void ) . pipe (
238+ Effect . zipRight ( fs . writeFileString ( targetPath , data , options ) )
239+ )
240+ }
241+
242+ yield * _ (
243+ authClaudeLogin ( {
244+ _tag : "AuthClaudeLogin" ,
245+ label : null ,
246+ claudeAuthPath
247+ } ) . pipe (
248+ Effect . provideService ( FileSystem . FileSystem , guardedFs ) ,
249+ Effect . provideService ( CommandExecutor . CommandExecutor , makeFakeExecutor ( oauthToken , 0 ) )
250+ )
251+ )
252+
253+ const outsideText = yield * _ ( fs . readFileString ( outsidePath ) )
254+ const tokenText = yield * _ ( fs . readFileString ( tokenPath ) )
255+ const tokenInfo = yield * _ ( fs . stat ( tokenPath ) )
256+
257+ expect ( outsideText ) . toBe ( "outside-sentinel\n" )
258+ expect ( tokenText . trim ( ) ) . toBe ( oauthToken )
259+ expect ( tokenInfo . type ) . toBe ( "File" )
260+ expect ( Number ( tokenInfo . mode ) & 0o777 ) . toBe ( claudeOauthTokenFileMode )
261+ expect ( finalTokenWrites ) . toBe ( 0 )
262+ } )
263+ )
264+ ) . pipe ( Effect . provide ( NodeContext . layer ) ) )
265+
214266 it . effect ( "fails when setup-token completes without a captured OAuth token" , ( ) =>
215267 withTempDir ( ( root ) =>
216268 withPatchedEnv (
0 commit comments