Fix: Mega-Web Selbst-Cooldown bei belegter Queue + Legacy-Config Mega-Demotion (Audit Runde 9+10)
MW-1 (HIGH): Ein gesunder Mega-Web-Account wurde 120s gesperrt, wenn der
60s-Gesamttimeout ablief, waehrend die Umwandlung noch SERIELL in der
per-Account-Single-Flight-Queue auf ihren Vorgaenger wartete - also bevor echte
Arbeit begann. Die Queue-Wartezeit zaehlte zu elapsedMs (>= 8s), sodass der
Abbruch faelschlich wie ein In-Arbeit-Abbruch eines langsamen Accounts gewertet
wurde. Genau die gemeldete 'Tool sperrt sich selbst'-Klasse (Web-Variante).
Zweiteilig: (1) MegaWebFallback.runExclusive trackt workStarted und meldet einen
Abbruch-vor-Arbeitsbeginn als Queue-Timeout statt aborted:mega-web;
(2) unrestrictViaWeb bewahrt diese Klassifikation statt sie zu aborted:debrid zu
plaetten -> die Rotation trifft die Queue-Timeout-Ausnahme (cooldownMs 0) statt
den Abbruch-Cooldown. Ein echter In-Arbeit-Abbruch sperrt weiterhin (langsame
Accounts korrekt ueberspringen). Nach dem No-Cooldown-Pfad rotiert der Retry per
In-Flight-Tiefe auf einen freien Account.
SET-MIG-01 (MED): Eine Config von vor v1.6.90 kannte die getrennten
Mega-API/Web-Enable-Flags nicht. readSettingsFile merged {...defaultSettings(),
...parsed}, also fuellten die false-Defaults die fehlenden Flags BEVOR
normalizeSettings lief -> der creds-basierte Migrationszweig war tot, Mega blieb
auf aus trotz vorhandener Creds und wurde beim ersten Settings-Save still aus der
providerOrder demotet. Fix: reine migrateLegacyMegaEnableFlags seedet die Flags
nur, wenn BEIDE im RAW-parsed fehlen und Creds vorhanden sind (absent-both als
einziger sicherer Trigger; present-false bleibt unberuehrt = bewusst-deaktiviert
nicht re-aktivieren). Greift nur bei Legacy-Configs, die seit dem Upgrade noch
nicht neu gespeichert wurden.
Je rot-bewiesener Test (per Temp-Revert verifiziert, nicht-vakuum). Volle Suite
890 gruen, tsc unveraendert (6 vorbestehende Fehler). Runde-9/10-Doku + die zwei
offenen Nutzer-Entscheidungen aktualisiert.
This commit is contained in:
@@ -1961,6 +1961,43 @@ describe("debrid service", () => {
|
||||
expect(result.directUrl).toBe("https://mega-web.example/ok.rar");
|
||||
}, 20000);
|
||||
|
||||
it("setzt KEINEN Account-Cooldown, wenn der Mega-Web-Abbruch nur ein Queue-Timeout war (Account war belegt, nicht ungesund)", async () => {
|
||||
process.env.RD_MEGA_ABORT_MIN_RUN_MS = "0";
|
||||
const settings = {
|
||||
...defaultSettings(),
|
||||
token: "",
|
||||
bestToken: "",
|
||||
allDebridToken: "",
|
||||
megaLogin: "user",
|
||||
megaPassword: "pass",
|
||||
megaCredentials: "user:pass",
|
||||
megaDebridPreferApi: false,
|
||||
providerOrder: [] as const,
|
||||
providerPrimary: "megadebrid" as const,
|
||||
providerSecondary: "none" as const,
|
||||
providerTertiary: "none" as const,
|
||||
autoProviderFallback: false
|
||||
};
|
||||
globalThis.fetch = (async () => new Response("error", { status: 500 })) as typeof fetch;
|
||||
|
||||
const controller = new AbortController();
|
||||
let calls = 0;
|
||||
const megaWeb = vi.fn(async () => {
|
||||
calls += 1;
|
||||
controller.abort("caller-timeout");
|
||||
throw new Error("Mega-Web Queue-Timeout (abgebrochen nach 60s Wartezeit, Account war belegt)");
|
||||
});
|
||||
|
||||
const service = new DebridService(settings, { megaWebUnrestrict: megaWeb });
|
||||
await expect(
|
||||
service.unrestrictLink("https://rapidgator.net/file/queue-timeout-no-cooldown", controller.signal)
|
||||
).rejects.toThrow();
|
||||
|
||||
const key = `${getMegaDebridAccountId("user")}:web`;
|
||||
expect(getMegaDebridAccountCooldownState(key)).toBeNull();
|
||||
expect(calls).toBeGreaterThanOrEqual(1);
|
||||
}, 20000);
|
||||
|
||||
it("escalates a Mega-Debrid account to 'until restart' after the empty-response streak threshold", () => {
|
||||
const key = `${getMegaDebridAccountId("user1")}:web`;
|
||||
expect(MEGA_DEBRID_EMPTY_STREAK_UNTIL_RESTART).toBe(3);
|
||||
|
||||
@@ -314,5 +314,41 @@ describe("mega-web-fallback", () => {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
});
|
||||
|
||||
it("klassifiziert einen Abbruch WAEHREND in der Queue als Queue-Timeout (nicht harter Abbruch), damit der belegte Account nicht bestraft wird", async () => {
|
||||
let releaseLogin: () => void = () => {};
|
||||
const loginGate = new Promise<void>((resolve) => { releaseLogin = resolve; });
|
||||
globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
|
||||
const u = String(url);
|
||||
if (u.includes("form=login")) {
|
||||
await loginGate;
|
||||
const headers = new Headers();
|
||||
headers.append("set-cookie", "session=c; path=/");
|
||||
return new Response("", { headers, status: 200 });
|
||||
}
|
||||
if (u.includes("page=debrideur")) return new Response('<form id="debridForm"></form>', { status: 200 });
|
||||
if (u.includes("form=debrid")) return new Response(`<div class="acp-box"><h3>Link: https://mega.debrid/l</h3><a href="javascript:processDebrid(1,'code',0)">d</a></div>`, { status: 200 });
|
||||
if (u.includes("ajax=debrid")) return new Response(JSON.stringify({ link: "https://mega.direct/ok" }), { status: 200 });
|
||||
return new Response("Not found", { status: 404 });
|
||||
}) as unknown as typeof fetch;
|
||||
|
||||
const fallback = new MegaWebFallback(() => ({ login: "same", password: "pw" }));
|
||||
const firstCtrl = new AbortController();
|
||||
const first = fallback.unrestrict("https://mega.debrid/a", firstCtrl.signal, { login: "same", password: "pw" });
|
||||
|
||||
await new Promise((r) => setTimeout(r, 25));
|
||||
|
||||
const secondCtrl = new AbortController();
|
||||
const second = fallback.unrestrict("https://mega.debrid/b", secondCtrl.signal, { login: "same", password: "pw" });
|
||||
|
||||
await new Promise((r) => setTimeout(r, 25));
|
||||
secondCtrl.abort("caller-timeout");
|
||||
|
||||
await expect(second).rejects.toThrow(/queue.?timeout/i);
|
||||
|
||||
releaseLogin();
|
||||
await first.catch(() => null);
|
||||
await new Promise((r) => setTimeout(r, 20));
|
||||
}, 10000);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -180,6 +180,67 @@ describe("settings storage", () => {
|
||||
expect(webNormalized.hosterRouting.rapidgator).toBe("megadebrid-web");
|
||||
});
|
||||
|
||||
it("migriert eine pre-v1.6.90-Config (Mega-Creds, beide Enable-Flags fehlen) zu aktiviertem Mega-Debrid statt es still auf false zu setzen", () => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "rd-store-"));
|
||||
tempDirs.push(dir);
|
||||
const paths = createStoragePaths(dir);
|
||||
const legacyApi = {
|
||||
megaLogin: "mega-user",
|
||||
megaPassword: "mega-pass",
|
||||
megaDebridPreferApi: true,
|
||||
providerPrimary: "realdebrid",
|
||||
providerSecondary: "megadebrid"
|
||||
};
|
||||
fs.writeFileSync(paths.configFile, JSON.stringify(legacyApi), "utf8");
|
||||
const loadedApi = loadSettings(paths);
|
||||
expect(loadedApi.megaDebridApiEnabled).toBe(true);
|
||||
expect(loadedApi.megaDebridWebEnabled).toBe(false);
|
||||
|
||||
const dir2 = fs.mkdtempSync(path.join(os.tmpdir(), "rd-store-"));
|
||||
tempDirs.push(dir2);
|
||||
const paths2 = createStoragePaths(dir2);
|
||||
const legacyWeb = {
|
||||
megaLogin: "mega-user",
|
||||
megaPassword: "mega-pass",
|
||||
megaDebridPreferApi: false,
|
||||
providerPrimary: "realdebrid",
|
||||
providerSecondary: "megadebrid"
|
||||
};
|
||||
fs.writeFileSync(paths2.configFile, JSON.stringify(legacyWeb), "utf8");
|
||||
const loadedWeb = loadSettings(paths2);
|
||||
expect(loadedWeb.megaDebridApiEnabled).toBe(false);
|
||||
expect(loadedWeb.megaDebridWebEnabled).toBe(true);
|
||||
});
|
||||
|
||||
it("re-aktiviert KEINE bewusst deaktivierten Mega-Flags und migriert nicht ohne Mega-Creds", () => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "rd-store-"));
|
||||
tempDirs.push(dir);
|
||||
const paths = createStoragePaths(dir);
|
||||
const deliberatelyDisabled = {
|
||||
megaLogin: "mega-user",
|
||||
megaPassword: "mega-pass",
|
||||
megaDebridPreferApi: true,
|
||||
megaDebridApiEnabled: false,
|
||||
megaDebridWebEnabled: false
|
||||
};
|
||||
fs.writeFileSync(paths.configFile, JSON.stringify(deliberatelyDisabled), "utf8");
|
||||
const loaded = loadSettings(paths);
|
||||
expect(loaded.megaDebridApiEnabled).toBe(false);
|
||||
expect(loaded.megaDebridWebEnabled).toBe(false);
|
||||
|
||||
const dir2 = fs.mkdtempSync(path.join(os.tmpdir(), "rd-store-"));
|
||||
tempDirs.push(dir2);
|
||||
const paths2 = createStoragePaths(dir2);
|
||||
const noCreds = {
|
||||
megaDebridPreferApi: true,
|
||||
providerPrimary: "realdebrid"
|
||||
};
|
||||
fs.writeFileSync(paths2.configFile, JSON.stringify(noCreds), "utf8");
|
||||
const loadedNoCreds = loadSettings(paths2);
|
||||
expect(loadedNoCreds.megaDebridApiEnabled).toBe(false);
|
||||
expect(loadedNoCreds.megaDebridWebEnabled).toBe(false);
|
||||
});
|
||||
|
||||
it("normalizes provider daily limits and resets stale daily usage", () => {
|
||||
const [debridLinkKey] = parseDebridLinkApiKeys("dl-key-one");
|
||||
const normalized = normalizeSettings({
|
||||
|
||||
Reference in New Issue
Block a user