@@ -98,6 +98,75 @@ describe("push", () => {
9898 await fs . promises . rm ( stateDir , { recursive : true , force : true } ) ;
9999 } ) ;
100100
101+ it ( "returns a lock error when another process wins the stale-lock retry" , async ( ) => {
102+ const stateDir = await fs . promises . mkdtemp ( path . join ( os . tmpdir ( ) , "b2-state-" ) ) ;
103+ const lockDir = path . join ( stateDir , ".b2-backup-push.lock" ) ;
104+ await fs . promises . writeFile ( path . join ( stateDir , "openclaw.json" ) , "{}" ) ;
105+ await fs . promises . mkdir ( lockDir ) ;
106+ await fs . promises . writeFile ( path . join ( lockDir , "owner.json" ) , JSON . stringify ( { pid : 12345 } ) ) ;
107+ const config : ResolvedB2BackupConfig = {
108+ keyId : "test-key" ,
109+ applicationKey : "test-secret" ,
110+ bucket : "test-bucket" ,
111+ region : "test-region" ,
112+ encrypt : false ,
113+ } ;
114+ const logger = { info : vi . fn ( ) , warn : vi . fn ( ) , debug : vi . fn ( ) } ;
115+ const rm = fs . promises . rm . bind ( fs . promises ) ;
116+ const rmSpy = vi . spyOn ( fs . promises , "rm" ) . mockImplementation ( async ( target , options ) => {
117+ await rm ( target , options ) ;
118+ if ( path . resolve ( String ( target ) ) === path . resolve ( lockDir ) ) {
119+ await fs . promises . mkdir ( lockDir , { recursive : true } ) ;
120+ await fs . promises . writeFile (
121+ path . join ( lockDir , "owner.json" ) ,
122+ JSON . stringify ( { pid : process . pid } ) ,
123+ ) ;
124+ }
125+ } ) ;
126+ const killSpy = vi . spyOn ( process , "kill" ) . mockImplementation ( ( ) => {
127+ const err = new Error ( "no such process" ) as Error & { code : string } ;
128+ err . code = "ESRCH" ;
129+ throw err ;
130+ } ) ;
131+
132+ await expect ( push ( config , stateDir , mockB2 ( ) , logger ) ) . rejects . toThrow ( PushLockError ) ;
133+ await expect ( push ( config , stateDir , mockB2 ( ) , logger ) ) . rejects . toThrow (
134+ "another push is already running" ,
135+ ) ;
136+
137+ rmSpy . mockRestore ( ) ;
138+ killSpy . mockRestore ( ) ;
139+ await fs . promises . rm ( stateDir , { recursive : true , force : true } ) ;
140+ } ) ;
141+
142+ it ( "removes the lock directory if writing owner metadata fails" , async ( ) => {
143+ const stateDir = await fs . promises . mkdtemp ( path . join ( os . tmpdir ( ) , "b2-state-" ) ) ;
144+ const lockDir = path . join ( stateDir , ".b2-backup-push.lock" ) ;
145+ const ownerPath = path . join ( lockDir , "owner.json" ) ;
146+ await fs . promises . writeFile ( path . join ( stateDir , "openclaw.json" ) , "{}" ) ;
147+ const config : ResolvedB2BackupConfig = {
148+ keyId : "test-key" ,
149+ applicationKey : "test-secret" ,
150+ bucket : "test-bucket" ,
151+ region : "test-region" ,
152+ encrypt : false ,
153+ } ;
154+ const logger = { info : vi . fn ( ) , warn : vi . fn ( ) , debug : vi . fn ( ) } ;
155+ const writeFile = fs . promises . writeFile . bind ( fs . promises ) ;
156+ const writeSpy = vi . spyOn ( fs . promises , "writeFile" ) . mockImplementation ( async ( file , data , options ) => {
157+ if ( path . resolve ( String ( file ) ) === path . resolve ( ownerPath ) ) {
158+ throw new Error ( "disk full" ) ;
159+ }
160+ return writeFile ( file , data , options ) ;
161+ } ) ;
162+
163+ await expect ( push ( config , stateDir , mockB2 ( ) , logger ) ) . rejects . toThrow ( "disk full" ) ;
164+ await expect ( fs . promises . access ( lockDir ) ) . rejects . toThrow ( ) ;
165+
166+ writeSpy . mockRestore ( ) ;
167+ await fs . promises . rm ( stateDir , { recursive : true , force : true } ) ;
168+ } ) ;
169+
101170 it ( "adds a random suffix to snapshot IDs" , ( ) => {
102171 const snapshotId = createSnapshotId ( "2026-02-19T12:00:00.000Z" ) ;
103172
0 commit comments