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
22 changes: 11 additions & 11 deletions tests/harness/differential.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { promisify } from "node:util";
import { describe, expect, test } from "vitest";
import ts5 from "typescript";
import { compile } from "@scriptc/compiler";
import { oracleCacheKeyBase } from "./oracle-environment.js";
import { shardSelect, shardSuffix } from "./shard.js";

const execFileAsync = promisify(execFile);
Expand Down Expand Up @@ -223,8 +224,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 match a non-Node native binary byte-for-byte). 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 complete
// inherited environment 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 cc.ts).
Expand All @@ -239,17 +240,16 @@ function oracleKeyBase(): Promise<string> {
// The spawned `node` comes from PATH, so ask IT for its version rather than
// trusting process.version (vitest's own node could differ).
oracleKeyBaseMemo ??= execFileAsync("node", ["--version"]).then(({ stdout }) =>
createHash("sha256")
.update("oracle-v1\0")
.update(stdout.trim()).update("\0")
oracleCacheKeyBase({
nodeVersion: stdout.trim(),
// Decorator programs run tsc's downlevel on the Node side — its
// emitter version is part of the verdict.
.update(ts5.version).update("\0")
.update(readFileSync(fileURLToPath(comptimeShim))).update("\0")
.update(readFileSync(fileURLToPath(islandShim))).update("\0")
.update(process.env["SCRIPTC_TEST_ENV"] ?? "").update("\0")
.update(process.cwd()).update("\0")
.digest("hex"),
typescriptVersion: ts5.version,
comptimeShim: readFileSync(fileURLToPath(comptimeShim), "utf8"),
islandShim: readFileSync(fileURLToPath(islandShim), "utf8"),
environment: process.env,
cwd: process.cwd(),
}),
);
return oracleKeyBaseMemo;
}
Expand Down
46 changes: 46 additions & 0 deletions tests/harness/oracle-environment.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { expect, test } from "vitest";
import { oracleCacheKeyBase, oracleEnvironmentFingerprint } from "./oracle-environment.js";

test("oracle environment fingerprint covers arbitrary output-affecting variables", () => {
const base = oracleEnvironmentFingerprint({ NODE_ENV: "development", SCRIPTC_NEVER: "no" });

expect(oracleEnvironmentFingerprint({ NODE_ENV: "production", SCRIPTC_NEVER: "no" })).not.toBe(base);
expect(oracleEnvironmentFingerprint({ NODE_ENV: "development", SCRIPTC_NEVER: "yes" })).not.toBe(base);
expect(oracleEnvironmentFingerprint({ NODE_ENV: "development", SCRIPTC_NEVER: "no", EXTRA: "value" })).not.toBe(base);
});

test("oracle environment fingerprint is independent of insertion order", () => {
expect(oracleEnvironmentFingerprint({ NODE_ENV: "production", PATH: "/bin", EMPTY: "" })).toBe(
oracleEnvironmentFingerprint({ EMPTY: "", PATH: "/bin", NODE_ENV: "production" }),
);
});

test("oracle environment fingerprint distinguishes missing, unset, and empty variables", () => {
expect(oracleEnvironmentFingerprint({})).not.toBe(oracleEnvironmentFingerprint({ VALUE: undefined }));
expect(oracleEnvironmentFingerprint({ VALUE: undefined })).not.toBe(
oracleEnvironmentFingerprint({ VALUE: "" }),
);
});

test("oracle environment fingerprint length-frames keys and values", () => {
expect(oracleEnvironmentFingerprint({ "A:B": "C;D" })).not.toBe(
oracleEnvironmentFingerprint({ A: "B:C;D" }),
);
});

test("oracle cache key invalidates when corpus output-affecting variables change", () => {
const inputs = {
nodeVersion: "v24.0.0",
typescriptVersion: "5.9.0",
comptimeShim: "comptime",
islandShim: "island",
cwd: "/repo",
};
const base = oracleCacheKeyBase({
...inputs,
environment: { NODE_ENV: "development", SCRIPTC_NEVER: "no" },
});

expect(oracleCacheKeyBase({ ...inputs, environment: { NODE_ENV: "production", SCRIPTC_NEVER: "no" } })).not.toBe(base);
expect(oracleCacheKeyBase({ ...inputs, environment: { NODE_ENV: "development", SCRIPTC_NEVER: "yes" } })).not.toBe(base);
});
42 changes: 42 additions & 0 deletions tests/harness/oracle-environment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { createHash } from "node:crypto";

/**
* The complete inherited environment visible to the Node oracle. Corpus
* programs may read arbitrary process.env keys directly or through imported
* modules, so an allowlist cannot soundly describe this input. Keys sort by
* UTF-16 code unit for a deterministic order; names and values are
* length-framed so missing, empty, and delimiter-containing entries remain
* distinct.
*/
export function oracleEnvironmentFingerprint(env: NodeJS.ProcessEnv): string {
return Object.keys(env)
.sort((a, b) => a < b ? -1 : a > b ? 1 : 0)
.map((key) => {
const value = env[key];
const framedValue = value === undefined ? "unset" : `${value.length}:${value}`;
return `${key.length}:${key}:${framedValue};`;
})
.join("");
}

interface OracleCacheKeyBaseInputs {
nodeVersion: string;
typescriptVersion: string;
comptimeShim: string;
islandShim: string;
environment: NodeJS.ProcessEnv;
cwd: string;
}

/** The shared, testable base of every per-program Node oracle cache key. */
export function oracleCacheKeyBase(inputs: OracleCacheKeyBaseInputs): string {
return createHash("sha256")
.update("oracle-v3\0")
.update(inputs.nodeVersion).update("\0")
.update(inputs.typescriptVersion).update("\0")
.update(inputs.comptimeShim).update("\0")
.update(inputs.islandShim).update("\0")
.update(oracleEnvironmentFingerprint(inputs.environment)).update("\0")
.update(inputs.cwd).update("\0")
.digest("hex");
}