Skip to content

Commit 2135e76

Browse files
committed
refactor(snapshots): extract normalizers, test actual exports
Extract parsing logic from snapshotClient.ts into exported helpers: - normalizeSnapshotListResponse(raw) - normalizeSnapshotStatusResponse(raw) - normalizeSnapshotUnchanged(raw) snapshotClient.ts and snapshotService.ts now use these helpers. tests/snapshot-compat.ts imports and tests the ACTUAL exported functions, not simulated parsers that could drift from real implementation. 29 assertions, 0 failures.
1 parent bb9e919 commit 2135e76

3 files changed

Lines changed: 171 additions & 157 deletions

File tree

src/snapshots/snapshotService.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
requestSnapshotNow,
1010
requestPrune,
1111
restoreFromSnapshot,
12+
normalizeSnapshotUnchanged,
1213
type SnapshotIndex,
1314
} from "../sync/snapshotClient";
1415
import { VaultSync } from "../sync/vaultSync";
@@ -86,7 +87,7 @@ export class SnapshotService {
8687
);
8788
if (result.status === "created" && result.index) {
8889
// Handle both new and old server response field names
89-
const identical = result.snapshotIdenticalToLatest ?? result.semanticUnchanged;
90+
const identical = normalizeSnapshotUnchanged(result);
9091
const unchangedNote = identical
9192
? " (note: identical to latest snapshot)"
9293
: "";

src/sync/snapshotClient.ts

Lines changed: 52 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -259,12 +259,20 @@ export async function listSnapshots(
259259
trace?: TraceHttpContext,
260260
): Promise<SnapshotIndex[]> {
261261
const result = await serverGet(settings, "snapshots?limit=50", trace);
262-
// Handle both shapes: old servers return { snapshots }, new servers
263-
// also return { snapshots } by default. Array response is not expected
264-
// but handle it defensively for any edge case.
265-
if (Array.isArray(result)) return result as SnapshotIndex[];
266-
const obj = result as { snapshots?: SnapshotIndex[] };
267-
return obj.snapshots ?? [];
262+
return normalizeSnapshotListResponse(result);
263+
}
264+
265+
/**
266+
* Normalize a raw snapshot list response into a SnapshotIndex array.
267+
* Handles: bare array, { snapshots: [...] }, or { snapshots: [...], ...metadata }.
268+
*/
269+
export function normalizeSnapshotListResponse(raw: unknown): SnapshotIndex[] {
270+
if (Array.isArray(raw)) return raw as SnapshotIndex[];
271+
if (raw && typeof raw === "object" && "snapshots" in raw) {
272+
const arr = (raw as { snapshots?: unknown }).snapshots;
273+
if (Array.isArray(arr)) return arr as SnapshotIndex[];
274+
}
275+
return [];
268276
}
269277

270278
/**
@@ -296,27 +304,56 @@ export async function getSnapshotStatus(
296304
settings: VaultSyncSettings,
297305
trace?: TraceHttpContext,
298306
): Promise<SnapshotStatus> {
299-
const raw = await serverGet(settings, "snapshots/status", trace) as Record<string, unknown>;
307+
const raw = await serverGet(settings, "snapshots/status", trace);
308+
return normalizeSnapshotStatusResponse(raw);
309+
}
300310

301-
// Parse with fallbacks for old server field names
311+
/**
312+
* Normalize a raw status response into SnapshotStatus.
313+
* Falls back to old field names (snapshotCount, estimatedStorageBytes, pinnedCount)
314+
* when new LowerBound-suffixed fields are absent.
315+
*/
316+
export function normalizeSnapshotStatusResponse(raw: unknown): SnapshotStatus {
317+
if (!raw || typeof raw !== "object") {
318+
return {
319+
snapshotCountLowerBound: 0,
320+
listedSnapshotCount: 0,
321+
listingLimited: false,
322+
estimatedStorageBytesLowerBound: 0,
323+
latestSnapshotId: null,
324+
latestCreatedAt: null,
325+
pinnedCountLowerBound: 0,
326+
};
327+
}
328+
const r = raw as Record<string, unknown>;
302329
return {
303330
snapshotCountLowerBound:
304-
(raw.snapshotCountLowerBound as number) ?? (raw.snapshotCount as number) ?? 0,
331+
(r.snapshotCountLowerBound as number) ?? (r.snapshotCount as number) ?? 0,
305332
listedSnapshotCount:
306-
(raw.listedSnapshotCount as number) ?? (raw.snapshotCount as number) ?? 0,
333+
(r.listedSnapshotCount as number) ?? (r.snapshotCount as number) ?? 0,
307334
listingLimited:
308-
(raw.listingLimited as boolean) ?? false,
335+
(r.listingLimited as boolean) ?? false,
309336
estimatedStorageBytesLowerBound:
310-
(raw.estimatedStorageBytesLowerBound as number) ?? (raw.estimatedStorageBytes as number) ?? 0,
337+
(r.estimatedStorageBytesLowerBound as number) ?? (r.estimatedStorageBytes as number) ?? 0,
311338
latestSnapshotId:
312-
(raw.latestSnapshotId as string | null) ?? null,
339+
(r.latestSnapshotId as string | null) ?? null,
313340
latestCreatedAt:
314-
(raw.latestCreatedAt as string | null) ?? null,
341+
(r.latestCreatedAt as string | null) ?? null,
315342
pinnedCountLowerBound:
316-
(raw.pinnedCountLowerBound as number) ?? (raw.pinnedCount as number) ?? 0,
343+
(r.pinnedCountLowerBound as number) ?? (r.pinnedCount as number) ?? 0,
317344
};
318345
}
319346

347+
/**
348+
* Normalize the "identical to latest" field from a manual snapshot response.
349+
* Handles both new (snapshotIdenticalToLatest) and old (semanticUnchanged) field names.
350+
*/
351+
export function normalizeSnapshotUnchanged(raw: unknown): boolean {
352+
if (!raw || typeof raw !== "object") return false;
353+
const r = raw as Record<string, unknown>;
354+
return !!(r.snapshotIdenticalToLatest ?? r.semanticUnchanged);
355+
}
356+
320357
// -------------------------------------------------------------------
321358
// Snapshot download + decode
322359
// -------------------------------------------------------------------

0 commit comments

Comments
 (0)