Harden extraction verification, cleanup safety, and logging in v1.3.10

This commit is contained in:
Sucukdeluxe 2026-02-27 15:43:52 +01:00
parent ef821b69a5
commit e2a8673c94
6 changed files with 321 additions and 17 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "real-debrid-downloader", "name": "real-debrid-downloader",
"version": "1.3.9", "version": "1.3.10",
"description": "Real-Debrid Downloader Desktop (Electron + React + TypeScript)", "description": "Real-Debrid Downloader Desktop (Electron + React + TypeScript)",
"main": "build/main/main/main.js", "main": "build/main/main/main.js",
"author": "Sucukdeluxe", "author": "Sucukdeluxe",

View File

@ -5,7 +5,7 @@ import { importDlcContainers } from "./container";
import { APP_VERSION, defaultSettings } from "./constants"; import { APP_VERSION, defaultSettings } from "./constants";
import { DownloadManager } from "./download-manager"; import { DownloadManager } from "./download-manager";
import { parseCollectorInput } from "./link-parser"; import { parseCollectorInput } from "./link-parser";
import { configureLogger, logger } from "./logger"; import { configureLogger, getLogFilePath, logger } from "./logger";
import { MegaWebFallback } from "./mega-web-fallback"; import { MegaWebFallback } from "./mega-web-fallback";
import { createStoragePaths, loadSession, loadSettings, normalizeSettings, saveSettings } from "./storage"; import { createStoragePaths, loadSession, loadSettings, normalizeSettings, saveSettings } from "./storage";
import { checkGitHubUpdate, installLatestUpdate } from "./update"; import { checkGitHubUpdate, installLatestUpdate } from "./update";
@ -34,6 +34,7 @@ export class AppController {
this.onState?.(snapshot); this.onState?.(snapshot);
}); });
logger.info(`App gestartet v${APP_VERSION}`); logger.info(`App gestartet v${APP_VERSION}`);
logger.info(`Log-Datei: ${getLogFilePath()}`);
if (this.settings.autoResumeOnStart) { if (this.settings.autoResumeOnStart) {
const snapshot = this.manager.getSnapshot(); const snapshot = this.manager.getSnapshot();

View File

@ -174,6 +174,8 @@ export class DownloadManager extends EventEmitter {
private runCompletedPackages = new Set<string>(); private runCompletedPackages = new Set<string>();
private lastSchedulerHeartbeatAt = 0;
public constructor(settings: AppSettings, session: SessionState, storagePaths: StoragePaths, options: DownloadManagerOptions = {}) { public constructor(settings: AppSettings, session: SessionState, storagePaths: StoragePaths, options: DownloadManagerOptions = {}) {
super(); super();
this.settings = settings; this.settings = settings;
@ -592,6 +594,16 @@ export class DownloadManager extends EventEmitter {
return; return;
} }
const extractDirUsage = new Map<string, number>();
for (const packageId of this.session.packageOrder) {
const pkg = this.session.packages[packageId];
if (!pkg || pkg.cancelled || !pkg.extractDir) {
continue;
}
const key = pathKey(pkg.extractDir);
extractDirUsage.set(key, (extractDirUsage.get(key) || 0) + 1);
}
const cleanupTargetsByPackage = new Map<string, Set<string>>(); const cleanupTargetsByPackage = new Map<string, Set<string>>();
for (const packageId of this.session.packageOrder) { for (const packageId of this.session.packageOrder) {
const pkg = this.session.packages[packageId]; const pkg = this.session.packages[packageId];
@ -607,7 +619,8 @@ export class DownloadManager extends EventEmitter {
} }
const hasExtractMarker = items.some((item) => /entpack/i.test(item.fullStatus)); const hasExtractMarker = items.some((item) => /entpack/i.test(item.fullStatus));
const hasExtractedOutput = this.directoryHasAnyFiles(pkg.extractDir); const extractDirIsUnique = (extractDirUsage.get(pathKey(pkg.extractDir)) || 0) === 1;
const hasExtractedOutput = extractDirIsUnique && this.directoryHasAnyFiles(pkg.extractDir);
if (!hasExtractMarker && !hasExtractedOutput) { if (!hasExtractMarker && !hasExtractedOutput) {
continue; continue;
} }
@ -641,6 +654,8 @@ export class DownloadManager extends EventEmitter {
continue; continue;
} }
logger.info(`Nachtraegliches Cleanup geprueft: pkg=${pkg.name}, targets=${targets.size}, marker=${pkg.itemIds.some((id) => /entpack/i.test(this.session.items[id]?.fullStatus || ""))}`);
let removed = 0; let removed = 0;
for (const targetPath of targets) { for (const targetPath of targets) {
if (!fs.existsSync(targetPath)) { if (!fs.existsSync(targetPath)) {
@ -656,6 +671,8 @@ export class DownloadManager extends EventEmitter {
if (removed > 0) { if (removed > 0) {
logger.info(`Nachtraegliches Archive-Cleanup fuer ${pkg.name}: ${removed} Datei(en) geloescht`); logger.info(`Nachtraegliches Archive-Cleanup fuer ${pkg.name}: ${removed} Datei(en) geloescht`);
} else {
logger.info(`Nachtraegliches Archive-Cleanup fuer ${pkg.name}: keine Dateien entfernt`);
} }
} }
}) })
@ -793,11 +810,13 @@ export class DownloadManager extends EventEmitter {
} }
public prepareForShutdown(): void { public prepareForShutdown(): void {
logger.info(`Shutdown-Vorbereitung gestartet: active=${this.activeTasks.size}, running=${this.session.running}, paused=${this.session.paused}`);
this.session.running = false; this.session.running = false;
this.session.paused = false; this.session.paused = false;
this.session.reconnectUntil = 0; this.session.reconnectUntil = 0;
this.session.reconnectReason = ""; this.session.reconnectReason = "";
let requeuedItems = 0;
for (const active of this.activeTasks.values()) { for (const active of this.activeTasks.values()) {
const item = this.session.items[active.itemId]; const item = this.session.items[active.itemId];
if (item && !isFinishedStatus(item.status)) { if (item && !isFinishedStatus(item.status)) {
@ -806,6 +825,7 @@ export class DownloadManager extends EventEmitter {
const pkg = this.session.packages[item.packageId]; const pkg = this.session.packages[item.packageId];
item.fullStatus = pkg && !pkg.enabled ? "Paket gestoppt" : "Wartet"; item.fullStatus = pkg && !pkg.enabled ? "Paket gestoppt" : "Wartet";
item.updatedAt = nowMs(); item.updatedAt = nowMs();
requeuedItems += 1;
} }
active.abortReason = "shutdown"; active.abortReason = "shutdown";
active.abortController.abort("shutdown"); active.abortController.abort("shutdown");
@ -832,6 +852,7 @@ export class DownloadManager extends EventEmitter {
this.session.summaryText = ""; this.session.summaryText = "";
this.persistNow(); this.persistNow();
this.emitState(true); this.emitState(true);
logger.info(`Shutdown-Vorbereitung beendet: requeued=${requeuedItems}`);
} }
public togglePause(): boolean { public togglePause(): boolean {
@ -883,6 +904,39 @@ export class DownloadManager extends EventEmitter {
|| pkg.status === "reconnect_wait") { || pkg.status === "reconnect_wait") {
pkg.status = "queued"; pkg.status = "queued";
} }
const items = pkg.itemIds
.map((itemId) => this.session.items[itemId])
.filter(Boolean) as DownloadItem[];
if (items.length === 0) {
continue;
}
const hasPending = items.some((item) => (
item.status === "queued"
|| item.status === "reconnect_wait"
|| item.status === "validating"
|| item.status === "downloading"
|| item.status === "paused"
|| item.status === "extracting"
|| item.status === "integrity_check"
));
if (hasPending) {
pkg.status = pkg.enabled ? "queued" : "paused";
continue;
}
const success = items.filter((item) => item.status === "completed").length;
const failed = items.filter((item) => item.status === "failed").length;
const cancelled = items.filter((item) => item.status === "cancelled").length;
if (failed > 0) {
pkg.status = "failed";
} else if (cancelled > 0 && success === 0) {
pkg.status = "cancelled";
} else if (success > 0) {
pkg.status = "completed";
}
} }
this.persistSoon(); this.persistSoon();
} }
@ -1101,8 +1155,15 @@ export class DownloadManager extends EventEmitter {
return; return;
} }
this.scheduleRunning = true; this.scheduleRunning = true;
logger.info("Scheduler gestartet");
try { try {
while (this.session.running) { while (this.session.running) {
const now = nowMs();
if (now - this.lastSchedulerHeartbeatAt >= 60000) {
this.lastSchedulerHeartbeatAt = now;
logger.info(`Scheduler Heartbeat: active=${this.activeTasks.size}, queued=${this.countQueuedItems()}, reconnect=${this.reconnectActive()}, paused=${this.session.paused}, postProcess=${this.packagePostProcessTasks.size}`);
}
if (this.session.paused) { if (this.session.paused) {
await sleep(120); await sleep(120);
continue; continue;
@ -1131,6 +1192,7 @@ export class DownloadManager extends EventEmitter {
} }
} finally { } finally {
this.scheduleRunning = false; this.scheduleRunning = false;
logger.info("Scheduler beendet");
} }
} }
@ -1211,6 +1273,26 @@ export class DownloadManager extends EventEmitter {
return false; return false;
} }
private countQueuedItems(): number {
let count = 0;
for (const packageId of this.session.packageOrder) {
const pkg = this.session.packages[packageId];
if (!pkg || pkg.cancelled || !pkg.enabled) {
continue;
}
for (const itemId of pkg.itemIds) {
const item = this.session.items[itemId];
if (!item) {
continue;
}
if (item.status === "queued" || item.status === "reconnect_wait") {
count += 1;
}
}
}
return count;
}
private startItem(packageId: string, itemId: string): void { private startItem(packageId: string, itemId: string): void {
const item = this.session.items[itemId]; const item = this.session.items[itemId];
const pkg = this.session.packages[packageId]; const pkg = this.session.packages[packageId];
@ -1758,9 +1840,11 @@ export class DownloadManager extends EventEmitter {
const success = items.filter((item) => item.status === "completed").length; const success = items.filter((item) => item.status === "completed").length;
const failed = items.filter((item) => item.status === "failed").length; const failed = items.filter((item) => item.status === "failed").length;
const cancelled = items.filter((item) => item.status === "cancelled").length; const cancelled = items.filter((item) => item.status === "cancelled").length;
logger.info(`Post-Processing Start: pkg=${pkg.name}, success=${success}, failed=${failed}, cancelled=${cancelled}, autoExtract=${this.settings.autoExtract}`);
if (success + failed + cancelled < items.length) { if (success + failed + cancelled < items.length) {
pkg.status = "downloading"; pkg.status = "downloading";
logger.info(`Post-Processing verschoben: pkg=${pkg.name}, noch offene items`);
return; return;
} }
@ -1779,6 +1863,7 @@ export class DownloadManager extends EventEmitter {
removeLinks: this.settings.removeLinkFilesAfterExtract, removeLinks: this.settings.removeLinkFilesAfterExtract,
removeSamples: this.settings.removeSamplesAfterExtract removeSamples: this.settings.removeSamplesAfterExtract
}); });
logger.info(`Post-Processing Entpacken Ende: pkg=${pkg.name}, extracted=${result.extracted}, failed=${result.failed}, lastError=${result.lastError || ""}`);
if (result.failed > 0) { if (result.failed > 0) {
const reason = compactErrorText(result.lastError || "Entpacken fehlgeschlagen"); const reason = compactErrorText(result.lastError || "Entpacken fehlgeschlagen");
for (const entry of completedItems) { for (const entry of completedItems) {
@ -1797,6 +1882,7 @@ export class DownloadManager extends EventEmitter {
} }
} catch (error) { } catch (error) {
const reason = compactErrorText(error); const reason = compactErrorText(error);
logger.error(`Post-Processing Entpacken Exception: pkg=${pkg.name}, reason=${reason}`);
for (const entry of completedItems) { for (const entry of completedItems) {
entry.fullStatus = `Entpack-Fehler: ${reason}`; entry.fullStatus = `Entpack-Fehler: ${reason}`;
entry.updatedAt = nowMs(); entry.updatedAt = nowMs();
@ -1818,6 +1904,7 @@ export class DownloadManager extends EventEmitter {
} }
} }
pkg.updatedAt = nowMs(); pkg.updatedAt = nowMs();
logger.info(`Post-Processing Ende: pkg=${pkg.name}, status=${pkg.status}`);
} }
private applyCompletedCleanupPolicy(packageId: string, itemId: string): void { private applyCompletedCleanupPolicy(packageId: string, itemId: string): void {

View File

@ -249,6 +249,56 @@ function escapeRegex(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
} }
function captureDirFingerprint(rootDir: string): Map<string, string> {
const fingerprint = new Map<string, string>();
if (!fs.existsSync(rootDir)) {
return fingerprint;
}
const stack = [rootDir];
while (stack.length > 0) {
const current = stack.pop() as string;
let entries: fs.Dirent[] = [];
try {
entries = fs.readdirSync(current, { withFileTypes: true });
} catch {
continue;
}
for (const entry of entries) {
const full = path.join(current, entry.name);
if (entry.isDirectory()) {
stack.push(full);
continue;
}
if (!entry.isFile()) {
continue;
}
try {
const stat = fs.statSync(full);
const relative = path.relative(rootDir, full).toLowerCase();
fingerprint.set(relative, `${stat.size}:${stat.mtimeMs}`);
} catch {
// ignore
}
}
}
return fingerprint;
}
function hasDirChanges(before: Map<string, string>, after: Map<string, string>): boolean {
if (after.size > before.size) {
return true;
}
for (const [relative, meta] of after.entries()) {
if (before.get(relative) !== meta) {
return true;
}
}
return false;
}
export function collectArchiveCleanupTargets(sourceArchivePath: string): string[] { export function collectArchiveCleanupTargets(sourceArchivePath: string): string[] {
const targets = new Set<string>([sourceArchivePath]); const targets = new Set<string>([sourceArchivePath]);
const dir = path.dirname(sourceArchivePath); const dir = path.dirname(sourceArchivePath);
@ -302,9 +352,9 @@ export function collectArchiveCleanupTargets(sourceArchivePath: string): string[
return Array.from(targets); return Array.from(targets);
} }
function cleanupArchives(sourceFiles: string[], cleanupMode: CleanupMode): void { function cleanupArchives(sourceFiles: string[], cleanupMode: CleanupMode): number {
if (cleanupMode === "none") { if (cleanupMode === "none") {
return; return 0;
} }
const targets = new Set<string>(); const targets = new Set<string>();
@ -314,26 +364,37 @@ function cleanupArchives(sourceFiles: string[], cleanupMode: CleanupMode): void
} }
} }
let removed = 0;
for (const filePath of targets) { for (const filePath of targets) {
try { try {
if (!fs.existsSync(filePath)) {
continue;
}
fs.rmSync(filePath, { force: true }); fs.rmSync(filePath, { force: true });
removed += 1;
} catch { } catch {
// ignore // ignore
} }
} }
return removed;
} }
export async function extractPackageArchives(options: ExtractOptions): Promise<{ extracted: number; failed: number; lastError: string }> { export async function extractPackageArchives(options: ExtractOptions): Promise<{ extracted: number; failed: number; lastError: string }> {
const candidates = findArchiveCandidates(options.packageDir); const candidates = findArchiveCandidates(options.packageDir);
logger.info(`Entpacken gestartet: packageDir=${options.packageDir}, targetDir=${options.targetDir}, archives=${candidates.length}, cleanupMode=${options.cleanupMode}, conflictMode=${options.conflictMode}`);
if (candidates.length === 0) { if (candidates.length === 0) {
logger.info(`Entpacken uebersprungen (keine Archive gefunden): ${options.packageDir}`);
return { extracted: 0, failed: 0, lastError: "" }; return { extracted: 0, failed: 0, lastError: "" };
} }
const conflictMode = effectiveConflictMode(options.conflictMode);
const beforeFingerprint = captureDirFingerprint(options.targetDir);
let extracted = 0; let extracted = 0;
let failed = 0; let failed = 0;
let lastError = ""; let lastError = "";
const extractedArchives: string[] = []; const extractedArchives: string[] = [];
for (const archivePath of candidates) { for (const archivePath of candidates) {
logger.info(`Entpacke Archiv: ${path.basename(archivePath)} -> ${options.targetDir}`);
try { try {
const ext = path.extname(archivePath).toLowerCase(); const ext = path.extname(archivePath).toLowerCase();
if (ext === ".zip") { if (ext === ".zip") {
@ -347,6 +408,7 @@ export async function extractPackageArchives(options: ExtractOptions): Promise<{
} }
extracted += 1; extracted += 1;
extractedArchives.push(archivePath); extractedArchives.push(archivePath);
logger.info(`Entpacken erfolgreich: ${path.basename(archivePath)}`);
} catch (error) { } catch (error) {
failed += 1; failed += 1;
const errorText = String(error); const errorText = String(error);
@ -363,12 +425,26 @@ export async function extractPackageArchives(options: ExtractOptions): Promise<{
} }
if (extracted > 0) { if (extracted > 0) {
cleanupArchives(extractedArchives, options.cleanupMode); const afterFingerprint = captureDirFingerprint(options.targetDir);
if (options.removeLinks) { const changedOutput = hasDirChanges(beforeFingerprint, afterFingerprint);
removeDownloadLinkArtifacts(options.targetDir); if (!changedOutput && conflictMode !== "skip") {
} lastError = "Keine entpackten Dateien erkannt";
if (options.removeSamples) { failed += extracted;
removeSampleArtifacts(options.targetDir); extracted = 0;
logger.error(`Entpacken ohne neue Ausgabe erkannt: ${options.targetDir}. Cleanup wird NICHT ausgefuehrt.`);
} else {
const removedArchives = cleanupArchives(extractedArchives, options.cleanupMode);
if (options.cleanupMode !== "none") {
logger.info(`Archive-Cleanup abgeschlossen: ${removedArchives} Datei(en) entfernt`);
}
if (options.removeLinks) {
const removedLinks = removeDownloadLinkArtifacts(options.targetDir);
logger.info(`Link-Artefakt-Cleanup: ${removedLinks} Datei(en) entfernt`);
}
if (options.removeSamples) {
const removedSamples = removeSampleArtifacts(options.targetDir);
logger.info(`Sample-Cleanup: ${removedSamples.files} Datei(en), ${removedSamples.dirs} Ordner entfernt`);
}
} }
} else { } else {
try { try {
@ -380,5 +456,7 @@ export async function extractPackageArchives(options: ExtractOptions): Promise<{
} }
} }
logger.info(`Entpacken beendet: extracted=${extracted}, failed=${failed}, targetDir=${options.targetDir}`);
return { extracted, failed, lastError }; return { extracted, failed, lastError };
} }

View File

@ -2,18 +2,46 @@ import fs from "node:fs";
import path from "node:path"; import path from "node:path";
let logFilePath = path.resolve(process.cwd(), "rd_downloader.log"); let logFilePath = path.resolve(process.cwd(), "rd_downloader.log");
let fallbackLogFilePath: string | null = null;
export function configureLogger(baseDir: string): void { export function configureLogger(baseDir: string): void {
logFilePath = path.join(baseDir, "rd_downloader.log"); logFilePath = path.join(baseDir, "rd_downloader.log");
const cwdLogPath = path.resolve(process.cwd(), "rd_downloader.log");
fallbackLogFilePath = cwdLogPath === logFilePath ? null : cwdLogPath;
}
function appendLine(filePath: string, line: string): { ok: boolean; errorText: string } {
try {
fs.mkdirSync(path.dirname(filePath), { recursive: true });
fs.appendFileSync(filePath, line, "utf8");
return { ok: true, errorText: "" };
} catch (error) {
return { ok: false, errorText: String(error) };
}
} }
function write(level: "INFO" | "WARN" | "ERROR", message: string): void { function write(level: "INFO" | "WARN" | "ERROR", message: string): void {
const line = `${new Date().toISOString()} [${level}] ${message}\n`; const line = `${new Date().toISOString()} [${level}] ${message}\n`;
try { const primary = appendLine(logFilePath, line);
fs.mkdirSync(path.dirname(logFilePath), { recursive: true });
fs.appendFileSync(logFilePath, line, "utf8"); if (fallbackLogFilePath) {
} catch { const fallback = appendLine(fallbackLogFilePath, line);
// ignore logging failures if (!primary.ok && !fallback.ok) {
try {
process.stderr.write(`LOGGER write failed (primary+fallback): ${primary.errorText} | ${fallback.errorText}\n`);
} catch {
// ignore stderr failures
}
}
return;
}
if (!primary.ok) {
try {
process.stderr.write(`LOGGER write failed: ${primary.errorText}\n`);
} catch {
// ignore stderr failures
}
} }
} }

View File

@ -835,7 +835,7 @@ describe("download manager", () => {
name: "stopped", name: "stopped",
outputDir: path.join(root, "downloads", "stopped"), outputDir: path.join(root, "downloads", "stopped"),
extractDir: path.join(root, "extract", "stopped"), extractDir: path.join(root, "extract", "stopped"),
status: "downloading", status: "completed",
itemIds: [itemId], itemIds: [itemId],
cancelled: false, cancelled: false,
enabled: true, enabled: true,
@ -1027,6 +1027,116 @@ describe("download manager", () => {
expect(fs.existsSync(path.join(extractDir, "episode.mkv"))).toBe(true); expect(fs.existsSync(path.join(extractDir, "episode.mkv"))).toBe(true);
}); });
it("does not over-clean packages that share one extract directory", async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-"));
tempDirs.push(root);
const sharedExtractDir = path.join(root, "extract", "shared");
fs.mkdirSync(sharedExtractDir, { recursive: true });
fs.writeFileSync(path.join(sharedExtractDir, "already-extracted.mkv"), "ok", "utf8");
const pkg1Dir = path.join(root, "downloads", "pkg1");
const pkg2Dir = path.join(root, "downloads", "pkg2");
fs.mkdirSync(pkg1Dir, { recursive: true });
fs.mkdirSync(pkg2Dir, { recursive: true });
const pkg1Part1 = path.join(pkg1Dir, "show.one.part01.rar");
const pkg1Part2 = path.join(pkg1Dir, "show.one.part02.rar");
const pkg2Part1 = path.join(pkg2Dir, "show.two.part01.rar");
const pkg2Part2 = path.join(pkg2Dir, "show.two.part02.rar");
fs.writeFileSync(pkg1Part1, "a1", "utf8");
fs.writeFileSync(pkg1Part2, "a2", "utf8");
fs.writeFileSync(pkg2Part1, "b1", "utf8");
fs.writeFileSync(pkg2Part2, "b2", "utf8");
const session = emptySession();
const createdAt = Date.now() - 30_000;
session.packageOrder = ["pkg1", "pkg2"];
session.packages.pkg1 = {
id: "pkg1",
name: "pkg1",
outputDir: pkg1Dir,
extractDir: sharedExtractDir,
status: "completed",
itemIds: ["pkg1-item"],
cancelled: false,
enabled: true,
createdAt,
updatedAt: createdAt
};
session.packages.pkg2 = {
id: "pkg2",
name: "pkg2",
outputDir: pkg2Dir,
extractDir: sharedExtractDir,
status: "completed",
itemIds: ["pkg2-item"],
cancelled: false,
enabled: true,
createdAt,
updatedAt: createdAt
};
session.items["pkg1-item"] = {
id: "pkg1-item",
packageId: "pkg1",
url: "https://dummy/pkg1",
provider: "realdebrid",
status: "completed",
retries: 0,
speedBps: 0,
downloadedBytes: 1,
totalBytes: 1,
progressPercent: 100,
fileName: path.basename(pkg1Part1),
targetPath: pkg1Part1,
resumable: true,
attempts: 1,
lastError: "",
fullStatus: "Entpackt",
createdAt,
updatedAt: createdAt
};
session.items["pkg2-item"] = {
id: "pkg2-item",
packageId: "pkg2",
url: "https://dummy/pkg2",
provider: "realdebrid",
status: "completed",
retries: 0,
speedBps: 0,
downloadedBytes: 1,
totalBytes: 1,
progressPercent: 100,
fileName: path.basename(pkg2Part1),
targetPath: pkg2Part1,
resumable: true,
attempts: 1,
lastError: "",
fullStatus: "Fertig (100 MB)",
createdAt,
updatedAt: createdAt
};
new DownloadManager(
{
...defaultSettings(),
token: "rd-token",
outputDir: path.join(root, "downloads"),
extractDir: path.join(root, "extract"),
autoExtract: false,
cleanupMode: "delete"
},
session,
createStoragePaths(path.join(root, "state"))
);
await waitFor(() => !fs.existsSync(pkg1Part1) && !fs.existsSync(pkg1Part2), 5000);
expect(fs.existsSync(pkg2Part1)).toBe(true);
expect(fs.existsSync(pkg2Part2)).toBe(true);
});
it("resets run counters and reconnect state on start", async () => { it("resets run counters and reconnect state on start", async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-")); const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-"));
tempDirs.push(root); tempDirs.push(root);