Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 2 additions & 6 deletions src/agent/file-watcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,9 @@ import { watch, type FSWatcher } from 'fs';
import { access } from 'fs/promises';
import type { AgentConfig } from '../shared/types';
import { expandPath } from '../config/loader';
import { getCredentialFilePaths } from '../agents';

const STANDARD_CREDENTIAL_FILES = [
'~/.gitconfig',
'~/.claude/.credentials.json',
'~/.codex/auth.json',
'~/.codex/config.toml',
];
const STANDARD_CREDENTIAL_FILES = ['~/.gitconfig', ...getCredentialFilePaths()];

interface FileWatcherOptions {
config: AgentConfig;
Expand Down
159 changes: 159 additions & 0 deletions src/agents/__tests__/claude-code.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import { describe, it, expect } from 'vitest';
import { claudeCodeSync } from '../sync/claude-code';
import type { SyncContext } from '../types';

function createMockContext(overrides: Partial<SyncContext> = {}): SyncContext {
return {
containerName: 'test-container',
agentConfig: {
port: 7777,
credentials: { env: {}, files: {} },
scripts: {},
},
hostFileExists: async () => false,
hostDirExists: async () => false,
readHostFile: async () => null,
...overrides,
};
}

describe('claudeCodeSync', () => {
describe('getRequiredDirs', () => {
it('returns .claude directory', () => {
const dirs = claudeCodeSync.getRequiredDirs();
expect(dirs).toContain('/home/workspace/.claude');
});
});

describe('getFilesToSync', () => {
it('returns credentials file', async () => {
const context = createMockContext();
const files = await claudeCodeSync.getFilesToSync(context);

const credFile = files.find((f) => f.source === '~/.claude/.credentials.json');
expect(credFile).toBeDefined();
expect(credFile?.category).toBe('credential');
expect(credFile?.permissions).toBe('600');
expect(credFile?.optional).toBe(true);
});

it('returns settings file', async () => {
const context = createMockContext();
const files = await claudeCodeSync.getFilesToSync(context);

const settingsFile = files.find((f) => f.source === '~/.claude/settings.json');
expect(settingsFile).toBeDefined();
expect(settingsFile?.category).toBe('preference');
expect(settingsFile?.optional).toBe(true);
});

it('returns CLAUDE.md file', async () => {
const context = createMockContext();
const files = await claudeCodeSync.getFilesToSync(context);

const claudeMdFile = files.find((f) => f.source === '~/.claude/CLAUDE.md');
expect(claudeMdFile).toBeDefined();
expect(claudeMdFile?.category).toBe('preference');
expect(claudeMdFile?.optional).toBe(true);
});

it('marks all files as optional', async () => {
const context = createMockContext();
const files = await claudeCodeSync.getFilesToSync(context);

expect(files.every((f) => f.optional === true)).toBe(true);
});
});

describe('getDirectoriesToSync', () => {
it('returns empty when agents dir does not exist', async () => {
const context = createMockContext({
hostDirExists: async () => false,
});

const dirs = await claudeCodeSync.getDirectoriesToSync(context);
expect(dirs).toHaveLength(0);
});

it('returns agents directory when it exists', async () => {
const context = createMockContext({
hostDirExists: async (path) => path === '~/.claude/agents',
});

const dirs = await claudeCodeSync.getDirectoriesToSync(context);
expect(dirs).toHaveLength(1);
expect(dirs[0].source).toBe('~/.claude/agents');
expect(dirs[0].dest).toBe('/home/workspace/.claude/agents');
expect(dirs[0].category).toBe('preference');
});
});

describe('getGeneratedConfigs', () => {
it('generates .claude.json with onboarding flag', async () => {
const context = createMockContext();

const configs = await claudeCodeSync.getGeneratedConfigs(context);

expect(configs).toHaveLength(1);
expect(configs[0].dest).toBe('/home/workspace/.claude.json');
expect(configs[0].category).toBe('preference');

const parsed = JSON.parse(configs[0].content);
expect(parsed.hasCompletedOnboarding).toBe(true);
});

it('does not include mcpServers when host has none', async () => {
const context = createMockContext();

const configs = await claudeCodeSync.getGeneratedConfigs(context);
const parsed = JSON.parse(configs[0].content);

expect(parsed.mcpServers).toBeUndefined();
});

it('merges mcpServers from host config', async () => {
const hostConfig = {
mcpServers: {
'my-server': { command: 'node', args: ['server.js'] },
},
};

const context = createMockContext({
readHostFile: async (path) =>
path === '~/.claude.json' ? JSON.stringify(hostConfig) : null,
});

const configs = await claudeCodeSync.getGeneratedConfigs(context);
const parsed = JSON.parse(configs[0].content);

expect(parsed.mcpServers).toEqual(hostConfig.mcpServers);
expect(parsed.hasCompletedOnboarding).toBe(true);
});

it('handles invalid JSON in host config gracefully', async () => {
const context = createMockContext({
readHostFile: async (path) => (path === '~/.claude.json' ? 'invalid json{' : null),
});

const configs = await claudeCodeSync.getGeneratedConfigs(context);
const parsed = JSON.parse(configs[0].content);

expect(parsed.hasCompletedOnboarding).toBe(true);
expect(parsed.mcpServers).toBeUndefined();
});

it('handles empty mcpServers object', async () => {
const hostConfig = { mcpServers: {} };

const context = createMockContext({
readHostFile: async (path) =>
path === '~/.claude.json' ? JSON.stringify(hostConfig) : null,
});

const configs = await claudeCodeSync.getGeneratedConfigs(context);
const parsed = JSON.parse(configs[0].content);

expect(parsed.mcpServers).toBeUndefined();
});
});
});
76 changes: 76 additions & 0 deletions src/agents/__tests__/codex.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { describe, it, expect } from 'vitest';
import { codexSync } from '../sync/codex';
import type { SyncContext } from '../types';

function createMockContext(overrides: Partial<SyncContext> = {}): SyncContext {
return {
containerName: 'test-container',
agentConfig: {
port: 7777,
credentials: { env: {}, files: {} },
scripts: {},
},
hostFileExists: async () => false,
hostDirExists: async () => false,
readHostFile: async () => null,
...overrides,
};
}

describe('codexSync', () => {
describe('getRequiredDirs', () => {
it('returns .codex directory', () => {
const dirs = codexSync.getRequiredDirs();
expect(dirs).toContain('/home/workspace/.codex');
});
});

describe('getFilesToSync', () => {
it('returns auth.json as credential', async () => {
const context = createMockContext();
const files = await codexSync.getFilesToSync(context);

const authFile = files.find((f) => f.source === '~/.codex/auth.json');
expect(authFile).toBeDefined();
expect(authFile?.category).toBe('credential');
expect(authFile?.permissions).toBe('600');
expect(authFile?.optional).toBe(true);
});

it('returns config.toml as preference', async () => {
const context = createMockContext();
const files = await codexSync.getFilesToSync(context);

const configFile = files.find((f) => f.source === '~/.codex/config.toml');
expect(configFile).toBeDefined();
expect(configFile?.category).toBe('preference');
expect(configFile?.permissions).toBe('600');
expect(configFile?.optional).toBe(true);
});

it('marks all files as optional', async () => {
const context = createMockContext();
const files = await codexSync.getFilesToSync(context);

expect(files.every((f) => f.optional === true)).toBe(true);
});
});

describe('getDirectoriesToSync', () => {
it('returns empty array (no directories to sync)', async () => {
const context = createMockContext();
const dirs = await codexSync.getDirectoriesToSync(context);

expect(dirs).toHaveLength(0);
});
});

describe('getGeneratedConfigs', () => {
it('returns empty array (no generated configs)', async () => {
const context = createMockContext();
const configs = await codexSync.getGeneratedConfigs(context);

expect(configs).toHaveLength(0);
});
});
});
Loading