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:
Sucukdeluxe 2026-06-17 14:23:41 +02:00
parent 85c8d6bdf9
commit fa2c7eb6be
8 changed files with 294 additions and 5 deletions

View File

@ -1885,6 +1885,9 @@ class MegaDebridClient {
return null;
});
if (signal?.aborted) {
if (/queue.?timeout/i.test(lastError)) {
throw new Error(lastError.replace(/^Error:\s*/i, ""));
}
throw new Error("aborted:debrid");
}
if (web?.directUrl) {

View File

@ -177,12 +177,12 @@ async function sleepWithSignal(ms: number, signal?: AbortSignal): Promise<void>
});
}
async function raceWithAbort<T>(promise: Promise<T>, signal?: AbortSignal): Promise<T> {
async function raceWithAbort<T>(promise: Promise<T>, signal?: AbortSignal, abortErrorFactory: () => Error = abortError): Promise<T> {
if (!signal) {
return promise;
}
if (signal.aborted) {
throw abortError();
throw abortErrorFactory();
}
return new Promise<T>((resolve, reject) => {
@ -194,7 +194,7 @@ async function raceWithAbort<T>(promise: Promise<T>, signal?: AbortSignal): Prom
}
settled = true;
signal.removeEventListener("abort", onAbort);
reject(abortError());
reject(abortErrorFactory());
};
signal.addEventListener("abort", onAbort, { once: true });
@ -284,6 +284,7 @@ export class MegaWebFallback {
private async runExclusive<T>(job: () => Promise<T>, key: string, signal?: AbortSignal): Promise<T> {
const queuedAt = Date.now();
const QUEUE_WAIT_TIMEOUT_MS = 90000;
let workStarted = false;
const guardedJob = async (): Promise<T> => {
throwIfAborted(signal);
const waited = Date.now() - queuedAt;
@ -291,6 +292,7 @@ export class MegaWebFallback {
traceConversionPhase({ phase: "web-queue", provider: "megadebrid-web", queueWaitMs: waited, outcome: "queue-timeout", detail: `${Math.floor(waited / 1000)}s in Web-Queue gewartet` });
throw new Error(`Mega-Web Queue-Timeout (${Math.floor(waited / 1000)}s gewartet)`);
}
workStarted = true;
const workStartedAt = Date.now();
try {
const result = await job();
@ -304,7 +306,11 @@ export class MegaWebFallback {
const prev = this.queues.get(key) ?? Promise.resolve();
const run = prev.then(guardedJob, guardedJob);
this.queues.set(key, run.then(() => undefined, () => undefined));
return raceWithAbort(run, signal);
return raceWithAbort(run, signal, () =>
workStarted
? abortError()
: new Error(`Mega-Web Queue-Timeout (abgebrochen nach ${Math.floor((Date.now() - queuedAt) / 1000)}s Wartezeit, Account war belegt)`)
);
}
private async login(login: string, password: string, signal?: AbortSignal): Promise<string> {

View File

@ -627,12 +627,24 @@ function normalizeAudioStripSummary(raw: unknown): AudioStripSummary | undefined
};
}
function migrateLegacyMegaEnableFlags(parsed: AppSettings): AppSettings {
if (parsed.megaDebridApiEnabled !== undefined || parsed.megaDebridWebEnabled !== undefined) {
return parsed;
}
const hasMegaCreds = Boolean(asText(parsed.megaLogin) && asText(parsed.megaPassword));
if (!hasMegaCreds) {
return parsed;
}
const preferApi = parsed.megaDebridPreferApi !== undefined ? Boolean(parsed.megaDebridPreferApi) : true;
return { ...parsed, megaDebridApiEnabled: preferApi, megaDebridWebEnabled: !preferApi };
}
function readSettingsFile(filePath: string): AppSettings | null {
try {
const parsed = JSON.parse(fs.readFileSync(filePath, "utf8")) as AppSettings;
const merged = normalizeSettings({
...defaultSettings(),
...parsed
...migrateLegacyMegaEnableFlags(parsed)
});
return sanitizeCredentialPersistence(merged);
} catch (error) {

View File

@ -10,6 +10,116 @@ Backup/Restore, account-check) → Abschlussbericht + Ende. Fixes je rot-bewiese
Disziplin: erst BELEGEN (Code-Zitat + konkretes Szenario), dann adversarisch verifizieren,
dann TDD-Fix. Kein Blind-Fix. Tests gruen + tsc=6 nach jeder Runde. Periodisch releasen.
## Runde 9 (Provider-spezifische Unrestrict-/Rotations-Pfade) — Mega-Web-Single-Flight + DebridLink-Key + Concurrency
Fokus: Mega-Web-Fallback Session/Single-Flight-Queue, DebridLink-Key-Rotation, In-Flight-Verteilung.
- **CONFIRMED HIGH (GEFIXT) MW-1 Selbst-Cooldown eines GESUNDEN, nur in der Queue wartenden Web-Accounts:**
Der Caller-Timeout (`DEFAULT_UNRESTRICT_TIMEOUT_MS`=60s) feuert, waehrend die zweite Umwandlung eines
Accounts noch SERIELL in der Mega-Web-Single-Flight-Queue (90s `QUEUE_WAIT_TIMEOUT_MS`) auf den laufenden
Vorgaenger wartet — also bevor ueberhaupt echte Arbeit begann. `raceWithAbort` warf bisher unbedingt
`aborted:mega-web`; `unrestrictViaWeb` (debrid.ts:1887) flachte JEDEN signal-aborted-Fall zu `aborted:debrid`
ab; die Rotation (debrid.ts:2072) wertete das via `/aborted/i && !/timeout/i``ranLongEnough`
(elapsedMs schliesst die Queue-Wartezeit ein, also >= 8s) → **120s Account-Cooldown auf einen voellig
gesunden Account**. Genau die vom Nutzer gemeldete „Tool sperrt sich selbst"-Klasse (Web-Variante).
Zweiteiliger Fix:
(1) `MegaWebFallback.runExclusive` (mega-web-fallback.ts) trackt `workStarted` und reicht eine
`abortErrorFactory` an `raceWithAbort`: abgebrochen-bevor-Arbeit-begann → `Mega-Web Queue-Timeout (…)`
(matcht `/queue.?timeout/i`), nur ein echter In-Arbeit-Abbruch bleibt `aborted:mega-web`.
(2) `unrestrictViaWeb` (debrid.ts:1887) bewahrt einen `/queue.?timeout/i`-klassifizierten lastError statt
ihn zu `aborted:debrid` zu plaetten → Rotation trifft die bestehende Queue-Timeout-Ausnahme
(classifyAccountFailure 2243 → cooldownMs 0) statt den Abbruch-Cooldown-Zweig.
Rot-bewiesen NICHT-vakuum (zwei Tests, beide per Temp-Revert rot verifiziert):
- Teil 1 (mega-web-fallback.test.ts): echtes `MegaWebFallback`, Queue mit langsamem Erst-Job belegt,
Zweit-Call WAEHREND in Queue abgebrochen → `rejects.toThrow(/queue.?timeout/i)` (ohne Fix: `aborted:mega-web`).
- Teil 2 (debrid.test.ts): durch `DebridService.unrestrictLink` mit `RD_MEGA_ABORT_MIN_RUN_MS=0` (besiegt die
Vakuum-Falle: der buggy Pfad WUERDE selbst bei Sofort-Abbruch cooldownen), Signal bricht WAEHREND des
Calls ab, megaWeb meldet Queue-Timeout → `getMegaDebridAccountCooldownState` bleibt null (ohne Fix: 120s
„Abbruch/Timeout nach 0s"). tsc=6, volle mega-web+debrid-Suite 103 gruen inkl. der bestehenden
Abbruch-Cooldown-Tests (echter langsamer Account cooled WEITERHIN — keine Regression).
- **Rotations-Verifikation (Advisor-Shippability-Diskriminator):** Nach dem No-Cooldown-Pfad retried der
Manager `unrestrictWithAccounts` frisch. Die Account-Wahl sortiert per `megaDebridInFlight`-TIEFE
(debrid.ts:1981-1986, least-busy zuerst). Invariante: jeder Belegt-Halter der MegaWebFallback-Queue
entspricht einem in-flight `client.unrestrictLink`, das `megaDebridInFlight` inkrementiert hat (2033,
Dekrement nur im finally 2152). Also sieht der Retry des queue-getimeouteten Links den saturierenden
Account bei Tiefe>=1 und rotiert auf einen freien — MW-1 ist NACHWEISLICH besser als das alte 120s-Lockout
(das einen nur-belegten gesunden Account sperrte). Kein Tight-Retry-Loop auf saturierter Queue, solange
irgendein Account frei ist; sind ALLE saturiert, queued der least-busy (unvermeidbar — keine freie Kapazitaet,
aber das alte Cooldown haette es via Faux-Park SCHLIMMER gemacht).
- **DOKUMENTIERT, nicht gefixt (LOW) DL-1 DebridLink-Key-Cooldown bei User-Cancel-Abbruch:** classifyKeyFailure
(debrid.ts:3118-3126) gibt fuer Text mit „aborted" (via isRetryableErrorText 597 + isTransport 3119)
`cooldownMs: 15_000` zurueck — auch bei einem SCHNELLEN User-Cancel (kein Min-Run-Gate wie bei Mega 2072).
Symptom: 15s Key-Cooldown nach Nutzer-Abbruch. Advisor (revidiert eigenen frueheren „clean fix"-Call):
classifyKeyFailure hat KEIN elapsedMs und DebridLink hat keinen Min-Run-Knopf-Aequivalent zu
`getMegaDebridAbortMinRunMs()`. Der treue Fix (Caller-Gate bei 2787 spiegeln) braucht entweder cross-Provider-
Wiederverwendung des Mega-Knopfes oder einen neuen DebridLink-Knopf = neue Oberflaeche im Live-Hot-Path fuer
einen LOW-Bug (Symptom: 15s nach User-Cancel). Klart die eigene „clean + null-Regression"-Schwelle NICHT →
dokumentiert, nicht gefixt.
- **DOKUMENTIERT, nicht gefixt (MED) DL-CONCURRENCY-PILEUP:** Bei gleichzeitigen Umwandlungen, die ALLE
Mega-Accounts saturieren, queued der least-busy-Account weitere Links seriell (per-Account-Single-Flight) →
Wartezeiten stapeln sich. MW-1 entfernt die FRUEHERE versehentliche Backpressure (der falsche 120s-Cooldown
wirkte als grobe Ratenbegrenzung), routet aber korrekt per In-Flight-Tiefe um belegte Accounts herum statt sie
faelschlich zu sperren — strikt besser. Echter Fix (In-Flight-Tiefen-Spread groesser ziehen / globale
Web-Parallelitaetsgrenze) = groessere Aenderung, 4.-Bug-Risiko auf Live-Server → deferred. MW-1 reduziert das
Pileup-in-Park-Risiko bereits (kein Faux-Lockout merely-busy Accounts).
## Runde 10 (Concurrency/Locking + Settings/Persistenz-Integritaet) — LETZTE RUNDE (Nutzer: „nach der Runde ist Schluss")
4 Finder (hybrid/targetPath-Races, Slot-Accounting, Settings-Migration/Persistenz, Backup-Restore/account-check)
→ adversarisch 3 Lenses → Synthese. Workflow wwyqsdrf9: 2 Finder (slot-accounting, settings-persistence) starben
an Stream-Idle-Timeout (Riesen-Dateien) → 0/7 confirmed war NUR fuer 2 von 4 Dimensionen ehrlich. KEIN stilles
Coverage-Loch akzeptiert → Re-Run wwyqsdrf9b (wfmu4pw2n) mit engerem Scope (Grep-dann-Region statt Ganzdatei).
- **Dim hybrid/targetPath + backup/account-check (wwyqsdrf9): 7 Kandidaten, ALLE refutiert (>=2/3), quell-reverifiziert.**
Kern-Invarianten halten: synchrones `claimTargetPath` (dl-mgr:6257) = nie zwei Items auf einem Pfad → alle drei
Hybrid-„Races" kollabieren zu Sub-Sekunden-Redundanzarbeit, kein Collision/Korruption; jeder Loss-Ausgang von
CRC/Extraction-Fail re-queued (`hybridExtractRequeue.add` 11739). Restore ist fail-closed (encrypted-or-reject,
Binaerheader „MDD1" wirft in JSON.parse vor normalizeSettings). account-check `valid:false` ist reiner
Renderer-Badge (kein src/main liest ihn als Gate; Disable laeuft ueber megaDebridDisabledAccountIds).
- **CONFIRMED MED (DOKUMENTIERT, nicht gefixt) BYTE-DROP-RETRY-1 (wfmu4pw2n):** Integrity-/too-small-/tiny-Retry
doppelzaehlt eine volle Datei in die Byte-Statistik. `dropItemContribution` (6247) loescht den
itemContributedBytes-Eintrag OHNE von session.totalDownloadedBytes abzuziehen (eigener Kommentar: „retry path
subtracts on its own"), aber die EINZIGE Subtraktion (9991-9997) liest genau diesen geloeschten Eintrag → 0 →
Subtraktion tot → Re-Download addiert N nochmal → (k+1)*fileSize nach k Integrity-Fails. enableIntegrityCheck
default true → organisch. Verdict isReal (CORRECTNESS+REPRO), aber BLAST-RADIUS = NUR Statistik: kein
Slot/Admission/Semaphore liest diese Counter (grep-belegt), Session-Counter self-healen bei jedem Neustart,
nur persistiertes totalDownloadedAllTime + avg-Speed bleiben dauerhaft inflationiert = kosmetisch. NICHT clean
(dropItemContribution ueber 26 Call-Sites ueberladen; ~10 im Retry-Bereich; ein 3-Site-Patch liefert
partielles/inkonsistentes Accounting = arguably schlimmer; Guard-Test dl-mgr.test.ts:6152 sperrt die
Completion-Removal-Semantik) → DOKUMENTIERT mit Rezept (dropItemContribution splitten in ForCompletion/ForRetry
nach Klassifikation aller Retry-Sites re-download-vs-terminal). Kosmetisch + nicht-clean → klart Live-Schwelle nicht.
- **CONFIRMED MED (GEFIXT) SET-MIG-01 (wfmu4pw2n):** Pre-v1.6.90-Config-Migration tot → Mega-Debrid wird beim
ersten Settings-Panel-Save still aus der Provider-Reihenfolge demotet. readSettingsFile (storage.ts:633) merged
`{...defaultSettings(), ...parsed}`; defaultSettings setzt megaDebridApiEnabled/WebEnabled:false (constants.ts:50-51)
BEVOR normalizeSettings laeuft → der `=== undefined`-Migrationszweig (355-360) ist tot auf dem Disk-Pfad (Git:
tot seit v1.6.90/0003d78). Renderer (App.tsx:447-494) gated Mega-Inklusion am false-Flag → persistDraftSettings
schreibt eine Mega-bereinigte providerOrder zurueck. Fix: reine `migrateLegacyMegaEnableFlags(parsed)` in
readSettingsFile — seedet apiEnabled=preferApi/webEnabled=!preferApi NUR wenn BEIDE Flags im RAW-parsed fehlen
UND Creds da sind (Creds-Erkennung wie normalizeSettings: asText(login)&&asText(password)). Lokalisiert, keine
normalizeSettings-Signatur-Aenderung, getypt (kein Cast). EHRLICHER Scope (Advisor): rettet NUR Legacy-Configs,
die seit dem Upgrade noch NICHT ueber das Settings-Panel neu gespeichert wurden (ein Post-Upgrade-Save schreibt
present-false → Trigger feuert nicht mehr) — schmales historisches Fenster, NICHT „rettet die Live-Mega dieses
Nutzers". HARTE Grenze (Advisor): Trigger bleibt absent-both; present-false NICHT anfassen (= bewusst-deaktiviert,
ununterscheidbar → das geparkte entscheidungen-offen #2-Migrationsrisiko). Rot-bewiesen NICHT-vakuum (Assertion NUR
auf den geladenen Flags — providerOrder demotet backend-seitig nicht = waere vakuum; via Temp-Revert rot bestaetigt:
apiEnabled true→false). Boundary-Test (bewusst-deaktiviert bleibt false + ohne Creds keine Migration) bleibt beim
Revert gruen = unabhaengig. Volle Suite 890 gruen, tsc=6.
- **REFUTED (wfmu4pw2n) SET-PERSIST-02:** Residual-Lost-Update nach R2-Generations-Guard — der einzige verlorene
Payload ist totalRuntimeAllTimeMs (`<=`-Ratchet, naechste Session re-derived) + contrived Sub-ms-Race der die
Prozess-Teardown gewinnen muss → self-healing kosmetisch, nicht erreichbar.
- **Coherence-Verdikt:** Concurrency-Accounting + Settings-Persistenz sind kohaerent. Byte-Counter sind
telemetry-only, voll entkoppelt von Admission/Slot (grep-belegt) → BYTE-DROP kann nicht stranden/freezen/
ueber-admiten. Persistenz kohaerent bis auf EINE benannte Inkohaerenz: Backend normalizeConfiguredProvider
(storage.ts:144) mapt „megadebrid"→konkret unabhaengig der Flags, Renderer gated auf dem Flag = Wurzel von
SET-MIG-01 (jetzt am Loader gefixt).
- **EHRLICHE Coverage-Luecke (vom Audit selbst aufgedeckt, R10-HYB-3):** Die in project_pending genannte
Passwort-Cache-Race liegt NICHT in download-manager.ts (dort kein passwordCache/resolvePassword), sondern in
src/main/extractor.ts — ausserhalb des Round-10-Scopes. Bewusst NICHT in dieser letzten Runde auditiert (Nutzer:
„nach der Runde ist Schluss"); als das eine identifizierte, un-auditierte Concurrency-Seam fuer eine etwaige
kuenftige Runde dokumentiert statt still fallengelassen.
## GOAL-ABSCHLUSS
Runde 9 + 10 erledigt (Nutzer-Anpassung). Release v1.7.220 buendelt: 85c8d6b (Synthese C1/C2/RANGE1-Haertung) +
MW-1 (HIGH, Web-Selbstcooldown) + SET-MIG-01 (MED, Legacy-Mega-Demotion). Gitea + GitHub-Mirror MIT .exe (4 Assets).
Dokumentiert-nicht-gefixt: DL-1, DL-CONCURRENCY-PILEUP, BYTE-DROP-RETRY-1, extractor.ts-Passwort-Cache-Seam.
Beim Nutzer (nicht autonom): 60s-Failover-Kappung + gespiegelter Mega-API/Web-Schalter (entscheidungen-offen.md).
## Runde 1 (laeuft)
- Discover+Verify-Workflow ueber 7 Subsysteme (scheduler-slots, unrestrict-retry, mega-rotation,
classify-cooldown, mega-web-token, provider-chain-timeout, account-availability).

View File

@ -63,6 +63,30 @@ ODER zwei unabhängige Pro-Modus-Schalter (mehr Kontrolle, aber UI + Migration n
## Erledigt in dieser Runde (zur Info, kein Handlungsbedarf)
- **Alte Konfiguration: Mega-Debrid fällt nach einem Upgrade nicht mehr still aus der Provider-Reihenfolge:**
Eine Konfigurationsdatei, die noch von einer sehr alten Version (vor v1.6.90) stammt, kannte die getrennten
Mega-Debrid „API aktiv"/„Web aktiv"-Schalter noch nicht. Beim Laden wurden diese fehlenden Schalter still auf
„aus" gesetzt, obwohl Mega-Zugangsdaten vorhanden waren — und sobald man danach das erste Mal die Einstellungen
speicherte, wurde Mega-Debrid dadurch lautlos aus der Provider-Reihenfolge entfernt. Jetzt wird beim Laden einer
solchen alten Datei erkannt, dass die Schalter komplett fehlen, und Mega-Debrid passend zu deiner Bevorzugung
(API oder Web) aktiviert — genau die Migration, die ursprünglich gedacht war, aber durch einen Default-Vorrang
nie ausgelöst hatte. Ehrlicher Umfang: Das betrifft nur alte Dateien, die seit dem Upgrade noch NICHT über die
Einstellungen neu gespeichert wurden — wer seit dem Update schon einmal in den Einstellungen gespeichert hat, hat
die Schalter bereits als „aus" stehen und greift dort weiterhin manuell ein (das ist Absicht: ein bewusst auf
„aus" gestellter Schalter wird NICHT wieder angeschaltet). Rot-bewiesener Test; voller Testlauf grün.
- **Mega-Web: gesunder Account sperrt sich nicht mehr selbst, nur weil er gerade belegt war (deine „Tool sperrt sich selbst"-Klasse, Web-Variante):**
Wenn mehrere Links gleichzeitig über DENSELBEN Mega-Account umgewandelt wurden, laufen sie absichtlich nacheinander
(eine Warteschlange pro Account, damit nicht doppelt eingeloggt/gehämmert wird). Wartete ein Link in dieser Schlange
noch auf seinen Vorgänger und lief dabei der 60-Sekunden-Gesamttimeout ab, wurde der Abbruch fälschlich wie ein echter
Account-Fehler gewertet → der völlig gesunde Account bekam 120 Sekunden Sperre. Jetzt wird ein Abbruch, der NOCH IN DER
Warteschlange passiert (bevor echte Arbeit begann), als reiner Warteschlangen-Timeout erkannt: KEINE Account-Sperre,
der Link wird einfach erneut versucht und rotiert dann von selbst auf einen freien Account. Ein echter Abbruch MITTEN
in der Arbeit sperrt den Account weiterhin (damit langsame Accounts korrekt übersprungen werden) — das blieb unverändert.
Zwei rot-bewiesene Tests (jeder ohne Fix nachweislich rot; der zweite läuft komplett durch die echte Account-Rotation
und prüft, dass keine Sperre gesetzt wird). Die Rotation auf einen freien Account ist über die Auslastungs-Verteilung
(am-wenigsten-belegter-Account-zuerst) abgesichert — strikt besser als die alte 120s-Pauschalsperre.
- **Stille Datei-Beschädigung beim Resume nach Verbindungsabbruch behoben (für Dateien mit bekannter Größe):**
Wenn ein Debrid-Server beim Abbruch einen kleinen Fehler-Müll-Block mitten in den Datenstrom schreibt und
das genau im LETZTEN Wiederhol-Versuch passierte, blieb dieser Müll in der Datei und der anschließende

View File

@ -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);

View File

@ -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);
});
});

View File

@ -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({