fix(management): serialize marketplace registry reconciliation

- keep bare and non-bare marketplace normalization inside the plugin-layout lock
- warn when malformed registry sources are skipped during reconciliation
- add regression coverage for cross-instance refresh metadata and legacy layout upgrade
This commit is contained in:
Tam Nhu Tran
2026-03-18 07:34:05 -04:00
parent 242a095edb
commit 68a5d17327
4 changed files with 156 additions and 36 deletions
+7 -6
View File
@@ -59,14 +59,15 @@ class InstanceManager {
await this.sharedManager.syncProjectContext(instancePath, contextPolicy);
await this.sharedManager.syncAdvancedContinuityArtifacts(instancePath, contextPolicy);
if (!options.bare) {
await this.pluginLayoutLock.withNamedLock('__plugin-layout__', async () => {
await this.pluginLayoutLock.withNamedLock('__plugin-layout__', async () => {
if (!options.bare) {
this.sharedManager.linkSharedDirectories(instancePath);
});
}
});
return;
}
this.sharedManager.normalizeSharedPluginMetadataPaths(options.bare ? undefined : instancePath);
this.sharedManager.normalizeSharedPluginMetadataPaths();
});
});
// Sync MCP servers from global ~/.claude.json (unless bare)
if (!options.bare) {
+4 -2
View File
@@ -899,8 +899,10 @@ class SharedManager {
for (const [name, value] of Object.entries(parsed as Record<string, unknown>)) {
merged[name] = normalizePluginMetadataValue(value, targetConfigDir).normalized;
}
} catch (_err) {
// Best-effort merge: malformed sources should not block self-heal.
} catch (err) {
console.log(
warn(`Skipping malformed marketplace registry ${registryPath}: ${(err as Error).message}`)
);
}
}
+107 -28
View File
@@ -34,6 +34,28 @@ describe('InstanceManager MCP sync', () => {
);
}
function writeMarketplaceRegistryWithMetadata(
registryPath: string,
installLocation: string,
metadata: Record<string, unknown>
): void {
fs.mkdirSync(path.dirname(registryPath), { recursive: true });
fs.writeFileSync(
registryPath,
JSON.stringify(
{
'claude-code-plugins': {
installLocation,
...metadata,
},
},
null,
2
),
'utf8'
);
}
function expectMarketplaceLocation(registryPath: string, expectedLocation: string): void {
const parsed = readJson(registryPath) as Record<string, { installLocation?: string }>;
expect(parsed['claude-code-plugins']?.installLocation).toBe(expectedLocation);
@@ -226,43 +248,100 @@ describe('InstanceManager MCP sync', () => {
expect(syncMcpSpy).toHaveBeenCalledWith(instancePath);
});
it('keeps alternating instances independently valid for Claude Code validation', async () => {
it('reconciles marketplace metadata across isolated instances without losing refresh fields', async () => {
spyOn(SharedManager.prototype, 'syncProjectContext').mockResolvedValue(undefined);
spyOn(SharedManager.prototype, 'syncAdvancedContinuityArtifacts').mockResolvedValue(undefined);
spyOn(InstanceManager.prototype, 'syncMcpServers').mockImplementation(() => false);
const globalRegistryPath = path.join(claudeDir(), 'plugins', 'known_marketplaces.json');
writeMarketplaceRegistry(
globalRegistryPath,
path.join(tempRoot, '.claude', 'plugins', 'marketplaces', 'claude-code-plugins')
);
const manager = new InstanceManager();
const workPath = await manager.ensureInstance('work', { mode: 'isolated' });
const workRegistryPath = path.join(workPath, 'plugins', 'known_marketplaces.json');
writeMarketplaceRegistryWithMetadata(workRegistryPath, marketplacePath(workPath), {
label: 'Official marketplace',
refreshToken: 'refresh-token',
metadata: {
source: 'refresh-flow',
lastSyncedAt: '2026-03-18T00:00:00Z',
},
});
const personalPath = await manager.ensureInstance('personal', { mode: 'isolated' });
await manager.ensureInstance('work', { mode: 'isolated' });
const workRegistry = readJson(workRegistryPath) as Record<
string,
{
installLocation?: string;
label?: string;
refreshToken?: string;
metadata?: Record<string, unknown>;
}
>;
const personalRegistry = readJson(path.join(personalPath, 'plugins', 'known_marketplaces.json')) as Record<
string,
{
installLocation?: string;
label?: string;
refreshToken?: string;
metadata?: Record<string, unknown>;
}
>;
const workInstallLocation = (
readJson(path.join(workPath, 'plugins', 'known_marketplaces.json')) as Record<
string,
{ installLocation?: string }
>
)['claude-code-plugins']?.installLocation;
const personalInstallLocation = (
readJson(path.join(personalPath, 'plugins', 'known_marketplaces.json')) as Record<
string,
{ installLocation?: string }
>
)['claude-code-plugins']?.installLocation;
expect(workRegistry['claude-code-plugins']).toMatchObject({
installLocation: marketplacePath(workPath),
label: 'Official marketplace',
refreshToken: 'refresh-token',
metadata: {
source: 'refresh-flow',
lastSyncedAt: '2026-03-18T00:00:00Z',
},
});
expect(personalRegistry['claude-code-plugins']).toMatchObject({
installLocation: marketplacePath(personalPath),
label: 'Official marketplace',
refreshToken: 'refresh-token',
metadata: {
source: 'refresh-flow',
lastSyncedAt: '2026-03-18T00:00:00Z',
},
});
});
expect(workInstallLocation).toBe(marketplacePath(workPath));
expect(personalInstallLocation).toBe(marketplacePath(personalPath));
expect(path.resolve(workInstallLocation ?? '')).toStartWith(
path.resolve(path.join(workPath, 'plugins', 'marketplaces'))
it('upgrades a legacy shared plugins symlink to an instance-local layout', async () => {
spyOn(SharedManager.prototype, 'syncProjectContext').mockResolvedValue(undefined);
spyOn(SharedManager.prototype, 'syncAdvancedContinuityArtifacts').mockResolvedValue(undefined);
const manager = new InstanceManager();
const legacyPath = manager.getInstancePath('legacy');
const sharedPluginsPath = path.join(tempRoot, '.ccs', 'shared', 'plugins');
fs.mkdirSync(sharedPluginsPath, { recursive: true });
fs.mkdirSync(legacyPath, { recursive: true });
fs.symlinkSync(sharedPluginsPath, path.join(legacyPath, 'plugins'), 'dir');
writeMarketplaceRegistryWithMetadata(
path.join(claudeDir(), 'plugins', 'known_marketplaces.json'),
path.join(tempRoot, '.ccs', 'shared', 'plugins', 'marketplaces', 'claude-code-plugins'),
{
label: 'Legacy marketplace',
refreshToken: 'legacy-refresh-token',
}
);
expect(path.resolve(personalInstallLocation ?? '')).toStartWith(
path.resolve(path.join(personalPath, 'plugins', 'marketplaces'))
await manager.ensureInstance('legacy', { mode: 'isolated' });
expect(fs.lstatSync(path.join(legacyPath, 'plugins')).isSymbolicLink()).toBe(false);
expectMarketplaceLocation(
path.join(legacyPath, 'plugins', 'known_marketplaces.json'),
marketplacePath(legacyPath)
);
expectMarketplaceLocation(globalRegistryPath, marketplacePath(claudeDir()));
const legacyRegistry = readJson(
path.join(legacyPath, 'plugins', 'known_marketplaces.json')
) as Record<
string,
{ installLocation?: string; label?: string; refreshToken?: string }
>;
expect(legacyRegistry['claude-code-plugins']).toMatchObject({
installLocation: marketplacePath(legacyPath),
label: 'Legacy marketplace',
refreshToken: 'legacy-refresh-token',
});
});
});
+38
View File
@@ -204,6 +204,44 @@ describe('SharedManager', () => {
});
});
it('warns and skips malformed marketplace registries while keeping valid sources', () => {
const manager = new SharedManager();
const instancePath = instanceDir('work');
fs.mkdirSync(instancePath, { recursive: true });
manager.linkSharedDirectories(instancePath);
const globalRegistryPath = path.join(claudeDir(), 'plugins', 'known_marketplaces.json');
writeJson(globalRegistryPath, {
'claude-code-plugins': {
installLocation: path.join(
tempRoot,
'.ccs',
'instances',
'work',
'plugins',
'marketplaces',
'claude-code-plugins'
),
label: 'Official marketplace',
},
});
const malformedRegistryPath = path.join(instancePath, 'plugins', 'known_marketplaces.json');
fs.writeFileSync(malformedRegistryPath, '{invalid-json', 'utf8');
const logSpy = spyOn(console, 'log').mockImplementation(() => {});
manager.normalizeMarketplaceRegistryPaths(instancePath);
expect(readMarketplaceLocation(malformedRegistryPath)).toBe(marketplacePath(instancePath));
expect(
logSpy.mock.calls.some(
([message]) =>
String(message).includes('Skipping malformed marketplace registry') &&
String(message).includes(malformedRegistryPath)
)
).toBe(true);
});
it('keeps the instance-local registry valid under Windows copy fallback', () => {
Object.defineProperty(process, 'platform', { value: 'win32' });
spyOn(fs, 'symlinkSync').mockImplementation(() => {