@@ -126,3 +126,65 @@ test("app.exit prints the session epilogue after scoped cleanup", async () => {
126126 mock . restore ( )
127127 }
128128} )
129+
130+ test ( "fatal startup errors set a nonzero exit after scoped cleanup" , async ( ) => {
131+ const setup = await createTestRenderer ( { width : 80 , height : 24 , useThread : false } )
132+ const core = await import ( "@opentui/core" )
133+ await mock . module ( "@opentui/core" , ( ) => ( { ...core , createCliRenderer : async ( ) => setup . renderer } ) )
134+ const events = createEventSource ( )
135+ const calls = createFetch ( ( url ) => {
136+ if ( url . pathname === "/config" )
137+ return json (
138+ {
139+ name : "ConfigRemoteAuthError" ,
140+ data : {
141+ url : "https://example.com" ,
142+ remote : "https://config.example.com/opencode.json" ,
143+ } ,
144+ } ,
145+ { status : 400 } ,
146+ )
147+ return undefined
148+ } )
149+ let disposes = 0
150+ const originalWrite = process . stderr . write . bind ( process . stderr )
151+ const originalExitCode = process . exitCode ?? 0
152+ let stderr = ""
153+
154+ process . stderr . write = ( ( chunk : string | Uint8Array ) => {
155+ stderr += String ( chunk )
156+ return true
157+ } ) as typeof process . stderr . write
158+
159+ try {
160+ const { run } = await import ( "../src/app" )
161+ const task = Effect . runPromise (
162+ run ( {
163+ url : "http://test" ,
164+ directory,
165+ config : createTuiResolvedConfig ( { plugin_enabled : { } } ) ,
166+ fetch : calls . fetch ,
167+ events : events . source ,
168+ args : { } ,
169+ pluginHost : {
170+ async start ( ) { } ,
171+ async dispose ( ) {
172+ disposes ++
173+ } ,
174+ } ,
175+ } ) . pipe ( Effect . provide ( AppNodeBuilder . build ( Global . node ) ) ) ,
176+ )
177+
178+ await task
179+ expect ( stderr ) . toContain ( "Run `opencode auth login https://example.com` to re-authenticate." )
180+ expect ( stderr ) . not . toContain ( "Unexpected server error" )
181+ expect ( process . exitCode ) . toBe ( 1 )
182+ expect ( setup . renderer . isDestroyed ) . toBe ( true )
183+ expect ( disposes ) . toBe ( 1 )
184+ } finally {
185+ process . stderr . write = originalWrite
186+ process . exitCode = originalExitCode
187+ if ( ! setup . renderer . isDestroyed ) setup . renderer . destroy ( )
188+ mock . restore ( )
189+ }
190+ } )
0 commit comments