mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-15 18:15:40 +00:00
feat(continuity): support cross-profile continuity inheritance from account profiles
This commit is contained in:
1 parent
dbe8f6f68c
commit
0b9f9826e2
16 files changed
+646
-6
No files matched your search
@@ -0,0 +1,213 @@
|
||||
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 hasUnifiedSpy = spyOn(ProfileRegistry.prototype, 'hasAccountUnified').mockReturnValue(true);
|
||||
const touchUnifiedSpy = spyOn(ProfileRegistry.prototype, 'touchAccountUnified').mockImplementation(
|
||||
() => undefined
|
||||
);
|
||||
const touchLegacySpy = spyOn(ProfileRegistry.prototype, 'touchProfile').mockImplementation(
|
||||
() => undefined
|
||||
);
|
||||
|
||||
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(hasUnifiedSpy).toHaveBeenCalledWith('pro');
|
||||
expect(touchUnifiedSpy).toHaveBeenCalledWith('pro');
|
||||
expect(touchLegacySpy).not.toHaveBeenCalled();
|
||||
expect(ensureInstanceSpy).toHaveBeenCalledWith('pro', {
|
||||
mode: 'shared',
|
||||
group: 'team-alpha',
|
||||
continuityMode: 'deeper',
|
||||
});
|
||||
});
|
||||
|
||||
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(ProfileRegistry.prototype, 'hasAccountUnified').mockReturnValue(false);
|
||||
spyOn(ProfileRegistry.prototype, 'hasProfile').mockReturnValue(true);
|
||||
const touchLegacySpy = spyOn(ProfileRegistry.prototype, 'touchProfile').mockImplementation(
|
||||
() => undefined
|
||||
);
|
||||
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',
|
||||
});
|
||||
expect(touchLegacySpy).toHaveBeenCalledWith('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(ProfileRegistry.prototype, 'hasAccountUnified').mockReturnValue(true);
|
||||
spyOn(ProfileRegistry.prototype, 'touchAccountUnified').mockImplementation(() => undefined);
|
||||
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',
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user