Files
jellytau/src/lib/services/pendingSync.logic.test.ts
T
dtourolle ad48d89dfe chore(format): run prettier over src/ and scripts/
Formatting was configured but never enforced: `bun run format:check`
reported 199 unformatted files and ran in no workflow and in no git hook,
so .prettierrc (printWidth 100, trailing commas) described an intention
rather than the tree.

This is the one-time sweep that makes the check gateable. Whitespace and
token-reflow only -- no behavioural change: `bun run check` reports 0
errors and all 1053 frontend tests pass before and after.

Kept out of every other commit on purpose. A 199-file diff mixed with
real changes is unreviewable, and the next commit turns format:check
into a hard CI gate so this cannot silently accumulate again.
2026-08-21 17:41:44 +02:00

84 lines
2.9 KiB
TypeScript

// TRACES: UR-025 | DR-132 | UT-123
import { describe, it, expect } from "vitest";
import type { SyncQueueItem } from "$lib/api/bindings";
import {
describeOperation,
describeSubject,
isStuck,
summarize,
sortForDisplay,
} from "./pendingSync.logic";
function row(overrides: Partial<SyncQueueItem> = {}): SyncQueueItem {
return {
id: 1,
userId: "u1",
operation: "report_playback_stopped",
itemId: "ep1",
payload: null,
status: "pending",
retryCount: 0,
createdAt: "2026-08-01T10:00:00Z",
errorMessage: null,
itemName: null,
...overrides,
} as SyncQueueItem;
}
describe("pending sync row description", () => {
it("labels the operations the backend can queue", () => {
expect(describeOperation("report_playback_stopped")).toBe("Watch position");
expect(describeOperation("mark_played")).toBe("Marked as watched");
expect(describeOperation("report_playback_start")).toBe("Playback started");
});
it("still renders an operation it has no label for", () => {
// An unlabelled row is the one heading for abandonment — it must not
// render blank, which is the failure this whole surface exists to fix.
expect(describeOperation("teleport_item")).toBe("teleport item");
});
it("names the item when the catalog knows it, and falls back to the id", () => {
expect(describeSubject(row({ itemName: "The Expanse S01E01" }))).toBe("The Expanse S01E01");
expect(describeSubject(row({ itemName: null, itemId: "abc123" }))).toBe("abc123");
expect(describeSubject(row({ itemName: null, itemId: null }))).toBe("Unknown item");
});
});
describe("stuck rows", () => {
it("treats a failed or retried row as stuck", () => {
expect(isStuck(row({ status: "failed", retryCount: 1 }))).toBe(true);
expect(isStuck(row({ status: "pending", retryCount: 2 }))).toBe(true);
expect(isStuck(row())).toBe(false);
});
it("summarizes a mixed queue", () => {
const summary = summarize([row(), row({ id: 2, status: "failed", retryCount: 1 })]);
expect(summary).toEqual({ total: 2, stuck: 1, allStuck: false });
});
it("reports allStuck only when every row has failed", () => {
expect(summarize([row({ status: "failed", retryCount: 3 })]).allStuck).toBe(true);
expect(summarize([]).allStuck).toBe(false);
});
});
describe("display order", () => {
it("lists oldest first — the order they will be replayed in", () => {
const sorted = sortForDisplay([
row({ id: 3, createdAt: "2026-08-01T12:00:00Z" }),
row({ id: 1, createdAt: "2026-08-01T10:00:00Z" }),
row({ id: 2, createdAt: "2026-08-01T11:00:00Z" }),
]);
expect(sorted.map((r) => r.id)).toEqual([1, 2, 3]);
});
it("keeps timestamp-less rows instead of dropping them", () => {
const sorted = sortForDisplay([
row({ id: 2, createdAt: null }),
row({ id: 1, createdAt: "2026-08-01T10:00:00Z" }),
]);
expect(sorted.map((r) => r.id)).toEqual([1, 2]);
});
});