From 733e4868934b149a9c87ae80278f2e640b0ad123 Mon Sep 17 00:00:00 2001 From: Rudy Celekli Date: Fri, 16 Oct 2026 16:30:12 -0400 Subject: [PATCH] fix: normalize generated harness relative path keys --- .github/workflows/validate-plugins.yml | 3 ++ scripts/generate-pstack-harness-trees.mjs | 6 +-- .../generate-pstack-harness-trees.test.mjs | 45 +++++++++++++++++++ 3 files changed, 51 insertions(+), 3 deletions(-) create mode 100644 scripts/generate-pstack-harness-trees.test.mjs diff --git a/.github/workflows/validate-plugins.yml b/.github/workflows/validate-plugins.yml index c3edbc934..5e5bff7fe 100644 --- a/.github/workflows/validate-plugins.yml +++ b/.github/workflows/validate-plugins.yml @@ -33,3 +33,6 @@ jobs: - name: Test pstack skill scripts run: node --test pstack/skills/*/scripts/test/*.test.mjs + + - name: Test harness tree path portability + run: node --experimental-vm-modules --test scripts/generate-pstack-harness-trees.test.mjs diff --git a/scripts/generate-pstack-harness-trees.mjs b/scripts/generate-pstack-harness-trees.mjs index 83335a116..fe4711fe4 100644 --- a/scripts/generate-pstack-harness-trees.mjs +++ b/scripts/generate-pstack-harness-trees.mjs @@ -1,7 +1,7 @@ #!/usr/bin/env node import { chmodSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs"; -import { dirname, join, posix, relative } from "node:path"; +import { dirname, join, posix, relative, sep } from "node:path"; import { fileURLToPath } from "node:url"; const ROOT = join(dirname(fileURLToPath(import.meta.url)), ".."); @@ -124,7 +124,7 @@ function render() { join(PSTACK, ADAPTER), join(PSTACK, "README.md"), ]; - const files = new Map(copies.map((path) => [relative(PSTACK, path), file(path)])); + const files = new Map(copies.map((path) => [relative(PSTACK, path).split(sep).join(posix.sep), file(path)])); const line = `In Claude Code, read [the Claude Code adapter](../../${ADAPTER}) once per session before you follow this skill. ` + "It maps the Cursor names in pstack skills to Claude Code."; @@ -165,7 +165,7 @@ for (const link of dangling) console.log(`FAIL ${link}`); if (dangling.length > 0) process.exit(1); if (process.argv.includes("--check")) { - const onDisk = new Map(existsSync(OUT) ? walk(OUT).map((path) => [relative(OUT, path), file(path)]) : []); + const onDisk = new Map(existsSync(OUT) ? walk(OUT).map((path) => [relative(OUT, path).split(sep).join(posix.sep), file(path)]) : []); const drifted = [...new Set([...rendered.keys(), ...onDisk.keys()])].sort().filter((path) => { const [want, have] = [rendered.get(path), onDisk.get(path)]; return !want || !have || !want.bytes.equals(have.bytes) || want.executable !== have.executable; diff --git a/scripts/generate-pstack-harness-trees.test.mjs b/scripts/generate-pstack-harness-trees.test.mjs new file mode 100644 index 000000000..c9019b0c8 --- /dev/null +++ b/scripts/generate-pstack-harness-trees.test.mjs @@ -0,0 +1,45 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import * as path from "node:path"; +import { fileURLToPath } from "node:url"; +import { SourceTextModule, SyntheticModule, createContext } from "node:vm"; + +const url = new URL("./generate-pstack-harness-trees.mjs", import.meta.url); +const filename = fileURLToPath(url); +const source = readFileSync(url, "utf8"); + +// Execute the complete production check against the real source/generated +// trees. Only the native path separator boundary varies; filesystem calls, +// rendering, generated bytes and drift detection are all production code. +async function checkTree(windowsPaths) { + const output = []; + const context = createContext({ + Buffer, + console: { log: (...args) => output.push(args.join(" ")) }, + process: { + argv: [process.execPath, filename, "--check"], + exit: (code) => { throw new Error(`generator exited ${code}: ${output.join("\n")}`); }, + }, + }); + const main = new SourceTextModule(source, { + context, identifier: url.href, + initializeImportMeta: (meta) => { meta.url = url.href; }, + }); + await main.link(async (specifier) => { + const imported = await import(specifier); + const exports = { ...imported }; + if (specifier === "node:path" && windowsPaths) { + exports.relative = path.win32.relative; + exports.sep = path.win32.sep; + } + return new SyntheticModule(Object.keys(exports), function () { + for (const [name, value] of Object.entries(exports)) this.setExport(name, value); + }, { context }); + }); + await main.evaluate(); + assert.deepEqual(output, ["harness/pstack is up to date."]); +} + +test("generated tree checks with native relative path keys", () => checkTree(false)); +test("generated tree checks with Windows relative path keys", () => checkTree(true));