From c08c501682a7ead593bcf1b4315b05c5a644c073 Mon Sep 17 00:00:00 2001 From: Vito Sansevero Date: Thu, 22 Oct 2026 07:36:08 -0700 Subject: [PATCH] test(inspect): import oracle modules by file URL for Windows The local-inspect oracle imported product modules with path.join(...), which Node parses as a URL scheme on Windows (D:\... -> ERR_UNSUPPORTED_ESM_URL_SCHEME), failing every tests/test_local_inspect_parity.py oracle test on the Windows CI job. Every dynamic import in tests/js/support now goes through moduleUrl, a pathToFileURL wrapper, and a node unit proves the Windows form on any host and scans the oracles so a path import cannot return. --- tests/js/support-module-urls.test.js | 39 +++++++++++++++++++++++ tests/js/support/local-inspect-oracle.mjs | 8 +++-- tests/js/support/module-url.mjs | 9 ++++++ 3 files changed, 53 insertions(+), 3 deletions(-) create mode 100644 tests/js/support-module-urls.test.js create mode 100644 tests/js/support/module-url.mjs diff --git a/tests/js/support-module-urls.test.js b/tests/js/support-module-urls.test.js new file mode 100644 index 00000000..7a961324 --- /dev/null +++ b/tests/js/support-module-urls.test.js @@ -0,0 +1,39 @@ +// The Node oracles that Python drives (tests/js/support/*.mjs) import product +// modules dynamically. A dynamic import takes a URL or a bare/relative specifier, +// never a filesystem path: on Windows `D:\...\x.mjs` parses as the `d:` scheme +// (ERR_UNSUPPORTED_ESM_URL_SCHEME). Every such import goes through moduleUrl, and +// this test proves the Windows form on any host so Linux catches a regression. +import test from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const here = path.dirname(fileURLToPath(import.meta.url)); +const support = path.join(here, "support"); +const m = await import("./support/module-url.mjs").catch((error) => { + if (error.code !== "ERR_MODULE_NOT_FOUND") throw error; + return {}; +}); + +test("moduleUrl turns a filesystem path into a file: URL on every platform", async () => { + assert.equal(typeof m.moduleUrl, "function", "moduleUrl is exported"); + assert.equal(m.moduleUrl("D:\\x\\y z.mjs", { windows: true }), "file:///D:/x/y%20z.mjs"); + assert.equal(m.moduleUrl("C:\\repo\\web\\js\\workspace\\local-video.mjs", { windows: true }), "file:///C:/repo/web/js/workspace/local-video.mjs"); + assert.equal(m.moduleUrl("/repo/web/js/a b.mjs", { windows: false }), "file:///repo/web/js/a%20b.mjs"); + const own = m.moduleUrl(path.join(here, "support", "module-url.mjs")); + assert.ok(own.startsWith("file:///"), own); + assert.equal((await import(own)).moduleUrl, m.moduleUrl, "the URL imports the same module instance"); +}); + +test("every dynamic import in the Node oracles is a file URL, never a path", () => { + const scripts = fs.readdirSync(support).filter((name) => name.endsWith(".mjs")); + assert.ok(scripts.includes("local-inspect-oracle.mjs")); + for (const name of scripts) { + const source = fs.readFileSync(path.join(support, name), "utf8").split("\n").filter((line) => !line.trim().startsWith("//")).join("\n"); + for (const match of source.matchAll(/\bimport\(\s*([^)]*?)[,)]/g)) { + const argument = match[1].trim(); + assert.ok(/^(moduleUrl\(|pathToFileURL\(|["'`]\.{1,2}\/)/.test(argument), `${name}: import(${argument}) must be a file URL or a relative specifier`); + } + } +}); diff --git a/tests/js/support/local-inspect-oracle.mjs b/tests/js/support/local-inspect-oracle.mjs index b6898cc1..61711ced 100644 --- a/tests/js/support/local-inspect-oracle.mjs +++ b/tests/js/support/local-inspect-oracle.mjs @@ -18,14 +18,16 @@ import fs from 'node:fs'; import path from 'node:path'; import { createRequire } from 'node:module'; import { fileURLToPath } from 'node:url'; +import { moduleUrl } from './module-url.mjs'; const here = path.dirname(fileURLToPath(import.meta.url)); const require = createRequire(import.meta.url); const pngMetadata = require(path.join(here, '..', '..', '..', 'web', 'js', 'png-metadata.js')); const web = path.join(here, '..', '..', '..', 'web', 'js', 'workspace'); -const { selectVideoCarrier } = await import(path.join(web, 'local-video.mjs')); -const { projectMetadata } = await import(path.join(web, 'local-projection.mjs')); -const { readLocalFile, toInspectionRead } = await import(path.join(web, 'local-inspection.mjs')); +// ES modules import by file URL (a Windows path would parse as a URL scheme). +const { selectVideoCarrier } = await import(moduleUrl(path.join(web, 'local-video.mjs'))); +const { projectMetadata } = await import(moduleUrl(path.join(web, 'local-projection.mjs'))); +const { readLocalFile, toInspectionRead } = await import(moduleUrl(path.join(web, 'local-inspection.mjs'))); const selectionRead = (selection) => selection.status === 'complete' ? { records: selection.records, record_formats: selection.record_formats, carrierText: selection.carrierText, limited: selection.limited } : null; diff --git a/tests/js/support/module-url.mjs b/tests/js/support/module-url.mjs new file mode 100644 index 00000000..1c8c93f4 --- /dev/null +++ b/tests/js/support/module-url.mjs @@ -0,0 +1,9 @@ +// A dynamic import() takes a URL, never a filesystem path: on Windows a path such +// as D:\repo\x.mjs parses as the "d:" scheme (ERR_UNSUPPORTED_ESM_URL_SCHEME). +// Every oracle in this directory imports product modules through moduleUrl. +import { pathToFileURL } from 'node:url'; + +/** The file: URL for a filesystem path; `windows` forces the Windows form on any host (for tests). */ +export function moduleUrl(filePath, { windows = process.platform === 'win32' } = {}) { + return pathToFileURL(filePath, { windows }).href; +}