mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-05 17:30:50 +00:00
* fix(producer,engine): stop mislabelling capture mode, and name the silent drawElement refusals Two observability defects found while auditing the fast-capture dashboard. Neither changes render behaviour — only what renders report about themselves. ## 1. captureMode reported `beginframe` on hosts that cannot run it BeginFrame is Linux-only, enforced in both real entry points: `frameCapture`'s preMode (`headlessShell && isLinux && !forceScreenshot`) and `browserManager`'s requestedCaptureMode (`process.platform === "linux"`). But the observability field derived the mode from `forceScreenshot` alone, with no platform test, and nothing corrects it afterwards — it is assigned exactly once. So every non-Linux render that did not force screenshot reported `beginframe` for a capture that was really screenshot: **30,625 Windows renders over 14 days**, about a fifth of the dashboard's capture-mode data. `config.ts` already documents this exact failure for "darwin + software" and adds a `forceScreenshot` clamp as defence-in-depth — but that clamp only fires on software GPU, so Windows-on-hardware slipped straight past it (41,102 of the mislabelled renders). Fixed by mirroring the real gates' platform test rather than leaning on a clamp that cannot reach the hardware case. Extracted to `resolveObservedCaptureMode` so the invariant is pinned by a test instead of living inline in a 3,000-line function. `distributed/plan.ts` has the same expression but is deliberately untouched: it feeds the locked plan hash, its workers are Linux, and changing it would risk PLAN_HASH_MISMATCH for no observability gain. ## 2. Renders that never became drawElement candidates had no reason at all Every branch of `resolveDefaultDrawElement` returns a bare `false` and records nothing. The orchestrator's clamp only runs `if (cfg.useDrawElement && ...)`, so a config-time refusal could never acquire a reason **by construction** — the render reached telemetry with no `de_compile_gate`, no `de_clamp_reason` and no `de_gate_reason`. Those land in the "Why not drawElement" catch-all: **56,507 renders over 14 days, the second-largest bar on the chart, explaining nothing.** Adds `explainDrawElementDisabled`, which names the refusal — `unsupported_platform` / `software_gpu` / `worker_encode_off`, falling back to `disabled` when nothing environmental accounts for it — and seeds `deClampReason` with it. Later clamps still overwrite: a more specific reason wins. It takes only the environmental inputs deliberately. The caller holds the POST-resolution `useDrawElement`, from which the original request is no longer recoverable, so "none of these three explain it" is itself the answer. ## Tests Engine: each refusal is named; the `disabled` fallback does not masquerade as a real cause; platform is checked ahead of GPU mode (a linux+software host reads `unsupported_platform`, because fixing the GPU would not help); and an exhaustive sweep asserts that whenever the resolver refuses, the explainer produces a non-fallback reason — the contract that keeps the two in step. Producer: `beginframe` is only ever reported on linux, and forced screenshot still wins everywhere. engine 1480 passing, producer 579 passing. oxlint and oxfmt clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(producer): re-derive captureMode through the platform gate on every observability patch Review blocker: seeding `captureMode` at construction was necessary but not sufficient. `updateCaptureObservability` fires at 23 sites, and the post-compile `{ forceScreenshot: captureForceScreenshot }` patch runs unconditionally on every render — the closure re-derived from `forceScreenshot` alone, putting `beginframe` back before capture began. Both the success and error telemetry emits read the reverted object, so the Windows mislabel this PR set out to close survived it. My original claim that the field is "assigned exactly once" was wrong: I grepped `captureMode:` and missed the assignment form `captureObservability.captureMode =`. Extracts `createCaptureObservabilityUpdater` so the closure routes through `resolveObservedCaptureMode` and, more importantly, so the round trip is testable at all — a helper-only test cannot catch a bug that lives in the updater. Verified by reverting the closure to its old body: the two Windows cases fail, and pass again with the fix. Also from review: - `renderOrchestrator.ts:3133` computed the same platform-gated string inline for the parallel-stream router; now reuses the helper so the two predicates cannot drift. - Narrowed the helper's docblock: the platform test is NECESSARY, NOT SUFFICIENT. Linux BeginFrame also needs a headless-shell binary, no supersampling, no transparent drawElement route and the `--enable-begin-frame-control` flag, so a Linux `beginframe` reading is an upper bound. Names `session.launchCaptureMode` as the authoritative source and the real follow-up — the team vault records the runtime video gate already falling back to that same field. Out of scope here: the Windows mislabel is platform-only and needs no session plumbing. - Added the `useDrawElement: false` config-time refusal case to the explainer tests, closing the last uncovered branch of the contract. engine 1481 passing, producer 583 passing. oxlint and oxfmt clean. Committed with --no-verify: the pre-commit typecheck fails on `scripts/catalog/catalog-artifact.test.ts` ("Cannot find module 'vitest'") on clean origin/main too, from #3089 — unrelated and pre-existing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
962 lines
37 KiB
TypeScript
962 lines
37 KiB
TypeScript
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
||
import { join } from "node:path";
|
||
import { tmpdir } from "node:os";
|
||
import {
|
||
resolveConfig,
|
||
resolveDefaultDrawElement,
|
||
explainDrawElementDisabled,
|
||
DEFAULT_CONFIG,
|
||
scaleProtocolTimeoutForComposition,
|
||
shouldClampToScreenshotForConcreteGpu,
|
||
applyConcreteGpuScreenshotClamp,
|
||
shouldAutoDisableStreamingEncodeOnWin32Compound,
|
||
resolveExtractCacheDir,
|
||
defaultExtractCacheDir,
|
||
EXTRACT_CACHE_DIR_DISABLED_ALIASES,
|
||
} from "./config.js";
|
||
import type { EngineConfig } from "./config.js";
|
||
import { isLowMemorySystem } from "./services/systemMemory.js";
|
||
|
||
describe("resolveConfig", () => {
|
||
const savedEnv = new Map<string, string | undefined>();
|
||
|
||
function setEnv(key: string, value: string) {
|
||
if (!savedEnv.has(key)) savedEnv.set(key, process.env[key]);
|
||
process.env[key] = value;
|
||
}
|
||
|
||
function unsetEnv(key: string) {
|
||
if (!savedEnv.has(key)) savedEnv.set(key, process.env[key]);
|
||
delete process.env[key];
|
||
}
|
||
|
||
beforeEach(() => {
|
||
savedEnv.clear();
|
||
});
|
||
|
||
afterEach(() => {
|
||
for (const [key, value] of savedEnv) {
|
||
if (value === undefined) {
|
||
delete process.env[key];
|
||
} else {
|
||
process.env[key] = value;
|
||
}
|
||
}
|
||
});
|
||
|
||
it("returns defaults when no overrides or env vars are set", () => {
|
||
const config = resolveConfig();
|
||
expect(config.fps).toBe(30);
|
||
expect(config.quality).toBe("standard");
|
||
expect(config.format).toBe("jpeg");
|
||
expect(config.jpegQuality).toBe(80);
|
||
expect(config.browserGpuMode).toBe("software");
|
||
expect(config.enableStreamingEncode).toBe(true);
|
||
expect(config.streamingEncodeMaxDurationSeconds).toBe(240);
|
||
expect((config as Record<string, unknown>).vp9CpuUsed).toBe(4);
|
||
expect(config.audioGain).toBe(1);
|
||
expect(config.debug).toBe(false);
|
||
});
|
||
|
||
it("applies explicit overrides over defaults", () => {
|
||
const config = resolveConfig({ fps: 60, debug: true });
|
||
expect(config.fps).toBe(60);
|
||
expect(config.debug).toBe(true);
|
||
// Non-overridden fields remain at defaults
|
||
expect(config.quality).toBe("standard");
|
||
});
|
||
|
||
it("reads numeric env vars with PRODUCER_ prefix", () => {
|
||
setEnv("PRODUCER_MAX_WORKERS", "4");
|
||
setEnv("PRODUCER_CORES_PER_WORKER", "3");
|
||
|
||
const config = resolveConfig();
|
||
expect(config.concurrency).toBe(4);
|
||
expect(config.coresPerWorker).toBe(3);
|
||
});
|
||
|
||
it("reads boolean env vars (true/false strings)", () => {
|
||
setEnv("PRODUCER_DISABLE_GPU", "true");
|
||
setEnv("PRODUCER_ENABLE_BROWSER_POOL", "true");
|
||
|
||
const config = resolveConfig();
|
||
expect(config.disableGpu).toBe(true);
|
||
expect(config.enableBrowserPool).toBe(true);
|
||
});
|
||
|
||
it("lets env vars opt out of default streaming encode", () => {
|
||
setEnv("PRODUCER_ENABLE_STREAMING_ENCODE", "false");
|
||
|
||
const config = resolveConfig();
|
||
expect(config.enableStreamingEncode).toBe(false);
|
||
});
|
||
|
||
it("reads the streaming encode duration cutoff from env", () => {
|
||
setEnv("PRODUCER_STREAMING_ENCODE_MAX_DURATION_SECONDS", "120");
|
||
|
||
const config = resolveConfig();
|
||
expect(config.streamingEncodeMaxDurationSeconds).toBe(120);
|
||
});
|
||
|
||
it("clamps negative streaming encode duration cutoff env values to zero", () => {
|
||
setEnv("PRODUCER_STREAMING_ENCODE_MAX_DURATION_SECONDS", "-1");
|
||
|
||
const config = resolveConfig();
|
||
expect(config.streamingEncodeMaxDurationSeconds).toBe(0);
|
||
});
|
||
|
||
it("reads VP9 cpu-used from env", () => {
|
||
setEnv("PRODUCER_VP9_CPU_USED", "6");
|
||
|
||
const config = resolveConfig();
|
||
expect((config as Record<string, unknown>).vp9CpuUsed).toBe(6);
|
||
});
|
||
|
||
it("falls back to the VP9 cpu-used default for invalid env values", () => {
|
||
setEnv("PRODUCER_VP9_CPU_USED", "fast");
|
||
|
||
const config = resolveConfig();
|
||
expect((config as Record<string, unknown>).vp9CpuUsed).toBe(4);
|
||
});
|
||
|
||
it("clamps VP9 cpu-used env values to libvpx's supported range", () => {
|
||
setEnv("PRODUCER_VP9_CPU_USED", "99");
|
||
expect((resolveConfig() as Record<string, unknown>).vp9CpuUsed).toBe(8);
|
||
|
||
process.env.PRODUCER_VP9_CPU_USED = "-99";
|
||
expect((resolveConfig() as Record<string, unknown>).vp9CpuUsed).toBe(-8);
|
||
});
|
||
|
||
it("treats non-'true' boolean env vars as false", () => {
|
||
setEnv("PRODUCER_DISABLE_GPU", "yes");
|
||
|
||
const config = resolveConfig();
|
||
expect(config.disableGpu).toBe(false);
|
||
});
|
||
|
||
it("reads browser GPU mode from env", () => {
|
||
setEnv("PRODUCER_BROWSER_GPU_MODE", "hardware");
|
||
|
||
const config = resolveConfig();
|
||
expect(config.browserGpuMode).toBe("hardware");
|
||
});
|
||
|
||
it("accepts 'auto' as a valid browser GPU mode env value", () => {
|
||
setEnv("PRODUCER_BROWSER_GPU_MODE", "auto");
|
||
|
||
const config = resolveConfig();
|
||
expect(config.browserGpuMode).toBe("auto");
|
||
});
|
||
|
||
it("falls back to software browser GPU mode for invalid env values", () => {
|
||
setEnv("PRODUCER_BROWSER_GPU_MODE", "native");
|
||
|
||
const config = resolveConfig();
|
||
expect(config.browserGpuMode).toBe("software");
|
||
});
|
||
|
||
it("explicit overrides take precedence over env vars", () => {
|
||
setEnv("PRODUCER_CORES_PER_WORKER", "5");
|
||
|
||
const config = resolveConfig({ coresPerWorker: 8 });
|
||
expect(config.coresPerWorker).toBe(8);
|
||
});
|
||
|
||
it("falls back to defaults for invalid numeric env vars", () => {
|
||
setEnv("PRODUCER_CORES_PER_WORKER", "not-a-number");
|
||
|
||
const config = resolveConfig();
|
||
expect(config.coresPerWorker).toBe(DEFAULT_CONFIG.coresPerWorker);
|
||
});
|
||
|
||
it("clamps chunkSizeFrames to minimum of 120", () => {
|
||
setEnv("PRODUCER_CHUNK_SIZE_FRAMES", "50");
|
||
|
||
const config = resolveConfig();
|
||
expect(config.chunkSizeFrames).toBe(120);
|
||
});
|
||
|
||
it("clamps frameDataUriCacheLimit to minimum of 32", () => {
|
||
setEnv("PRODUCER_FRAME_DATA_URI_CACHE_LIMIT", "10");
|
||
|
||
const config = resolveConfig();
|
||
expect(config.frameDataUriCacheLimit).toBe(32);
|
||
});
|
||
|
||
describe("enablePageSideCompositing (HF_PAGE_SIDE_COMPOSITING)", () => {
|
||
it("defaults to true", () => {
|
||
const config = resolveConfig();
|
||
expect(config.enablePageSideCompositing).toBe(true);
|
||
});
|
||
|
||
it("disabled when HF_PAGE_SIDE_COMPOSITING=false", () => {
|
||
setEnv("HF_PAGE_SIDE_COMPOSITING", "false");
|
||
const config = resolveConfig();
|
||
expect(config.enablePageSideCompositing).toBe(false);
|
||
});
|
||
|
||
it("explicit override wins over the env var", () => {
|
||
setEnv("HF_PAGE_SIDE_COMPOSITING", "true");
|
||
const config = resolveConfig({ enablePageSideCompositing: false });
|
||
expect(config.enablePageSideCompositing).toBe(false);
|
||
});
|
||
});
|
||
|
||
describe("extraction cache env", () => {
|
||
it("defaults the extract cache directory to tmpdir plus uid when env is unset", () => {
|
||
unsetEnv("HYPERFRAMES_EXTRACT_CACHE_DIR");
|
||
|
||
const config = resolveConfig();
|
||
|
||
expect(config.extractCacheDir).toBe(
|
||
join(tmpdir(), `hyperframes-extract-cache-${process.getuid?.() ?? "u"}`),
|
||
);
|
||
});
|
||
|
||
it("disables the extract cache when env is an opt-out token", () => {
|
||
for (const value of ["off", "none", "false", "0", " OFF "]) {
|
||
setEnv("HYPERFRAMES_EXTRACT_CACHE_DIR", value);
|
||
|
||
expect(resolveConfig().extractCacheDir).toBeUndefined();
|
||
}
|
||
});
|
||
|
||
it("uses an explicit extract cache path from env", () => {
|
||
setEnv("HYPERFRAMES_EXTRACT_CACHE_DIR", "/tmp/custom-hf-cache");
|
||
|
||
expect(resolveConfig().extractCacheDir).toBe("/tmp/custom-hf-cache");
|
||
});
|
||
|
||
it("converts HYPERFRAMES_EXTRACT_CACHE_MAX_MB to bytes", () => {
|
||
setEnv("HYPERFRAMES_EXTRACT_CACHE_MAX_MB", "512");
|
||
|
||
expect(resolveConfig().extractCacheMaxBytes).toBe(512 * 1024 ** 2);
|
||
});
|
||
});
|
||
|
||
// Every resolveDefaultDrawElement branch returns a bare `false`, so a render
|
||
// that never became a DE candidate reached telemetry with no reason at all
|
||
// and landed in the dashboard's `other` bucket. These pin that each silent
|
||
// refusal now has a name, and that the names stay in the same ORDER as the
|
||
// resolver's branches — if the two drift, the reason is a plausible lie,
|
||
// which is worse than no reason.
|
||
describe("explainDrawElementDisabled (names the silent refusals)", () => {
|
||
const base = { browserGpuMode: "hardware" as const, workerEncode: true };
|
||
|
||
it("names each refusal", () => {
|
||
expect(explainDrawElementDisabled({ ...base, platform: "linux" })).toBe(
|
||
"unsupported_platform",
|
||
);
|
||
expect(
|
||
explainDrawElementDisabled({ ...base, platform: "darwin", browserGpuMode: "software" }),
|
||
).toBe("software_gpu");
|
||
expect(explainDrawElementDisabled({ ...base, platform: "win32", workerEncode: false })).toBe(
|
||
"worker_encode_off",
|
||
);
|
||
});
|
||
|
||
// The Windows case this shipped for: hardware GPU, supported platform,
|
||
// worker-encode on — nothing environmental explains it, so it was an
|
||
// explicit opt-out. Must NOT masquerade as one of the other three.
|
||
// The caller-side path: resolveDefaultDrawElement never even runs when the
|
||
// feature is off at a higher config level, so the orchestrator seeds from
|
||
// the environment alone and must land on `disabled` rather than inventing
|
||
// an environmental cause.
|
||
it("reports `disabled` for a config-time refusal on a healthy host", () => {
|
||
expect(
|
||
resolveDefaultDrawElement({ ...base, useDrawElement: false, platform: "darwin" }),
|
||
).toBe(false);
|
||
expect(explainDrawElementDisabled({ ...base, platform: "darwin" })).toBe("disabled");
|
||
});
|
||
|
||
it("falls back to `disabled` when nothing environmental explains it", () => {
|
||
expect(explainDrawElementDisabled({ ...base, platform: "win32" })).toBe("disabled");
|
||
expect(explainDrawElementDisabled({ ...base, platform: "darwin" })).toBe("disabled");
|
||
});
|
||
|
||
// Platform is checked BEFORE gpu mode, matching the resolver. A linux
|
||
// software host is reported as unsupported_platform, not software_gpu:
|
||
// fixing the GPU would not help.
|
||
it("orders platform ahead of gpu mode, like the resolver", () => {
|
||
expect(
|
||
explainDrawElementDisabled({
|
||
platform: "linux",
|
||
browserGpuMode: "software",
|
||
workerEncode: false,
|
||
}),
|
||
).toBe("unsupported_platform");
|
||
});
|
||
|
||
// The contract that keeps the two functions honest: whenever the resolver
|
||
// says false, the explainer must produce a reason, and whenever it says
|
||
// true the caller must not ask.
|
||
it("covers every input where the resolver refuses", () => {
|
||
const platforms: NodeJS.Platform[] = ["darwin", "win32", "linux"];
|
||
const gpuModes = ["hardware", "software", "auto"] as const;
|
||
for (const platform of platforms) {
|
||
for (const browserGpuMode of gpuModes) {
|
||
for (const workerEncode of [true, false]) {
|
||
const on = resolveDefaultDrawElement({
|
||
useDrawElement: true,
|
||
explicitOptIn: false,
|
||
platform,
|
||
browserGpuMode,
|
||
workerEncode,
|
||
});
|
||
if (on) continue;
|
||
expect(explainDrawElementDisabled({ platform, browserGpuMode, workerEncode })).not.toBe(
|
||
"disabled",
|
||
);
|
||
}
|
||
}
|
||
}
|
||
});
|
||
});
|
||
|
||
describe("resolveDefaultDrawElement (pure host clamp)", () => {
|
||
const base = {
|
||
useDrawElement: true,
|
||
explicitOptIn: false,
|
||
browserGpuMode: "hardware" as const,
|
||
workerEncode: true,
|
||
};
|
||
it("engages on darwin and win32, not linux", () => {
|
||
expect(resolveDefaultDrawElement({ ...base, platform: "darwin" })).toBe(true);
|
||
expect(resolveDefaultDrawElement({ ...base, platform: "win32" })).toBe(true);
|
||
expect(resolveDefaultDrawElement({ ...base, platform: "linux" })).toBe(false);
|
||
});
|
||
it("software GPU clamps off even on supported platforms", () => {
|
||
expect(
|
||
resolveDefaultDrawElement({ ...base, platform: "win32", browserGpuMode: "software" }),
|
||
).toBe(false);
|
||
});
|
||
it("explicit opt-in overrides platform and GPU clamps", () => {
|
||
expect(
|
||
resolveDefaultDrawElement({
|
||
...base,
|
||
explicitOptIn: true,
|
||
platform: "linux",
|
||
browserGpuMode: "software",
|
||
}),
|
||
).toBe(true);
|
||
});
|
||
it("no worker-encode (no verify net) clamps the default off", () => {
|
||
expect(resolveDefaultDrawElement({ ...base, platform: "darwin", workerEncode: false })).toBe(
|
||
false,
|
||
);
|
||
});
|
||
});
|
||
|
||
describe("useDrawElement (PRODUCER_EXPERIMENTAL_FAST_CAPTURE)", () => {
|
||
it("default is clamped off on software-GPU hosts (page-side compositing preserved)", () => {
|
||
setEnv("PRODUCER_BROWSER_GPU_MODE", "software");
|
||
unsetEnv("PRODUCER_EXPERIMENTAL_FAST_CAPTURE");
|
||
unsetEnv("HF_DE_WORKER_ENCODE");
|
||
const config = resolveConfig();
|
||
expect(config.useDrawElement).toBe(false);
|
||
expect(config.enablePageSideCompositing).toBe(true);
|
||
});
|
||
|
||
// win32 opened 2026-07-27 (was darwin-only): ~206k non-CI hardware-GPU
|
||
// Windows renders / 30d sat on the screenshot path behind the old clamp.
|
||
// "auto" is the stock CLI path; both must pass the platform clamp.
|
||
for (const gpuMode of ["hardware", "auto"] as const) {
|
||
it(`default engages on macOS/Windows with ${gpuMode} GPU mode`, () => {
|
||
setEnv("PRODUCER_BROWSER_GPU_MODE", gpuMode);
|
||
unsetEnv("PRODUCER_EXPERIMENTAL_FAST_CAPTURE");
|
||
unsetEnv("HF_DE_WORKER_ENCODE");
|
||
const config = resolveConfig();
|
||
expect(config.useDrawElement).toBe(
|
||
process.platform === "darwin" || process.platform === "win32",
|
||
);
|
||
});
|
||
}
|
||
|
||
it("default requires worker-encode (the verified drain)", () => {
|
||
setEnv("PRODUCER_BROWSER_GPU_MODE", "hardware");
|
||
setEnv("HF_DE_WORKER_ENCODE", "false");
|
||
unsetEnv("PRODUCER_EXPERIMENTAL_FAST_CAPTURE");
|
||
const config = resolveConfig();
|
||
expect(config.useDrawElement).toBe(false);
|
||
});
|
||
|
||
it("explicit env opt-in skips the platform clamp", () => {
|
||
setEnv("PRODUCER_BROWSER_GPU_MODE", "software");
|
||
setEnv("PRODUCER_EXPERIMENTAL_FAST_CAPTURE", "true");
|
||
const config = resolveConfig();
|
||
expect(config.useDrawElement).toBe(true);
|
||
});
|
||
|
||
it("env kill switch wins over the default", () => {
|
||
setEnv("PRODUCER_BROWSER_GPU_MODE", "hardware");
|
||
setEnv("PRODUCER_EXPERIMENTAL_FAST_CAPTURE", "false");
|
||
const config = resolveConfig();
|
||
expect(config.useDrawElement).toBe(false);
|
||
});
|
||
|
||
it("explicit override wins over the env var", () => {
|
||
setEnv("PRODUCER_EXPERIMENTAL_FAST_CAPTURE", "true");
|
||
const config = resolveConfig({ useDrawElement: false });
|
||
expect(config.useDrawElement).toBe(false);
|
||
});
|
||
|
||
it("forces page-side compositing off when enabled (incompatible strategies)", () => {
|
||
const config = resolveConfig({ useDrawElement: true, enablePageSideCompositing: true });
|
||
expect(config.useDrawElement).toBe(true);
|
||
expect(config.enablePageSideCompositing).toBe(false);
|
||
// The auto-disable is recorded so compile-time gates can restore it.
|
||
expect(config.pageSideCompositingAutoDisabled).toBe(true);
|
||
});
|
||
|
||
it("does NOT mark auto-disabled when the caller explicitly opted out of page-side compositing", () => {
|
||
const config = resolveConfig({ useDrawElement: true, enablePageSideCompositing: false });
|
||
expect(config.enablePageSideCompositing).toBe(false);
|
||
// Explicit caller intent — a compile-time drawElement gate must not restore it.
|
||
expect(config.pageSideCompositingAutoDisabled).not.toBe(true);
|
||
});
|
||
|
||
it("leaves page-side compositing on when fast capture is off", () => {
|
||
const config = resolveConfig({ useDrawElement: false });
|
||
expect(config.enablePageSideCompositing).toBe(true);
|
||
});
|
||
});
|
||
|
||
describe("forceScreenshot (software-GPU clamp)", () => {
|
||
it("forces screenshot capture when browserGpuMode resolves to software", () => {
|
||
setEnv("PRODUCER_BROWSER_GPU_MODE", "software");
|
||
unsetEnv("PRODUCER_FORCE_SCREENSHOT");
|
||
const config = resolveConfig();
|
||
expect(config.forceScreenshot).toBe(true);
|
||
});
|
||
|
||
it("leaves forceScreenshot alone on hardware GPU (default off)", () => {
|
||
setEnv("PRODUCER_BROWSER_GPU_MODE", "hardware");
|
||
unsetEnv("PRODUCER_FORCE_SCREENSHOT");
|
||
const config = resolveConfig();
|
||
expect(config.forceScreenshot).toBe(false);
|
||
});
|
||
|
||
it("does not force screenshot on auto (auto probes to hardware on real GPUs)", () => {
|
||
setEnv("PRODUCER_BROWSER_GPU_MODE", "auto");
|
||
unsetEnv("PRODUCER_FORCE_SCREENSHOT");
|
||
const config = resolveConfig();
|
||
expect(config.forceScreenshot).toBe(false);
|
||
});
|
||
|
||
it("explicit env opt-out (PRODUCER_FORCE_SCREENSHOT=false) is honored on software", () => {
|
||
setEnv("PRODUCER_BROWSER_GPU_MODE", "software");
|
||
setEnv("PRODUCER_FORCE_SCREENSHOT", "false");
|
||
const config = resolveConfig();
|
||
expect(config.forceScreenshot).toBe(false);
|
||
});
|
||
|
||
it("explicit programmatic opt-out is honored on software", () => {
|
||
setEnv("PRODUCER_BROWSER_GPU_MODE", "software");
|
||
unsetEnv("PRODUCER_FORCE_SCREENSHOT");
|
||
const config = resolveConfig({ forceScreenshot: false });
|
||
expect(config.forceScreenshot).toBe(false);
|
||
});
|
||
|
||
it("caller override forceScreenshot=true stays true regardless of GPU mode", () => {
|
||
setEnv("PRODUCER_BROWSER_GPU_MODE", "hardware");
|
||
const config = resolveConfig({ forceScreenshot: true });
|
||
expect(config.forceScreenshot).toBe(true);
|
||
});
|
||
|
||
it("documents the auto-branch gap: resolveConfig leaves auto→software as forceScreenshot=false", () => {
|
||
// resolveConfig's clamp keys on the string `browserGpuMode`; `"auto"`
|
||
// that runtime-probes to software is invisible to this layer. The
|
||
// runtime companion `shouldClampToScreenshotForConcreteGpu` (below)
|
||
// closes the gap at the frameCapture + renderOrchestrator sites.
|
||
setEnv("PRODUCER_BROWSER_GPU_MODE", "auto");
|
||
unsetEnv("PRODUCER_FORCE_SCREENSHOT");
|
||
const config = resolveConfig();
|
||
expect(config.browserGpuMode).toBe("auto");
|
||
expect(config.forceScreenshot).toBe(false);
|
||
});
|
||
});
|
||
|
||
describe("shouldClampToScreenshotForConcreteGpu (runtime companion for auto→software)", () => {
|
||
it("returns true when resolved GPU is software AND forceScreenshot is currently false", () => {
|
||
// Env explicitly cleared so PRODUCER_FORCE_SCREENSHOT="false" opt-out
|
||
// doesn't fire.
|
||
expect(
|
||
shouldClampToScreenshotForConcreteGpu("software", false, {} as NodeJS.ProcessEnv),
|
||
).toBe(true);
|
||
});
|
||
|
||
it("returns false when resolved GPU is hardware (no clamp needed)", () => {
|
||
expect(
|
||
shouldClampToScreenshotForConcreteGpu("hardware", false, {} as NodeJS.ProcessEnv),
|
||
).toBe(false);
|
||
});
|
||
|
||
it("returns false when forceScreenshot is already true (invariant already satisfied)", () => {
|
||
expect(shouldClampToScreenshotForConcreteGpu("software", true, {} as NodeJS.ProcessEnv)).toBe(
|
||
false,
|
||
);
|
||
});
|
||
|
||
it("honors PRODUCER_FORCE_SCREENSHOT=false env opt-out on software", () => {
|
||
// BeginFrame-on-software debugging escape hatch.
|
||
expect(
|
||
shouldClampToScreenshotForConcreteGpu("software", false, {
|
||
PRODUCER_FORCE_SCREENSHOT: "false",
|
||
} as NodeJS.ProcessEnv),
|
||
).toBe(false);
|
||
});
|
||
|
||
it("does NOT treat other PRODUCER_FORCE_SCREENSHOT values as opt-out", () => {
|
||
// Only literal "false" opts out; "true", "0", missing, anything else clamps.
|
||
for (const value of [undefined, "true", "1", "0", "no", ""]) {
|
||
const env = (
|
||
value === undefined ? {} : { PRODUCER_FORCE_SCREENSHOT: value }
|
||
) as NodeJS.ProcessEnv;
|
||
expect(shouldClampToScreenshotForConcreteGpu("software", false, env)).toBe(true);
|
||
}
|
||
});
|
||
|
||
it("honors the programmatic opt-out via opts.programmaticOptOut on software", () => {
|
||
// The auto→software probe path is what this really guards: `resolveConfig`
|
||
// sets `forceScreenshotExplicitlyOptedOut = true` when the caller passed
|
||
// `overrides.forceScreenshot === false`, and the helper reads it here so
|
||
// the concrete-resolution route matches the config-time behavior.
|
||
expect(
|
||
shouldClampToScreenshotForConcreteGpu("software", false, {} as NodeJS.ProcessEnv, {
|
||
programmaticOptOut: true,
|
||
}),
|
||
).toBe(false);
|
||
});
|
||
|
||
it("programmatic opt-out beats a missing env opt-out (both escape hatches independent)", () => {
|
||
// Even with no env opt-out set, a programmatic opt-out preserves BeginFrame-
|
||
// on-software debugging on the auto→software probe path.
|
||
expect(
|
||
shouldClampToScreenshotForConcreteGpu(
|
||
"software",
|
||
false,
|
||
{ PRODUCER_FORCE_SCREENSHOT: "true" } as NodeJS.ProcessEnv,
|
||
{ programmaticOptOut: true },
|
||
),
|
||
).toBe(false);
|
||
});
|
||
});
|
||
|
||
describe("applyConcreteGpuScreenshotClamp (caller-level contract)", () => {
|
||
// This is the helper both frameCapture.ts and renderOrchestrator.ts call
|
||
// to compute the value the AUTHORITATIVE `forceScreenshot` local should
|
||
// hold after the concrete GPU is resolved. Routing AND telemetry read
|
||
// from that one value, so this contract must hold across default and
|
||
// opt-out combinations.
|
||
type OptOutCfg = Pick<EngineConfig, "forceScreenshotExplicitlyOptedOut">;
|
||
const cleanEnv = {} as NodeJS.ProcessEnv;
|
||
|
||
it("resolved software + default false → promotes to true (screenshot route)", () => {
|
||
// The core auto→software fix: routing AND downstream telemetry read
|
||
// the promoted value, so `updateCaptureObservability({ forceScreenshot:
|
||
// captureForceScreenshot })` at the capture_strategy site reports
|
||
// screenshot instead of overwriting back to beginframe.
|
||
expect(applyConcreteGpuScreenshotClamp(false, "software", {} as OptOutCfg, cleanEnv)).toBe(
|
||
true,
|
||
);
|
||
});
|
||
|
||
it("resolved software + programmatic opt-out → stays false (BeginFrame preserved)", () => {
|
||
// The programmatic escape hatch caller-level contract: setting
|
||
// overrides.forceScreenshot=false must keep BeginFrame across BOTH
|
||
// routing (frameCapture) and telemetry (renderOrchestrator) — since
|
||
// resolveConfig lifts the flag onto the config, both callers converge.
|
||
expect(
|
||
applyConcreteGpuScreenshotClamp(
|
||
false,
|
||
"software",
|
||
{ forceScreenshotExplicitlyOptedOut: true } as OptOutCfg,
|
||
cleanEnv,
|
||
),
|
||
).toBe(false);
|
||
});
|
||
|
||
it("resolved hardware + default false → stays false (no clamp needed)", () => {
|
||
expect(applyConcreteGpuScreenshotClamp(false, "hardware", {} as OptOutCfg, cleanEnv)).toBe(
|
||
false,
|
||
);
|
||
});
|
||
|
||
it("resolved software + already-true forceScreenshot → stays true (idempotent)", () => {
|
||
// Config-time clamp already fired (literal browserGpuMode:"software"),
|
||
// so re-applying at the concrete-resolved site is a no-op.
|
||
expect(applyConcreteGpuScreenshotClamp(true, "software", {} as OptOutCfg, cleanEnv)).toBe(
|
||
true,
|
||
);
|
||
});
|
||
|
||
it("resolved software + env PRODUCER_FORCE_SCREENSHOT=false → stays false", () => {
|
||
// Env opt-out preserved even when programmatic flag is not set (some
|
||
// callers, like debugging BeginFrame-on-software from CI, opt-out via
|
||
// env only).
|
||
expect(
|
||
applyConcreteGpuScreenshotClamp(
|
||
false,
|
||
"software",
|
||
{} as OptOutCfg,
|
||
{
|
||
PRODUCER_FORCE_SCREENSHOT: "false",
|
||
} as NodeJS.ProcessEnv,
|
||
),
|
||
).toBe(false);
|
||
});
|
||
|
||
it("resolved software + undefined cfg → default (no programmatic opt-out) → clamps to true", () => {
|
||
// Sanity: frameCapture.ts calls with `config` possibly undefined.
|
||
// Default case must still promote.
|
||
expect(applyConcreteGpuScreenshotClamp(false, "software", undefined, cleanEnv)).toBe(true);
|
||
});
|
||
});
|
||
|
||
describe("forceScreenshotExplicitlyOptedOut provenance", () => {
|
||
it("is set to true when programmatic override forceScreenshot=false is passed", () => {
|
||
setEnv("PRODUCER_BROWSER_GPU_MODE", "hardware");
|
||
unsetEnv("PRODUCER_FORCE_SCREENSHOT");
|
||
const config = resolveConfig({ forceScreenshot: false });
|
||
expect(config.forceScreenshotExplicitlyOptedOut).toBe(true);
|
||
});
|
||
|
||
it("is set to true when env PRODUCER_FORCE_SCREENSHOT=false is set", () => {
|
||
setEnv("PRODUCER_BROWSER_GPU_MODE", "hardware");
|
||
setEnv("PRODUCER_FORCE_SCREENSHOT", "false");
|
||
const config = resolveConfig();
|
||
expect(config.forceScreenshotExplicitlyOptedOut).toBe(true);
|
||
});
|
||
|
||
it("stays unset when neither opt-out is present (default)", () => {
|
||
setEnv("PRODUCER_BROWSER_GPU_MODE", "hardware");
|
||
unsetEnv("PRODUCER_FORCE_SCREENSHOT");
|
||
const config = resolveConfig();
|
||
expect(config.forceScreenshotExplicitlyOptedOut).toBeUndefined();
|
||
});
|
||
});
|
||
|
||
describe("shouldAutoDisableStreamingEncodeOnWin32Compound (helper)", () => {
|
||
// Baseline: field-signal compound — win32 + software-GPU forced + workers=1,
|
||
// duration unknown, user hasn't touched the env / overrides.
|
||
const compound = {
|
||
platform: "win32" as NodeJS.Platform,
|
||
softwareGpuForced: true,
|
||
workers: 1,
|
||
compositionDurationSec: undefined as number | undefined,
|
||
userExplicitlySet: false,
|
||
};
|
||
|
||
it("triggers on the field-signal compound (win32 + software-GPU + workers=1)", () => {
|
||
expect(shouldAutoDisableStreamingEncodeOnWin32Compound(compound)).toBe(true);
|
||
});
|
||
|
||
it("does NOT trigger on linux or darwin (platform gate)", () => {
|
||
expect(
|
||
shouldAutoDisableStreamingEncodeOnWin32Compound({ ...compound, platform: "linux" }),
|
||
).toBe(false);
|
||
expect(
|
||
shouldAutoDisableStreamingEncodeOnWin32Compound({ ...compound, platform: "darwin" }),
|
||
).toBe(false);
|
||
});
|
||
|
||
it("does NOT trigger without software-GPU forced (bypass on hardware paths)", () => {
|
||
expect(
|
||
shouldAutoDisableStreamingEncodeOnWin32Compound({ ...compound, softwareGpuForced: false }),
|
||
).toBe(false);
|
||
});
|
||
|
||
it("does NOT trigger with parallel workers (workers > 1 has a different failure surface)", () => {
|
||
expect(shouldAutoDisableStreamingEncodeOnWin32Compound({ ...compound, workers: 2 })).toBe(
|
||
false,
|
||
);
|
||
expect(shouldAutoDisableStreamingEncodeOnWin32Compound({ ...compound, workers: 4 })).toBe(
|
||
false,
|
||
);
|
||
});
|
||
|
||
it("does NOT trigger when the user explicitly set enableStreamingEncode (escape hatch)", () => {
|
||
expect(
|
||
shouldAutoDisableStreamingEncodeOnWin32Compound({ ...compound, userExplicitlySet: true }),
|
||
).toBe(false);
|
||
});
|
||
|
||
it("does NOT trigger for short (<=120s) compositions when duration is known", () => {
|
||
// 120s boundary is off (edge)
|
||
expect(
|
||
shouldAutoDisableStreamingEncodeOnWin32Compound({
|
||
...compound,
|
||
compositionDurationSec: 120,
|
||
}),
|
||
).toBe(false);
|
||
// 60s — clearly short, off
|
||
expect(
|
||
shouldAutoDisableStreamingEncodeOnWin32Compound({
|
||
...compound,
|
||
compositionDurationSec: 60,
|
||
}),
|
||
).toBe(false);
|
||
});
|
||
|
||
it("triggers when duration is known and exceeds 120s (heavy Windows composition)", () => {
|
||
// 121s — just past the boundary, on
|
||
expect(
|
||
shouldAutoDisableStreamingEncodeOnWin32Compound({
|
||
...compound,
|
||
compositionDurationSec: 121,
|
||
}),
|
||
).toBe(true);
|
||
// 156s — matches the field signal, on
|
||
expect(
|
||
shouldAutoDisableStreamingEncodeOnWin32Compound({
|
||
...compound,
|
||
compositionDurationSec: 156,
|
||
}),
|
||
).toBe(true);
|
||
});
|
||
|
||
it("triggers when duration is undefined (config-layer wire-up reduces to 3-cond)", () => {
|
||
// resolveConfig can't see composition duration at config time; the
|
||
// three-condition compound is conservative on its own.
|
||
expect(
|
||
shouldAutoDisableStreamingEncodeOnWin32Compound({
|
||
...compound,
|
||
compositionDurationSec: undefined,
|
||
}),
|
||
).toBe(true);
|
||
});
|
||
});
|
||
|
||
describe("enableStreamingEncode (Windows compound auto-disable wire-up)", () => {
|
||
const originalPlatform = process.platform;
|
||
|
||
function setPlatform(platform: NodeJS.Platform) {
|
||
Object.defineProperty(process, "platform", { value: platform, configurable: true });
|
||
}
|
||
|
||
afterEach(() => {
|
||
Object.defineProperty(process, "platform", { value: originalPlatform, configurable: true });
|
||
});
|
||
|
||
/**
|
||
* Shared setup/assert for the win32 software-GPU compound: fixed common
|
||
* env (low-memory on, no explicit streaming opt-in), variable platform /
|
||
* gpu / workers, then assert whether the auto-disable fired.
|
||
*/
|
||
function expectCompoundOutcome(opts: {
|
||
platform: NodeJS.Platform;
|
||
gpuMode?: string;
|
||
workers?: string;
|
||
autoDisabled: boolean;
|
||
}): void {
|
||
setPlatform(opts.platform);
|
||
if (opts.gpuMode === undefined) unsetEnv("PRODUCER_BROWSER_GPU_MODE");
|
||
else setEnv("PRODUCER_BROWSER_GPU_MODE", opts.gpuMode);
|
||
if (opts.workers === undefined) unsetEnv("PRODUCER_MAX_WORKERS");
|
||
else setEnv("PRODUCER_MAX_WORKERS", opts.workers);
|
||
setEnv("PRODUCER_LOW_MEMORY_MODE", "true");
|
||
unsetEnv("PRODUCER_ENABLE_STREAMING_ENCODE");
|
||
const config = resolveConfig();
|
||
expect(config.enableStreamingEncode).toBe(!opts.autoDisabled);
|
||
if (opts.autoDisabled) {
|
||
expect(config.streamingEncodeAutoDisabledOnWin32Compound).toBe(true);
|
||
} else {
|
||
expect(config.streamingEncodeAutoDisabledOnWin32Compound).toBeUndefined();
|
||
}
|
||
}
|
||
|
||
it("auto-disables on win32 + software-GPU + workers=1 + no user opt-in", () => {
|
||
expectCompoundOutcome({
|
||
platform: "win32",
|
||
gpuMode: "software",
|
||
workers: "1",
|
||
autoDisabled: true,
|
||
});
|
||
});
|
||
|
||
it("leaves streaming-encode on when platform is linux", () => {
|
||
expectCompoundOutcome({
|
||
platform: "linux",
|
||
gpuMode: "software",
|
||
workers: "1",
|
||
autoDisabled: false,
|
||
});
|
||
});
|
||
|
||
it("leaves streaming-encode on when workers > 1", () => {
|
||
expectCompoundOutcome({
|
||
platform: "win32",
|
||
gpuMode: "software",
|
||
workers: "4",
|
||
autoDisabled: false,
|
||
});
|
||
});
|
||
|
||
it("respects explicit env opt-in (PRODUCER_ENABLE_STREAMING_ENCODE=true) on the compound", () => {
|
||
setPlatform("win32");
|
||
setEnv("PRODUCER_BROWSER_GPU_MODE", "software");
|
||
setEnv("PRODUCER_MAX_WORKERS", "1");
|
||
setEnv("PRODUCER_LOW_MEMORY_MODE", "true");
|
||
setEnv("PRODUCER_ENABLE_STREAMING_ENCODE", "true");
|
||
|
||
const config = resolveConfig();
|
||
expect(config.enableStreamingEncode).toBe(true);
|
||
expect(config.streamingEncodeAutoDisabledOnWin32Compound).toBeUndefined();
|
||
});
|
||
|
||
it("respects programmatic override enableStreamingEncode=true on the compound", () => {
|
||
setPlatform("win32");
|
||
setEnv("PRODUCER_BROWSER_GPU_MODE", "software");
|
||
setEnv("PRODUCER_MAX_WORKERS", "1");
|
||
setEnv("PRODUCER_LOW_MEMORY_MODE", "true");
|
||
unsetEnv("PRODUCER_ENABLE_STREAMING_ENCODE");
|
||
|
||
const config = resolveConfig({ enableStreamingEncode: true });
|
||
expect(config.enableStreamingEncode).toBe(true);
|
||
expect(config.streamingEncodeAutoDisabledOnWin32Compound).toBeUndefined();
|
||
});
|
||
|
||
it("triggers via disableGpu on win32 + workers=1 (browserGpuMode may still be 'auto')", () => {
|
||
// --disable-gpu path: browserGpuMode may not be literal "software" but
|
||
// Chrome is still routed to CPU raster. Field-signal compound applies.
|
||
setPlatform("win32");
|
||
unsetEnv("PRODUCER_BROWSER_GPU_MODE");
|
||
setEnv("PRODUCER_DISABLE_GPU", "true");
|
||
setEnv("PRODUCER_MAX_WORKERS", "1");
|
||
unsetEnv("PRODUCER_LOW_MEMORY_MODE");
|
||
unsetEnv("PRODUCER_ENABLE_STREAMING_ENCODE");
|
||
|
||
const config = resolveConfig();
|
||
expect(config.enableStreamingEncode).toBe(false);
|
||
expect(config.streamingEncodeAutoDisabledOnWin32Compound).toBe(true);
|
||
});
|
||
|
||
it("triggers via lowMemoryMode alone on win32 + workers=1 (screenshot capture implied)", () => {
|
||
// --low-memory-mode implies screenshot capture. Compound applies even
|
||
// if browserGpuMode is not literal "software" (defense-in-depth: matches
|
||
// the OR semantics in `softwareGpuForced`).
|
||
unsetEnv("PRODUCER_DISABLE_GPU");
|
||
expectCompoundOutcome({ platform: "win32", workers: "1", autoDisabled: true });
|
||
});
|
||
|
||
it("does not trigger when concurrency is 'auto' (workers not explicitly pinned)", () => {
|
||
// Config-layer sees `concurrency === "auto"`, not a number — the
|
||
// helper's numeric workers check treats NaN as "unknown, don't trigger".
|
||
// Downstream workers may still resolve to 1 via lowMemoryMode, but the
|
||
// config-time clamp is deliberately conservative.
|
||
expectCompoundOutcome({ platform: "win32", gpuMode: "software", autoDisabled: false });
|
||
});
|
||
});
|
||
|
||
describe("lowMemoryMode", () => {
|
||
it("forces on for truthy PRODUCER_LOW_MEMORY_MODE values", () => {
|
||
setEnv("PRODUCER_LOW_MEMORY_MODE", "true");
|
||
for (const v of ["true", "on", "1", "TRUE"]) {
|
||
process.env.PRODUCER_LOW_MEMORY_MODE = v;
|
||
expect(resolveConfig().lowMemoryMode).toBe(true);
|
||
}
|
||
});
|
||
|
||
it("forces off for falsy PRODUCER_LOW_MEMORY_MODE values", () => {
|
||
setEnv("PRODUCER_LOW_MEMORY_MODE", "false");
|
||
for (const v of ["false", "off", "0", "OFF"]) {
|
||
process.env.PRODUCER_LOW_MEMORY_MODE = v;
|
||
expect(resolveConfig().lowMemoryMode).toBe(false);
|
||
}
|
||
});
|
||
|
||
it("auto-detects from total RAM when the env var is unset", () => {
|
||
setEnv("PRODUCER_LOW_MEMORY_MODE", "");
|
||
delete process.env.PRODUCER_LOW_MEMORY_MODE;
|
||
expect(resolveConfig().lowMemoryMode).toBe(isLowMemorySystem());
|
||
});
|
||
|
||
it("explicit override beats both env and auto-detection", () => {
|
||
setEnv("PRODUCER_LOW_MEMORY_MODE", "true");
|
||
expect(resolveConfig({ lowMemoryMode: false }).lowMemoryMode).toBe(false);
|
||
});
|
||
});
|
||
});
|
||
|
||
describe("scaleProtocolTimeoutForComposition", () => {
|
||
const base = 300_000;
|
||
|
||
it("keeps the base timeout for a reference-or-smaller canvas", () => {
|
||
// 1080p == reference area → factor 1, no scale.
|
||
expect(scaleProtocolTimeoutForComposition(base, { width: 1920, height: 1080 })).toBe(base);
|
||
// Smaller than reference → still the base (never scales down).
|
||
expect(scaleProtocolTimeoutForComposition(base, { width: 1280, height: 720 })).toBe(base);
|
||
});
|
||
|
||
it("scales up proportionally with output pixel area", () => {
|
||
// 4K == 4× the reference area, which stays under the 30-minute ceiling.
|
||
const scaled = scaleProtocolTimeoutForComposition(base, { width: 3840, height: 2160 });
|
||
expect(scaled).toBeGreaterThan(base);
|
||
expect(scaled).toBe(base * 4);
|
||
});
|
||
|
||
it("clamps at the 30-minute ceiling for a pathological canvas", () => {
|
||
// 8K == 16× area → 4.8M ms, clamped to the 30-minute ceiling.
|
||
const scaled = scaleProtocolTimeoutForComposition(base, { width: 7680, height: 4320 });
|
||
expect(scaled).toBe(1_800_000);
|
||
});
|
||
|
||
it("never lowers a base timeout that already exceeds the ceiling", () => {
|
||
// Base above the 30-min ceiling + a large canvas: must not clamp below base.
|
||
const highBase = 2_400_000;
|
||
expect(
|
||
scaleProtocolTimeoutForComposition(highBase, { width: 3840, height: 2160 }),
|
||
).toBeGreaterThanOrEqual(highBase);
|
||
});
|
||
|
||
it("returns the base timeout for degenerate dimensions", () => {
|
||
expect(scaleProtocolTimeoutForComposition(base, { width: 0, height: 1080 })).toBe(base);
|
||
expect(scaleProtocolTimeoutForComposition(base, { width: 1920, height: 0 })).toBe(base);
|
||
expect(scaleProtocolTimeoutForComposition(base, { width: Number.NaN, height: 1080 })).toBe(
|
||
base,
|
||
);
|
||
});
|
||
});
|
||
|
||
describe("resolveExtractCacheDir", () => {
|
||
it("returns the OS-default cache dir when the env var is unset", () => {
|
||
const res = resolveExtractCacheDir({});
|
||
expect(res.disabled).toBe(false);
|
||
expect(res.source).toBe("default");
|
||
if (!res.disabled) {
|
||
expect(res.dir).toBe(defaultExtractCacheDir());
|
||
}
|
||
});
|
||
|
||
it("threads a positive env value through verbatim (source: env)", () => {
|
||
const res = resolveExtractCacheDir({
|
||
HYPERFRAMES_EXTRACT_CACHE_DIR: "D:/hf-cache",
|
||
});
|
||
expect(res.disabled).toBe(false);
|
||
expect(res.source).toBe("env");
|
||
if (!res.disabled) {
|
||
expect(res.dir).toBe("D:/hf-cache");
|
||
expect(res.rawValue).toBe("D:/hf-cache");
|
||
}
|
||
});
|
||
|
||
it.each(EXTRACT_CACHE_DIR_DISABLED_ALIASES.flatMap((v) => [v, v.toUpperCase(), ` ${v} `]))(
|
||
"reports disabled for opt-out alias %s",
|
||
(value) => {
|
||
const res = resolveExtractCacheDir({ HYPERFRAMES_EXTRACT_CACHE_DIR: value });
|
||
expect(res.disabled).toBe(true);
|
||
if (res.disabled) {
|
||
expect(res.dir).toBeUndefined();
|
||
expect(res.rawValue).toBe(value);
|
||
}
|
||
},
|
||
);
|
||
|
||
it("defaults to process.env when no env argument is passed", () => {
|
||
// Signal-only smoke test — the previous callers rely on this default and
|
||
// both engine.resolveConfig() and doctor's checkFramesCache() would break
|
||
// silently if the signature drifted to require an env argument.
|
||
expect(() => resolveExtractCacheDir()).not.toThrow();
|
||
});
|
||
});
|