Files
hyperframes/packages/cli/src/server/portUtils.test.ts
T
Vance Ingalls cc9403b6bd 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.
2026-04-23 14:29:36 -07:00

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"]);
});
});