Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 101 additions & 0 deletions packages/producer/src/services/render/runtimeEnvSnapshot.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/**
* Tests for the `snapshotRuntimeEnv` helper that backs the
* `LockedRenderConfig.runtimeEnv` field.
*/

import { describe, expect, it } from "bun:test";
import { RUNTIME_ENV_PREFIXES, snapshotRuntimeEnv } from "./runtimeEnvSnapshot.js";

describe("snapshotRuntimeEnv", () => {
it("captures PRODUCER_RUNTIME_* keys", () => {
const env = {
PRODUCER_RUNTIME_RENDER_SEEK_MODE: "strict-boundary",
PRODUCER_RUNTIME_RENDER_SEEK_OFFSET_FRACTION: "0.5",
};
expect(snapshotRuntimeEnv(env)).toEqual({
PRODUCER_RUNTIME_RENDER_SEEK_MODE: "strict-boundary",
PRODUCER_RUNTIME_RENDER_SEEK_OFFSET_FRACTION: "0.5",
});
});

it("captures PRODUCER_RENDER_* keys", () => {
const env = {
PRODUCER_RENDER_SEEK_STEP: "0.008",
};
expect(snapshotRuntimeEnv(env)).toEqual({
PRODUCER_RENDER_SEEK_STEP: "0.008",
});
});

it("captures both prefix families in a single snapshot", () => {
const env = {
PRODUCER_RUNTIME_RENDER_SEEK_MODE: "strict-boundary",
PRODUCER_RENDER_SEEK_STEP: "0.008",
};
expect(snapshotRuntimeEnv(env)).toEqual({
PRODUCER_RUNTIME_RENDER_SEEK_MODE: "strict-boundary",
PRODUCER_RENDER_SEEK_STEP: "0.008",
});
});

it("ignores keys that don't match either prefix", () => {
const env = {
PRODUCER_RUNTIME_RENDER_SEEK_MODE: "strict-boundary",
HOME: "/home/ci",
PATH: "/usr/bin:/bin",
NODE_ENV: "production",
// Off-by-one prefix variants — must NOT be captured.
PRODUCER_RUNTIM_FOO: "x",
PRODUCER_RENDR_BAR: "y",
PRODUCER_DEBUG_SEEK_DIAGNOSTICS: "true",
};
const snapshot = snapshotRuntimeEnv(env);
expect(snapshot).toEqual({
PRODUCER_RUNTIME_RENDER_SEEK_MODE: "strict-boundary",
});
expect(Object.keys(snapshot)).toHaveLength(1);
});

it("skips keys whose value is undefined", () => {
const env = {
PRODUCER_RUNTIME_RENDER_SEEK_MODE: "strict-boundary",
PRODUCER_RUNTIME_OTHER: undefined,
};
expect(snapshotRuntimeEnv(env)).toEqual({
PRODUCER_RUNTIME_RENDER_SEEK_MODE: "strict-boundary",
});
});

it("returns an empty object when no keys match", () => {
expect(snapshotRuntimeEnv({ HOME: "/home/ci", PATH: "/usr/bin" })).toEqual({});
});

it("returns a NEW object each call (no live reference to process.env)", () => {
const env = { PRODUCER_RUNTIME_X: "v1" };
const first = snapshotRuntimeEnv(env);
env.PRODUCER_RUNTIME_X = "v2";
const second = snapshotRuntimeEnv(env);
expect(first.PRODUCER_RUNTIME_X).toBe("v1");
expect(second.PRODUCER_RUNTIME_X).toBe("v2");
expect(first).not.toBe(second);
});

it("defaults to process.env when no argument is passed", () => {
const key = `PRODUCER_RUNTIME_FREEZEPLAN_TEST_${Date.now()}`;
const sentinel = "freezeplan-test-value";
process.env[key] = sentinel;
try {
const snapshot = snapshotRuntimeEnv();
expect(snapshot[key]).toBe(sentinel);
} finally {
delete process.env[key];
}
});

it("exports the prefix list for chunk-worker materialization", () => {
// Chunk workers must apply the SAME prefix filter when reading the
// snapshot back; asymmetric handling would let stale controller env
// leak into chunk-worker behavior.
expect(RUNTIME_ENV_PREFIXES).toEqual(["PRODUCER_RUNTIME_", "PRODUCER_RENDER_"]);
});
});
53 changes: 53 additions & 0 deletions packages/producer/src/services/render/runtimeEnvSnapshot.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/**
* runtimeEnvSnapshot — capture / re-apply the env vars that drive in-page
* render behavior.
*
* `fileServer.ts` reads several `PRODUCER_RUNTIME_*` and `PRODUCER_RENDER_*`
* variables at module-load time and bakes them into the served HTML's
* `RENDER_MODE_SCRIPT`. Distributed chunk workers are separate processes
* that may inherit a different environment, so the plan freezes a snapshot
* of the controller's env. The chunk worker then materializes the snapshot
* back into `process.env` before launching its file server, which keeps the
* served HTML byte-identical to what the controller would have served.
*
* Used by `freezePlan` (capture side) and the chunked render worker
* (re-apply side). Kept here as a standalone utility because it has no
* dependency on the plan-freeze pipeline.
*/

