real-debrid-downloader/tests/download-completion.test.ts
Sucukdeluxe 7d52d5a495 Deferred-Post-Processing Lifecycle härten (H1/H2/M1) + 0-Byte-Fix (H3) + Dead Code (N1)
Aus der Bug-Analyse (3 Subagents): die Deferred-Post-Processing-Pipeline war
nur halb ins Abbruch-/Lifecycle-Management integriert — gleiche Ecke wie der
v1.7.156-Datenverlust.

H1: abortPostProcessing (globaler Stop/Shutdown/clearAll/external) bricht jetzt
    auch packageDeferredPostProcessAbortControllers + die neue Hybrid-Map ab.
    Vorher rasten MKV-Move/Cleanup/Rename gegen den synchronen Shutdown-Save.

H2: Hybrid-Post-Extract (Rename+MKV-Collect) lief als komplett ungetracktes
    detached Promise. Jetzt in packageHybridPostProcessControllers (Set/Package)
    registriert — SYNCHRON vor dem Promise, mit shouldAbort an beide Aufrufe.
    Bewusst SEPARAT von der Deferred-Map, sonst würde runDeferredPostExtraction's
    replace-Logik die laufende Hybrid-Arbeit selbst killen (Advisor-Fund).
    Cancel/Reset/Stop stoppt jetzt laufende Hybrid-Verschiebungen.

M1: hasAnyDeferredPostProcessPending() — Scheduler-Abschluss + finishRun-Clear
    gaten darauf. Run endet/Summary feuert nicht mehr während im Hintergrund
    noch Dateien verschoben werden; Run-State wird nicht mehr mittendrin geleert.

H3: validateDownloadedFileCompletion akzeptierte 0-Byte bei source=stream-end
    (kein Content-Length, keine Provider-Größe) als "fertig". Jetzt ok:false
    -> bestehender download_underflow-Retry-Pfad. Verhindert leere Datei = komplett.

N1: toter (unerreichbarer) Disk-Fallback-Block in findReadyArchiveSets +
    verwaiste pendingItemStatus-Map entfernt (verhaltensneutral).

Bewusst übersprungen: M2 (blockAllPersistence — vorgeschlagener Reset wäre
unsicher, In-Memory-Session ist nach Import stale) und M3 (cancelPendingAsyncSaves
— Generation-Guard schützt Korrektheit bereits). Siehe tasks/todo.md.

8 neue Tests (tests/download-completion.test.ts) inkl. H3-Regression. 621 Tests grün.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 16:39:34 +02:00

65 lines
2.9 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { planDownloadCompletion, validateDownloadedFileCompletion } from "../src/main/download-completion";
describe("download-completion", () => {
describe("planDownloadCompletion", () => {
it("uses content-length when present", () => {
const plan = planDownloadCompletion({
existingBytes: 0, responseStatus: 200, contentLength: 1000,
totalFromRange: null, knownTotal: null, correctedTotal: null
});
expect(plan.source).toBe("content-length");
expect(plan.expectedTotal).toBe(1000);
});
it("falls back to stream-end when no size info is available", () => {
const plan = planDownloadCompletion({
existingBytes: 0, responseStatus: 200, contentLength: 0,
totalFromRange: null, knownTotal: null, correctedTotal: null
});
expect(plan.source).toBe("stream-end");
expect(plan.expectedTotal).toBeNull();
});
});
describe("validateDownloadedFileCompletion", () => {
const streamEnd = { expectedTotal: null, source: "stream-end" as const, canFinishEarly: false };
const contentLength = (n: number) => ({ expectedTotal: n, source: "content-length" as const, canFinishEarly: true });
const providerMeta = (n: number) => ({ expectedTotal: n, source: "provider-metadata" as const, canFinishEarly: false });
// H3 regression: a stream-end download (no Content-Length, no provider size)
// that yielded 0 bytes is a FAILED download, not a valid completion.
it("rejects a 0-byte stream-end download (H3)", () => {
const result = validateDownloadedFileCompletion({ actualBytes: 0, plan: streamEnd });
expect(result.ok).toBe(false);
expect(result.error).toContain("download_underflow");
});
it("accepts a non-empty stream-end download", () => {
const result = validateDownloadedFileCompletion({ actualBytes: 5_000_000, plan: streamEnd });
expect(result.ok).toBe(true);
expect(result.totalBytes).toBe(5_000_000);
});
it("rejects an underflowing content-length download", () => {
const result = validateDownloadedFileCompletion({ actualBytes: 400, plan: contentLength(1000), toleranceBytes: 0 });
expect(result.ok).toBe(false);
});
it("accepts a complete content-length download", () => {
const result = validateDownloadedFileCompletion({ actualBytes: 1000, plan: contentLength(1000) });
expect(result.ok).toBe(true);
});
it("rejects a 0-byte download even with known provider size", () => {
const result = validateDownloadedFileCompletion({ actualBytes: 0, plan: providerMeta(2000) });
expect(result.ok).toBe(false);
});
it("accepts provider-metadata download and flags size mismatch", () => {
const result = validateDownloadedFileCompletion({ actualBytes: 1900, plan: providerMeta(2000), toleranceBytes: 0 });
// provider-metadata: shorter than expected -> underflow rejected
expect(result.ok).toBe(false);
});
});
});