Skip to content

Commit cdacea6

Browse files
committed
test(infra): strengthen gateway lock cleanup proof
1 parent 8d3d0f3 commit cdacea6

2 files changed

Lines changed: 16 additions & 15 deletions

File tree

src/infra/gateway-lock.test.ts

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -378,34 +378,33 @@ describe("gateway lock", () => {
378378
});
379379

380380
it("closes handle and removes lock file when writeFile fails after open succeeds", async () => {
381-
// When fs.open succeeds but the subsequent writeFile fails (e.g. disk full),
382-
// the file handle must be closed and the partial lock file removed before
383-
// re-throwing to avoid a file descriptor leak.
384381
vi.useRealTimers();
385382
const env = await makeEnv();
386383
const { lockPath } = resolveLockPath(env);
387384

388385
const writeError = Object.assign(new Error("ENOSPC: no space left on device"), {
389386
code: "ENOSPC",
390387
});
391-
const closeMock = vi.fn<() => Promise<void>>().mockResolvedValue(undefined);
388+
const close = vi.fn<() => Promise<void>>().mockResolvedValue(undefined);
392389
const mockHandle = {
393-
writeFile: vi.fn().mockRejectedValue(writeError),
394-
close: closeMock,
390+
writeFile: vi.fn().mockImplementation(async () => {
391+
await fs.writeFile(lockPath, "partial", "utf8");
392+
throw writeError;
393+
}),
394+
close,
395395
};
396396

397397
const openSpy = vi.spyOn(fs, "open").mockResolvedValueOnce(mockHandle as never);
398-
const rmSpy = vi.spyOn(fs, "rm");
399398

400-
await expect(acquireForTest(env)).rejects.toBeInstanceOf(GatewayLockError);
399+
await expect(acquireForTest(env)).rejects.toMatchObject({
400+
name: "GatewayLockError",
401+
cause: writeError,
402+
});
401403

402-
// The handle must be closed so the file descriptor is not leaked.
403-
expect(closeMock).toHaveBeenCalledTimes(1);
404-
// The partially-written lock file must be removed so no stale lock lingers.
405-
expect(rmSpy).toHaveBeenCalledWith(lockPath, { force: true });
404+
expect(close).toHaveBeenCalledTimes(1);
405+
await expect(fs.access(lockPath)).rejects.toMatchObject({ code: "ENOENT" });
406406

407407
openSpy.mockRestore();
408-
rmSpy.mockRestore();
409408
});
410409

411410
it("clears stale lock on win32 when process cmdline is not a gateway", async () => {

src/infra/gateway-lock.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -273,10 +273,12 @@ export async function acquireGatewayLock(
273273
payload.startTime = startTime;
274274
}
275275
await handle.writeFile(JSON.stringify(payload), "utf8");
276-
} catch (writeErr) {
276+
} catch (error) {
277+
// Acquisition owns both resources until the release callback exists.
278+
// Unwind them if payload preparation fails before ownership transfers.
277279
await handle.close().catch(() => undefined);
278280
await fs.rm(lockPath, { force: true }).catch(() => undefined);
279-
throw writeErr;
281+
throw error;
280282
}
281283
return {
282284
lockPath,

0 commit comments

Comments
 (0)