@@ -29,14 +29,22 @@ const setupTokenOutput = (token: string): string =>
2929 " Store this token securely. You won't be able to see it again."
3030 ] . join ( "\n" )
3131
32+ const setupTokenOutputWithoutToken = ( ) : string =>
33+ [
34+ "Welcome to Claude Code" ,
35+ "" ,
36+ " OAuth flow finished without printing a long-lived token." ,
37+ ""
38+ ] . join ( "\n" )
39+
3240const isSetupToken = ( args : ReadonlyArray < string > ) : boolean => args . includes ( "setup-token" )
3341const isPingProbe = ( args : ReadonlyArray < string > ) : boolean => args . includes ( "-p" ) && args . includes ( "ping" )
3442
3543// CHANGE: fake docker executor that captures a setup-token and lets the ping probe fail
3644// WHY: reproduce issue-439 where a successful OAuth login was discarded by a failing probe
3745// REF: issue-439
3846const makeFakeExecutor = (
39- token : string ,
47+ token : string | null ,
4048 pingExitCode : number
4149) : CommandExecutor . CommandExecutor => {
4250 const start = ( command : Command . Command ) : Effect . Effect < CommandExecutor . Process , never > =>
@@ -45,7 +53,11 @@ const makeFakeExecutor = (
4553 const invocation = flattened [ flattened . length - 1 ] !
4654 const args = invocation . args
4755
48- const stdoutText = isSetupToken ( args ) ? setupTokenOutput ( token ) : ""
56+ const stdoutText = isSetupToken ( args )
57+ ? token === null
58+ ? setupTokenOutputWithoutToken ( )
59+ : setupTokenOutput ( token )
60+ : ""
4961 const exitCode = isPingProbe ( args ) ? pingExitCode : 0
5062 const stdout = stdoutText . length === 0 ? Stream . empty : Stream . succeed ( encode ( stdoutText ) )
5163
@@ -136,6 +148,34 @@ const runLoginAndReadToken = (
136148 return yield * _ ( fs . readFileString ( path . join ( claudeAuthPath , "default" , ".oauth-token" ) ) )
137149 } )
138150
151+ const runLoginWithoutCapturedToken = (
152+ root : string
153+ ) : Effect . Effect < void , unknown , FileSystem . FileSystem | Path . Path > =>
154+ Effect . gen ( function * ( _ ) {
155+ const fs = yield * _ ( FileSystem . FileSystem )
156+ const path = yield * _ ( Path . Path )
157+ const claudeAuthPath = path . join ( root , ".docker-git/.orch/auth/claude" )
158+ const tokenPath = path . join ( claudeAuthPath , "default" , ".oauth-token" )
159+
160+ const error = yield * _ (
161+ authClaudeLogin ( {
162+ _tag : "AuthClaudeLogin" ,
163+ label : null ,
164+ claudeAuthPath
165+ } ) . pipe (
166+ Effect . provideService ( CommandExecutor . CommandExecutor , makeFakeExecutor ( null , 0 ) ) ,
167+ Effect . flip
168+ )
169+ )
170+
171+ expect ( error . _tag ) . toBe ( "AuthError" )
172+ if ( error . _tag === "AuthError" ) {
173+ expect ( error . message ) . toContain ( "without a captured token" )
174+ }
175+ const hasTokenFile = yield * _ ( fs . exists ( tokenPath ) )
176+ expect ( hasTokenFile ) . toBe ( false )
177+ } )
178+
139179describe ( "authClaudeLogin" , ( ) => {
140180 // Regression for issue-439: a non-zero probe exit must not discard a created token.
141181 it . effect ( "persists the OAuth token even when the post-login API probe fails" , ( ) =>
@@ -159,4 +199,14 @@ describe("authClaudeLogin", () => {
159199 } )
160200 )
161201 ) . pipe ( Effect . provide ( NodeContext . layer ) ) )
202+
203+ it . effect ( "fails when setup-token completes without a captured OAuth token" , ( ) =>
204+ withTempDir ( ( root ) =>
205+ withPatchedEnv (
206+ { HOME : root , DOCKER_GIT_STATE_AUTO_SYNC : "0" , DOCKER_GIT_PROJECTS_ROOT : undefined } ,
207+ Effect . gen ( function * ( _ ) {
208+ yield * _ ( runLoginWithoutCapturedToken ( root ) )
209+ } )
210+ )
211+ ) . pipe ( Effect . provide ( NodeContext . layer ) ) )
162212} )
0 commit comments