From 7d9275954d96988a750c8928b21947a38b5acd98 Mon Sep 17 00:00:00 2001 From: Debug Agent Date: Tue, 16 Jun 2026 17:22:03 +0200 Subject: [PATCH] test(acp): cover provider credential descriptors + re-sync set_session_model mirror MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Part 2 of OpenHands/software-agent-sdk#3722 ([AgentProfile][ts-client]), the "ACP provider descriptor extension". The canvas ACP authentication section (sdk#3728) renders provider-specific credential forms from ACP_PROVIDERS' descriptor fields (api_key_env_var, base_url_env_var, file_secrets) instead of hardcoding provider lists. Those fields already exist on ACPProviderInfo and are populated for all three providers (landed with the registry mirror in #173/#187/#205), so the substantive ask is already met. This adds the missing unit test that locks the credential metadata in for each built-in provider. Also re-sync the one drifted field — claude-code supports_set_session_model (false -> true) — to match SDK main, which flipped it (#3654) after claude-agent-acp 0.30.0 was found to ignore session-_meta selection. The validate-acp-providers drift check was red on main until this. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/__tests__/acp-providers.test.ts | 96 +++++++++++++++++++++++++++++ src/models/acp-providers.json | 2 +- 2 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 src/__tests__/acp-providers.test.ts diff --git a/src/__tests__/acp-providers.test.ts b/src/__tests__/acp-providers.test.ts new file mode 100644 index 0000000..39f3b5f --- /dev/null +++ b/src/__tests__/acp-providers.test.ts @@ -0,0 +1,96 @@ +import { ACP_PROVIDERS, getAcpProvider } from '../index'; +import type { ACPProviderKey } from '../index'; + +/** + * The canvas ACP authentication section (#3728) renders provider-specific + * credential forms by reading the descriptor fields off {@link ACP_PROVIDERS} + * rather than hardcoding provider lists. These tests lock in that every + * built-in provider exposes the credential metadata that UI relies on: + * `api_key_env_var`, `base_url_env_var`, and `file_secrets`. + */ +describe('ACP provider credential descriptors', () => { + const KEYS: ACPProviderKey[] = ['claude-code', 'codex', 'gemini-cli']; + + it('exposes an entry for each built-in provider', () => { + for (const key of KEYS) { + expect(ACP_PROVIDERS[key]).toBeDefined(); + expect(ACP_PROVIDERS[key].key).toBe(key); + } + }); + + it('defines the credential descriptor fields on every provider', () => { + for (const key of KEYS) { + const provider = ACP_PROVIDERS[key]; + // api_key_env_var / base_url_env_var are `string | null` — present (not + // undefined) on every provider so callers can branch on the value. + expect(provider).toHaveProperty('api_key_env_var'); + expect(provider).toHaveProperty('base_url_env_var'); + expect(Array.isArray(provider.file_secrets)).toBe(true); + } + }); + + it.each<[ACPProviderKey, string, string]>([ + ['claude-code', 'ANTHROPIC_API_KEY', 'ANTHROPIC_BASE_URL'], + ['codex', 'OPENAI_API_KEY', 'OPENAI_BASE_URL'], + ['gemini-cli', 'GEMINI_API_KEY', 'GEMINI_BASE_URL'], + ])('%s declares its api_key/base_url env vars', (key, apiKeyEnvVar, baseUrlEnvVar) => { + const provider = ACP_PROVIDERS[key]; + expect(provider.api_key_env_var).toBe(apiKeyEnvVar); + expect(provider.base_url_env_var).toBe(baseUrlEnvVar); + }); + + describe('file_secrets', () => { + it('claude-code authenticates via env var only (no file secrets)', () => { + expect(ACP_PROVIDERS['claude-code'].file_secrets).toEqual([]); + }); + + it('codex declares its ChatGPT auth.json file secret', () => { + const fileSecrets = ACP_PROVIDERS['codex'].file_secrets; + expect(fileSecrets).toHaveLength(1); + expect(fileSecrets[0]).toMatchObject({ + secret_name: 'CODEX_AUTH_JSON', + filename: 'auth.json', + env_var: 'CODEX_HOME', + }); + }); + + it('gemini-cli declares its Vertex SA credential file secret', () => { + const fileSecrets = ACP_PROVIDERS['gemini-cli'].file_secrets; + expect(fileSecrets).toHaveLength(1); + expect(fileSecrets[0]).toMatchObject({ + secret_name: 'GOOGLE_APPLICATION_CREDENTIALS_JSON', + filename: 'gcloud-credentials.json', + env_var: 'GOOGLE_APPLICATION_CREDENTIALS', + }); + }); + + it('every file secret carries the descriptor fields the canvas renders from', () => { + for (const key of KEYS) { + for (const spec of ACP_PROVIDERS[key].file_secrets) { + expect(typeof spec.secret_name).toBe('string'); + expect(spec.secret_name.length).toBeGreaterThan(0); + expect(typeof spec.filename).toBe('string'); + expect(spec.filename.length).toBeGreaterThan(0); + expect(typeof spec.env_var).toBe('string'); + expect(spec.env_var.length).toBeGreaterThan(0); + } + } + }); + }); + + describe('getAcpProvider', () => { + it('returns the descriptor (with credential fields) for a known key', () => { + const provider = getAcpProvider('codex'); + expect(provider).not.toBeNull(); + expect(provider?.api_key_env_var).toBe('OPENAI_API_KEY'); + expect(provider?.base_url_env_var).toBe('OPENAI_BASE_URL'); + }); + + it('returns null for the custom discriminator and unknown / falsy keys', () => { + expect(getAcpProvider('custom')).toBeNull(); + expect(getAcpProvider('nonexistent')).toBeNull(); + expect(getAcpProvider(null)).toBeNull(); + expect(getAcpProvider(undefined)).toBeNull(); + }); + }); +}); diff --git a/src/models/acp-providers.json b/src/models/acp-providers.json index eb7953a..6b2a56f 100644 --- a/src/models/acp-providers.json +++ b/src/models/acp-providers.json @@ -7,7 +7,7 @@ "base_url_env_var": "ANTHROPIC_BASE_URL", "default_session_mode": "bypassPermissions", "agent_name_patterns": ["claude-agent"], - "supports_set_session_model": false, + "supports_set_session_model": true, "session_meta_key": "claudeCode", "available_models": [ {