chore: import upstream snapshot with attribution
regression / regression-shards (style-16-prod style-9-prod style-17-prod iframe-render-compat variables-prod mp4-h265-sdr, shard-4) (push) Has been cancelled
regression / regression-shards (style-4-prod style-11-prod style-2-prod animejs-adapter typegpu-adapter parallel-capture-regression, shard-5) (push) Has been cancelled
regression / regression-shards (style-7-prod style-8-prod style-10-prod css-spinner-render-compat webm-transparency mp4-h264-sdr webm-vp9, shard-3) (push) Has been cancelled
regression / regression-shards (sub-composition-video style-18-prod raf-ball-render-compat font-variant-numeric sub-comp-t0 sub-comp-id-selector, shard-7) (push) Has been cancelled
Windows render verification / Detect changes (push) Has been cancelled
Windows render verification / Preflight (lint + format) (push) Has been cancelled
Windows render verification / Render on windows-latest (push) Has been cancelled
Windows render verification / Tests on windows-latest (push) Has been cancelled
CI / Detect changes (push) Has been cancelled
CI / Build (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / Fallow audit (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Test (push) Has been cancelled
CI / Producer: integration tests (push) Has been cancelled
CI / Producer: unit tests (push) Has been cancelled
CI / File size check (push) Has been cancelled
CI / Test: skills (push) Has been cancelled
CI / Skills: manifest in sync (push) Has been cancelled
CI / CLI: npx shim (macos-latest) (push) Has been cancelled
CI / CLI: npx shim (ubuntu-latest) (push) Has been cancelled
CI / CLI: npx shim (windows-latest) (push) Has been cancelled
CI / SDK: unit + contract + smoke (push) Has been cancelled
CI / Test: runtime contract (push) Has been cancelled
CI / Studio: load smoke (push) Has been cancelled
CI / Smoke: global install (push) Has been cancelled
CI / CLI smoke (required) (push) Has been cancelled
CI / Semantic PR title (push) Has been cancelled
Player perf / Detect changes (push) Has been cancelled
Player perf / Preflight (lint + format) (push) Has been cancelled
Player perf / player-perf (push) Has been cancelled
Player perf / Perf: drift (push) Has been cancelled
Player perf / Perf: fps (push) Has been cancelled
Player perf / Perf: parity (push) Has been cancelled
Player perf / Perf: scrub (push) Has been cancelled
Player perf / Perf: load (push) Has been cancelled
preview-regression / Detect changes (push) Has been cancelled
preview-regression / Preflight (lint + format) (push) Has been cancelled
preview-regression / Preview parity (push) Has been cancelled
preview-regression / preview-regression (push) Has been cancelled
regression / regression (push) Has been cancelled
regression / Detect changes (push) Has been cancelled
regression / Preflight (lint + format) (push) Has been cancelled
regression / regression-shards (hdr-regression style-5-prod style-3-prod mov-prores, shard-1) (push) Has been cancelled
regression / regression-shards (overlay-montage-prod style-12-prod chat missing-host-comp-id png-sequence portrait-edge-bleed, shard-6) (push) Has been cancelled
regression / regression-shards (style-13-prod style-6-prod vignelli-stacking gsap-letters-render-compat audio-mux-parity, shard-8) (push) Has been cancelled
regression / regression-shards (style-15-prod hdr-hlg-regression style-1-prod many-cuts vfr-screen-recording render-symlinked-assets, shard-2) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Docs / Validate docs (push) Has been cancelled
Sync skills to ClawHub / Publish changed skills (push) Has been cancelled
regression / regression-shards (style-16-prod style-9-prod style-17-prod iframe-render-compat variables-prod mp4-h265-sdr, shard-4) (push) Has been cancelled
regression / regression-shards (style-4-prod style-11-prod style-2-prod animejs-adapter typegpu-adapter parallel-capture-regression, shard-5) (push) Has been cancelled
regression / regression-shards (style-7-prod style-8-prod style-10-prod css-spinner-render-compat webm-transparency mp4-h264-sdr webm-vp9, shard-3) (push) Has been cancelled
regression / regression-shards (sub-composition-video style-18-prod raf-ball-render-compat font-variant-numeric sub-comp-t0 sub-comp-id-selector, shard-7) (push) Has been cancelled
Windows render verification / Detect changes (push) Has been cancelled
Windows render verification / Preflight (lint + format) (push) Has been cancelled
Windows render verification / Render on windows-latest (push) Has been cancelled
Windows render verification / Tests on windows-latest (push) Has been cancelled
CI / Detect changes (push) Has been cancelled
CI / Build (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / Fallow audit (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Test (push) Has been cancelled
CI / Producer: integration tests (push) Has been cancelled
CI / Producer: unit tests (push) Has been cancelled
CI / File size check (push) Has been cancelled
CI / Test: skills (push) Has been cancelled
CI / Skills: manifest in sync (push) Has been cancelled
CI / CLI: npx shim (macos-latest) (push) Has been cancelled
CI / CLI: npx shim (ubuntu-latest) (push) Has been cancelled
CI / CLI: npx shim (windows-latest) (push) Has been cancelled
CI / SDK: unit + contract + smoke (push) Has been cancelled
CI / Test: runtime contract (push) Has been cancelled
CI / Studio: load smoke (push) Has been cancelled
CI / Smoke: global install (push) Has been cancelled
CI / CLI smoke (required) (push) Has been cancelled
CI / Semantic PR title (push) Has been cancelled
Player perf / Detect changes (push) Has been cancelled
Player perf / Preflight (lint + format) (push) Has been cancelled
Player perf / player-perf (push) Has been cancelled
Player perf / Perf: drift (push) Has been cancelled
Player perf / Perf: fps (push) Has been cancelled
Player perf / Perf: parity (push) Has been cancelled
Player perf / Perf: scrub (push) Has been cancelled
Player perf / Perf: load (push) Has been cancelled
preview-regression / Detect changes (push) Has been cancelled
preview-regression / Preflight (lint + format) (push) Has been cancelled
preview-regression / Preview parity (push) Has been cancelled
preview-regression / preview-regression (push) Has been cancelled
regression / regression (push) Has been cancelled
regression / Detect changes (push) Has been cancelled
regression / Preflight (lint + format) (push) Has been cancelled
regression / regression-shards (hdr-regression style-5-prod style-3-prod mov-prores, shard-1) (push) Has been cancelled
regression / regression-shards (overlay-montage-prod style-12-prod chat missing-host-comp-id png-sequence portrait-edge-bleed, shard-6) (push) Has been cancelled
regression / regression-shards (style-13-prod style-6-prod vignelli-stacking gsap-letters-render-compat audio-mux-parity, shard-8) (push) Has been cancelled
regression / regression-shards (style-15-prod hdr-hlg-regression style-1-prod many-cuts vfr-screen-recording render-symlinked-assets, shard-2) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Docs / Validate docs (push) Has been cancelled
Sync skills to ClawHub / Publish changed skills (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,381 @@
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import {
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
mkdtempSync,
|
||||
readdirSync,
|
||||
rmSync,
|
||||
statSync,
|
||||
utimesSync,
|
||||
writeFileSync,
|
||||
} from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
|
||||
import {
|
||||
COMPLETE_SENTINEL,
|
||||
FRAME_FILENAME_PREFIX,
|
||||
SCHEMA_PREFIX,
|
||||
cacheEntryDirName,
|
||||
computeCacheKey,
|
||||
ensureCacheEntryDir,
|
||||
gcExtractionCache,
|
||||
lookupCacheEntry,
|
||||
markCacheEntryComplete,
|
||||
partialCacheEntryDir,
|
||||
publishCacheEntry,
|
||||
readKeyStat,
|
||||
type CacheKeyInput,
|
||||
} from "./extractionCache.js";
|
||||
|
||||
const keyFor = (videoPath: string, overrides: Partial<CacheKeyInput> = {}): CacheKeyInput => {
|
||||
const stat = readKeyStat(videoPath);
|
||||
if (!stat) throw new Error(`keyFor fixture missing on disk: ${videoPath}`);
|
||||
return {
|
||||
videoPath,
|
||||
mtimeMs: stat.mtimeMs,
|
||||
size: stat.size,
|
||||
mediaStart: 0,
|
||||
duration: 3,
|
||||
fps: 30,
|
||||
format: "jpg",
|
||||
...overrides,
|
||||
};
|
||||
};
|
||||
|
||||
function makeCacheRoot(): { tmpRoot: string; sourceFile: string } {
|
||||
const tmpRoot = mkdtempSync(join(tmpdir(), "hf-extract-cache-test-"));
|
||||
const sourceFile = join(tmpRoot, "clip.mp4");
|
||||
writeFileSync(sourceFile, "fake-video-bytes", "utf-8");
|
||||
return { tmpRoot, sourceFile };
|
||||
}
|
||||
|
||||
function removeCacheRoot(tmpRoot: string): void {
|
||||
if (existsSync(tmpRoot)) rmSync(tmpRoot, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
/** Create and populate a partial dir for `entry` with one frame file. */
|
||||
function seedPartialDir(entry: { dir: string; keyHash: string }, frameContent: string): string {
|
||||
const partialDir = partialCacheEntryDir(entry);
|
||||
mkdirSync(partialDir, { recursive: true });
|
||||
writeFileSync(join(partialDir, "frame_00001.jpg"), frameContent, "utf-8");
|
||||
return partialDir;
|
||||
}
|
||||
|
||||
describe("extractionCache constants", () => {
|
||||
it("exposes the v2 schema prefix", () => {
|
||||
expect(SCHEMA_PREFIX).toBe("hfcache-v3-");
|
||||
});
|
||||
|
||||
it("exposes the frame filename prefix shared with the extractor", () => {
|
||||
expect(FRAME_FILENAME_PREFIX).toBe("frame_");
|
||||
});
|
||||
|
||||
it("uses a dotfile sentinel so ls-without-A hides it", () => {
|
||||
expect(COMPLETE_SENTINEL.startsWith(".")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("computeCacheKey", () => {
|
||||
let tmpRoot: string;
|
||||
let sourceFile: string;
|
||||
|
||||
beforeEach(() => {
|
||||
({ tmpRoot, sourceFile } = makeCacheRoot());
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
removeCacheRoot(tmpRoot);
|
||||
});
|
||||
|
||||
const base = (videoPath: string): CacheKeyInput => keyFor(videoPath);
|
||||
|
||||
it("returns the same key for identical inputs", () => {
|
||||
const a = computeCacheKey(base(sourceFile));
|
||||
const b = computeCacheKey(base(sourceFile));
|
||||
expect(a).toBe(b);
|
||||
});
|
||||
|
||||
it("produces a 64-char hex SHA-256 digest", () => {
|
||||
const key = computeCacheKey(base(sourceFile));
|
||||
expect(key).toMatch(/^[0-9a-f]{64}$/);
|
||||
});
|
||||
|
||||
it("changes when path changes (moved files re-extract)", () => {
|
||||
const other = join(tmpRoot, "other.mp4");
|
||||
writeFileSync(other, "fake-video-bytes", "utf-8");
|
||||
const a = computeCacheKey(base(sourceFile));
|
||||
const b = computeCacheKey(base(other));
|
||||
expect(a).not.toBe(b);
|
||||
});
|
||||
|
||||
it("changes when mediaStart changes", () => {
|
||||
const a = computeCacheKey(base(sourceFile));
|
||||
const b = computeCacheKey({ ...base(sourceFile), mediaStart: 1 });
|
||||
expect(a).not.toBe(b);
|
||||
});
|
||||
|
||||
it("changes when duration changes", () => {
|
||||
const a = computeCacheKey(base(sourceFile));
|
||||
const b = computeCacheKey({ ...base(sourceFile), duration: 5 });
|
||||
expect(a).not.toBe(b);
|
||||
});
|
||||
|
||||
it("changes when fps changes (different frame count invalidates key)", () => {
|
||||
const a = computeCacheKey(base(sourceFile));
|
||||
const b = computeCacheKey({ ...base(sourceFile), fps: 60 });
|
||||
expect(a).not.toBe(b);
|
||||
});
|
||||
|
||||
it("changes when format changes", () => {
|
||||
const a = computeCacheKey(base(sourceFile));
|
||||
const b = computeCacheKey({ ...base(sourceFile), format: "png" });
|
||||
expect(a).not.toBe(b);
|
||||
});
|
||||
|
||||
it("changes when a source transform is applied", () => {
|
||||
const plain = computeCacheKey(base(sourceFile));
|
||||
const transformedInput = { ...base(sourceFile), transform: "sdr2hdr-pq" };
|
||||
const transformed = computeCacheKey(transformedInput);
|
||||
expect(transformed).not.toBe(plain);
|
||||
});
|
||||
|
||||
it("keeps undefined transform byte-compatible with omitted transform", () => {
|
||||
const omitted = computeCacheKey(base(sourceFile));
|
||||
const input = { ...base(sourceFile), transform: undefined };
|
||||
const explicitUndefined = computeCacheKey(input);
|
||||
expect(explicitUndefined).toBe(omitted);
|
||||
});
|
||||
|
||||
it("normalizes non-finite duration so Infinity doesn't produce unstable keys", () => {
|
||||
const a = computeCacheKey({ ...base(sourceFile), duration: Infinity });
|
||||
const b = computeCacheKey({ ...base(sourceFile), duration: Infinity });
|
||||
expect(a).toBe(b);
|
||||
});
|
||||
|
||||
it("changes when file content changes (mtime+size bump)", () => {
|
||||
const before = computeCacheKey(base(sourceFile));
|
||||
// Force an mtime change by waiting 5ms then overwriting with different bytes.
|
||||
// 5ms is well above the Linux mtime resolution (typically nanoseconds) and
|
||||
// below any Windows cache coherency window. Using a longer sleep pads against
|
||||
// coarse filesystem mtime granularity without slowing the suite.
|
||||
const start = Date.now();
|
||||
while (Date.now() - start < 5) {
|
||||
/* spin */
|
||||
}
|
||||
writeFileSync(sourceFile, "different-bytes-longer-than-before", "utf-8");
|
||||
const after = computeCacheKey(base(sourceFile));
|
||||
expect(after).not.toBe(before);
|
||||
});
|
||||
|
||||
it("readKeyStat returns null for a missing source (callers skip the cache)", () => {
|
||||
// Previously readKeyStat returned a `{mtimeMs: 0, size: 0}` sentinel for
|
||||
// missing files; two unrelated missing paths then shared the same cache
|
||||
// key tuple and polluted the cache. The contract now returns null so
|
||||
// callers can explicitly skip the cache path and let the extractor
|
||||
// surface the real file-not-found error.
|
||||
const missing = join(tmpRoot, "does-not-exist.mp4");
|
||||
expect(readKeyStat(missing)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("cacheEntryDirName", () => {
|
||||
it("prefixes with the schema and truncates to 16 hex chars", () => {
|
||||
const full = "a".repeat(64);
|
||||
expect(cacheEntryDirName(full)).toBe(`${SCHEMA_PREFIX}${"a".repeat(16)}`);
|
||||
});
|
||||
});
|
||||
|
||||
describe("lookupCacheEntry / markCacheEntryComplete", () => {
|
||||
let tmpRoot: string;
|
||||
let sourceFile: string;
|
||||
|
||||
beforeEach(() => {
|
||||
({ tmpRoot, sourceFile } = makeCacheRoot());
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
removeCacheRoot(tmpRoot);
|
||||
});
|
||||
|
||||
const base = (videoPath: string): CacheKeyInput => keyFor(videoPath);
|
||||
|
||||
it("misses on an empty cache root", () => {
|
||||
const lookup = lookupCacheEntry(tmpRoot, base(sourceFile));
|
||||
expect(lookup.hit).toBe(false);
|
||||
expect(lookup.entry.dir.startsWith(tmpRoot)).toBe(true);
|
||||
});
|
||||
|
||||
it("hits after ensureCacheEntryDir + markCacheEntryComplete", () => {
|
||||
const first = lookupCacheEntry(tmpRoot, base(sourceFile));
|
||||
ensureCacheEntryDir(first.entry);
|
||||
markCacheEntryComplete(first.entry);
|
||||
|
||||
const second = lookupCacheEntry(tmpRoot, base(sourceFile));
|
||||
expect(second.hit).toBe(true);
|
||||
expect(second.entry.dir).toBe(first.entry.dir);
|
||||
});
|
||||
|
||||
it("treats an in-progress dir without the sentinel as a miss", () => {
|
||||
const lookup = lookupCacheEntry(tmpRoot, base(sourceFile));
|
||||
ensureCacheEntryDir(lookup.entry);
|
||||
// Simulate abandoned extraction — frames written but sentinel never marked.
|
||||
writeFileSync(join(lookup.entry.dir, "frame_00001.jpg"), "x", "utf-8");
|
||||
const again = lookupCacheEntry(tmpRoot, base(sourceFile));
|
||||
expect(again.hit).toBe(false);
|
||||
});
|
||||
|
||||
it("places entries under the cache root, not the source parent", () => {
|
||||
const subroot = join(tmpRoot, "cache-root");
|
||||
mkdirSync(subroot, { recursive: true });
|
||||
const lookup = lookupCacheEntry(subroot, base(sourceFile));
|
||||
expect(lookup.entry.dir.startsWith(subroot)).toBe(true);
|
||||
});
|
||||
|
||||
it("uses the same directory for identical inputs across lookups", () => {
|
||||
const a = lookupCacheEntry(tmpRoot, base(sourceFile));
|
||||
const b = lookupCacheEntry(tmpRoot, base(sourceFile));
|
||||
expect(a.entry.dir).toBe(b.entry.dir);
|
||||
});
|
||||
});
|
||||
|
||||
describe("publishCacheEntry", () => {
|
||||
let tmpRoot: string;
|
||||
let sourceFile: string;
|
||||
|
||||
beforeEach(() => {
|
||||
({ tmpRoot, sourceFile } = makeCacheRoot());
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
removeCacheRoot(tmpRoot);
|
||||
});
|
||||
|
||||
function entry() {
|
||||
return lookupCacheEntry(tmpRoot, keyFor(sourceFile)).entry;
|
||||
}
|
||||
|
||||
it("publishes a partial directory atomically with the complete sentinel inside", () => {
|
||||
const cacheEntry = entry();
|
||||
const partialDir = seedPartialDir(cacheEntry, "frame");
|
||||
|
||||
const result = publishCacheEntry(cacheEntry, partialDir);
|
||||
|
||||
expect(result).toEqual({ dir: cacheEntry.dir, published: true });
|
||||
expect(existsSync(partialDir)).toBe(false);
|
||||
expect(existsSync(join(cacheEntry.dir, "frame_00001.jpg"))).toBe(true);
|
||||
expect(existsSync(join(cacheEntry.dir, COMPLETE_SENTINEL))).toBe(true);
|
||||
});
|
||||
|
||||
it("serves a complete winner when another writer publishes the same entry first", () => {
|
||||
const cacheEntry = entry();
|
||||
mkdirSync(cacheEntry.dir, { recursive: true });
|
||||
writeFileSync(join(cacheEntry.dir, "frame_00001.jpg"), "winner", "utf-8");
|
||||
markCacheEntryComplete(cacheEntry);
|
||||
|
||||
const partialDir = seedPartialDir(cacheEntry, "loser");
|
||||
|
||||
const result = publishCacheEntry(cacheEntry, partialDir);
|
||||
|
||||
expect(result).toEqual({ dir: cacheEntry.dir, published: true });
|
||||
expect(existsSync(partialDir)).toBe(false);
|
||||
expect(existsSync(join(cacheEntry.dir, COMPLETE_SENTINEL))).toBe(true);
|
||||
expect(statSync(join(cacheEntry.dir, "frame_00001.jpg")).size).toBe("winner".length);
|
||||
});
|
||||
|
||||
it("replaces a stale unsentineled final directory and retries publish once", () => {
|
||||
const cacheEntry = entry();
|
||||
mkdirSync(cacheEntry.dir, { recursive: true });
|
||||
writeFileSync(join(cacheEntry.dir, "frame_00001.jpg"), "stale", "utf-8");
|
||||
|
||||
const partialDir = seedPartialDir(cacheEntry, "fresh");
|
||||
|
||||
const result = publishCacheEntry(cacheEntry, partialDir);
|
||||
|
||||
expect(result).toEqual({ dir: cacheEntry.dir, published: true });
|
||||
expect(existsSync(partialDir)).toBe(false);
|
||||
expect(statSync(join(cacheEntry.dir, "frame_00001.jpg")).size).toBe("fresh".length);
|
||||
expect(existsSync(join(cacheEntry.dir, COMPLETE_SENTINEL))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("gcExtractionCache", () => {
|
||||
let tmpRoot: string;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpRoot = mkdtempSync(join(tmpdir(), "hf-extract-cache-gc-test-"));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (existsSync(tmpRoot)) rmSync(tmpRoot, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
function makeEntry(name: string, bytes: number, ageMs: number): string {
|
||||
const dir = join(tmpRoot, `${SCHEMA_PREFIX}${name}`);
|
||||
mkdirSync(dir, { recursive: true });
|
||||
writeFileSync(join(dir, "frame_00001.jpg"), "x".repeat(bytes), "utf-8");
|
||||
markCacheEntryComplete({ dir, keyHash: name.padEnd(64, "0") });
|
||||
const when = new Date(Date.now() - ageMs);
|
||||
utimesSync(join(dir, COMPLETE_SENTINEL), when, when);
|
||||
return dir;
|
||||
}
|
||||
|
||||
it("evicts superseded-generation entries (hfcache-v2-*) under the size cap", () => {
|
||||
const oldGen = join(tmpRoot, "hfcache-v2-0123456789abcdef");
|
||||
mkdirSync(oldGen, { recursive: true });
|
||||
writeFileSync(join(oldGen, "frame_00001.jpg"), "x".repeat(2048), "utf-8");
|
||||
writeFileSync(join(oldGen, ".hf-complete"), "", "utf-8");
|
||||
const aged = new Date(Date.now() - 2 * 60 * 60 * 1000);
|
||||
utimesSync(join(oldGen, ".hf-complete"), aged, aged);
|
||||
utimesSync(oldGen, aged, aged);
|
||||
|
||||
const stats = gcExtractionCache(tmpRoot, { maxBytes: 1024, minAgeMs: 60 * 60 * 1000 });
|
||||
|
||||
expect(existsSync(oldGen)).toBe(false);
|
||||
expect(stats.evictedEntries).toBe(1);
|
||||
});
|
||||
|
||||
it("evicts oldest complete entries first until under maxBytes while respecting minAge", () => {
|
||||
const oldest = makeEntry("oldest", 60, 120_000);
|
||||
const middle = makeEntry("middle", 60, 90_000);
|
||||
const young = makeEntry("young", 60, 1_000);
|
||||
|
||||
gcExtractionCache(tmpRoot, { maxBytes: 100, minAgeMs: 60_000 });
|
||||
|
||||
expect(existsSync(oldest)).toBe(false);
|
||||
expect(existsSync(middle)).toBe(false);
|
||||
expect(existsSync(young)).toBe(true);
|
||||
});
|
||||
|
||||
it("removes aged partial directories", () => {
|
||||
const agedPartial = join(tmpRoot, `${SCHEMA_PREFIX}abc.partial-1234-deadbeef`);
|
||||
const freshPartial = join(tmpRoot, `${SCHEMA_PREFIX}def.partial-1234-feedface`);
|
||||
mkdirSync(agedPartial, { recursive: true });
|
||||
mkdirSync(freshPartial, { recursive: true });
|
||||
const old = new Date(Date.now() - 120_000);
|
||||
utimesSync(agedPartial, old, old);
|
||||
|
||||
gcExtractionCache(tmpRoot, { maxBytes: 1_000_000, minAgeMs: 60_000 });
|
||||
|
||||
expect(existsSync(agedPartial)).toBe(false);
|
||||
expect(existsSync(freshPartial)).toBe(true);
|
||||
});
|
||||
|
||||
it("ignores non-cache-prefix directories under the same root", () => {
|
||||
const animatedGif = join(tmpRoot, "animated-gif");
|
||||
mkdirSync(animatedGif, { recursive: true });
|
||||
writeFileSync(join(animatedGif, "frame.png"), "keep", "utf-8");
|
||||
makeEntry("old", 200, 120_000);
|
||||
|
||||
gcExtractionCache(tmpRoot, { maxBytes: 1, minAgeMs: 60_000 });
|
||||
|
||||
expect(existsSync(animatedGif)).toBe(true);
|
||||
expect(readdirSync(animatedGif)).toEqual(["frame.png"]);
|
||||
});
|
||||
|
||||
it("never throws when the cache root is missing", () => {
|
||||
expect(() =>
|
||||
gcExtractionCache(join(tmpRoot, "missing"), { maxBytes: 1, minAgeMs: 60_000 }),
|
||||
).not.toThrow();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user