From 91b9596d6f73c0e61027fa4fe066d6145c531f20 Mon Sep 17 00:00:00 2001 From: Sucukdeluxe Date: Sat, 22 Aug 2026 12:24:18 +0200 Subject: [PATCH] fix: retain complete package failure telemetry Include immediately cleaned successful files and byte aggregates when finalizing package results so mixed cleanup states remain partial instead of failed. Carry additive download, offline, extraction, remux, and cleanup failure counters alongside the prioritized display phase, aggregate them into run results, and expose cleanup failures in the final summary. --- src/main/notification-events.ts | 13 ++++--- src/main/package-telemetry.ts | 21 +++++++++--- src/shared/types.ts | 5 +++ tests/notification-outbox.test.ts | 5 +++ tests/package-telemetry.test.ts | 56 +++++++++++++++++++++++++++++++ 5 files changed, 91 insertions(+), 9 deletions(-) diff --git a/src/main/notification-events.ts b/src/main/notification-events.ts index ed8c505..58b33a2 100644 --- a/src/main/notification-events.ts +++ b/src/main/notification-events.ts @@ -42,6 +42,7 @@ export interface RunResult { remuxFailures: number; downloadFailures: number; offlineFailures: number; + cleanupFailures: number; } export interface RunResultInput { @@ -289,10 +290,11 @@ export function buildRunResult(input: RunResultInput): RunResult { extractionDurationSeconds: sum((result) => result.extractionDurationSeconds), remuxDurationSeconds: sum((result) => result.remuxDurationSeconds), postProcessDurationSeconds: sum((result) => result.postProcessDurationSeconds), - extractionFailures: packages.reduce((total, result) => total + result.archiveOperations.filter((operation) => operation.status === "failed").length, 0), - remuxFailures: packages.reduce((total, result) => total + result.remuxOperations.filter((operation) => operation.status === "failed").length, 0), - downloadFailures: packages.filter((result) => result.failurePhase === "download").reduce((total, result) => total + result.failedFiles, 0), - offlineFailures: packages.filter((result) => /offline|not found|nicht gefunden/i.test(result.errorCategory)).reduce((total, result) => total + result.failedFiles, 0) + extractionFailures: sum((result) => result.extractionFailures), + remuxFailures: sum((result) => result.remuxFailures), + downloadFailures: sum((result) => result.downloadFailures), + offlineFailures: sum((result) => result.offlineFailures), + cleanupFailures: sum((result) => result.cleanupFailures) }; } @@ -323,7 +325,8 @@ export function buildRunNotificationEvent(result: RunResult): NotificationEvent { name: "Entpackfehler", value: String(result.extractionFailures), inline: true }, { name: "Remuxfehler", value: String(result.remuxFailures), inline: true }, { name: "Downloadfehler", value: String(result.downloadFailures), inline: true }, - { name: "Offline", value: String(result.offlineFailures), inline: true } + { name: "Offline", value: String(result.offlineFailures), inline: true }, + { name: "Cleanupfehler", value: String(result.cleanupFailures), inline: true } ] ); } diff --git a/src/main/package-telemetry.ts b/src/main/package-telemetry.ts index d1ec9e9..610cd05 100644 --- a/src/main/package-telemetry.ts +++ b/src/main/package-telemetry.ts @@ -123,7 +123,8 @@ export function finalizePackageResult(telemetry: PackageTelemetry): PackageResul .map((operation) => ({ ...operation, itemIds: [...operation.itemIds] })); const remuxOperations = (telemetry.remuxOperations ?? packageEntry.remuxOperations ?? []) .map((operation) => ({ ...operation })); - const completedDownloads = telemetry.items.filter((item) => item.status === "completed").length; + const cleanedCompletedDownloads = Math.max(0, Math.floor(finiteNonNegative(packageEntry.cleanedCompletedItemCount))); + const completedDownloads = cleanedCompletedDownloads + telemetry.items.filter((item) => item.status === "completed").length; const failedDownloads = telemetry.items.filter((item) => item.status === "failed"); const cancelledDownloads = telemetry.items.filter((item) => item.status === "cancelled").length; const failedArchives = archiveOperations.filter((operation) => operation.status === "failed").length; @@ -133,7 +134,12 @@ export function finalizePackageResult(telemetry: PackageTelemetry): PackageResul const audioStripFailures = Math.max(0, Math.floor(finiteNonNegative(packageEntry.audioStripSummary?.failed))); const remuxFailures = Math.max(failedRemuxOperations, audioStripFailures); const cleanupErrorCategory = String(telemetry.cleanupErrorCategory ?? packageEntry.cleanupErrorCategory ?? "").trim(); - const postProcessFailures = failedArchives + remuxFailures + (cleanupErrorCategory ? 1 : 0); + const downloadFailureCategories = failedDownloads.map((item) => + projectPackageFailureCategory("download", item.lastError || item.fullStatus) + ); + const offlineFailures = downloadFailureCategories.filter((category) => category === "Offline").length; + const cleanupFailures = cleanupErrorCategory ? 1 : 0; + const postProcessFailures = failedArchives + remuxFailures + cleanupFailures; const postProcessCancellations = cancelledArchives + cancelledRemuxOperations; const failedFiles = failedDownloads.length + postProcessFailures; const cancelledFiles = cancelledDownloads + postProcessCancellations; @@ -143,8 +149,10 @@ export function finalizePackageResult(telemetry: PackageTelemetry): PackageResul const postProcessStartedAt = finiteNonNegative(packageEntry.postProcessStartedAt); const postProcessCompletedAt = finiteNonNegative(packageEntry.postProcessCompletedAt); const completedAt = finiteNonNegative(packageEntry.terminalAt); - const downloadedBytes = telemetry.items.reduce((total, item) => total + finiteNonNegative(item.downloadedBytes), 0); - const totalBytes = telemetry.items.reduce((total, item) => total + finiteNonNegative(item.totalBytes ?? item.downloadedBytes), 0); + const downloadedBytes = finiteNonNegative(packageEntry.cleanedDownloadedBytes) + + telemetry.items.reduce((total, item) => total + finiteNonNegative(item.downloadedBytes), 0); + const totalBytes = finiteNonNegative(packageEntry.cleanedTotalBytes) + + telemetry.items.reduce((total, item) => total + finiteNonNegative(item.totalBytes ?? item.downloadedBytes), 0); const downloadDurationSeconds = durationSecondsBetween(startedAt, downloadEndedAt); const extractionDurationSeconds = sumOperationDurationSeconds(archiveOperations); const remuxDurationSeconds = sumOperationDurationSeconds(remuxOperations); @@ -177,6 +185,11 @@ export function finalizePackageResult(telemetry: PackageTelemetry): PackageResul successfulFiles, failedFiles, cancelledFiles, + downloadFailures: failedDownloads.length, + offlineFailures, + extractionFailures: failedArchives, + remuxFailures, + cleanupFailures, archiveCount: archiveOperations.length, partCount: archiveOperations.reduce((total, operation) => total + Math.max(0, Math.floor(finiteNonNegative(operation.partCount))), 0), outputCount: Math.max(0, Math.floor(finiteNonNegative(telemetry.outputCount ?? packageEntry.outputCount))), diff --git a/src/shared/types.ts b/src/shared/types.ts index 53eeee3..8f4506a 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -546,6 +546,11 @@ export interface PackageResult { successfulFiles: number; failedFiles: number; cancelledFiles: number; + downloadFailures: number; + offlineFailures: number; + extractionFailures: number; + remuxFailures: number; + cleanupFailures: number; archiveCount: number; partCount: number; outputCount: number; diff --git a/tests/notification-outbox.test.ts b/tests/notification-outbox.test.ts index 8a6802d..42cbabe 100644 --- a/tests/notification-outbox.test.ts +++ b/tests/notification-outbox.test.ts @@ -401,6 +401,11 @@ describe("NotificationOutbox", () => { successfulFiles: 0, failedFiles: 1, cancelledFiles: 0, + downloadFailures: 1, + offlineFailures: 0, + extractionFailures: 0, + remuxFailures: 0, + cleanupFailures: 0, archiveCount: 0, partCount: 0, outputCount: 0, diff --git a/tests/package-telemetry.test.ts b/tests/package-telemetry.test.ts index 1fb457f..9cbd4b0 100644 --- a/tests/package-telemetry.test.ts +++ b/tests/package-telemetry.test.ts @@ -11,6 +11,7 @@ import { durationSecondsBetween, finalizePackageResult } from "../src/main/package-telemetry"; +import { buildRunResult } from "../src/main/notification-events"; import { normalizeLoadedSession } from "../src/main/storage"; function packageEntry(overrides: Partial = {}): PackageEntry { @@ -174,6 +175,28 @@ describe("package lifecycle telemetry", () => { })); }); + it("includes immediately cleaned successes and bytes in a mixed package result", () => { + const failedItem = downloadItem("item-3", "failed"); + const result = finalizePackageResult(telemetry({ + package: packageEntry({ + status: "failed", + itemIds: [failedItem.id], + cleanedCompletedItemCount: 2, + cleanedDownloadedBytes: 2_000, + cleanedTotalBytes: 2_000 + }), + items: [failedItem] + })); + + expect(result).toEqual(expect.objectContaining({ + status: "partial", + successfulFiles: 2, + failedFiles: 1, + downloadedBytes: 2_000, + totalBytes: 3_000 + })); + }); + it("classifies a failed download without error text as a download-phase failure", () => { const item = { ...downloadItem("item-1", "failed"), lastError: "", fullStatus: "" }; const result = finalizePackageResult(telemetry({ @@ -319,6 +342,39 @@ describe("package lifecycle telemetry", () => { ]); }); + it("retains additive failure counters when cleanup is the primary failure phase", () => { + const failedItem = { ...downloadItem("item-1", "failed"), lastError: "Host ist offline" }; + const packageResult = finalizePackageResult(telemetry({ + items: [failedItem], + archiveOperations: [archiveOperation({ status: "failed", errorCategory: "archive-error" })], + remuxOperations: [remuxOperation({ status: "failed", errorCategory: "remux-error" })], + cleanupErrorCategory: "cleanup-error" + })); + const runResult = buildRunResult({ + id: "mixed-failures", + stopped: false, + startedAt: 1_000, + completedAt: 166_000, + packages: [packageResult] + }); + + expect(packageResult).toEqual(expect.objectContaining({ + failurePhase: "cleanup", + downloadFailures: 1, + offlineFailures: 1, + extractionFailures: 1, + remuxFailures: 1, + cleanupFailures: 1 + })); + expect(runResult).toEqual(expect.objectContaining({ + downloadFailures: 1, + offlineFailures: 1, + extractionFailures: 1, + remuxFailures: 1, + cleanupFailures: 1 + })); + }); + it("uses audio-strip outcomes when no individual remux operation was recorded", () => { const items = [downloadItem("item-1"), downloadItem("item-2")]; const result = finalizePackageResult(telemetry({