Files
ccs/tests/e2e/openai-provider-routing.e2e.test.ts
T

189 lines
5.9 KiB
TypeScript

import { afterEach, beforeAll, beforeEach, describe, expect, it } from 'bun:test';
import * as fs from 'fs';
import * as http from 'http';
import * as os from 'os';
import * as path from 'path';
import { spawn, spawnSync } from 'child_process';
import getPort from 'get-port';
const DIST_ENTRY = path.join(process.cwd(), 'dist', 'ccs.js');
let originalCcsHome: string | undefined;
let tempDir: string;
let upstreamServer: http.Server;
let upstreamBody: unknown;
beforeEach(() => {
originalCcsHome = process.env.CCS_HOME;
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-routing-e2e-'));
upstreamBody = undefined;
});
afterEach(() => {
try {
upstreamServer?.close();
} catch {
// Best-effort cleanup.
}
spawnSync(process.execPath, [DIST_ENTRY, 'proxy', 'stop'], {
env: { ...process.env, CCS_HOME: tempDir },
});
if (originalCcsHome !== undefined) {
process.env.CCS_HOME = originalCcsHome;
} else {
delete process.env.CCS_HOME;
}
fs.rmSync(tempDir, { recursive: true, force: true });
});
function startMockUpstream(port: number): Promise<void> {
return new Promise((resolve) => {
upstreamServer = http.createServer(async (req, res) => {
let body = '';
for await (const chunk of req) {
body += chunk.toString();
}
upstreamBody = JSON.parse(body);
res.writeHead(200, { 'Content-Type': 'text/event-stream' });
res.write(
'data: {"id":"chatcmpl_1","model":"hf-model","choices":[{"index":0,"delta":{"role":"assistant","content":"Hello"}}]}\n\n'
);
res.write(
'data: {"id":"chatcmpl_1","model":"hf-model","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"id":"call_1","type":"function","function":{"name":"search","arguments":"{\\"q\\":\\"docs\\"}"}}]}}]}\n\n'
);
res.write(
'data: {"id":"chatcmpl_1","model":"hf-model","choices":[{"index":0,"delta":{},"finish_reason":"tool_calls"}],"usage":{"prompt_tokens":9,"completion_tokens":4}}\n\n'
);
res.end('data: [DONE]\n\n');
});
upstreamServer.listen(port, '127.0.0.1', () => resolve());
});
}
function runCli(args: string[], env: Record<string, string>): Promise<{ code: number | null; stdout: string; stderr: string }> {
return new Promise((resolve) => {
const child = spawn(process.execPath, [DIST_ENTRY, ...args], {
env: {
...process.env,
...env,
},
stdio: ['ignore', 'pipe', 'pipe'],
});
let stdout = '';
let stderr = '';
child.stdout.on('data', (chunk) => {
stdout += chunk.toString();
});
child.stderr.on('data', (chunk) => {
stderr += chunk.toString();
});
child.on('close', (code) => {
resolve({ code, stdout, stderr });
});
});
}
beforeAll(() => {
const result = spawnSync(process.execPath, ['run', 'build'], {
encoding: 'utf8',
env: process.env,
});
expect(result.status).toBe(0);
});
describe('openai provider routing e2e', () => {
it('routes a settings profile through the local proxy into an OpenAI-compatible upstream', async () => {
const upstreamPort = await getPort();
await startMockUpstream(upstreamPort);
const ccsDir = path.join(tempDir, '.ccs');
const binDir = path.join(tempDir, 'bin');
const outputPath = path.join(tempDir, 'claude-output.json');
fs.mkdirSync(ccsDir, { recursive: true });
fs.mkdirSync(binDir, { recursive: true });
const settingsPath = path.join(ccsDir, 'hf.settings.json');
fs.writeFileSync(
path.join(ccsDir, 'config.json'),
JSON.stringify({ profiles: { hf: settingsPath } }, null, 2),
'utf8'
);
fs.writeFileSync(
settingsPath,
JSON.stringify({
env: {
ANTHROPIC_BASE_URL: `http://127.0.0.1:${upstreamPort}`,
ANTHROPIC_AUTH_TOKEN: 'hf_token',
ANTHROPIC_MODEL: 'hf-model',
CCS_DROID_PROVIDER: 'generic-chat-completion-api',
},
}),
'utf8'
);
fs.writeFileSync(
path.join(binDir, 'claude'),
`#!/usr/bin/env node
const fs = require('fs');
(async () => {
const response = await fetch(\`\${process.env.ANTHROPIC_BASE_URL}/v1/messages\`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'anthropic-version': '2023-06-01',
'x-api-key': process.env.ANTHROPIC_AUTH_TOKEN,
},
body: JSON.stringify({
model: process.env.ANTHROPIC_MODEL,
stream: true,
tools: [{ name: 'search', description: 'Search docs', input_schema: { type: 'object' } }],
messages: [{ role: 'user', content: 'Find docs' }],
}),
});
const text = await response.text();
fs.writeFileSync(process.env.CCS_E2E_OUTPUT, JSON.stringify({
status: response.status,
baseUrl: process.env.ANTHROPIC_BASE_URL,
authToken: process.env.ANTHROPIC_AUTH_TOKEN,
text
}, null, 2));
})().catch((error) => {
console.error(error);
process.exit(1);
});
`,
{ mode: 0o755 }
);
const result = await runCli(['hf'], {
...process.env,
CCS_HOME: tempDir,
CCS_E2E_OUTPUT: outputPath,
PATH: `${binDir}:${process.env.PATH || ''}`,
});
expect(result.code).toBe(0);
const payload = JSON.parse(fs.readFileSync(outputPath, 'utf8')) as {
status: number;
baseUrl: string;
authToken: string;
text: string;
};
expect(payload.status).toBe(200);
expect(payload.baseUrl).toMatch(/^http:\/\/127\.0\.0\.1:\d+$/);
expect(payload.authToken).toMatch(/^[a-f0-9]{48}$/);
expect(payload.text).toContain('event: message_start');
expect(payload.text).toContain('tool_use');
expect(payload.text).toContain('message_stop');
const parsedUpstream = upstreamBody as {
messages?: Array<{ role: string; content: string }>;
tools?: Array<{ type: string }>;
};
expect(parsedUpstream.messages?.[0]).toEqual({ role: 'user', content: 'Find docs' });
expect(parsedUpstream.tools?.[0]?.type).toBe('function');
}, 35000);
});