chore: merge dev into openai provider routing

This commit is contained in:
Tam Nhu Tran committed 2026-04-15 20:36:03 -04:00
commit 1923d87a84
118 files changed
+4162 -3620

No files matched your search

@@ -149,68 +149,72 @@ describe('api-routes remote write guard', () => {
});
});
it('allows remote writes again when dashboard auth is enabled', async () => {
const password = 'testpassword123';
process.env.CCS_DASHBOARD_AUTH_ENABLED = 'true';
process.env.CCS_DASHBOARD_USERNAME = 'admin';
process.env.CCS_DASHBOARD_PASSWORD_HASH = await bcrypt.hash(password, 10);
it(
'allows remote writes again when dashboard auth is enabled',
async () => {
const password = 'testpassword123';
process.env.CCS_DASHBOARD_AUTH_ENABLED = 'true';
process.env.CCS_DASHBOARD_USERNAME = 'admin';
process.env.CCS_DASHBOARD_PASSWORD_HASH = await bcrypt.hash(password, 4);
const authApp = express();
authApp.use(express.json());
authApp.use((req, _res, next) => {
Object.defineProperty(req.socket, 'remoteAddress', {
value: forcedRemoteAddress,
configurable: true,
const authApp = express();
authApp.use(express.json());
authApp.use((req, _res, next) => {
Object.defineProperty(req.socket, 'remoteAddress', {
value: forcedRemoteAddress,
configurable: true,
});
next();
});
next();
});
authApp.use(createSessionMiddleware());
authApp.use(authMiddleware);
authApp.use('/api', apiRoutes);
authApp.use(createSessionMiddleware());
authApp.use(authMiddleware);
authApp.use('/api', apiRoutes);
const authServer = await new Promise<Server>((resolve, reject) => {
const instance = authApp.listen(0, '127.0.0.1');
instance.once('error', reject);
instance.once('listening', () => resolve(instance));
});
const authServer = await new Promise<Server>((resolve, reject) => {
const instance = authApp.listen(0, '127.0.0.1');
instance.once('error', reject);
instance.once('listening', () => resolve(instance));
});
const address = authServer.address();
if (!address || typeof address === 'string') {
throw new Error('Unable to resolve auth-enabled test server port');
}
const authBaseUrl = `http://127.0.0.1:${address.port}`;
const address = authServer.address();
if (!address || typeof address === 'string') {
throw new Error('Unable to resolve auth-enabled test server port');
}
const authBaseUrl = `http://127.0.0.1:${address.port}`;
const loginResponse = await fetch(`${authBaseUrl}/api/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
username: 'admin',
password,
}),
});
const cookie = loginResponse.headers.get('set-cookie');
const loginResponse = await fetch(`${authBaseUrl}/api/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
username: 'admin',
password,
}),
});
const cookie = loginResponse.headers.get('set-cookie');
expect(loginResponse.status).toBe(200);
expect(cookie).toBeTruthy();
expect(loginResponse.status).toBe(200);
expect(cookie).toBeTruthy();
const response = await fetch(`${authBaseUrl}/api/profiles`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Cookie: cookie as string,
},
body: JSON.stringify({
name: 'demo',
baseUrl: 'https://api.example.com',
apiKey: 'token',
}),
});
const response = await fetch(`${authBaseUrl}/api/profiles`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Cookie: cookie as string,
},
body: JSON.stringify({
name: 'demo',
baseUrl: 'https://api.example.com',
apiKey: 'token',
}),
});
expect(response.status).toBe(201);
expect(response.status).toBe(201);
await new Promise<void>((resolve) => authServer.close(() => resolve()));
await new Promise<void>((resolve) => authServer.close(() => resolve()));
delete process.env.CCS_DASHBOARD_USERNAME;
delete process.env.CCS_DASHBOARD_PASSWORD_HASH;
});
delete process.env.CCS_DASHBOARD_USERNAME;
delete process.env.CCS_DASHBOARD_PASSWORD_HASH;
},
15000
);
});
@@ -6,6 +6,11 @@ import * as path from 'path';
import * as http from 'http';
import type { Server } from 'http';
import cliproxyAuthRoutes from '../../../src/web-server/routes/cliproxy-auth-routes';
import {
clearQuotaCache,
getCachedQuota,
setCachedQuota,
} from '../../../src/cliproxy/quota-response-cache';
import { restoreFetch, mockFetch } from '../../mocks';
describe('cliproxy-auth-routes manual callback nickname persistence', () => {
@@ -49,6 +54,7 @@ describe('cliproxy-auth-routes manual callback nickname persistence', () => {
afterEach(() => {
restoreFetch();
clearQuotaCache();
if (originalCcsHome === undefined) {
delete process.env.CCS_HOME;
@@ -564,4 +570,62 @@ describe('cliproxy-auth-routes manual callback nickname persistence', () => {
expect(registry.providers.codex.accounts['new@example.com']?.email).toBe('new@example.com');
});
it('clears stale Claude quota cache after polling auth success', async () => {
mockFetch([
{
url: /\/v0\/management\/anthropic-auth-url\?is_webui=true$/,
response: {
auth_url: 'https://auth.example.com/authorize?state=state-claude-cache-clear',
state: 'state-claude-cache-clear',
},
},
{
url: /\/v0\/management\/get-auth-status\?state=state-claude-cache-clear$/,
response: { status: 'ok' },
},
]);
const startResponse = await postJson('/api/cliproxy/auth/claude/start-url', {});
expect(startResponse.status).toBe(200);
const accountId = 'claude-team@example.com';
setCachedQuota('claude', accountId, {
success: false,
error: 'Authentication required for policy limits',
needsReauth: true,
});
expect(getCachedQuota('claude', accountId)).not.toBeNull();
const tokenDir = path.join(tempHome, '.ccs', 'cliproxy', 'auth');
fs.mkdirSync(tokenDir, { recursive: true });
fs.writeFileSync(
path.join(tokenDir, 'claude-claude-team@example.com.json'),
JSON.stringify({
type: 'claude',
email: accountId,
access_token: 'fresh-token',
refresh_token: 'refresh-token',
expired: '2099-01-01T00:00:00.000Z',
}),
'utf8'
);
const statusResponse = await getJson(
'/api/cliproxy/auth/claude/status?state=state-claude-cache-clear'
);
expect(statusResponse.status).toBe(200);
expect(statusResponse.body).toEqual({
status: 'ok',
account: {
id: accountId,
email: accountId,
nickname: 'claude-team',
provider: 'claude',
isDefault: true,
},
});
expect(getCachedQuota('claude', accountId)).toBeNull();
});
});
@@ -13,6 +13,15 @@ describe('cliproxy-auth-routes start-url guard', () => {
);
expect(getStartUrlUnsupportedReason('ghcp')).toContain("Provider 'ghcp' uses Device Code flow");
expect(getStartUrlUnsupportedReason('qwen')).toContain("Provider 'qwen' uses Device Code flow");
expect(getStartUrlUnsupportedReason('cursor')).toContain(
"Provider 'cursor' uses Device Code flow"
);
expect(getStartUrlUnsupportedReason('codebuddy')).toContain(
"Provider 'codebuddy' uses Device Code flow"
);
expect(getStartUrlUnsupportedReason('kilo')).toContain(
"Provider 'kilo' uses Device Code flow"
);
});
it('allows Kiro social methods on start-url', () => {
@@ -36,6 +45,7 @@ describe('cliproxy-auth-routes start-url guard', () => {
expect(getStartUrlUnsupportedReason('gemini')).toBeNull();
expect(getStartUrlUnsupportedReason('codex')).toBeNull();
expect(getStartUrlUnsupportedReason('claude')).toBeNull();
expect(getStartUrlUnsupportedReason('gitlab')).toBeNull();
});
});
@@ -87,6 +97,7 @@ describe('cliproxy-auth-routes start failure messaging', () => {
it('keeps generic failure text for other providers', () => {
expect(getStartAuthFailureMessage('gemini')).toBe('Authentication failed or was cancelled');
expect(getStartAuthFailureMessage('kiro')).toBe('Authentication failed or was cancelled');
expect(getStartAuthFailureMessage('gitlab')).toBe('Authentication failed or was cancelled');
});
});
+59 -51
View File
@@ -54,65 +54,73 @@ afterAll(async () => {
});
describe('codex routes', () => {
it('returns the current raw config snapshot from PATCH /config/patch', async () => {
const res = await fetch(`${baseUrl}/api/codex/config/patch`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
kind: 'top-level',
values: {
model: 'gpt-5.4',
sandboxMode: 'workspace-write',
},
}),
});
it(
'returns the current raw config snapshot from PATCH /config/patch',
async () => {
const res = await fetch(`${baseUrl}/api/codex/config/patch`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
kind: 'top-level',
values: {
model: 'gpt-5.4',
sandboxMode: 'workspace-write',
},
}),
});
expect(res.status).toBe(200);
expect(res.status).toBe(200);
const json = (await res.json()) as {
success: boolean;
exists: boolean;
mtime: number;
rawText: string;
config: Record<string, unknown> | null;
parseError: string | null;
readError: string | null;
};
const json = (await res.json()) as {
success: boolean;
exists: boolean;
mtime: number;
rawText: string;
config: Record<string, unknown> | null;
parseError: string | null;
readError: string | null;
};
expect(json.success).toBe(true);
expect(json.exists).toBe(true);
expect(json.mtime).toBeGreaterThan(0);
expect(json.parseError).toBeNull();
expect(json.readError).toBeNull();
expect(json.rawText).toContain('model = "gpt-5.4"');
expect(json.rawText).toContain('sandbox_mode = "workspace-write"');
expect(json.config?.model).toBe('gpt-5.4');
expect(json.success).toBe(true);
expect(json.exists).toBe(true);
expect(json.mtime).toBeGreaterThan(0);
expect(json.parseError).toBeNull();
expect(json.readError).toBeNull();
expect(json.rawText).toContain('model = "gpt-5.4"');
expect(json.rawText).toContain('sandbox_mode = "workspace-write"');
expect(json.config?.model).toBe('gpt-5.4');
const written = fs.readFileSync(path.join(codexHome, 'config.toml'), 'utf8');
expect(written).toBe(json.rawText);
});
const written = fs.readFileSync(path.join(codexHome, 'config.toml'), 'utf8');
expect(written).toBe(json.rawText);
},
10000
);
it('returns 409 when PATCH /config/patch receives a stale expectedMtime', async () => {
const configPath = path.join(codexHome, 'config.toml');
fs.writeFileSync(configPath, 'model = "gpt-5.3-codex"\n');
it(
'returns 409 when PATCH /config/patch receives a stale expectedMtime',
async () => {
const configPath = path.join(codexHome, 'config.toml');
fs.writeFileSync(configPath, 'model = "gpt-5.3-codex"\n');
const res = await fetch(`${baseUrl}/api/codex/config/patch`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
kind: 'feature',
feature: 'multi_agent',
enabled: true,
expectedMtime: 1,
}),
});
const res = await fetch(`${baseUrl}/api/codex/config/patch`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
kind: 'feature',
feature: 'multi_agent',
enabled: true,
expectedMtime: 1,
}),
});
expect(res.status).toBe(409);
expect(res.status).toBe(409);
const json = (await res.json()) as { error: string; mtime: number };
expect(json.error).toContain('File modified externally.');
expect(json.mtime).toBeGreaterThan(0);
});
const json = (await res.json()) as { error: string; mtime: number };
expect(json.error).toContain('File modified externally.');
expect(json.mtime).toBeGreaterThan(0);
},
10000
);
it('allows PATCH /config/patch on an existing config.toml without expectedMtime', async () => {
fs.writeFileSync(path.join(codexHome, 'config.toml'), 'model = "gpt-5.4"\n');
@@ -131,18 +131,22 @@ describe('droid-dashboard-service', () => {
expect(raw.rawText).toContain('invalid-json');
});
it('includes structured docs links for fact-checking providers', async () => {
const diagnostics = await getDroidDashboardDiagnostics();
it(
'includes structured docs links for fact-checking providers',
async () => {
const diagnostics = await getDroidDashboardDiagnostics();
expect(diagnostics.docsReference.links.length).toBeGreaterThan(0);
expect(diagnostics.docsReference.providerDocs.length).toBeGreaterThan(0);
expect(diagnostics.docsReference.links.every((link) => link.url.startsWith('https://'))).toBe(
true
);
expect(
diagnostics.docsReference.providerDocs.some((doc) => doc.provider === 'anthropic')
).toBe(true);
});
expect(diagnostics.docsReference.links.length).toBeGreaterThan(0);
expect(diagnostics.docsReference.providerDocs.length).toBeGreaterThan(0);
expect(diagnostics.docsReference.links.every((link) => link.url.startsWith('https://'))).toBe(
true
);
expect(
diagnostics.docsReference.providerDocs.some((doc) => doc.provider === 'anthropic')
).toBe(true);
},
10000
);
it('falls back to legacy config custom_models when settings customModels is absent', async () => {
const settingsDir = path.join(testRoot, '.factory');