mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-02 18:18:43 +00:00
fix(cliproxy): tighten session affinity review issues
This commit is contained in:
@@ -173,12 +173,18 @@ export async function handleRoutingAffinityStatus(): Promise<void> {
|
|||||||
export async function handleRoutingAffinitySet(args: string[]): Promise<void> {
|
export async function handleRoutingAffinitySet(args: string[]): Promise<void> {
|
||||||
const requested = normalizeCliproxySessionAffinityEnabled(args[0]);
|
const requested = normalizeCliproxySessionAffinityEnabled(args[0]);
|
||||||
const extractedTtl = extractOption(args.slice(1), ['--ttl']);
|
const extractedTtl = extractOption(args.slice(1), ['--ttl']);
|
||||||
|
const remainingArgs = extractedTtl.remainingArgs.filter((token) => token.trim().length > 0);
|
||||||
const ttl: string | undefined =
|
const ttl: string | undefined =
|
||||||
extractedTtl.found && !extractedTtl.missingValue
|
extractedTtl.found && !extractedTtl.missingValue
|
||||||
? (normalizeCliproxySessionAffinityTtl(extractedTtl.value) ?? undefined)
|
? (normalizeCliproxySessionAffinityTtl(extractedTtl.value) ?? undefined)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
if (requested === null || extractedTtl.missingValue || (extractedTtl.found && !ttl)) {
|
if (
|
||||||
|
requested === null ||
|
||||||
|
extractedTtl.missingValue ||
|
||||||
|
(extractedTtl.found && !ttl) ||
|
||||||
|
remainingArgs.length > 0
|
||||||
|
) {
|
||||||
await initUI();
|
await initUI();
|
||||||
console.log('');
|
console.log('');
|
||||||
console.log(
|
console.log(
|
||||||
|
|||||||
@@ -168,11 +168,25 @@ function getSuggestionsForCommand(tokensBeforeCurrent: string[]): CompletionSugg
|
|||||||
'-h',
|
'-h',
|
||||||
]);
|
]);
|
||||||
if (subcommand === 'routing') {
|
if (subcommand === 'routing') {
|
||||||
|
const routingSubcommand = tokensBeforeCurrent[2];
|
||||||
|
const routingAffinityMode = tokensBeforeCurrent[3];
|
||||||
if (lastToken === 'set') {
|
if (lastToken === 'set') {
|
||||||
return completeSubcommands(['round-robin', 'fill-first']);
|
return completeSubcommands(['round-robin', 'fill-first']);
|
||||||
}
|
}
|
||||||
if (lastToken === 'affinity') {
|
if (routingSubcommand === 'affinity') {
|
||||||
return completeSubcommands(['on', 'off', '--ttl']);
|
if (!routingAffinityMode || lastToken === 'affinity') {
|
||||||
|
return completeSubcommands(['on', 'off']);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
(routingAffinityMode === 'on' || routingAffinityMode === 'off') &&
|
||||||
|
!tokensBeforeCurrent.includes('--ttl')
|
||||||
|
) {
|
||||||
|
return completeSubcommands([], ['--ttl']);
|
||||||
|
}
|
||||||
|
if (lastToken === '--ttl') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return completeSubcommands([]);
|
||||||
}
|
}
|
||||||
return completeSubcommands(['set', 'explain', 'affinity']);
|
return completeSubcommands(['set', 'explain', 'affinity']);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,6 +59,8 @@ const CONFIG_YAML = 'config.yaml';
|
|||||||
const CONFIG_JSON = 'config.json';
|
const CONFIG_JSON = 'config.json';
|
||||||
const CONFIG_LOCK = 'config.yaml.lock';
|
const CONFIG_LOCK = 'config.yaml.lock';
|
||||||
const LOCK_STALE_MS = 5000; // Lock is stale after 5 seconds
|
const LOCK_STALE_MS = 5000; // Lock is stale after 5 seconds
|
||||||
|
const GO_DURATION_SEGMENT = String.raw`(?:\d+(?:\.\d+)?(?:ns|us|µs|μs|ms|s|m|h))`;
|
||||||
|
const GO_DURATION_PATTERN = new RegExp(`^${GO_DURATION_SEGMENT}+$`);
|
||||||
|
|
||||||
function normalizeBrowserDevtoolsPort(value: number | undefined): number {
|
function normalizeBrowserDevtoolsPort(value: number | undefined): number {
|
||||||
if (!Number.isFinite(value)) {
|
if (!Number.isFinite(value)) {
|
||||||
@@ -112,6 +114,19 @@ function canonicalizeBrowserConfig(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeSessionAffinityTtl(value: unknown, fallback: string): string {
|
||||||
|
if (typeof value !== 'string') {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
const trimmed = value.trim();
|
||||||
|
if (!trimmed || !GO_DURATION_PATTERN.test(trimmed)) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get path to unified config.yaml
|
* Get path to unified config.yaml
|
||||||
*/
|
*/
|
||||||
@@ -444,11 +459,10 @@ function mergeWithDefaults(partial: Partial<UnifiedConfig>): UnifiedConfig {
|
|||||||
typeof partial.cliproxy?.routing?.session_affinity === 'boolean'
|
typeof partial.cliproxy?.routing?.session_affinity === 'boolean'
|
||||||
? partial.cliproxy.routing.session_affinity
|
? partial.cliproxy.routing.session_affinity
|
||||||
: defaults.cliproxy.routing?.session_affinity,
|
: defaults.cliproxy.routing?.session_affinity,
|
||||||
session_affinity_ttl:
|
session_affinity_ttl: normalizeSessionAffinityTtl(
|
||||||
typeof partial.cliproxy?.routing?.session_affinity_ttl === 'string' &&
|
partial.cliproxy?.routing?.session_affinity_ttl,
|
||||||
partial.cliproxy.routing.session_affinity_ttl.trim()
|
defaults.cliproxy.routing?.session_affinity_ttl ?? '1h'
|
||||||
? partial.cliproxy.routing.session_affinity_ttl.trim()
|
),
|
||||||
: defaults.cliproxy.routing?.session_affinity_ttl,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
proxy: {
|
proxy: {
|
||||||
|
|||||||
@@ -128,6 +128,22 @@ describe('completion backend', () => {
|
|||||||
expect(values).toContain('my-codex');
|
expect(values).toContain('my-codex');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('suggests the correct routing affinity completion shape', () => {
|
||||||
|
expect(suggestionValues(['cliproxy', 'routing'])).toEqual(
|
||||||
|
expect.arrayContaining(['set', 'explain', 'affinity'])
|
||||||
|
);
|
||||||
|
expect(suggestionValues(['cliproxy', 'routing', 'affinity'])).toEqual(
|
||||||
|
expect.arrayContaining(['on', 'off'])
|
||||||
|
);
|
||||||
|
expect(suggestionValues(['cliproxy', 'routing', 'affinity', 'on'])).toEqual(
|
||||||
|
expect.arrayContaining(['--ttl'])
|
||||||
|
);
|
||||||
|
expect(suggestionValues(['cliproxy', 'routing', 'affinity', 'off'])).toEqual(
|
||||||
|
expect.arrayContaining(['--ttl'])
|
||||||
|
);
|
||||||
|
expect(suggestionValues(['cliproxy', 'routing', 'affinity', '--ttl'])).not.toContain('--ttl');
|
||||||
|
});
|
||||||
|
|
||||||
test('suggests env format values after the format flag', () => {
|
test('suggests env format values after the format flag', () => {
|
||||||
const values = suggestionValues(['env', '--format']);
|
const values = suggestionValues(['env', '--format']);
|
||||||
expect(values).toEqual(
|
expect(values).toEqual(
|
||||||
|
|||||||
@@ -285,6 +285,41 @@ describe('continuity-inheritance-config', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('cliproxy session-affinity ttl normalization', () => {
|
||||||
|
it('normalizes invalid stored ttl values back to the safe default', () => {
|
||||||
|
const originalCcsHome = process.env.CCS_HOME;
|
||||||
|
const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-affinity-ttl-home-'));
|
||||||
|
const ccsDir = path.join(tempHome, '.ccs');
|
||||||
|
fs.mkdirSync(ccsDir, { recursive: true });
|
||||||
|
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(ccsDir, 'config.yaml'),
|
||||||
|
[
|
||||||
|
'version: 8',
|
||||||
|
'cliproxy:',
|
||||||
|
' routing:',
|
||||||
|
' strategy: round-robin',
|
||||||
|
' session_affinity: true',
|
||||||
|
' session_affinity_ttl: forever',
|
||||||
|
'',
|
||||||
|
].join('\n')
|
||||||
|
);
|
||||||
|
|
||||||
|
process.env.CCS_HOME = tempHome;
|
||||||
|
try {
|
||||||
|
const config = loadOrCreateUnifiedConfig();
|
||||||
|
expect(config.cliproxy.routing.session_affinity_ttl).toBe('1h');
|
||||||
|
} finally {
|
||||||
|
if (originalCcsHome === undefined) {
|
||||||
|
delete process.env.CCS_HOME;
|
||||||
|
} else {
|
||||||
|
process.env.CCS_HOME = originalCcsHome;
|
||||||
|
}
|
||||||
|
fs.rmSync(tempHome, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('official-channels-config', () => {
|
describe('official-channels-config', () => {
|
||||||
it('keeps explicit channels.selected empty even when legacy discord_channels.enabled is true', () => {
|
it('keeps explicit channels.selected empty even when legacy discord_channels.enabled is true', () => {
|
||||||
const originalCcsHome = process.env.CCS_HOME;
|
const originalCcsHome = process.env.CCS_HOME;
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ export function useUpdateCliproxySessionAffinity() {
|
|||||||
mutationFn: (data: { enabled: boolean; ttl?: string }) =>
|
mutationFn: (data: { enabled: boolean; ttl?: string }) =>
|
||||||
api.cliproxy.updateSessionAffinity(data),
|
api.cliproxy.updateSessionAffinity(data),
|
||||||
onSuccess: (result: CliproxySessionAffinityApplyResult) => {
|
onSuccess: (result: CliproxySessionAffinityApplyResult) => {
|
||||||
|
queryClient.setQueryData(['cliproxy-session-affinity'], result);
|
||||||
queryClient.invalidateQueries({ queryKey: ['cliproxy-session-affinity'] });
|
queryClient.invalidateQueries({ queryKey: ['cliproxy-session-affinity'] });
|
||||||
const label = result.enabled ? 'enabled' : 'disabled';
|
const label = result.enabled ? 'enabled' : 'disabled';
|
||||||
toast.success(result.message || `Session affinity ${label}.`);
|
toast.success(result.message || `Session affinity ${label}.`);
|
||||||
|
|||||||
Reference in New Issue
Block a user