diff --git a/hermes_cli/auth.py b/hermes_cli/auth.py index 5fd3676bdd3..073a136c251 100644 --- a/hermes_cli/auth.py +++ b/hermes_cli/auth.py @@ -2065,7 +2065,10 @@ def resolve_qwen_runtime_credentials( def get_qwen_auth_status() -> Dict[str, Any]: auth_path = _qwen_cli_auth_path() try: - creds = resolve_qwen_runtime_credentials(refresh_if_expiring=False) + # Validate the runtime credentials, including refresh when the cached + # CLI token is expired. Otherwise stale tokens show up as "logged in" + # and `hermes model` walks users into a broken Qwen setup flow. + creds = resolve_qwen_runtime_credentials(refresh_if_expiring=True) return { "logged_in": True, "auth_file": str(auth_path), diff --git a/tests/hermes_cli/test_auth_qwen_provider.py b/tests/hermes_cli/test_auth_qwen_provider.py index f1943d8459b..a2f58df6b0b 100644 --- a/tests/hermes_cli/test_auth_qwen_provider.py +++ b/tests/hermes_cli/test_auth_qwen_provider.py @@ -392,8 +392,84 @@ def test_get_qwen_auth_status_logged_in(qwen_env): assert status["api_key"] == "status-at" +def test_get_qwen_auth_status_refreshes_expired_token(qwen_env): + expired_ms = int((time.time() - 3600) * 1000) + tokens = _make_qwen_tokens(access_token="old-at", expiry_date=expired_ms) + _write_qwen_creds(qwen_env, tokens) + + refreshed = _make_qwen_tokens(access_token="refreshed-at") + + with patch( + "hermes_cli.auth._refresh_qwen_cli_tokens", return_value=refreshed + ) as mock_refresh: + status = get_qwen_auth_status() + + mock_refresh.assert_called_once() + assert status["logged_in"] is True + assert status["api_key"] == "refreshed-at" + + +def test_get_qwen_auth_status_expired_unrefreshable_token_is_not_logged_in(qwen_env): + expired_ms = int((time.time() - 3600) * 1000) + tokens = _make_qwen_tokens(access_token="dead-at", expiry_date=expired_ms) + _write_qwen_creds(qwen_env, tokens) + + with patch( + "hermes_cli.auth._refresh_qwen_cli_tokens", + side_effect=AuthError( + "Qwen refresh rejected. Re-run 'qwen auth qwen-oauth'.", + provider="qwen-oauth", + code="qwen_refresh_failed", + ), + ) as mock_refresh: + status = get_qwen_auth_status() + + mock_refresh.assert_called_once() + assert status["logged_in"] is False + assert "qwen auth qwen-oauth" in status["error"] + + def test_get_qwen_auth_status_not_logged_in(qwen_env): # No credentials file status = get_qwen_auth_status() assert status["logged_in"] is False assert "error" in status + + +def test_model_flow_qwen_oauth_stale_token_shows_reauth_guidance(qwen_env, monkeypatch, capsys): + from hermes_cli.main import _model_flow_qwen_oauth + + expired_ms = int((time.time() - 3600) * 1000) + tokens = _make_qwen_tokens(access_token="dead-at", expiry_date=expired_ms) + _write_qwen_creds(qwen_env, tokens) + + monkeypatch.setattr( + "hermes_cli.auth._refresh_qwen_cli_tokens", + lambda *args, **kwargs: (_ for _ in ()).throw( + AuthError( + "Qwen refresh rejected. Re-run 'qwen auth qwen-oauth'.", + provider="qwen-oauth", + code="qwen_refresh_failed", + ) + ), + ) + + prompt_called = {"value": False} + update_called = {"value": False} + + monkeypatch.setattr( + "hermes_cli.auth._prompt_model_selection", + lambda *args, **kwargs: prompt_called.__setitem__("value", True), + ) + monkeypatch.setattr( + "hermes_cli.auth._update_config_for_provider", + lambda *args, **kwargs: update_called.__setitem__("value", True), + ) + + _model_flow_qwen_oauth({}, current_model="qwen3-coder-plus") + + out = capsys.readouterr().out + assert "Run: qwen auth qwen-oauth" in out + assert "Qwen refresh rejected" in out + assert prompt_called["value"] is False + assert update_called["value"] is False