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:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user