From 1899b5f6ae34f7fa5ffeb0cad92d43c191b31460 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 18 Oct 2026 03:28:59 -0500 Subject: [PATCH] Key the differential oracle cache on the inherited environment The Node oracle runs with the harness's environment, but its cached verdict was keyed only on SCRIPTC_TEST_ENV among the variables. A program whose output reads NODE_ENV or another variable reused a verdict recorded under a different value. The key now covers every inherited variable except the test runner's worker numbers, which differ between workers of one run. Closes #51 Co-authored-by: Patrick Wozniak <4481570+patlux@users.noreply.github.com> --- tests/harness/differential.test.ts | 7 ++++--- tests/harness/oracle-env.test.ts | 25 +++++++++++++++++++++++++ tests/harness/oracle-env.ts | 15 +++++++++++++++ 3 files changed, 44 insertions(+), 3 deletions(-) create mode 100644 tests/harness/oracle-env.test.ts create mode 100644 tests/harness/oracle-env.ts diff --git a/tests/harness/differential.test.ts b/tests/harness/differential.test.ts index fbf9cb1ec..a2206fd97 100644 --- a/tests/harness/differential.test.ts +++ b/tests/harness/differential.test.ts @@ -22,6 +22,7 @@ import { describe, expect, test } from "vitest"; import ts5 from "typescript"; import { compile } from "@scriptc/compiler"; import { corpusSelect, shardSuffix } from "./shard.js"; +import { oracleEnvKey } from "./oracle-env.js"; const execFileAsync = promisify(execFile); const repoRoot = join(import.meta.dirname, "../.."); @@ -238,8 +239,8 @@ function programInputs(file: string): string[] { // Node's verdict for a corpus program is a pure function of the program bytes, // the shims, and the Node build (corpus stdout is deterministic by // construction — it must be identical to a non-Node native binary's output). So -// cache it, keyed by all of those plus the invocation shape (SCRIPTC_TEST_ENV -// and the cwd). Only the SPAWN is skipped: the native side always runs live +// cache it, keyed by all of those plus the invocation shape (the inherited +// environment, see oracle-env.ts, and the cwd). Only the SPAWN is skipped: the native side always runs live // and the comparison itself never changes. SCRIPTC_NO_CACHE=1 (or an unset // SCRIPTC_CACHE_DIR) disables the cache in both directions — no reads, no writes. // Storage shares the compile cache's root and its LRU sweep (see native-toolchain.ts). @@ -265,7 +266,7 @@ function oracleKeyBase(): Promise { .update(readFileSync(fileURLToPath(comptimeShim))) .update("\0") .update("\0") - .update(process.env["SCRIPTC_TEST_ENV"] ?? "") + .update(oracleEnvKey(process.env)) .update("\0") .update(process.cwd()) .update("\0") diff --git a/tests/harness/oracle-env.test.ts b/tests/harness/oracle-env.test.ts new file mode 100644 index 000000000..da09dde25 --- /dev/null +++ b/tests/harness/oracle-env.test.ts @@ -0,0 +1,25 @@ +import { expect, test } from "vitest"; +import { oracleEnvKey } from "./oracle-env.js"; + +test("the oracle key changes with any inherited variable", () => { + const base = { PATH: "/bin", SCRIPTC_TEST_ENV: "from-harness" }; + expect(oracleEnvKey({ ...base, NODE_ENV: "production" })).not.toBe( + oracleEnvKey({ ...base, NODE_ENV: "test" }), + ); + expect(oracleEnvKey({ ...base, NODE_ENV: "test" })).not.toBe(oracleEnvKey(base)); + expect(oracleEnvKey({ ...base, FOO: "a=b" })).not.toBe( + oracleEnvKey({ ...base, FOO: "a", B: "" }), + ); +}); + +test("the oracle key ignores the runner's worker numbers and variable order", () => { + expect(oracleEnvKey({ A: "1", B: "2", VITEST_WORKER_ID: "1", VITEST_POOL_ID: "1" })).toBe( + oracleEnvKey({ + VITEST_POOL_ID: "7", + B: "2", + TINYPOOL_WORKER_ID: "3", + A: "1", + VITEST_WORKER_ID: "9", + }), + ); +}); diff --git a/tests/harness/oracle-env.ts b/tests/harness/oracle-env.ts new file mode 100644 index 000000000..5f66f26bb --- /dev/null +++ b/tests/harness/oracle-env.ts @@ -0,0 +1,15 @@ +/* The environment part of the differential oracle cache key. The Node + * oracle runs with the harness's inherited environment, so a program whose + * output reads a variable (NODE_ENV, SCRIPTC_TEST_ENV, ...) must not reuse a + * verdict recorded under a different value. The variables that number the + * test runner's own workers differ between workers of one run and are left + * out, so every worker shares the cache. */ +const WORKER_VARIABLES = new Set(["VITEST_POOL_ID", "VITEST_WORKER_ID", "TINYPOOL_WORKER_ID"]); + +export function oracleEnvKey(env: NodeJS.ProcessEnv): string { + return Object.keys(env) + .filter((name) => !WORKER_VARIABLES.has(name) && env[name] !== undefined) + .sort() + .map((name) => `${name}=${env[name]}\0`) + .join(""); +}