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/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)) { 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);