Files
hermes-webui/tests/test_issue1362_codex_oauth_onboarding.py
T
nesquena-hermesandrodboev 0a32fb899f fix(onboarding): rehydrate plain-select fallback so saved model isn't reset to option[0]
Codex gate finding on #5031: when _mountSearchableModelSelect is absent the
fallback returns the old plain <select>, but the post-render rehydration
(modelSel.value = ONBOARDING.form.model) was removed — so a saved/default model
that isn't the first option would be silently replaced by option[0] on render.
Restore the rehydration in an else branch + add a regression test.

Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-06-27 04:33:02 +00:00

962 lines
32 KiB
Python

"""Regression tests for issue #1362 — Codex OAuth from onboarding."""
from __future__ import annotations
import json
import os
import shutil
import stat
import subprocess
import tempfile
import threading
import time
from pathlib import Path
import pytest
REPO = Path(__file__).resolve().parents[1]
UI_JS = REPO / "static" / "ui.js"
NODE = shutil.which("node")
def test_onboarding_codex_oauth_routes_use_post_start_cancel_and_get_poll():
routes = (REPO / "api" / "routes.py").read_text(encoding="utf-8")
get_idx = routes.find("def handle_get(")
post_idx = routes.find("def handle_post(")
assert get_idx != -1 and post_idx != -1
get_body = routes[get_idx:post_idx]
post_body = routes[post_idx:]
assert '"/api/onboarding/oauth/poll"' in get_body
assert '"/api/onboarding/oauth/start"' not in get_body
assert '"/api/oauth/codex/start"' not in routes
assert '"/api/oauth/codex/poll"' not in routes
assert '"/api/onboarding/oauth/start"' in post_body
assert '"/api/onboarding/oauth/cancel"' in post_body
def test_onboarding_oauth_rejects_unsupported_providers(monkeypatch):
import api.oauth as oauth
for provider in ("nous", "qwen-oauth", "copilot", "bogus"):
with pytest.raises(ValueError):
oauth.start_onboarding_oauth_flow({"provider": provider})
def test_start_payload_does_not_leak_provider_device_secrets(monkeypatch, tmp_path):
import api.oauth as oauth
oauth._OAUTH_FLOWS.clear()
monkeypatch.setattr(oauth, "_get_active_hermes_home", lambda: tmp_path)
monkeypatch.setattr(oauth, "_request_codex_user_code", lambda: {
"device_auth_id": "device-secret",
"user_code": "ABCD-EFGH",
"interval": 3,
})
monkeypatch.setattr(oauth, "_spawn_codex_oauth_worker", lambda flow_id: None)
payload = oauth.start_onboarding_oauth_flow({"provider": "openai-codex"})
assert payload["ok"] is True
assert payload["provider"] == "openai-codex"
assert payload["status"] == "pending"
assert payload["verification_uri"] == "https://auth.openai.com/codex/device"
assert payload["user_code"] == "ABCD-EFGH"
serialized = json.dumps(payload)
for forbidden in (
"device_auth_id",
"device-secret",
"authorization_code",
"code_verifier",
"access_token",
"refresh_token",
):
assert forbidden not in serialized
def test_poll_returns_high_level_status_only(monkeypatch, tmp_path):
import api.oauth as oauth
oauth._OAUTH_FLOWS.clear()
flow_id = "flow-test"
oauth._OAUTH_FLOWS[flow_id] = {
"provider": "openai-codex",
"status": "pending",
"device_auth_id": "device-secret",
"user_code": "ABCD-EFGH",
"code_verifier": "verifier-secret",
"authorization_code": "auth-secret",
"expires_at": time.time() + 60,
"poll_interval_seconds": 3,
"hermes_home": tmp_path,
}
payload = oauth.poll_onboarding_oauth_flow(flow_id)
assert payload == {"ok": True, "provider": "openai-codex", "flow_id": flow_id, "status": "pending"}
serialized = json.dumps(payload)
for forbidden in ("device_auth_id", "device-secret", "code_verifier", "authorization_code"):
assert forbidden not in serialized
def test_cancel_marks_flow_cancelled_and_poll_stops(tmp_path):
import api.oauth as oauth
oauth._OAUTH_FLOWS.clear()
flow_id = "flow-cancel"
oauth._OAUTH_FLOWS[flow_id] = {
"provider": "openai-codex",
"status": "pending",
"expires_at": time.time() + 60,
"hermes_home": tmp_path,
}
cancelled = oauth.cancel_onboarding_oauth_flow({"flow_id": flow_id})
polled = oauth.poll_onboarding_oauth_flow(flow_id)
assert cancelled["status"] == "cancelled"
assert polled["status"] == "cancelled"
def test_cancel_during_token_exchange_does_not_persist_credentials(monkeypatch, tmp_path):
"""Cancel arriving while the worker is mid-network-call must win.
Without the post-exchange status re-check, the worker would proceed to
persist credentials to auth.json AND override the cancelled status with
"success" — silently storing tokens the user explicitly aborted.
"""
import threading
import api.oauth as oauth
oauth._OAUTH_FLOWS.clear()
poll_started = threading.Event()
poll_continue = threading.Event()
def _slow_poll(device_auth_id, user_code):
poll_started.set()
assert poll_continue.wait(timeout=5)
return {"authorization_code": "auth-code", "code_verifier": "verifier"}
def _exchange(authorization_code, code_verifier):
return {"access_token": "ACCESS", "refresh_token": "REFRESH"}
monkeypatch.setattr(oauth, "_poll_codex_authorization", _slow_poll)
monkeypatch.setattr(oauth, "_exchange_codex_authorization", _exchange)
flow_id = "race-flow"
oauth._OAUTH_FLOWS[flow_id] = {
"provider": "openai-codex",
"status": "pending",
"device_auth_id": "device-secret",
"user_code": "ABCD-EFGH",
"expires_at": time.time() + 600,
"poll_interval_seconds": 1,
"hermes_home": str(tmp_path),
"created_at": time.time(),
"updated_at": time.time(),
}
worker = threading.Thread(target=oauth._run_codex_oauth_worker, args=(flow_id,), daemon=True)
worker.start()
assert poll_started.wait(timeout=5)
oauth.cancel_onboarding_oauth_flow({"flow_id": flow_id})
assert oauth._OAUTH_FLOWS[flow_id]["status"] == "cancelled"
poll_continue.set()
worker.join(timeout=5)
assert not worker.is_alive()
assert oauth._OAUTH_FLOWS[flow_id]["status"] == "cancelled"
assert not (tmp_path / "auth.json").exists()
def test_expired_flow_reports_expired_and_drops_sensitive_lifecycle(tmp_path):
import api.oauth as oauth
oauth._OAUTH_FLOWS.clear()
flow_id = "flow-expired"
oauth._OAUTH_FLOWS[flow_id] = {
"provider": "openai-codex",
"status": "pending",
"device_auth_id": "device-secret",
"expires_at": time.time() - 1,
"hermes_home": tmp_path,
}
payload = oauth.poll_onboarding_oauth_flow(flow_id)
assert payload["status"] == "expired"
assert oauth._OAUTH_FLOWS[flow_id]["status"] == "expired"
assert "device_auth_id" not in oauth._OAUTH_FLOWS[flow_id]
def test_codex_credentials_written_to_active_profile_auth_json(monkeypatch, tmp_path):
import api.oauth as oauth
from api.onboarding import _provider_oauth_authenticated
active_home = tmp_path / "active-profile"
realish_home = tmp_path / "process-home"
active_home.mkdir()
realish_home.mkdir()
monkeypatch.setattr(Path, "home", lambda: realish_home)
auth_path = oauth._persist_codex_credentials(
active_home,
{"access_token": "access-secret", "refresh_token": "refresh-secret"},
)
assert auth_path == active_home / "auth.json"
assert auth_path.exists()
assert not (realish_home / ".hermes" / "auth.json").exists()
mode = stat.S_IMODE(auth_path.stat().st_mode)
assert mode == 0o600
store = json.loads(auth_path.read_text(encoding="utf-8"))
entry = store["credential_pool"]["openai-codex"][0]
assert entry["auth_type"] == "oauth"
assert entry["source"] == "manual:device_code"
assert entry["base_url"] == "https://chatgpt.com/backend-api/codex"
assert _provider_oauth_authenticated("openai-codex", active_home) is True
def test_frontend_uses_onboarding_oauth_endpoints_and_no_secret_poll_url():
js = (REPO / "static" / "onboarding.js").read_text(encoding="utf-8")
assert "/api/onboarding/oauth/start" in js
assert "/api/onboarding/oauth/poll" in js
assert "/api/onboarding/oauth/cancel" in js
assert "window.open(verification_uri" not in js
assert "device_code=" not in js
assert "device_code" not in js
assert "flow_id" in js
assert "copyCodexOAuthCode" in js
assert "cancelCodexOAuth" in js
def test_unsupported_note_mentions_codex_and_claude_as_in_app():
src = (REPO / "api" / "onboarding.py").read_text(encoding="utf-8")
start = src.find("_UNSUPPORTED_PROVIDER_NOTE")
body = src[start:start + 500]
assert "OpenAI Codex, and GitHub" not in body
assert "OpenAI Codex" in body and "authenticated in this onboarding flow" in body
assert "Claude" in body or "Anthropic" in body
# ── Claude / Anthropic OAuth slice ─────────────────────────────────────────
def test_claude_provider_aliases_normalize_to_anthropic(monkeypatch, tmp_path):
import api.oauth as oauth
oauth._OAUTH_FLOWS.clear()
monkeypatch.setattr(oauth, "_get_active_hermes_home", lambda: tmp_path)
monkeypatch.setattr(oauth, "_read_claude_code_credentials", lambda: None)
monkeypatch.setattr(oauth, "_spawn_anthropic_credential_worker", lambda fid: None)
for alias in ("anthropic", "claude", "claude-code"):
payload = oauth.start_onboarding_oauth_flow({"provider": alias})
assert payload["ok"] is True
assert payload["provider"] == "anthropic"
assert payload["status"] == "pending"
def test_anthropic_immediate_success_when_credentials_exist(monkeypatch, tmp_path):
import api.oauth as oauth
oauth._OAUTH_FLOWS.clear()
monkeypatch.setattr(oauth, "_get_active_hermes_home", lambda: tmp_path)
monkeypatch.setattr(oauth, "_read_claude_code_credentials", lambda: {
"accessToken": "cc-access-secret",
"refreshToken": "cc-refresh-secret",
"expiresAt": 9999999999999,
})
linked = []
monkeypatch.setattr(oauth, "_link_anthropic_credentials", lambda hh: linked.append(str(hh)))
payload = oauth.start_onboarding_oauth_flow({"provider": "anthropic"})
assert payload["status"] == "success"
assert payload["provider"] == "anthropic"
assert linked == [str(tmp_path)]
serialized = json.dumps(payload)
for forbidden in ("cc-access-secret", "cc-refresh-secret", "accessToken", "refreshToken", "access_token", "refresh_token"):
assert forbidden not in serialized
def test_anthropic_pending_payload_is_action_only_and_secret_free(monkeypatch, tmp_path):
import api.oauth as oauth
oauth._OAUTH_FLOWS.clear()
monkeypatch.setattr(oauth, "_get_active_hermes_home", lambda: tmp_path)
monkeypatch.setattr(oauth, "_read_claude_code_credentials", lambda: None)
monkeypatch.setattr(oauth, "_spawn_anthropic_credential_worker", lambda fid: None)
payload = oauth.start_onboarding_oauth_flow({"provider": "anthropic"})
assert payload["status"] == "pending"
assert payload["provider"] == "anthropic"
assert payload["flow_id"]
assert "action_required" in payload
assert "claude" in payload["action_required"].lower()
serialized = json.dumps(payload)
for forbidden in (
"access_token", "refresh_token", "accessToken", "refreshToken",
".credentials.json", ".claude", "hermes_home", str(tmp_path),
"ANTHROPIC_API_KEY", "ANTHROPIC_TOKEN",
):
assert forbidden not in serialized
def test_anthropic_poll_and_cancel_return_high_level_status(tmp_path):
import api.oauth as oauth
oauth._OAUTH_FLOWS.clear()
flow_id = "claude-flow-test"
oauth._OAUTH_FLOWS[flow_id] = {
"provider": "anthropic",
"status": "pending",
"expires_at": time.time() + 60,
"poll_interval_seconds": 5,
"hermes_home": str(tmp_path),
}
assert oauth.poll_onboarding_oauth_flow(flow_id) == {
"ok": True,
"provider": "anthropic",
"flow_id": flow_id,
"status": "pending",
}
assert oauth.cancel_onboarding_oauth_flow({"flow_id": flow_id}) == {
"ok": True,
"provider": "anthropic",
"flow_id": flow_id,
"status": "cancelled",
}
def test_anthropic_worker_detects_credentials_and_cancel_wins(monkeypatch, tmp_path):
import threading
import api.oauth as oauth
oauth._OAUTH_FLOWS.clear()
started = threading.Event()
proceed = threading.Event()
linked = []
def _slow_read_creds():
started.set()
assert proceed.wait(timeout=5)
return {"accessToken": "cc-access-secret", "refreshToken": "cc-refresh-secret"}
monkeypatch.setattr(oauth, "_read_claude_code_credentials", _slow_read_creds)
monkeypatch.setattr(oauth, "_link_anthropic_credentials", lambda hh: linked.append(str(hh)))
flow_id = "claude-race-flow"
oauth._OAUTH_FLOWS[flow_id] = {
"provider": "anthropic",
"status": "pending",
"expires_at": time.time() + 600,
"poll_interval_seconds": 1,
"hermes_home": str(tmp_path),
"created_at": time.time(),
"updated_at": time.time(),
}
worker = threading.Thread(target=oauth._run_anthropic_credential_worker, args=(flow_id,), daemon=True)
worker.start()
assert started.wait(timeout=5)
oauth.cancel_onboarding_oauth_flow({"flow_id": flow_id})
proceed.set()
worker.join(timeout=5)
assert oauth._OAUTH_FLOWS[flow_id]["status"] == "cancelled"
assert not linked
def test_anthropic_cancel_during_link_keeps_flow_cancelled(monkeypatch, tmp_path):
import threading
import api.oauth as oauth
from api.onboarding import _provider_oauth_authenticated
oauth._OAUTH_FLOWS.clear()
link_started = threading.Event()
link_continue = threading.Event()
monkeypatch.setattr(oauth.time, "sleep", lambda _seconds: None)
monkeypatch.setattr(oauth, "_read_claude_code_credentials", lambda: {"accessToken": "cc-access-secret", "refreshToken": "cc-refresh-secret"})
def _slow_clear(_home):
link_started.set()
assert link_continue.wait(timeout=5)
monkeypatch.setattr(oauth, "_clear_anthropic_env_values", _slow_clear)
flow_id = "claude-link-cancel-race"
oauth._OAUTH_FLOWS[flow_id] = {
"provider": "anthropic",
"status": "pending",
"expires_at": time.time() + 60,
"poll_interval_seconds": 1,
"hermes_home": str(tmp_path),
"created_at": time.time(),
"updated_at": time.time(),
}
worker = threading.Thread(target=oauth._run_anthropic_credential_worker, args=(flow_id,), daemon=True)
worker.start()
assert link_started.wait(timeout=5)
assert oauth.cancel_onboarding_oauth_flow({"flow_id": flow_id})["status"] == "cancelled"
link_continue.set()
worker.join(timeout=5)
assert not worker.is_alive()
assert oauth._OAUTH_FLOWS[flow_id]["status"] == "cancelled"
assert _provider_oauth_authenticated("anthropic", tmp_path) is False
def test_anthropic_cancel_missing_flow_keeps_requested_provider():
import api.oauth as oauth
oauth._OAUTH_FLOWS.clear()
assert oauth.cancel_onboarding_oauth_flow({"flow_id": "missing", "provider": "claude-code"}) == {
"ok": True,
"provider": "anthropic",
"flow_id": "missing",
"status": "cancelled",
}
def test_anthropic_worker_expires_flow(tmp_path):
import api.oauth as oauth
oauth._OAUTH_FLOWS.clear()
flow_id = "claude-expired-worker-flow"
oauth._OAUTH_FLOWS[flow_id] = {
"provider": "anthropic",
"status": "pending",
"expires_at": time.time() - 1,
"poll_interval_seconds": 1,
"hermes_home": str(tmp_path),
"created_at": time.time(),
"updated_at": time.time(),
}
oauth._run_anthropic_credential_worker(flow_id)
assert oauth._OAUTH_FLOWS[flow_id]["status"] == "expired"
def test_anthropic_worker_reports_link_errors(monkeypatch, tmp_path):
import api.oauth as oauth
oauth._OAUTH_FLOWS.clear()
monkeypatch.setattr(oauth.time, "sleep", lambda _seconds: None)
monkeypatch.setattr(oauth, "_read_claude_code_credentials", lambda: {"accessToken": "cc-access-secret", "refreshToken": "cc-refresh-secret"})
def _raise_link_error(_home):
raise RuntimeError("link failed without secrets")
monkeypatch.setattr(oauth, "_link_anthropic_credentials", _raise_link_error)
flow_id = "claude-link-error-flow"
oauth._OAUTH_FLOWS[flow_id] = {
"provider": "anthropic",
"status": "pending",
"expires_at": time.time() + 60,
"poll_interval_seconds": 1,
"hermes_home": str(tmp_path),
"created_at": time.time(),
"updated_at": time.time(),
}
oauth._run_anthropic_credential_worker(flow_id)
assert oauth._OAUTH_FLOWS[flow_id]["status"] == "error"
assert "link failed" in oauth._OAUTH_FLOWS[flow_id]["error"]
payload = oauth.poll_onboarding_oauth_flow(flow_id)
assert payload == {
"ok": True,
"provider": "anthropic",
"flow_id": flow_id,
"status": "error",
"error": "Claude Code credential linking failed. Check server logs.",
}
def test_anthropic_link_clears_env_and_writes_secret_free_marker(monkeypatch, tmp_path):
import api.oauth as oauth
from api.onboarding import _provider_oauth_authenticated
env_path = tmp_path / ".env"
env_path.write_text("ANTHROPIC_TOKEN=old-token\nANTHROPIC_API_KEY=old-key\nOTHER=value\n", encoding="utf-8")
monkeypatch.setenv("ANTHROPIC_TOKEN", "old-token")
monkeypatch.setenv("ANTHROPIC_API_KEY", "old-key")
oauth._link_anthropic_credentials(tmp_path)
env_text = env_path.read_text(encoding="utf-8")
assert "ANTHROPIC_TOKEN" not in env_text
assert "ANTHROPIC_API_KEY" not in env_text
assert "OTHER=value" in env_text
assert "ANTHROPIC_TOKEN" not in os.environ
assert "ANTHROPIC_API_KEY" not in os.environ
auth = json.loads((tmp_path / "auth.json").read_text(encoding="utf-8"))
marker = auth["credential_pool"]["anthropic"][0]
assert marker["auth_type"] == "oauth"
assert marker["source"] == "claude_code_linked"
assert "access_token" not in marker
assert "refresh_token" not in marker
assert _provider_oauth_authenticated("anthropic", tmp_path) is True
assert _provider_oauth_authenticated("claude-code", tmp_path) is True
def test_anthropic_env_clear_waits_for_chat_env_read_lock(monkeypatch, tmp_path):
import api.oauth as oauth
import api.providers as providers
from api.streaming import _ENV_LOCK
monkeypatch.setenv("ANTHROPIC_TOKEN", "old-token")
monkeypatch.setenv("ANTHROPIC_API_KEY", "old-key")
def _fail_before_env_lock(_env_path, _updates):
raise RuntimeError("env write failed before process-env clear")
monkeypatch.setattr(providers, "_write_env_file", _fail_before_env_lock)
started = threading.Event()
done = threading.Event()
errors = []
def _onboarding_clear():
started.set()
try:
oauth._clear_anthropic_env_values(tmp_path)
except Exception as exc: # pragma: no cover - assertion below reports it
errors.append(exc)
finally:
done.set()
with _ENV_LOCK:
worker = threading.Thread(target=_onboarding_clear)
worker.start()
assert started.wait(timeout=1)
assert not done.wait(timeout=0.1)
assert os.environ["ANTHROPIC_TOKEN"] == "old-token"
assert os.environ["ANTHROPIC_API_KEY"] == "old-key"
worker.join(timeout=1)
assert done.is_set()
assert errors == []
assert "ANTHROPIC_TOKEN" not in os.environ
assert "ANTHROPIC_API_KEY" not in os.environ
def test_runtime_provider_reads_use_anthropic_env_lock():
streaming_src = (REPO / "api" / "streaming.py").read_text(encoding="utf-8")
routes_src = (REPO / "api" / "routes.py").read_text(encoding="utf-8")
assert "resolve_runtime_provider_with_anthropic_env_lock" in streaming_src
assert "resolve_runtime_provider_with_anthropic_env_lock" in routes_src
def test_anthropic_onboarding_setup_allows_linked_oauth_without_api_key(monkeypatch, tmp_path):
import api.onboarding as onboarding
# apply_onboarding_setup() short-circuits when HERMES_WEBUI_SKIP_ONBOARDING
# is set in the environment (hosting providers like Agent37 use it to ship
# a pre-configured WebUI). Local test runs may also set it for the same
# reason. The test exercises the file-writing branch, so delete the var
# for the test's scope. monkeypatch.delenv is a no-op if the var is unset.
monkeypatch.delenv("HERMES_WEBUI_SKIP_ONBOARDING", raising=False)
cfg_path = tmp_path / "config.yaml"
home = tmp_path / "home"
home.mkdir()
(home / "auth.json").write_text(json.dumps({
"credential_pool": {"anthropic": [{"auth_type": "oauth", "source": "claude_code_linked"}]}
}), encoding="utf-8")
monkeypatch.setattr(onboarding, "_get_config_path", lambda: cfg_path)
monkeypatch.setattr(onboarding, "_get_active_hermes_home", lambda: home)
monkeypatch.setattr(onboarding, "get_onboarding_status", lambda: {"ok": True})
monkeypatch.setattr(onboarding, "reload_config", lambda: None)
result = onboarding.apply_onboarding_setup({"provider": "anthropic", "model": "claude-sonnet-4.6"})
assert result == {"ok": True}
saved = cfg_path.read_text(encoding="utf-8")
assert "provider: anthropic" in saved
assert "default: claude-sonnet-4.6" in saved
def test_frontend_has_anthropic_oauth_support():
js = (REPO / "static" / "onboarding.js").read_text(encoding="utf-8")
assert "startAnthropicOAuth" in js
assert "cancelAnthropicOAuth" in js
assert "anthropicOAuthBtn" in js
assert "Login with Claude Code" in js
assert "Anthropic API key" in js
assert "Claude Code subscription" in js
assert "not the same as an Anthropic API key" in js
assert "/api/onboarding/oauth/start" in js
assert "/api/onboarding/oauth/poll" in js
assert "/api/onboarding/oauth/cancel" in js
assert "window.open(" not in js[js.find("startAnthropicOAuth"):]
assert "accessToken" not in js[js.find("startAnthropicOAuth"):]
assert "refreshToken" not in js[js.find("startAnthropicOAuth"):]
def test_onboarding_non_custom_provider_mounts_searchable_model_picker():
js = (REPO / "static" / "onboarding.js").read_text(encoding="utf-8")
assert "onboardingModelPickerRoot" in js
assert "_mountSearchableModelSelect" in js
assert "choices:_getOnboardingProviderModelChoices()" in js
assert "selectId:'onboardingModelSelect'" in js
assert "customInputId:'onboardingModelInput'" in js
def test_onboarding_plain_select_fallback_rehydrates_saved_model():
"""When the searchable picker helper is unavailable, the plain <select>
fallback must still rehydrate ONBOARDING.form.model so a saved/default
model that isn't the first option isn't silently replaced by option[0]."""
js = (REPO / "static" / "onboarding.js").read_text(encoding="utf-8")
# The mount block must have an else branch that restores the plain select value.
mount_idx = js.find("if(modelPickerRoot && typeof _mountSearchableModelSelect")
assert mount_idx != -1
after_mount = js[mount_idx:mount_idx + 900]
assert "}else{" in after_mount or "} else {" in after_mount
assert "const modelSel=$('onboardingModelSelect')" in after_mount
assert "modelSel.value=ONBOARDING.form.model" in after_mount
def test_onboarding_custom_provider_text_input_branch_stays_intact():
js = (REPO / "static" / "onboarding.js").read_text(encoding="utf-8")
assert "ONBOARDING.form.provider==='custom'" in js
assert 'id="onboardingModelInput"' in js
assert "onboarding_custom_model_placeholder" in js
assert "onboarding_custom_model_help" in js
@pytest.mark.skipif(NODE is None, reason="node is required")
def test_onboarding_searchable_picker_runtime_and_submit_fallback():
driver = r"""
const fs = require('fs');
const src = fs.readFileSync(process.argv[2], 'utf8');
function extractFunc(name) {
const re = new RegExp('function\\s+' + name + '\\s*\\(');
const start = src.search(re);
if (start < 0) throw new Error(name + ' not found');
const parenStart = src.indexOf('(', start);
let parenDepth = 0;
let inString = null;
let escaped = false;
let bodyStart = -1;
for (let i = parenStart; i < src.length; i++) {
const ch = src[i];
if (inString) {
if (escaped) escaped = false;
else if (ch === '\\') escaped = true;
else if (ch === inString) inString = null;
continue;
}
if (ch === "'" || ch === '"' || ch === '`') {
inString = ch;
continue;
}
if (ch === '(') parenDepth++;
else if (ch === ')') {
parenDepth--;
if (parenDepth === 0) {
bodyStart = src.indexOf('{', i);
break;
}
}
}
if (bodyStart < 0) throw new Error(name + ' body not found');
let depth = 0;
inString = null;
escaped = false;
for (let i = bodyStart; i < src.length; i++) {
const ch = src[i];
if (inString) {
if (escaped) escaped = false;
else if (ch === '\\') escaped = true;
else if (ch === inString) inString = null;
continue;
}
if (ch === "'" || ch === '"' || ch === '`') {
inString = ch;
continue;
}
if (ch === '{') depth++;
else if (ch === '}') {
depth--;
if (depth === 0) return src.slice(start, i + 1);
}
}
throw new Error(name + ' brace scan failed');
}
const elementsById = new Map();
class FakeElement {
constructor(tagName) {
this.tagName = tagName.toUpperCase();
this.children = [];
this.parentNode = null;
this.className = '';
this.id = '';
this.hidden = false;
this.disabled = false;
this.textContent = '';
this._value = '';
this._selectedIndex = -1;
this._listeners = {};
}
appendChild(child) {
child.parentNode = this;
this.children.push(child);
if (child.id) elementsById.set(child.id, child);
if (this.tagName === 'SELECT' && child.tagName === 'OPTION' && this._selectedIndex < 0 && !child.disabled) {
this.selectedIndex = this.children.length - 1;
}
return child;
}
addEventListener(type, handler) {
this._listeners[type] = handler;
}
dispatch(type, extra = {}) {
const handler = this._listeners[type];
if (!handler) return;
handler({
type,
target: this,
key: extra.key,
preventDefault() {},
});
}
focus() {}
querySelector(selector) {
const queue = [...this.children];
while (queue.length) {
const node = queue.shift();
if (selector.startsWith('.')) {
const cls = selector.slice(1);
if ((node.className || '').split(/\s+/).includes(cls)) return node;
} else if (selector.startsWith('#')) {
if (node.id === selector.slice(1)) return node;
} else if (node.tagName === selector.toUpperCase()) {
return node;
}
queue.push(...node.children);
}
return null;
}
set innerHTML(html) {
this.children = [];
const selectId = (html.match(/<select id="([^"]+)"/) || [])[1] || '';
const customInputId = (html.match(/<input id="([^"]+)" class="model-custom-input"/) || [])[1] || '';
const searchRow = new FakeElement('div');
searchRow.className = 'model-search-row';
const searchInput = new FakeElement('input');
searchInput.className = 'model-search-input';
const clearBtn = new FakeElement('button');
clearBtn.className = 'model-search-clear';
searchRow.appendChild(searchInput);
searchRow.appendChild(clearBtn);
const selectEl = new FakeElement('select');
selectEl.id = selectId;
const sep = new FakeElement('div');
sep.className = 'model-group model-custom-sep';
const customRow = new FakeElement('div');
customRow.className = 'model-custom-row';
const customInput = new FakeElement('input');
customInput.className = 'model-custom-input';
customInput.id = customInputId;
const customBtn = new FakeElement('button');
customBtn.className = 'model-custom-btn';
customRow.appendChild(customInput);
customRow.appendChild(customBtn);
this.appendChild(searchRow);
this.appendChild(selectEl);
this.appendChild(sep);
this.appendChild(customRow);
}
get options() {
return this.children;
}
set value(value) {
this._value = String(value);
if (this.tagName === 'SELECT') {
const idx = this.children.findIndex((child) => child.value === this._value);
this._selectedIndex = idx;
}
}
get value() {
if (this.tagName === 'SELECT') {
if (this._selectedIndex >= 0 && this.children[this._selectedIndex]) {
return this.children[this._selectedIndex].value;
}
}
return this._value || '';
}
set selectedIndex(index) {
this._selectedIndex = index;
if (this.tagName === 'SELECT') {
this._value = index >= 0 && this.children[index] ? this.children[index].value : '';
}
}
get selectedIndex() {
return this._selectedIndex;
}
}
const document = {
createElement(tagName) {
return new FakeElement(tagName);
},
getElementById(id) {
return elementsById.get(id) || null;
},
};
global.document = document;
global.window = {};
global.esc = (value) => String(value);
global.t = (key) => key === 'model_search_placeholder'
? 'Search models…'
: key === 'model_custom_label'
? 'Custom model ID'
: key === 'model_custom_placeholder'
? 'e.g. openai/gpt-5.4'
: key;
global.li = () => '+';
eval(extractFunc('_mountSearchableModelSelect'));
const ONBOARDING = { form: { model: '' } };
const $ = (id) => document.getElementById(id);
const root = new FakeElement('div');
_mountSearchableModelSelect({
root,
selectId: 'onboardingModelSelect',
customInputId: 'onboardingModelInput',
choices: [
{ id: 'openrouter/alpha', label: 'OpenRouter Alpha' },
{ id: 'openrouter/beta', label: 'OpenRouter Beta' },
],
selectedValue: ONBOARDING.form.model,
onModelChange: (value) => { ONBOARDING.form.model = value; },
});
const selectEl = $('onboardingModelSelect');
const customInput = $('onboardingModelInput');
const searchInput = root.querySelector('.model-search-input');
const initial = {
model: ONBOARDING.form.model,
selectValue: selectEl.value,
selectedIndex: selectEl.selectedIndex,
};
searchInput.value = 'beta';
searchInput.dispatch('input');
const hiddenStates = selectEl.options.map((option) => ({
value: option.value,
hidden: option.hidden,
}));
customInput.value = 'vendor/custom-model';
customInput.dispatch('input');
const afterCustom = {
model: ONBOARDING.form.model,
selectedIndex: selectEl.selectedIndex,
selectValue: selectEl.value,
};
selectEl.value = 'openrouter/beta';
selectEl.dispatch('change');
const afterSelect = {
model: ONBOARDING.form.model,
customValue: customInput.value,
selectValue: selectEl.value,
};
customInput.value = 'vendor/final-model';
customInput.dispatch('input');
const submitValue = (($('onboardingModelInput') || {}).value || ($('onboardingModelSelect') || {}).value || ONBOARDING.form.model || '').trim();
customInput.value = ' ';
customInput.dispatch('input');
const afterWhitespaceClear = {
model: ONBOARDING.form.model,
selectedIndex: selectEl.selectedIndex,
selectValue: selectEl.value,
};
const whitespaceSubmitValue = (($('onboardingModelInput') || {}).value || ($('onboardingModelSelect') || {}).value || ONBOARDING.form.model || '').trim();
console.log(JSON.stringify({ initial, hiddenStates, afterCustom, afterSelect, submitValue, afterWhitespaceClear, whitespaceSubmitValue, whitespaceCustomValue: customInput.value }));
"""
with tempfile.NamedTemporaryFile("w", suffix=".cjs", encoding="utf-8", dir=REPO, delete=False) as handle:
handle.write(driver)
script = Path(handle.name)
try:
result = subprocess.run(
[NODE, str(script), str(UI_JS)],
capture_output=True,
text=True,
timeout=30,
cwd=str(REPO),
)
finally:
script.unlink(missing_ok=True)
if result.returncode != 0:
raise RuntimeError(result.stderr)
out = json.loads(result.stdout.strip())
assert out["initial"] == {
"model": "openrouter/alpha",
"selectValue": "openrouter/alpha",
"selectedIndex": 1,
}
assert out["hiddenStates"] == [
{"value": "", "hidden": True},
{"value": "openrouter/alpha", "hidden": True},
{"value": "openrouter/beta", "hidden": False},
]
assert out["afterCustom"] == {
"model": "vendor/custom-model",
"selectedIndex": -1,
"selectValue": "",
}
assert out["afterSelect"] == {
"model": "openrouter/beta",
"customValue": "",
"selectValue": "openrouter/beta",
}
assert out["submitValue"] == "vendor/final-model"
assert out["afterWhitespaceClear"] == {
"model": "openrouter/beta",
"selectedIndex": 2,
"selectValue": "openrouter/beta",
}
assert out["whitespaceSubmitValue"] == "openrouter/beta"
assert out["whitespaceCustomValue"] == ""