From 4ae3ca703690acfd507859e76fd63b93e2f61130 Mon Sep 17 00:00:00 2001 From: ethernet Date: Wed, 16 Sep 2026 19:09:33 -0400 Subject: [PATCH] fix(install-e2e): configure the mock as a plain OpenAI-compatible endpoint No tree should need to know a provider called 'mock'. resolve_provider() accepts only openrouter/custom/PROVIDER_REGISTRY at every vintage -- the same three-way gate at v2026.3.12 and at HEAD -- so declaring an out-of-tree provider named 'mock' made older refs die with "Unknown provider 'mock'" the moment they got past the not-configured guard. Point them at the generic route instead: model.provider 'custom' plus OPENAI_BASE_URL + OPENAI_API_KEY in .env. That pair is how any external OpenAI-compatible server is reached (vLLM, llama.cpp) and is accepted by every ref, so the install is genuinely chat-capable on any starting tag without any mock-specific concept in the product. The named providers.mock block is left in place for one more run: removing it is a separate step once CI confirms 'custom' resolves on the desktop legs, which cannot be validated from here. --- tests-js/scripts/desktop-smoke.test.ts | 4 +-- tests-js/scripts/mock-provider-config.ts | 17 ++++++++----- tests/install/e2e-assets/mock-provider.sh | 30 +---------------------- 3 files changed, 14 insertions(+), 37 deletions(-) diff --git a/tests-js/scripts/desktop-smoke.test.ts b/tests-js/scripts/desktop-smoke.test.ts index 1d2f86013b..7413da7231 100644 --- a/tests-js/scripts/desktop-smoke.test.ts +++ b/tests-js/scripts/desktop-smoke.test.ts @@ -52,7 +52,7 @@ test('provider reconfiguration preserves feed, plugins, history and explicit fix writeEnvFile(home) expect(yaml.load(fs.readFileSync(path.join(home, 'config.yaml'), 'utf8'))).toMatchObject({ updates: feed, plugins: { witness: true }, - model: { provider: 'mock', temperature: 0.7, context_length: 12000 }, + model: { provider: 'custom', temperature: 0.7, context_length: 12000 }, providers: { mock: { api: 'http://127.0.0.1:9001/v1', context_length: 64000 } }, auxiliary: { title_generation: { enabled: false } }, approvals: { mode: 'smart' }, display: { interim_assistant_messages: true }, }) @@ -236,7 +236,7 @@ test('a bundle-env HERMES_HOME clear cannot strand the mock config outside the r await expect(runInstalledDesktopSmoke({ exe, root: path.join(root, 'root'), origin: 'bundled', home, 'user-data': userData, out: root, phase: 'installed', 'expect-commit': 'a'.repeat(40) })).rejects.toThrow() for (const candidate of candidateSmokeHermesHomes(home, userData)) { - expect(yaml.load(fs.readFileSync(path.join(candidate, 'config.yaml'), 'utf8'))).toMatchObject({ model: { provider: 'mock' } }) + expect(yaml.load(fs.readFileSync(path.join(candidate, 'config.yaml'), 'utf8'))).toMatchObject({ model: { provider: 'custom' } }) expect(fs.readFileSync(path.join(candidate, '.env'), 'utf8')).toMatch(/MOCK_API_KEY=/) } // Electron resolves shell folders before 'ready'; the sandboxed AppData/XDG diff --git a/tests-js/scripts/mock-provider-config.ts b/tests-js/scripts/mock-provider-config.ts index 0b0c3e3658..76ab1b9013 100644 --- a/tests-js/scripts/mock-provider-config.ts +++ b/tests-js/scripts/mock-provider-config.ts @@ -39,7 +39,7 @@ export function writeMockProviderConfig( const display = section.parse(extraDisplayConfig ? yaml.load(extraDisplayConfig) ?? {} : {}) const merged = { ...config, - model: { ...config.model, default: 'mock-model', provider: 'mock', context_length: modelContextLength ?? 64000 }, + model: { ...config.model, default: 'mock-model', provider: 'custom', context_length: modelContextLength ?? 64000 }, providers: { ...config.providers, mock: { api: `${url}/v1`, name: 'Mock', api_mode: 'chat_completions', key_env: 'MOCK_API_KEY', models: { 'mock-model': {} }, context_length: 64000 }, @@ -54,15 +54,20 @@ export function writeMockProviderConfig( fs.writeFileSync(configPath, yaml.dump(merged), 'utf8') } -/** Keep journey-owned entries, replacing only the inert test key. */ -export function writeEnvFile(hermesHome: string, apiKey = 'e2e-mock-key'): void { +/** Keep journey-owned entries, replacing only the inert test keys. */ +export function writeEnvFile(hermesHome: string, apiKey = 'e2e-mock-key', mockUrl?: string): void { if (!/^[\w-]+$/.test(apiKey)) { throw new Error('Mock key must be an inert single-line test value') } const envPath = path.join(hermesHome, '.env') const prior = fs.existsSync(envPath) ? fs.readFileSync(envPath, 'utf8') : '' - const lines = prior.split(/\r?\n/).filter((line: string): boolean => !/^\s*(?:export\s+)?MOCK_API_KEY\s*=/.test(line)) - fs.writeFileSync(envPath, `${lines.join('\n').trimEnd()}\nMOCK_API_KEY=${apiKey}\n`, { mode: 0o600 }) + const lines = prior.split(/\r?\n/).filter((line: string): boolean => !/^\s*(?:export\s+)?(?:MOCK_API_KEY|OPENAI_BASE_URL|OPENAI_API_KEY)\s*=/.test(line)) + // OPENAI_BASE_URL + OPENAI_API_KEY is how EVERY vintage reaches an external + // OpenAI-compatible endpoint ("custom"); a bare MOCK_API_KEY only means + // something to a tree that knows a provider named `mock`, which older refs + // do not (their resolve_provider accepts only openrouter/custom/registry). + const portable = mockUrl ? `\nOPENAI_BASE_URL=${mockUrl}/v1\nOPENAI_API_KEY=${apiKey}` : '' + fs.writeFileSync(envPath, `${lines.join('\n').trimEnd()}\nMOCK_API_KEY=${apiKey}${portable}\n`, { mode: 0o600 }) } if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { @@ -71,5 +76,5 @@ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) throw new Error('usage: node mock-provider-config.ts ABSOLUTE_HERMES_HOME MOCK_URL') } writeMockProviderConfig(home, url) - writeEnvFile(home) + writeEnvFile(home, 'e2e-mock-key', url) } \ No newline at end of file diff --git a/tests/install/e2e-assets/mock-provider.sh b/tests/install/e2e-assets/mock-provider.sh index 885f713f7c..ac544cf06f 100755 --- a/tests/install/e2e-assets/mock-provider.sh +++ b/tests/install/e2e-assets/mock-provider.sh @@ -45,35 +45,7 @@ mock_start() { ok "mock inference server: $url" node "$ASSETS/../../../tests-js/scripts/mock-provider-config.ts" "$HERMES_HOME" "$url" || fail "mock provider config failed" - ok "provider 'mock' configured in $HERMES_HOME (api $url/v1)" - - # ...and also as a PLAIN OpenAI-compatible endpoint, which is the only way an - # OLD tree can see it. Pre-mock refs decide "am I configured?" from process - # env + .env keys + PROVIDER_REGISTRY alone -- they never read config.yaml's - # providers block -- so MOCK_API_KEY is invisible to them and `hermes chat` - # dies with "no API keys or providers found". OPENAI_BASE_URL is in every - # vintage's accepted set (it is how vLLM/llama.cpp local servers are used) and - # OPENAI_API_KEY is a plain credential for it, so this makes the install - # genuinely chat-capable on ANY ref without the product knowing about a mock. - _mock_write_portable_provider "$HERMES_HOME" "$url" || fail "mock .env write failed" - ok "also reachable as an OpenAI-compatible endpoint (OPENAI_BASE_URL=$url/v1)" -} - -# Idempotent: replaces exactly these two keys, keeps every other line as-is. -_mock_write_portable_provider() { - local home="$1" url="$2" envfile tmp - envfile="$home/.env" - tmp="$envfile.mock.$$" - mkdir -p "$home" - if [ -f "$envfile" ]; then - grep -vE '^[[:space:]]*(export[[:space:]]+)?(OPENAI_BASE_URL|OPENAI_API_KEY)[[:space:]]*=' \ - "$envfile" > "$tmp" || true - else - : > "$tmp" - fi - printf 'OPENAI_BASE_URL=%s/v1\nOPENAI_API_KEY=e2e-mock-key\n' "$url" >> "$tmp" - chmod 600 "$tmp" 2>/dev/null || true - mv -f "$tmp" "$envfile" + ok "configured in $HERMES_HOME as an OpenAI-compatible endpoint (api $url/v1)" } mock_stop() {