From c107a6752c9d1eb2d510b85fed37ea9bd26fc767 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Wed, 29 Jul 2026 13:31:20 -0400 Subject: [PATCH] test(cliproxy): cover auto compact windows --- .../__tests__/model-catalog-compat.test.ts | 12 ++ .../__tests__/auto-compact-window.test.ts | 180 ++++++++++++++++++ .../commands/cliproxy-catalog-json.test.ts | 27 ++- 3 files changed, 203 insertions(+), 16 deletions(-) create mode 100644 src/cliproxy/config/__tests__/auto-compact-window.test.ts diff --git a/src/cliproxy/__tests__/model-catalog-compat.test.ts b/src/cliproxy/__tests__/model-catalog-compat.test.ts index ff8bce9f..aaaa01d6 100644 --- a/src/cliproxy/__tests__/model-catalog-compat.test.ts +++ b/src/cliproxy/__tests__/model-catalog-compat.test.ts @@ -75,6 +75,18 @@ describe('model-catalog compatibility lookups', () => { expect(catalog?.models[0]?.extendedContext).toBeUndefined(); }); + it('maps positive integer live context length into contextWindow metadata', () => { + const catalog = mergeCatalog('xai', [ + { + id: 'grok-4.3', + display_name: 'Grok 4.3 Live', + context_length: 1_000_000, + }, + ]); + + expect(catalog?.models[0]?.contextWindow).toBe(1_000_000); + }); + it('does not re-add stale static-only models when live catalog data is present', () => { const catalog = mergeCatalog('gemini', [ { diff --git a/src/cliproxy/config/__tests__/auto-compact-window.test.ts b/src/cliproxy/config/__tests__/auto-compact-window.test.ts new file mode 100644 index 00000000..c3a99476 --- /dev/null +++ b/src/cliproxy/config/__tests__/auto-compact-window.test.ts @@ -0,0 +1,180 @@ +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { afterEach, beforeEach, describe, expect, it } from 'bun:test'; + +import { invalidateConfigCache } from '../../../config/config-loader-facade'; +import { clearConfigCache } from '../base-config-loader'; +import { applyClaudeAutoCompactWindow } from '../env-builder'; +import { buildClaudeEnvironment } from '../../executor/env-resolver'; + +describe('applyClaudeAutoCompactWindow', () => { + it('injects the catalog window for the effective model after suffix normalization', () => { + const env = applyClaudeAutoCompactWindow( + { + ANTHROPIC_MODEL: 'gpt-5.6-sol-xhigh[1m]', + }, + 'codex' + ); + + expect(env.CLAUDE_CODE_AUTO_COMPACT_WINDOW).toBe('372000'); + }); + + it('uses the selected provider when the same model family exists in multiple catalogs', () => { + const env = applyClaudeAutoCompactWindow( + { + ANTHROPIC_MODEL: 'claude-opus-4-6-thinking', + }, + 'agy' + ); + + expect(env.CLAUDE_CODE_AUTO_COMPACT_WINDOW).toBe('200000'); + }); + + it('does not inject a value for unknown or custom models', () => { + const env = applyClaudeAutoCompactWindow( + { + ANTHROPIC_MODEL: 'custom-model-with-unknown-window', + }, + 'codex' + ); + + expect('CLAUDE_CODE_AUTO_COMPACT_WINDOW' in env).toBe(false); + }); + + it('preserves an explicit user value', () => { + const env = applyClaudeAutoCompactWindow( + { + ANTHROPIC_MODEL: 'gpt-5.6-sol', + CLAUDE_CODE_AUTO_COMPACT_WINDOW: '123456', + }, + 'codex' + ); + + expect(env.CLAUDE_CODE_AUTO_COMPACT_WINDOW).toBe('123456'); + }); + + it('treats an explicit empty value as authoritative', () => { + const env = applyClaudeAutoCompactWindow( + { + ANTHROPIC_MODEL: 'gpt-5.6-sol', + CLAUDE_CODE_AUTO_COMPACT_WINDOW: '', + }, + 'codex' + ); + + expect(env.CLAUDE_CODE_AUTO_COMPACT_WINDOW).toBe(''); + }); + + it('does not mistake a differently cased variable for the Claude Code key', () => { + const env = applyClaudeAutoCompactWindow( + { + ANTHROPIC_MODEL: 'gpt-5.6-sol', + claude_code_auto_compact_window: '123456', + }, + 'codex' + ); + + expect(env.claude_code_auto_compact_window).toBe('123456'); + expect(env.CLAUDE_CODE_AUTO_COMPACT_WINDOW).toBe('372000'); + }); +}); + +describe('Claude launch auto-compact integration', () => { + let tempHome: string; + let originalCcsHome: string | undefined; + let originalAutoCompactWindow: string | undefined; + + beforeEach(() => { + originalCcsHome = process.env.CCS_HOME; + originalAutoCompactWindow = process.env.CLAUDE_CODE_AUTO_COMPACT_WINDOW; + tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-auto-compact-')); + process.env.CCS_HOME = tempHome; + delete process.env.CLAUDE_CODE_AUTO_COMPACT_WINDOW; + invalidateConfigCache(); + clearConfigCache(); + }); + + afterEach(() => { + if (originalCcsHome === undefined) delete process.env.CCS_HOME; + else process.env.CCS_HOME = originalCcsHome; + + if (originalAutoCompactWindow === undefined) { + delete process.env.CLAUDE_CODE_AUTO_COMPACT_WINDOW; + } else { + process.env.CLAUDE_CODE_AUTO_COMPACT_WINDOW = originalAutoCompactWindow; + } + + invalidateConfigCache(); + clearConfigCache(); + fs.rmSync(tempHome, { recursive: true, force: true }); + }); + + it('injects the catalog window for the provider default model on Claude launches', () => { + const env = buildClaudeEnvironment({ + provider: 'codex', + useRemoteProxy: false, + localPort: 8317, + verbose: false, + }); + + expect(env.ANTHROPIC_MODEL).toBe('gpt-5.4(high)'); + expect(env.CLAUDE_CODE_AUTO_COMPACT_WINDOW).toBe('1050000'); + }); + + it('preserves an inherited user value on Claude launches', () => { + process.env.CLAUDE_CODE_AUTO_COMPACT_WINDOW = ''; + + const env = buildClaudeEnvironment({ + provider: 'codex', + useRemoteProxy: false, + localPort: 8317, + verbose: false, + }); + + expect(env.CLAUDE_CODE_AUTO_COMPACT_WINDOW).toBe(''); + }); + + it('uses the composite default tier provider for model lookup', () => { + const env = buildClaudeEnvironment({ + provider: 'codex', + useRemoteProxy: false, + localPort: 8317, + verbose: false, + isComposite: true, + compositeTiers: { + opus: { provider: 'agy', model: 'claude-opus-4-6-thinking' }, + sonnet: { provider: 'codex', model: 'gpt-5.6-sol' }, + haiku: { provider: 'kimi', model: 'kimi-k2' }, + }, + compositeDefaultTier: 'opus', + }); + + expect(env.CLAUDE_CODE_AUTO_COMPACT_WINDOW).toBe('200000'); + }); + + it('preserves an explicit empty value from a custom settings file', () => { + const settingsPath = path.join(tempHome, 'codex-custom.settings.json'); + fs.writeFileSync( + settingsPath, + JSON.stringify({ + env: { + ANTHROPIC_BASE_URL: 'http://127.0.0.1:8317', + ANTHROPIC_AUTH_TOKEN: 'test-token', + ANTHROPIC_MODEL: 'gpt-5.6-sol', + CLAUDE_CODE_AUTO_COMPACT_WINDOW: '', + }, + }) + ); + + const env = buildClaudeEnvironment({ + provider: 'codex', + useRemoteProxy: false, + localPort: 8317, + customSettingsPath: settingsPath, + verbose: false, + }); + + expect(env.CLAUDE_CODE_AUTO_COMPACT_WINDOW).toBe(''); + }); +}); diff --git a/tests/unit/commands/cliproxy-catalog-json.test.ts b/tests/unit/commands/cliproxy-catalog-json.test.ts index 7bc3e5ea..8c99c79a 100644 --- a/tests/unit/commands/cliproxy-catalog-json.test.ts +++ b/tests/unit/commands/cliproxy-catalog-json.test.ts @@ -42,10 +42,7 @@ describe('cliproxy catalog --json output', () => { it('includes metadata fields when present on model entries', () => { handleCatalogJson(); - const parsed = JSON.parse(capturedOutput[0]) as Record< - string, - Array> - >; + const parsed = JSON.parse(capturedOutput[0]) as Record>>; const allModels = Object.values(parsed).flat(); // At least some models in the static catalog have tier set @@ -56,15 +53,19 @@ describe('cliproxy catalog --json output', () => { for (const model of withTier) { expect(['free', 'pro', 'ultra']).toContain(model.tier); } + + const withContextWindow = allModels.filter((m) => m.contextWindow !== undefined); + expect(withContextWindow.length).toBeGreaterThan(0); + for (const model of withContextWindow) { + expect(Number.isInteger(model.contextWindow)).toBe(true); + expect(model.contextWindow as number).toBeGreaterThan(0); + } }); it('omits undefined optional fields instead of including nulls', () => { handleCatalogJson(); - const parsed = JSON.parse(capturedOutput[0]) as Record< - string, - Array> - >; + const parsed = JSON.parse(capturedOutput[0]) as Record>>; const allModels = Object.values(parsed).flat(); for (const model of allModels) { @@ -78,10 +79,7 @@ describe('cliproxy catalog --json output', () => { it('includes explicit false boolean values in output', () => { handleCatalogJson(); - const parsed = JSON.parse(capturedOutput[0]) as Record< - string, - Array> - >; + const parsed = JSON.parse(capturedOutput[0]) as Record>>; const allModels = Object.values(parsed).flat(); // Static catalog has models with extendedContext: false @@ -92,10 +90,7 @@ describe('cliproxy catalog --json output', () => { it('includes thinking configuration when present on models', () => { handleCatalogJson(); - const parsed = JSON.parse(capturedOutput[0]) as Record< - string, - Array> - >; + const parsed = JSON.parse(capturedOutput[0]) as Record>>; const allModels = Object.values(parsed).flat(); // Static catalog has thinking models (e.g. Claude Opus 4.6 Thinking)