test(mcp-server): add regression tests for importLocalModule candidate resolution

Extracts _buildImportCandidates() as a testable export and adds 5 tests
verifying src/<->dist/ path swapping and .ts extension fallback logic.

Refs #3954
This commit is contained in:
Jeremy 2026-04-10 19:33:00 -05:00
parent ad8405c372
commit 67767c2527
2 changed files with 60 additions and 7 deletions

View file

@ -0,0 +1,48 @@
// GSD-2 — Regression tests for importLocalModule candidate resolution (#3954)
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import { _buildImportCandidates } from "./workflow-tools.js";
describe("_buildImportCandidates", () => {
it("includes dist/ fallback for src/ paths", () => {
const candidates = _buildImportCandidates("../../../src/resources/extensions/gsd/db-writer.js");
assert.ok(
candidates.some((c) => c.includes("/dist/resources/extensions/gsd/db-writer.js")),
"should include dist/ swapped candidate",
);
});
it("includes src/ fallback for dist/ paths", () => {
const candidates = _buildImportCandidates("../../../dist/resources/extensions/gsd/db-writer.js");
assert.ok(
candidates.some((c) => c.includes("/src/resources/extensions/gsd/db-writer.js")),
"should include src/ swapped candidate",
);
});
it("includes .ts variants for .js paths", () => {
const candidates = _buildImportCandidates("../../../src/resources/extensions/gsd/db-writer.js");
assert.ok(
candidates.some((c) => c.endsWith("db-writer.ts") && c.includes("/src/")),
"should include .ts variant for original src/ path",
);
assert.ok(
candidates.some((c) => c.endsWith("db-writer.ts") && c.includes("/dist/")),
"should include .ts variant for swapped dist/ path",
);
});
it("returns original path first", () => {
const input = "../../../src/resources/extensions/gsd/db-writer.js";
const candidates = _buildImportCandidates(input);
assert.equal(candidates[0], input, "first candidate should be the original path");
});
it("handles paths without src/ or dist/ gracefully", () => {
const candidates = _buildImportCandidates("./local-module.js");
assert.equal(candidates.length, 2, "should have original + .ts variant only");
assert.equal(candidates[0], "./local-module.js");
assert.equal(candidates[1], "./local-module.ts");
});
});

View file

@ -337,24 +337,29 @@ function toFileUrl(modulePath: string): string {
return pathToFileURL(resolve(modulePath)).href;
}
async function importLocalModule<T>(relativePath: string): Promise<T> {
/** @internal — exported for testing only */
export function _buildImportCandidates(relativePath: string): string[] {
// Build candidate paths: try the given path first, then swap src/<->dist/
// and try .ts extension. This handles both dev (tsx from src/) and prod
// (compiled from dist/) execution contexts.
const candidates: string[] = [
new URL(relativePath, import.meta.url).href,
];
const candidates: string[] = [relativePath];
const swapped = relativePath.includes("/src/")
? relativePath.replace("/src/", "/dist/")
: relativePath.includes("/dist/")
? relativePath.replace("/dist/", "/src/")
: null;
if (swapped) candidates.push(new URL(swapped, import.meta.url).href);
if (swapped) candidates.push(swapped);
// Also try .ts variants for dev-mode tsx execution
if (relativePath.endsWith(".js")) {
candidates.push(new URL(relativePath.replace(/\.js$/, ".ts"), import.meta.url).href);
if (swapped) candidates.push(new URL(swapped.replace(/\.js$/, ".ts"), import.meta.url).href);
candidates.push(relativePath.replace(/\.js$/, ".ts"));
if (swapped) candidates.push(swapped.replace(/\.js$/, ".ts"));
}
return candidates;
}
async function importLocalModule<T>(relativePath: string): Promise<T> {
const candidates = _buildImportCandidates(relativePath)
.map((p) => new URL(p, import.meta.url).href);
let lastErr: unknown;
for (const candidate of candidates) {