From f69122a77c5b7911cd9de60163f15b02e680cc4c Mon Sep 17 00:00:00 2001 From: cliffhall Date: Thu, 1 Oct 2026 09:53:57 -0400 Subject: [PATCH 1/2] fix(memory): run the file-path tests in a temporary directory ensureMemoryFilePath takes the directory its default files live in, defaulting to the package directory. The file-path tests pass a per-test temporary directory instead of writing memory.json and memory.jsonl beside the source, where the main() that every importing test file starts migrated them away mid-test. Co-Authored-By: Claude Opus 5.5 (1M context) Signed-off-by: cliffhall --- src/memory/__tests__/file-path.test.ts | 71 +++++++++++++++----------- src/memory/index.ts | 27 +++++----- 2 files changed, 56 insertions(+), 42 deletions(-) diff --git a/src/memory/__tests__/file-path.test.ts b/src/memory/__tests__/file-path.test.ts index 20adff3d18..822e7a741b 100644 --- a/src/memory/__tests__/file-path.test.ts +++ b/src/memory/__tests__/file-path.test.ts @@ -1,8 +1,15 @@ +// Tests for how the memory server picks its graph file: MEMORY_FILE_PATH +// handling, the memory.json -> memory.jsonl migration, and "~" expansion. +// +// Every test runs ensureMemoryFilePath against its own temporary directory. +// Importing ../index.js starts the server's main(), which runs the same +// migration on the package directory, and Vitest runs test files in parallel: +// files written beside the source are shared with every other test file, and +// were migrated away mid-test (#4922). import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; import { promises as fs } from "fs"; import path from "path"; import os from "os"; -import { fileURLToPath } from "url"; import { ensureMemoryFilePath, defaultMemoryPath, @@ -10,17 +17,20 @@ import { } from "../index.js"; describe("ensureMemoryFilePath", () => { - const testDir = path.dirname(fileURLToPath(import.meta.url)); - const oldMemoryPath = path.join(testDir, "..", "memory.json"); - const newMemoryPath = path.join(testDir, "..", "memory.jsonl"); - let originalEnv: string | undefined; + let testDir: string; + let oldMemoryPath: string; + let newMemoryPath: string; - beforeEach(() => { + beforeEach(async () => { // Save original environment variable originalEnv = process.env.MEMORY_FILE_PATH; // Delete environment variable delete process.env.MEMORY_FILE_PATH; + + testDir = await fs.mkdtemp(path.join(os.tmpdir(), "mcp-memory-file-path-")); + oldMemoryPath = path.join(testDir, "memory.json"); + newMemoryPath = path.join(testDir, "memory.jsonl"); }); afterEach(async () => { @@ -31,25 +41,15 @@ describe("ensureMemoryFilePath", () => { delete process.env.MEMORY_FILE_PATH; } - // Clean up test files - try { - await fs.unlink(oldMemoryPath); - } catch { - // Ignore if file doesn't exist - } - try { - await fs.unlink(newMemoryPath); - } catch { - // Ignore if file doesn't exist - } + await fs.rm(testDir, { recursive: true, force: true }); }); describe("with MEMORY_FILE_PATH environment variable", () => { it("should return absolute path when MEMORY_FILE_PATH is absolute", async () => { - const absolutePath = "/tmp/custom-memory.jsonl"; + const absolutePath = path.join(testDir, "custom-memory.jsonl"); process.env.MEMORY_FILE_PATH = absolutePath; - const result = await ensureMemoryFilePath(); + const result = await ensureMemoryFilePath(testDir); expect(result).toBe(absolutePath); }); @@ -58,17 +58,28 @@ describe("ensureMemoryFilePath", () => { const relativePath = "custom-memory.jsonl"; process.env.MEMORY_FILE_PATH = relativePath; + const result = await ensureMemoryFilePath(testDir); + + expect(result).toBe(path.join(testDir, relativePath)); + }); + + it("should resolve a relative path against the package directory by default", async () => { + const relativePath = "custom-memory.jsonl"; + process.env.MEMORY_FILE_PATH = relativePath; + + // No base directory: the call the server makes. Touches no file. const result = await ensureMemoryFilePath(); - expect(path.isAbsolute(result)).toBe(true); - expect(result).toContain("custom-memory.jsonl"); + expect(result).toBe( + path.join(path.dirname(defaultMemoryPath), relativePath), + ); }); it("should handle Windows absolute paths", async () => { const windowsPath = "C:\\temp\\memory.jsonl"; process.env.MEMORY_FILE_PATH = windowsPath; - const result = await ensureMemoryFilePath(); + const result = await ensureMemoryFilePath(testDir); // On Windows, should return as-is; on Unix, will be treated as relative if (process.platform === "win32") { @@ -81,7 +92,7 @@ describe("ensureMemoryFilePath", () => { it('should expand a leading "~/" to the home directory', async () => { process.env.MEMORY_FILE_PATH = "~/custom-memory.jsonl"; - const result = await ensureMemoryFilePath(); + const result = await ensureMemoryFilePath(testDir); expect(result).toBe(path.join(os.homedir(), "custom-memory.jsonl")); expect(path.isAbsolute(result)).toBe(true); @@ -90,9 +101,9 @@ describe("ensureMemoryFilePath", () => { describe("without MEMORY_FILE_PATH environment variable", () => { it("should return default path when no files exist", async () => { - const result = await ensureMemoryFilePath(); + const result = await ensureMemoryFilePath(testDir); - expect(result).toBe(defaultMemoryPath); + expect(result).toBe(newMemoryPath); }); it("should migrate from memory.json to memory.jsonl when only old file exists", async () => { @@ -103,9 +114,9 @@ describe("ensureMemoryFilePath", () => { .spyOn(console, "error") .mockImplementation(() => {}); - const result = await ensureMemoryFilePath(); + const result = await ensureMemoryFilePath(testDir); - expect(result).toBe(defaultMemoryPath); + expect(result).toBe(newMemoryPath); // Verify migration happened const newFileExists = await fs @@ -140,9 +151,9 @@ describe("ensureMemoryFilePath", () => { .spyOn(console, "error") .mockImplementation(() => {}); - const result = await ensureMemoryFilePath(); + const result = await ensureMemoryFilePath(testDir); - expect(result).toBe(defaultMemoryPath); + expect(result).toBe(newMemoryPath); // Verify no migration happened (both files should still exist) const newFileExists = await fs @@ -167,7 +178,7 @@ describe("ensureMemoryFilePath", () => { const testContent = '{"entities": [{"name": "test", "type": "person"}]}'; await fs.writeFile(oldMemoryPath, testContent); - await ensureMemoryFilePath(); + await ensureMemoryFilePath(testDir); const migratedContent = await fs.readFile(newMemoryPath, "utf-8"); expect(migratedContent).toBe(testContent); diff --git a/src/memory/index.ts b/src/memory/index.ts index 6eb6cec7c3..8317c1eb18 100644 --- a/src/memory/index.ts +++ b/src/memory/index.ts @@ -14,11 +14,12 @@ import { randomBytes } from "crypto"; import { fileURLToPath } from "url"; import { SERVER_VERSION } from "./version.js"; +// The package directory: where the default graph file lives, and what a +// relative MEMORY_FILE_PATH resolves against. +const defaultMemoryDir = path.dirname(fileURLToPath(import.meta.url)); + // Define memory file path using environment variable with fallback -export const defaultMemoryPath = path.join( - path.dirname(fileURLToPath(import.meta.url)), - "memory.jsonl", -); +export const defaultMemoryPath = path.join(defaultMemoryDir, "memory.jsonl"); // Expand a leading "~" to the user's home directory. MCP clients pass // MEMORY_FILE_PATH from JSON config, where no shell performs tilde expansion, @@ -32,23 +33,25 @@ export function expandHome(filepath: string): string { return filepath; } -// Handle backward compatibility: migrate memory.json to memory.jsonl if needed -export async function ensureMemoryFilePath(): Promise { +// Handle backward compatibility: migrate memory.json to memory.jsonl if needed. +// baseDir is the directory the default files live in. The server never passes +// it; it exists so tests can run the migration in a temporary directory rather +// than in the package directory, which every importer of this module shares. +export async function ensureMemoryFilePath( + baseDir: string = defaultMemoryDir, +): Promise { if (process.env.MEMORY_FILE_PATH) { // Custom path provided. Expand a leading "~" first, then resolve relative // paths against the package directory (absolute paths are used as-is). const customPath = expandHome(process.env.MEMORY_FILE_PATH); return path.isAbsolute(customPath) ? customPath - : path.join(path.dirname(fileURLToPath(import.meta.url)), customPath); + : path.join(baseDir, customPath); } // No custom path set, check for backward compatibility migration - const oldMemoryPath = path.join( - path.dirname(fileURLToPath(import.meta.url)), - "memory.json", - ); - const newMemoryPath = defaultMemoryPath; + const oldMemoryPath = path.join(baseDir, "memory.json"); + const newMemoryPath = path.join(baseDir, "memory.jsonl"); try { // Check if old file exists and new file doesn't From 478db6a5d5bc0bec3196e2c8c9461ec1597ded34 Mon Sep 17 00:00:00 2001 From: cliffhall Date: Thu, 1 Oct 2026 09:59:47 -0400 Subject: [PATCH 2/2] chore(memory): add a patch changeset for the file-path helper change Co-Authored-By: Claude Opus 5.5 (1M context) Signed-off-by: cliffhall --- .changeset/memory-file-path-base-dir.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/memory-file-path-base-dir.md diff --git a/.changeset/memory-file-path-base-dir.md b/.changeset/memory-file-path-base-dir.md new file mode 100644 index 0000000000..ec2f84b554 --- /dev/null +++ b/.changeset/memory-file-path-base-dir.md @@ -0,0 +1,5 @@ +--- +"@modelcontextprotocol/server-memory": patch +--- + +Internal: `ensureMemoryFilePath` accepts the directory its default files live in, so the server's tests no longer write into the package directory. No change to how the server chooses or migrates its memory file.