diff --git a/frontend/e2e/mocks/cassettes/settings-providers.ts b/frontend/e2e/mocks/cassettes/settings-providers.ts index 7ca89a22..25b41674 100644 --- a/frontend/e2e/mocks/cassettes/settings-providers.ts +++ b/frontend/e2e/mocks/cassettes/settings-providers.ts @@ -16,14 +16,14 @@ import { baseQueries, baseRest } from './base.ts'; const T = '2026-01-15T09:00:00Z'; -const agentConfig = (): AgentConfigFragmentFragment => +const agentConfig = (model = 'e2e-model'): AgentConfigFragmentFragment => entity('AgentConfig', { extraBody: null, frequencyPenalty: null, maxLength: null, maxTokens: null, minLength: null, - model: 'e2e-model', + model, presencePenalty: null, price: null, reasoning: null, @@ -33,29 +33,35 @@ const agentConfig = (): AgentConfigFragmentFragment => topP: null, }); -const agentsConfig = (): AgentsConfigFragmentFragment => +const agentsConfig = (model?: string): AgentsConfigFragmentFragment => entity('AgentsConfig', { - adviser: agentConfig(), - assistant: agentConfig(), - coder: agentConfig(), - enricher: agentConfig(), - generator: agentConfig(), - installer: agentConfig(), - pentester: agentConfig(), - primaryAgent: agentConfig(), - refiner: agentConfig(), - reflector: agentConfig(), - searcher: agentConfig(), - simple: agentConfig(), - simpleJson: agentConfig(), + adviser: agentConfig(model), + assistant: agentConfig(model), + coder: agentConfig(model), + enricher: agentConfig(model), + generator: agentConfig(model), + installer: agentConfig(model), + pentester: agentConfig(model), + primaryAgent: agentConfig(model), + refiner: agentConfig(model), + reflector: agentConfig(model), + searcher: agentConfig(model), + simple: agentConfig(model), + simpleJson: agentConfig(model), }); -const defaultConfig = (id: string, type: ProviderType): ProviderConfigFragmentFragment => +/** + * Defaults are not database rows: the resolver builds them without an ID, so the wire carries the + * bare number `0` for every one. `ProviderConfig.keyFields`'s id-0 branch exists for that shape, and + * a distinct string id here leaves it unexercised. + */ +const defaultConfig = (type: ProviderType): ProviderConfigFragmentFragment => entity('ProviderConfig', { - agents: agentsConfig(), + // Distinct per type, so two defaults collapsing onto one cache entry is observable. + agents: agentsConfig(`e2e-${type}-model`), createdAt: T, - id, - name: `default-${type}`, + id: 0 as unknown as string, + name: type, type, updatedAt: T, }); @@ -63,7 +69,7 @@ const defaultConfig = (id: string, type: ProviderType): ProviderConfigFragmentFr const noProviders: ResultOf = { settingsProviders: entity('ProvidersConfig', { default: entity('DefaultProvidersConfig', { - anthropic: defaultConfig('default-anthropic', ProviderType.Anthropic), + anthropic: defaultConfig(ProviderType.Anthropic), bedrock: null, custom: null, deepseek: null, @@ -72,7 +78,7 @@ const noProviders: ResultOf = { kimi: null, minimax: null, ollama: null, - openai: defaultConfig('default-openai', ProviderType.Openai), + openai: defaultConfig(ProviderType.Openai), qwen: null, }), enabled: entity('ProvidersReadinessStatus', { @@ -120,7 +126,7 @@ export const settingsProvidersCassette = (override: Cassette = {}): Cassette => const populatedProviders: ResultOf = { settingsProviders: entity('ProvidersConfig', { default: entity('DefaultProvidersConfig', { - anthropic: defaultConfig('default-anthropic', ProviderType.Anthropic), + anthropic: defaultConfig(ProviderType.Anthropic), bedrock: null, custom: null, deepseek: null, @@ -129,7 +135,7 @@ const populatedProviders: ResultOf = { kimi: null, minimax: null, ollama: null, - openai: defaultConfig('default-openai', ProviderType.Openai), + openai: defaultConfig(ProviderType.Openai), qwen: null, }), enabled: entity('ProvidersReadinessStatus', { diff --git a/frontend/src/lib/apollo.test.ts b/frontend/src/lib/apollo.test.ts index 4e320c17..965b9e8d 100644 --- a/frontend/src/lib/apollo.test.ts +++ b/frontend/src/lib/apollo.test.ts @@ -284,3 +284,52 @@ describe('streaming assistant-log link (createStreamingLink)', () => { expect(emitted).toEqual([{ somethingElse: { id: '1' } }]); }); }); + +describe('ProviderConfig cache identity', () => { + const PROVIDERS = gql` + query P { + settingsProviders { + default { + anthropic { + id + name + type + } + openai { + id + name + type + } + } + } + } + `; + + // The resolver builds default providers without an ID, so every one of them arrives as id 0. + // Normalising on that shared id would collapse them onto a single cache entry and the last + // write would win — the create form then seeds one provider type from another's defaults. + it('keeps two defaults apart even though both carry id 0', () => { + const cache = createCache(); + + cache.writeQuery({ + data: { + settingsProviders: { + __typename: 'ProvidersConfig', + default: { + __typename: 'DefaultProvidersConfig', + anthropic: { __typename: 'ProviderConfig', id: 0, name: 'anthropic', type: 'anthropic' }, + openai: { __typename: 'ProviderConfig', id: 0, name: 'openai', type: 'openai' }, + }, + }, + }, + query: PROVIDERS, + }); + + const read = cache.readQuery<{ + settingsProviders: { default: { anthropic: { name: string }; openai: { name: string } } }; + }>({ query: PROVIDERS }); + + expect(read?.settingsProviders.default.anthropic.name).toBe('anthropic'); + expect(read?.settingsProviders.default.openai.name).toBe('openai'); + }); +});