/**
* Env-var name prefixes captured by {@link snapshotRuntimeEnv}. Exported so
* the chunk-worker side can apply the same filter when materializing a
* snapshot — asymmetric handling would leak stale controller env into
* worker behavior.
*/
export const RUNTIME_ENV_PREFIXES: readonly string[] = [
"PRODUCER_RUNTIME_",
"PRODUCER_RENDER_",
] as const;

/**
* Snapshot `process.env` keys that match any of {@link RUNTIME_ENV_PREFIXES}
* into a plain string→string record. Returns a NEW object each call (never a
* live reference to `process.env`) so subsequent mutations of the process
* env do not retroactively change a frozen plan.
*
* Pass an optional `env` for tests that don't want to mutate the real
* process env. The default reads `process.env`.
*/
export function snapshotRuntimeEnv(
env: Record<string, string | undefined> = process.env,
): Record<string, string> {
const snapshot: Record<string, string> = {};
for (const key of Object.keys(env)) {
const matches = RUNTIME_ENV_PREFIXES.some((prefix) => key.startsWith(prefix));
if (!matches) continue;
const value = env[key];
// Skip undefined / non-string values. `process.env` only ever returns
// strings, but `Record<string, string | undefined>` lets tests pass an
// env object with explicit `undefined` slots (e.g. after `delete`).
if (typeof value !== "string") continue;
snapshot[key] = value;
}
return snapshot;
}
14 changes: 12 additions & 2 deletions packages/producer/src/services/render/stages/freezePlan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,13 +89,23 @@ export interface FreezePlanResult {
planHash: string;
}

/**
* Re-export the runtime-env snapshot helper for backward compatibility with
* earlier imports from `./freezePlan`. The implementation lives in
* `../runtimeEnvSnapshot.ts` — chunk workers re-apply the snapshot during
* boot, so it needs to be importable without dragging in the freeze pipeline.
*/
export { snapshotRuntimeEnv, RUNTIME_ENV_PREFIXES } from "../runtimeEnvSnapshot.js";

/**
* Freeze a plan directory: write `meta/*.json` + top-level `plan.json`, then
* compute `planHash` over the canonicalized contents.
*
* Skeleton — body lands when the distributed-render primitives compose the
* stage functions.
* stage functions. The body will resolve `input.encoder.runtimeEnv ||=
* snapshotRuntimeEnv()` so callers can optionally pre-populate the field,
* with the live env as the default.
*/
export async function freezePlan(_input: FreezePlanInput): Promise<FreezePlanResult> {
throw new Error("freezePlan is not implemented yet — see DISTRIBUTED-RENDERING-PLAN.md §11.");
throw new Error("freezePlan is not implemented yet.");
}
Loading