Skip to content

Commit 1bb4473

Browse files
committed
fix: surface push lock release errors
1 parent 22dab2e commit 1bb4473

3 files changed

Lines changed: 76 additions & 3 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,7 @@ openclaw plugins list # should show openclaw-b2-backup
316316
pnpm test
317317
```
318318

319-
132 tests across 11 test files covering encryption round-trips, manifest diffing, snapshot filtering, file gathering, B2 client signing, debounce timing, CLI behavior, push coordination, and plugin registration.
319+
134 tests across 11 test files covering encryption round-trips, manifest diffing, snapshot filtering, file gathering, B2 client signing, debounce timing, CLI behavior, push coordination, and plugin registration.
320320

321321
## License
322322

src/push.test.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,70 @@ describe("push", () => {
191191
await fs.promises.rm(stateDir, { recursive: true, force: true });
192192
});
193193

194+
it("fails a successful push when releasing the lock fails", async () => {
195+
const stateDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "b2-state-"));
196+
const lockDir = path.join(stateDir, ".b2-backup-push.lock");
197+
await fs.promises.writeFile(path.join(stateDir, "openclaw.json"), "{}");
198+
const config: ResolvedB2BackupConfig = {
199+
keyId: "test-key",
200+
applicationKey: "test-secret",
201+
bucket: "test-bucket",
202+
region: "test-region",
203+
encrypt: false,
204+
};
205+
const b2 = mockB2();
206+
const logger = { info: vi.fn(), warn: vi.fn(), debug: vi.fn() };
207+
const rm = fs.promises.rm.bind(fs.promises);
208+
const rmSpy = vi.spyOn(fs.promises, "rm").mockImplementation(async (target, options) => {
209+
if (path.resolve(String(target)) === path.resolve(lockDir)) {
210+
throw new Error("release failed");
211+
}
212+
return rm(target, options);
213+
});
214+
215+
await expect(push(config, stateDir, b2, logger)).rejects.toThrow("release failed");
216+
expect(logger.warn).toHaveBeenCalledWith(
217+
expect.stringContaining("failed to release push lock"),
218+
);
219+
220+
rmSpy.mockRestore();
221+
await fs.promises.rm(stateDir, { recursive: true, force: true });
222+
});
223+
224+
it("preserves the push error when lock release also fails", async () => {
225+
const stateDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "b2-state-"));
226+
const lockDir = path.join(stateDir, ".b2-backup-push.lock");
227+
await fs.promises.writeFile(path.join(stateDir, "openclaw.json"), "{}");
228+
const config: ResolvedB2BackupConfig = {
229+
keyId: "test-key",
230+
applicationKey: "test-secret",
231+
bucket: "test-bucket",
232+
region: "test-region",
233+
encrypt: false,
234+
};
235+
const b2 = mockB2({
236+
putObject: vi.fn(async () => {
237+
throw new Error("upload failed");
238+
}),
239+
});
240+
const logger = { info: vi.fn(), warn: vi.fn(), debug: vi.fn() };
241+
const rm = fs.promises.rm.bind(fs.promises);
242+
const rmSpy = vi.spyOn(fs.promises, "rm").mockImplementation(async (target, options) => {
243+
if (path.resolve(String(target)) === path.resolve(lockDir)) {
244+
throw new Error("release failed");
245+
}
246+
return rm(target, options);
247+
});
248+
249+
await expect(push(config, stateDir, b2, logger)).rejects.toThrow("upload failed");
250+
expect(logger.warn).toHaveBeenCalledWith(
251+
expect.stringContaining("failed to release push lock"),
252+
);
253+
254+
rmSpy.mockRestore();
255+
await fs.promises.rm(stateDir, { recursive: true, force: true });
256+
});
257+
194258
it("adds a random suffix to snapshot IDs", () => {
195259
const snapshotId = createSnapshotId("2026-02-19T12:00:00.000Z");
196260

src/push.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,19 @@ export async function push(
4343
options?: PushOptions,
4444
): Promise<void> {
4545
const lock = await acquirePushLock(stateDir);
46+
let pushError: unknown;
4647
try {
4748
await pushWithLock(config, stateDir, b2, logger, options);
49+
} catch (err) {
50+
pushError = err;
51+
throw err;
4852
} finally {
49-
await lock.release();
53+
try {
54+
await lock.release();
55+
} catch (err) {
56+
logger.warn(`b2-backup: failed to release push lock: ${String(err)}`);
57+
if (!pushError) throw err;
58+
}
5059
}
5160
}
5261

@@ -193,7 +202,7 @@ async function acquirePushLock(stateDir: string): Promise<{ release: () => Promi
193202

194203
return {
195204
async release() {
196-
await fs.promises.rm(lockDir, { recursive: true, force: true }).catch(() => undefined);
205+
await fs.promises.rm(lockDir, { recursive: true, force: true });
197206
},
198207
};
199208
}

0 commit comments

Comments
 (0)