fix(session): select the registered entrypoint as the running runtime does

Node resolves its script entrypoint lexically, so a registered spelling that
crosses a symlink before `..` names the running file lexically, while Bun
follows native traversal (#8844). Select the expected server path with the
shared runtime-aware entrypoint normalizer before comparing it with the
running entrypoint, and fall back to file identity only when they differ.

Co-Authored-By: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
This commit is contained in:
gensokyo-devandgensokyo-dev authored and Ysopwnable committed 2026-11-14 14:06:23 +01:00
1 parent 982ea7fa9f
commit 065fbec4b3
2 files changed
+41 -5

No files matched your search

+20 -5
View File
@@ -2,6 +2,12 @@ import { statSync } from "node:fs";
import { dirname, basename, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import {
javaScriptRuntime,
type JavaScriptRuntime,
} from "./domain/runtimeVersion.js";
import { normalizeEntrypointPath } from "./process/CurrentEntrypointPath.js";
import type { createCatalogIdentity } from "./catalogIdentity.js";
import { PRODUCT_IDENTITY, SDK_IDENTITY } from "./identity.js";
@@ -28,12 +34,15 @@ const packageRoot = (): string => {
const compareServerPaths = (
expected: string,
observed: string,
runtime: JavaScriptRuntime["id"],
): "aligned" | "mismatch" | "unknown" => {
if (expected === observed) return "aligned";
if (process.platform === "win32")
return resolve(expected) === observed ? "aligned" : "mismatch";
// Select the registered script the way this runtime does: Node resolves it
// lexically, while Bun follows native traversal through a symlink before `..`.
const script = normalizeEntrypointPath(expected, runtime) ?? expected;
if (script === observed) return "aligned";
if (process.platform === "win32") return "mismatch";
try {
const selected = statSync(expected, { bigint: true });
const selected = statSync(script, { bigint: true });
const running = statSync(observed, { bigint: true });
return selected.dev === running.dev && selected.ino === running.ino
? "aligned"
@@ -51,13 +60,19 @@ export const createServerIdentity = (input: {
readonly client?: ConnectedClientIdentity;
readonly protocolVersion?: string | undefined;
readonly catalog: ReturnType<typeof createCatalogIdentity>;
/** The runtime that selected `commandPath`; defaults to the current one. */
readonly runtime?: JavaScriptRuntime["id"];
}) => {
const catalog = input.catalog;
const serverPath = input.commandPath || undefined;
const pathComparison =
input.expected?.server_path === undefined || serverPath === undefined
? "unknown"
: compareServerPaths(input.expected.server_path, serverPath);
: compareServerPaths(
input.expected.server_path,
serverPath,
input.runtime ?? javaScriptRuntime(process.versions).id,
);
const reasons: string[] = [];
if (
input.expected?.package_version !== undefined &&
+21
View File
@@ -259,12 +259,14 @@ it.skipIf(process.platform === "win32")(
await writeFile(other, "other entrypoint");
await symlink(scripts, join(scratch, "link"));
const selected = `${scratch}/link/../scripts/rea.mjs`;
// Bun selects a script through native traversal (#8844).
const identity = (server_path: string) =>
createServerIdentity({
catalog: DEFAULT_CATALOG_IDENTITY,
startedAt: "2026-07-13T00:00:00.000Z",
commandPath: canonical,
expected: { server_path },
runtime: "bun",
});
expect(identity(selected).alignment).toMatchObject({
@@ -279,6 +281,25 @@ it.skipIf(process.platform === "win32")(
state: "unknown",
reasons: [],
});
// Node resolves its entrypoint lexically, so this spelling runs
// scratch/rea.mjs even though traversal through the link leaves scratch.
// Bun traverses the link and runs a different file.
const lexical = join(scratch, "rea.mjs");
await writeFile(lexical, "lexical entrypoint");
await writeFile(join(root, "package", "rea.mjs"), "traversed entrypoint");
const crossing = (runtime: "node" | "bun") =>
createServerIdentity({
catalog: DEFAULT_CATALOG_IDENTITY,
startedAt: "2026-07-13T00:00:00.000Z",
commandPath: lexical,
expected: { server_path: `${scratch}/link/../rea.mjs` },
runtime,
}).alignment;
expect(crossing("node")).toMatchObject({ state: "aligned", reasons: [] });
expect(crossing("bun")).toMatchObject({
state: "mcp_server_restart_required",
reasons: ["registration_path_mismatch"],
});
},
);