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.
This commit is contained in:
ethernet
2026-09-16 19:09:33 -04:00
parent 5e4c6ea0ae
commit 4ae3ca7036
3 changed files with 14 additions and 37 deletions

View File

@@ -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

View File

@@ -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)
}

View File

@@ -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() {