Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/validate-plugins.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
6 changes: 3 additions & 3 deletions scripts/generate-pstack-harness-trees.mjs
Original file line number Diff line number Diff line change
@@ -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)), "..");
Expand Down Expand Up @@ -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.";
Expand Down Expand Up @@ -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;
Expand Down
45 changes: 45 additions & 0 deletions scripts/generate-pstack-harness-trees.test.mjs
Original file line number Diff line number Diff line change
@@ -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));
Loading