Härtung Download/Rotation (Audit-Batch 1): Scheduler-Freeze, Cooldown-Respekt, Daily-Reset, Kategorisierung

Aus einem adversarisch verifizierten Multi-Agent-Audit (14 confirmed/11 refuted):

#1 HIGH Scheduler-Freeze: findNextQueuedItem hatte keinen activeTasks-Guard.
Wird ein Item zurueckgesetzt/ueberschrieben, waehrend sein alter Task noch in
einem nicht-abbrechbaren await parkt (z.B. Integritaets-Check), liefert
findNextQueuedItem dasselbe Item, startItem lehnt es ab ohne activeTasks zu
verkleinern → der SYNCHRONE Admission-Loop dreht endlos → Event-Loop friert
permanent ein. Fix: `if (this.activeTasks.has(itemId)) continue;`. Repro-Test
(ohne Fix haengt sogar der vitest-Timeout — Freeze bewiesen).

#2/#3 MED mega_debrid_cooldown:<ms> wurde verworfen: kein Parser (nur das
debrid_link-Analogon) → Item lief in den generischen 5s-Exponential-Backoff
und fragte cooled Accounts im Sekundentakt erneut an. Neuer
parseMegaDebridCooldownRetry (nimmt das frueheste Cooldown-Ende ueber alle
Accounts) + Handler VOR transient/generic → Item wartet die echte Cooldown-Zeit.

#7/#8 MED Mega per-Account Tages-Usage wurde am Tageswechsel nie zurueckgesetzt
(ensureProviderDailyUsageFresh ruecksetzte nur provider/debrid-link), und weil
es den Tagesschluessel zuerst weiterstellte, lief auch der Reset in
addMegaDebridAccountDailyUsageBytes ins Leere → Accounts blieben den ganzen Tag
faelschlich "am Limit" und schrumpften das (neue) Pro-Account-Umwandlungslimit.
Fix: megaDebridAccountDailyUsageBytes im Tageswechsel mit zuruecksetzen.

#14 LOW classifyAccountFailure: rate_limit-Branch vor quota (quota matchte
"limit" in "rate limit" → Fehl-Kategorisierung).

