@@ -247,6 +247,14 @@ const stdinIsTTYDescriptor = Object.getOwnPropertyDescriptor(
247247 process . stdin ,
248248 "isTTY" ,
249249) ;
250+ const stdinReadableEndedDescriptor = Object . getOwnPropertyDescriptor (
251+ process . stdin ,
252+ "readableEnded" ,
253+ ) ;
254+ const stdinDestroyedDescriptor = Object . getOwnPropertyDescriptor (
255+ process . stdin ,
256+ "destroyed" ,
257+ ) ;
250258const stdoutIsTTYDescriptor = Object . getOwnPropertyDescriptor (
251259 process . stdout ,
252260 "isTTY" ,
@@ -269,13 +277,38 @@ function restoreTTYDescriptors(): void {
269277 } else {
270278 delete ( process . stdin as unknown as { isTTY ?: boolean } ) . isTTY ;
271279 }
280+ if ( stdinReadableEndedDescriptor ) {
281+ Object . defineProperty (
282+ process . stdin ,
283+ "readableEnded" ,
284+ stdinReadableEndedDescriptor ,
285+ ) ;
286+ } else {
287+ delete ( process . stdin as unknown as { readableEnded ?: boolean } ) . readableEnded ;
288+ }
289+ if ( stdinDestroyedDescriptor ) {
290+ Object . defineProperty ( process . stdin , "destroyed" , stdinDestroyedDescriptor ) ;
291+ } else {
292+ delete ( process . stdin as unknown as { destroyed ?: boolean } ) . destroyed ;
293+ }
272294 if ( stdoutIsTTYDescriptor ) {
273295 Object . defineProperty ( process . stdout , "isTTY" , stdoutIsTTYDescriptor ) ;
274296 } else {
275297 delete ( process . stdout as unknown as { isTTY ?: boolean } ) . isTTY ;
276298 }
277299}
278300
301+ function setOpenStdinState ( ) : void {
302+ Object . defineProperty ( process . stdin , "readableEnded" , {
303+ value : false ,
304+ configurable : true ,
305+ } ) ;
306+ Object . defineProperty ( process . stdin , "destroyed" , {
307+ value : false ,
308+ configurable : true ,
309+ } ) ;
310+ }
311+
279312function createDeferred < T > ( ) : {
280313 promise : Promise < T > ;
281314 resolve : ( value : T | PromiseLike < T > ) => void ;
@@ -557,7 +590,7 @@ function readSettingsHubPanelContract(): string[] {
557590}
558591
559592describe ( "codex manager cli commands" , ( ) => {
560- beforeEach ( ( ) => {
593+ beforeEach ( async ( ) => {
561594 vi . resetModules ( ) ;
562595 vi . clearAllMocks ( ) ;
563596 loadAccountsMock . mockReset ( ) ;
@@ -574,6 +607,7 @@ describe("codex manager cli commands", () => {
574607 loadCodexCliStateMock . mockReset ( ) ;
575608 promptAddAnotherAccountMock . mockReset ( ) ;
576609 promptLoginModeMock . mockReset ( ) ;
610+ promptQuestionMock . mockReset ( ) ;
577611 fetchCodexQuotaSnapshotMock . mockReset ( ) ;
578612 loadDashboardDisplaySettingsMock . mockReset ( ) ;
579613 saveDashboardDisplaySettingsMock . mockReset ( ) ;
@@ -583,6 +617,12 @@ describe("codex manager cli commands", () => {
583617 savePluginConfigMock . mockReset ( ) ;
584618 selectMock . mockReset ( ) ;
585619 confirmMock . mockReset ( ) ;
620+ planOcChatgptSyncMock . mockReset ( ) ;
621+ applyOcChatgptSyncMock . mockReset ( ) ;
622+ runNamedBackupExportMock . mockReset ( ) ;
623+ exportNamedBackupMock . mockReset ( ) ;
624+ detectOcChatgptMultiAuthTargetMock . mockReset ( ) ;
625+ normalizeAccountStorageMock . mockReset ( ) ;
586626 confirmMock . mockResolvedValue ( true ) ;
587627 fetchCodexQuotaSnapshotMock . mockResolvedValue ( {
588628 status : 200 ,
@@ -727,8 +767,35 @@ describe("codex manager cli commands", () => {
727767 selectMock . mockResolvedValue ( undefined ) ;
728768 getNamedBackupsMock . mockResolvedValue ( [ ] ) ;
729769 restoreTTYDescriptors ( ) ;
770+ setOpenStdinState ( ) ;
730771 setStoragePathMock . mockReset ( ) ;
731772 getStoragePathMock . mockReturnValue ( "/mock/openai-codex-accounts.json" ) ;
773+ normalizeAccountStorageMock . mockImplementation ( ( value ) => value ) ;
774+
775+ const authModule = await import ( "../lib/auth/auth.js" ) ;
776+ vi . mocked ( authModule . createAuthorizationFlow ) . mockReset ( ) ;
777+ vi . mocked ( authModule . exchangeAuthorizationCode ) . mockReset ( ) ;
778+ vi . mocked ( authModule . parseAuthorizationInput ) . mockReset ( ) ;
779+ vi . mocked ( authModule . parseAuthorizationInput ) . mockImplementation (
780+ ( input : string ) => {
781+ const codeMatch = input . match ( / c o d e = ( [ ^ & ] + ) / ) ;
782+ const stateMatch = input . match ( / s t a t e = ( [ ^ & # ] + ) / ) ;
783+ return {
784+ code : codeMatch ?. [ 1 ] ,
785+ state : stateMatch ?. [ 1 ] ,
786+ } ;
787+ } ,
788+ ) ;
789+
790+ const browserModule = await import ( "../lib/auth/browser.js" ) ;
791+ vi . mocked ( browserModule . isBrowserLaunchSuppressed ) . mockReset ( ) ;
792+ vi . mocked ( browserModule . isBrowserLaunchSuppressed ) . mockReturnValue ( false ) ;
793+ vi . mocked ( browserModule . openBrowserUrl ) . mockReset ( ) ;
794+ vi . mocked ( browserModule . copyTextToClipboard ) . mockReset ( ) ;
795+ vi . mocked ( browserModule . copyTextToClipboard ) . mockReturnValue ( true ) ;
796+
797+ const serverModule = await import ( "../lib/auth/server.js" ) ;
798+ vi . mocked ( serverModule . startLocalOAuthServer ) . mockReset ( ) ;
732799 } ) ;
733800
734801 afterEach ( ( ) => {
@@ -5350,6 +5417,54 @@ describe("codex manager cli commands", () => {
53505417 expect ( storageState . accounts ) . toHaveLength ( 0 ) ;
53515418 } ) ;
53525419
5420+ it ( "skips manual callback prompting when stdin is already closed in non-tty mode" , async ( ) => {
5421+ setInteractiveTTY ( false ) ;
5422+ setOpenStdinState ( ) ;
5423+ Object . defineProperty ( process . stdin , "readableEnded" , {
5424+ value : true ,
5425+ configurable : true ,
5426+ } ) ;
5427+ let storageState = {
5428+ version : 3 as const ,
5429+ activeIndex : 0 ,
5430+ activeIndexByFamily : { codex : 0 } ,
5431+ accounts : [ ] as Array < Record < string , unknown > > ,
5432+ } ;
5433+ loadAccountsMock . mockImplementation ( async ( ) =>
5434+ structuredClone ( storageState ) ,
5435+ ) ;
5436+ saveAccountsMock . mockImplementation ( async ( nextStorage ) => {
5437+ storageState = structuredClone ( nextStorage ) ;
5438+ } ) ;
5439+ promptLoginModeMock . mockResolvedValueOnce ( { mode : "cancel" } ) ;
5440+
5441+ const authModule = await import ( "../lib/auth/auth.js" ) ;
5442+ vi . mocked ( authModule . createAuthorizationFlow ) . mockResolvedValueOnce ( {
5443+ pkce : { challenge : "pkce-challenge" , verifier : "pkce-verifier" } ,
5444+ state : "oauth-state" ,
5445+ url : "https://auth.openai.com/mock" ,
5446+ } ) ;
5447+ const exchangeAuthorizationCodeMock = vi . mocked (
5448+ authModule . exchangeAuthorizationCode ,
5449+ ) ;
5450+
5451+ const browserModule = await import ( "../lib/auth/browser.js" ) ;
5452+ const openBrowserUrlMock = vi . mocked ( browserModule . openBrowserUrl ) ;
5453+ const serverModule = await import ( "../lib/auth/server.js" ) ;
5454+
5455+ const { runCodexMultiAuthCli } = await import ( "../lib/codex-manager.js" ) ;
5456+ const exitCode = await runCodexMultiAuthCli ( [ "auth" , "login" , "--manual" ] ) ;
5457+
5458+ expect ( exitCode ) . toBe ( 0 ) ;
5459+ expect ( promptQuestionMock ) . not . toHaveBeenCalled ( ) ;
5460+ expect ( openBrowserUrlMock ) . not . toHaveBeenCalled ( ) ;
5461+ expect (
5462+ vi . mocked ( serverModule . startLocalOAuthServer ) ,
5463+ ) . not . toHaveBeenCalled ( ) ;
5464+ expect ( exchangeAuthorizationCodeMock ) . not . toHaveBeenCalled ( ) ;
5465+ expect ( storageState . accounts ) . toHaveLength ( 0 ) ;
5466+ } ) ;
5467+
53535468 it ( "falls back to pasted manual input when Windows-style callback bind fails" , async ( ) => {
53545469 setInteractiveTTY ( false ) ;
53555470 const now = Date . now ( ) ;
0 commit comments