@@ -1699,10 +1699,15 @@ describe("codex-cli sync", () => {
16991699 const plan = await getLatestCodexCliSyncRollbackPlan ( ) ;
17001700 expect ( plan . status ) . toBe ( "unavailable" ) ;
17011701 expect ( plan . reason ) . toContain ( "missing" ) ;
1702+ expect ( plan . reason ) . toContain ( missingRun . rollbackSnapshot ?. name ?? "" ) ;
1703+ expect ( plan . reason ) . not . toContain ( missingRun . rollbackSnapshot ?. path ?? "" ) ;
17021704
17031705 const rollbackResult = await rollbackLatestCodexCliSync ( plan ) ;
17041706 expect ( rollbackResult . status ) . toBe ( "unavailable" ) ;
17051707 expect ( rollbackResult . reason ) . toContain ( "missing" ) ;
1708+ expect ( rollbackResult . reason ) . not . toContain (
1709+ missingRun . rollbackSnapshot ?. path ?? "" ,
1710+ ) ;
17061711 } ) ;
17071712
17081713 it . each ( [
@@ -1821,6 +1826,78 @@ describe("codex-cli sync", () => {
18211826 saveSpy . mockRestore ( ) ;
18221827 } ) ;
18231828
1829+ it ( "retries transient rollback save failures before succeeding" , async ( ) => {
1830+ const snapshotPath = join ( tempDir , "rollback-retry-snapshot.json" ) ;
1831+ await writeFile (
1832+ snapshotPath ,
1833+ JSON . stringify (
1834+ {
1835+ version : 3 ,
1836+ accounts : [
1837+ {
1838+ accountId : "acc_old" ,
1839+ accountIdSource : "token" ,
1840+ email : "old@example.com" ,
1841+ refreshToken : "refresh-old" ,
1842+ accessToken : "access-old" ,
1843+ addedAt : 1 ,
1844+ lastUsed : 1 ,
1845+ } ,
1846+ ] ,
1847+ activeIndex : 0 ,
1848+ activeIndexByFamily : { codex : 0 } ,
1849+ } satisfies AccountStorageV3 ,
1850+ null ,
1851+ 2 ,
1852+ ) ,
1853+ "utf-8" ,
1854+ ) ;
1855+
1856+ const recordedRun : CodexCliSyncRun = {
1857+ outcome : "changed" ,
1858+ runAt : 10 ,
1859+ sourcePath : accountsPath ,
1860+ targetPath : targetStoragePath ,
1861+ summary : {
1862+ sourceAccountCount : 1 ,
1863+ targetAccountCountBefore : 1 ,
1864+ targetAccountCountAfter : 1 ,
1865+ addedAccountCount : 0 ,
1866+ updatedAccountCount : 1 ,
1867+ unchangedAccountCount : 0 ,
1868+ destinationOnlyPreservedCount : 0 ,
1869+ selectionChanged : false ,
1870+ } ,
1871+ trigger : "manual" ,
1872+ rollbackSnapshot : {
1873+ name : "accounts-codex-cli-sync-snapshot-retry" ,
1874+ path : snapshotPath ,
1875+ } ,
1876+ } ;
1877+ await appendSyncHistoryEntry ( {
1878+ kind : "codex-cli-sync" ,
1879+ recordedAt : recordedRun . runAt ,
1880+ run : recordedRun ,
1881+ } ) ;
1882+
1883+ const transientError = Object . assign ( new Error ( "save busy" ) , {
1884+ code : "EBUSY" ,
1885+ } ) ;
1886+ const saveSpy = vi
1887+ . spyOn ( storageModule , "saveAccounts" )
1888+ . mockRejectedValueOnce ( transientError )
1889+ . mockResolvedValueOnce ( undefined ) ;
1890+
1891+ const plan = await getLatestCodexCliSyncRollbackPlan ( ) ;
1892+ expect ( plan . status ) . toBe ( "ready" ) ;
1893+
1894+ const rollbackResult = await rollbackLatestCodexCliSync ( plan ) ;
1895+ expect ( rollbackResult . status ) . toBe ( "restored" ) ;
1896+ expect ( saveSpy ) . toHaveBeenCalledTimes ( 2 ) ;
1897+
1898+ saveSpy . mockRestore ( ) ;
1899+ } ) ;
1900+
18241901 it ( "re-reads Codex CLI state on apply when forceRefresh is requested" , async ( ) => {
18251902 await writeFile (
18261903 accountsPath ,
0 commit comments