@@ -193,6 +193,72 @@ describe("unified settings", () => {
193193 } ) ;
194194 } ) ;
195195
196+ it ( "keeps the last good backup when a concurrent writer updates the primary after a backup-derived read" , async ( ) => {
197+ const {
198+ getUnifiedSettingsPath,
199+ loadUnifiedPluginConfigSync,
200+ saveUnifiedPluginConfig,
201+ } = await import ( "../lib/unified-settings.js" ) ;
202+
203+ await saveUnifiedPluginConfig ( { codexMode : true , fetchTimeoutMs : 45_000 } ) ;
204+ await saveUnifiedPluginConfig ( { codexMode : false , fetchTimeoutMs : 90_000 } ) ;
205+ await fs . writeFile ( getUnifiedSettingsPath ( ) , "{ invalid json" , "utf8" ) ;
206+
207+ expect ( loadUnifiedPluginConfigSync ( ) ) . toEqual ( {
208+ codexMode : true ,
209+ fetchTimeoutMs : 45_000 ,
210+ } ) ;
211+
212+ const concurrentPrimary = {
213+ version : 1 ,
214+ pluginConfig : {
215+ codexMode : false ,
216+ fetchTimeoutMs : 150_000 ,
217+ retries : 4 ,
218+ } ,
219+ } ;
220+ const copySpy = vi . spyOn ( fs , "copyFile" ) ;
221+ const renameSpy = vi . spyOn ( fs , "rename" ) ;
222+ let injectedConcurrentWrite = false ;
223+ renameSpy . mockImplementationOnce ( async ( ) => {
224+ injectedConcurrentWrite = true ;
225+ await fs . writeFile (
226+ getUnifiedSettingsPath ( ) ,
227+ `${ JSON . stringify ( concurrentPrimary , null , 2 ) } \n` ,
228+ "utf8" ,
229+ ) ;
230+ const error = new Error ( "denied" ) as NodeJS . ErrnoException ;
231+ error . code = "EACCES" ;
232+ throw error ;
233+ } ) ;
234+
235+ try {
236+ await expect (
237+ saveUnifiedPluginConfig ( { codexMode : true , fetchTimeoutMs : 120_000 } ) ,
238+ ) . rejects . toThrow ( ) ;
239+ } finally {
240+ copySpy . mockRestore ( ) ;
241+ renameSpy . mockRestore ( ) ;
242+ }
243+
244+ expect ( injectedConcurrentWrite ) . toBe ( true ) ;
245+ expect ( copySpy ) . not . toHaveBeenCalledWith (
246+ getUnifiedSettingsPath ( ) ,
247+ `${ getUnifiedSettingsPath ( ) } .bak` ,
248+ ) ;
249+ const backupRecord = JSON . parse (
250+ await fs . readFile ( `${ getUnifiedSettingsPath ( ) } .bak` , "utf8" ) ,
251+ ) as { pluginConfig ?: Record < string , unknown > } ;
252+ expect ( backupRecord . pluginConfig ) . toEqual ( {
253+ codexMode : true ,
254+ fetchTimeoutMs : 45_000 ,
255+ } ) ;
256+ const primaryRecord = JSON . parse (
257+ await fs . readFile ( getUnifiedSettingsPath ( ) , "utf8" ) ,
258+ ) as { pluginConfig ?: Record < string , unknown > } ;
259+ expect ( primaryRecord . pluginConfig ) . toEqual ( concurrentPrimary . pluginConfig ) ;
260+ } ) ;
261+
196262 it ( "resumes snapshotting after a backup-derived write succeeds" , async ( ) => {
197263 const {
198264 getUnifiedSettingsPath,
0 commit comments