test(producer): extract frameDirMaxIndexCache to its own module and pin cross-job isolation (#381)

## Summary

Extract the `frameDirMaxIndexCache` from a private module-scoped Map inside `renderOrchestrator.ts` into its own `frameDirCache.ts` module, then add a 11-test bun:test suite that pins the cross-job isolation contract added in Chunk 5B.

## Why

`Chunk 9E` of `plans/hdr-followups.md`. The cache lived as a private Map inside `renderOrchestrator.ts`, which made the cross-job isolation contract from Chunk 5B impossible to unit-test directly. Extracting it both makes the contract testable and reduces orchestrator complexity slightly.

## What changed

- New `packages/producer/src/services/frameDirCache.ts` exposes `getMaxFrameIndex` / `clearMaxFrameIndex` / `getMaxFrameIndexCacheSize` (plus a test-only `__resetMaxFrameIndexCacheForTests` helper). Behavior is unchanged: callers still get the same module-scoped sharing inside a job, and `renderOrchestrator`'s outer `finally` still clears every entry it registered so the cache cannot grow monotonically across renders.
- `renderOrchestrator.ts`: imports the new helpers, drops the unused `readdirSync` import, updates inline comments, and replaces two `frameDirMaxIndexCache.delete` sites with `clearMaxFrameIndex`.
- New `frameDirCache.test.ts` (bun:test, 11 tests) covering:
  - Reading the max index from a populated directory.
  - Ignoring filenames that don't match `frame_NNNN.png` (wrong ext, wrong prefix, wrong case, double extension, empty index group, same-named subdirectory).
  - Empty- and missing-directory paths returning `0` and being cached.
  - Intra-job invariant: subsequent readdir mutations not observed once cached.
  - `clearMaxFrameIndex` forcing a re-read; returns `false` for paths that were never cached.
  - Per-directory isolation when multiple directories are registered.
  - The cross-job contract from Chunk 5B: cache empty between well-behaved jobs, doesn't grow monotonically across 20 simulated renders with 3 HDR videos each (steady-state cache size stays at 3), and a buggy job that forgets to clear leaks exactly its own entries rather than affecting unrelated jobs.

## Test plan

- [x] `frameDirCache.test.ts` 11/11 pass.
- [x] Existing producer tests unchanged.
- [x] Behavior preserved: same module-scoped sharing inside a job, same outer-`finally` eviction.

## Stack

Chunk 9E of `plans/hdr-followups.md`. Test-driven extraction; complements Chunk 5B.
This commit is contained in:
Vance Ingalls
2026-04-23 14:29:36 -07:00
committed by GitHub
parent 154511247a
commit cc9403b6bd
4 changed files with 453 additions and 9 deletions
+11 -7
View File
@@ -2,13 +2,17 @@ import { afterEach, describe, expect, it, vi } from "vitest";
import { createServer, type Server } from "node:net";
import { PORT_PROBE_HOSTS, testPortOnAllHosts } from "./portUtils.js";
// High-ephemeral range with runway so parallel test shards don't collide.
const BASE = 45_000;
const openServers: Server[] = [];
function allocFreePort(): number {
return BASE + Math.floor(Math.random() * 1_000);
async function allocFreePort(): Promise<number> {
const srv = createServer();
await new Promise<void>((resolve, reject) => {
srv.once("error", reject);
srv.listen(0, "127.0.0.1", () => resolve());
});
const port = (srv.address() as import("node:net").AddressInfo).port;
await new Promise<void>((resolve) => srv.close(() => resolve()));
return port;
}
afterEach(async () => {
@@ -31,13 +35,13 @@ describe("testPortOnAllHosts — real-socket behaviour (OS-dependent)", () => {
// regression gate.
it("returns true for a genuinely free port (regression: #309)", async () => {
const port = allocFreePort();
const port = await allocFreePort();
const result = await testPortOnAllHosts(port);
expect(result).toBe(true);
});
it("returns false when the port is occupied on 0.0.0.0", async () => {
const port = allocFreePort();
const port = await allocFreePort();
const blocker = createServer();
openServers.push(blocker);
await new Promise<void>((resolve, reject) => {