fix(notifications): redact failure details
This commit is contained in:
@@ -10,6 +10,7 @@ import type {
|
|||||||
NotificationEventType,
|
NotificationEventType,
|
||||||
NotificationPriority
|
NotificationPriority
|
||||||
} from "./notification-outbox";
|
} from "./notification-outbox";
|
||||||
|
import { projectPackageFailureCategory } from "./package-telemetry";
|
||||||
|
|
||||||
export interface PackageResultEnvelope {
|
export interface PackageResultEnvelope {
|
||||||
generation: number;
|
generation: number;
|
||||||
@@ -202,9 +203,10 @@ export function buildPackageNotificationEvent(
|
|||||||
{ name: "Archive", value: `${result.archiveCount} Gruppen · ${result.partCount} Parts · ${result.outputCount} Ausgaben`, inline: true }
|
{ name: "Archive", value: `${result.archiveCount} Gruppen · ${result.partCount} Parts · ${result.outputCount} Ausgaben`, inline: true }
|
||||||
];
|
];
|
||||||
if (result.failurePhase) {
|
if (result.failurePhase) {
|
||||||
|
const errorCategory = projectPackageFailureCategory(result.failurePhase, result.errorCategory);
|
||||||
fields.push({
|
fields.push({
|
||||||
name: "Fehler",
|
name: "Fehler",
|
||||||
value: `${failurePhaseLabel(result)}${result.errorCategory ? ` · ${result.errorCategory.slice(0, 256)}` : ""}`,
|
value: `${failurePhaseLabel(result)} · ${errorCategory}`,
|
||||||
inline: false
|
inline: false
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import fs from "node:fs";
|
|||||||
import fsp from "node:fs/promises";
|
import fsp from "node:fs/promises";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import type { DiscordEmbedFieldPayload } from "./notify";
|
import type { DiscordEmbedFieldPayload } from "./notify";
|
||||||
|
import { projectPackageFailureCategory } from "./package-telemetry";
|
||||||
|
import type { FailurePhase } from "../shared/types";
|
||||||
|
|
||||||
export type NotificationEventType =
|
export type NotificationEventType =
|
||||||
| "package_completed"
|
| "package_completed"
|
||||||
@@ -66,12 +68,31 @@ const EVENT_TYPES = new Set<NotificationEventType>([
|
|||||||
const MAX_EVENTS = 250;
|
const MAX_EVENTS = 250;
|
||||||
const MAX_RETRY_DELAY_MS = 10 * 60 * 1000;
|
const MAX_RETRY_DELAY_MS = 10 * 60 * 1000;
|
||||||
const DEFAULT_SHUTDOWN_TIMEOUT_MS = 3000;
|
const DEFAULT_SHUTDOWN_TIMEOUT_MS = 3000;
|
||||||
|
const PACKAGE_FAILURE_EVENT_TYPES = new Set<NotificationEventType>([
|
||||||
|
"package_partial",
|
||||||
|
"package_failed"
|
||||||
|
]);
|
||||||
|
const PACKAGE_FAILURE_PHASES = new Map<string, FailurePhase>([
|
||||||
|
["Download", "download"],
|
||||||
|
["Entpacken", "extract"],
|
||||||
|
["Remux", "remux"],
|
||||||
|
["Aufräumen", "cleanup"]
|
||||||
|
]);
|
||||||
|
|
||||||
function finiteInteger(value: unknown, fallback = 0): number {
|
function finiteInteger(value: unknown, fallback = 0): number {
|
||||||
const numeric = Number(value);
|
const numeric = Number(value);
|
||||||
return Number.isFinite(numeric) ? Math.max(0, Math.floor(numeric)) : fallback;
|
return Number.isFinite(numeric) ? Math.max(0, Math.floor(numeric)) : fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sanitizePackageFailureFieldValue(value: string): string {
|
||||||
|
const match = /^(Download|Entpacken|Remux|Aufräumen)(?:\s*·\s*(.*))?$/s.exec(value.trim());
|
||||||
|
if (!match) {
|
||||||
|
return "Unbekannt";
|
||||||
|
}
|
||||||
|
const phase = PACKAGE_FAILURE_PHASES.get(match[1]) ?? null;
|
||||||
|
return `${match[1]} · ${projectPackageFailureCategory(phase, match[2])}`;
|
||||||
|
}
|
||||||
|
|
||||||
function sanitizeEvent(value: unknown): NotificationEvent | null {
|
function sanitizeEvent(value: unknown): NotificationEvent | null {
|
||||||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||||
return null;
|
return null;
|
||||||
@@ -93,7 +114,10 @@ function sanitizeEvent(value: unknown): NotificationEvent | null {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const name = typeof field.name === "string" ? field.name.slice(0, 1024) : "";
|
const name = typeof field.name === "string" ? field.name.slice(0, 1024) : "";
|
||||||
const fieldValue = typeof field.value === "string" ? field.value.slice(0, 4096) : "";
|
const rawFieldValue = typeof field.value === "string" ? field.value.slice(0, 4096) : "";
|
||||||
|
const fieldValue = name === "Fehler" && PACKAGE_FAILURE_EVENT_TYPES.has(type)
|
||||||
|
? sanitizePackageFailureFieldValue(rawFieldValue)
|
||||||
|
: rawFieldValue;
|
||||||
return name && fieldValue ? [{ name, value: fieldValue, inline: Boolean(field.inline) }] : [];
|
return name && fieldValue ? [{ name, value: fieldValue, inline: Boolean(field.inline) }] : [];
|
||||||
})
|
})
|
||||||
: [];
|
: [];
|
||||||
|
|||||||
@@ -7,6 +7,31 @@ import type {
|
|||||||
RemuxOperationMetric
|
RemuxOperationMetric
|
||||||
} from "../shared/types";
|
} from "../shared/types";
|
||||||
|
|
||||||
|
export type PackageFailureCategory =
|
||||||
|
| "Netzwerk"
|
||||||
|
| "Timeout"
|
||||||
|
| "Offline"
|
||||||
|
| "Speicherplatz"
|
||||||
|
| "Berechtigung"
|
||||||
|
| "Download"
|
||||||
|
| "Entpacken"
|
||||||
|
| "Remux"
|
||||||
|
| "Cleanup"
|
||||||
|
| "Unbekannt";
|
||||||
|
|
||||||
|
const packageFailureCategories = new Map<string, PackageFailureCategory>([
|
||||||
|
["netzwerk", "Netzwerk"],
|
||||||
|
["timeout", "Timeout"],
|
||||||
|
["offline", "Offline"],
|
||||||
|
["speicherplatz", "Speicherplatz"],
|
||||||
|
["berechtigung", "Berechtigung"],
|
||||||
|
["download", "Download"],
|
||||||
|
["entpacken", "Entpacken"],
|
||||||
|
["remux", "Remux"],
|
||||||
|
["cleanup", "Cleanup"],
|
||||||
|
["unbekannt", "Unbekannt"]
|
||||||
|
]);
|
||||||
|
|
||||||
function finiteNonNegative(value: unknown): number {
|
function finiteNonNegative(value: unknown): number {
|
||||||
const number = Number(value);
|
const number = Number(value);
|
||||||
return Number.isFinite(number) && number > 0 ? number : 0;
|
return Number.isFinite(number) && number > 0 ? number : 0;
|
||||||
@@ -26,6 +51,34 @@ export function sumOperationDurationSeconds(operations: readonly { durationMs: n
|
|||||||
return durationMsToSeconds(operations.reduce((total, operation) => total + finiteNonNegative(operation.durationMs), 0));
|
return durationMsToSeconds(operations.reduce((total, operation) => total + finiteNonNegative(operation.durationMs), 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function projectPackageFailureCategory(failurePhase: FailurePhase, detail: unknown): PackageFailureCategory {
|
||||||
|
const normalized = String(detail ?? "").trim().slice(0, 2048).toLowerCase();
|
||||||
|
const knownCategory = packageFailureCategories.get(normalized);
|
||||||
|
if (knownCategory) {
|
||||||
|
return knownCategory;
|
||||||
|
}
|
||||||
|
if (/\b(?:e?timed?[\s_-]*out|timeout)\b|zeit(?:ü|ue)berschreitung/.test(normalized)) {
|
||||||
|
return "Timeout";
|
||||||
|
}
|
||||||
|
if (/\boffline\b|not[\s_-]*found|nicht[\s_-]*gefunden|dead[\s_-]*link|http\s*404/.test(normalized)) {
|
||||||
|
return "Offline";
|
||||||
|
}
|
||||||
|
if (/\benospc\b|disk[\s_-]*full|no[\s_-]+space[\s_-]+left|not[\s_-]+enough[\s_-]+(?:disk[\s_-]+)?space|insufficient[\s_-]+(?:disk[\s_-]+)?space|speicherplatz|datenträger[^\n]*voll/.test(normalized)) {
|
||||||
|
return "Speicherplatz";
|
||||||
|
}
|
||||||
|
if (/\beacces\b|\beperm\b|permission|access[\s_-]*denied|zugriff[^\n]*verweigert|berechtigung/.test(normalized)) {
|
||||||
|
return "Berechtigung";
|
||||||
|
}
|
||||||
|
if (/network|netzwerk|\beconn|\benet|\behost|\beai_again\b|\bdns\b|socket|connection|verbindung|fetch[\s_-]*failed/.test(normalized)) {
|
||||||
|
return "Netzwerk";
|
||||||
|
}
|
||||||
|
if (failurePhase === "download") return "Download";
|
||||||
|
if (failurePhase === "extract") return "Entpacken";
|
||||||
|
if (failurePhase === "remux") return "Remux";
|
||||||
|
if (failurePhase === "cleanup") return "Cleanup";
|
||||||
|
return "Unbekannt";
|
||||||
|
}
|
||||||
|
|
||||||
function classifyStatus(successfulFiles: number, failedFiles: number, cancelledFiles: number, packageCancelled: boolean): PackageResultStatus {
|
function classifyStatus(successfulFiles: number, failedFiles: number, cancelledFiles: number, packageCancelled: boolean): PackageResultStatus {
|
||||||
if (successfulFiles > 0 && (failedFiles > 0 || cancelledFiles > 0 || packageCancelled)) {
|
if (successfulFiles > 0 && (failedFiles > 0 || cancelledFiles > 0 || packageCancelled)) {
|
||||||
return "partial";
|
return "partial";
|
||||||
@@ -47,19 +100,19 @@ function getFailure(
|
|||||||
downloadErrors: readonly string[]
|
downloadErrors: readonly string[]
|
||||||
): { failurePhase: FailurePhase; errorCategory: string } {
|
): { failurePhase: FailurePhase; errorCategory: string } {
|
||||||
if (cleanupErrorCategory) {
|
if (cleanupErrorCategory) {
|
||||||
return { failurePhase: "cleanup", errorCategory: cleanupErrorCategory };
|
return { failurePhase: "cleanup", errorCategory: projectPackageFailureCategory("cleanup", cleanupErrorCategory) };
|
||||||
}
|
}
|
||||||
const failedRemux = remuxOperations.find((operation) => operation.status === "failed");
|
const failedRemux = remuxOperations.find((operation) => operation.status === "failed");
|
||||||
if (failedRemux || remuxFallbackFailures > 0) {
|
if (failedRemux || remuxFallbackFailures > 0) {
|
||||||
return { failurePhase: "remux", errorCategory: failedRemux?.errorCategory || "remux" };
|
return { failurePhase: "remux", errorCategory: projectPackageFailureCategory("remux", failedRemux?.errorCategory) };
|
||||||
}
|
}
|
||||||
const failedArchive = archiveOperations.find((operation) => operation.status === "failed");
|
const failedArchive = archiveOperations.find((operation) => operation.status === "failed");
|
||||||
if (failedArchive) {
|
if (failedArchive) {
|
||||||
return { failurePhase: "extract", errorCategory: failedArchive.errorCategory };
|
return { failurePhase: "extract", errorCategory: projectPackageFailureCategory("extract", failedArchive.errorCategory) };
|
||||||
}
|
}
|
||||||
const downloadError = downloadErrors.find(Boolean);
|
const downloadError = downloadErrors.find(Boolean);
|
||||||
if (downloadErrors.length > 0) {
|
if (downloadErrors.length > 0) {
|
||||||
return { failurePhase: "download", errorCategory: downloadError || "download" };
|
return { failurePhase: "download", errorCategory: projectPackageFailureCategory("download", downloadError) };
|
||||||
}
|
}
|
||||||
return { failurePhase: null, errorCategory: "" };
|
return { failurePhase: null, errorCategory: "" };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ import os from "node:os";
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||||
import { NotificationEvent, NotificationOutbox } from "../src/main/notification-outbox";
|
import { NotificationEvent, NotificationOutbox } from "../src/main/notification-outbox";
|
||||||
import { sendNotification } from "../src/main/notify";
|
import { buildPackageNotificationEvent } from "../src/main/notification-events";
|
||||||
|
import { buildNotifyRequest, sendNotification } from "../src/main/notify";
|
||||||
|
import type { PackageResult } from "../src/shared/types";
|
||||||
|
|
||||||
const tempDirs: string[] = [];
|
const tempDirs: string[] = [];
|
||||||
|
|
||||||
@@ -277,6 +279,93 @@ describe("NotificationOutbox", () => {
|
|||||||
expect(persisted(filePath).events[0]).toEqual(event("safe"));
|
expect(persisted(filePath).events[0]).toEqual(event("safe"));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("keeps private package failure details out of events, requests, and persisted state", async () => {
|
||||||
|
const filePath = createOutboxFile();
|
||||||
|
const privateDetails = "https://private.example.test/hook C:/Private/target alice@example.test token=SUPERSECRET";
|
||||||
|
const result: PackageResult = {
|
||||||
|
packageId: "pkg-private",
|
||||||
|
name: "Paket",
|
||||||
|
status: "failed",
|
||||||
|
startedAt: 1000,
|
||||||
|
downloadEndedAt: 2000,
|
||||||
|
postProcessStartedAt: 0,
|
||||||
|
completedAt: 2000,
|
||||||
|
downloadDurationSeconds: 1,
|
||||||
|
extractionDurationSeconds: 0,
|
||||||
|
remuxDurationSeconds: 0,
|
||||||
|
postProcessDurationSeconds: 0,
|
||||||
|
totalDurationSeconds: 1,
|
||||||
|
totalBytes: 1000,
|
||||||
|
downloadedBytes: 0,
|
||||||
|
averageDownloadSpeedBps: 0,
|
||||||
|
successfulFiles: 0,
|
||||||
|
failedFiles: 1,
|
||||||
|
cancelledFiles: 0,
|
||||||
|
archiveCount: 0,
|
||||||
|
partCount: 0,
|
||||||
|
outputCount: 0,
|
||||||
|
failurePhase: "download",
|
||||||
|
errorCategory: privateDetails,
|
||||||
|
archiveOperations: [],
|
||||||
|
remuxOperations: []
|
||||||
|
};
|
||||||
|
const notificationEvent = buildPackageNotificationEvent({ generation: 1, result }, 1000);
|
||||||
|
const request = buildNotifyRequest("https://discord.com/api/webhooks/123/abc", {
|
||||||
|
title: notificationEvent.payload.title,
|
||||||
|
message: notificationEvent.payload.description || "",
|
||||||
|
color: notificationEvent.payload.color,
|
||||||
|
fields: notificationEvent.payload.fields,
|
||||||
|
timestamp: notificationEvent.createdAt
|
||||||
|
});
|
||||||
|
const outbox = new NotificationOutbox({ filePath, send: async () => true, now: () => 1000 });
|
||||||
|
|
||||||
|
await outbox.enqueue(notificationEvent);
|
||||||
|
|
||||||
|
const eventText = JSON.stringify(notificationEvent);
|
||||||
|
const requestText = String(request.init.body);
|
||||||
|
const persistedText = fs.readFileSync(filePath, "utf8");
|
||||||
|
const sensitiveValues = [
|
||||||
|
"https://private.example.test/hook",
|
||||||
|
"C:/Private/target",
|
||||||
|
"alice@example.test",
|
||||||
|
"token=SUPERSECRET"
|
||||||
|
];
|
||||||
|
expect(notificationEvent.payload.fields.find((field) => field.name === "Fehler")?.value).toBe("Download · Download");
|
||||||
|
for (const sensitiveValue of sensitiveValues) {
|
||||||
|
expect(eventText).not.toContain(sensitiveValue);
|
||||||
|
expect(requestText).not.toContain(sensitiveValue);
|
||||||
|
expect(persistedText).not.toContain(sensitiveValue);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("projects private failure details from an existing outbox before persisting again", async () => {
|
||||||
|
const filePath = createOutboxFile();
|
||||||
|
const privateDetails = "https://private.example.test/hook C:/Private/target alice@example.test token=SUPERSECRET";
|
||||||
|
const legacyEvent = event("legacy-private", {
|
||||||
|
payload: {
|
||||||
|
title: "Paket fehlgeschlagen",
|
||||||
|
description: "Paket",
|
||||||
|
fields: [{ name: "Fehler", value: `Download · ${privateDetails}`, inline: false }]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
fs.writeFileSync(filePath, JSON.stringify({
|
||||||
|
version: 1,
|
||||||
|
events: [legacyEvent],
|
||||||
|
lastSuccessAt: 0,
|
||||||
|
lastFailureAt: 0
|
||||||
|
}), "utf8");
|
||||||
|
const outbox = new NotificationOutbox({ filePath, send: async () => true, now: () => 1000 });
|
||||||
|
|
||||||
|
await outbox.enqueue(event("safe"));
|
||||||
|
|
||||||
|
const persistedText = fs.readFileSync(filePath, "utf8");
|
||||||
|
expect(persisted(filePath).events[0].payload.fields[0]?.value).toBe("Download · Download");
|
||||||
|
expect(persistedText).not.toContain("https://private.example.test/hook");
|
||||||
|
expect(persistedText).not.toContain("C:/Private/target");
|
||||||
|
expect(persistedText).not.toContain("alice@example.test");
|
||||||
|
expect(persistedText).not.toContain("token=SUPERSECRET");
|
||||||
|
});
|
||||||
|
|
||||||
it("returns after the default three-second shutdown budget when sending hangs", async () => {
|
it("returns after the default three-second shutdown budget when sending hangs", async () => {
|
||||||
vi.useFakeTimers();
|
vi.useFakeTimers();
|
||||||
const filePath = createOutboxFile();
|
const filePath = createOutboxFile();
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ describe("package lifecycle telemetry", () => {
|
|||||||
partCount: 16,
|
partCount: 16,
|
||||||
archiveCount: 1,
|
archiveCount: 1,
|
||||||
failurePhase: "extract",
|
failurePhase: "extract",
|
||||||
errorCategory: "checksum"
|
errorCategory: "Entpacken"
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -170,7 +170,7 @@ describe("package lifecycle telemetry", () => {
|
|||||||
failedFiles: 1,
|
failedFiles: 1,
|
||||||
cancelledFiles: 0,
|
cancelledFiles: 0,
|
||||||
failurePhase: "download",
|
failurePhase: "download",
|
||||||
errorCategory: "download-error"
|
errorCategory: "Download"
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -185,10 +185,46 @@ describe("package lifecycle telemetry", () => {
|
|||||||
status: "failed",
|
status: "failed",
|
||||||
failedFiles: 1,
|
failedFiles: 1,
|
||||||
failurePhase: "download",
|
failurePhase: "download",
|
||||||
errorCategory: "download"
|
errorCategory: "Download"
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("projects private download failure details to a fixed category", () => {
|
||||||
|
const privateDetails = "https://private.example.test/hook C:/Private/target alice@example.test token=SUPERSECRET";
|
||||||
|
const item = {
|
||||||
|
...downloadItem("item-1", "failed"),
|
||||||
|
lastError: privateDetails,
|
||||||
|
fullStatus: privateDetails
|
||||||
|
};
|
||||||
|
const result = finalizePackageResult(telemetry({
|
||||||
|
package: packageEntry({ status: "failed", itemIds: [item.id] }),
|
||||||
|
items: [item]
|
||||||
|
}));
|
||||||
|
|
||||||
|
expect(result.errorCategory).toBe("Download");
|
||||||
|
expect(result.errorCategory).not.toContain("private.example.test");
|
||||||
|
expect(result.errorCategory).not.toContain("C:/Private/target");
|
||||||
|
expect(result.errorCategory).not.toContain("alice@example.test");
|
||||||
|
expect(result.errorCategory).not.toContain("SUPERSECRET");
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
["Host ist offline", "Offline"],
|
||||||
|
["Request ETIMEDOUT", "Timeout"],
|
||||||
|
["socket ECONNRESET", "Netzwerk"],
|
||||||
|
["ENOSPC: no space left on device", "Speicherplatz"],
|
||||||
|
["EACCES: permission denied", "Berechtigung"],
|
||||||
|
["nicht näher klassifizierbar", "Download"]
|
||||||
|
])("maps download failure detail %s to %s", (detail, expectedCategory) => {
|
||||||
|
const item = { ...downloadItem("item-1", "failed"), lastError: detail };
|
||||||
|
const result = finalizePackageResult(telemetry({
|
||||||
|
package: packageEntry({ status: "failed", itemIds: [item.id] }),
|
||||||
|
items: [item]
|
||||||
|
}));
|
||||||
|
|
||||||
|
expect(result.errorCategory).toBe(expectedCategory);
|
||||||
|
});
|
||||||
|
|
||||||
it("classifies a package with only cancelled work as cancelled", () => {
|
it("classifies a package with only cancelled work as cancelled", () => {
|
||||||
const item = downloadItem("item-1", "cancelled");
|
const item = downloadItem("item-1", "cancelled");
|
||||||
const result = finalizePackageResult(telemetry({
|
const result = finalizePackageResult(telemetry({
|
||||||
@@ -268,11 +304,19 @@ describe("package lifecycle telemetry", () => {
|
|||||||
const failedItem = downloadItem("item-1", "failed");
|
const failedItem = downloadItem("item-1", "failed");
|
||||||
const failedArchive = archiveOperation({ status: "failed", errorCategory: "archive-error" });
|
const failedArchive = archiveOperation({ status: "failed", errorCategory: "archive-error" });
|
||||||
const failedRemux = remuxOperation({ status: "failed", errorCategory: "remux-error" });
|
const failedRemux = remuxOperation({ status: "failed", errorCategory: "remux-error" });
|
||||||
|
const failures = [
|
||||||
|
finalizePackageResult(telemetry({ items: [failedItem], archiveOperations: [failedArchive], remuxOperations: [failedRemux], cleanupErrorCategory: "cleanup-error" })),
|
||||||
|
finalizePackageResult(telemetry({ items: [failedItem], archiveOperations: [failedArchive], remuxOperations: [failedRemux] })),
|
||||||
|
finalizePackageResult(telemetry({ items: [failedItem], archiveOperations: [failedArchive] })),
|
||||||
|
finalizePackageResult(telemetry({ items: [failedItem] }))
|
||||||
|
];
|
||||||
|
|
||||||
expect(finalizePackageResult(telemetry({ items: [failedItem], archiveOperations: [failedArchive], remuxOperations: [failedRemux], cleanupErrorCategory: "cleanup-error" })).failurePhase).toBe("cleanup");
|
expect(failures.map(({ failurePhase, errorCategory }) => ({ failurePhase, errorCategory }))).toEqual([
|
||||||
expect(finalizePackageResult(telemetry({ items: [failedItem], archiveOperations: [failedArchive], remuxOperations: [failedRemux] })).failurePhase).toBe("remux");
|
{ failurePhase: "cleanup", errorCategory: "Cleanup" },
|
||||||
expect(finalizePackageResult(telemetry({ items: [failedItem], archiveOperations: [failedArchive] })).failurePhase).toBe("extract");
|
{ failurePhase: "remux", errorCategory: "Remux" },
|
||||||
expect(finalizePackageResult(telemetry({ items: [failedItem] })).failurePhase).toBe("download");
|
{ failurePhase: "extract", errorCategory: "Entpacken" },
|
||||||
|
{ failurePhase: "download", errorCategory: "Download" }
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("uses audio-strip outcomes when no individual remux operation was recorded", () => {
|
it("uses audio-strip outcomes when no individual remux operation was recorded", () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user