fix(session): compare entrypoint file identity

Fixes #8844
Supersedes #8849

Co-Authored-By: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
This commit is contained in:
1052326311andgensokyo-dev authored and Ysopwnable committed 2026-11-14 14:06:23 +01:00
1 parent 90fd58d373
commit eee8c5fa97
2 files changed
+67 -9

No files matched your search

+29 -8
View File
@@ -1,3 +1,4 @@
import { statSync } from "node:fs";
import { dirname, basename, resolve } from "node:path";
import { fileURLToPath } from "node:url";
@@ -24,6 +25,24 @@ const packageRoot = (): string => {
: resolve(moduleDirectory, "..");
};
const compareServerPaths = (
expected: string,
observed: string,
): "aligned" | "mismatch" | "unknown" => {
if (expected === observed) return "aligned";
if (process.platform === "win32")
return resolve(expected) === observed ? "aligned" : "mismatch";
try {
const selected = statSync(expected, { bigint: true });
const running = statSync(observed, { bigint: true });
return selected.dev === running.dev && selected.ino === running.ino
? "aligned"
: "mismatch";
} catch {
return "unknown";
}
};
/** Build one runtime identity without claiming alignment from absent observations. */
export const createServerIdentity = (input: {
readonly startedAt: string;
@@ -35,6 +54,10 @@ export const createServerIdentity = (input: {
}) => {
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);
const reasons: string[] = [];
if (
input.expected?.package_version !== undefined &&
@@ -46,17 +69,12 @@ export const createServerIdentity = (input: {
input.expected.catalog_digest !== catalog.digests.combined_sha256
)
reasons.push("catalog_digest_mismatch");
if (
input.expected?.server_path !== undefined &&
serverPath !== undefined &&
resolve(input.expected.server_path) !== serverPath
)
reasons.push("registration_path_mismatch");
if (pathComparison === "mismatch") reasons.push("registration_path_mismatch");
const observedExpectation =
input.expected !== undefined &&
Object.values(input.expected).some((value) => value !== undefined);
const pathComparable =
input.expected?.server_path === undefined || serverPath !== undefined;
input.expected?.server_path === undefined || pathComparison !== "unknown";
return {
package: {
name: PRODUCT_IDENTITY.packageName,
@@ -105,7 +123,10 @@ export const createServerIdentity = (input: {
: input.expected?.server_path !== undefined &&
serverPath === undefined
? "The server entrypoint path is unavailable, so registration identity cannot be compared."
: "Supply expected package, catalog, or registration identity to compare the live server.",
: input.expected?.server_path !== undefined &&
pathComparison === "unknown"
? "The expected or running server entrypoint could not be inspected, so registration identity cannot be compared."
: "Supply expected package, catalog, or registration identity to compare the live server.",
},
} as const;
};
+38 -1
View File
@@ -1,6 +1,6 @@
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
import { ok as resultOk } from "../../../src/domain/result.js";
import { readFile, writeFile } from "node:fs/promises";
import { mkdir, readFile, symlink, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
@@ -245,6 +245,43 @@ describe("server and catalog identity", () => {
});
});
it.skipIf(process.platform === "win32")(
"compares the selected entrypoint through symlink and parent traversal",
async () => {
const root = await createTestTempDirectory("server-identity-path-");
const scripts = join(root, "package", "scripts");
const scratch = join(root, "scratch");
await mkdir(scripts, { recursive: true });
await mkdir(scratch);
const canonical = join(scripts, "rea.mjs");
const other = join(root, "other.mjs");
await writeFile(canonical, "entrypoint");
await writeFile(other, "other entrypoint");
await symlink(scripts, join(scratch, "link"));
const selected = `${scratch}/link/../scripts/rea.mjs`;
const identity = (server_path: string) =>
createServerIdentity({
catalog: DEFAULT_CATALOG_IDENTITY,
startedAt: "2026-07-13T00:00:00.000Z",
commandPath: canonical,
expected: { server_path },
});
expect(identity(selected).alignment).toMatchObject({
state: "aligned",
reasons: [],
});
expect(identity(other).alignment).toMatchObject({
state: "mcp_server_restart_required",
reasons: ["registration_path_mismatch"],
});
expect(identity(join(root, "missing.mjs")).alignment).toMatchObject({
state: "unknown",
reasons: [],
});
},
);
describe("provider capability inventory", () => {
it("reports composed, host, and target-specific availability truthfully", () => {
const policy = {