Offline: Fotos bei abgelaufener Testphase nicht mehr verwerfen

Upload-Antwort 422 trial_expired gilt als vorübergehend: Datei bleibt auf dem Gerät,
wartende Op bleibt in der Warteschlange, der Sync-Durchlauf stoppt mit Backoff.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-15 19:23:14 +02:00
co-authored by Claude Opus 5
parent a8fedc390f
commit dd6a67d329
3 changed files with 31 additions and 6 deletions
+12 -1
View File
@@ -43,7 +43,7 @@ const at = (ms: number) => new Date(T0.getTime() + ms);
const fixedRandom = () => 1; // backoff = full step
/** Fake server: records every batch, answers per op via `decide`. */
function fakeTransport(decide: (op: SyncOperationInput) => SyncOpResult, opts: { failBatches?: number; uploadError?: "network" | "invalid" } = {}) {
function fakeTransport(decide: (op: SyncOperationInput) => SyncOpResult, opts: { failBatches?: number; uploadError?: "network" | "invalid" | "suspended" } = {}) {
const batches: SyncOperationInput[][] = [];
const uploads: string[] = [];
const log: string[] = [];
@@ -230,6 +230,17 @@ async function main() {
const p = await runSyncPass({ store: store3, ctx: A, transport: offline.transport, deviceId: "d", now: () => at(10), random: fixedRandom });
const blob3 = await store3.getBlob(c3);
ok(p.stopped === "network" && blob3?.status === "failed" && blob3.nextAttemptAt === at(2010).toISOString() && offline.batches.length === 0, "Upload ohne Netz → Backoff, Op wartet");
// L15: expired trial (read-only tenant) must not discard photos
const store5 = createMemoryStore();
const c5 = randomUUID();
const op5 = await enqueueOp(store5, A, { opType: "photo.attach", payload: { workOrderId: "wo-1", documentId: await enqueueBlob(store5, A, { clientId: c5, workOrderId: "wo-1", kind: "photo", blob: new Blob(["x"]), fileName: "f" }, at(0)) } }, randomUUID(), at(1));
const suspended = fakeTransport((o) => applied(o), { uploadError: "suspended" });
const p5 = await runSyncPass({ store: store5, ctx: A, transport: suspended.transport, deviceId: "d", now: () => at(10), random: fixedRandom });
const blob5 = await store5.getBlob(c5);
const op5After = (await store5.listOps(ctxKeyOf(A))).find((o) => o.clientOpId === op5.clientOpId);
ok(p5.stopped === "suspended" && p5.rejected === 0 && suspended.batches.length === 0, "Testphase abgelaufen → Pass stoppt, nichts verworfen");
ok(!!blob5 && blob5.nextAttemptAt !== null && blob5.lastError?.message === "trial_expired" && op5After?.status === "queued", "Foto bleibt auf dem Gerät, Op wartet (Backoff)");
}
console.log("\n— idMap / verkettete baseVersion —");