fix: normalize plugin metadata across shared launch flows

This commit is contained in:
Tam Nhu Tran committed 2026-03-17 09:56:31 -04:00
1 parent 2cb5b4ebec
commit 1043a433f6
8 files changed
+431 -40

No files matched your search

+87 -4
View File
@@ -5,11 +5,13 @@ import { describe, it, expect, beforeEach, afterEach, mock, spyOn } from 'bun:te
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import SharedManager from '../../src/management/shared-manager';
import SharedManager, {
normalizePluginMetadataPathString,
} from '../../src/management/shared-manager';
// Test the normalization regex pattern directly
const normalizePluginPaths = (content: string): string => {
return content.replace(/\/\.ccs\/instances\/[^/]+\//g, '/.claude/');
return normalizePluginMetadataPathString(content);
};
describe('SharedManager', () => {
@@ -17,12 +19,14 @@ describe('SharedManager', () => {
let originalHome: string | undefined;
let originalCcsHome: string | undefined;
let originalCcsDir: string | undefined;
let originalPlatform: PropertyDescriptor | 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;
originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform');
spyOn(os, 'homedir').mockReturnValue(tempRoot);
process.env.HOME = tempRoot;
@@ -42,6 +46,10 @@ describe('SharedManager', () => {
if (originalCcsDir !== undefined) process.env.CCS_DIR = originalCcsDir;
else delete process.env.CCS_DIR;
if (originalPlatform) {
Object.defineProperty(process, 'platform', originalPlatform);
}
if (tempRoot && fs.existsSync(tempRoot)) {
fs.rmSync(tempRoot, { recursive: true, force: true });
}
@@ -149,9 +157,8 @@ describe('SharedManager', () => {
});
it('should handle Windows-style paths (backslash)', () => {
// Windows paths use backslashes, regex should not match
const input = 'C:\\Users\\user\\.ccs\\instances\\ck\\plugins\\cache';
expect(normalizePluginPaths(input)).toBe(input);
expect(normalizePluginPaths(input)).toBe('C:\\Users\\user\\.claude\\plugins\\cache');
});
});
});
@@ -185,5 +192,81 @@ describe('SharedManager', () => {
'/home/kai/.claude/plugins/marketplaces/claude-code-plugins'
);
});
it('rewrites Windows-style known_marketplaces.json paths 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:
'C:\\Users\\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(
'C:\\Users\\kai\\.claude\\plugins\\marketplaces\\claude-code-plugins'
);
});
it('normalizes copied shared and instance metadata under Windows fallback', () => {
Object.defineProperty(process, 'platform', { value: 'win32' });
spyOn(fs, 'symlinkSync').mockImplementation(() => {
throw Object.assign(new Error('simulated symlink failure'), { code: 'EPERM' });
});
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();
const instancePath = path.join(tempRoot, '.ccs', 'instances', 'personal');
fs.mkdirSync(instancePath, { recursive: true });
manager.linkSharedDirectories(instancePath);
const expected = '/home/kai/.claude/plugins/marketplaces/claude-code-plugins';
const claudeRegistry = JSON.parse(fs.readFileSync(registryPath, 'utf8'));
const sharedRegistry = JSON.parse(
fs.readFileSync(
path.join(tempRoot, '.ccs', 'shared', 'plugins', 'known_marketplaces.json'),
'utf8'
)
);
const instanceRegistry = JSON.parse(
fs.readFileSync(path.join(instancePath, 'plugins', 'known_marketplaces.json'), 'utf8')
);
expect(claudeRegistry['claude-code-plugins'].installLocation).toBe(expected);
expect(sharedRegistry['claude-code-plugins'].installLocation).toBe(expected);
expect(instanceRegistry['claude-code-plugins'].installLocation).toBe(expected);
});
});
});