mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-19 00:14:49 +00:00
feat(auth): add opt-in shared context groups across accounts
This commit is contained in:
1 parent
1dae52014b
commit
4fed74c64b
21 files changed
+787
-25
No files matched your search
@@ -0,0 +1,34 @@
|
||||
import { describe, expect, it } from 'bun:test';
|
||||
import { parseArgs } from '../../src/auth/commands/types';
|
||||
|
||||
describe('auth command args parsing', () => {
|
||||
it('parses create with explicit shared context', () => {
|
||||
const parsed = parseArgs(['work', '--share-context']);
|
||||
|
||||
expect(parsed.profileName).toBe('work');
|
||||
expect(parsed.shareContext).toBe(true);
|
||||
expect(parsed.contextGroup).toBeUndefined();
|
||||
});
|
||||
|
||||
it('parses context group with separate value', () => {
|
||||
const parsed = parseArgs(['work', '--context-group', 'sprint-a']);
|
||||
|
||||
expect(parsed.profileName).toBe('work');
|
||||
expect(parsed.shareContext).toBe(false);
|
||||
expect(parsed.contextGroup).toBe('sprint-a');
|
||||
});
|
||||
|
||||
it('parses context group with equals form', () => {
|
||||
const parsed = parseArgs(['--context-group=sprint-a', 'work']);
|
||||
|
||||
expect(parsed.profileName).toBe('work');
|
||||
expect(parsed.contextGroup).toBe('sprint-a');
|
||||
});
|
||||
|
||||
it('flags missing context group value as empty string', () => {
|
||||
const parsed = parseArgs(['work', '--context-group']);
|
||||
|
||||
expect(parsed.profileName).toBe('work');
|
||||
expect(parsed.contextGroup).toBe('');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,121 @@
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import SharedManager from '../../src/management/shared-manager';
|
||||
import type { AccountContextPolicy } from '../../src/auth/account-context';
|
||||
|
||||
function getTestCcsDir(): string {
|
||||
if (!process.env.CCS_HOME) {
|
||||
throw new Error('CCS_HOME must be set in tests');
|
||||
}
|
||||
return path.join(path.resolve(process.env.CCS_HOME), '.ccs');
|
||||
}
|
||||
|
||||
describe('SharedManager context policy', () => {
|
||||
let tempRoot = '';
|
||||
let originalHome: string | undefined;
|
||||
let originalCcsHome: string | undefined;
|
||||
let originalCcsDir: string | undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-context-policy-test-'));
|
||||
originalHome = process.env.HOME;
|
||||
originalCcsHome = process.env.CCS_HOME;
|
||||
originalCcsDir = process.env.CCS_DIR;
|
||||
|
||||
const isolatedHome = path.join(tempRoot, 'home');
|
||||
fs.mkdirSync(isolatedHome, { recursive: true });
|
||||
process.env.HOME = isolatedHome;
|
||||
process.env.CCS_HOME = tempRoot;
|
||||
delete process.env.CCS_DIR;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (originalHome !== undefined) process.env.HOME = originalHome;
|
||||
else delete process.env.HOME;
|
||||
|
||||
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 });
|
||||
}
|
||||
});
|
||||
|
||||
async function applyPolicy(policy: AccountContextPolicy): Promise<{ instancePath: string; ccsDir: string }> {
|
||||
const ccsDir = getTestCcsDir();
|
||||
const instancePath = path.join(ccsDir, 'instances', 'work');
|
||||
fs.mkdirSync(instancePath, { recursive: true });
|
||||
|
||||
const manager = new SharedManager();
|
||||
await manager.syncProjectContext(instancePath, policy);
|
||||
|
||||
return { instancePath, ccsDir };
|
||||
}
|
||||
|
||||
it('keeps projects isolated by default', async () => {
|
||||
const { instancePath } = await applyPolicy({ mode: 'isolated' });
|
||||
const projectsPath = path.join(instancePath, 'projects');
|
||||
|
||||
expect(fs.existsSync(projectsPath)).toBe(true);
|
||||
expect(fs.lstatSync(projectsPath).isDirectory()).toBe(true);
|
||||
});
|
||||
|
||||
it('migrates local projects into shared context group', async () => {
|
||||
const ccsDir = getTestCcsDir();
|
||||
const instancePath = path.join(ccsDir, 'instances', 'work');
|
||||
const localProjectsPath = path.join(instancePath, 'projects');
|
||||
const localFile = path.join(localProjectsPath, '-tmp-project', 'notes.md');
|
||||
|
||||
fs.mkdirSync(path.dirname(localFile), { recursive: true });
|
||||
fs.writeFileSync(localFile, 'local context', 'utf8');
|
||||
|
||||
const manager = new SharedManager();
|
||||
await manager.syncProjectContext(instancePath, { mode: 'shared', group: 'sprint-a' });
|
||||
|
||||
const linkStats = fs.lstatSync(localProjectsPath);
|
||||
expect(linkStats.isSymbolicLink()).toBe(true);
|
||||
|
||||
const sharedFile = path.join(
|
||||
ccsDir,
|
||||
'shared',
|
||||
'context-groups',
|
||||
'sprint-a',
|
||||
'projects',
|
||||
'-tmp-project',
|
||||
'notes.md'
|
||||
);
|
||||
expect(fs.existsSync(sharedFile)).toBe(true);
|
||||
expect(fs.readFileSync(sharedFile, 'utf8')).toBe('local context');
|
||||
});
|
||||
|
||||
it('switches from shared mode back to isolated without data loss', async () => {
|
||||
const { instancePath, ccsDir } = await applyPolicy({ mode: 'shared', group: 'sprint-a' });
|
||||
const sharedFile = path.join(
|
||||
ccsDir,
|
||||
'shared',
|
||||
'context-groups',
|
||||
'sprint-a',
|
||||
'projects',
|
||||
'-tmp-project',
|
||||
'history.md'
|
||||
);
|
||||
|
||||
fs.mkdirSync(path.dirname(sharedFile), { recursive: true });
|
||||
fs.writeFileSync(sharedFile, 'shared history', 'utf8');
|
||||
|
||||
const manager = new SharedManager();
|
||||
await manager.syncProjectContext(instancePath, { mode: 'isolated' });
|
||||
|
||||
const projectsPath = path.join(instancePath, 'projects');
|
||||
const projectFile = path.join(projectsPath, '-tmp-project', 'history.md');
|
||||
|
||||
expect(fs.lstatSync(projectsPath).isDirectory()).toBe(true);
|
||||
expect(fs.existsSync(projectFile)).toBe(true);
|
||||
expect(fs.readFileSync(projectFile, 'utf8')).toBe('shared history');
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user