Files
hermes-agent/tests/agent/test_missing_provider_credentials_hint.py
ethernet f3b1399211 fix: round-5 CI backlog after the 779-commit main merge
Merge fallout (my resolution errors, all caught by CI):
- hermes_cli/backup.py + gateway.py: `theirs` on those hunks re-imported clusters HEAD had
  already moved to backup_restore.py / kept in the facade. backup.py loses the 349-line
  duplicate (main's #110179 fix is ported into backup_restore._import_db_member); the
  systemd service-unit cluster returns to gateway.py (PM's _prepare_service_launcher /
  _pm_managed_node_dirs / _systemd_command have no home in main's extraction) with main's
  utf-8-sig read. gateway_service_unit.py is dropped.
- gateway/run.py: main's plugin-update chore is not profile-scoped (the housekeeping
  ordering test pins the scope/drain sequence).
- pyproject + 30 test files: `import yaml` -> `import hermes_yaml as yaml` (pm-clean has no
  pyyaml); gateway/config._bundled_platform_manifest_name reads through hermes_yaml.
- tests re-seamed onto pm-clean's shape: residency admission (installed_engine),
  supervisor child env (binary is a constructor argument), update import guard
  (update_cmd_deps is gone; our probe already scrubs PYTHONPATH — both #115032 invariants
  pass), shallow-count git responses (stash path asks `status --porcelain -z`); dropped
  tests for retired code (_run_node_bootstrap/_ensure_tui_node, Windows resume demotion).
- tests/tools/test_local_env_blocklist.py: restore the two helpers the suite-reduction
  commit dropped and the blocklist import.

Real fixes:
- pm: classify_uv_failure/ResolutionConflict move beside the uv runner (pm.environment,
  stdlib-only). pm.workspace imports tomllib at module level and cannot load on the 3.10
  bootstrap python that streams uv output in the Docker arm64 image.
- tools/browser_tool.warm_agent_browser_npx_cache: back as a permanent definition — it is on
  the frozen old-updater surface, and the revert-scheduled compat pointer does not count.
- hermes_cli/memory_setup: the dashboard's pip row uses pm.environments.
  running_from_selected_environment for installed vs restart_required.
- scripts/windows-build-deps.ps1: export DISTUTILS_USE_SDK/MSSdk so setuptools trusts the
  primed MSVC environment instead of asking vswhere (`env -i` test runner on win32-arm64
  compiling ruamel-yaml-clib); run_tests.sh forwards them.
- tests/pm/test_windows_build_deps.py: start the protocol test from a parent env without the
  toolchain variables the runner job already exports.
- tests/conftest.py scrubs HERMES_BUNDLED_PLUGINS (Nix-wrapped hermes on the dev host);
  tests/home_io_guard.py treats sys.path site-packages under the real home as the
  interpreter's installation (PM-activated developer shell).
- tests-js: four `curly` lint errors from main's new scripts.
2026-09-21 02:47:48 -04:00

120 lines
5.9 KiB
Python

"""The "no credentials" error for an explicit provider names a remedy that can actually work.
Deriving the env var from the provider id (``f"{id.upper()}_API_KEY"``) invents names nothing reads:
``MINIMAX-OAUTH_API_KEY`` for OAuth ids (#114405, #78996) and ``ALIBABA_API_KEY`` where the registry
reads ``DASHSCOPE_API_KEY``. Both the auxiliary ladder and main-agent init share one helper.
"""
import pytest
import hermes_yaml as yaml
from agent.auxiliary_unavailable import missing_provider_credentials_message
from hermes_cli.auth import PROVIDER_REGISTRY
@pytest.mark.parametrize("provider, expected, forbidden", [
("minimax-oauth", "hermes auth add minimax-oauth", "MINIMAX-OAUTH_API_KEY"),
("alibaba", "Set the DASHSCOPE_API_KEY environment variable", "ALIBABA_API_KEY"),
])
def test_aux_ladder_names_registry_remedy_for_explicit_provider(tmp_path, monkeypatch, provider, expected, forbidden):
"""Real call_llm → ladder with the compression provider pinned and no credentials anywhere."""
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
for var in ("MINIMAX_API_KEY", "DASHSCOPE_API_KEY"):
monkeypatch.delenv(var, raising=False)
(tmp_path / "config.yaml").write_text(yaml.safe_dump({
"model": {"provider": provider, "default": "test-model"},
"auxiliary": {"compression": {"provider": provider, "model": "test-model"}},
}), encoding="utf-8")
from agent.auxiliary_client import call_llm
from agent.auxiliary_unavailable import AuxiliaryClientUnavailable
with pytest.raises(AuxiliaryClientUnavailable) as excinfo:
call_llm(task="compression", messages=[{"role": "user", "content": "hi"}], max_tokens=5)
assert expected in str(excinfo.value)
assert forbidden not in str(excinfo.value)
def test_main_init_shares_helper_and_no_registry_provider_gets_an_invented_env_var(tmp_path, monkeypatch):
"""agent_init raises the same text as the helper; no registry id yields a made-up ``<ID>_API_KEY``."""
from types import SimpleNamespace
from agent.agent_init import _routed_client_kwargs
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
agent = SimpleNamespace(provider="minimax-oauth", model="m", base_url=None, api_key=None,
_fallback_activated=False, _explicit_provider="minimax-oauth")
with pytest.raises(RuntimeError, match=r"hermes auth add minimax-oauth"):
_routed_client_kwargs(agent, None, 60)
for pid, pconfig in PROVIDER_REGISTRY.items():
invented = f"{pid.upper()}_API_KEY"
message = missing_provider_credentials_message(pid)
if invented in message:
assert invented in pconfig.api_key_env_vars, (pid, message)
if not pconfig.api_key_env_vars:
assert "_API_KEY environment variable" not in message, (pid, message)
def _write_exhausted_codex_pool(home, *, count: int, reset_at: float):
"""Persist a Codex OAuth pool the way ``mark_exhausted`` leaves it after a 429."""
import json
import time
from agent.credential_pool import PooledCredential
entries = [PooledCredential(
provider="openai-codex", id=f"codex-{i}", label=f"acct{i}", auth_type="oauth", priority=i,
source="manual", access_token=f"eyJ.fake.{i}", refresh_token="rt", last_status="exhausted",
last_status_at=time.time() - 60, last_error_code=429, last_error_reason="usage_limit_reached",
last_error_reset_at=reset_at).to_dict() for i in range(count)]
(home / "auth.json").write_text(json.dumps({"version": 1, "credential_pool": {"openai-codex": entries}}))
def test_exhausted_oauth_pool_reports_cooldown_not_missing_credentials(tmp_path, monkeypatch):
"""#56810: a valid OAuth grant in 429 cooldown is not "no credentials … hermes auth add".
Both raise sites (main-agent init and the auxiliary ladder) render the pool state: how many
credentials are benched and when the next one resets.
"""
import time
from types import SimpleNamespace
from agent.agent_init import _routed_client_kwargs
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
monkeypatch.setenv("HOME", str(tmp_path)) # never adopt the host's Codex CLI tokens
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
reset_at = time.time() + 3 * 3600
_write_exhausted_codex_pool(tmp_path, count=2, reset_at=reset_at)
(tmp_path / "config.yaml").write_text(yaml.safe_dump({
"model": {"provider": "openai-codex", "default": "test-model"},
"auxiliary": {"compression": {"provider": "openai-codex", "model": "test-model"}},
}), encoding="utf-8")
expected_time = time.strftime("%Y-%m-%d %H:%M", time.localtime(reset_at))
agent = SimpleNamespace(provider="openai-codex", model="test-model", base_url=None, api_key=None,
_fallback_activated=False, _explicit_provider="openai-codex")
with pytest.raises(RuntimeError) as excinfo:
_routed_client_kwargs(agent, None, 60)
message = str(excinfo.value)
assert "all 2 credentials are cooling down" in message
assert expected_time in message
assert "no credentials were found" not in message
from agent.auxiliary_client import call_llm
from agent.auxiliary_unavailable import AuxiliaryClientUnavailable
with pytest.raises(AuxiliaryClientUnavailable) as aux_exc:
call_llm(task="compression", messages=[{"role": "user", "content": "hi"}], max_tokens=5)
assert "cooling down" in str(aux_exc.value)
assert "no credentials were found" not in str(aux_exc.value)
def test_elapsed_cooldown_or_empty_pool_keeps_missing_credentials_text(tmp_path, monkeypatch):
"""Control: a pool whose cooldown already elapsed (or no pool at all) is not called "cooling down"."""
import time
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
assert "cooling down" not in missing_provider_credentials_message("openai-codex")
_write_exhausted_codex_pool(tmp_path, count=1, reset_at=time.time() - 60)
assert "cooling down" not in missing_provider_credentials_message("openai-codex")