From 25f643ddc2333ef061c8f851dba218b281614361 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6khan=20=C3=96zdemir?= Date: Thu, 10 Sep 2026 03:33:07 +0300 Subject: [PATCH 1/2] chore: preferred Claude models to update for desktop --- .../connectors/__tests__/desktop.test.ts | 42 ++++++++++++------- src/cli/commands/proxy/connectors/desktop.ts | 38 +++++++++++------ 2 files changed, 54 insertions(+), 26 deletions(-) diff --git a/src/cli/commands/proxy/connectors/__tests__/desktop.test.ts b/src/cli/commands/proxy/connectors/__tests__/desktop.test.ts index de089b2e0..c4a971255 100644 --- a/src/cli/commands/proxy/connectors/__tests__/desktop.test.ts +++ b/src/cli/commands/proxy/connectors/__tests__/desktop.test.ts @@ -205,10 +205,13 @@ describe('fetchClaudeModels', () => { json: async () => [ { base_name: 'claude-sonnet-4-5-20250929' }, { base_name: 'claude-4-5-sonnet' }, + { base_name: 'claude-sonnet-5' }, { base_name: 'claude-sonnet-4-6' }, { base_name: 'claude-opus-4-5-20251101' }, { base_name: 'claude-opus-4-6-20260205' }, { base_name: 'claude-opus-4-7' }, + { base_name: 'claude-opus-4-8' }, + { base_name: 'claude-opus-5' }, { base_name: 'claude-haiku-4-5-20251001' }, { base_name: 'claude-opus-4-6-vertex' }, { base_name: 'gpt-5.5-2026-04-24' }, @@ -219,10 +222,13 @@ describe('fetchClaudeModels', () => { expect(models).toEqual([ 'claude-sonnet-4-5-20250929', 'claude-4-5-sonnet', + 'claude-sonnet-5', 'claude-sonnet-4-6', 'claude-opus-4-5-20251101', 'claude-opus-4-6-20260205', 'claude-opus-4-7', + 'claude-opus-4-8', + 'claude-opus-5', 'claude-haiku-4-5-20251001', ]); }); @@ -252,7 +258,15 @@ describe('fetchClaudeModels', () => { }) as unknown as typeof globalThis.fetch; const models = await fetchClaudeModels('http://127.0.0.1:4001', 'codemie-proxy'); - expect(models).toEqual(['claude-sonnet-4-6', 'claude-opus-4-8', 'claude-opus-4-7', 'claude-opus-4-6', 'claude-haiku-4-5']); + expect(models).toEqual([ + 'claude-opus-5', + 'claude-opus-4-8', + 'claude-opus-4-7', + 'claude-opus-4-6', + 'claude-sonnet-5', + 'claude-sonnet-4-6', + 'claude-haiku-4-5', + ]); }); it('throws when response is not ok', async () => { @@ -298,7 +312,7 @@ describe('fetchClaudeModels', () => { globalThis.fetch = vi.fn().mockResolvedValue({ ok: true, headers: mkHeaders('text/html; charset=utf-8'), - json: async () => { throw new SyntaxError("Unexpected token '<'"); }, + json: async () => { throw new SyntaxError('Unexpected token '<''); }, }) as unknown as typeof globalThis.fetch; await expect(fetchClaudeModels('http://127.0.0.1:4001', 'codemie-proxy')) @@ -341,11 +355,11 @@ describe('selectPreferredClaudeModels', () => { it('returns exact matches when present and dated fallbacks otherwise', () => { expect(selectPreferredClaudeModels(available)).toEqual([ - 'claude-sonnet-4-6', // exact - 'claude-opus-4-7', // exact + 'claude-opus-4-7', // exact 'claude-opus-4-6-20260205', // dated fallback - 'claude-haiku-4-5-20251001',// dated fallback - ]); + 'claude-sonnet-4-6', // exact + 'claude-haiku-4-5-20251001', // dated fallback + ]); }); it('preserves the order of the preferred list', () => { @@ -383,10 +397,10 @@ describe('selectDesktopClaudeModels', () => { 'claude-haiku-4-5-20251001', ]); expect(result).toEqual([ - 'claude-sonnet-4-6', 'claude-opus-4-8', + 'claude-sonnet-4-6', 'claude-haiku-4-5-20251001', - ]); + ]); }); it('falls back to the highest available opus when 4.8 is absent', () => { @@ -397,10 +411,10 @@ describe('selectDesktopClaudeModels', () => { 'claude-haiku-4-5-20251001', ]); expect(result).toEqual([ - 'claude-sonnet-4-6', 'claude-opus-4-7', + 'claude-sonnet-4-6', 'claude-haiku-4-5-20251001', - ]); + ]); }); it('uses the next opus down when only 4.6 is available', () => { @@ -500,10 +514,10 @@ describe('writeDesktopConfig', () => { const written = await writeDesktopConfig('http://127.0.0.1:4001', 'codemie-proxy', baseDir, [], statePath); const config = JSON.parse(await readFile(written, 'utf-8')); expect(JSON.parse(config.inferenceModels)).toEqual([ - { name: 'claude-sonnet-4-6' }, { name: 'claude-opus-4-7' }, + { name: 'claude-sonnet-4-6' }, { name: 'claude-haiku-4-5-20251001' }, - ]); + ]); }); it('replaces existing inferenceModels entries — does not merge user-added ones', async () => { @@ -517,10 +531,10 @@ describe('writeDesktopConfig', () => { const written = await writeDesktopConfig('http://127.0.0.1:4001', 'codemie-proxy', baseDir, [], statePath); const config = JSON.parse(await readFile(written, 'utf-8')); expect(JSON.parse(config.inferenceModels)).toEqual([ - { name: 'claude-sonnet-4-6' }, { name: 'claude-opus-4-7' }, + { name: 'claude-sonnet-4-6' }, { name: 'claude-haiku-4-5-20251001' }, - ]); + ]); }); it('fails fast when discovery returns nothing', async () => { diff --git a/src/cli/commands/proxy/connectors/desktop.ts b/src/cli/commands/proxy/connectors/desktop.ts index fe3be06e8..871c5638d 100644 --- a/src/cli/commands/proxy/connectors/desktop.ts +++ b/src/cli/commands/proxy/connectors/desktop.ts @@ -54,15 +54,19 @@ interface CodeMieLlmModel { * resolved ID is what gets written to the Desktop config so the gateway * receives a model name it has registered. * - * The opus entries are listed in descending preference (`4-8 → 4-7 → 4-6`): - * {@link selectDesktopClaudeModels} collapses them to the single highest-priority - * opus the gateway actually serves, so Desktop never shows more than one Opus. + * Opus and Sonnet entries are listed in descending preference + * (`opus: 5 → 4-8 → 4-7 → 4-6 → 3`, `sonnet: 5 → 4-6`): + * {@link selectDesktopClaudeModels} collapses each family to the single + * highest-priority ID the gateway actually serves, so Desktop never shows more + * than one Opus or one Sonnet (matching the latest cloud Desktop lineup). */ export const PREFERRED_CLAUDE_MODELS = [ - 'claude-sonnet-4-6', + 'claude-opus-5', 'claude-opus-4-8', 'claude-opus-4-7', 'claude-opus-4-6', + 'claude-sonnet-5', + 'claude-sonnet-4-6', 'claude-haiku-4-5', ] as const; @@ -225,11 +229,13 @@ export function selectPreferredClaudeModels( * Build the exact model set Claude Desktop should be offered. * * Resolves the curated preferred list via {@link selectPreferredClaudeModels}, - * then collapses the opus family to a single entry: the first (highest-priority) - * opus that resolved. With opus ids ordered `4-8 → 4-7 → 4-6` in - * {@link PREFERRED_CLAUDE_MODELS}, this exposes Opus 4.8 when the gateway serves - * it and otherwise falls back to the next-best available opus. Non-opus models - * are passed through untouched and order is preserved. + * then collapses the opus and sonnet families to a single entry each: the first + * (highest-priority) ID that resolved. With preferred order + * `opus: 5 → 4-8 → 4-7 → 4-6 → 3` and `sonnet: 5 → 4-6` in + * {@link PREFERRED_CLAUDE_MODELS}, this exposes Opus 5 / Sonnet 5 when the + * gateway serves them and otherwise falls back to the next-best available ID + * in each family. Haiku and any other non-opus/non-sonnet models are passed + * through untouched; order is preserved. */ export function selectDesktopClaudeModels( available: string[], @@ -237,10 +243,18 @@ export function selectDesktopClaudeModels( ): string[] { const resolved = selectPreferredClaudeModels(available, preferred); let opusKept = false; + let sonnetKept = false; return resolved.filter((id) => { - if (!/^claude-opus-/i.test(id)) return true; - if (opusKept) return false; - opusKept = true; + if (/^claude-opus-/i.test(id)) { + if (opusKept) return false; + opusKept = true; + return true; + } + if (/^claude-sonnet-/i.test(id)) { + if (sonnetKept) return false; + sonnetKept = true; + return true; + } return true; }); } From bae3c329776a43817995e4edd2bb8755391fc1f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6khan=20=C3=96zdemir?= Date: Thu, 10 Sep 2026 03:33:43 +0300 Subject: [PATCH 2/2] fix: update tests for failing windows tests --- tests/integration/analytics-auth-gate.test.ts | 61 ++++++++++++++----- 1 file changed, 46 insertions(+), 15 deletions(-) diff --git a/tests/integration/analytics-auth-gate.test.ts b/tests/integration/analytics-auth-gate.test.ts index 293093009..e4143d54f 100644 --- a/tests/integration/analytics-auth-gate.test.ts +++ b/tests/integration/analytics-auth-gate.test.ts @@ -30,6 +30,8 @@ const BIN = join(REPO_ROOT, 'bin', 'codemie.js'); const BLOCK_HEADER = 'CodeMie analytics authentication is invalid — session metrics are NOT being uploaded.'; +/** Hard cap per hook invocation; must stay well under vitest's 30s testTimeout. */ +const HOOK_TIMEOUT_MS = 20_000; interface HookResult { code: number | null; @@ -46,7 +48,12 @@ describe('analytics auth gate (codemie hook UserPromptSubmit)', () => { afterEach(() => { try { - rmSync(home, { recursive: true, force: true }); + rmSync(home, { + recursive: true, + force: true, + maxRetries: 5, + retryDelay: 100, + }); } catch { /* ignore cleanup errors */ } @@ -65,8 +72,11 @@ describe('analytics auth gate (codemie hook UserPromptSubmit)', () => { /** * Drive `codemie hook` with a UserPromptSubmit event on stdin. - * `extraEnv` supplies the analytics-relevant configuration; the child gets a - * deliberately minimal environment so nothing leaks from the test runner. + * + * The child inherits the runner environment (required on Windows — a stripped + * env without SystemRoot/COMSPEC/TEMP makes Node children hang or fail), + * but every CODEMIE_* var is stripped first, then the isolated temp home and + * the case-specific config are applied. */ function runHook(extraEnv: Record): HookResult { const transcript = join(home, 'transcript.jsonl'); @@ -77,21 +87,42 @@ describe('analytics auth gate (codemie hook UserPromptSubmit)', () => { transcript_path: transcript, }; - const result = spawnSync('node', [BIN, 'hook'], { + const env: NodeJS.ProcessEnv = { ...process.env }; + // Remove any analytics config leaking in from the runner. + for (const key of Object.keys(env)) { + if (key.startsWith("CODEMIE_")) delete env[key]; + } + // Isolated home for both POSIX and Windows homedir resolution. + env.HOME = home; + env.USERPROFILE = home; + env.APPDATA = join(home, "AppData", "Roaming"); + env.LOCALAPPDATA = join(home, "AppData", "Local"); + env.DO_NOT_TRACK = "1"; + // Baseline hook config, then the case-specific overrides. + env.CODEMIE_HOME = home; + env.CODEMIE_AGENT = "claude"; + env.CODEMIE_SESSION_ID = "codemie-session-under-test"; + env.CODEMIE_SKIP_UPDATE_CHECK = "true"; + Object.assign(env, extraEnv); + + const result = spawnSync(process.execPath, [BIN, "hook"], { input: JSON.stringify(event), - encoding: 'utf-8', + encoding: "utf-8", cwd: REPO_ROOT, - env: { - PATH: process.env.PATH, - HOME: process.env.HOME, - CODEMIE_HOME: home, - CODEMIE_AGENT: 'claude', - CODEMIE_SESSION_ID: 'codemie-session-under-test', - CODEMIE_SKIP_UPDATE_CHECK: 'true', - ...extraEnv, - }, + env, + timeout: HOOK_TIMEOUT_MS, + killSignal: "SIGKILL", + windowsHide: true, }); + if (result.error) { + throw new Error( + `codemie hook failed to run within ${HOOK_TIMEOUT_MS}ms` + + (result.signal ? ` (killed via ${result.signal})` : "") + + `\nstderr: ${result.stderr ?? ""}`, + ); + } + return { code: result.status, stderr: result.stderr ?? '', @@ -173,4 +204,4 @@ describe('analytics auth gate (codemie hook UserPromptSubmit)', () => { expect(res.code).toBe(0); expect(res.stderr).not.toContain(BLOCK_HEADER); }); -}); +}); \ No newline at end of file