From 6abb1802a7ea57fc5080c4b87d0012b20866634f Mon Sep 17 00:00:00 2001 From: filipeforattini Date: Tue, 1 Sep 2026 23:53:41 -0300 Subject: [PATCH 1/2] fix: pin the semantic oracle in every harness suite that spawns node raw MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit npm.test.ts and npm-static.test.ts (PR #15's gap) spawned a bare "node" resolved by whatever PATH the runner happened to have, instead of primaryOracleExecutable(NODE_COMPAT_MATRIX) — the resolver differential.test.ts and friends have used since PR #14 to keep the semantic oracle pinned to the compat matrix's primary Node regardless of which Node is running the suite. fetch.test.ts, server.test.ts, dgram.test.ts, node-test.test.ts, console-io.test.ts and event-loop.test.ts had the identical gap. All now resolve their oracle through primaryOracleExecutable. The client drivers spawned inside server.test.ts and dgram.test.ts stay plain "node" deliberately: they are the identical fixed workload run against both lanes, not themselves an oracle comparison subject. Verified under Node 26 (npm.test.ts's commander lane still resolves the oracle to the pinned Node 24) and under gate:node-matrix (both Nodes). Claude-Session: https://claude.ai/code/session_01L4tTZUEZzWnw3rHKVQDTMn --- tests/harness/console-io.test.ts | 12 ++++++++--- tests/harness/dgram.test.ts | 12 +++++++++-- tests/harness/event-loop.test.ts | 10 +++++++-- tests/harness/fetch.test.ts | 36 +++++++++++++++++++------------- tests/harness/node-test.test.ts | 10 +++++++-- tests/harness/npm-static.test.ts | 20 +++++++++++------- tests/harness/npm.test.ts | 11 ++++++++-- tests/harness/server.test.ts | 12 +++++++++-- 8 files changed, 88 insertions(+), 35 deletions(-) diff --git a/tests/harness/console-io.test.ts b/tests/harness/console-io.test.ts index 5377c27fd..8c4622674 100644 --- a/tests/harness/console-io.test.ts +++ b/tests/harness/console-io.test.ts @@ -12,12 +12,18 @@ import { createHash } from "node:crypto"; import { mkdirSync, readFileSync } from "node:fs"; import { join } from "node:path"; import { describe, expect, test } from "vitest"; -import { compile } from "@scriptc/compiler"; +import { NODE_COMPAT_MATRIX, compile } from "@scriptc/compiler"; +import { primaryOracleExecutable } from "./node-matrix.js"; const repoRoot = join(import.meta.dirname, "../.."); const fixtureDir = join(repoRoot, "tests/fixtures/console-io"); const cacheDir = join(repoRoot, "node_modules/.cache/scriptc-tests"); const sanitize = process.env["SCRIPTC_SAN"] === "1"; +// SEMANTIC oracle (differential.test.ts's rationale, node-matrix.ts's +// header): the probes must match ONE Node's fixed byte behavior, so this +// pins to the compat matrix primary rather than whichever `node` the +// PATH happens to resolve. +const oracleExecutable = primaryOracleExecutable(NODE_COMPAT_MATRIX); interface ClosedChild { stdout: Buffer; @@ -131,7 +137,7 @@ describe(`console/process output visibility${sanitize ? " (sanitized)" : ""}`, ( const probe = await build("live-stdout"); const [nodeOut, nativeOut] = await Promise.all([ - observeLiveStdout("node", [probe.sourceFile], expected), + observeLiveStdout(oracleExecutable, [probe.sourceFile], expected), observeLiveStdout(probe.binary, [], expected), ]); expect(nodeOut.subarray(0, expected.length)).toEqual(expected); @@ -143,7 +149,7 @@ describe(`console/process output visibility${sanitize ? " (sanitized)" : ""}`, ( const probe = await build("sigkill-stdout"); const [nodeRes, nativeRes] = await Promise.all([ - runToClose("node", [probe.sourceFile]), + runToClose(oracleExecutable, [probe.sourceFile]), runToClose(probe.binary, []), ]); expect(nodeRes.stdout).toEqual(expected); diff --git a/tests/harness/dgram.test.ts b/tests/harness/dgram.test.ts index 95597778f..749b38015 100644 --- a/tests/harness/dgram.test.ts +++ b/tests/harness/dgram.test.ts @@ -14,12 +14,20 @@ import { createHash } from "node:crypto"; import { existsSync, globSync, mkdirSync, readFileSync } from "node:fs"; import { join } from "node:path"; import { describe, expect, test } from "vitest"; -import { compile } from "@scriptc/compiler"; +import { NODE_COMPAT_MATRIX, compile } from "@scriptc/compiler"; +import { primaryOracleExecutable } from "./node-matrix.js"; const repoRoot = join(import.meta.dirname, "../.."); const fixturesRoot = join(repoRoot, "tests/fixtures/dgram"); const cacheDir = join(repoRoot, "node_modules/.cache/scriptc-tests"); const sanitize = process.env["SCRIPTC_SAN"] === "1"; +// SEMANTIC oracle (differential.test.ts's rationale, node-matrix.ts's +// header, server.test.ts's identical pattern): the fixture's stdout/exit +// code must match ONE Node's fixed behavior, so this pins to the compat +// matrix primary rather than whichever `node` the PATH happens to +// resolve. The driver spawned inside runLane stays plain "node" — it is +// the identical fixed workload on both lanes, not an oracle subject. +const oracleExecutable = primaryOracleExecutable(NODE_COMPAT_MATRIX); interface ProgramRun { stdout: Buffer; @@ -115,7 +123,7 @@ describe(`dgram differential (${cases.length} programs${sanitize ? ", sanitized" const binary = await build(c.entry); // Sequential, not parallel: both lanes bind ephemeral ports and drive // real sockets — parallelism buys little and interleaves kernel state. - const nodeRes = await runLane("node", [c.entry], c.driver); + const nodeRes = await runLane(oracleExecutable, [c.entry], c.driver); const nativeRes = await runLane(binary, [], c.driver); expect(nativeRes.stdout.toString("utf8")).toBe(nodeRes.stdout.toString("utf8")); if (!nodeRes.stdout.equals(nativeRes.stdout)) { diff --git a/tests/harness/event-loop.test.ts b/tests/harness/event-loop.test.ts index 94f7c309f..eae942165 100644 --- a/tests/harness/event-loop.test.ts +++ b/tests/harness/event-loop.test.ts @@ -11,13 +11,19 @@ import { createHash } from "node:crypto"; import { mkdirSync, readFileSync } from "node:fs"; import { join } from "node:path"; import { describe, expect, test } from "vitest"; -import { compile } from "@scriptc/compiler"; +import { NODE_COMPAT_MATRIX, compile } from "@scriptc/compiler"; import { eventLoopCases, type StdinScript } from "./event-loop-cases.js"; +import { primaryOracleExecutable } from "./node-matrix.js"; const repoRoot = join(import.meta.dirname, "../.."); const fixtureDir = join(repoRoot, "tests/fixtures/event-loop"); const cacheDir = join(repoRoot, "node_modules/.cache/scriptc-tests"); const sanitize = process.env["SCRIPTC_SAN"] === "1"; +// SEMANTIC oracle (differential.test.ts's rationale, node-matrix.ts's +// header): stdout/exit code must match ONE Node's fixed behavior, so +// this pins to the compat matrix primary rather than whichever `node` +// the PATH happens to resolve. +const oracleExecutable = primaryOracleExecutable(NODE_COMPAT_MATRIX); interface RunResult { stdout: string; @@ -76,7 +82,7 @@ async function compileFixture(name: string): Promise { async function differential(fixture: string, script: StdinScript): Promise { const binary = await compileFixture(fixture); const [nodeRes, nativeRes] = await Promise.all([ - runWithStdin("node", [join(fixtureDir, fixture)], script), + runWithStdin(oracleExecutable, [join(fixtureDir, fixture)], script), runWithStdin(binary, [], script), ]); expect(nativeRes.stdout).toBe(nodeRes.stdout); diff --git a/tests/harness/fetch.test.ts b/tests/harness/fetch.test.ts index 69862cf9b..f0226da09 100644 --- a/tests/harness/fetch.test.ts +++ b/tests/harness/fetch.test.ts @@ -27,17 +27,23 @@ import { createServer as createHttpsServer } from "node:https"; import { join } from "node:path"; import { promisify } from "node:util"; import { afterAll, beforeAll, describe, expect, test } from "vitest"; -import { compile } from "@scriptc/compiler"; +import { NODE_COMPAT_MATRIX, compile } from "@scriptc/compiler"; // The servers live in the fixture tree (a plain .mjs): the Linux lane runs // the IDENTICAL routes standalone inside its container. // eslint-disable-next-line import/no-relative-packages import { startFetchServers } from "../fixtures/fetch/servers.mjs"; +import { primaryOracleExecutable } from "./node-matrix.js"; const execFileAsync = promisify(execFile); const repoRoot = join(import.meta.dirname, "../.."); const fixturesRoot = join(repoRoot, "tests/fixtures/fetch"); const cacheDir = join(repoRoot, "node_modules/.cache/scriptc-tests"); const sanitize = process.env["SCRIPTC_SAN"] === "1"; +// SEMANTIC oracle (differential.test.ts's rationale, node-matrix.ts's +// header): stdout must match ONE Node's fixed behavior, so this pins to +// the compat matrix primary rather than whichever `node` the PATH happens +// to resolve. +const oracleExecutable = primaryOracleExecutable(NODE_COMPAT_MATRIX); /* ── the local servers (tests/fixtures/fetch/servers.mjs: all routes, the * refused port, and the counting forward proxy for the NODE_USE_ENV_PROXY @@ -201,7 +207,7 @@ describe(`static fetch differential${sanitize ? " (sanitized)" : ""}`, () => { const binary = await buildStatic(entry, backend); const redirectKey = `${name}-${backend}`; const [nodeRes, nativeRes] = await Promise.all([ - runBinary("node", [entry, baseUrl, `${redirectKey}-node`]), + runBinary(oracleExecutable, [entry, baseUrl, `${redirectKey}-node`]), runBinary(binary, [baseUrl, `${redirectKey}-native`]), ]); expect(nativeRes.stdout.toString("utf8")).toBe( @@ -216,7 +222,7 @@ describe(`static fetch differential${sanitize ? " (sanitized)" : ""}`, () => { const entry = join(fixturesRoot, "static-abandon/main.mts"); const binary = await buildStatic(entry, backend); const [nodeRes, nativeRes] = await Promise.all([ - runBinary("node", [entry, baseUrl], undefined, 3_000), + runBinary(oracleExecutable, [entry, baseUrl], undefined, 3_000), runBinary(binary, [baseUrl], undefined, 3_000), ]); expect(nativeRes.stdout.equals(nodeRes.stdout)).toBe(true); @@ -254,7 +260,7 @@ describe(`static fetch differential${sanitize ? " (sanitized)" : ""}`, () => { NODE_EXTRA_CA_CERTS: join(certs, "ca.pem"), }; const [nodeRes, nativeRes] = await Promise.all([ - runBinary("node", [entry, ...argv], env), + runBinary(oracleExecutable, [entry, ...argv], env), runBinary(binary, argv, env), ]); expect(nativeRes.stdout.toString("utf8")).toBe( @@ -323,7 +329,7 @@ describe(`static fetch differential${sanitize ? " (sanitized)" : ""}`, () => { } const before = servers.proxiedRequests(); const [nodeRes, ...nativeResults] = await Promise.all([ - runBinary("node", [entry, ...argv], env), + runBinary(oracleExecutable, [entry, ...argv], env), runBinary(dynamic, argv, env), runBinary(c, argv, env), runBinary(llvm, argv, env), @@ -373,7 +379,7 @@ describe(`static fetch differential${sanitize ? " (sanitized)" : ""}`, () => { }; const before = servers.proxiedRequests(); const [nodeRes, nativeRes] = await Promise.all([ - runBinary("node", [entry, ...argv], env), + runBinary(oracleExecutable, [entry, ...argv], env), runBinary(binary, argv, env), ]); expect(nativeRes.stdout.toString("utf8")).toBe( @@ -459,7 +465,7 @@ describe(`static fetch differential${sanitize ? " (sanitized)" : ""}`, () => { ]); const argv = [blockedUrl, redirectUrl]; const [nodeRes, ...nativeResults] = await Promise.all([ - runBinary("node", [entry, ...argv]), + runBinary(oracleExecutable, [entry, ...argv]), runBinary(dynamic, argv), runBinary(c, argv), runBinary(llvm, argv), @@ -546,7 +552,7 @@ describe(`proxy env opt-in (NODE_USE_ENV_PROXY${sanitize ? ", sanitized" : ""})` const before = servers.proxiedRequests(); const authBefore = servers.proxyAuthorizations().length; const [nodeRes, nativeRes] = await Promise.all([ - runBinary("node", [entry, ...argv], env), + runBinary(oracleExecutable, [entry, ...argv], env), runBinary(binary, argv, env), ]); expect(nativeRes.stdout.toString("utf8")).toBe(nodeRes.stdout.toString("utf8")); @@ -574,7 +580,7 @@ describe(`proxy env opt-in (NODE_USE_ENV_PROXY${sanitize ? ", sanitized" : ""})` const before = servers.proxiedRequests(); const authBefore = servers.proxyAuthorizations().length; const [nodeRes, nativeRes] = await Promise.all([ - runBinary("node", [entry, baseUrl], env), + runBinary(oracleExecutable, [entry, baseUrl], env), runBinary(binary, [baseUrl], env), ]); expect(nativeRes.stdout.equals(nodeRes.stdout)).toBe(true); @@ -611,7 +617,7 @@ describe(`proxy env opt-in (NODE_USE_ENV_PROXY${sanitize ? ", sanitized" : ""})` const before = servers.proxiedRequests(); const authBefore = servers.proxyAuthorizations().length; const [nodeRes, nativeRes] = await Promise.all([ - runBinary("node", [entry, baseUrl], env), + runBinary(oracleExecutable, [entry, baseUrl], env), runBinary(binary, [baseUrl], env), ]); expect(nativeRes.stdout.equals(nodeRes.stdout)).toBe(true); @@ -669,7 +675,7 @@ describe(`proxy env opt-in (NODE_USE_ENV_PROXY${sanitize ? ", sanitized" : ""})` NO_PROXY: "", }; const [nodeRes, ...nativeResults] = await Promise.all([ - runBinary("node", [entry, ...argv], env), + runBinary(oracleExecutable, [entry, ...argv], env), runBinary(dynamic, argv, env), runBinary(c, argv, env), runBinary(llvm, argv, env), @@ -705,7 +711,7 @@ describe(`proxy env opt-in (NODE_USE_ENV_PROXY${sanitize ? ", sanitized" : ""})` NO_PROXY: "", }; const [nodeRes, nativeRes] = await Promise.all([ - runBinary("node", [entry, baseUrl], env), + runBinary(oracleExecutable, [entry, baseUrl], env), runBinary(binary, [baseUrl], env), ]); expect(nativeRes.stdout.equals(nodeRes.stdout)).toBe(true); @@ -729,7 +735,7 @@ describe(`proxy env opt-in (NODE_USE_ENV_PROXY${sanitize ? ", sanitized" : ""})` }; const before = servers.proxiedRequests(); const [nodeRes, nativeRes] = await Promise.all([ - runBinary("node", [entry, ...argv], env), + runBinary(oracleExecutable, [entry, ...argv], env), runBinary(binary, argv, env), ]); expect(nativeRes.stdout.toString("utf8")).toBe( @@ -755,7 +761,7 @@ describe(`proxy env opt-in (NODE_USE_ENV_PROXY${sanitize ? ", sanitized" : ""})` }; const before = servers.proxiedRequests(); const [nodeRes, nativeRes] = await Promise.all([ - runBinary("node", [entry, ...argv], env), + runBinary(oracleExecutable, [entry, ...argv], env), runBinary(binary, argv, env), ]); expect(nativeRes.stdout.toString("utf8")).toBe( @@ -781,7 +787,7 @@ describe(`fetch differential (${cases.length} programs${sanitize ? ", sanitized" ? [...argv, "redirect-resolution-native"] : argv; const [nodeRes, nativeRes] = await Promise.all([ - runBinary("node", [c.entry, ...nodeArgv]), + runBinary(oracleExecutable, [c.entry, ...nodeArgv]), runBinary(binary, nativeArgv), ]); if (!nodeRes.stdout.equals(nativeRes.stdout)) { diff --git a/tests/harness/node-test.test.ts b/tests/harness/node-test.test.ts index 9273b363a..1883d93b7 100644 --- a/tests/harness/node-test.test.ts +++ b/tests/harness/node-test.test.ts @@ -28,13 +28,19 @@ import { createHash } from "node:crypto"; import { globSync, mkdirSync, readFileSync } from "node:fs"; import { join } from "node:path"; import { describe, expect, test } from "vitest"; -import { compile } from "@scriptc/compiler"; +import { NODE_COMPAT_MATRIX, compile } from "@scriptc/compiler"; +import { primaryOracleExecutable } from "./node-matrix.js"; import { normalizeNodeTestOutput } from "./node-test-normalize.js"; const repoRoot = join(import.meta.dirname, "../.."); const fixturesRoot = join(repoRoot, "tests/fixtures/node-test"); const cacheDir = join(repoRoot, "node_modules/.cache/scriptc-tests"); const sanitize = process.env["SCRIPTC_SAN"] === "1"; +// SEMANTIC oracle (differential.test.ts's rationale, node-matrix.ts's +// header): the reporter's stdout/exit code must match ONE Node's fixed +// behavior, so this pins to the compat matrix primary rather than +// whichever `node` the PATH happens to resolve. +const oracleExecutable = primaryOracleExecutable(NODE_COMPAT_MATRIX); interface ProgramRun { stdout: string; @@ -109,7 +115,7 @@ describe(`node:test differential (${cases.length} programs${sanitize ? ", saniti test.for(cases.map((c) => [c.name, c] as const))("%s", async ([, c]) => { const binary = await build(c.entry); const [nodeRes, nativeRes] = await Promise.all([ - runLane("node", [c.entry]), + runLane(oracleExecutable, [c.entry]), runLane(binary, []), ]); expect(normalize(nativeRes.stdout)).toBe(normalize(nodeRes.stdout)); diff --git a/tests/harness/npm-static.test.ts b/tests/harness/npm-static.test.ts index 06729bade..9b138dfa1 100644 --- a/tests/harness/npm-static.test.ts +++ b/tests/harness/npm-static.test.ts @@ -22,7 +22,8 @@ import { globSync, mkdirSync, readFileSync } from "node:fs"; import { join } from "node:path"; import { promisify } from "node:util"; import { describe, expect, test } from "vitest"; -import { analyze, compile } from "@scriptc/compiler"; +import { NODE_COMPAT_MATRIX, analyze, compile } from "@scriptc/compiler"; +import { primaryOracleExecutable } from "./node-matrix.js"; const execFileAsync = promisify(execFile); const repoRoot = join(import.meta.dirname, "../.."); @@ -30,6 +31,11 @@ const fixturesRoot = join(repoRoot, "tests/fixtures"); const pilotRoot = join(fixturesRoot, "npm-static"); const cacheDir = join(repoRoot, "node_modules/.cache/scriptc-tests"); const sanitize = process.env["SCRIPTC_SAN"] === "1"; +// SEMANTIC oracle (differential.test.ts's rationale, node-matrix.ts's +// header): stdout must match ONE Node's fixed behavior, so this pins to +// the compat matrix primary rather than whichever `node` the PATH happens +// to resolve. +const oracleExecutable = primaryOracleExecutable(NODE_COMPAT_MATRIX); interface RunResult { stdout: Buffer; @@ -110,7 +116,7 @@ describe(`npm-static pilots${sanitize ? " (sanitized)" : ""}`, () => { const entry = join(pilotRoot, file); const binary = await buildStatic(entry, [pkg]); const [nodeRes, nativeRes] = await Promise.all([ - runBinary("node", [entry]), + runBinary(oracleExecutable, [entry]), runBinary(binary, []), ]); expect(nativeRes.stdout.toString("utf8")).toBe(nodeRes.stdout.toString("utf8")); @@ -150,7 +156,7 @@ describe(`npm-static pilots${sanitize ? " (sanitized)" : ""}`, () => { expect(coverage.stats.statementsFailed).toBe(0); const binary = await buildStatic(entry, "auto"); const [nodeRes, nativeRes] = await Promise.all([ - runBinary("node", [entry]), + runBinary(oracleExecutable, [entry]), runBinary(binary, []), ]); expect(nativeRes.stdout.toString("utf8")).toBe(nodeRes.stdout.toString("utf8")); @@ -327,7 +333,7 @@ describe(`npm-static pilots${sanitize ? " (sanitized)" : ""}`, () => { const result = await compile(entry, { outPath: join(outDir, "program"), outDir, sanitize, npmStatic: ["wslinked"] }); if (!result.ok) throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); const [nodeRes, nativeRes] = await Promise.all([ - runBinary("node", [entry]), + runBinary(oracleExecutable, [entry]), runBinary(result.binaryPath, []), ]); expect(nativeRes.stdout.toString("utf8")).toBe(nodeRes.stdout.toString("utf8")); @@ -373,7 +379,7 @@ describe(`npm-static pilots${sanitize ? " (sanitized)" : ""}`, () => { expect(coverage.diagnostics).toHaveLength(0); // builds — fences are runtime const binary = await buildStatic(entry, [pkg]); const [nodeRes, nativeRes] = await Promise.all([ - runBinary("node", [entry]), + runBinary(oracleExecutable, [entry]), runBinary(binary, []), ]); expect(nativeRes.stdout.toString("utf8")).toBe(nodeRes.stdout.toString("utf8")); @@ -405,7 +411,7 @@ describe(`npm-static pilots${sanitize ? " (sanitized)" : ""}`, () => { expect(coverage.diagnostics).toHaveLength(0); const binary = await buildStatic(entry, ["gtwrap", "gtcore", "gtable"]); const [nodeRes, nativeRes] = await Promise.all([ - runBinary("node", [entry]), + runBinary(oracleExecutable, [entry]), runBinary(binary, []), ]); expect(nativeRes.stdout.toString("utf8")).toBe(nodeRes.stdout.toString("utf8")); @@ -529,7 +535,7 @@ describe(`npm-static pilots${sanitize ? " (sanitized)" : ""}`, () => { expect(coverage.runtimeFences ?? []).toHaveLength(0); const binary = await buildStatic(entry, ["jsonzoo"]); const [nodeRes, nativeRes] = await Promise.all([ - runBinary("node", [entry]), + runBinary(oracleExecutable, [entry]), runBinary(binary, []), ]); expect(nativeRes.stdout.toString("utf8")).toBe(nodeRes.stdout.toString("utf8")); diff --git a/tests/harness/npm.test.ts b/tests/harness/npm.test.ts index e5217e22b..a5fe1f178 100644 --- a/tests/harness/npm.test.ts +++ b/tests/harness/npm.test.ts @@ -19,8 +19,9 @@ import { globSync, mkdirSync, readFileSync, realpathSync } from "node:fs"; import { join } from "node:path"; import { promisify } from "node:util"; import { describe, expect, test } from "vitest"; -import { compile } from "@scriptc/compiler"; +import { NODE_COMPAT_MATRIX, compile } from "@scriptc/compiler"; import { npmCases } from "./npm-cases.js"; +import { primaryOracleExecutable } from "./node-matrix.js"; import { shardSelect, shardSuffix } from "./shard.js"; const execFileAsync = promisify(execFile); @@ -28,6 +29,12 @@ const repoRoot = join(import.meta.dirname, "../.."); const fixturesRoot = join(repoRoot, "tests/fixtures"); const cacheDir = join(repoRoot, "node_modules/.cache/scriptc-tests"); const sanitize = process.env["SCRIPTC_SAN"] === "1"; +// SEMANTIC oracle (differential.test.ts's rationale, node-matrix.ts's +// header): stdout must match ONE Node's fixed behavior, so this pins to +// the compat matrix primary rather than whichever `node` the PATH happens +// to resolve — a bare "node" spawn would silently compare against +// whatever major is running the suite. +const oracleExecutable = primaryOracleExecutable(NODE_COMPAT_MATRIX); /* SCRIPTC_TEST_BACKEND pins the lane. Unset — the normal run, and what CI * gates on — this suite rides the RELEASE DEFAULT, because embedded npm * tables are production surface and this differential is what keeps @@ -187,7 +194,7 @@ describe(`npm differential (${cases.length} programs${sanitize ? ", sanitized" : const binary = await build(c.entry); for (const argv of c.argvs ?? [[]]) { const [nodeRes, nativeRes] = await Promise.all([ - runBinary("node", [c.entry, ...argv]), + runBinary(oracleExecutable, [c.entry, ...argv]), runBinary(binary, argv), ]); const label = argv.join(" "); diff --git a/tests/harness/server.test.ts b/tests/harness/server.test.ts index 41a1b23a7..6e0a0cf45 100644 --- a/tests/harness/server.test.ts +++ b/tests/harness/server.test.ts @@ -25,13 +25,21 @@ import { createHash } from "node:crypto"; import { existsSync, globSync, mkdirSync, readFileSync } from "node:fs"; import { join } from "node:path"; import { describe, expect, test } from "vitest"; -import { compile } from "@scriptc/compiler"; +import { NODE_COMPAT_MATRIX, compile } from "@scriptc/compiler"; +import { primaryOracleExecutable } from "./node-matrix.js"; import { shardSelect, shardSuffix } from "./shard.js"; const repoRoot = join(import.meta.dirname, "../.."); const fixturesRoot = join(repoRoot, "tests/fixtures/server"); const cacheDir = join(repoRoot, "node_modules/.cache/scriptc-tests"); const sanitize = process.env["SCRIPTC_SAN"] === "1"; +// SEMANTIC oracle (differential.test.ts's rationale, node-matrix.ts's +// header): the server program's stdout/exit code must match ONE Node's +// fixed behavior, so this pins to the compat matrix primary rather than +// whichever `node` the PATH happens to resolve. The client driver spawned +// inside runLane stays plain "node" — it is the identical fixed workload +// on both lanes, not itself an oracle comparison subject. +const oracleExecutable = primaryOracleExecutable(NODE_COMPAT_MATRIX); interface ProgramRun { stdout: Buffer; @@ -134,7 +142,7 @@ describe(`server differential (${cases.length} programs${sanitize ? ", sanitized const binary = await build(c.entry); // Sequential, not parallel: both lanes bind ephemeral ports and drive // real sockets — parallelism buys little and interleaves kernel state. - const nodeRes = await runLane("node", [c.entry], c.driver); + const nodeRes = await runLane(oracleExecutable, [c.entry], c.driver); const nativeRes = await runLane(binary, [], c.driver); expect(nativeRes.stdout.toString("utf8")).toBe(nodeRes.stdout.toString("utf8")); if (!nodeRes.stdout.equals(nativeRes.stdout)) { From 0e40e7707768c9dbe810292d93f3ba85e848022e Mon Sep 17 00:00:00 2001 From: filipeforattini Date: Tue, 1 Sep 2026 23:53:48 -0300 Subject: [PATCH 2/2] fix: correct the URL setter fence claim from SC1090 to SC0001 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The nine non-pathname URL component setters (href, protocol, username, password, host, hostname, port, search, hash) were classified as SC1090 ("unsupported expression shape") in the WHATWG URL compat profile — the fence for a construct the lowerer refuses because nothing claims it. That was never what actually happens: the ambient .d.ts marks every component but pathname readonly, so `u.protocol = "..."` is a TypeScript type error (SC0001) before the program ever reaches the lowerer. Probed by compiling one assignment per component (the profile's own documented method) against the current ambient — pathname compiles (maintainer commit c6aef3e3 made it the one writable component), and all nine others fail identically with "Cannot assign to '' because it is a read-only property" (SC0001). Updated the profile's compatEntries binding and componentWrite rationale, regenerated surface-manifest.json, and updated url-conformance.test.ts's fence assertions accordingly (its allowed-fence-code list and its "some entry carries the refusal fence" check). Verified: pnpm manifest --check, url-conformance under gate:node-matrix (both Nodes). Claude-Session: https://claude.ai/code/session_01L4tTZUEZzWnw3rHKVQDTMn --- packages/compiler/src/compat/url-profile.ts | 12 ++++--- packages/compiler/surface-manifest.json | 36 ++++++++++----------- tests/harness/url-conformance.test.ts | 8 +++-- 3 files changed, 30 insertions(+), 26 deletions(-) diff --git a/packages/compiler/src/compat/url-profile.ts b/packages/compiler/src/compat/url-profile.ts index 23a9662b5..a411e292e 100644 --- a/packages/compiler/src/compat/url-profile.ts +++ b/packages/compiler/src/compat/url-profile.ts @@ -63,16 +63,18 @@ export interface UrlCompatProfile { const corpus = compatCorpus; /** Most URL refusals are stdlib-surface fences (SC2020). Component WRITES - * are not: nothing claims the assignment, so it is refused as an - * unsupported expression shape (SC1090, 'assignment to non-variables'). - * The fence code is per row, not per profile. */ + * are not: the ambient `.d.ts` marks every component but pathname + * `readonly`, so the assignment never reaches the lowerer — it is a + * TypeScript type error (SC0001, 'Cannot assign ... because it is a + * read-only property'), probed by compiling one assignment per component + * rather than assumed. The fence code is per row, not per profile. */ const { staticEntry, unsupportedEntry, outOfScopeEntry } = compatEntries("SC2020"); -const { unsupportedEntry: unsupportedShapeEntry } = compatEntries("SC1090"); +const { unsupportedEntry: unsupportedShapeEntry } = compatEntries("SC0001"); const islandOnly = "the emulated URL class inside the dynamic engine serves island and npm JS only; a compiled URL value exposes no lowering for this member in either tier"; const componentWrite = - "URL components are read-only in the static tier: there is no component-assignment lowering and no native mutation path behind it"; + "URL components are read-only in the static tier: the ambient .d.ts declares the member readonly, so the assignment is a TypeScript type error before the lowerer ever sees it — there is no component-assignment lowering and no native mutation path behind it"; const iteratorHandle = "materialized iterator objects are not first-class handles; for-of over the params, or directly over keys()/values()/entries(), is the lowered iteration form"; const iteratorHelpers = diff --git a/packages/compiler/surface-manifest.json b/packages/compiler/surface-manifest.json index 826e451cc..6072cac3a 100644 --- a/packages/compiler/surface-manifest.json +++ b/packages/compiler/surface-manifest.json @@ -4177,40 +4177,40 @@ "kind": "stdlib", "name": "URL.hash (setter)", "status": "unsupported", - "code": "SC1090", - "note": "Node 24.15.0; URL components are read-only in the static tier: there is no component-assignment lowering and no native mutation path behind it" + "code": "SC0001", + "note": "Node 24.15.0; URL components are read-only in the static tier: the ambient .d.ts declares the member readonly, so the assignment is a TypeScript type error before the lowerer ever sees it — there is no component-assignment lowering and no native mutation path behind it" }, { "id": "stdlib.url.setter.host", "kind": "stdlib", "name": "URL.host (setter)", "status": "unsupported", - "code": "SC1090", - "note": "Node 24.15.0; URL components are read-only in the static tier: there is no component-assignment lowering and no native mutation path behind it" + "code": "SC0001", + "note": "Node 24.15.0; URL components are read-only in the static tier: the ambient .d.ts declares the member readonly, so the assignment is a TypeScript type error before the lowerer ever sees it — there is no component-assignment lowering and no native mutation path behind it" }, { "id": "stdlib.url.setter.hostname", "kind": "stdlib", "name": "URL.hostname (setter)", "status": "unsupported", - "code": "SC1090", - "note": "Node 24.15.0; URL components are read-only in the static tier: there is no component-assignment lowering and no native mutation path behind it" + "code": "SC0001", + "note": "Node 24.15.0; URL components are read-only in the static tier: the ambient .d.ts declares the member readonly, so the assignment is a TypeScript type error before the lowerer ever sees it — there is no component-assignment lowering and no native mutation path behind it" }, { "id": "stdlib.url.setter.href", "kind": "stdlib", "name": "URL.href (setter)", "status": "unsupported", - "code": "SC1090", - "note": "Node 24.15.0; URL components are read-only in the static tier: there is no component-assignment lowering and no native mutation path behind it" + "code": "SC0001", + "note": "Node 24.15.0; URL components are read-only in the static tier: the ambient .d.ts declares the member readonly, so the assignment is a TypeScript type error before the lowerer ever sees it — there is no component-assignment lowering and no native mutation path behind it" }, { "id": "stdlib.url.setter.password", "kind": "stdlib", "name": "URL.password (setter)", "status": "unsupported", - "code": "SC1090", - "note": "Node 24.15.0; URL components are read-only in the static tier: there is no component-assignment lowering and no native mutation path behind it" + "code": "SC0001", + "note": "Node 24.15.0; URL components are read-only in the static tier: the ambient .d.ts declares the member readonly, so the assignment is a TypeScript type error before the lowerer ever sees it — there is no component-assignment lowering and no native mutation path behind it" }, { "id": "stdlib.url.setter.pathname", @@ -4224,32 +4224,32 @@ "kind": "stdlib", "name": "URL.port (setter)", "status": "unsupported", - "code": "SC1090", - "note": "Node 24.15.0; URL components are read-only in the static tier: there is no component-assignment lowering and no native mutation path behind it" + "code": "SC0001", + "note": "Node 24.15.0; URL components are read-only in the static tier: the ambient .d.ts declares the member readonly, so the assignment is a TypeScript type error before the lowerer ever sees it — there is no component-assignment lowering and no native mutation path behind it" }, { "id": "stdlib.url.setter.protocol", "kind": "stdlib", "name": "URL.protocol (setter)", "status": "unsupported", - "code": "SC1090", - "note": "Node 24.15.0; URL components are read-only in the static tier: there is no component-assignment lowering and no native mutation path behind it" + "code": "SC0001", + "note": "Node 24.15.0; URL components are read-only in the static tier: the ambient .d.ts declares the member readonly, so the assignment is a TypeScript type error before the lowerer ever sees it — there is no component-assignment lowering and no native mutation path behind it" }, { "id": "stdlib.url.setter.search", "kind": "stdlib", "name": "URL.search (setter)", "status": "unsupported", - "code": "SC1090", - "note": "Node 24.15.0; URL components are read-only in the static tier: there is no component-assignment lowering and no native mutation path behind it" + "code": "SC0001", + "note": "Node 24.15.0; URL components are read-only in the static tier: the ambient .d.ts declares the member readonly, so the assignment is a TypeScript type error before the lowerer ever sees it — there is no component-assignment lowering and no native mutation path behind it" }, { "id": "stdlib.url.setter.username", "kind": "stdlib", "name": "URL.username (setter)", "status": "unsupported", - "code": "SC1090", - "note": "Node 24.15.0; URL components are read-only in the static tier: there is no component-assignment lowering and no native mutation path behind it" + "code": "SC0001", + "note": "Node 24.15.0; URL components are read-only in the static tier: the ambient .d.ts declares the member readonly, so the assignment is a TypeScript type error before the lowerer ever sees it — there is no component-assignment lowering and no native mutation path behind it" }, { "id": "stdlib.url.static.canParse", diff --git a/tests/harness/url-conformance.test.ts b/tests/harness/url-conformance.test.ts index 591d9ac2c..83f30dffb 100644 --- a/tests/harness/url-conformance.test.ts +++ b/tests/harness/url-conformance.test.ts @@ -102,8 +102,10 @@ describe("URL compatibility profile", () => { expect(entry.reason, `${entry.id}: static rows are explained by evidence`).toBeUndefined(); } else if (entry.status === "dynamic-only" || entry.status === "unsupported") { // The fence is per row: member-shaped refusals raise the stdlib - // fence, whole-expression refusals raise the syntax fence. - expect(["SC2020", "SC1090"], `${entry.id}: unexpected fence`).toContain(entry.code); + // fence, component-write refusals raise the TypeScript preflight + // gate (the ambient .d.ts marks the component readonly, so the + // assignment never reaches the lowerer). + expect(["SC2020", "SC0001"], `${entry.id}: unexpected fence`).toContain(entry.code); expect(entry.reason?.length, `${entry.id}: missing gap rationale`).toBeGreaterThan(0); } else { expect(entry.code, `${entry.id}: exclusions are not refusal claims`).toBeUndefined(); @@ -113,7 +115,7 @@ describe("URL compatibility profile", () => { expect(inventory.entries.some((entry) => entry.status === "unsupported")).toBe(true); expect(inventory.entries.some((entry) => entry.status === "out-of-scope")).toBe(true); - expect(inventory.entries.some((entry) => entry.code === "SC1090")).toBe(true); + expect(inventory.entries.some((entry) => entry.code === "SC0001")).toBe(true); expect(inventory.excludedInterfaces.length).toBeGreaterThan(0); for (const exclusion of inventory.excludedInterfaces) { expect(exclusion.name.length).toBeGreaterThan(0);