fix(auth): preserve bare profile behavior across runtime and sync

This commit is contained in:
Tam Nhu Tran
2026-03-05 12:36:51 +07:00
parent bc9b04444e
commit 8f8684ce85
14 changed files with 355 additions and 34 deletions
+12 -7
View File
@@ -111,6 +111,9 @@ export async function handleCreate(ctx: CommandContext, args: string[]): Promise
const previousUnifiedProfile = existsUnified const previousUnifiedProfile = existsUnified
? ctx.registry.getAllAccountsUnified()[profileName] ? ctx.registry.getAllAccountsUnified()[profileName]
: undefined; : undefined;
const previousBare =
previousLegacyProfile?.bare === true || previousUnifiedProfile?.bare === true;
const effectiveBare = bare === true || (profileExistedBeforeCreate && previousBare);
const previousContextPolicy = const previousContextPolicy =
profileExistedBeforeCreate && (previousUnifiedProfile || previousLegacyProfile) profileExistedBeforeCreate && (previousUnifiedProfile || previousLegacyProfile)
? resolveAccountContextPolicy(previousUnifiedProfile || previousLegacyProfile) ? resolveAccountContextPolicy(previousUnifiedProfile || previousLegacyProfile)
@@ -169,7 +172,9 @@ export async function handleCreate(ctx: CommandContext, args: string[]): Promise
if (previousContextPolicy) { if (previousContextPolicy) {
try { try {
await ctx.instanceMgr.ensureInstance(profileName, previousContextPolicy); await ctx.instanceMgr.ensureInstance(profileName, previousContextPolicy, {
bare: previousBare,
});
} catch { } catch {
// Best-effort rollback for context mode/group. // Best-effort rollback for context mode/group.
} }
@@ -180,7 +185,7 @@ export async function handleCreate(ctx: CommandContext, args: string[]): Promise
// Create instance directory // Create instance directory
console.log(info(`Creating profile: ${profileName}`)); console.log(info(`Creating profile: ${profileName}`));
const instancePath = await ctx.instanceMgr.ensureInstance(profileName, contextPolicy, { const instancePath = await ctx.instanceMgr.ensureInstance(profileName, contextPolicy, {
bare: !!bare, bare: effectiveBare,
}); });
// Create/update profile entry based on config mode // Create/update profile entry based on config mode
@@ -190,13 +195,13 @@ export async function handleCreate(ctx: CommandContext, args: string[]): Promise
ctx.registry.updateAccountUnified(profileName, { ctx.registry.updateAccountUnified(profileName, {
context_mode: contextMetadata.context_mode, context_mode: contextMetadata.context_mode,
context_group: contextMetadata.context_group, context_group: contextMetadata.context_group,
...(bare ? { bare: true } : {}), ...(effectiveBare ? { bare: true } : {}),
}); });
ctx.registry.touchAccountUnified(profileName); ctx.registry.touchAccountUnified(profileName);
} else { } else {
ctx.registry.createAccountUnified(profileName, { ctx.registry.createAccountUnified(profileName, {
...contextMetadata, ...contextMetadata,
...(bare ? { bare: true } : {}), ...(effectiveBare ? { bare: true } : {}),
}); });
} }
} else { } else {
@@ -206,14 +211,14 @@ export async function handleCreate(ctx: CommandContext, args: string[]): Promise
type: 'account', type: 'account',
context_mode: contextMetadata.context_mode, context_mode: contextMetadata.context_mode,
context_group: contextMetadata.context_group, context_group: contextMetadata.context_group,
...(bare ? { bare: true } : {}), ...(effectiveBare ? { bare: true } : {}),
}); });
} else { } else {
ctx.registry.createProfile(profileName, { ctx.registry.createProfile(profileName, {
type: 'account', type: 'account',
context_mode: contextMetadata.context_mode, context_mode: contextMetadata.context_mode,
context_group: contextMetadata.context_group, context_group: contextMetadata.context_group,
...(bare ? { bare: true } : {}), ...(effectiveBare ? { bare: true } : {}),
}); });
} }
} }
@@ -271,7 +276,7 @@ export async function handleCreate(ctx: CommandContext, args: string[]): Promise
`Instance: ${instancePath}\n` + `Instance: ${instancePath}\n` +
`Type: account\n` + `Type: account\n` +
`Context: ${formatAccountContextPolicy(contextPolicy)}` + `Context: ${formatAccountContextPolicy(contextPolicy)}` +
(bare ? '\nMode: bare (no shared symlinks)' : ''), (effectiveBare ? '\nMode: bare (no shared symlinks)' : ''),
'Profile Created' 'Profile Created'
) )
); );
+3 -1
View File
@@ -146,7 +146,9 @@ export async function resolveProfileContinuityInheritance(
isAccountContextMetadata(mappedProfile) ? mappedProfile : undefined isAccountContextMetadata(mappedProfile) ? mappedProfile : undefined
); );
const instanceMgr = new InstanceManager(); const instanceMgr = new InstanceManager();
const instancePath = await instanceMgr.ensureInstance(sourceAccount, contextPolicy); const instancePath = await instanceMgr.ensureInstance(sourceAccount, contextPolicy, {
bare: mappedProfile.bare === true,
});
return { return {
sourceAccount, sourceAccount,
+1
View File
@@ -203,6 +203,7 @@ class ProfileDetector {
context_mode: account.context_mode, context_mode: account.context_mode,
context_group: account.context_group, context_group: account.context_group,
continuity_mode: account.continuity_mode, continuity_mode: account.continuity_mode,
bare: account.bare,
}, },
}; };
} }
+4
View File
@@ -45,6 +45,7 @@ interface CreateMetadata {
context_mode?: 'isolated' | 'shared'; context_mode?: 'isolated' | 'shared';
context_group?: string; context_group?: string;
continuity_mode?: 'standard' | 'deeper'; continuity_mode?: 'standard' | 'deeper';
bare?: boolean;
} }
export class ProfileRegistry { export class ProfileRegistry {
@@ -173,6 +174,7 @@ export class ProfileRegistry {
context_mode: metadata.context_mode, context_mode: metadata.context_mode,
context_group: metadata.context_group, context_group: metadata.context_group,
continuity_mode: metadata.continuity_mode, continuity_mode: metadata.continuity_mode,
bare: metadata.bare,
}); });
// Note: No longer auto-set as default // Note: No longer auto-set as default
@@ -321,6 +323,7 @@ export class ProfileRegistry {
context_mode: metadata.context_mode, context_mode: metadata.context_mode,
context_group: metadata.context_group, context_group: metadata.context_group,
continuity_mode: metadata.continuity_mode, continuity_mode: metadata.continuity_mode,
bare: metadata.bare,
}); });
saveUnifiedConfig(config); saveUnifiedConfig(config);
} }
@@ -449,6 +452,7 @@ export class ProfileRegistry {
context_mode: account.context_mode, context_mode: account.context_mode,
context_group: account.context_group, context_group: account.context_group,
continuity_mode: account.continuity_mode, continuity_mode: account.continuity_mode,
bare: account.bare,
}; };
} }
+7 -1
View File
@@ -1132,10 +1132,16 @@ async function main(): Promise<void> {
const accountMetadata = isAccountContextMetadata(profileInfo.profile) const accountMetadata = isAccountContextMetadata(profileInfo.profile)
? profileInfo.profile ? profileInfo.profile
: undefined; : undefined;
const isBareProfile =
typeof profileInfo.profile === 'object' &&
profileInfo.profile !== null &&
(profileInfo.profile as { bare?: unknown }).bare === true;
const contextPolicy = resolveAccountContextPolicy(accountMetadata); const contextPolicy = resolveAccountContextPolicy(accountMetadata);
// Ensure instance exists (lazy init if needed) // Ensure instance exists (lazy init if needed)
const instancePath = await instanceMgr.ensureInstance(profileInfo.name, contextPolicy); const instancePath = await instanceMgr.ensureInstance(profileInfo.name, contextPolicy, {
bare: isBareProfile,
});
// Update last_used timestamp (check unified config first, fallback to legacy) // Update last_used timestamp (check unified config first, fallback to legacy)
if (registry.hasAccountUnified(profileInfo.name)) { if (registry.hasAccountUnified(profileInfo.name)) {
+6 -2
View File
@@ -53,9 +53,13 @@ export async function handleSyncCommand(): Promise<void> {
if (profile.bare) { if (profile.bare) {
continue; // Skip bare profiles continue; // Skip bare profiles
} }
if (!instanceMgr.hasInstance(name)) {
continue;
}
const instancePath = instanceMgr.getInstancePath(name); const instancePath = instanceMgr.getInstancePath(name);
if (instancePath) { if (instanceMgr.syncMcpServers(instancePath)) {
instanceMgr.syncMcpServers(instancePath);
mcpSynced++; mcpSynced++;
} }
} }
+25 -20
View File
@@ -14,7 +14,7 @@ import { AccountContextPolicy, DEFAULT_ACCOUNT_CONTEXT_MODE } from '../auth/acco
import { getCcsDir, getCcsHome } from '../utils/config-manager'; import { getCcsDir, getCcsHome } from '../utils/config-manager';
/** Options for instance creation */ /** Options for instance creation */
interface InstanceOptions { export interface InstanceOptions {
/** Skip shared symlinks (commands, skills, agents, settings.json) */ /** Skip shared symlinks (commands, skills, agents, settings.json) */
bare?: boolean; bare?: boolean;
} }
@@ -108,9 +108,6 @@ class InstanceManager {
if (!options.bare) { if (!options.bare) {
this.sharedManager.linkSharedDirectories(instancePath); this.sharedManager.linkSharedDirectories(instancePath);
} }
// Copy global configs if exist (settings.json only)
this.copyGlobalConfigs(instancePath);
} catch (error) { } catch (error) {
throw new Error( throw new Error(
`Failed to initialize instance for ${profileName}: ${(error as Error).message}` `Failed to initialize instance for ${profileName}: ${(error as Error).message}`
@@ -180,33 +177,31 @@ class InstanceManager {
return fs.existsSync(instancePath); return fs.existsSync(instancePath);
} }
/**
* Copy global configs to instance (optional)
*/
private copyGlobalConfigs(_instancePath: string): void {
// No longer needed - settings.json now symlinked via SharedManager
}
/** /**
* Sync MCP servers from global ~/.claude.json to instance .claude.json. * Sync MCP servers from global ~/.claude.json to instance .claude.json.
* Selectively copies only mcpServers key (not OAuth sessions or caches). * Selectively copies only mcpServers key (not OAuth sessions or caches).
*/ */
syncMcpServers(instancePath: string): void { syncMcpServers(instancePath: string): boolean {
const homeDir = getCcsHome(); const homeDir = getCcsHome();
const globalClaudeJson = path.join(homeDir, '.claude.json'); const globalClaudeJson = path.join(homeDir, '.claude.json');
if (!fs.existsSync(globalClaudeJson)) { if (!fs.existsSync(globalClaudeJson)) {
return; return false;
} }
try { try {
const globalContent = JSON.parse(fs.readFileSync(globalClaudeJson, 'utf8')); const globalContent = JSON.parse(fs.readFileSync(globalClaudeJson, 'utf8'));
const mcpServers = globalContent.mcpServers; const rawMcpServers = globalContent.mcpServers;
if (
if (!mcpServers || Object.keys(mcpServers).length === 0) { !rawMcpServers ||
return; typeof rawMcpServers !== 'object' ||
Array.isArray(rawMcpServers) ||
Object.keys(rawMcpServers).length === 0
) {
return false;
} }
const mcpServers = rawMcpServers as Record<string, unknown>;
const instanceClaudeJson = path.join(instancePath, '.claude.json'); const instanceClaudeJson = path.join(instancePath, '.claude.json');
let instanceContent: Record<string, unknown> = {}; let instanceContent: Record<string, unknown> = {};
@@ -220,12 +215,22 @@ class InstanceManager {
} }
// Merge: global MCP servers as base, instance-specific overrides on top // Merge: global MCP servers as base, instance-specific overrides on top
const existingMcp = (instanceContent.mcpServers as Record<string, unknown> | undefined) || {}; const rawExistingMcp = instanceContent.mcpServers;
const existingMcp =
rawExistingMcp && typeof rawExistingMcp === 'object' && !Array.isArray(rawExistingMcp)
? (rawExistingMcp as Record<string, unknown>)
: {};
instanceContent.mcpServers = { ...mcpServers, ...existingMcp }; instanceContent.mcpServers = { ...mcpServers, ...existingMcp };
fs.writeFileSync(instanceClaudeJson, JSON.stringify(instanceContent, null, 2), 'utf8'); fs.writeFileSync(instanceClaudeJson, JSON.stringify(instanceContent, null, 2), {
} catch { encoding: 'utf8',
mode: 0o600,
});
return true;
} catch (error) {
// Best-effort: don't fail instance creation if MCP sync fails // Best-effort: don't fail instance creation if MCP sync fails
console.warn(`[!] MCP sync skipped: ${(error as Error).message}`);
return false;
} }
} }
+2 -1
View File
@@ -268,6 +268,7 @@ router.put('/:name/context', async (req: Request, res: Response): Promise<void>
const previousUnified = existsUnified ? registry.getAllAccountsUnified()[name] : undefined; const previousUnified = existsUnified ? registry.getAllAccountsUnified()[name] : undefined;
const previousLegacy = existsLegacy ? registry.getProfile(name) : undefined; const previousLegacy = existsLegacy ? registry.getProfile(name) : undefined;
const isBare = previousUnified?.bare === true || previousLegacy?.bare === true;
try { try {
if (existsUnified) { if (existsUnified) {
@@ -277,7 +278,7 @@ router.put('/:name/context', async (req: Request, res: Response): Promise<void>
registry.updateProfile(name, metadata); registry.updateProfile(name, metadata);
} }
await instanceMgr.ensureInstance(name, policy); await instanceMgr.ensureInstance(name, policy, { bare: isBare });
} catch (error) { } catch (error) {
if (existsUnified && previousUnified) { if (existsUnified && previousUnified) {
registry.updateAccountUnified(name, previousUnified); registry.updateAccountUnified(name, previousUnified);
+16
View File
@@ -47,6 +47,22 @@ describe('auth command args parsing', () => {
expect(parsed.deeperContinuity).toBe(true); expect(parsed.deeperContinuity).toBe(true);
}); });
it('parses bare flag for create command', () => {
const parsed = parseArgs(['work', '--bare']);
expect(parsed.profileName).toBe('work');
expect(parsed.bare).toBe(true);
});
it('parses bare flag with shared context flags', () => {
const parsed = parseArgs(['work', '--bare', '--share-context', '--context-group', 'sprint-a']);
expect(parsed.profileName).toBe('work');
expect(parsed.bare).toBe(true);
expect(parsed.shareContext).toBe(true);
expect(parsed.contextGroup).toBe('sprint-a');
});
it('tracks unknown flags and keeps positional profile intact', () => { it('tracks unknown flags and keeps positional profile intact', () => {
const parsed = parseArgs(['--foo', 'bar', 'work']); const parsed = parseArgs(['--foo', 'bar', 'work']);
@@ -61,7 +61,7 @@ describe('resolveProfileContinuityInheritance', () => {
mode: 'shared', mode: 'shared',
group: 'team-alpha', group: 'team-alpha',
continuityMode: 'deeper', continuityMode: 'deeper',
}); }, { bare: false });
}); });
it('supports legacy continuity_inherit_from_account fallback', async () => { it('supports legacy continuity_inherit_from_account fallback', async () => {
@@ -255,7 +255,46 @@ describe('resolveProfileContinuityInheritance', () => {
}); });
expect(ensureInstanceSpy).toHaveBeenCalledWith('pro', { expect(ensureInstanceSpy).toHaveBeenCalledWith('pro', {
mode: 'isolated', mode: 'isolated',
}, { bare: false });
});
it('propagates bare source-account mode when inheriting continuity', async () => {
spyOn(configLoader, 'loadOrCreateUnifiedConfig').mockReturnValue({
version: 8,
continuity: {
inherit_from_account: {
glm: 'pro',
},
},
} as ReturnType<typeof configLoader.loadOrCreateUnifiedConfig>);
const ensureInstanceSpy = spyOn(InstanceManager.prototype, 'ensureInstance').mockResolvedValue(
'/tmp/.ccs/instances/pro'
);
spyOn(ProfileRegistry.prototype, 'getAllProfilesMerged').mockReturnValue({
pro: {
type: 'account',
created: '2026-03-01T00:00:00.000Z',
last_used: null,
bare: true,
},
}); });
const result = await resolveProfileContinuityInheritance({
profileName: 'glm',
profileType: 'settings',
target: 'claude',
});
expect(result).toEqual({
sourceAccount: 'pro',
claudeConfigDir: '/tmp/.ccs/instances/pro',
});
expect(ensureInstanceSpy).toHaveBeenCalledWith(
'pro',
{ mode: 'isolated' },
{ bare: true }
);
}); });
it('does not apply km settings alias mapping to kimi cliproxy profile', async () => { it('does not apply km settings alias mapping to kimi cliproxy profile', async () => {
+2 -1
View File
@@ -161,7 +161,7 @@ describe('ProfileDetector', () => {
const mockUnifiedConfig = { const mockUnifiedConfig = {
version: 2, version: 2,
accounts: { accounts: {
work: { created: '2025-01-01', last_used: '2025-01-02' }, work: { created: '2025-01-01', last_used: '2025-01-02', bare: true },
}, },
}; };
@@ -176,6 +176,7 @@ describe('ProfileDetector', () => {
expect(result.name).toBe('work'); expect(result.name).toBe('work');
expect(result.profile).toBeDefined(); expect(result.profile).toBeDefined();
expect((result.profile as any).type).toBe('account'); expect((result.profile as any).type).toBe('account');
expect((result.profile as any).bare).toBe(true);
} finally { } finally {
isUnifiedModeSpy.mockRestore(); isUnifiedModeSpy.mockRestore();
loadUnifiedConfigSpy.mockRestore(); loadUnifiedConfigSpy.mockRestore();
@@ -91,4 +91,43 @@ describe('profile-registry context normalization', () => {
expect(accounts.work.context_group).toBeUndefined(); expect(accounts.work.context_group).toBeUndefined();
expect(accounts.work.continuity_mode).toBe('standard'); expect(accounts.work.continuity_mode).toBe('standard');
}); });
it('persists bare flag for legacy profiles', () => {
const registry = new ProfileRegistry();
registry.createProfile('work', { type: 'account', bare: true });
const profile = registry.getProfile('work');
expect(profile.bare).toBe(true);
});
it('persists bare flag for unified accounts and merged projection', () => {
process.env.CCS_UNIFIED_CONFIG = '1';
const ccsDir = path.join(tempHome, '.ccs');
fs.mkdirSync(ccsDir, { recursive: true });
fs.writeFileSync(
path.join(ccsDir, 'config.yaml'),
[
'version: 8',
'accounts:',
' work:',
' created: "2026-03-05T00:00:00.000Z"',
' last_used: null',
' bare: true',
'profiles: {}',
'cliproxy:',
' oauth_accounts: {}',
' providers: {}',
' variants: {}',
].join('\n'),
'utf8'
);
const registry = new ProfileRegistry();
const accounts = registry.getAllAccountsUnified();
expect(accounts.work.bare).toBe(true);
const merged = registry.getAllProfilesMerged();
expect(merged.work.bare).toBe(true);
});
}); });
+84
View File
@@ -0,0 +1,84 @@
import { afterEach, beforeEach, describe, expect, it, mock, spyOn } from 'bun:test';
import { handleSyncCommand } from '../../../src/commands/sync-command';
import { ClaudeDirInstaller } from '../../../src/utils/claude-dir-installer';
import { ClaudeSymlinkManager } from '../../../src/utils/claude-symlink-manager';
import SharedManager from '../../../src/management/shared-manager';
import { InstanceManager } from '../../../src/management/instance-manager';
import ProfileRegistry from '../../../src/auth/profile-registry';
import type { ProfileMetadata } from '../../../src/types';
function profile(metadata: Partial<ProfileMetadata> = {}): ProfileMetadata {
return {
type: 'account',
created: '2026-03-05T00:00:00.000Z',
last_used: null,
...metadata,
};
}
describe('sync command MCP sync behavior', () => {
let originalProcessExit: typeof process.exit;
beforeEach(() => {
originalProcessExit = process.exit;
process.exit = ((code?: number) => {
throw new Error(`process.exit(${code ?? 0})`);
}) as typeof process.exit;
});
afterEach(() => {
process.exit = originalProcessExit;
mock.restore();
});
it('syncs MCP servers only to non-bare profiles', async () => {
spyOn(ClaudeDirInstaller.prototype, 'install').mockReturnValue(true);
spyOn(ClaudeDirInstaller.prototype, 'cleanupDeprecated').mockReturnValue({
success: true,
cleanedFiles: [],
});
spyOn(ClaudeSymlinkManager.prototype, 'install').mockImplementation(() => {});
spyOn(SharedManager.prototype, 'ensureSharedDirectories').mockImplementation(() => {});
spyOn(ProfileRegistry.prototype, 'getAllProfilesMerged').mockReturnValue({
work: profile(),
sandbox: profile({ bare: true }),
personal: profile(),
});
spyOn(InstanceManager.prototype, 'hasInstance').mockReturnValue(true);
const getInstancePathSpy = spyOn(InstanceManager.prototype, 'getInstancePath').mockImplementation(
(name: string) => `/tmp/${name}`
);
const syncMcpSpy = spyOn(InstanceManager.prototype, 'syncMcpServers').mockImplementation(
() => true
);
await expect(handleSyncCommand()).rejects.toThrow('process.exit(0)');
expect(getInstancePathSpy.mock.calls.map((call) => call[0])).toEqual(['work', 'personal']);
expect(syncMcpSpy.mock.calls.map((call) => call[0])).toEqual(['/tmp/work', '/tmp/personal']);
});
it('skips MCP sync when all profiles are bare', async () => {
spyOn(ClaudeDirInstaller.prototype, 'install').mockReturnValue(true);
spyOn(ClaudeDirInstaller.prototype, 'cleanupDeprecated').mockReturnValue({
success: true,
cleanedFiles: [],
});
spyOn(ClaudeSymlinkManager.prototype, 'install').mockImplementation(() => {});
spyOn(SharedManager.prototype, 'ensureSharedDirectories').mockImplementation(() => {});
spyOn(ProfileRegistry.prototype, 'getAllProfilesMerged').mockReturnValue({
sandbox: profile({ bare: true }),
experiment: profile({ bare: true }),
});
spyOn(InstanceManager.prototype, 'hasInstance').mockReturnValue(true);
const syncMcpSpy = spyOn(InstanceManager.prototype, 'syncMcpServers').mockImplementation(
() => true
);
await expect(handleSyncCommand()).rejects.toThrow('process.exit(0)');
expect(syncMcpSpy).not.toHaveBeenCalled();
});
});
@@ -0,0 +1,114 @@
import { afterEach, beforeEach, describe, expect, it, mock, spyOn } from 'bun:test';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { InstanceManager } from '../../src/management/instance-manager';
import SharedManager from '../../src/management/shared-manager';
describe('InstanceManager MCP sync', () => {
let tempRoot = '';
let originalCcsHome: string | undefined;
let originalCcsDir: string | undefined;
beforeEach(() => {
tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-instance-mcp-test-'));
originalCcsHome = process.env.CCS_HOME;
originalCcsDir = process.env.CCS_DIR;
process.env.CCS_HOME = tempRoot;
delete process.env.CCS_DIR;
});
afterEach(() => {
mock.restore();
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 });
}
});
it('merges global MCP servers and preserves instance-specific overrides', () => {
fs.writeFileSync(
path.join(tempRoot, '.claude.json'),
JSON.stringify(
{
mcpServers: {
globalOnly: { command: 'global-cmd' },
shared: { command: 'global-shared' },
},
},
null,
2
),
'utf8'
);
const manager = new InstanceManager();
const instancePath = manager.getInstancePath('work');
fs.mkdirSync(instancePath, { recursive: true });
fs.writeFileSync(
path.join(instancePath, '.claude.json'),
JSON.stringify(
{
mcpServers: {
shared: { command: 'instance-shared' },
instanceOnly: { command: 'instance-only' },
},
otherKey: 'keep-me',
},
null,
2
),
'utf8'
);
const synced = manager.syncMcpServers(instancePath);
expect(synced).toBe(true);
const instanceContent = JSON.parse(fs.readFileSync(path.join(instancePath, '.claude.json'), 'utf8'));
expect(instanceContent.otherKey).toBe('keep-me');
expect(instanceContent.mcpServers).toEqual({
globalOnly: { command: 'global-cmd' },
shared: { command: 'instance-shared' },
instanceOnly: { command: 'instance-only' },
});
});
it('logs warning when global MCP sync fails', () => {
fs.writeFileSync(path.join(tempRoot, '.claude.json'), '{invalid-json', 'utf8');
const warnSpy = spyOn(console, 'warn').mockImplementation(() => {});
const manager = new InstanceManager();
const instancePath = manager.getInstancePath('work');
fs.mkdirSync(instancePath, { recursive: true });
const synced = manager.syncMcpServers(instancePath);
expect(synced).toBe(false);
expect(warnSpy).toHaveBeenCalledTimes(1);
expect(String(warnSpy.mock.calls[0]?.[0] || '')).toContain('MCP sync skipped');
});
it('skips shared symlinks and MCP sync for bare instance creation', async () => {
const linkSharedSpy = spyOn(SharedManager.prototype, 'linkSharedDirectories').mockImplementation(
() => {}
);
spyOn(SharedManager.prototype, 'syncProjectContext').mockResolvedValue(undefined);
spyOn(SharedManager.prototype, 'syncAdvancedContinuityArtifacts').mockResolvedValue(undefined);
const syncMcpSpy = spyOn(InstanceManager.prototype, 'syncMcpServers').mockImplementation(
() => false
);
const manager = new InstanceManager();
await manager.ensureInstance('sandbox', { mode: 'isolated' }, { bare: true });
expect(linkSharedSpy).not.toHaveBeenCalled();
expect(syncMcpSpy).not.toHaveBeenCalled();
});
});