Skip to content

Commit 7d0614e

Browse files
committed
test: cover account snapshot metadata edges
(cherry picked from commit 51313b6)
1 parent 359089e commit 7d0614e

2 files changed

Lines changed: 60 additions & 9 deletions

File tree

lib/storage/account-snapshot.ts

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ export async function statSnapshot(
3333
}
3434

3535
/**
36-
* Build backup metadata for an account snapshot, treating ENOENT races as missing files.
36+
* Build backup metadata for an account snapshot, treating ENOENT load races as invalid snapshots
37+
* that were present when the initial stat succeeded.
3738
*/
3839
export async function describeAccountSnapshot(
3940
path: string,
@@ -56,18 +57,26 @@ export async function describeAccountSnapshot(
5657
try {
5758
const { normalized, schemaErrors, storedVersion } =
5859
await deps.loadAccountsFromPath(path);
59-
const resolvedStats =
60-
stats.bytes === undefined || stats.mtimeMs === undefined
61-
? await deps.statSnapshot(path)
62-
: stats;
60+
let resolvedStats = stats;
61+
for (
62+
let attempt = 0;
63+
attempt < 2 &&
64+
(resolvedStats.bytes === undefined || resolvedStats.mtimeMs === undefined);
65+
attempt += 1
66+
) {
67+
const refreshedStats = await deps.statSnapshot(path);
68+
if (refreshedStats.exists) {
69+
resolvedStats = refreshedStats;
70+
}
71+
}
6372
return {
6473
kind,
6574
path,
6675
index: deps.index,
6776
exists: true,
6877
valid: !!normalized,
69-
bytes: resolvedStats.bytes,
70-
mtimeMs: resolvedStats.mtimeMs,
78+
bytes: resolvedStats.bytes ?? 0,
79+
mtimeMs: resolvedStats.mtimeMs ?? 0,
7180
version: typeof storedVersion === "number" ? storedVersion : undefined,
7281
accountCount: normalized?.accounts.length,
7382
schemaErrors: schemaErrors.length > 0 ? schemaErrors : undefined,

test/account-snapshot.test.ts

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,15 @@ import { describe, expect, it, vi } from "vitest";
22
import { describeAccountSnapshot, statSnapshot } from "../lib/storage/account-snapshot.js";
33

44
describe("statSnapshot", () => {
5+
it("returns size and mtime for accessible snapshots", async () => {
6+
await expect(
7+
statSnapshot("accounts.json", {
8+
stat: vi.fn(async () => ({ size: 1234, mtimeMs: 5678 })),
9+
logWarn: vi.fn(),
10+
}),
11+
).resolves.toEqual({ exists: true, bytes: 1234, mtimeMs: 5678 });
12+
});
13+
514
it("returns missing metadata for ENOENT", async () => {
615
await expect(
716
statSnapshot("missing.json", {
@@ -16,7 +25,7 @@ describe("statSnapshot", () => {
1625
it("logs and returns missing metadata for non-ENOENT stat failures", async () => {
1726
const logWarn = vi.fn();
1827
await expect(
19-
statSnapshot("locked.json", {
28+
statSnapshot("denied.json", {
2029
stat: vi.fn(async () => {
2130
throw Object.assign(new Error("denied"), { code: "EACCES" });
2231
}),
@@ -25,7 +34,7 @@ describe("statSnapshot", () => {
2534
).resolves.toEqual({ exists: false });
2635
expect(logWarn).toHaveBeenCalledWith(
2736
"Failed to stat backup candidate",
28-
expect.objectContaining({ path: "locked.json" }),
37+
expect.objectContaining({ path: "denied.json" }),
2938
);
3039
});
3140
it("treats locked snapshots as existing when stat returns EBUSY", async () => {
@@ -164,6 +173,39 @@ describe("describeAccountSnapshot", () => {
164173
expect(statSnapshot).toHaveBeenCalledTimes(2);
165174
});
166175

176+
it("falls back to zeroed metadata after repeated stat locks", async () => {
177+
const statSnapshot = vi
178+
.fn()
179+
.mockResolvedValueOnce({ exists: true })
180+
.mockResolvedValueOnce({ exists: true })
181+
.mockResolvedValueOnce({ exists: true });
182+
183+
await expect(
184+
describeAccountSnapshot("accounts.json", "accounts-primary", {
185+
index: 0,
186+
statSnapshot,
187+
loadAccountsFromPath: vi.fn(async () => ({
188+
normalized: { accounts: [{ id: 1 }] },
189+
schemaErrors: [],
190+
storedVersion: 3,
191+
})),
192+
logWarn: vi.fn(),
193+
}),
194+
).resolves.toEqual({
195+
kind: "accounts-primary",
196+
path: "accounts.json",
197+
index: 0,
198+
exists: true,
199+
valid: true,
200+
bytes: 0,
201+
mtimeMs: 0,
202+
version: 3,
203+
accountCount: 1,
204+
});
205+
206+
expect(statSnapshot).toHaveBeenCalledTimes(3);
207+
});
208+
167209
it("returns invalid metadata when the loader fails", async () => {
168210
const logWarn = vi.fn();
169211
await expect(

0 commit comments

Comments
 (0)