- Create the storage directory before locking. `writeStoreFile` creates it, but from inside the locked section — so on a fresh install, where `~/.mcp-inspector` does not exist, proper-lockfile failed ENOENT and every *first* save degraded to an unlocked write with a warning. That is the save most likely to be racing another (two Inspectors started together both reach it), so the lock was absent from precisely the interleaving it exists to close. - `anythingToMigrate` no longer reads every `readdir` failure as "nothing to migrate". Only ENOENT/ENOTDIR proves the fresh-install case; a directory that denies listing can still permit access to the known `secrets.json`, and returning false there selected the keychain and left those secrets invisible with nothing said. Other errors fall through to the under-lock checks, which is what happened before the fast path existed. - Pin `proper-lockfile` to the repo-root install in `vitest.shared.mts`, beside express and yaml: it is reached only through root-owned `core/`. Resolution already finds the root copy since no client declares it; the pin is what stops that depending on it never arriving as some client's transitive dependency, which would give a test two copies of a module whose whole job is one registry of held locks. - Make the cross-process test prove `mutate` takes the lock. The previous one asserted only that both secrets survived, which the optimistic verify delivers with the lock removed entirely. It now asserts the parent's `set` has *not* settled while a child holds the lock — an observation only a real lock produces. Verified by removing `withSecretFileLock` from `mutate`: three tests fail, where none did before. The degrade test's "lock cannot be created" case is now a path whose parent is a *file* rather than a missing directory, since a missing directory is no longer that case. It also fails identically for root, so it cannot pass locally and flake in a container. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JTHVxSu8AUgHRLvo1ntZ8H Signed-off-by: cliffhall <cliff@futurescale.com>
141 lines
4.5 KiB
TypeScript
141 lines
4.5 KiB
TypeScript
/**
|
|
* Vitest/Vite resolve aliases shared between clients/web and node clients
|
|
* (cli, tui). Pass each client's directory so bare-module pins resolve against
|
|
* that client's node_modules.
|
|
*/
|
|
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
export function vitestSharedPaths(clientDir: string) {
|
|
const dirname = path.resolve(clientDir);
|
|
const repoRoot = path.resolve(dirname, "../..");
|
|
|
|
const sharedAliases = {
|
|
"@inspector/core": path.resolve(repoRoot, "core"),
|
|
"@modelcontextprotocol/inspector-test-server": path.resolve(
|
|
repoRoot,
|
|
"test-servers/build/index.js",
|
|
),
|
|
};
|
|
|
|
const sharedDedupe = [
|
|
"react",
|
|
"react-dom",
|
|
"@modelcontextprotocol/client",
|
|
"@modelcontextprotocol/core",
|
|
];
|
|
|
|
const nodeModulesAliases = [
|
|
{
|
|
find: /^react$/,
|
|
replacement: path.resolve(dirname, "node_modules/react"),
|
|
},
|
|
{
|
|
find: /^react\/jsx-runtime$/,
|
|
replacement: path.resolve(dirname, "node_modules/react/jsx-runtime.js"),
|
|
},
|
|
{
|
|
find: /^react\/jsx-dev-runtime$/,
|
|
replacement: path.resolve(
|
|
dirname,
|
|
"node_modules/react/jsx-dev-runtime.js",
|
|
),
|
|
},
|
|
{
|
|
find: /^react-dom$/,
|
|
replacement: path.resolve(dirname, "node_modules/react-dom"),
|
|
},
|
|
{
|
|
find: /^react-dom\/client$/,
|
|
replacement: path.resolve(dirname, "node_modules/react-dom/client.js"),
|
|
},
|
|
{ find: /^pino$/, replacement: path.resolve(dirname, "node_modules/pino") },
|
|
{
|
|
find: /^pino\/browser\.js$/,
|
|
replacement: path.resolve(dirname, "node_modules/pino/browser.js"),
|
|
},
|
|
{
|
|
find: /^hono$/,
|
|
replacement: path.resolve(dirname, "node_modules/hono/dist/index.js"),
|
|
},
|
|
{
|
|
find: /^hono\/streaming$/,
|
|
replacement: path.resolve(
|
|
dirname,
|
|
"node_modules/hono/dist/helper/streaming/index.js",
|
|
),
|
|
},
|
|
{
|
|
find: /^@hono\/node-server$/,
|
|
replacement: path.resolve(dirname, "node_modules/@hono/node-server"),
|
|
},
|
|
{
|
|
find: /^atomically$/,
|
|
replacement: path.resolve(dirname, "node_modules/atomically"),
|
|
},
|
|
{
|
|
find: /^chokidar$/,
|
|
replacement: path.resolve(dirname, "node_modules/chokidar"),
|
|
},
|
|
{
|
|
find: /^@napi-rs\/keyring$/,
|
|
replacement: path.resolve(dirname, "node_modules/@napi-rs/keyring"),
|
|
},
|
|
// `express` and `yaml` resolve from the **repo root**, unlike every pin
|
|
// above. Both are reached only through `test-servers/src` — express by the
|
|
// http/oauth servers, yaml by `load-config.ts` — which is root-owned code
|
|
// with no manifest of its own, so the root is where they are declared and
|
|
// the only place a client's resolution chain is guaranteed to find them.
|
|
// Pointing these at `<client>/node_modules` is what broke when the MCP
|
|
// packages moved to the root (#1970): express was never declared by a client
|
|
// at all, it arrived in `clients/cli` as a peer of `express-rate-limit`
|
|
// under `@modelcontextprotocol/server-legacy`, so removing that manifest
|
|
// entry took express with it and every cli test that spawns a test server
|
|
// failed to resolve it.
|
|
{
|
|
find: /^express$/,
|
|
replacement: path.resolve(repoRoot, "node_modules/express"),
|
|
},
|
|
{
|
|
find: /^yaml$/,
|
|
replacement: path.resolve(repoRoot, "node_modules/yaml"),
|
|
},
|
|
// Same reasoning, one layer in: `proper-lockfile` is reached only through
|
|
// `core/` (the secrets file's cross-process lock, #2082), which is the
|
|
// other root-owned tree with no manifest of its own. Resolution finds the
|
|
// root copy on its own today — nothing declares it in a client — and this
|
|
// pin is what keeps that from depending on nothing ever arriving as some
|
|
// client's transitive dependency, which would otherwise give a test two
|
|
// copies of a module whose whole job is a single registry of held locks.
|
|
{
|
|
find: /^proper-lockfile$/,
|
|
replacement: path.resolve(repoRoot, "node_modules/proper-lockfile"),
|
|
},
|
|
];
|
|
|
|
const projectResolve = {
|
|
alias: [
|
|
...Object.entries(sharedAliases).map(([find, replacement]) => ({
|
|
find,
|
|
replacement,
|
|
})),
|
|
...nodeModulesAliases,
|
|
],
|
|
dedupe: sharedDedupe,
|
|
};
|
|
|
|
return {
|
|
repoRoot,
|
|
sharedAliases,
|
|
sharedDedupe,
|
|
nodeModulesAliases,
|
|
projectResolve,
|
|
};
|
|
}
|
|
|
|
/** Convenience for importers that only have import.meta.url. */
|
|
export function vitestSharedPathsFromMetaUrl(metaUrl: string) {
|
|
const clientDir = path.dirname(fileURLToPath(metaUrl));
|
|
return vitestSharedPaths(clientDir);
|
|
}
|