fix(notifications): preserve run and package result identity
This commit is contained in:
@@ -4455,9 +4455,10 @@ describe("download manager", () => {
|
||||
return originalFetch(input, init);
|
||||
};
|
||||
|
||||
let releaseBlockedPostProcess: ((value?: void | PromiseLike<void>) => void) | undefined;
|
||||
try {
|
||||
const manager = new DownloadManager(
|
||||
let releaseBlockedPostProcess: ((value?: void | PromiseLike<void>) => void) | undefined;
|
||||
try {
|
||||
const history: HistoryEntry[] = [];
|
||||
const manager = new DownloadManager(
|
||||
{
|
||||
...defaultSettings(),
|
||||
token: "rd-token",
|
||||
@@ -4466,14 +4467,15 @@ describe("download manager", () => {
|
||||
autoExtract: false,
|
||||
maxParallel: 1
|
||||
},
|
||||
emptySession(),
|
||||
createStoragePaths(path.join(root, "state"))
|
||||
emptySession(),
|
||||
createStoragePaths(path.join(root, "state")),
|
||||
{ onHistoryEntry: (entry) => history.push(entry) }
|
||||
);
|
||||
|
||||
const blocker = new Promise<void>((resolve) => {
|
||||
releaseBlockedPostProcess = resolve;
|
||||
});
|
||||
(manager as unknown as { packagePostProcessQueue: Promise<void> }).packagePostProcessQueue = blocker;
|
||||
const blocker = new Promise<void>((resolve) => {
|
||||
releaseBlockedPostProcess = resolve;
|
||||
});
|
||||
(manager as any).handlePackagePostProcessing = vi.fn(async () => blocker);
|
||||
|
||||
manager.addPackages([
|
||||
{ name: "first", links: ["https://dummy/first"] },
|
||||
@@ -4489,19 +4491,31 @@ describe("download manager", () => {
|
||||
manager.start();
|
||||
|
||||
await waitFor(() => manager.getSnapshot().session.items[firstItem]?.status === "completed", 12000);
|
||||
await waitFor(() => {
|
||||
const state = manager.getSnapshot().session.items[secondItem]?.status;
|
||||
return state === "validating" || state === "downloading" || state === "integrity_check" || state === "completed";
|
||||
}, 6000);
|
||||
await waitFor(() => {
|
||||
const state = manager.getSnapshot().session.items[secondItem]?.status;
|
||||
return state === "validating" || state === "downloading" || state === "integrity_check" || state === "completed";
|
||||
}, 6000);
|
||||
|
||||
const pendingPackage = manager.getSnapshot().session.packages[firstPackage];
|
||||
expect(pendingPackage?.downloadStartedAt).toBeGreaterThan(0);
|
||||
expect(pendingPackage?.downloadEndedAt).toBeGreaterThanOrEqual(pendingPackage?.downloadStartedAt || 0);
|
||||
expect(pendingPackage?.downloadEndedAt).toBe(pendingPackage?.downloadCompletedAt);
|
||||
const downloadEndedAt = pendingPackage?.downloadEndedAt || 0;
|
||||
await new Promise((resolve) => setTimeout(resolve, 1_200));
|
||||
expect(manager.getSnapshot().session.packages[firstPackage]?.downloadEndedAt).toBe(downloadEndedAt);
|
||||
|
||||
if (releaseBlockedPostProcess) {
|
||||
releaseBlockedPostProcess();
|
||||
}
|
||||
await waitFor(() => !manager.getSnapshot().session.running, 25000);
|
||||
|
||||
const done = manager.getSnapshot();
|
||||
expect(done.session.items[firstItem]?.status).toBe("completed");
|
||||
expect(done.session.items[secondItem]?.status).toBe("completed");
|
||||
}
|
||||
await waitFor(() => !manager.getSnapshot().session.running, 25000);
|
||||
await waitFor(() => history.some((entry) => entry.name === "first"), 6000);
|
||||
|
||||
const done = manager.getSnapshot();
|
||||
expect(done.session.items[firstItem]?.status).toBe("completed");
|
||||
expect(done.session.items[secondItem]?.status).toBe("completed");
|
||||
const firstHistory = history.find((entry) => entry.name === "first");
|
||||
expect(firstHistory?.downloadEndedAt).toBe(downloadEndedAt);
|
||||
expect(firstHistory?.totalDurationSeconds).toBeGreaterThan(firstHistory?.downloadDurationSeconds || 0);
|
||||
} finally {
|
||||
if (releaseBlockedPostProcess) {
|
||||
releaseBlockedPostProcess();
|
||||
@@ -11055,6 +11069,7 @@ describe("download manager", () => {
|
||||
errorCategory: ""
|
||||
});
|
||||
expect(snapshot.session.packages[packageId]?.archiveOperations?.[0].durationMs).toBeGreaterThanOrEqual(0);
|
||||
expect(snapshot.session.packages[packageId]?.outputCount).toBe(1);
|
||||
}, 30000);
|
||||
|
||||
it("does not fail startup post-processing when source package dir is missing but extract output exists", async () => {
|
||||
|
||||
+100
-2
@@ -243,6 +243,51 @@ describe("authoritative package completion", () => {
|
||||
expect(history).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("continues the persisted result generation after an extraction retry following restart", async () => {
|
||||
const { manager, session, events, history } = setup();
|
||||
const pkg = addPackage(session);
|
||||
const state = internal(manager);
|
||||
pkg.resultGeneration = 7;
|
||||
pkg.terminalAt = Date.now() - 1_000;
|
||||
pkg.archiveOperations = [{
|
||||
id: "archive-restart-failed",
|
||||
name: "episode.rar",
|
||||
itemIds: [...pkg.itemIds],
|
||||
partCount: 1,
|
||||
startedAt: 10_000,
|
||||
completedAt: 12_000,
|
||||
durationMs: 2_000,
|
||||
status: "failed",
|
||||
errorCategory: "crc_error"
|
||||
}];
|
||||
session.items[pkg.itemIds[0]].fullStatus = "Entpacken - Error";
|
||||
vi.spyOn(state, "runPackagePostProcessing").mockResolvedValue(undefined);
|
||||
|
||||
manager.retryExtraction(pkg.id);
|
||||
expect(pkg.resultGeneration).toBe(8);
|
||||
|
||||
pkg.archiveOperations = [{
|
||||
id: "archive-restart-completed",
|
||||
name: "episode.rar",
|
||||
itemIds: [...pkg.itemIds],
|
||||
partCount: 1,
|
||||
startedAt: 20_000,
|
||||
completedAt: 23_000,
|
||||
durationMs: 3_000,
|
||||
status: "completed",
|
||||
errorCategory: ""
|
||||
}];
|
||||
session.items[pkg.itemIds[0]].fullStatus = "Entpackt - Done (3.0s)";
|
||||
pkg.status = "completed";
|
||||
state.tryFinalizePackageResult(pkg.id);
|
||||
await flushNotifications();
|
||||
|
||||
expect(events).toHaveLength(1);
|
||||
expect(events[0].id).toContain(":8:");
|
||||
expect(history).toHaveLength(1);
|
||||
expect(history[0].id).toBe(`hist-${pkg.id}-8`);
|
||||
});
|
||||
|
||||
it("moves a pending success digest into the outbox before shutdown", async () => {
|
||||
const { manager, session, events } = setup({ notifyPackageSuccessMode: "digest" });
|
||||
const pkg = addPackage(session);
|
||||
@@ -279,6 +324,29 @@ describe("authoritative run completion", () => {
|
||||
expect(events.some((event) => event.type === "run_completed")).toBe(false);
|
||||
});
|
||||
|
||||
it("builds run_stopped file counters only from finalized package results", async () => {
|
||||
const { manager, session, events } = setup();
|
||||
const pkg = addPackage(session);
|
||||
const state = internal(manager);
|
||||
state.runPackageIds = new Set([pkg.id]);
|
||||
state.tryFinalizePackageResult(pkg.id);
|
||||
await flushNotifications();
|
||||
events.length = 0;
|
||||
|
||||
session.running = true;
|
||||
session.runStartedAt = Date.now() - 10_000;
|
||||
state.runItemIds = new Set(pkg.itemIds);
|
||||
state.runPackageIds = new Set([pkg.id]);
|
||||
state.runOutcomes = new Map([[pkg.itemIds[0], "failed"]]);
|
||||
|
||||
manager.stop();
|
||||
await flushNotifications();
|
||||
|
||||
const stopped = events.find((event) => event.type === "run_stopped");
|
||||
expect(stopped).toBeDefined();
|
||||
expect(stopped?.payload.fields.some((field) => field.name === "Dateien" && field.value === "1 erfolgreich · 0 fehlgeschlagen · 0 abgebrochen")).toBe(true);
|
||||
});
|
||||
|
||||
it("waits for failed extraction package results before emitting the final run summary", async () => {
|
||||
const { manager, session, events } = setup({ notifyPackageSuccessMode: "digest" });
|
||||
const pkg = addPackage(session);
|
||||
@@ -344,8 +412,6 @@ describe("authoritative run completion", () => {
|
||||
session.runStartedAt = Date.now() - 20_000;
|
||||
state.runItemIds = new Set(pkg.itemIds);
|
||||
state.runPackageIds = new Set([pkg.id]);
|
||||
state.packageResultGenerations = new Map([[pkg.id, 1]]);
|
||||
state.runPackageGenerations = new Map([[pkg.id, 1]]);
|
||||
state.runOutcomes = new Map([[pkg.itemIds[0], "completed"]]);
|
||||
|
||||
state.tryFinalizePackageResult(pkg.id);
|
||||
@@ -357,4 +423,36 @@ describe("authoritative run completion", () => {
|
||||
expect(events.map((event) => event.type)).toEqual(["package_completed", "run_completed"]);
|
||||
expect(events[0].payload.title).toContain("Paket-Digest");
|
||||
});
|
||||
|
||||
it("finalizes overlapping runs independently when the earlier run finishes deferred work last", async () => {
|
||||
const { manager, session, events, history } = setup();
|
||||
const packageA = addPackage(session, ["completed"], "package-a");
|
||||
const packageB = addPackage(session, ["completed"], "package-b");
|
||||
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"]]);
|
||||
state.packageDeferredPostProcessTasks.set(packageA.id, new Set([Promise.resolve()]));
|
||||
state.finishRun();
|
||||
|
||||
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.finishRun();
|
||||
|
||||
state.packageDeferredPostProcessTasks.delete(packageA.id);
|
||||
state.tryFinalizePackageResult(packageA.id);
|
||||
await flushNotifications();
|
||||
|
||||
expect(events.filter((event) => event.type === "package_completed")).toHaveLength(2);
|
||||
expect(events.filter((event) => event.type === "run_completed")).toHaveLength(2);
|
||||
expect(new Set(events.filter((event) => event.type === "package_completed").map((event) => event.id)).size).toBe(2);
|
||||
expect(new Set(events.filter((event) => event.type === "run_completed").map((event) => event.id)).size).toBe(2);
|
||||
expect(history.map((entry) => entry.name).sort()).toEqual([packageA.name, packageB.name].sort());
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1137,6 +1137,7 @@ describe("settings storage", () => {
|
||||
itemIds: [],
|
||||
cancelled: false,
|
||||
enabled: true,
|
||||
resultGeneration: 7,
|
||||
downloadStartedAt: 1_000,
|
||||
downloadCompletedAt: 10_000,
|
||||
downloadEndedAt: 12_000,
|
||||
@@ -1174,6 +1175,7 @@ describe("settings storage", () => {
|
||||
});
|
||||
|
||||
expect(normalized.packages["pkg-1"]).toEqual(expect.objectContaining({
|
||||
resultGeneration: 7,
|
||||
downloadEndedAt: 12_000,
|
||||
postProcessQueuedAt: 13_000,
|
||||
postProcessStartedAt: 14_000,
|
||||
|
||||
Reference in New Issue
Block a user