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:
Dan Schnurbusch
2026-07-15 18:28:25 -05:00
committed by Teknium
parent 261b0f8240
commit f68fd80f41
8 changed files with 388 additions and 71 deletions
+51
View File
@@ -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(