mirror of
https://github.com/thedotmack/claude-mem
synced 2026-04-25 17:15:04 +02:00
- Implement tests for cursor context updates in `cursor-context-update.test.ts`, validating context file creation, content structure, and edge cases. - Create tests for cursor hook outputs in `cursor-hook-outputs.test.ts`, ensuring correct JSON output from hook scripts and handling of various input scenarios. - Add tests for JSON utility functions in `cursor-hooks-json-utils.test.ts`, covering parsing, project name extraction, and URL encoding. - Introduce tests for MCP configuration in `cursor-mcp-config.test.ts`, verifying configuration creation, updates, and format validation. - Develop tests for the cursor project registry in `cursor-registry.test.ts`, ensuring correct registration, unregistration, and JSON format compliance.
248 lines
8.6 KiB
TypeScript
248 lines
8.6 KiB
TypeScript
import { describe, it, expect, beforeEach, afterEach } from 'bun:test';
|
|
import { mkdirSync, writeFileSync, readFileSync, existsSync, rmSync } from 'fs';
|
|
import { join } from 'path';
|
|
import { tmpdir } from 'os';
|
|
import {
|
|
configureCursorMcp,
|
|
removeMcpConfig,
|
|
type CursorMcpConfig
|
|
} from '../src/utils/cursor-utils';
|
|
|
|
/**
|
|
* Tests for Cursor MCP Configuration
|
|
*
|
|
* These tests validate the MCP server configuration that gets written
|
|
* to .cursor/mcp.json (project-level) or ~/.cursor/mcp.json (user-level).
|
|
*
|
|
* The config must match Cursor's expected format for MCP servers.
|
|
*/
|
|
|
|
describe('Cursor MCP Configuration', () => {
|
|
let tempDir: string;
|
|
let mcpJsonPath: string;
|
|
const mcpServerPath = '/path/to/mcp-server.cjs';
|
|
|
|
beforeEach(() => {
|
|
// Create unique temp directory for each test
|
|
tempDir = join(tmpdir(), `cursor-mcp-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
mkdirSync(tempDir, { recursive: true });
|
|
mcpJsonPath = join(tempDir, '.cursor', 'mcp.json');
|
|
});
|
|
|
|
afterEach(() => {
|
|
// Clean up temp directory
|
|
try {
|
|
rmSync(tempDir, { recursive: true, force: true });
|
|
} catch {
|
|
// Ignore cleanup errors
|
|
}
|
|
});
|
|
|
|
describe('configureCursorMcp', () => {
|
|
it('creates mcp.json if it does not exist', () => {
|
|
configureCursorMcp(mcpJsonPath, mcpServerPath);
|
|
|
|
expect(existsSync(mcpJsonPath)).toBe(true);
|
|
});
|
|
|
|
it('creates .cursor directory if it does not exist', () => {
|
|
configureCursorMcp(mcpJsonPath, mcpServerPath);
|
|
|
|
expect(existsSync(join(tempDir, '.cursor'))).toBe(true);
|
|
});
|
|
|
|
it('adds claude-mem server with correct structure', () => {
|
|
configureCursorMcp(mcpJsonPath, mcpServerPath);
|
|
|
|
const config: CursorMcpConfig = JSON.parse(readFileSync(mcpJsonPath, 'utf-8'));
|
|
|
|
expect(config.mcpServers).toBeDefined();
|
|
expect(config.mcpServers['claude-mem']).toBeDefined();
|
|
expect(config.mcpServers['claude-mem'].command).toBe('node');
|
|
expect(config.mcpServers['claude-mem'].args).toEqual([mcpServerPath]);
|
|
});
|
|
|
|
it('preserves existing MCP servers when adding claude-mem', () => {
|
|
// Pre-create config with another server
|
|
mkdirSync(join(tempDir, '.cursor'), { recursive: true });
|
|
const existingConfig = {
|
|
mcpServers: {
|
|
'other-server': {
|
|
command: 'python',
|
|
args: ['/path/to/other.py']
|
|
}
|
|
}
|
|
};
|
|
writeFileSync(mcpJsonPath, JSON.stringify(existingConfig));
|
|
|
|
configureCursorMcp(mcpJsonPath, mcpServerPath);
|
|
|
|
const config: CursorMcpConfig = JSON.parse(readFileSync(mcpJsonPath, 'utf-8'));
|
|
|
|
// Both servers should exist
|
|
expect(config.mcpServers['other-server']).toBeDefined();
|
|
expect(config.mcpServers['other-server'].command).toBe('python');
|
|
expect(config.mcpServers['claude-mem']).toBeDefined();
|
|
});
|
|
|
|
it('updates existing claude-mem server path', () => {
|
|
// First config
|
|
configureCursorMcp(mcpJsonPath, '/old/path.cjs');
|
|
|
|
// Update with new path
|
|
const newPath = '/new/path.cjs';
|
|
configureCursorMcp(mcpJsonPath, newPath);
|
|
|
|
const config: CursorMcpConfig = JSON.parse(readFileSync(mcpJsonPath, 'utf-8'));
|
|
|
|
expect(config.mcpServers['claude-mem'].args).toEqual([newPath]);
|
|
});
|
|
|
|
it('recovers from corrupt mcp.json', () => {
|
|
// Create corrupt file
|
|
mkdirSync(join(tempDir, '.cursor'), { recursive: true });
|
|
writeFileSync(mcpJsonPath, 'not valid json {{{{');
|
|
|
|
// Should not throw, should overwrite
|
|
configureCursorMcp(mcpJsonPath, mcpServerPath);
|
|
|
|
const config: CursorMcpConfig = JSON.parse(readFileSync(mcpJsonPath, 'utf-8'));
|
|
expect(config.mcpServers['claude-mem']).toBeDefined();
|
|
});
|
|
|
|
it('handles mcp.json with missing mcpServers key', () => {
|
|
// Create file with empty object
|
|
mkdirSync(join(tempDir, '.cursor'), { recursive: true });
|
|
writeFileSync(mcpJsonPath, '{}');
|
|
|
|
configureCursorMcp(mcpJsonPath, mcpServerPath);
|
|
|
|
const config: CursorMcpConfig = JSON.parse(readFileSync(mcpJsonPath, 'utf-8'));
|
|
expect(config.mcpServers['claude-mem']).toBeDefined();
|
|
});
|
|
});
|
|
|
|
describe('MCP config format validation', () => {
|
|
it('produces valid JSON', () => {
|
|
configureCursorMcp(mcpJsonPath, mcpServerPath);
|
|
|
|
const content = readFileSync(mcpJsonPath, 'utf-8');
|
|
|
|
// Should not throw
|
|
expect(() => JSON.parse(content)).not.toThrow();
|
|
});
|
|
|
|
it('uses pretty-printed JSON (2-space indent)', () => {
|
|
configureCursorMcp(mcpJsonPath, mcpServerPath);
|
|
|
|
const content = readFileSync(mcpJsonPath, 'utf-8');
|
|
|
|
// Should contain newlines and indentation
|
|
expect(content).toContain('\n');
|
|
expect(content).toContain(' "mcpServers"');
|
|
});
|
|
|
|
it('matches Cursor MCP server schema', () => {
|
|
configureCursorMcp(mcpJsonPath, mcpServerPath);
|
|
|
|
const config = JSON.parse(readFileSync(mcpJsonPath, 'utf-8'));
|
|
|
|
// Top-level must have mcpServers
|
|
expect(config).toHaveProperty('mcpServers');
|
|
expect(typeof config.mcpServers).toBe('object');
|
|
|
|
// Each server must have command (string) and optionally args (array)
|
|
for (const [name, server] of Object.entries(config.mcpServers)) {
|
|
expect(typeof name).toBe('string');
|
|
expect((server as { command: string }).command).toBeDefined();
|
|
expect(typeof (server as { command: string }).command).toBe('string');
|
|
|
|
const args = (server as { args?: string[] }).args;
|
|
if (args !== undefined) {
|
|
expect(Array.isArray(args)).toBe(true);
|
|
args.forEach((arg: string) => expect(typeof arg).toBe('string'));
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('removeMcpConfig', () => {
|
|
it('removes claude-mem server from config', () => {
|
|
configureCursorMcp(mcpJsonPath, mcpServerPath);
|
|
removeMcpConfig(mcpJsonPath);
|
|
|
|
const config: CursorMcpConfig = JSON.parse(readFileSync(mcpJsonPath, 'utf-8'));
|
|
expect(config.mcpServers['claude-mem']).toBeUndefined();
|
|
});
|
|
|
|
it('preserves other servers when removing claude-mem', () => {
|
|
// Setup: both servers
|
|
mkdirSync(join(tempDir, '.cursor'), { recursive: true });
|
|
const config = {
|
|
mcpServers: {
|
|
'other-server': { command: 'python', args: ['/path.py'] },
|
|
'claude-mem': { command: 'node', args: ['/mcp.cjs'] }
|
|
}
|
|
};
|
|
writeFileSync(mcpJsonPath, JSON.stringify(config));
|
|
|
|
removeMcpConfig(mcpJsonPath);
|
|
|
|
const updated: CursorMcpConfig = JSON.parse(readFileSync(mcpJsonPath, 'utf-8'));
|
|
expect(updated.mcpServers['other-server']).toBeDefined();
|
|
expect(updated.mcpServers['claude-mem']).toBeUndefined();
|
|
});
|
|
|
|
it('does nothing if mcp.json does not exist', () => {
|
|
// Should not throw
|
|
expect(() => removeMcpConfig(mcpJsonPath)).not.toThrow();
|
|
expect(existsSync(mcpJsonPath)).toBe(false);
|
|
});
|
|
|
|
it('does nothing if claude-mem not in config', () => {
|
|
mkdirSync(join(tempDir, '.cursor'), { recursive: true });
|
|
const config = {
|
|
mcpServers: {
|
|
'other-server': { command: 'python', args: ['/path.py'] }
|
|
}
|
|
};
|
|
writeFileSync(mcpJsonPath, JSON.stringify(config));
|
|
|
|
removeMcpConfig(mcpJsonPath);
|
|
|
|
const updated: CursorMcpConfig = JSON.parse(readFileSync(mcpJsonPath, 'utf-8'));
|
|
expect(updated.mcpServers['other-server']).toBeDefined();
|
|
});
|
|
});
|
|
|
|
describe('path handling', () => {
|
|
it('handles absolute path with spaces', () => {
|
|
const pathWithSpaces = '/path/to/my project/mcp-server.cjs';
|
|
configureCursorMcp(mcpJsonPath, pathWithSpaces);
|
|
|
|
const config: CursorMcpConfig = JSON.parse(readFileSync(mcpJsonPath, 'utf-8'));
|
|
expect(config.mcpServers['claude-mem'].args).toEqual([pathWithSpaces]);
|
|
});
|
|
|
|
it('handles Windows-style path', () => {
|
|
const windowsPath = 'C:\\Users\\alex\\.claude\\plugins\\mcp-server.cjs';
|
|
configureCursorMcp(mcpJsonPath, windowsPath);
|
|
|
|
const config: CursorMcpConfig = JSON.parse(readFileSync(mcpJsonPath, 'utf-8'));
|
|
expect(config.mcpServers['claude-mem'].args).toEqual([windowsPath]);
|
|
});
|
|
|
|
it('handles path with special characters', () => {
|
|
const specialPath = "/path/to/project-name_v2.0 (beta)/mcp-server.cjs";
|
|
configureCursorMcp(mcpJsonPath, specialPath);
|
|
|
|
const config: CursorMcpConfig = JSON.parse(readFileSync(mcpJsonPath, 'utf-8'));
|
|
expect(config.mcpServers['claude-mem'].args).toEqual([specialPath]);
|
|
|
|
// Verify it survives JSON round-trip
|
|
const reread: CursorMcpConfig = JSON.parse(readFileSync(mcpJsonPath, 'utf-8'));
|
|
expect(reread.mcpServers['claude-mem'].args![0]).toBe(specialPath);
|
|
});
|
|
});
|
|
});
|