mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-08 10:46:06 +00:00
fix(cli): fix batch re-entrancy, config race, exposure cap, and shouldTrack gap in DE trial
Four confirmed findings from a max-effort code review of the CLI trial mechanism: 1. maybeEnableDeParallelRouterTrial's `process.env.HF_DE_PARALLEL_ROUTER !== undefined` guard couldn't distinguish "the user set this" from "an earlier renderLocal() call in this same process already armed it" — so in --batch (all rows share one process), only row 1's outcome could ever reach maybeConsumeDeParallelRouterTrial. A revert on any later row was silently never persisted. Added a module-level deParallelRouterTrialManagedByUs flag to disambiguate, with a test-only reset export since it's process-lifetime state a real CLI invocation never needs to reset but a test suite sharing one module instance does. 2. writeConfig is a non-atomic whole-file overwrite with no locking, and readConfig's cache never invalidates — a concurrently running second CLI process (another terminal, a parallel script; doesn't even need to be a render, any command calls incrementCommandCount) could silently clobber a just-persisted deParallelRouterTrialFired:true with its own stale snapshot. Added readConfigFresh (bypasses the cache) and use it immediately before the trial's read-modify-write, narrowing the race window without a full config-subsystem locking rewrite. 3. The prior commit's semantics flip removed the only exposure cap — a healthy router that never reverts now force-enabled the experimental path on every eligible render forever. Added DE_PARALLEL_ROUTER_TRIAL_MAX_RENDERS (25) as a backstop: the trial turns off after this many engaged renders even absent an actual failure. 4. maybeEnableDeParallelRouterTrial only checked config.telemetryEnabled, not shouldTrack() — so a dev-mode run or a DO_NOT_TRACK/ HYPERFRAMES_NO_TELEMETRY user got the experimental path silently armed while telemetry was simultaneously blocked underneath it. Now gates on shouldTrack() (a strict superset). Also fixed, lower severity: readConfig's deParallelRouterTrialFired/ deParallelRouterTrialRenderCount parsing now validates the JSON type explicitly instead of a bare truthy/nullish read, so a hand-edited or corrupted config can't have the string "false" misread as truthy. Refactored maybeEnableDeParallelRouterTrial into three smaller functions (isDeParallelRouterTrialBlocked, stopManagingDeParallelRouterTrial) to bring cyclomatic/cognitive complexity back under the repo's threshold — also de-duplicates the "stop managing the env var" logic shared with maybeConsumeDeParallelRouterTrial. 14 new/updated tests (43 total in render.test.ts), including a direct regression test for the batch re-entrancy scenario and a loop test for the render-count cap. Verified the config primitives end-to-end against a real file, not just the mocked unit tests. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
19f90b0b92
commit
532dad7cc7
@@ -18,6 +18,14 @@ const configState = vi.hoisted(() => ({
|
||||
writeConfigCalls: [] as Array<Record<string, unknown>>,
|
||||
}));
|
||||
|
||||
const trackingState = vi.hoisted(() => ({
|
||||
// maybeEnableDeParallelRouterTrial gates on the real shouldTrack(), which
|
||||
// (via isDevMode()) always returns false when this file itself runs as
|
||||
// `.ts` source under vitest — mocked here so the CLI-trial tests can
|
||||
// control it directly instead of inheriting that environment quirk.
|
||||
shouldTrack: true,
|
||||
}));
|
||||
|
||||
const preflightState = vi.hoisted(() => ({
|
||||
result: {
|
||||
outcomes: [
|
||||
@@ -59,12 +67,17 @@ vi.mock("../utils/producer.js", () => ({
|
||||
|
||||
vi.mock("../telemetry/config.js", () => ({
|
||||
readConfig: vi.fn(() => ({ ...configState.config })),
|
||||
readConfigFresh: vi.fn(() => ({ ...configState.config })),
|
||||
writeConfig: vi.fn((config: Record<string, unknown>) => {
|
||||
configState.config = { ...config };
|
||||
configState.writeConfigCalls.push({ ...config });
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("../telemetry/client.js", () => ({
|
||||
shouldTrack: vi.fn(() => trackingState.shouldTrack),
|
||||
}));
|
||||
|
||||
vi.mock("../telemetry/events.js", () => ({
|
||||
trackRenderComplete: vi.fn(),
|
||||
trackRenderError: vi.fn(),
|
||||
@@ -88,9 +101,14 @@ describe("renderLocal browser GPU config", () => {
|
||||
// suites). Importing once in `beforeAll` keeps every test fast and isolated.
|
||||
let renderLocal: typeof import("./render.js").renderLocal;
|
||||
let resolveBrowserGpuForCli: typeof import("./render.js").resolveBrowserGpuForCli;
|
||||
let resetTrialState: typeof import("./render.js").__resetDeParallelRouterTrialStateForTests;
|
||||
|
||||
beforeAll(async () => {
|
||||
({ renderLocal, resolveBrowserGpuForCli } = await import("./render.js"));
|
||||
({
|
||||
renderLocal,
|
||||
resolveBrowserGpuForCli,
|
||||
__resetDeParallelRouterTrialStateForTests: resetTrialState,
|
||||
} = await import("./render.js"));
|
||||
});
|
||||
|
||||
function setEnv(key: string, value: string) {
|
||||
@@ -104,6 +122,8 @@ describe("renderLocal browser GPU config", () => {
|
||||
producerState.executeImpl = async () => undefined;
|
||||
configState.config = { telemetryEnabled: true, deParallelRouterTrialFired: true };
|
||||
configState.writeConfigCalls = [];
|
||||
trackingState.shouldTrack = true;
|
||||
resetTrialState();
|
||||
savedEnv.clear();
|
||||
savedEnv.set("HYPERFRAMES_FFMPEG_PATH", process.env.HYPERFRAMES_FFMPEG_PATH);
|
||||
savedEnv.set("HYPERFRAMES_FFPROBE_PATH", process.env.HYPERFRAMES_FFPROBE_PATH);
|
||||
@@ -441,16 +461,24 @@ describe("renderLocal browser GPU config", () => {
|
||||
|
||||
describe("renderLocal — DE parallel-router CLI trial", () => {
|
||||
let renderLocal: typeof import("./render.js").renderLocal;
|
||||
let resetTrialState: typeof import("./render.js").__resetDeParallelRouterTrialStateForTests;
|
||||
const savedEnv = new Map<string, string | undefined>();
|
||||
|
||||
beforeAll(async () => {
|
||||
({ renderLocal } = await import("./render.js"));
|
||||
({ renderLocal, __resetDeParallelRouterTrialStateForTests: resetTrialState } =
|
||||
await import("./render.js"));
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
producerState.createdJobs = [];
|
||||
producerState.executeImpl = async () => undefined;
|
||||
configState.writeConfigCalls = [];
|
||||
trackingState.shouldTrack = true;
|
||||
// The "managed by us" flag lives at module scope in render.ts (real CLI
|
||||
// processes only ever run one --batch sequence, so it never needs
|
||||
// resetting there) — reset explicitly here so tests don't leak arm/
|
||||
// consume state into each other via shared module instance + test order.
|
||||
resetTrialState();
|
||||
savedEnv.clear();
|
||||
savedEnv.set("HF_DE_PARALLEL_ROUTER", process.env.HF_DE_PARALLEL_ROUTER);
|
||||
savedEnv.set("HYPERFRAMES_FFMPEG_PATH", process.env.HYPERFRAMES_FFMPEG_PATH);
|
||||
@@ -499,8 +527,9 @@ describe("renderLocal — DE parallel-router CLI trial", () => {
|
||||
expect(process.env.HF_DE_PARALLEL_ROUTER).toBeUndefined();
|
||||
});
|
||||
|
||||
it("does not enable the trial when telemetry is disabled", async () => {
|
||||
configState.config = { telemetryEnabled: false, deParallelRouterTrialFired: false };
|
||||
it("does not enable the trial when telemetry isn't actually trackable (shouldTrack() false — dev mode / DO_NOT_TRACK / disabled)", async () => {
|
||||
configState.config = { telemetryEnabled: true, deParallelRouterTrialFired: false };
|
||||
trackingState.shouldTrack = false;
|
||||
await renderLocal("/tmp/project", "/tmp/out.mp4", baseOptions);
|
||||
expect(process.env.HF_DE_PARALLEL_ROUTER).toBeUndefined();
|
||||
});
|
||||
@@ -514,7 +543,14 @@ describe("renderLocal — DE parallel-router CLI trial", () => {
|
||||
};
|
||||
};
|
||||
await renderLocal("/tmp/project", "/tmp/out.mp4", baseOptions);
|
||||
expect(configState.writeConfigCalls).toHaveLength(0);
|
||||
// A write DOES happen — the render-count backstop is tracked on every
|
||||
// engaged render — but it must not flip deParallelRouterTrialFired.
|
||||
expect(configState.writeConfigCalls).toContainEqual(
|
||||
expect.objectContaining({
|
||||
deParallelRouterTrialFired: false,
|
||||
deParallelRouterTrialRenderCount: 1,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("persists the trial as fired when the router's own safety net actually reverted", async () => {
|
||||
@@ -549,7 +585,15 @@ describe("renderLocal — DE parallel-router CLI trial", () => {
|
||||
await renderLocal("/tmp/project", "/tmp/out.mp4", { ...baseOptions, throwOnError: true }).catch(
|
||||
() => {},
|
||||
);
|
||||
expect(configState.writeConfigCalls).toHaveLength(0);
|
||||
// Still counts toward the render-count backstop (the router DID engage),
|
||||
// but must not flip deParallelRouterTrialFired — the crash wasn't the
|
||||
// router's own safety net firing.
|
||||
expect(configState.writeConfigCalls).toContainEqual(
|
||||
expect.objectContaining({
|
||||
deParallelRouterTrialFired: false,
|
||||
deParallelRouterTrialRenderCount: 1,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("persists the trial as fired from the failure path when the router's safety net reverted but the retry still failed", async () => {
|
||||
@@ -565,6 +609,83 @@ describe("renderLocal — DE parallel-router CLI trial", () => {
|
||||
expect.objectContaining({ deParallelRouterTrialFired: true }),
|
||||
);
|
||||
});
|
||||
|
||||
it("persists a later --batch row's revert even though this process already armed the trial on an earlier row", async () => {
|
||||
// Regression test for the exact scenario a --batch run hits: multiple
|
||||
// renderLocal calls in one process. Before the fix, row 2's
|
||||
// maybeEnableDeParallelRouterTrial saw process.env.HF_DE_PARALLEL_ROUTER
|
||||
// already "true" (set by row 1) and mistook that for "the user set it",
|
||||
// returning trialArmed=false — silently dropping row 2's revert.
|
||||
configState.config = { telemetryEnabled: true, deParallelRouterTrialFired: false };
|
||||
|
||||
producerState.executeImpl = async (job) => {
|
||||
job.perfSummary = {
|
||||
resolution: { width: 100, height: 100 },
|
||||
drawElement: { parallelRouter: "routed" },
|
||||
};
|
||||
};
|
||||
await renderLocal("/tmp/project", "/tmp/out.mp4", baseOptions);
|
||||
expect(process.env.HF_DE_PARALLEL_ROUTER).toBe("true");
|
||||
expect(configState.config.deParallelRouterTrialFired).toBe(false);
|
||||
|
||||
producerState.executeImpl = async (job) => {
|
||||
job.perfSummary = {
|
||||
resolution: { width: 100, height: 100 },
|
||||
drawElement: { parallelRouter: "reverted" },
|
||||
};
|
||||
};
|
||||
await renderLocal("/tmp/project", "/tmp/out.mp4", baseOptions);
|
||||
|
||||
expect(configState.writeConfigCalls).toContainEqual(
|
||||
expect.objectContaining({ deParallelRouterTrialFired: true }),
|
||||
);
|
||||
expect(process.env.HF_DE_PARALLEL_ROUTER).toBeUndefined();
|
||||
});
|
||||
|
||||
it("does not override an env var the user set between two renders in the same process", async () => {
|
||||
configState.config = { telemetryEnabled: true, deParallelRouterTrialFired: false };
|
||||
producerState.executeImpl = async (job) => {
|
||||
job.perfSummary = {
|
||||
resolution: { width: 100, height: 100 },
|
||||
drawElement: { parallelRouter: "routed" },
|
||||
};
|
||||
};
|
||||
await renderLocal("/tmp/project", "/tmp/out.mp4", baseOptions);
|
||||
expect(process.env.HF_DE_PARALLEL_ROUTER).toBe("true");
|
||||
|
||||
// A real interactive user can't do this mid-batch, but a wrapper script
|
||||
// invoking the CLI programmatically in the same process could — the
|
||||
// explicit override must still win on the next call.
|
||||
process.env.HF_DE_PARALLEL_ROUTER = "false";
|
||||
await renderLocal("/tmp/project", "/tmp/out.mp4", baseOptions);
|
||||
expect(process.env.HF_DE_PARALLEL_ROUTER).toBe("false");
|
||||
});
|
||||
|
||||
it("caps exposure at DE_PARALLEL_ROUTER_TRIAL_MAX_RENDERS even when the router never reverts", async () => {
|
||||
configState.config = { telemetryEnabled: true, deParallelRouterTrialFired: false };
|
||||
producerState.executeImpl = async (job) => {
|
||||
job.perfSummary = {
|
||||
resolution: { width: 100, height: 100 },
|
||||
drawElement: { parallelRouter: "routed" },
|
||||
};
|
||||
};
|
||||
|
||||
for (let i = 0; i < 25; i++) {
|
||||
await renderLocal("/tmp/project", "/tmp/out.mp4", baseOptions);
|
||||
}
|
||||
|
||||
expect(configState.writeConfigCalls).toContainEqual(
|
||||
expect.objectContaining({
|
||||
deParallelRouterTrialFired: true,
|
||||
deParallelRouterTrialRenderCount: 25,
|
||||
}),
|
||||
);
|
||||
expect(process.env.HF_DE_PARALLEL_ROUTER).toBeUndefined();
|
||||
|
||||
// The 26th eligible render must not re-arm it.
|
||||
await renderLocal("/tmp/project", "/tmp/out.mp4", baseOptions);
|
||||
expect(process.env.HF_DE_PARALLEL_ROUTER).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("checkRenderResolutionPreflight", () => {
|
||||
|
||||
Reference in New Issue
Block a user