mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-05 10:14:30 +00:00
## 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.
100 lines
3.4 KiB
TypeScript
100 lines
3.4 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import { createServer, type Server } from "node:net";
|
|
import { PORT_PROBE_HOSTS, testPortOnAllHosts } from "./portUtils.js";
|
|
|
|
const openServers: Server[] = [];
|
|
|
|
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 () => {
|
|
await Promise.all(
|
|
openServers.splice(0).map(
|
|
(s) =>
|
|
new Promise<void>((resolve) => {
|
|
s.close(() => resolve());
|
|
}),
|
|
),
|
|
);
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
describe("testPortOnAllHosts — real-socket behaviour (OS-dependent)", () => {
|
|
// These exercise the real network stack. On Linux the buggy parallel
|
|
// implementation reliably fails the first test (issue #309 repro); on
|
|
// macOS the race is not deterministic so both old and new code pass
|
|
// here. The sequential-contract test below is the platform-agnostic
|
|
// regression gate.
|
|
|
|
it("returns true for a genuinely free port (regression: #309)", async () => {
|
|
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 = await allocFreePort();
|
|
const blocker = createServer();
|
|
openServers.push(blocker);
|
|
await new Promise<void>((resolve, reject) => {
|
|
blocker.once("error", reject);
|
|
blocker.listen({ port, host: "0.0.0.0" }, () => resolve());
|
|
});
|
|
const result = await testPortOnAllHosts(port);
|
|
expect(result).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("testPortOnAllHosts — sequential contract (platform-agnostic)", () => {
|
|
/**
|
|
* Load-bearing regression test. Injects a recording fake probe that
|
|
* holds each call open for a few ms and tracks how many are in flight.
|
|
* The parallel (buggy) implementation would drive overlap to 4; the
|
|
* sequential fix keeps it at 1. Deterministic on every OS.
|
|
*/
|
|
it("runs host probes sequentially — never more than one concurrent", async () => {
|
|
let inFlight = 0;
|
|
let peakConcurrency = 0;
|
|
const hostsProbed: string[] = [];
|
|
|
|
const fakeProbe = async (_port: number, host: string): Promise<boolean> => {
|
|
inFlight++;
|
|
if (inFlight > peakConcurrency) peakConcurrency = inFlight;
|
|
hostsProbed.push(host);
|
|
// Hold so any parallel overlap from a regression would be visible
|
|
// here regardless of OS scheduling.
|
|
await new Promise((r) => setTimeout(r, 20));
|
|
inFlight--;
|
|
return true;
|
|
};
|
|
|
|
const result = await testPortOnAllHosts(7777, fakeProbe);
|
|
|
|
expect(result).toBe(true);
|
|
expect(peakConcurrency).toBe(1);
|
|
expect(hostsProbed).toEqual([...PORT_PROBE_HOSTS]);
|
|
});
|
|
|
|
it("short-circuits on the first unavailable host", async () => {
|
|
const hostsProbed: string[] = [];
|
|
const fakeProbe = async (_port: number, host: string): Promise<boolean> => {
|
|
hostsProbed.push(host);
|
|
// Second host reports in-use; verify we never probe hosts three and four.
|
|
return host === "127.0.0.1";
|
|
};
|
|
|
|
const result = await testPortOnAllHosts(7777, fakeProbe);
|
|
|
|
expect(result).toBe(false);
|
|
expect(hostsProbed).toEqual(["127.0.0.1", "0.0.0.0"]);
|
|
});
|
|
});
|