mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-14 06:17:27 +00:00
fix(mcp): retry Claude user config locks
This commit is contained in:
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 {
|
||||
|
||||
Reference in new issue
Block a user