diff --git a/packages/engine/src/config.ts b/packages/engine/src/config.ts index 969bbd611..b350f9e67 100644 --- a/packages/engine/src/config.ts +++ b/packages/engine/src/config.ts @@ -344,7 +344,6 @@ const BOOLEAN_ENGINE_CONFIG_FIELDS = [ ] as const; const POSITIVE_NUMBER_ENGINE_CONFIG_FIELDS = [ - "coresPerWorker", "browserTimeout", "protocolTimeout", "chunkSizeFrames", @@ -394,6 +393,13 @@ function assertEngineConfigNumber( } } +function assertPositiveEngineConfigNumber(config: Record, field: string): void { + const value = config[field]; + if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) { + throw new Error(`Engine config ${field} must be a finite number > 0`); + } +} + function assertEngineConfigEnum( config: Record, field: string, @@ -435,7 +441,7 @@ function validateEngineConfigParallelism(config: Record): void ) { throw new Error("Engine config concurrency must be a positive integer or auto"); } - assertEngineConfigNumber(config, "coresPerWorker", 0); + assertPositiveEngineConfigNumber(config, "coresPerWorker"); for (const field of ["minParallelFrames", "largeRenderThreshold"] as const) { assertEngineConfigNumber(config, field, 0, true); } diff --git a/packages/producer/src/renderRequest.test.ts b/packages/producer/src/renderRequest.test.ts index 728ccdf59..7fa68b6d2 100644 --- a/packages/producer/src/renderRequest.test.ts +++ b/packages/producer/src/renderRequest.test.ts @@ -1,5 +1,5 @@ import { afterEach, describe, expect, it } from "vitest"; -import { DEFAULT_CONFIG } from "@hyperframes/engine"; +import { DEFAULT_CONFIG, resolveConfig } from "@hyperframes/engine"; import { createRenderRequest, distributedConfigFromRequest, @@ -8,6 +8,7 @@ import { renderRequestFromDistributedConfig, serializeRenderRequest, } from "./renderRequest.js"; +import { InvalidConfigError } from "./services/distributed/renderConfigValidation.js"; const originalForceScreenshot = process.env.PRODUCER_FORCE_SCREENSHOT; @@ -203,6 +204,52 @@ describe("RenderRequest", () => { } }); + it("accepts fractional coresPerWorker snapshots from the canonical resolver", () => { + const engineConfig = resolveConfig({ coresPerWorker: 0.5 }); + const value = createRenderRequest({ + projectDir: "/project", + outputPath: "/output/video.mp4", + engineConfig, + options: { + fps: { num: 30, den: 1 }, + quality: "standard", + format: "mp4", + distributed: { width: 1920, height: 1080, cfr: true }, + }, + }); + + expect(value.options.engineConfig.coresPerWorker).toBe(0.5); + expect(parseRenderRequest(serializeRenderRequest(value))).toEqual(value); + + const distributed = distributedConfigFromRequest(value); + (distributed as { engineConfig: unknown }).engineConfig = engineConfig; + expect( + renderRequestFromDistributedConfig({ + projectDir: value.projectDir, + outputPath: value.outputPath, + config: distributed, + }).options.engineConfig.coresPerWorker, + ).toBe(0.5); + }); + + it("preserves the distributed InvalidConfigError contract for engine snapshots", () => { + const value = request(); + const distributed = distributedConfigFromRequest(value); + (distributed as { engineConfig: unknown }).engineConfig = {}; + + try { + renderRequestFromDistributedConfig({ + projectDir: value.projectDir, + outputPath: value.outputPath, + config: distributed, + }); + throw new Error("expected distributed validation to fail"); + } catch (error) { + expect(error).toBeInstanceOf(InvalidConfigError); + expect((error as InvalidConfigError).field).toBe("config.engineConfig"); + } + }); + it("validates the reverse distributed adapter at the wire boundary", () => { const distributed = distributedConfigFromRequest(request()); distributed.width = 1921; diff --git a/packages/producer/src/services/distributed/renderConfigValidation.ts b/packages/producer/src/services/distributed/renderConfigValidation.ts index 812e725d1..261fc11d1 100644 --- a/packages/producer/src/services/distributed/renderConfigValidation.ts +++ b/packages/producer/src/services/distributed/renderConfigValidation.ts @@ -208,7 +208,14 @@ export function validateDistributedRenderConfig( validateVariablesPayload(config.variables); } if (config.engineConfig !== undefined) { - validateEngineConfigSnapshot(config.engineConfig); + try { + validateEngineConfigSnapshot(config.engineConfig); + } catch (error) { + throw new InvalidConfigError( + "config.engineConfig", + error instanceof Error ? error.message : "must be a valid engine config snapshot", + ); + } } return config;