mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 02:11:28 +00:00
384 lines
12 KiB
TypeScript
384 lines
12 KiB
TypeScript
import { afterEach, describe, expect, it, mock, spyOn } from 'bun:test';
|
|
import * as fs from 'fs';
|
|
import * as configLoader from '../../../src/config/unified-config-loader';
|
|
import ProfileRegistry from '../../../src/auth/profile-registry';
|
|
import InstanceManager from '../../../src/management/instance-manager';
|
|
import { resolveProfileContinuityInheritance } from '../../../src/auth/profile-continuity-inheritance';
|
|
|
|
describe('resolveProfileContinuityInheritance', () => {
|
|
afterEach(() => {
|
|
mock.restore();
|
|
});
|
|
|
|
it('returns no inheritance for non-claude targets', async () => {
|
|
const result = await resolveProfileContinuityInheritance({
|
|
profileName: 'glm',
|
|
profileType: 'settings',
|
|
target: 'droid',
|
|
});
|
|
|
|
expect(result).toEqual({});
|
|
});
|
|
|
|
it('resolves mapped account and ensures inherited instance path', 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'
|
|
);
|
|
const getProfilesSpy = spyOn(ProfileRegistry.prototype, 'getAllProfilesMerged').mockReturnValue(
|
|
{
|
|
pro: {
|
|
type: 'account',
|
|
created: '2026-03-01T00:00:00.000Z',
|
|
last_used: null,
|
|
context_mode: 'shared',
|
|
context_group: 'Team Alpha',
|
|
continuity_mode: 'deeper',
|
|
},
|
|
}
|
|
);
|
|
|
|
const result = await resolveProfileContinuityInheritance({
|
|
profileName: 'glm',
|
|
profileType: 'settings',
|
|
target: 'claude',
|
|
});
|
|
|
|
expect(result).toEqual({
|
|
sourceAccount: 'pro',
|
|
claudeConfigDir: '/tmp/.ccs/instances/pro',
|
|
});
|
|
expect(getProfilesSpy).toHaveBeenCalledTimes(1);
|
|
expect(ensureInstanceSpy).toHaveBeenCalledWith('pro', {
|
|
mode: 'shared',
|
|
group: 'team-alpha',
|
|
continuityMode: 'deeper',
|
|
}, { bare: false });
|
|
});
|
|
|
|
it('supports legacy continuity_inherit_from_account fallback', async () => {
|
|
spyOn(configLoader, 'loadOrCreateUnifiedConfig').mockReturnValue({
|
|
version: 8,
|
|
} as ReturnType<typeof configLoader.loadOrCreateUnifiedConfig>);
|
|
spyOn(configLoader, 'isUnifiedMode').mockReturnValue(false);
|
|
spyOn(configLoader, 'getConfigJsonPath').mockReturnValue('/tmp/ccs-test-config.json');
|
|
spyOn(fs, 'existsSync').mockImplementation((filePath) => {
|
|
return filePath === '/tmp/ccs-test-config.json';
|
|
});
|
|
spyOn(fs, 'readFileSync').mockImplementation((filePath) => {
|
|
if (filePath === '/tmp/ccs-test-config.json') {
|
|
return JSON.stringify({
|
|
continuity_inherit_from_account: {
|
|
copilot: 'work',
|
|
},
|
|
});
|
|
}
|
|
return '';
|
|
});
|
|
|
|
spyOn(ProfileRegistry.prototype, 'getAllProfilesMerged').mockReturnValue({
|
|
work: {
|
|
type: 'account',
|
|
created: '2026-03-01T00:00:00.000Z',
|
|
last_used: null,
|
|
},
|
|
});
|
|
spyOn(InstanceManager.prototype, 'ensureInstance').mockResolvedValue('/tmp/.ccs/instances/work');
|
|
|
|
const result = await resolveProfileContinuityInheritance({
|
|
profileName: 'copilot',
|
|
profileType: 'copilot',
|
|
target: 'claude',
|
|
});
|
|
|
|
expect(result).toEqual({
|
|
sourceAccount: 'work',
|
|
claudeConfigDir: '/tmp/.ccs/instances/work',
|
|
});
|
|
});
|
|
|
|
it('returns empty result when mapped source account does not exist', async () => {
|
|
spyOn(configLoader, 'loadOrCreateUnifiedConfig').mockReturnValue({
|
|
version: 8,
|
|
continuity: {
|
|
inherit_from_account: {
|
|
glm: 'missing',
|
|
},
|
|
},
|
|
} as ReturnType<typeof configLoader.loadOrCreateUnifiedConfig>);
|
|
|
|
spyOn(ProfileRegistry.prototype, 'getAllProfilesMerged').mockReturnValue({});
|
|
const ensureInstanceSpy = spyOn(InstanceManager.prototype, 'ensureInstance');
|
|
|
|
const result = await resolveProfileContinuityInheritance({
|
|
profileName: 'glm',
|
|
profileType: 'settings',
|
|
target: 'claude',
|
|
});
|
|
|
|
expect(result).toEqual({});
|
|
expect(ensureInstanceSpy).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('does not read legacy config fallback when unified mode is active', async () => {
|
|
spyOn(configLoader, 'loadOrCreateUnifiedConfig').mockReturnValue({
|
|
version: 8,
|
|
} as ReturnType<typeof configLoader.loadOrCreateUnifiedConfig>);
|
|
spyOn(configLoader, 'isUnifiedMode').mockReturnValue(true);
|
|
spyOn(configLoader, 'getConfigJsonPath').mockReturnValue('/tmp/ccs-test-config.json');
|
|
const existsSpy = spyOn(fs, 'existsSync').mockImplementation((filePath) => {
|
|
return filePath === '/tmp/ccs-test-config.json';
|
|
});
|
|
const readSpy = spyOn(fs, 'readFileSync').mockImplementation((filePath) => {
|
|
if (filePath === '/tmp/ccs-test-config.json') {
|
|
return JSON.stringify({
|
|
continuity_inherit_from_account: {
|
|
glm: 'pro',
|
|
},
|
|
});
|
|
}
|
|
return '';
|
|
});
|
|
|
|
spyOn(ProfileRegistry.prototype, 'getAllProfilesMerged').mockReturnValue({});
|
|
const ensureInstanceSpy = spyOn(InstanceManager.prototype, 'ensureInstance');
|
|
|
|
const result = await resolveProfileContinuityInheritance({
|
|
profileName: 'glm',
|
|
profileType: 'settings',
|
|
target: 'claude',
|
|
});
|
|
|
|
expect(result).toEqual({});
|
|
expect(ensureInstanceSpy).not.toHaveBeenCalled();
|
|
expect(existsSpy).not.toHaveBeenCalled();
|
|
expect(readSpy).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('supports default profile inheritance when explicitly mapped', async () => {
|
|
spyOn(configLoader, 'loadOrCreateUnifiedConfig').mockReturnValue({
|
|
version: 8,
|
|
continuity: {
|
|
inherit_from_account: {
|
|
default: 'pro',
|
|
},
|
|
},
|
|
} as ReturnType<typeof configLoader.loadOrCreateUnifiedConfig>);
|
|
spyOn(ProfileRegistry.prototype, 'getAllProfilesMerged').mockReturnValue({
|
|
pro: {
|
|
type: 'account',
|
|
created: '2026-03-01T00:00:00.000Z',
|
|
last_used: null,
|
|
},
|
|
});
|
|
spyOn(InstanceManager.prototype, 'ensureInstance').mockResolvedValue('/tmp/.ccs/instances/pro');
|
|
|
|
const result = await resolveProfileContinuityInheritance({
|
|
profileName: 'default',
|
|
profileType: 'default',
|
|
target: 'claude',
|
|
});
|
|
|
|
expect(result).toEqual({
|
|
sourceAccount: 'pro',
|
|
claudeConfigDir: '/tmp/.ccs/instances/pro',
|
|
});
|
|
});
|
|
|
|
it('returns empty result when mapped source exists but is not an account profile', async () => {
|
|
spyOn(configLoader, 'loadOrCreateUnifiedConfig').mockReturnValue({
|
|
version: 8,
|
|
continuity: {
|
|
inherit_from_account: {
|
|
glm: 'settings-profile',
|
|
},
|
|
},
|
|
} as ReturnType<typeof configLoader.loadOrCreateUnifiedConfig>);
|
|
|
|
spyOn(ProfileRegistry.prototype, 'getAllProfilesMerged').mockReturnValue({
|
|
'settings-profile': {
|
|
type: 'settings',
|
|
created: '2026-03-01T00:00:00.000Z',
|
|
last_used: null,
|
|
},
|
|
});
|
|
const ensureInstanceSpy = spyOn(InstanceManager.prototype, 'ensureInstance');
|
|
|
|
const result = await resolveProfileContinuityInheritance({
|
|
profileName: 'glm',
|
|
profileType: 'settings',
|
|
target: 'claude',
|
|
});
|
|
|
|
expect(result).toEqual({});
|
|
expect(ensureInstanceSpy).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('supports profile alias lookup when continuity mapping uses legacy key', async () => {
|
|
spyOn(configLoader, 'loadOrCreateUnifiedConfig').mockReturnValue({
|
|
version: 8,
|
|
continuity: {
|
|
inherit_from_account: {
|
|
kimi: 'pro',
|
|
},
|
|
},
|
|
} as ReturnType<typeof configLoader.loadOrCreateUnifiedConfig>);
|
|
|
|
spyOn(ProfileRegistry.prototype, 'getAllProfilesMerged').mockReturnValue({
|
|
pro: {
|
|
type: 'account',
|
|
created: '2026-03-01T00:00:00.000Z',
|
|
last_used: null,
|
|
},
|
|
});
|
|
const ensureInstanceSpy = spyOn(InstanceManager.prototype, 'ensureInstance').mockResolvedValue(
|
|
'/tmp/.ccs/instances/pro'
|
|
);
|
|
|
|
const result = await resolveProfileContinuityInheritance({
|
|
profileName: 'km',
|
|
profileType: 'settings',
|
|
target: 'claude',
|
|
});
|
|
|
|
expect(result).toEqual({
|
|
sourceAccount: 'pro',
|
|
claudeConfigDir: '/tmp/.ccs/instances/pro',
|
|
});
|
|
expect(ensureInstanceSpy).toHaveBeenCalledWith('pro', {
|
|
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 () => {
|
|
spyOn(configLoader, 'loadOrCreateUnifiedConfig').mockReturnValue({
|
|
version: 8,
|
|
continuity: {
|
|
inherit_from_account: {
|
|
km: 'pro',
|
|
},
|
|
},
|
|
} as ReturnType<typeof configLoader.loadOrCreateUnifiedConfig>);
|
|
|
|
const ensureInstanceSpy = spyOn(InstanceManager.prototype, 'ensureInstance');
|
|
|
|
const result = await resolveProfileContinuityInheritance({
|
|
profileName: 'kimi',
|
|
profileType: 'cliproxy',
|
|
target: 'claude',
|
|
});
|
|
|
|
expect(result).toEqual({});
|
|
expect(ensureInstanceSpy).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('does not apply km settings alias mapping to kimi cliproxy profile in legacy mode', async () => {
|
|
spyOn(configLoader, 'loadOrCreateUnifiedConfig').mockReturnValue({
|
|
version: 8,
|
|
} as ReturnType<typeof configLoader.loadOrCreateUnifiedConfig>);
|
|
spyOn(configLoader, 'isUnifiedMode').mockReturnValue(false);
|
|
spyOn(configLoader, 'getConfigJsonPath').mockReturnValue('/tmp/ccs-test-config.json');
|
|
spyOn(fs, 'existsSync').mockImplementation((filePath) => {
|
|
return filePath === '/tmp/ccs-test-config.json';
|
|
});
|
|
spyOn(fs, 'readFileSync').mockImplementation((filePath) => {
|
|
if (filePath === '/tmp/ccs-test-config.json') {
|
|
return JSON.stringify({
|
|
continuity_inherit_from_account: {
|
|
km: 'pro',
|
|
},
|
|
});
|
|
}
|
|
return '';
|
|
});
|
|
|
|
const ensureInstanceSpy = spyOn(InstanceManager.prototype, 'ensureInstance');
|
|
|
|
const result = await resolveProfileContinuityInheritance({
|
|
profileName: 'kimi',
|
|
profileType: 'cliproxy',
|
|
target: 'claude',
|
|
});
|
|
|
|
expect(result).toEqual({});
|
|
expect(ensureInstanceSpy).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('fails open when source account instance initialization throws', async () => {
|
|
spyOn(configLoader, 'loadOrCreateUnifiedConfig').mockReturnValue({
|
|
version: 8,
|
|
continuity: {
|
|
inherit_from_account: {
|
|
glm: 'pro',
|
|
},
|
|
},
|
|
} as ReturnType<typeof configLoader.loadOrCreateUnifiedConfig>);
|
|
|
|
spyOn(ProfileRegistry.prototype, 'getAllProfilesMerged').mockReturnValue({
|
|
pro: {
|
|
type: 'account',
|
|
created: '2026-03-01T00:00:00.000Z',
|
|
last_used: null,
|
|
},
|
|
});
|
|
spyOn(InstanceManager.prototype, 'ensureInstance').mockRejectedValue(
|
|
new Error('instance init failed')
|
|
);
|
|
|
|
const result = await resolveProfileContinuityInheritance({
|
|
profileName: 'glm',
|
|
profileType: 'settings',
|
|
target: 'claude',
|
|
});
|
|
|
|
expect(result).toEqual({});
|
|
});
|
|
});
|