fix(cliproxy-sync): honor profile settings paths and targets

- resolve settings file from profile.settingsPath instead of reconstructing by name

- skip non-claude targets for local claude-api-key sync semantics

- add mapper regression tests for both behaviors
This commit is contained in:
Tam Nhu Tran
2026-02-25 17:01:22 +07:00
parent 2e8c7a3691
commit ac1c744239
2 changed files with 113 additions and 2 deletions
+17 -2
View File
@@ -7,6 +7,7 @@
import * as fs from 'fs';
import * as path from 'path';
import { getCcsDir } from '../../utils/config-manager';
import { expandPath } from '../../utils/helpers';
import { listApiProfiles, isApiProfileConfigured } from '../../api/services/profile-reader';
import type { ClaudeKey } from '../management-api-types';
@@ -31,6 +32,15 @@ interface SettingsJson {
env?: Record<string, string>;
}
function resolveProfileSettingsPath(settingsPath: string): string {
const normalized = settingsPath.replace(/\\/g, '/');
if (normalized.startsWith('~/.ccs/')) {
return path.join(getCcsDir(), normalized.slice('~/.ccs/'.length));
}
return expandPath(settingsPath);
}
/**
* Load syncable API profiles from CCS config.
* Filters to only configured profiles (with real API keys).
@@ -45,9 +55,14 @@ export function loadSyncableProfiles(): SyncableProfile[] {
continue;
}
// Local CLIProxy sync writes Claude-compatible entries only.
// Profiles pinned to non-claude targets are intentionally skipped.
if (profile.target !== 'claude') {
continue;
}
// Load settings.json for env vars
const ccsDir = getCcsDir();
const settingsPath = path.join(ccsDir, `${profile.name}.settings.json`);
const settingsPath = resolveProfileSettingsPath(profile.settingsPath);
let env: Record<string, string> | undefined;
try {
@@ -4,9 +4,11 @@
*/
import * as assert from 'assert';
const fs = require('fs');
describe('Profile Mapper', () => {
const profileMapper = require('../../../dist/cliproxy/sync/profile-mapper');
const profileReader = require('../../../dist/api/services/profile-reader');
describe('mapProfileToClaudeKey', () => {
it('returns null when env is missing', () => {
@@ -86,6 +88,100 @@ describe('Profile Mapper', () => {
const result = profileMapper.loadSyncableProfiles();
assert.ok(Array.isArray(result));
});
it('uses profile-provided settingsPath instead of reconstructing from profile name', () => {
const originalListApiProfiles = profileReader.listApiProfiles;
const originalExistsSync = fs.existsSync;
const originalReadFileSync = fs.readFileSync;
const customSettingsPath = '/tmp/custom-sync-path.settings.json';
const readPaths: string[] = [];
try {
profileReader.listApiProfiles = () => ({
profiles: [
{
name: 'glm',
settingsPath: customSettingsPath,
isConfigured: true,
configSource: 'legacy',
target: 'claude',
},
],
variants: [],
});
fs.existsSync = (filePath: string) => filePath === customSettingsPath;
fs.readFileSync = (filePath: string) => {
readPaths.push(filePath);
return JSON.stringify({
env: {
ANTHROPIC_AUTH_TOKEN: 'sk-test-key',
},
});
};
const result = profileMapper.loadSyncableProfiles();
assert.strictEqual(result.length, 1);
assert.strictEqual(result[0].settingsPath, customSettingsPath);
assert.deepStrictEqual(readPaths, [customSettingsPath]);
} finally {
profileReader.listApiProfiles = originalListApiProfiles;
fs.existsSync = originalExistsSync;
fs.readFileSync = originalReadFileSync;
}
});
it('skips profiles pinned to non-claude targets during local sync mapping', () => {
const originalListApiProfiles = profileReader.listApiProfiles;
const originalExistsSync = fs.existsSync;
const originalReadFileSync = fs.readFileSync;
const claudePath = '/tmp/claude-target.settings.json';
const droidPath = '/tmp/droid-target.settings.json';
const readPaths: string[] = [];
try {
profileReader.listApiProfiles = () => ({
profiles: [
{
name: 'claude-profile',
settingsPath: claudePath,
isConfigured: true,
configSource: 'legacy',
target: 'claude',
},
{
name: 'droid-profile',
settingsPath: droidPath,
isConfigured: true,
configSource: 'legacy',
target: 'droid',
},
],
variants: [],
});
fs.existsSync = (filePath: string) => filePath === claudePath || filePath === droidPath;
fs.readFileSync = (filePath: string) => {
readPaths.push(filePath);
return JSON.stringify({
env: {
ANTHROPIC_AUTH_TOKEN: 'sk-test-key',
},
});
};
const result = profileMapper.loadSyncableProfiles();
assert.strictEqual(result.length, 1);
assert.strictEqual(result[0].name, 'claude-profile');
assert.deepStrictEqual(readPaths, [claudePath]);
} finally {
profileReader.listApiProfiles = originalListApiProfiles;
fs.existsSync = originalExistsSync;
fs.readFileSync = originalReadFileSync;
}
});
});
describe('generateSyncPayload', () => {