diff --git a/tests/integration/proxy/daemon-lifecycle.test.ts b/tests/integration/proxy/daemon-lifecycle.test.ts index 5a5aa86d..f6cb8e63 100644 --- a/tests/integration/proxy/daemon-lifecycle.test.ts +++ b/tests/integration/proxy/daemon-lifecycle.test.ts @@ -80,7 +80,7 @@ describe('openai proxy daemon lifecycle', () => { expect((await getOpenAICompatProxyStatus()).running).toBe(false); }, 35000); - it('refuses to replace a running proxy for a different profile', async () => { + it('allows different profiles to run on different ports', async () => { const firstPort = await getPort(); const firstSettingsPath = path.join(tempDir, 'hf.settings.json'); fs.writeFileSync( @@ -108,7 +108,20 @@ describe('openai proxy daemon lifecycle', () => { const firstStart = await startOpenAICompatProxy(firstProfile, { port: firstPort }); expect(firstStart.success).toBe(true); - const secondProfile = resolveOpenAICompatProfileConfig('openai', path.join(tempDir, 'openai.settings.json'), { + const secondPort = await getPort(); + const secondSettingsPath = path.join(tempDir, 'openai.settings.json'); + fs.writeFileSync( + secondSettingsPath, + JSON.stringify({ + env: { + ANTHROPIC_BASE_URL: 'https://api.openai.com/v1', + ANTHROPIC_AUTH_TOKEN: 'sk-openai', + ANTHROPIC_MODEL: 'gpt-4.1', + }, + }), + 'utf8' + ); + const secondProfile = resolveOpenAICompatProfileConfig('openai', secondSettingsPath, { ANTHROPIC_BASE_URL: 'https://api.openai.com/v1', ANTHROPIC_AUTH_TOKEN: 'sk-openai', ANTHROPIC_MODEL: 'gpt-4.1', @@ -117,11 +130,14 @@ describe('openai proxy daemon lifecycle', () => { throw new Error('Expected second OpenAI-compatible profile'); } - const secondStart = await startOpenAICompatProxy(secondProfile, { port: await getPort() }); - expect(secondStart.success).toBe(false); - expect(secondStart.error).toContain('Proxy already running for profile "hf"'); + const secondStart = await startOpenAICompatProxy(secondProfile, { port: secondPort }); + expect(secondStart.success).toBe(true); + expect(secondStart.port).toBe(secondPort); const health = await fetch(`http://127.0.0.1:${firstPort}/health`); expect(health.status).toBe(200); + + const secondHealth = await fetch(`http://127.0.0.1:${secondPort}/health`); + expect(secondHealth.status).toBe(200); }); }); diff --git a/tests/unit/proxy/proxy-port-resolver.test.ts b/tests/unit/proxy/proxy-port-resolver.test.ts new file mode 100644 index 00000000..c29d66c8 --- /dev/null +++ b/tests/unit/proxy/proxy-port-resolver.test.ts @@ -0,0 +1,42 @@ +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 { mutateUnifiedConfig } from '../../../src/config/unified-config-loader'; +import { resolveOpenAICompatProxyPreferredPort } from '../../../src/proxy/proxy-port-resolver'; + +let originalCcsHome: string | undefined; +let tempDir: string; + +beforeEach(() => { + originalCcsHome = process.env.CCS_HOME; + tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-proxy-config-')); + process.env.CCS_HOME = tempDir; +}); + +afterEach(() => { + if (originalCcsHome !== undefined) { + process.env.CCS_HOME = originalCcsHome; + } else { + delete process.env.CCS_HOME; + } + fs.rmSync(tempDir, { recursive: true, force: true }); +}); + +describe('resolveOpenAICompatProxyPreferredPort', () => { + it('returns the configured profile-scoped port when present', () => { + mutateUnifiedConfig((config) => { + config.proxy = { + ...(config.proxy ?? {}), + port: 3456, + profile_ports: { ccgm: 3461 }, + }; + }); + + expect(resolveOpenAICompatProxyPreferredPort('ccgm')).toBe(3461); + }); + + it('falls back to the shared default port when no profile mapping exists', () => { + expect(resolveOpenAICompatProxyPreferredPort('ccg')).toBe(3456); + }); +});