From c2ba2250477bfc995e48db3a66295e68591dc82d Mon Sep 17 00:00:00 2001 From: Sucukdeluxe Date: Wed, 10 Jun 2026 00:37:40 +0200 Subject: [PATCH] Test: Regressionstests fuer die Failed-Transition-Benachrichtigung (Mixed-Paket, Dedup, Run-Guard, Marker-Freigabe) --- tests/notify-hooks.test.ts | 153 +++++++++++++++++++++++++++++++++++++ 1 file changed, 153 insertions(+) create mode 100644 tests/notify-hooks.test.ts diff --git a/tests/notify-hooks.test.ts b/tests/notify-hooks.test.ts new file mode 100644 index 0000000..1888bcc --- /dev/null +++ b/tests/notify-hooks.test.ts @@ -0,0 +1,153 @@ +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { afterEach, describe, expect, it, vi } from "vitest"; + +vi.mock("../src/main/notify", async (importActual) => { + const actual = await importActual(); + return { ...actual, sendNotification: vi.fn().mockResolvedValue(true) }; +}); + +import { DownloadManager } from "../src/main/download-manager"; +import { defaultSettings } from "../src/main/constants"; +import { createStoragePaths, emptySession } from "../src/main/storage"; +import { shutdownItemLogs } from "../src/main/item-log"; +import { shutdownPackageLogs } from "../src/main/package-log"; +import { shutdownRenameLog } from "../src/main/rename-log"; +import { sendNotification } from "../src/main/notify"; + +const mockedSend = sendNotification as unknown as ReturnType; +const tempDirs: string[] = []; + +afterEach(() => { + mockedSend.mockClear(); + shutdownItemLogs(); + shutdownPackageLogs(); + shutdownRenameLog(); + for (const dir of tempDirs.splice(0)) { + try { fs.rmSync(dir, { recursive: true, force: true }); } catch { /* ignore */ } + } +}); + +function setup(): { manager: DownloadManager; session: ReturnType } { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-nh-")); + tempDirs.push(root); + const session = emptySession(); + const manager = new DownloadManager( + { + ...defaultSettings(), + token: "rd-token", + outputDir: path.join(root, "out"), + extractDir: path.join(root, "extract"), + notifyUrl: "https://discord.com/api/webhooks/123/abc", + notifyOnPackageCompleted: true, + notifyOnPackageFailed: true + }, + session, + createStoragePaths(path.join(root, "state")) + ); + return { manager, session }; +} + +function addPackage(session: ReturnType, itemStatuses: string[]): any { + const pkgId = "pkg-1"; + const pkg: any = { + id: pkgId, + name: "Test.Show.S01", + outputDir: "C:/out", + extractDir: "C:/extract", + status: "queued", + itemIds: itemStatuses.map((_s, i) => `it-${i}`), + cancelled: false, + enabled: true, + priority: "normal", + createdAt: 1, + updatedAt: 1 + }; + session.packages[pkgId] = pkg; + session.packageOrder.push(pkgId); + itemStatuses.forEach((status, i) => { + session.items[`it-${i}`] = { + id: `it-${i}`, + packageId: pkgId, + url: `https://dummy/${i}`, + provider: null, + status, + retries: 0, + speedBps: 0, + downloadedBytes: 0, + totalBytes: null, + progressPercent: 0, + fileName: `f${i}.rar`, + targetPath: "", + resumable: true, + attempts: 1, + lastError: "", + fullStatus: "", + createdAt: 1, + updatedAt: 1 + } as any; + }); + return pkg; +} + +describe("refreshPackageStatus failed-transition notify", () => { + it("notifies a MIXED package (some success, last finisher failed) — the lost-webhook case", () => { + const { manager, session } = setup(); + const pkg = addPackage(session, ["completed", "failed"]); + session.running = true; + + (manager as any).refreshPackageStatus(pkg); + + expect(pkg.status).toBe("failed"); + expect(mockedSend).toHaveBeenCalledTimes(1); + expect(mockedSend.mock.calls[0][1].title).toBe("❌ Paket fehlgeschlagen"); + expect(mockedSend.mock.calls[0][1].message).toContain("1 von 2"); + }); + + it("notifies an all-failed package and dedups repeat refreshes", () => { + const { manager, session } = setup(); + const pkg = addPackage(session, ["failed", "failed"]); + session.running = true; + + (manager as any).refreshPackageStatus(pkg); + (manager as any).refreshPackageStatus(pkg); + + expect(pkg.status).toBe("failed"); + expect(mockedSend).toHaveBeenCalledTimes(1); + }); + + it("stays silent outside a run (startup recovery must not spam)", () => { + const { manager, session } = setup(); + const pkg = addPackage(session, ["failed"]); + session.running = false; + + (manager as any).refreshPackageStatus(pkg); + + expect(pkg.status).toBe("failed"); + expect(mockedSend).not.toHaveBeenCalled(); + }); + + it("does not notify while items are still pending", () => { + const { manager, session } = setup(); + const pkg = addPackage(session, ["failed", "queued"]); + session.running = true; + + (manager as any).refreshPackageStatus(pkg); + + expect(pkg.status).toBe("queued"); + expect(mockedSend).not.toHaveBeenCalled(); + }); + + it("releases the dedup marker when the send ultimately fails (retro-notify possible)", async () => { + const { manager, session } = setup(); + const pkg = addPackage(session, ["failed", "failed"]); + session.running = true; + mockedSend.mockResolvedValueOnce(false); + + (manager as any).refreshPackageStatus(pkg); + await new Promise((r) => setTimeout(r, 0)); + + expect((manager as any).notifiedPackages.has(pkg.id)).toBe(false); + }); +});