diff --git a/frontend/e2e/mocks/cassettes/settings-providers.ts b/frontend/e2e/mocks/cassettes/settings-providers.ts index 25b41674..34aea2c7 100644 --- a/frontend/e2e/mocks/cassettes/settings-providers.ts +++ b/frontend/e2e/mocks/cassettes/settings-providers.ts @@ -3,6 +3,7 @@ import type { ResultOf } from '@graphql-typed-document-node/core'; import type { AgentConfigFragmentFragment, AgentsConfigFragmentFragment, + ModelConfigFragmentFragment, ProviderConfigFragmentFragment, SettingsProvidersDocument, } from '@/graphql/types'; @@ -66,21 +67,48 @@ const defaultConfig = (type: ProviderType): ProviderConfigFragmentFragment => updatedAt: T, }); +const modelConfig = (name: string): ModelConfigFragmentFragment => + entity('ModelConfig', { name, price: null, reasoning: null, thinking: null }); + +// The backend fills a default config AND a model catalog for EVERY type, keyed or not (verified live: +// gemini/deepseek/ollama all return a non-null default and a populated catalog with zero keys). A +// cassette leaving them null/[] encodes a wire state the resolver never emits and leaves the +// create-form's model-dropdown seeding path structurally unreachable. +const allDefaults = () => + entity('DefaultProvidersConfig', { + anthropic: defaultConfig(ProviderType.Anthropic), + bedrock: defaultConfig(ProviderType.Bedrock), + custom: defaultConfig(ProviderType.Custom), + deepseek: defaultConfig(ProviderType.Deepseek), + gemini: defaultConfig(ProviderType.Gemini), + glm: defaultConfig(ProviderType.Glm), + kimi: defaultConfig(ProviderType.Kimi), + minimax: defaultConfig(ProviderType.Minimax), + ollama: defaultConfig(ProviderType.Ollama), + openai: defaultConfig(ProviderType.Openai), + qwen: defaultConfig(ProviderType.Qwen), + }); + +const catalog = (type: ProviderType) => [modelConfig(`e2e-${type}-model`), modelConfig(`e2e-${type}-model-mini`)]; + +const allModels = () => + entity('ProvidersModelsList', { + anthropic: catalog(ProviderType.Anthropic), + bedrock: catalog(ProviderType.Bedrock), + custom: catalog(ProviderType.Custom), + deepseek: catalog(ProviderType.Deepseek), + gemini: catalog(ProviderType.Gemini), + glm: catalog(ProviderType.Glm), + kimi: catalog(ProviderType.Kimi), + minimax: catalog(ProviderType.Minimax), + ollama: catalog(ProviderType.Ollama), + openai: catalog(ProviderType.Openai), + qwen: catalog(ProviderType.Qwen), + }); + const noProviders: ResultOf = { settingsProviders: entity('ProvidersConfig', { - default: entity('DefaultProvidersConfig', { - anthropic: defaultConfig(ProviderType.Anthropic), - bedrock: null, - custom: null, - deepseek: null, - gemini: null, - glm: null, - kimi: null, - minimax: null, - ollama: null, - openai: defaultConfig(ProviderType.Openai), - qwen: null, - }), + default: allDefaults(), enabled: entity('ProvidersReadinessStatus', { anthropic: false, bedrock: false, @@ -94,19 +122,7 @@ const noProviders: ResultOf = { openai: false, qwen: false, }), - models: entity('ProvidersModelsList', { - anthropic: [], - bedrock: [], - custom: [], - deepseek: [], - gemini: [], - glm: [], - kimi: [], - minimax: [], - ollama: [], - openai: [], - qwen: [], - }), + models: allModels(), userDefined: [], }), }; @@ -125,19 +141,7 @@ export const settingsProvidersCassette = (override: Cassette = {}): Cassette => const populatedProviders: ResultOf = { settingsProviders: entity('ProvidersConfig', { - default: entity('DefaultProvidersConfig', { - anthropic: defaultConfig(ProviderType.Anthropic), - bedrock: null, - custom: null, - deepseek: null, - gemini: null, - glm: null, - kimi: null, - minimax: null, - ollama: null, - openai: defaultConfig(ProviderType.Openai), - qwen: null, - }), + default: allDefaults(), enabled: entity('ProvidersReadinessStatus', { anthropic: true, bedrock: false, @@ -151,19 +155,7 @@ const populatedProviders: ResultOf = { openai: true, qwen: false, }), - models: entity('ProvidersModelsList', { - anthropic: [], - bedrock: [], - custom: [], - deepseek: [], - gemini: [], - glm: [], - kimi: [], - minimax: [], - ollama: [], - openai: [], - qwen: [], - }), + models: allModels(), userDefined: [ entity('ProviderConfig', { agents: agentsConfig(), diff --git a/frontend/e2e/specs/settings/providers.spec.ts b/frontend/e2e/specs/settings/providers.spec.ts index 876b770f..98760ea2 100644 --- a/frontend/e2e/specs/settings/providers.spec.ts +++ b/frontend/e2e/specs/settings/providers.spec.ts @@ -1,6 +1,9 @@ import { expect, test } from '../../fixtures/test.ts'; import { expectCleanPage } from '../../helpers/errors.ts'; -import { settingsProvidersCassette } from '../../mocks/cassettes/settings-providers.ts'; +import { + populatedSettingsProvidersCassette, + settingsProvidersCassette, +} from '../../mocks/cassettes/settings-providers.ts'; test.describe('settings providers', { tag: '@coverage' }, () => { test.use({ cassette: settingsProvidersCassette() }); @@ -27,3 +30,26 @@ test.describe('settings providers', { tag: '@coverage' }, () => { expectCleanPage(pageErrorLog); }); }); + +test.describe('settings providers create form', { tag: '@coverage' }, () => { + // Needs a cassette where the type is enabled: the create form bounces `?type=` for a disabled + // one, so the empty seed would just redirect to the list. + test.use({ cassette: populatedSettingsProvidersCassette() }); + + // A selected type seeds each agent's model from that type's catalog. The catalog must carry the + // shape the backend actually emits (a non-empty model list for every type, keyed or not); an + // empty catalog makes this whole path unreachable and any assertion on it vacuous. `?type=` is + // the URL the type picker produces, driven directly to avoid the dirty-form navigation blocker. + test('seeds the agent model from the selected type catalog', async ({ page, pageErrorLog }) => { + await page.goto('/settings/providers/new?type=anthropic'); + + await page.getByRole('button', { name: /^Adviser/ }).click(); + await page.getByRole('button', { name: 'Open model list' }).first().click(); + + // A catalog-only model (not the seeded agent default), so it can only appear if the type's + // model list reached the dropdown — the path an empty catalog leaves unreachable. + await expect(page.getByRole('option', { name: 'e2e-anthropic-model-mini' })).toBeVisible(); + + expectCleanPage(pageErrorLog); + }); +});