fix(install-e2e): pin the mock provider's endpoint in config.yaml

The desktop smoke configured a provider as `model.provider: custom` plus
OPENAI_BASE_URL/OPENAI_API_KEY in .env, with deliberately no provider entry in
config.yaml. Every `installer-script+desktop` leg then died at its OLD
checkpoint: the v2026.8.31 app boots behind the onboarding overlay ("No usable
credentials found for custom. setup.status reports configured credentials, but
runtime resolution still failed"), so the smoke's trial click on the composer
never landed (225 retries against the covering div).

That vintage resolves a bare `custom` only from config.yaml
`custom_providers:` (name + base_url + key_env, with a bare-"custom" fallback to
the first valid entry); the env pair stays for the trees that resolve the
endpoint from the environment. Write both, replacing the entry this writer owns
rather than stacking duplicates.
This commit is contained in:
ethernet
2026-09-17 14:36:43 -04:00
parent b4a294fff9
commit f58d34cf5e
2 changed files with 21 additions and 5 deletions

View File

@@ -54,6 +54,10 @@ test('provider reconfiguration preserves feed, plugins, history and explicit fix
updates: feed, plugins: { witness: true },
model: { provider: 'custom', temperature: 0.7, context_length: 12000 },
providers: {},
// Exactly one entry, repointed at the second URL: a bare `custom` resolves
// from this list on every vintage, and reconfiguring must not stack
// duplicates.
custom_providers: [{ name: 'Mock', base_url: 'http://127.0.0.1:9001/v1', key_env: 'OPENAI_API_KEY' }],
auxiliary: { title_generation: { enabled: false } }, approvals: { mode: 'smart' }, display: { interim_assistant_messages: true },
})
expect(fs.readFileSync(path.join(home, '.env'), 'utf8')).toBe('OTHER_TEST_VALUE=kept\nMOCK_API_KEY=e2e-mock-key\n')

View File

@@ -9,11 +9,16 @@ const section = z.object({}).passthrough()
const configSchema = z.object({
model: section.optional(),
providers: section.optional(),
custom_providers: z.array(section).optional(),
auxiliary: z.object({ title_generation: section.optional() }).passthrough().optional(),
approvals: section.optional(),
display: section.optional(),
}).passthrough()
//: Entry name this writer owns in ``custom_providers``; reconfiguring replaces
//: it rather than stacking duplicates.
const MOCK_PROVIDER_NAME = 'Mock'
export function validateMockUrl(value: string): string {
const url = new URL(value)
if (url.protocol !== 'http:' || url.hostname !== '127.0.0.1' || !url.port
@@ -40,12 +45,19 @@ export function writeMockProviderConfig(
const merged = {
...config,
model: { ...config.model, default: 'mock-model', provider: 'custom', context_length: modelContextLength ?? 64000 },
// No named provider block: "an external OpenAI-compatible endpoint" is
// expressed by provider 'custom' + OPENAI_BASE_URL/OPENAI_API_KEY (written
// by writeEnvFile). A provider named 'mock' only resolved on trees that
// had registered such a profile, so older refs died with "Unknown
// provider 'mock'".
// Two shapes, one endpoint. `providers:` stays untouched (a provider named
// 'mock' only resolved on trees that had registered such a profile, so
// older refs died with "Unknown provider 'mock'"). A bare `custom` resolves
// from `custom_providers:` on every vintage -- v2026.8.31's desktop
// readiness check reports "No usable credentials found for custom ...
// runtime resolution still failed" without an entry here -- while the .env
// OPENAI_BASE_URL/OPENAI_API_KEY pair (writeEnvFile) is how trees that
// resolve the endpoint from the environment reach the same URL.
providers: { ...config.providers },
custom_providers: [
...(config.custom_providers ?? []).filter(entry => entry?.name !== MOCK_PROVIDER_NAME),
{ name: MOCK_PROVIDER_NAME, base_url: `${url}/v1`, key_env: 'OPENAI_API_KEY' },
],
auxiliary: { ...config.auxiliary, title_generation: { ...config.auxiliary?.title_generation, enabled: false } },
approvals: { ...config.approvals, mode: 'off' },
...extra,