fix: scope extraction follow-up work
Remove .rd-output staging and merge handling, parse validated native extractor output, and drive nested extraction, rename, audio, cleanup, residual handling, and library collection from package-owned output records. Preserve provenance migration, package pruning, and run-owned waiter isolation while allowing one bounded legacy adoption scan only for provably exclusive package roots.
This commit is contained in:
+37
-5
@@ -2,7 +2,13 @@ import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { cleanupCancelledPackageArtifacts, removeDownloadLinkArtifacts, removeSampleArtifacts } from "../src/main/cleanup";
|
||||
import {
|
||||
cleanupCancelledPackageArtifacts,
|
||||
removeDownloadLinkArtifacts,
|
||||
removeDownloadLinkArtifactsFromScope,
|
||||
removeSampleArtifacts,
|
||||
removeSampleArtifactsFromScope
|
||||
} from "../src/main/cleanup";
|
||||
|
||||
const tempDirs: string[] = [];
|
||||
|
||||
@@ -81,7 +87,7 @@ describe("cleanup", () => {
|
||||
expect(fs.existsSync(path.join(dir, "readme.txt"))).toBe(true);
|
||||
});
|
||||
|
||||
it("does not recurse into sample symlink or junction targets", async () => {
|
||||
it("does not recurse into sample symlink or junction targets", async () => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "rd-clean-"));
|
||||
const external = fs.mkdtempSync(path.join(os.tmpdir(), "rd-clean-ext-"));
|
||||
tempDirs.push(dir, external);
|
||||
@@ -95,6 +101,32 @@ describe("cleanup", () => {
|
||||
|
||||
const result = await removeSampleArtifacts(dir);
|
||||
expect(result.files).toBe(0);
|
||||
expect(fs.existsSync(outsideFile)).toBe(true);
|
||||
});
|
||||
});
|
||||
expect(fs.existsSync(outsideFile)).toBe(true);
|
||||
});
|
||||
|
||||
it("removes only scoped link and sample outputs from a shared root", async () => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "rd-clean-scope-"));
|
||||
tempDirs.push(dir);
|
||||
const sampleDir = path.join(dir, "Samples");
|
||||
fs.mkdirSync(sampleDir, { recursive: true });
|
||||
const ownedLink = path.join(dir, "owned.url");
|
||||
const foreignLink = path.join(dir, "foreign.url");
|
||||
const ownedSample = path.join(sampleDir, "owned-sample.mkv");
|
||||
const foreignSample = path.join(sampleDir, "foreign-sample.mkv");
|
||||
fs.writeFileSync(ownedLink, "owned");
|
||||
fs.writeFileSync(foreignLink, "foreign");
|
||||
fs.writeFileSync(ownedSample, "owned");
|
||||
fs.writeFileSync(foreignSample, "foreign");
|
||||
|
||||
const removedLinks = await removeDownloadLinkArtifactsFromScope([ownedLink]);
|
||||
const removedSamples = await removeSampleArtifactsFromScope([ownedSample]);
|
||||
|
||||
expect(removedLinks).toBe(1);
|
||||
expect(removedSamples).toEqual({ files: 1, dirs: 0 });
|
||||
expect(fs.existsSync(ownedLink)).toBe(false);
|
||||
expect(fs.existsSync(ownedSample)).toBe(false);
|
||||
expect(fs.existsSync(foreignLink)).toBe(true);
|
||||
expect(fs.existsSync(foreignSample)).toBe(true);
|
||||
expect(fs.existsSync(sampleDir)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
+102
-77
@@ -587,7 +587,16 @@ describe("disk write recovery", () => {
|
||||
})
|
||||
});
|
||||
|
||||
const processed = await (manager as any).keepGermanAudioOnlyImpl(extractDir, pkg);
|
||||
const outputScope = (manager as any).getPackageOutputScope(pkg);
|
||||
outputScope.add({
|
||||
version: 1,
|
||||
archivePath: path.join(pkg.outputDir, "archive.rar"),
|
||||
entryPath: path.basename(sourcePath),
|
||||
outputPath: sourcePath,
|
||||
state: "complete",
|
||||
disposition: "written"
|
||||
});
|
||||
const processed = await (manager as any).keepGermanAudioOnlyImpl(extractDir, outputScope, pkg);
|
||||
|
||||
expect(processed).toBe(0);
|
||||
expect(fs.existsSync(sourcePath)).toBe(true);
|
||||
@@ -12758,7 +12767,7 @@ describe("download manager", () => {
|
||||
void manager;
|
||||
}, 20000);
|
||||
|
||||
it("collect cleans a raw file sitting OUTSIDE extractDir (Downloader-Unfertig case) AND its .srt follows the rename", async () => {
|
||||
it("does not collect unscoped raw files outside the package extract directory", async () => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-"));
|
||||
tempDirs.push(root);
|
||||
|
||||
@@ -12808,10 +12817,9 @@ describe("download manager", () => {
|
||||
|
||||
await (manager as any).collectMkvFilesToLibrary(packageId, session.packages[packageId], undefined, false);
|
||||
|
||||
const cleanBase = "Fritzie.-.Der.Himmel.muss.warten.S04E01.GERMAN.720p.WEB.AVC-4SF";
|
||||
expect(fs.existsSync(path.join(mkvLibraryDir, `${cleanBase}.mkv`))).toBe(true);
|
||||
expect(fs.existsSync(path.join(mkvLibraryDir, rawName))).toBe(false);
|
||||
expect(fs.existsSync(path.join(mkvLibraryDir, `${cleanBase}.de.srt`))).toBe(true);
|
||||
expect(fs.existsSync(path.join(outputDir, rawName))).toBe(true);
|
||||
expect(fs.existsSync(path.join(outputDir, rawSrt))).toBe(true);
|
||||
expect(fs.existsSync(mkvLibraryDir) ? fs.readdirSync(mkvLibraryDir) : []).toEqual([]);
|
||||
|
||||
void manager;
|
||||
}, 20000);
|
||||
@@ -13246,7 +13254,7 @@ describe("download manager", () => {
|
||||
void manager;
|
||||
}, 20000);
|
||||
|
||||
it("does NOT delete pending RAR archive sets in outputDir when collecting MKVs from extractDir", async () => {
|
||||
it("does NOT delete pending RAR archive sets in outputDir when collecting MKVs from extractDir", async () => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-"));
|
||||
tempDirs.push(root);
|
||||
|
||||
@@ -13318,8 +13326,53 @@ describe("download manager", () => {
|
||||
expect(fs.existsSync(path.join(outputDir, part))).toBe(true);
|
||||
}
|
||||
|
||||
void manager;
|
||||
}, 20000);
|
||||
void manager;
|
||||
}, 20000);
|
||||
|
||||
it("does not adopt unscoped files from an extraction root shared by packages", async () => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-shared-adoption-"));
|
||||
tempDirs.push(root);
|
||||
const sharedExtractDir = path.join(root, "shared");
|
||||
const libraryDir = path.join(root, "library");
|
||||
fs.mkdirSync(sharedExtractDir, { recursive: true });
|
||||
const foreignPath = path.join(sharedExtractDir, "foreign.mkv");
|
||||
fs.writeFileSync(foreignPath, "foreign");
|
||||
const session = emptySession();
|
||||
for (const id of ["package-a", "package-b"]) {
|
||||
session.packages[id] = {
|
||||
id,
|
||||
name: id,
|
||||
outputDir: path.join(root, "downloads", id),
|
||||
extractDir: sharedExtractDir,
|
||||
status: "completed",
|
||||
itemIds: [],
|
||||
cancelled: false,
|
||||
enabled: true,
|
||||
outputProvenanceVersion: 1,
|
||||
outputRecords: [],
|
||||
createdAt: 1_000,
|
||||
updatedAt: 1_000
|
||||
};
|
||||
}
|
||||
session.packageOrder = ["package-a", "package-b"];
|
||||
const manager = new DownloadManager(
|
||||
{
|
||||
...defaultSettings(),
|
||||
autoExtract: true,
|
||||
createExtractSubfolder: true,
|
||||
collectMkvToLibrary: true,
|
||||
mkvLibraryDir: libraryDir
|
||||
},
|
||||
session,
|
||||
createStoragePaths(path.join(root, "state"))
|
||||
);
|
||||
|
||||
await (manager as any).collectMkvFilesToLibrary("package-a", session.packages["package-a"]);
|
||||
|
||||
expect(fs.existsSync(foreignPath)).toBe(true);
|
||||
expect(fs.existsSync(path.join(libraryDir, "foreign.mkv"))).toBe(false);
|
||||
expect(session.packages["package-a"].outputRecords).toEqual([]);
|
||||
});
|
||||
|
||||
it("does NOT move bonus files from Extras subdirectory to flat library", async () => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-"));
|
||||
@@ -15114,7 +15167,7 @@ describe("package priority ordering", () => {
|
||||
});
|
||||
|
||||
describe("package lifecycle telemetry boundaries", () => {
|
||||
it("captures shared-root provenance from package staging without scanning unrelated files", async () => {
|
||||
it("captures direct package output scopes concurrently without scanning a shared root", async () => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-output-provenance-lock-"));
|
||||
tempDirs.push(root);
|
||||
const extractDir = path.join(root, "extract");
|
||||
@@ -15122,17 +15175,10 @@ describe("package lifecycle telemetry boundaries", () => {
|
||||
for (let index = 0; index < 2_000; index += 1) {
|
||||
fs.writeFileSync(path.join(extractDir, `foreign-${index}.txt`), "foreign");
|
||||
}
|
||||
const traversedDirectories: string[] = [];
|
||||
const manager = new DownloadManager(
|
||||
{ ...defaultSettings(), extractConflictMode: "overwrite" },
|
||||
emptySession(),
|
||||
createStoragePaths(path.join(root, "state")),
|
||||
{
|
||||
readOutputDirectory: async (directory: string) => {
|
||||
traversedDirectories.push(path.resolve(directory));
|
||||
return fs.promises.readdir(directory, { withFileTypes: true });
|
||||
}
|
||||
} as any
|
||||
createStoragePaths(path.join(root, "state"))
|
||||
);
|
||||
const createPackage = (id: string): PackageEntry => ({
|
||||
id,
|
||||
@@ -15155,79 +15201,51 @@ describe("package lifecycle telemetry boundaries", () => {
|
||||
let enteredB = false;
|
||||
const state = manager as any;
|
||||
|
||||
const first = state.runWithPackageOutputProvenance(packageA, async (operationTarget = extractDir) => {
|
||||
fs.writeFileSync(path.join(operationTarget, "package-a.mkv"), "a");
|
||||
const first = state.runWithPackageOutputProvenance(packageA, async (operationTarget: string, scope: any) => {
|
||||
const outputPath = path.join(operationTarget, "package-a.mkv");
|
||||
fs.writeFileSync(outputPath, "a");
|
||||
scope.add({
|
||||
version: 1,
|
||||
archivePath: path.join(packageA.outputDir, "archive.rar"),
|
||||
entryPath: "package-a.mkv",
|
||||
outputPath,
|
||||
state: "complete",
|
||||
disposition: "written"
|
||||
});
|
||||
await gateA;
|
||||
});
|
||||
await vi.waitFor(() => expect(enteredB).toBe(false));
|
||||
const second = state.runWithPackageOutputProvenance(packageB, async (operationTarget = extractDir) => {
|
||||
const second = state.runWithPackageOutputProvenance(packageB, async (operationTarget: string, scope: any) => {
|
||||
enteredB = true;
|
||||
fs.writeFileSync(path.join(operationTarget, "package-b.mkv"), "b");
|
||||
const outputPath = path.join(operationTarget, "package-b.mkv");
|
||||
fs.writeFileSync(outputPath, "b");
|
||||
scope.add({
|
||||
version: 1,
|
||||
archivePath: path.join(packageB.outputDir, "archive.rar"),
|
||||
entryPath: "package-b.mkv",
|
||||
outputPath,
|
||||
state: "complete",
|
||||
disposition: "written"
|
||||
});
|
||||
});
|
||||
await vi.waitFor(() => expect(enteredB).toBe(true));
|
||||
releaseA();
|
||||
await Promise.all([first, second]);
|
||||
expect(packageA.outputCount).toBe(1);
|
||||
expect(packageB.outputCount).toBe(1);
|
||||
expect(traversedDirectories.length).toBeGreaterThan(0);
|
||||
expect(traversedDirectories).not.toContain(path.resolve(extractDir));
|
||||
expect(traversedDirectories.length).toBeLessThanOrEqual(4);
|
||||
expect(packageA.outputRecords).toEqual([expect.objectContaining({ outputPath: path.join(extractDir, "package-a.mkv") })]);
|
||||
expect(packageB.outputRecords).toEqual([expect.objectContaining({ outputPath: path.join(extractDir, "package-b.mkv") })]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["overwrite", "package", ["episode.mkv"]],
|
||||
["skip", "foreign", ["episode.mkv"]],
|
||||
["rename", "foreign", ["episode (1).mkv", "episode.mkv"]]
|
||||
] as const)("preserves %s conflicts while merging staged package outputs", async (conflictMode, expectedOriginal, expectedFiles) => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), `rd-output-${conflictMode}-`));
|
||||
tempDirs.push(root);
|
||||
const extractDir = path.join(root, "extract");
|
||||
fs.mkdirSync(extractDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(extractDir, "episode.mkv"), "foreign");
|
||||
const manager = new DownloadManager(
|
||||
{ ...defaultSettings(), extractConflictMode: conflictMode },
|
||||
emptySession(),
|
||||
createStoragePaths(path.join(root, "state"))
|
||||
);
|
||||
const pkg: PackageEntry = {
|
||||
id: `conflict-${conflictMode}`,
|
||||
name: `conflict-${conflictMode}`,
|
||||
outputDir: path.join(root, "downloads"),
|
||||
extractDir,
|
||||
status: "completed",
|
||||
itemIds: [],
|
||||
cancelled: false,
|
||||
enabled: true,
|
||||
createdAt: 1_000,
|
||||
updatedAt: 1_000
|
||||
};
|
||||
const state = manager as any;
|
||||
|
||||
await state.runWithPackageOutputProvenance(pkg, async (operationTarget = extractDir) => {
|
||||
fs.writeFileSync(path.join(operationTarget, "episode.mkv"), "package");
|
||||
});
|
||||
|
||||
expect(fs.readFileSync(path.join(extractDir, "episode.mkv"), "utf8")).toBe(expectedOriginal);
|
||||
expect(fs.readdirSync(extractDir).filter((name) => name.endsWith(".mkv")).sort()).toEqual([...expectedFiles]);
|
||||
expect(pkg.outputCount).toBe(conflictMode === "skip" ? 0 : 1);
|
||||
});
|
||||
|
||||
it("retains partial staged outputs deterministically when extraction aborts", async () => {
|
||||
it("retains directly reported partial outputs when extraction aborts", async () => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-output-abort-"));
|
||||
tempDirs.push(root);
|
||||
const extractDir = path.join(root, "extract");
|
||||
fs.mkdirSync(extractDir, { recursive: true });
|
||||
const traversedDirectories: string[] = [];
|
||||
const manager = new DownloadManager(
|
||||
defaultSettings(),
|
||||
emptySession(),
|
||||
createStoragePaths(path.join(root, "state")),
|
||||
{
|
||||
readOutputDirectory: async (directory: string) => {
|
||||
traversedDirectories.push(path.resolve(directory));
|
||||
return fs.promises.readdir(directory, { withFileTypes: true });
|
||||
}
|
||||
} as any
|
||||
createStoragePaths(path.join(root, "state"))
|
||||
);
|
||||
const pkg: PackageEntry = {
|
||||
id: "aborted-output",
|
||||
@@ -15243,16 +15261,23 @@ describe("package lifecycle telemetry boundaries", () => {
|
||||
};
|
||||
const state = manager as any;
|
||||
|
||||
await expect(state.runWithPackageOutputProvenance(pkg, async (operationTarget = extractDir) => {
|
||||
fs.writeFileSync(path.join(operationTarget, "partial.mkv"), "partial");
|
||||
await expect(state.runWithPackageOutputProvenance(pkg, async (operationTarget: string, scope: any) => {
|
||||
const outputPath = path.join(operationTarget, "partial.mkv");
|
||||
fs.writeFileSync(outputPath, "partial");
|
||||
scope.add({
|
||||
version: 1,
|
||||
archivePath: path.join(pkg.outputDir, "archive.rar"),
|
||||
entryPath: "partial.mkv",
|
||||
outputPath,
|
||||
state: "partial",
|
||||
disposition: "written"
|
||||
});
|
||||
throw new Error("aborted:extract");
|
||||
})).rejects.toThrow("aborted:extract");
|
||||
|
||||
expect(fs.readFileSync(path.join(extractDir, "partial.mkv"), "utf8")).toBe("partial");
|
||||
expect(pkg.outputCount).toBe(1);
|
||||
expect(traversedDirectories.length).toBeGreaterThan(0);
|
||||
expect(traversedDirectories).not.toContain(path.resolve(extractDir));
|
||||
expect(fs.readdirSync(extractDir).filter((name) => name.startsWith(".rd-output-"))).toEqual([]);
|
||||
expect(pkg.outputRecords).toEqual([expect.objectContaining({ state: "partial", outputPath: path.join(extractDir, "partial.mkv") })]);
|
||||
});
|
||||
|
||||
it("uses normalized nested item paths for archive identity and leaves empty item provenance at zero", () => {
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
shouldSerialRetryParallelFailures,
|
||||
findArchiveCandidates,
|
||||
orderExtractorCandidatesForArchive,
|
||||
parseNativeExtractOutput,
|
||||
resolveExtractorBackendModeForArchive,
|
||||
resolveExtractorBackendMode,
|
||||
shouldFallbackLegacyRarToJvm,
|
||||
@@ -1467,5 +1468,32 @@ describe("extractor", () => {
|
||||
expect(fs.existsSync(path.join(targetDir, "second.txt"))).toBe(false);
|
||||
});
|
||||
|
||||
it("strictly parses native output paths and fails closed for ambiguous rename output", () => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-native-output-"));
|
||||
tempDirs.push(root);
|
||||
const targetDir = path.join(root, "out");
|
||||
fs.mkdirSync(targetDir, { recursive: true });
|
||||
const archivePath = path.join(root, "archive.7z");
|
||||
const exactPath = path.join(targetDir, "folder", "episode.mkv");
|
||||
fs.mkdirSync(path.dirname(exactPath), { recursive: true });
|
||||
fs.writeFileSync(exactPath, "video");
|
||||
|
||||
expect(parseNativeExtractOutput("7z.exe", "- folder\\episode.mkv", archivePath, targetDir, "overwrite")).toEqual([
|
||||
expect.objectContaining({ entryPath: "folder/episode.mkv", outputPath: exactPath, disposition: "overwritten" })
|
||||
]);
|
||||
expect(parseNativeExtractOutput("UnRAR.exe", `Extracting ${exactPath} OK`, archivePath, targetDir, "overwrite")).toEqual([
|
||||
expect.objectContaining({ entryPath: "folder/episode.mkv", outputPath: exactPath })
|
||||
]);
|
||||
expect(parseNativeExtractOutput("7z.exe", "- ..\\foreign.mkv", archivePath, targetDir, "overwrite")).toEqual([]);
|
||||
|
||||
const renamedPath = path.join(targetDir, "episode (1).mkv");
|
||||
fs.writeFileSync(path.join(targetDir, "episode.mkv"), "foreign");
|
||||
fs.writeFileSync(renamedPath, "owned");
|
||||
expect(parseNativeExtractOutput("7z.exe", "- episode.mkv", archivePath, targetDir, "rename")).toEqual([]);
|
||||
expect(parseNativeExtractOutput("7z.exe", "- episode (1).mkv", archivePath, targetDir, "rename")).toEqual([
|
||||
expect.objectContaining({ outputPath: renamedPath, disposition: "renamed" })
|
||||
]);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1271,6 +1271,101 @@ describe("settings storage", () => {
|
||||
}));
|
||||
});
|
||||
|
||||
it("fails closed for unknown package output provenance versions without relabeling", () => {
|
||||
const normalized = normalizeLoadedSession({
|
||||
version: 2,
|
||||
packageOrder: ["future-output"],
|
||||
packages: {
|
||||
"future-output": {
|
||||
id: "future-output",
|
||||
name: "Future output",
|
||||
outputDir: "C:\\Downloads\\Future",
|
||||
extractDir: "C:\\Downloads\\Shared",
|
||||
status: "completed",
|
||||
itemIds: [],
|
||||
cancelled: false,
|
||||
enabled: true,
|
||||
outputCount: 2,
|
||||
outputProvenanceVersion: 99,
|
||||
outputProvenance: ["a".repeat(64), "b".repeat(64)],
|
||||
outputRecords: [{
|
||||
version: 1,
|
||||
archivePath: "C:\\Downloads\\Future\\archive.rar",
|
||||
entryPath: "episode.mkv",
|
||||
outputPath: "C:\\Downloads\\Shared\\episode.mkv",
|
||||
state: "complete",
|
||||
disposition: "written"
|
||||
}],
|
||||
createdAt: 1_000,
|
||||
updatedAt: 2_000
|
||||
}
|
||||
},
|
||||
items: {},
|
||||
runStartedAt: 0,
|
||||
totalDownloadedBytes: 0,
|
||||
summaryText: "",
|
||||
reconnectUntil: 0,
|
||||
reconnectReason: "",
|
||||
paused: false,
|
||||
running: false,
|
||||
updatedAt: 2_000
|
||||
});
|
||||
|
||||
expect(normalized.packages["future-output"]).toEqual(expect.objectContaining({
|
||||
outputCount: 0,
|
||||
outputProvenanceVersion: 99,
|
||||
outputProvenance: [],
|
||||
outputRecords: []
|
||||
}));
|
||||
});
|
||||
|
||||
it("migrates unversioned valid provenance hashes and concrete output records to v1", () => {
|
||||
const normalized = normalizeLoadedSession({
|
||||
version: 2,
|
||||
packageOrder: ["legacy-hashes"],
|
||||
packages: {
|
||||
"legacy-hashes": {
|
||||
id: "legacy-hashes",
|
||||
name: "Legacy hashes",
|
||||
outputDir: "C:\\Downloads\\Legacy",
|
||||
extractDir: "C:\\Downloads\\Shared",
|
||||
status: "completed",
|
||||
itemIds: [],
|
||||
cancelled: false,
|
||||
enabled: true,
|
||||
outputCount: 40_000,
|
||||
outputProvenance: ["a".repeat(64)],
|
||||
outputRecords: [{
|
||||
version: 1,
|
||||
archivePath: "C:\\Downloads\\Legacy\\archive.rar",
|
||||
entryPath: "episode.mkv",
|
||||
outputPath: "C:\\Downloads\\Shared\\episode.mkv",
|
||||
state: "complete",
|
||||
disposition: "written"
|
||||
}],
|
||||
createdAt: 1_000,
|
||||
updatedAt: 2_000
|
||||
}
|
||||
},
|
||||
items: {},
|
||||
runStartedAt: 0,
|
||||
totalDownloadedBytes: 0,
|
||||
summaryText: "",
|
||||
reconnectUntil: 0,
|
||||
reconnectReason: "",
|
||||
paused: false,
|
||||
running: false,
|
||||
updatedAt: 2_000
|
||||
});
|
||||
|
||||
expect(normalized.packages["legacy-hashes"]).toEqual(expect.objectContaining({
|
||||
outputCount: 1,
|
||||
outputProvenanceVersion: 1,
|
||||
outputProvenance: ["a".repeat(64)],
|
||||
outputRecords: [expect.objectContaining({ entryPath: "episode.mkv", state: "complete" })]
|
||||
}));
|
||||
});
|
||||
|
||||
it("skips adding persisted history entries when history retention is never", () => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "rd-store-"));
|
||||
tempDirs.push(dir);
|
||||
|
||||
Reference in New Issue
Block a user