@@ -19,6 +19,8 @@ import {
1919const mocks = vi . hoisted ( ( ) => ( {
2020 cachedConfig : null as Config | null ,
2121 getApiKeyForProvider : vi . fn < ( provider : string ) => string > ( ) ,
22+ codexRead : vi . fn < ( ) => Promise < unknown > > ( ) ,
23+ codexGetValidAccessToken : vi . fn < ( ) => Promise < string > > ( ) ,
2224 setCachedConfig : vi . fn < ( config : Config ) => void > ( ) ,
2325 writeConfig : vi . fn < ( config : Config ) => Promise < void > > ( ) ,
2426} ) ) ;
@@ -38,6 +40,13 @@ vi.mock('./onboarding-ipc', () => ({
3840 } ,
3941} ) ) ;
4042
43+ vi . mock ( './codex-oauth-ipc' , ( ) => ( {
44+ getCodexTokenStore : ( ) => ( {
45+ read : mocks . codexRead ,
46+ getValidAccessToken : mocks . codexGetValidAccessToken ,
47+ } ) ,
48+ } ) ) ;
49+
4150vi . mock ( './keychain' , ( ) => ( {
4251 buildSecretRef : ( value : string ) => ( { ciphertext : value , mask : '***' } ) ,
4352 decryptSecret : ( value : string ) => value ,
@@ -92,41 +101,53 @@ function expectThrowCode(fn: () => unknown, code: string): void {
92101 throw new Error ( `Expected function to throw ${ code } ` ) ;
93102}
94103
104+ async function expectRejectCode ( promise : Promise < unknown > , code : string ) : Promise < void > {
105+ try {
106+ await promise ;
107+ } catch ( err ) {
108+ expect ( err ) . toMatchObject ( { code } ) ;
109+ return ;
110+ }
111+ throw new Error ( `Expected promise to reject ${ code } ` ) ;
112+ }
113+
95114describe ( 'image generation enablement' , ( ) => {
96115 afterEach ( ( ) => {
97116 mocks . cachedConfig = null ;
98117 getApiKeyForProviderMock . mockReset ( ) ;
118+ mocks . codexRead . mockReset ( ) ;
119+ mocks . codexGetValidAccessToken . mockReset ( ) ;
99120 mocks . setCachedConfig . mockReset ( ) ;
100121 mocks . writeConfig . mockReset ( ) ;
101122 } ) ;
102123
103- it ( 'disables generate_image_asset when image generation is turned off' , ( ) => {
124+ it ( 'disables generate_image_asset when image generation is turned off' , async ( ) => {
104125 const cfg = makeConfig ( false ) ;
105- expect ( isGenerateImageAssetEnabled ( cfg ) ) . toBe ( false ) ;
106- expect ( resolveImageGenerationConfig ( cfg ) ) . toBeNull ( ) ;
126+ await expect ( isGenerateImageAssetEnabled ( cfg ) ) . resolves . toBe ( false ) ;
127+ await expect ( resolveImageGenerationConfig ( cfg ) ) . resolves . toBeNull ( ) ;
107128 } ) ;
108129
109- it ( 'enables generate_image_asset when image generation is on and key is available' , ( ) => {
130+ it ( 'enables generate_image_asset when image generation is on and key is available' , async ( ) => {
110131 getApiKeyForProviderMock . mockReturnValue ( 'sk-openai' ) ;
111132 const cfg = makeConfig ( true ) ;
112- expect ( isGenerateImageAssetEnabled ( cfg ) ) . toBe ( true ) ;
113- expect ( resolveImageGenerationConfig ( cfg ) ) . toMatchObject ( {
133+ await expect ( isGenerateImageAssetEnabled ( cfg ) ) . resolves . toBe ( true ) ;
134+ await expect ( resolveImageGenerationConfig ( cfg ) ) . resolves . toMatchObject ( {
114135 provider : 'openai' ,
115136 model : 'gpt-image-2' ,
116137 apiKey : 'sk-openai' ,
117138 } ) ;
118139 } ) ;
119140
120- it ( 'throws when image generation is on but inherited key is unavailable' , ( ) => {
141+ it ( 'throws when image generation is on but inherited key is unavailable' , async ( ) => {
121142 getApiKeyForProviderMock . mockImplementation ( ( ) => {
122143 throw new CodesignError ( 'missing key' , ERROR_CODES . PROVIDER_KEY_MISSING ) ;
123144 } ) ;
124145 const cfg = makeConfig ( true ) ;
125- expect ( ( ) => isGenerateImageAssetEnabled ( cfg ) ) . toThrow ( / m i s s i n g k e y / ) ;
126- expect ( ( ) => resolveImageGenerationConfig ( cfg ) ) . toThrow ( / m i s s i n g k e y / ) ;
146+ await expect ( isGenerateImageAssetEnabled ( cfg ) ) . rejects . toThrow ( / m i s s i n g k e y / ) ;
147+ await expect ( resolveImageGenerationConfig ( cfg ) ) . rejects . toThrow ( / m i s s i n g k e y / ) ;
127148 } ) ;
128149
129- it ( 'throws PROVIDER_KEY_MISSING when custom credential mode has no custom key' , ( ) => {
150+ it ( 'throws PROVIDER_KEY_MISSING when custom credential mode has no custom key' , async ( ) => {
130151 const cfg = makeConfig ( true ) ;
131152 const parsed = hydrateConfig ( {
132153 version : 3 ,
@@ -146,38 +167,155 @@ describe('image generation enablement', () => {
146167 } ,
147168 } ) ;
148169
149- expectThrowCode ( ( ) => resolveImageGenerationConfig ( parsed ) , ERROR_CODES . PROVIDER_KEY_MISSING ) ;
170+ await expectRejectCode ( resolveImageGenerationConfig ( parsed ) , ERROR_CODES . PROVIDER_KEY_MISSING ) ;
150171 } ) ;
151172
152- it ( 'reports inheritedKeyAvailable=false in the view when the provider key is missing' , ( ) => {
173+ it ( 'reports inheritedKeyAvailable=false in the view when the provider key is missing' , async ( ) => {
153174 getApiKeyForProviderMock . mockImplementation ( ( ) => {
154175 throw new CodesignError ( 'missing key' , ERROR_CODES . PROVIDER_KEY_MISSING ) ;
155176 } ) ;
156177 const cfg = makeConfig ( true ) ;
157- const view = imageSettingsToView ( cfg . imageGeneration ) ;
178+ const view = await imageSettingsToView ( cfg . imageGeneration ) ;
158179 expect ( view . enabled ) . toBe ( true ) ;
159180 expect ( view . credentialMode ) . toBe ( 'inherit' ) ;
160181 expect ( view . inheritedKeyAvailable ) . toBe ( false ) ;
161182 expect ( view . hasCustomKey ) . toBe ( false ) ;
162183 } ) ;
163184
164- it ( 'reports inheritedKeyAvailable=true in the view when the provider key exists' , ( ) => {
185+ it ( 'reports inheritedKeyAvailable=true in the view when the provider key exists' , async ( ) => {
165186 getApiKeyForProviderMock . mockReturnValue ( 'sk-openai' ) ;
166187 const cfg = makeConfig ( true ) ;
167- const view = imageSettingsToView ( cfg . imageGeneration ) ;
188+ const view = await imageSettingsToView ( cfg . imageGeneration ) ;
168189 expect ( view . inheritedKeyAvailable ) . toBe ( true ) ;
169190 } ) ;
170191
171- it ( 'throws credential corruption instead of reporting it as a missing inherited key' , ( ) => {
192+ it ( 'throws credential corruption instead of reporting it as a missing inherited key' , async ( ) => {
172193 getApiKeyForProviderMock . mockImplementation ( ( ) => {
173194 throw new CodesignError ( 'decrypt failed' , ERROR_CODES . KEYCHAIN_UNAVAILABLE ) ;
174195 } ) ;
175196 const cfg = makeConfig ( true ) ;
176- expectThrowCode (
177- ( ) => imageSettingsToView ( cfg . imageGeneration ) ,
197+ await expectRejectCode (
198+ imageSettingsToView ( cfg . imageGeneration ) ,
178199 ERROR_CODES . KEYCHAIN_UNAVAILABLE ,
179200 ) ;
180- expectThrowCode ( ( ) => imageGenerationKeyAvailable ( cfg ) , ERROR_CODES . KEYCHAIN_UNAVAILABLE ) ;
201+ await expectRejectCode ( imageGenerationKeyAvailable ( cfg ) , ERROR_CODES . KEYCHAIN_UNAVAILABLE ) ;
202+ } ) ;
203+
204+ it ( 'resolves ChatGPT subscription image generation through the OAuth token store' , async ( ) => {
205+ mocks . codexGetValidAccessToken . mockResolvedValue ( 'oauth-token' ) ;
206+ const cfg = hydrateConfig ( {
207+ version : 3 ,
208+ activeProvider : 'chatgpt-codex' ,
209+ activeModel : 'gpt-5.5' ,
210+ providers : {
211+ 'chatgpt-codex' : {
212+ id : 'chatgpt-codex' ,
213+ name : 'ChatGPT subscription' ,
214+ builtin : false ,
215+ wire : 'openai-codex-responses' ,
216+ baseUrl : 'https://chatgpt.com/backend-api' ,
217+ defaultModel : 'gpt-5.5' ,
218+ requiresApiKey : false ,
219+ } ,
220+ } ,
221+ secrets : { } ,
222+ imageGeneration : {
223+ schemaVersion : IMAGE_GENERATION_SCHEMA_VERSION ,
224+ enabled : true ,
225+ provider : 'chatgpt-codex' ,
226+ credentialMode : 'inherit' ,
227+ model : 'gpt-5.5' ,
228+ quality : 'high' ,
229+ size : '1536x1024' ,
230+ outputFormat : 'png' ,
231+ } ,
232+ } ) ;
233+
234+ await expect ( resolveImageGenerationConfig ( cfg ) ) . resolves . toMatchObject ( {
235+ provider : 'chatgpt-codex' ,
236+ model : 'gpt-5.5' ,
237+ apiKey : 'oauth-token' ,
238+ baseUrl : 'https://chatgpt.com/backend-api' ,
239+ } ) ;
240+ expect ( getApiKeyForProviderMock ) . not . toHaveBeenCalled ( ) ;
241+ } ) ;
242+
243+ it ( 'reports ChatGPT subscription inherited credential availability from OAuth status' , async ( ) => {
244+ mocks . codexRead . mockResolvedValue ( {
245+ schemaVersion : 1 ,
246+ accessToken : 'token' ,
247+ refreshToken : 'refresh' ,
248+ idToken : 'id' ,
249+ expiresAt : Date . now ( ) + 1000 ,
250+ accountId : 'acct' ,
251+ email : 'person@example.com' ,
252+ updatedAt : Date . now ( ) ,
253+ } ) ;
254+ const view = await imageSettingsToView ( {
255+ schemaVersion : IMAGE_GENERATION_SCHEMA_VERSION ,
256+ enabled : true ,
257+ provider : 'chatgpt-codex' ,
258+ credentialMode : 'inherit' ,
259+ model : 'gpt-5.5' ,
260+ quality : 'high' ,
261+ size : '1536x1024' ,
262+ outputFormat : 'png' ,
263+ } ) ;
264+
265+ expect ( view . inheritedKeyAvailable ) . toBe ( true ) ;
266+ } ) ;
267+
268+ it ( 'reports ChatGPT subscription key availability from imageGenerationKeyAvailable' , async ( ) => {
269+ const cfg = hydrateConfig ( {
270+ version : 3 ,
271+ activeProvider : 'openai' ,
272+ activeModel : 'gpt-5.4' ,
273+ providers : {
274+ openai : {
275+ id : 'openai' ,
276+ name : 'OpenAI' ,
277+ builtin : true ,
278+ wire : 'openai-chat' ,
279+ baseUrl : 'https://api.openai.com/v1' ,
280+ defaultModel : 'gpt-5.4' ,
281+ } ,
282+ 'chatgpt-codex' : {
283+ id : 'chatgpt-codex' ,
284+ name : 'ChatGPT subscription' ,
285+ builtin : false ,
286+ wire : 'openai-codex-responses' ,
287+ baseUrl : 'https://chatgpt.com/backend-api' ,
288+ defaultModel : 'gpt-5.5' ,
289+ requiresApiKey : false ,
290+ } ,
291+ } ,
292+ secrets : { } ,
293+ imageGeneration : {
294+ schemaVersion : IMAGE_GENERATION_SCHEMA_VERSION ,
295+ enabled : true ,
296+ provider : 'chatgpt-codex' ,
297+ credentialMode : 'inherit' ,
298+ model : 'gpt-5.5' ,
299+ quality : 'high' ,
300+ size : '1536x1024' ,
301+ outputFormat : 'png' ,
302+ } ,
303+ } ) ;
304+
305+ mocks . codexRead . mockResolvedValue ( {
306+ schemaVersion : 1 ,
307+ accessToken : 'token' ,
308+ refreshToken : 'refresh' ,
309+ idToken : 'id' ,
310+ expiresAt : Date . now ( ) + 1000 ,
311+ accountId : 'acct' ,
312+ email : 'person@example.com' ,
313+ updatedAt : Date . now ( ) ,
314+ } ) ;
315+ await expect ( imageGenerationKeyAvailable ( cfg ) ) . resolves . toBe ( true ) ;
316+
317+ mocks . codexRead . mockResolvedValue ( null ) ;
318+ await expect ( imageGenerationKeyAvailable ( cfg ) ) . resolves . toBe ( false ) ;
181319 } ) ;
182320
183321 it ( 'clears provider-scoped custom keys when the image provider changes' , async ( ) => {
0 commit comments