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>
65 lines
2.9 KiB
TypeScript
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);
|
|
});
|
|
});
|
|
});
|