fix(auth): preserve fallback routes and OAuth state
Switch provider and model together after setup-time auth failure. Serialize global auth-store merges under target-specific locks and preserve auth-to-shared lock ordering for profile OAuth refreshes.
This commit is contained in:
@@ -2002,6 +2002,57 @@ class TestRunJobConfigEnvVarExpansion:
|
||||
"config.yaml ${VAR} in fallback_providers was not expanded."
|
||||
)
|
||||
|
||||
def test_auth_fallback_switches_provider_and_model_together(self, tmp_path):
|
||||
"""Codex auth failure must produce OpenRouter+GLM, never OpenRouter+GPT."""
|
||||
from hermes_cli.auth import AuthError
|
||||
|
||||
(tmp_path / "config.yaml").write_text(
|
||||
"model:\n"
|
||||
" default: gpt-5.6-sol\n"
|
||||
" provider: openai-codex\n"
|
||||
"fallback_providers:\n"
|
||||
" - provider: openrouter\n"
|
||||
" model: z-ai/glm-5.2\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
job = {
|
||||
"id": "auth-fallback",
|
||||
"name": "auth fallback",
|
||||
"prompt": "hi",
|
||||
"provider_snapshot": "openai-codex",
|
||||
"model_snapshot": "gpt-5.6-sol",
|
||||
}
|
||||
fake_db = MagicMock()
|
||||
|
||||
def resolve_runtime(**kwargs):
|
||||
if kwargs.get("requested") in (None, "openai-codex"):
|
||||
raise AuthError(
|
||||
"No Codex credentials stored", provider="openai-codex"
|
||||
)
|
||||
assert kwargs["requested"] == "openrouter"
|
||||
assert kwargs["target_model"] == "z-ai/glm-5.2"
|
||||
return {**self._RUNTIME, "provider": "openrouter"}
|
||||
|
||||
with patch("cron.scheduler._hermes_home", tmp_path), \
|
||||
patch("cron.scheduler._resolve_origin", return_value=None), \
|
||||
patch("hermes_cli.env_loader.load_hermes_dotenv"), \
|
||||
patch("hermes_cli.env_loader.reset_secret_source_cache"), \
|
||||
patch("hermes_state.SessionDB", return_value=fake_db), \
|
||||
patch("hermes_cli.runtime_provider.resolve_runtime_provider",
|
||||
side_effect=resolve_runtime), \
|
||||
patch("tools.mcp_tool.discover_mcp_tools", return_value=[]), \
|
||||
patch("run_agent.AIAgent") as mock_agent_cls:
|
||||
mock_agent = MagicMock()
|
||||
mock_agent.run_conversation.return_value = {"final_response": "ok"}
|
||||
mock_agent_cls.return_value = mock_agent
|
||||
success, _, _, error = run_job(job)
|
||||
|
||||
assert success is True
|
||||
assert error is None
|
||||
kwargs = mock_agent_cls.call_args.kwargs
|
||||
assert kwargs["provider"] == "openrouter"
|
||||
assert kwargs["model"] == "z-ai/glm-5.2"
|
||||
|
||||
def test_fallback_chain_merges_providers_and_legacy_model(self, tmp_path, monkeypatch):
|
||||
"""Cron uses get_fallback_chain so legacy fallback_model is not dropped."""
|
||||
(tmp_path / "config.yaml").write_text(
|
||||
|
||||
Reference in New Issue
Block a user