fix(shared-manager): normalize marketplace registry paths

This commit is contained in:
Tam Nhu Tran
2026-03-17 09:56:31 -04:00
parent 7b1d7baefd
commit 2cb5b4ebec
4 changed files with 149 additions and 11 deletions
@@ -101,6 +101,10 @@ describe('InstanceManager MCP sync', () => {
);
spyOn(SharedManager.prototype, 'syncProjectContext').mockResolvedValue(undefined);
spyOn(SharedManager.prototype, 'syncAdvancedContinuityArtifacts').mockResolvedValue(undefined);
const normalizeSharedPluginMetadataSpy = spyOn(
SharedManager.prototype,
'normalizeSharedPluginMetadataPaths'
).mockImplementation(() => {});
const syncMcpSpy = spyOn(InstanceManager.prototype, 'syncMcpServers').mockImplementation(
() => false
);
@@ -109,6 +113,28 @@ describe('InstanceManager MCP sync', () => {
await manager.ensureInstance('sandbox', { mode: 'isolated' }, { bare: true });
expect(linkSharedSpy).not.toHaveBeenCalled();
expect(normalizeSharedPluginMetadataSpy).not.toHaveBeenCalled();
expect(syncMcpSpy).not.toHaveBeenCalled();
});
it('normalizes shared plugin metadata for existing non-bare instances', async () => {
spyOn(SharedManager.prototype, 'syncProjectContext').mockResolvedValue(undefined);
spyOn(SharedManager.prototype, 'syncAdvancedContinuityArtifacts').mockResolvedValue(undefined);
const normalizeSharedPluginMetadataSpy = spyOn(
SharedManager.prototype,
'normalizeSharedPluginMetadataPaths'
).mockImplementation(() => {});
const syncMcpSpy = spyOn(InstanceManager.prototype, 'syncMcpServers').mockImplementation(
() => false
);
const manager = new InstanceManager();
const instancePath = manager.getInstancePath('work');
fs.mkdirSync(instancePath, { recursive: true });
await manager.ensureInstance('work', { mode: 'isolated' });
expect(normalizeSharedPluginMetadataSpy).toHaveBeenCalledTimes(1);
expect(syncMcpSpy).toHaveBeenCalledWith(instancePath);
});
});
+85 -1
View File
@@ -1,10 +1,11 @@
/**
* Unit tests for SharedManager - plugin registry path normalization
*/
import { describe, it, expect, beforeEach, afterEach } from 'bun:test';
import { describe, it, expect, beforeEach, afterEach, mock, spyOn } from 'bun:test';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import SharedManager from '../../src/management/shared-manager';
// Test the normalization regex pattern directly
const normalizePluginPaths = (content: string): string => {
@@ -12,6 +13,40 @@ const normalizePluginPaths = (content: string): string => {
};
describe('SharedManager', () => {
let tempRoot = '';
let originalHome: string | undefined;
let originalCcsHome: string | undefined;
let originalCcsDir: string | undefined;
beforeEach(() => {
tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-shared-manager-test-'));
originalHome = process.env.HOME;
originalCcsHome = process.env.CCS_HOME;
originalCcsDir = process.env.CCS_DIR;
spyOn(os, 'homedir').mockReturnValue(tempRoot);
process.env.HOME = tempRoot;
process.env.CCS_HOME = tempRoot;
delete process.env.CCS_DIR;
});
afterEach(() => {
mock.restore();
if (originalHome !== undefined) process.env.HOME = originalHome;
else delete process.env.HOME;
if (originalCcsHome !== undefined) process.env.CCS_HOME = originalCcsHome;
else delete process.env.CCS_HOME;
if (originalCcsDir !== undefined) process.env.CCS_DIR = originalCcsDir;
else delete process.env.CCS_DIR;
if (tempRoot && fs.existsSync(tempRoot)) {
fs.rmSync(tempRoot, { recursive: true, force: true });
}
});
describe('normalizePluginRegistryPaths', () => {
describe('regex pattern', () => {
it('should replace instance paths with canonical claude path', () => {
@@ -82,6 +117,24 @@ describe('SharedManager', () => {
'/home/kai/.claude/plugins/cache/claude-hud/claude-hud/0.0.2'
);
});
it('should normalize marketplace installLocation values', () => {
const original = {
'claude-code-plugins': {
installLocation:
'/home/kai/.ccs/instances/work/plugins/marketplaces/claude-code-plugins',
},
};
const input = JSON.stringify(original, null, 2);
const result = normalizePluginPaths(input);
expect(() => JSON.parse(result)).not.toThrow();
const parsed = JSON.parse(result);
expect(parsed['claude-code-plugins'].installLocation).toBe(
'/home/kai/.claude/plugins/marketplaces/claude-code-plugins'
);
});
});
describe('edge cases', () => {
@@ -102,4 +155,35 @@ describe('SharedManager', () => {
});
});
});
describe('normalizeMarketplaceRegistryPaths', () => {
it('rewrites known_marketplaces.json on disk', () => {
const pluginsDir = path.join(tempRoot, '.claude', 'plugins');
fs.mkdirSync(pluginsDir, { recursive: true });
const registryPath = path.join(pluginsDir, 'known_marketplaces.json');
fs.writeFileSync(
registryPath,
JSON.stringify(
{
'claude-code-plugins': {
installLocation:
'/home/kai/.ccs/instances/work/plugins/marketplaces/claude-code-plugins',
},
},
null,
2
),
'utf8'
);
const manager = new SharedManager();
manager.normalizeMarketplaceRegistryPaths();
const normalized = JSON.parse(fs.readFileSync(registryPath, 'utf8'));
expect(normalized['claude-code-plugins'].installLocation).toBe(
'/home/kai/.claude/plugins/marketplaces/claude-code-plugins'
);
});
});
});