852/852 gruen, tsc unveraendert (6). #4 (empty→until-restart) bewusst deferred.
This commit is contained in:
Sucukdeluxe
2026-06-17 04:22:20 +02:00
parent a7d989ebd2
commit 3bd6e3b23e
6 changed files with 262 additions and 10 deletions
+36
View File
@@ -1417,6 +1417,42 @@ describe("debrid service", () => {
}
});
it("categorizes a Mega-Debrid 'rate limit' error as rate_limit, not quota (regex ordering)", async () => {
const settings = {
...defaultSettings(),
token: "",
bestToken: "",
allDebridToken: "",
megaLogin: "user",
megaPassword: "pass",
megaCredentials: "user:pass",
megaDebridApiEnabled: true,
megaDebridWebEnabled: false,
providerPrimary: "megadebrid-api" as const,
providerSecondary: "none" as const,
providerTertiary: "none" as const,
autoProviderFallback: true
};
globalThis.fetch = (async (input: RequestInfo | URL): Promise<Response> => {
const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
if (url.includes("action=connectUser")) {
return new Response(JSON.stringify({ response_code: "ok", token: "tok", vip_end: Math.floor(Date.now() / 1000) + 999999 }), { status: 200, headers: { "Content-Type": "application/json" } });
}
if (url.includes("action=getLink")) {
return new Response(JSON.stringify({ response_code: "error", response_text: "Rate limit exceeded, too many requests" }), { status: 200, headers: { "Content-Type": "application/json" } });
}
return new Response("not-found", { status: 404 });
}) as typeof fetch;
const service = new DebridService(settings);
await service.unrestrictLink("https://rapidgator.net/file/rl.rar.html").then(() => null, (e: unknown) => e);
const cooldown = getMegaDebridAccountCooldownState(`${getMegaDebridAccountId("user")}:api`);
expect(cooldown).not.toBeNull();
expect(cooldown!.category).toBe("rate_limit");
});
it("uses Mega Web only when it is configured as a separate fallback provider", async () => {
const settings = {
...defaultSettings(),
+82
View File
@@ -6721,6 +6721,88 @@ describe("download manager", () => {
await new Promise((resolve) => setTimeout(resolve, 150));
});
it("resets Mega-Debrid per-account daily usage at the day boundary (not just provider/debrid-link)", () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-"));
tempDirs.push(root);
const acctId = getMegaDebridAccountId("mega-user");
const manager = new DownloadManager(
{
...defaultSettings(),
megaCredentials: "mega-user:mega-pass",
providerDailyUsageDay: "2000-01-01",
providerDailyUsageBytes: { megadebrid: 9_000_000_000 } as Record<string, number>,
megaDebridAccountDailyUsageBytes: { [acctId]: 9_000_000_000 } as Record<string, number>,
megaDebridAccountDailyLimitBytes: { [acctId]: 1_000_000_000 } as Record<string, number>
},
emptySession(),
createStoragePaths(path.join(root, "state")),
{}
);
const snap = manager.getSnapshot();
expect(snap.settings.providerDailyUsageDay).not.toBe("2000-01-01");
expect(snap.settings.megaDebridAccountDailyUsageBytes || {}).toEqual({});
expect(snap.settings.providerDailyUsageBytes || {}).toEqual({});
});
it("does not freeze the scheduler when a reset item's old task is parked in a non-abort-observing await", async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-"));
tempDirs.push(root);
let convCalls = 0;
const manager = new DownloadManager(
{
...defaultSettings(),
megaCredentials: "mega-user:mega-pass",
megaDebridWebEnabled: true,
megaDebridApiEnabled: false,
megaDebridPreferApi: false,
providerOrder: [],
providerPrimary: "megadebrid",
providerSecondary: "none",
providerTertiary: "none",
outputDir: path.join(root, "downloads"),
extractDir: path.join(root, "extract"),
autoExtract: false,
autoReconnect: false,
enableIntegrityCheck: false,
maxParallel: 4
},
emptySession(),
createStoragePaths(path.join(root, "state")),
{
megaWebUnrestrict: vi.fn(async (): Promise<UnrestrictedLink | null> => {
convCalls += 1;
return await new Promise<UnrestrictedLink | null>(() => { /* never settles, ignores abort */ });
})
}
);
manager.addPackages([{
name: "freeze-repro",
links: [
"https://rapidgator.net/file/freeze-1.part1.rar.html",
"https://rapidgator.net/file/freeze-2.part2.rar.html",
"https://rapidgator.net/file/freeze-3.part3.rar.html"
]
}]);
await manager.start();
await waitFor(() => convCalls === 1, 10000);
const validatingItem = Object.values(manager.getSnapshot().session.items).find((i) => i.status === "validating");
expect(validatingItem).toBeTruthy();
manager.resetItems([validatingItem!.id]);
await waitFor(() => convCalls === 2, 5000);
expect(convCalls).toBe(2);
manager.stop();
await new Promise((resolve) => setTimeout(resolve, 150));
}, 20000);
it("serializes Mega-Debrid API conversions to one per account (no single-token hammering)", async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-"));
tempDirs.push(root);
+30 -1
View File
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { transientResolveRetryDelayMs } from "../src/main/download-manager";
import { transientResolveRetryDelayMs, parseMegaDebridCooldownRetry } from "../src/main/download-manager";
describe("transientResolveRetryDelayMs (fast, bounded retry for transient resolve failures)", () => {
it("starts fast (<= 3s) instead of the 5s..120s exponential", () => {
@@ -29,3 +29,32 @@ describe("transientResolveRetryDelayMs (fast, bounded retry for transient resolv
}
});
});
describe("parseMegaDebridCooldownRetry (honor the encoded account-cooldown delay)", () => {
it("parses the encoded delay from a bare mega_debrid_cooldown error", () => {
const r = parseMegaDebridCooldownRetry("mega_debrid_cooldown:20330:Mega-Debrid (Account 2/2, Da******el): Token error");
expect(r).not.toBeNull();
expect(r!.delayMs).toBe(20330);
expect(r!.detail).toContain("Mega-Debrid");
});
it("parses it when embedded in the aggregated provider-chain error", () => {
const aggregated = "Unrestrict fehlgeschlagen: Mega-Debrid API: mega_debrid_cooldown:20330:Mega-Debrid (Account 2/2): Token error";
expect(parseMegaDebridCooldownRetry(aggregated)!.delayMs).toBe(20330);
});
it("takes the SOONEST (min) cooldown when several accounts are cooled", () => {
const both = "Mega-Debrid API: mega_debrid_cooldown:116285:web | Mega-Debrid API: mega_debrid_cooldown:20330:api";
expect(parseMegaDebridCooldownRetry(both)!.delayMs).toBe(20330);
});
it("clamps to [1s, 15min]", () => {
expect(parseMegaDebridCooldownRetry("mega_debrid_cooldown:1:x")!.delayMs).toBe(1000);
expect(parseMegaDebridCooldownRetry("mega_debrid_cooldown:99999999:x")!.delayMs).toBe(15 * 60 * 1000);
});
it("returns null when there is no mega cooldown marker", () => {
expect(parseMegaDebridCooldownRetry("Datei beim Hoster gerade nicht abrufbar")).toBeNull();
expect(parseMegaDebridCooldownRetry("debrid_link_cooldown:5000:x")).toBeNull();
});
});