From 26ebd931b7a375e0bfa09ee28054112d3f7db7f8 Mon Sep 17 00:00:00 2001 From: Sucukdeluxe Date: Sat, 22 Aug 2026 06:06:27 +0200 Subject: [PATCH] fix(notifications): preserve stopped package suppression --- src/main/download-manager.ts | 35 +++++++++++++---- tests/notify-hooks.test.ts | 74 +++++++++++++++++++++++++----------- 2 files changed, 78 insertions(+), 31 deletions(-) diff --git a/src/main/download-manager.ts b/src/main/download-manager.ts index 52053c4..260cb60 100644 --- a/src/main/download-manager.ts +++ b/src/main/download-manager.ts @@ -6346,7 +6346,8 @@ export class DownloadManager extends EventEmitter { const stoppedRunContext = wasRunning ? this.stopActiveRunContext(this.runPackageIds, this.session.runStartedAt) : null; - this.schedulerGeneration += 1; + this.suppressStandalonePackageResults(); + this.schedulerGeneration += 1; this.session.running = false; this.session.paused = false; this.session.reconnectUntil = 0; @@ -8325,8 +8326,8 @@ export class DownloadManager extends EventEmitter { completedItems: completedItems.length, targetedItems: targetItems.length }); - const generation = this.beginPackageResultGeneration(packageId, false, true); - this.standalonePackageResults.add(this.packageResultKey(packageId, generation)); + this.beginPackageResultGeneration(packageId, false, true); + this.reactivateStandalonePackageResult(packageId); this.persistSoon(); this.emitState(true); void this.runPackagePostProcessing(packageId).catch((err) => logger.warn(`runPackagePostProcessing Fehler (retryExtraction): ${compactErrorText(err)}`)); @@ -8355,8 +8356,8 @@ export class DownloadManager extends EventEmitter { completedItems: completedItems.length, targetedItems: targetItems.length }); - const generation = this.beginPackageResultGeneration(packageId, false, true); - this.standalonePackageResults.add(this.packageResultKey(packageId, generation)); + this.beginPackageResultGeneration(packageId, false, true); + this.reactivateStandalonePackageResult(packageId); this.persistSoon(); this.emitState(true); void this.runPackagePostProcessing(packageId).catch((err) => logger.warn(`runPackagePostProcessing Fehler (extractNow): ${compactErrorText(err)}`)); @@ -11629,9 +11630,9 @@ export class DownloadManager extends EventEmitter { if (!pkg) { continue; } - const generation = this.beginPackageResultGeneration(packageId, false, true); + this.beginPackageResultGeneration(packageId, false, true); if (!this.runPackageIds.has(packageId)) { - this.standalonePackageResults.add(this.packageResultKey(packageId, generation)); + this.reactivateStandalonePackageResult(packageId); } this.refreshPackageStatus(pkg); } @@ -11827,11 +11828,26 @@ export class DownloadManager extends EventEmitter { } private trackStandalonePackageResult(packageId: string): void { + const key = this.packageResultKey(packageId, this.getPackageResultGeneration(packageId)); + if (this.suppressedPackageResults.has(key)) { + return; + } + this.standalonePackageResults.add(key); + } + + private reactivateStandalonePackageResult(packageId: string): void { const key = this.packageResultKey(packageId, this.getPackageResultGeneration(packageId)); this.suppressedPackageResults.delete(key); this.standalonePackageResults.add(key); } + private suppressStandalonePackageResults(): void { + for (const key of this.standalonePackageResults) { + this.suppressedPackageResults.add(key); + } + this.standalonePackageResults.clear(); + } + private trackPackagePostProcessResult(packageId: string): void { const generation = this.getPackageResultGeneration(packageId); const key = this.packageResultKey(packageId, generation); @@ -11909,6 +11925,10 @@ export class DownloadManager extends EventEmitter { return null; } const generation = this.getPackageResultGeneration(packageId); + const key = this.packageResultKey(packageId, generation); + if (this.suppressedPackageResults.has(key)) { + return null; + } if (!this.runPackageIds.has(packageId) && !this.isPackageResultTracked(packageId, generation)) { return null; } @@ -11916,7 +11936,6 @@ export class DownloadManager extends EventEmitter { if (items.some((item) => !isFinishedStatus(item.status)) || this.hasPackageLifecycleWork(packageId)) { return null; } - const key = this.packageResultKey(packageId, generation); const existing = this.finalizedPackageResults.get(key); if (existing) { return existing; diff --git a/tests/notify-hooks.test.ts b/tests/notify-hooks.test.ts index bbd9943..4606ae9 100644 --- a/tests/notify-hooks.test.ts +++ b/tests/notify-hooks.test.ts @@ -575,16 +575,20 @@ describe("authoritative run completion", () => { expect(history[0].name).toBe(pkg.name); }); - it("stops and removes the active run context before late package work can emit", async () => { + it("keeps stopped package postprocessing suppressed when a later start only runs another package", async () => { const { manager, session, events, history } = setup({ autoExtractWhenStopped: true }); - const packageA = addPackage(session, ["completed"], "stopped-package"); + const packageA = addPackage(session, ["queued"], "stopped-package"); const state = internal(manager); - session.running = true; - session.runStartedAt = Date.now() - 20_000; - state.runItemIds = new Set(packageA.itemIds); - state.runPackageIds = new Set([packageA.id]); - state.runOutcomes = new Map([[packageA.itemIds[0], "completed"]]); - const stoppedContext = state.beginActiveRunContext(state.runPackageIds, session.runStartedAt); + vi.spyOn(state, "ensureScheduler").mockResolvedValue(undefined); + + await manager.start(); + const packageAItem = session.items[packageA.itemIds[0]]; + packageAItem.status = "completed"; + packageAItem.downloadedBytes = 1_000; + packageAItem.totalBytes = 1_000; + packageAItem.progressPercent = 100; + packageAItem.fullStatus = "Fertig"; + packageA.status = "completed"; let releasePostProcess = (): void => {}; const postProcessGate = new Promise((resolve) => { releasePostProcess = resolve; @@ -595,31 +599,55 @@ describe("authoritative run completion", () => { manager.stop(); await flushNotifications(); - const stoppedEvent = events.find((event) => event.type === "run_stopped"); - expect(stoppedEvent?.id).toBe(`run:${stoppedContext.id}:run_stopped`); - expect(state.activeRunContextId).toBeNull(); - expect(state.runContexts.has(stoppedContext.id)).toBe(false); + + const packageB = addPackage(session, ["queued"], "follow-up-package"); + await manager.start(); + expect(session.running).toBe(true); + expect(state.runPackageIds).toEqual(new Set([packageB.id])); releasePostProcess(); await latePostProcess; await flushNotifications(); expect(events.filter((event) => event.type === "package_completed")).toHaveLength(0); expect(history).toHaveLength(0); + }); - const packageB = addPackage(session, ["completed"], "follow-up-package"); - session.running = true; - session.runStartedAt = Date.now() - 5_000; - state.runItemIds = new Set(packageB.itemIds); - state.runPackageIds = new Set([packageB.id]); - state.runOutcomes = new Map([[packageB.itemIds[0], "completed"]]); - state.beginActiveRunContext(state.runPackageIds, session.runStartedAt); - state.finishRun(); + it("suppresses a stopped postprocess-only generation and allows an explicit package retry", async () => { + const { manager, session, events, history } = setup({ autoExtract: true, autoExtractWhenStopped: true }); + const pkg = addPackage(session, ["completed"], "postprocess-only-package"); + const state = internal(manager); + vi.spyOn(state, "ensureScheduler").mockResolvedValue(undefined); + let releasePostProcess = (): void => {}; + let postProcessGate = new Promise((resolve) => { + releasePostProcess = resolve; + }); + state.handlePackagePostProcessing = vi.fn(async () => postProcessGate); + + await manager.start(); + const stoppedPostProcess = state.packagePostProcessTasks.get(pkg.id); + expect(stoppedPostProcess).toBeDefined(); + expect(state.runItemIds.size).toBe(0); + manager.stop(); + + releasePostProcess(); + await stoppedPostProcess; + await flushNotifications(); + expect(events.filter((event) => event.type === "package_completed")).toHaveLength(0); + expect(history).toHaveLength(0); + + session.items[pkg.itemIds[0]].fullStatus = "Entpacken - Error"; + postProcessGate = new Promise((resolve) => { + releasePostProcess = resolve; + }); + manager.retryExtraction(pkg.id); + const retriedPostProcess = state.packagePostProcessTasks.get(pkg.id); + expect(retriedPostProcess).toBeDefined(); + releasePostProcess(); + await retriedPostProcess; await flushNotifications(); - expect(events.filter((event) => event.type === "run_stopped")).toHaveLength(1); - expect(events.filter((event) => event.type === "run_completed")).toHaveLength(1); expect(events.filter((event) => event.type === "package_completed")).toHaveLength(1); - expect(history.map((entry) => entry.name)).toEqual([packageB.name]); + expect(history.map((entry) => entry.name)).toEqual([pkg.name]); }); it("finalizes overlapping runs independently when the earlier run finishes deferred work last", async () => {