mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-14 16:17:02 +00:00
Merge remote-tracking branch 'origin/dev' into codex/fix-cliproxy-v3-snapshot-migration-issue
# Conflicts: # src/web-server/usage/cliproxy-usage-transformer.ts # tests/unit/web-server/cliproxy-usage-transformer.test.ts
This commit is contained in:
commit
ced9317565
210 files changed
+5627
-1065
No files matched your search
@@ -313,6 +313,12 @@ describe('web-server claude-extension-routes', () => {
|
||||
|
||||
it('creates a binding and applies managed settings to shared + IDE targets', async () => {
|
||||
const ideSettingsPath = path.join(tempHome, 'ide', 'vscode', 'settings.json');
|
||||
const sharedSettingsPath = path.join(tempHome, '.claude', 'settings.json');
|
||||
fs.mkdirSync(path.dirname(ideSettingsPath), { recursive: true });
|
||||
fs.mkdirSync(path.dirname(sharedSettingsPath), { recursive: true });
|
||||
fs.writeFileSync(sharedSettingsPath, JSON.stringify({ env: {} }, null, 2) + '\n', { mode: 0o600 });
|
||||
fs.writeFileSync(ideSettingsPath, JSON.stringify({}, null, 2) + '\n', { mode: 0o600 });
|
||||
|
||||
const createResponse = await fetch(`${baseUrl}/api/claude-extension/bindings`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
@@ -352,7 +358,6 @@ describe('web-server claude-extension-routes', () => {
|
||||
expect(applied.sharedSettings.state).toBe('applied');
|
||||
expect(applied.ideSettings.state).toBe('applied');
|
||||
|
||||
const sharedSettingsPath = path.join(tempHome, '.claude', 'settings.json');
|
||||
const sharedSettings = JSON.parse(fs.readFileSync(sharedSettingsPath, 'utf8')) as {
|
||||
env?: Record<string, string>;
|
||||
};
|
||||
@@ -370,6 +375,9 @@ describe('web-server claude-extension-routes', () => {
|
||||
)
|
||||
).toBe(true);
|
||||
expect(ideSettings['claudeCode.disableLoginPrompt']).toBe(true);
|
||||
|
||||
expect(fs.statSync(sharedSettingsPath).mode & 0o777).toBe(0o600);
|
||||
expect(fs.statSync(ideSettingsPath).mode & 0o777).toBe(0o600);
|
||||
});
|
||||
|
||||
it('resets only managed keys and preserves unrelated shared + IDE settings', async () => {
|
||||
|
||||
@@ -6,6 +6,7 @@ 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 { registerAccountFromToken } from '../../../src/cliproxy/auth/token-manager';
|
||||
import {
|
||||
clearQuotaCache,
|
||||
getCachedQuota,
|
||||
@@ -593,6 +594,61 @@ describe('cliproxy-auth-routes manual callback nickname persistence', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('reauthenticates a targeted existing Codex account when its token file is rewritten', async () => {
|
||||
const tokenDir = path.join(tempHome, '.ccs', 'cliproxy', 'auth');
|
||||
fs.mkdirSync(tokenDir, { recursive: true });
|
||||
const tokenPath = path.join(tokenDir, 'codex-existing@example.com.json');
|
||||
fs.writeFileSync(
|
||||
tokenPath,
|
||||
JSON.stringify({ type: 'codex', email: 'existing@example.com', version: 1 }),
|
||||
'utf8'
|
||||
);
|
||||
|
||||
const initialAccount = registerAccountFromToken('codex', tokenDir, 'work');
|
||||
expect(initialAccount?.id).toBe('existing@example.com');
|
||||
|
||||
mockFetch([
|
||||
{
|
||||
url: /\/v0\/management\/codex-auth-url\?is_webui=true$/,
|
||||
response: {
|
||||
auth_url: 'https://auth.example.com/authorize?state=state-targeted-rewrite',
|
||||
state: 'state-targeted-rewrite',
|
||||
},
|
||||
},
|
||||
{
|
||||
url: /\/v0\/management\/get-auth-status\?state=state-targeted-rewrite$/,
|
||||
response: { status: 'ok' },
|
||||
},
|
||||
]);
|
||||
|
||||
const startResponse = await postJson('/api/cliproxy/auth/codex/start-url', {
|
||||
accountId: 'existing@example.com',
|
||||
});
|
||||
expect(startResponse.status).toBe(200);
|
||||
|
||||
fs.writeFileSync(
|
||||
tokenPath,
|
||||
JSON.stringify({ type: 'codex', email: 'existing@example.com', version: 2 }),
|
||||
'utf8'
|
||||
);
|
||||
|
||||
const statusResponse = await getJson(
|
||||
'/api/cliproxy/auth/codex/status?state=state-targeted-rewrite'
|
||||
);
|
||||
|
||||
expect(statusResponse.status).toBe(200);
|
||||
expect(statusResponse.body).toEqual({
|
||||
status: 'ok',
|
||||
account: {
|
||||
id: 'existing@example.com',
|
||||
email: 'existing@example.com',
|
||||
nickname: 'work',
|
||||
provider: 'codex',
|
||||
isDefault: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('registers the new account before reporting polled auth success', async () => {
|
||||
mockFetch([
|
||||
{
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { describe, expect, it } from 'bun:test';
|
||||
import {
|
||||
getKiroStartIDCValidationError,
|
||||
getReauthAccountTarget,
|
||||
getStartAuthFailureMessage,
|
||||
getStartAuthNicknameError,
|
||||
getStartUrlUnsupportedReason,
|
||||
@@ -17,6 +18,7 @@ describe('cliproxy-auth-routes start-url guard', () => {
|
||||
"Provider 'codebuddy' uses Device Code flow"
|
||||
);
|
||||
expect(getStartUrlUnsupportedReason('kilo')).toContain("Provider 'kilo' uses Device Code flow");
|
||||
expect(getStartUrlUnsupportedReason('qoder')).toContain("Provider 'qoder' uses Device Code flow");
|
||||
});
|
||||
|
||||
it('allows Cursor browser URL auth on start-url', () => {
|
||||
@@ -142,3 +144,27 @@ describe('cliproxy-auth-routes nickname validation', () => {
|
||||
).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('cliproxy-auth-routes reauth account targeting', () => {
|
||||
const existingAccounts = [
|
||||
{ id: 'codex-user@example.com', nickname: 'work' },
|
||||
{ id: 'codex-personal@example.com', nickname: 'personal' },
|
||||
];
|
||||
|
||||
it('does not require a target for normal add-account auth', () => {
|
||||
expect(getReauthAccountTarget(undefined, existingAccounts)).toEqual({});
|
||||
expect(getReauthAccountTarget('', existingAccounts)).toEqual({});
|
||||
});
|
||||
|
||||
it('resolves an existing account target for reauth', () => {
|
||||
expect(getReauthAccountTarget('codex-user@example.com', existingAccounts)).toEqual({
|
||||
account: { id: 'codex-user@example.com', nickname: 'work' },
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects unknown account targets instead of falling back to ambiguous registration', () => {
|
||||
expect(getReauthAccountTarget('missing', existingAccounts)).toEqual({
|
||||
error: "Account 'missing' not found for this provider",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -112,6 +112,21 @@ describe('cliproxy usage syncer', () => {
|
||||
const snapshotPath = path.join(ccsDir, 'cache', 'cliproxy-usage', 'latest.json');
|
||||
expect(fs.existsSync(snapshotPath)).toBe(true);
|
||||
|
||||
const snapshot = JSON.parse(fs.readFileSync(snapshotPath, 'utf-8')) as {
|
||||
details: Array<Record<string, unknown>>;
|
||||
};
|
||||
expect(snapshot.details[0]).not.toHaveProperty('source');
|
||||
expect(snapshot.details[0]).not.toHaveProperty('authIndex');
|
||||
|
||||
if (process.platform !== 'win32') {
|
||||
const cacheDir = path.join(ccsDir, 'cache');
|
||||
const cliproxyCacheDir = path.dirname(snapshotPath);
|
||||
expect(fs.statSync(ccsDir).mode & 0o777).toBe(0o700);
|
||||
expect(fs.statSync(cacheDir).mode & 0o777).toBe(0o700);
|
||||
expect(fs.statSync(cliproxyCacheDir).mode & 0o777).toBe(0o700);
|
||||
expect(fs.statSync(snapshotPath).mode & 0o777).toBe(0o600);
|
||||
}
|
||||
|
||||
const cached = await runWithScopedConfigDir(ccsDir, async () => {
|
||||
return await loadCachedCliproxyData();
|
||||
});
|
||||
|
||||
@@ -110,6 +110,8 @@ describe('cliproxy usage transformer', () => {
|
||||
const flat = extractCliproxyUsageHistoryDetails(sampleResponse);
|
||||
expect(flat).toHaveLength(4);
|
||||
expect(flat[0].provider).toBe('google');
|
||||
expect(flat[0]).not.toHaveProperty('source');
|
||||
expect(flat[0]).not.toHaveProperty('authIndex');
|
||||
expect(
|
||||
flat.some(
|
||||
(entry) => entry.failed === true && entry.inputTokens === 40 && entry.outputTokens === 10
|
||||
@@ -160,8 +162,8 @@ describe('cliproxy usage transformer', () => {
|
||||
const legacyDetail = normalizeCliproxyUsageHistoryDetail({
|
||||
model: incoming.model,
|
||||
timestamp: incoming.timestamp,
|
||||
source: incoming.source,
|
||||
authIndex: incoming.authIndex,
|
||||
source: 'account-a',
|
||||
authIndex: '0',
|
||||
inputTokens: incoming.inputTokens,
|
||||
outputTokens: incoming.outputTokens,
|
||||
cacheReadTokens: incoming.cacheReadTokens,
|
||||
@@ -170,6 +172,8 @@ describe('cliproxy usage transformer', () => {
|
||||
|
||||
expect(legacyDetail?.requestCount).toBe(1);
|
||||
expect(Number.isFinite(legacyDetail?.cost)).toBe(true);
|
||||
expect(legacyDetail).not.toHaveProperty('source');
|
||||
expect(legacyDetail).not.toHaveProperty('authIndex');
|
||||
|
||||
const merged = mergeCliproxyUsageHistoryDetails([legacyDetail!], [incoming]);
|
||||
expect(merged).toHaveLength(1);
|
||||
@@ -177,6 +181,19 @@ describe('cliproxy usage transformer', () => {
|
||||
expect(merged[0].cost).toBe(incoming.cost);
|
||||
});
|
||||
|
||||
it('strips legacy account identifiers when merging persisted history', () => {
|
||||
const details = extractCliproxyUsageHistoryDetails(sampleResponse);
|
||||
const legacyDetail = {
|
||||
...details[0],
|
||||
source: 'user@example.com',
|
||||
authIndex: 'auth-file-7',
|
||||
};
|
||||
const merged = mergeCliproxyUsageHistoryDetails([legacyDetail], []);
|
||||
|
||||
expect(merged[0]).not.toHaveProperty('source');
|
||||
expect(merged[0]).not.toHaveProperty('authIndex');
|
||||
});
|
||||
|
||||
it('preserves legitimate duplicate requests when the incoming batch has more occurrences', () => {
|
||||
const details = extractCliproxyUsageHistoryDetails(sampleResponse);
|
||||
const repeated = [details[0], { ...details[0] }];
|
||||
|
||||
@@ -311,6 +311,28 @@ describe('codex native usage collector', () => {
|
||||
expect(entries).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('writes native usage caches with owner-only permissions', async () => {
|
||||
if (process.platform === 'win32') return;
|
||||
|
||||
writeCodexRollout(tempRoot);
|
||||
const cacheDir = getCacheDir();
|
||||
const previousUmask = process.umask(0o022);
|
||||
|
||||
try {
|
||||
await scanCodexNativeUsageEntries({
|
||||
env: { CODEX_HOME: tempRoot },
|
||||
homeDir: tempRoot,
|
||||
cacheDir,
|
||||
});
|
||||
} finally {
|
||||
process.umask(previousUmask);
|
||||
}
|
||||
|
||||
const cachePath = path.join(cacheDir, 'codex-native-usage-v1.json');
|
||||
expect(fs.statSync(cacheDir).mode & 0o777).toBe(0o700);
|
||||
expect(fs.statSync(cachePath).mode & 0o777).toBe(0o600);
|
||||
});
|
||||
|
||||
it('keeps default and include-cliproxy cache entries separate', async () => {
|
||||
writeCodexRollout(tempRoot, { modelProvider: 'cliproxy' });
|
||||
const cacheDir = getCacheDir();
|
||||
|
||||
@@ -510,6 +510,17 @@ describe('Cursor Routes Logic', () => {
|
||||
expect(json.current).toBe('gpt-5.3-codex');
|
||||
});
|
||||
|
||||
it('POST /api/cursor/probe rejects cross-origin requests', async () => {
|
||||
const res = await fetch(`${baseUrl}/api/cursor/probe`, {
|
||||
method: 'POST',
|
||||
headers: { Origin: 'https://attacker.example' },
|
||||
});
|
||||
expect(res.status).toBe(403);
|
||||
|
||||
const json = (await res.json()) as { error?: string };
|
||||
expect(json.error).toContain('Cross-origin probe requests are not allowed.');
|
||||
});
|
||||
|
||||
it('POST /api/cursor/probe returns auth failure when credentials are missing', async () => {
|
||||
const res = await fetch(`${baseUrl}/api/cursor/probe`, { method: 'POST' });
|
||||
expect(res.status).toBe(401);
|
||||
|
||||
@@ -340,4 +340,31 @@ describe('image-analysis routes', () => {
|
||||
error: 'Profile mapping for "codexProfile" references an unknown backend.',
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects unsupported provider backends and preserves existing configuration', async () => {
|
||||
const putResponse = await fetch(`${baseUrl}/api/image-analysis`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
providerModels: {
|
||||
gemini: 'gemini-3-flash-preview',
|
||||
evil: 'vision-model',
|
||||
},
|
||||
fallbackBackend: 'gemini',
|
||||
}),
|
||||
});
|
||||
|
||||
expect(putResponse.status).toBe(400);
|
||||
expect(await putResponse.json()).toEqual({
|
||||
error: 'Unsupported provider backend "evil".',
|
||||
});
|
||||
|
||||
const getResponse = await fetch(`${baseUrl}/api/image-analysis`);
|
||||
expect(getResponse.status).toBe(200);
|
||||
const payload = await getResponse.json();
|
||||
expect(payload.config.providerModels).toMatchObject({
|
||||
gemini: 'gemini-3-flash-preview',
|
||||
ghcp: 'claude-haiku-4.5',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -6,11 +6,20 @@ import profileRoutes from '../../../src/web-server/routes/profile-routes';
|
||||
describe('profile-routes local runtime readiness', () => {
|
||||
let server: Server;
|
||||
let baseUrl = '';
|
||||
let forcedRemoteAddress = '127.0.0.1';
|
||||
let originalDashboardAuthEnabled: string | undefined;
|
||||
const originalFetch = globalThis.fetch;
|
||||
|
||||
beforeAll(async () => {
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
app.use((req, _res, next) => {
|
||||
Object.defineProperty(req.socket, 'remoteAddress', {
|
||||
value: forcedRemoteAddress,
|
||||
configurable: true,
|
||||
});
|
||||
next();
|
||||
});
|
||||
app.use('/api/profiles', profileRoutes);
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
@@ -36,6 +45,10 @@ describe('profile-routes local runtime readiness', () => {
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
originalDashboardAuthEnabled = process.env.CCS_DASHBOARD_AUTH_ENABLED;
|
||||
process.env.CCS_DASHBOARD_AUTH_ENABLED = 'false';
|
||||
forcedRemoteAddress = '127.0.0.1';
|
||||
|
||||
globalThis.fetch = (async (input: RequestInfo | URL) => {
|
||||
const url = String(input);
|
||||
|
||||
@@ -73,6 +86,12 @@ describe('profile-routes local runtime readiness', () => {
|
||||
|
||||
afterEach(() => {
|
||||
globalThis.fetch = originalFetch;
|
||||
|
||||
if (originalDashboardAuthEnabled !== undefined) {
|
||||
process.env.CCS_DASHBOARD_AUTH_ENABLED = originalDashboardAuthEnabled;
|
||||
} else {
|
||||
delete process.env.CCS_DASHBOARD_AUTH_ENABLED;
|
||||
}
|
||||
});
|
||||
|
||||
it('reports local runtimes as ready when their endpoints respond with models', async () => {
|
||||
@@ -103,6 +122,30 @@ describe('profile-routes local runtime readiness', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('blocks remote readiness probes when dashboard auth is disabled', async () => {
|
||||
let probeCount = 0;
|
||||
forcedRemoteAddress = '10.10.0.24';
|
||||
globalThis.fetch = (async (input: RequestInfo | URL) => {
|
||||
const url = String(input);
|
||||
if (url.startsWith(baseUrl)) {
|
||||
return originalFetch(input);
|
||||
}
|
||||
probeCount += 1;
|
||||
return new Response(JSON.stringify({ models: [{ name: 'gemma4:e4b' }] }), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}) as typeof fetch;
|
||||
|
||||
const response = await fetch(`${baseUrl}/api/profiles/local-runtime-readiness`);
|
||||
|
||||
expect(response.status).toBe(403);
|
||||
expect(await response.json()).toEqual({
|
||||
error: 'Local runtime readiness requires localhost access when dashboard auth is disabled.',
|
||||
});
|
||||
expect(probeCount).toBe(0);
|
||||
});
|
||||
|
||||
it('reports setup guidance when local endpoints are unavailable', async () => {
|
||||
globalThis.fetch = (async (input: RequestInfo | URL) => {
|
||||
const url = String(input);
|
||||
|
||||
@@ -7,16 +7,16 @@ describe('route target parsing', () => {
|
||||
it('accepts valid target values', () => {
|
||||
expect(parseProfileTarget('claude')).toBe('claude');
|
||||
expect(parseProfileTarget('DROID')).toBe('droid');
|
||||
expect(parseProfileTarget('codex')).toBe('codex');
|
||||
expect(parseVariantTarget(' claude ')).toBe('claude');
|
||||
expect(parseVariantTarget('droid')).toBe('droid');
|
||||
expect(parseVariantTarget(' codex ')).toBe('codex');
|
||||
});
|
||||
|
||||
it('returns null for invalid target values', () => {
|
||||
expect(parseProfileTarget('glm')).toBeNull();
|
||||
expect(parseProfileTarget('codex')).toBeNull();
|
||||
expect(parseProfileTarget('')).toBeNull();
|
||||
expect(parseVariantTarget('factory')).toBeNull();
|
||||
expect(parseVariantTarget('codex')).toBeNull();
|
||||
expect(parseVariantTarget(' ')).toBeNull();
|
||||
});
|
||||
|
||||
|
||||
@@ -83,11 +83,7 @@ function writeAssistantEntriesToDir(baseClaudeDir: string, entries: AssistantFix
|
||||
},
|
||||
});
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(projectDir, `${entry.sessionId}.jsonl`),
|
||||
`${line}\n`,
|
||||
'utf-8'
|
||||
);
|
||||
fs.writeFileSync(path.join(projectDir, `${entry.sessionId}.jsonl`), `${line}\n`, 'utf-8');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,7 +218,10 @@ describe('usage handlers semantics', () => {
|
||||
res as never
|
||||
);
|
||||
|
||||
const payload = res.payload as { success: boolean; data: Array<{ hour: string; requests: number }> };
|
||||
const payload = res.payload as {
|
||||
success: boolean;
|
||||
data: Array<{ hour: string; requests: number }>;
|
||||
};
|
||||
const targetHour = payload.data.find((row) => row.hour === '2026-03-02 10:00');
|
||||
|
||||
expect(targetHour?.requests).toBe(3);
|
||||
@@ -415,6 +414,19 @@ describe('usage handlers semantics', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects non-string profile filters as validation errors', async () => {
|
||||
for (const profile of [['work', 'default'], { name: 'work' }]) {
|
||||
const res = createMockResponse();
|
||||
await handlers.handleSummary({ query: { profile } } as never, res as never);
|
||||
|
||||
expect(res.statusCode).toBe(400);
|
||||
expect(res.payload).toMatchObject({
|
||||
success: false,
|
||||
error: 'Invalid profile filter',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it('rejects reversed date ranges before computing summary totals', async () => {
|
||||
const res = createMockResponse();
|
||||
|
||||
|
||||
Reference in new issue
Block a user