From 2a2f7bd55b0bb89ba882ba5e9e99ba6780b1277d Mon Sep 17 00:00:00 2001 From: u7-u7 <116776533+u7-u7@users.noreply.github.com> Date: Wed, 28 Oct 2026 00:28:12 +0800 Subject: [PATCH] fix(evals): escape relative scaffold import URLs --- evals/browser/lib/scaffold-case.mjs | 2 +- evals/browser/tests/eval.test.mjs | 31 ++++++++++++++++++++++++++++- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/evals/browser/lib/scaffold-case.mjs b/evals/browser/lib/scaffold-case.mjs index b4a9a211..760feeee 100644 --- a/evals/browser/lib/scaffold-case.mjs +++ b/evals/browser/lib/scaffold-case.mjs @@ -20,7 +20,7 @@ async function moduleSpecifier(fromDirectory, target) { const targetPath = await realpath(target); const relativePath = relative(await realpath(fromDirectory), targetPath); if (isAbsolute(relativePath)) return pathToFileURL(targetPath).href; - const path = relativePath.split(sep).join("/"); + const path = relativePath.split(sep).map(encodeURIComponent).join("/"); return path.startsWith(".") ? path : `./${path}`; } diff --git a/evals/browser/tests/eval.test.mjs b/evals/browser/tests/eval.test.mjs index 6f75054c..29d128f8 100644 --- a/evals/browser/tests/eval.test.mjs +++ b/evals/browser/tests/eval.test.mjs @@ -1,8 +1,9 @@ import assert from "node:assert/strict"; -import { mkdtemp, readFile, rm } from "node:fs/promises"; +import { copyFile, mkdir, mkdtemp, readFile, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join, resolve } from "node:path"; import test from "node:test"; +import { pathToFileURL } from "node:url"; import { runCommandTask } from "../lib/agent-runner.mjs"; import { loadCaseManifests, validateCaseManifest } from "../lib/case-loader.mjs"; @@ -279,6 +280,34 @@ test("badcase scaffold creates a loadable, escaped, self-contained case", async } }); +test("badcase scaffold escapes URL characters in relative fixture imports", async () => { + const temporaryRoot = await mkdtemp(join(tmpdir(), "browser-eval-import-")); + try { + const directoryNames = ["checkout#fragment", "checkout%20literal"]; + if (process.platform !== "win32") directoryNames.push("checkout?query"); + for (const directoryName of directoryNames) { + const helpers = join(temporaryRoot, directoryName, "lib"); + await mkdir(helpers, { recursive: true }); + for (const name of ["scaffold-case.mjs", "paths.mjs", "fixtures.mjs"]) { + await copyFile(new URL(`../lib/${name}`, import.meta.url), join(helpers, name)); + } + const { scaffoldCase: scaffold } = await import( + pathToFileURL(join(helpers, "scaffold-case.mjs")).href + ); + const result = await scaffold({ + id: "escaped-import", + rootCasesDirectory: join(temporaryRoot, `cases-${directoryName}`), + }); + const source = await readFile(result.files.fixture, "utf8"); + assert.match(source, /from "\.\.\//); + const { default: fixture } = await import(pathToFileURL(result.files.fixture).href); + assert.match(fixture.render(), /CASE-ESCAPED_IMPORT/); + } + } finally { + await rm(temporaryRoot, { recursive: true, force: true }); + } +}); + test("CLI accepts pnpm separator and supports suite and tag filters", async () => { const cli = resolve("evals/browser/cli.mjs"); const coverageResult = await runProcess(process.execPath, [cli, "--", "coverage", "--json"], {