mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-19 20:14:43 +00:00
fix(persist): block codex claude settings bridge
This commit is contained in:
1 parent
a6ee3536c4
commit
3b2016462a
8 files changed
+219
-27
No files matched your search
@@ -58,7 +58,11 @@ async function createRestoreFixture(
|
||||
};
|
||||
|
||||
await fs.promises.mkdir(claudeDir, { recursive: true });
|
||||
await fs.promises.writeFile(settingsPath, JSON.stringify(originalSettings, null, 2) + '\n', 'utf8');
|
||||
await fs.promises.writeFile(
|
||||
settingsPath,
|
||||
JSON.stringify(originalSettings, null, 2) + '\n',
|
||||
'utf8'
|
||||
);
|
||||
await fs.promises.writeFile(backupPath, JSON.stringify(backupSettings, null, 2) + '\n', 'utf8');
|
||||
|
||||
return { claudeDir, settingsPath, backupPath, timestamp, originalSettings, backupSettings };
|
||||
@@ -98,15 +102,15 @@ afterEach(async () => {
|
||||
|
||||
describe('persist command real handler paths', () => {
|
||||
it('throws parseError for missing --permission-mode before profile detection', async () => {
|
||||
await expect(withScopedHome(() => handlePersistCommand(['glm', '--permission-mode']))).rejects.toThrow(
|
||||
'Missing value for --permission-mode'
|
||||
);
|
||||
await expect(
|
||||
withScopedHome(() => handlePersistCommand(['glm', '--permission-mode']))
|
||||
).rejects.toThrow('Missing value for --permission-mode');
|
||||
});
|
||||
|
||||
it('throws parseError for empty inline --permission-mode before profile detection', async () => {
|
||||
await expect(withScopedHome(() => handlePersistCommand(['glm', '--permission-mode=']))).rejects.toThrow(
|
||||
'Missing value for --permission-mode'
|
||||
);
|
||||
await expect(
|
||||
withScopedHome(() => handlePersistCommand(['glm', '--permission-mode=']))
|
||||
).rejects.toThrow('Missing value for --permission-mode');
|
||||
});
|
||||
|
||||
it('throws parseError for invalid --permission-mode before profile detection', async () => {
|
||||
@@ -116,34 +120,36 @@ describe('persist command real handler paths', () => {
|
||||
});
|
||||
|
||||
it('throws parseError for unknown flags on real handler path', async () => {
|
||||
await expect(withScopedHome(() => handlePersistCommand(['glm', '--unknown-flag']))).rejects.toThrow(
|
||||
/Unknown option\(s\)/
|
||||
);
|
||||
await expect(
|
||||
withScopedHome(() => handlePersistCommand(['glm', '--unknown-flag']))
|
||||
).rejects.toThrow(/Unknown option\(s\)/);
|
||||
});
|
||||
|
||||
it('throws parseError for list/restore conflict on real handler path', async () => {
|
||||
await expect(withScopedHome(() => handlePersistCommand(['--list-backups', '--restore']))).rejects.toThrow(
|
||||
'--list-backups cannot be used with --restore'
|
||||
);
|
||||
await expect(
|
||||
withScopedHome(() => handlePersistCommand(['--list-backups', '--restore']))
|
||||
).rejects.toThrow('--list-backups cannot be used with --restore');
|
||||
});
|
||||
|
||||
it('throws parseError for permission flags with --restore on real handler path', async () => {
|
||||
await expect(withScopedHome(() => handlePersistCommand(['--restore', '--auto-approve']))).rejects.toThrow(
|
||||
/Permission flags are not valid with backup operations/
|
||||
);
|
||||
await expect(
|
||||
withScopedHome(() => handlePersistCommand(['--restore', '--auto-approve']))
|
||||
).rejects.toThrow(/Permission flags are not valid with backup operations/);
|
||||
});
|
||||
|
||||
it('shows help when --help is present even with other invalid args', async () => {
|
||||
await expect(withScopedHome(() => handlePersistCommand(['--help', '--permission-mode']))).resolves.toBeUndefined();
|
||||
await expect(
|
||||
withScopedHome(() => handlePersistCommand(['--help', '--permission-mode']))
|
||||
).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('does not create CLAUDE_CONFIG_DIR on parseError path', async () => {
|
||||
const isolatedClaudeDir = path.join(tempRoot, '.claude-parse-early');
|
||||
process.env.CLAUDE_CONFIG_DIR = isolatedClaudeDir;
|
||||
|
||||
await expect(withScopedHome(() => handlePersistCommand(['glm', '--permission-mode=']))).rejects.toThrow(
|
||||
'Missing value for --permission-mode'
|
||||
);
|
||||
await expect(
|
||||
withScopedHome(() => handlePersistCommand(['glm', '--permission-mode=']))
|
||||
).rejects.toThrow('Missing value for --permission-mode');
|
||||
expect(await pathExists(isolatedClaudeDir)).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -294,6 +300,10 @@ describe('persist command Claude extension parity', () => {
|
||||
type: 'api',
|
||||
settings: path.join(tempRoot, '.ccs', 'glm.settings.json'),
|
||||
};
|
||||
config.cliproxy.variants.codex = {
|
||||
provider: 'codex',
|
||||
settings: path.join(tempRoot, '.ccs', 'codex.settings.json'),
|
||||
};
|
||||
|
||||
await fs.promises.mkdir(path.join(tempRoot, '.ccs'), { recursive: true });
|
||||
await fs.promises.writeFile(
|
||||
@@ -383,4 +393,50 @@ describe('persist command Claude extension parity', () => {
|
||||
expect(persisted.env.CLAUDE_CONFIG_DIR).toBe(path.join(tempRoot, '.ccs', 'instances', 'work'));
|
||||
expect(fs.existsSync(persisted.env.CLAUDE_CONFIG_DIR)).toBe(true);
|
||||
});
|
||||
|
||||
it('blocks Codex CLIProxy profiles from Claude settings persistence', async () => {
|
||||
await writeUnifiedConfig();
|
||||
|
||||
const settingsPath = path.join(tempRoot, '.claude', 'settings.json');
|
||||
await fs.promises.mkdir(path.dirname(settingsPath), { recursive: true });
|
||||
await fs.promises.writeFile(
|
||||
settingsPath,
|
||||
JSON.stringify(
|
||||
{
|
||||
env: {
|
||||
KEEP_ME: 'still-here',
|
||||
},
|
||||
},
|
||||
null,
|
||||
2
|
||||
) + '\n',
|
||||
'utf8'
|
||||
);
|
||||
|
||||
const originalConsoleLog = console.log;
|
||||
const capturedLogs: string[] = [];
|
||||
console.log = (...args: unknown[]) => {
|
||||
capturedLogs.push(args.map((arg) => String(arg)).join(' '));
|
||||
};
|
||||
|
||||
stubProcessExit();
|
||||
try {
|
||||
await expect(withScopedHome(() => handlePersistCommand(['codex', '--yes']))).rejects.toThrow(
|
||||
'process.exit(1)'
|
||||
);
|
||||
} finally {
|
||||
console.log = originalConsoleLog;
|
||||
}
|
||||
|
||||
const renderedLogs = capturedLogs.join('\n');
|
||||
expect(renderedLogs).toContain('Codex CLIProxy profile');
|
||||
expect(renderedLogs).toContain('ccsxp');
|
||||
expect(renderedLogs).toContain('ccs persist default --yes');
|
||||
|
||||
const persisted = JSON.parse(await fs.promises.readFile(settingsPath, 'utf8')) as {
|
||||
env: Record<string, string>;
|
||||
};
|
||||
expect(persisted.env.KEEP_ME).toBe('still-here');
|
||||
expect(persisted.env.ANTHROPIC_BASE_URL).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,60 @@
|
||||
import { afterEach, describe, expect, it } from 'bun:test';
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
|
||||
import { ClaudeSettingsChecker } from '../../../../src/management/checks/config-check';
|
||||
import { HealthCheck } from '../../../../src/management/checks/types';
|
||||
import { runWithScopedCcsHome } from '../../../../src/utils/config-manager';
|
||||
|
||||
let originalClaudeConfigDir: string | undefined;
|
||||
|
||||
afterEach(() => {
|
||||
if (originalClaudeConfigDir === undefined) {
|
||||
delete process.env.CLAUDE_CONFIG_DIR;
|
||||
} else {
|
||||
process.env.CLAUDE_CONFIG_DIR = originalClaudeConfigDir;
|
||||
}
|
||||
});
|
||||
|
||||
describe('ClaudeSettingsChecker', () => {
|
||||
it('warns when Claude settings point at the Codex CLIProxy translator', async () => {
|
||||
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-config-check-'));
|
||||
originalClaudeConfigDir = process.env.CLAUDE_CONFIG_DIR;
|
||||
delete process.env.CLAUDE_CONFIG_DIR;
|
||||
|
||||
try {
|
||||
await runWithScopedCcsHome(tempRoot, async () => {
|
||||
const settingsPath = path.join(tempRoot, '.claude', 'settings.json');
|
||||
fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
settingsPath,
|
||||
JSON.stringify(
|
||||
{
|
||||
env: {
|
||||
ANTHROPIC_BASE_URL: 'http://127.0.0.1:8317/api/provider/codex',
|
||||
},
|
||||
},
|
||||
null,
|
||||
2
|
||||
) + '\n'
|
||||
);
|
||||
|
||||
const originalConsoleLog = console.log;
|
||||
console.log = () => {};
|
||||
try {
|
||||
const results = new HealthCheck();
|
||||
new ClaudeSettingsChecker().run(results);
|
||||
|
||||
expect(results.warnings).toHaveLength(1);
|
||||
expect(results.warnings[0].message).toContain('Codex CLIProxy translator');
|
||||
expect(results.warnings[0].fix).toContain('ccs persist default --yes');
|
||||
} finally {
|
||||
console.log = originalConsoleLog;
|
||||
}
|
||||
});
|
||||
} finally {
|
||||
fs.rmSync(tempRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -34,8 +34,9 @@ describe('web-server claude-extension-routes', () => {
|
||||
({ default: SharedManager } = await import('../../../src/management/shared-manager'));
|
||||
({ createEmptyUnifiedConfig } = await import('../../../src/config/unified-config-types'));
|
||||
({ saveUnifiedConfig } = await import('../../../src/config/unified-config-loader'));
|
||||
({ default: claudeExtensionRoutes } =
|
||||
await import('../../../src/web-server/routes/claude-extension-routes'));
|
||||
({ default: claudeExtensionRoutes } = await import(
|
||||
'../../../src/web-server/routes/claude-extension-routes'
|
||||
));
|
||||
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
@@ -127,6 +128,10 @@ describe('web-server claude-extension-routes', () => {
|
||||
context_mode: 'isolated',
|
||||
};
|
||||
config.default = 'work';
|
||||
config.cliproxy.variants.codex = {
|
||||
provider: 'codex',
|
||||
settings: path.join(ccsDir, 'codex.settings.json'),
|
||||
};
|
||||
saveUnifiedConfig(config);
|
||||
});
|
||||
|
||||
@@ -154,12 +159,23 @@ describe('web-server claude-extension-routes', () => {
|
||||
expect(payload.profiles.some((profile) => profile.name === 'glm')).toBe(true);
|
||||
expect(payload.profiles.some((profile) => profile.name === 'work')).toBe(true);
|
||||
expect(payload.profiles.some((profile) => profile.name === 'gemini')).toBe(true);
|
||||
expect(payload.profiles.some((profile) => profile.name === 'codex')).toBe(false);
|
||||
expect(payload.hosts.map((host) => host.id)).toEqual(['vscode', 'cursor', 'windsurf']);
|
||||
expect(payload.hosts.every((host) => host.defaultSettingsPath.endsWith('settings.json'))).toBe(
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects Codex CLIProxy setup with native Codex guidance', async () => {
|
||||
const response = await fetch(`${baseUrl}/api/claude-extension/setup?profile=codex&host=vscode`);
|
||||
expect(response.status).toBe(400);
|
||||
|
||||
const payload = (await response.json()) as { error: string };
|
||||
expect(payload.error).toContain('Codex CLIProxy profile');
|
||||
expect(payload.error).toContain('ccsxp');
|
||||
expect(payload.error).toContain('ccs codex --target codex');
|
||||
});
|
||||
|
||||
it('renders VS Code setup for API profiles with disableLoginPrompt', async () => {
|
||||
const response = await fetch(`${baseUrl}/api/claude-extension/setup?profile=glm&host=vscode`);
|
||||
expect(response.status).toBe(200);
|
||||
|
||||
Reference in new issue
Block a user