fix(mcp): retry Claude user config locks

This commit is contained in:
walker1211 authored and GitHub committed 2026-06-10 13:03:25 -04:00
1 parent f59536a81f
commit cf90f48240
6 files changed
+313 -176

No files matched your search

@@ -194,26 +194,54 @@ describe('ensureImageAnalysisMcp', () => {
expect(lockSpy.mock.calls[0]?.[0]).toBe(path.join(tempHome as string, '.claude.json.ccs-lock'));
});
it('returns false instead of throwing when ~/.claude.json is already locked', () => {
it('retries ~/.claude.json lock acquisition before provisioning', () => {
setupTempHome();
writeEnabledConfig();
const claudeUserConfigPath = path.join(tempHome as string, '.claude.json');
fs.writeFileSync(claudeUserConfigPath, '{}\n', 'utf8');
let calls = 0;
spyOn(lockfile, 'lockSync').mockImplementation(() => {
calls++;
if (calls === 1) {
const error = new Error('Lock file is already being held') as NodeJS.ErrnoException;
error.code = 'ELOCKED';
throw error;
}
return (() => undefined) as ReturnType<typeof lockfile.lockSync>;
});
expect(ensureImageAnalysisMcp()).toBe(true);
expect(calls).toBe(2);
const config = JSON.parse(fs.readFileSync(claudeUserConfigPath, 'utf8')) as {
mcpServers: Record<string, unknown>;
};
expect(config.mcpServers[getImageAnalysisMcpServerName()]).toEqual(getManagedConfig());
});
it('returns false instead of throwing when ~/.claude.json stays locked', () => {
setupTempHome();
writeEnabledConfig();
const claudeUserConfigPath = path.join(tempHome as string, '.claude.json');
fs.writeFileSync(claudeUserConfigPath, '{}\n', 'utf8');
fs.writeFileSync(path.join(tempHome as string, '.claude.json.ccs-lock'), '', 'utf8');
const release = lockfile.lockSync(path.join(tempHome as string, '.claude.json.ccs-lock'), {
stale: 10000,
}) as () => void;
let now = 0;
spyOn(Date, 'now').mockImplementation(() => {
now += 10001;
return now;
});
spyOn(lockfile, 'lockSync').mockImplementation(() => {
const error = new Error('Lock file is already being held') as NodeJS.ErrnoException;
error.code = 'ELOCKED';
throw error;
});
try {
let result: boolean | undefined;
expect(() => {
result = ensureImageAnalysisMcp();
}).not.toThrow();
expect(result).toBe(false);
} finally {
release();
}
let result: boolean | undefined;
expect(() => {
result = ensureImageAnalysisMcp();
}).not.toThrow();
expect(result).toBe(false);
});
});
@@ -2,6 +2,7 @@ import { afterEach, describe, expect, it, mock, spyOn } from 'bun:test';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import * as lockfile from 'proper-lockfile';
import { getHookPath } from '../../../../src/utils/websearch/hook-config';
import {
ensureWebSearchMcp,
@@ -33,7 +34,15 @@ describe('ensureWebSearchMcp', () => {
fs.mkdirSync(ccsDir, { recursive: true });
fs.writeFileSync(
path.join(ccsDir, 'config.yaml'),
['version: 12', 'websearch:', ' enabled: true', ' providers:', ' duckduckgo:', ' enabled: true', ''].join('\n'),
[
'version: 12',
'websearch:',
' enabled: true',
' providers:',
' duckduckgo:',
' enabled: true',
'',
].join('\n'),
'utf8'
);
}
@@ -95,6 +104,19 @@ describe('ensureWebSearchMcp', () => {
expect(config.mcpServers[getWebSearchMcpServerName()]).toEqual(getManagedConfig());
});
it('serializes ~/.claude.json updates with a file lock', () => {
setupTempHome();
writeEnabledConfig();
const claudeUserConfigPath = path.join(tempHome as string, '.claude.json');
fs.writeFileSync(claudeUserConfigPath, '{}\n', 'utf8');
const lockSpy = spyOn(lockfile, 'lockSync');
expect(ensureWebSearchMcp()).toBe(true);
expect(lockSpy).toHaveBeenCalled();
expect(lockSpy.mock.calls[0]?.[0]).toBe(path.join(tempHome as string, '.claude.json.ccs-lock'));
});
it('preserves the existing ~/.claude.json permissions when provisioning WebSearch MCP', () => {
setupTempHome();
writeEnabledConfig();
@@ -170,7 +192,11 @@ describe('ensureWebSearchMcp', () => {
'utf8'
);
const lockSpy = spyOn(lockfile, 'lockSync');
expect(uninstallWebSearchMcp()).toBe(true);
expect(lockSpy).toHaveBeenCalled();
expect(lockSpy.mock.calls[0]?.[0]).toBe(path.join(tempHome as string, '.claude.json.ccs-lock'));
expect(fs.existsSync(getWebSearchMcpServerPath())).toBe(false);
const globalConfig = JSON.parse(fs.readFileSync(claudeUserConfigPath, 'utf8')) as {