hermes-agent/tests/hermes_cli/test_billing_cli.py
Teknium 39975613b1
test: prune wave 2 + speed fixes — 28,106 → 19,757 test functions, suite wall 315s → 294s
Second, deeper pass over tools/gateway/hermes_cli plus first pass over
the trees wave 1 missed (acp, acp_adapter, skills, computer_use, docker,
dashboard, conformance, monitoring, secret_sources, hermes_state,
providers). Same rubric as wave 1 (AGENTS.md test policy); security,
alternation/caching invariants, issue-number regressions, and E2E kept.

Real test-quality fixes found and rooted out along the way:
- tests/tools/test_command_guards.py made real auxiliary-LLM HTTPS calls
  (DEFAULT_CONFIG smart-approval leaked in) — pinned approval
  mode=manual via autouse fixture: 17.4s → 0.4s.
- test_model_switch_custom_providers.py / test_user_providers_model_switch.py
  silently probed live provider catalogs (~2s/test) — stubbed
  cached_provider_model_ids/provider_model_ids/fetch_api_models.
- test_telegram_noise_filter.py: 15-platform copy-paste matrix over
  shared gateway.run logic → 3 representative platforms (55s → 3.9s).
- test_gateway_shutdown.py: stop()'s 5s interrupt-deadline loop spun on
  MagicMock agents — interrupt.side_effect now clears _running_agents
  (22s → 1.0s).
- test_gateway_inactivity_timeout.py poll-harness timings shrunk 3-5x
  (24s → 1.1s); test_mcp_stability.py backoff/SIGTERM-grace sleeps
  patched (15.4s → 2.5s); test_async_delegation.py negative-drain wait
  5s → 0.5s.
- test_telegram_init_deadline.py: loop-block margin restored to 1.0s
  with rationale comment — the watchdog-dump assertion needs the loop
  blocked well past deadline+grace under parallel load (flaked once in
  the 40-worker verification run at a 0.2s margin).

Verification: full hermetic suite via scripts/run_tests.sh —
2,438 files, 21,718 tests passed, 0 failed, 293.9s wall.
Suite totals vs original baseline: 46,820 → 19,757 test functions
(−57.8%), wall 583.5s → 293.9s (−50%), subprocess CPU 13,564s → 11,623s.
2026-07-29 13:39:40 -07:00

124 lines
4.1 KiB
Python

"""Tests for the /billing CLI handler (cli.py::_show_billing).
Focus on the non-interactive (no live prompt_toolkit app) path — the same
discipline as the /credits non-interactive test: it must render text, never
invoke the modal (which would read the slash-worker's JSON-RPC stdin and hang).
Plus role/kill-switch gating and logged-out handling.
"""
from __future__ import annotations
from decimal import Decimal
import pytest
import agent.billing_view as bv
from agent.billing_view import BillingState, CardInfo, MonthlyCap
from cli import HermesCLI
@pytest.fixture
def cli():
obj = HermesCLI.__new__(HermesCLI) # bypass __init__ (no full app needed)
obj._app = None # non-interactive: forces the text path
return obj
def _boom_modal(*a, **kw):
raise AssertionError("modal must NOT be called in non-interactive mode")
# ── Card visibility + the add-card path (inline w/ NAS card-resolver) ──
def _scripted(*responses):
it = iter(responses)
def _modal(self, **kw):
return next(it)
return _modal
def test_topup_overview_splits_onetime_from_automatic_copy(cli, monkeypatch, capsys):
# (c): the interactive /topup overview states the one-time-vs-automatic
# distinction up front in each first sentence, and keeps "credits" out of the
# dollars-only surface. Auto-reload OFF → the automatic line omits amounts.
cli._app = object() # interactive → reaches the split-copy explainer
state = BillingState(
logged_in=True, role="OWNER", balance_usd=Decimal("50"),
cli_billing_enabled=True, charge_presets=(Decimal("25"),),
card=CardInfo(brand="Visa", last4="4242"),
portal_url="https://portal/billing",
)
monkeypatch.setattr(bv, "build_billing_state", lambda *a, **kw: state)
# Overview prints the explainer, then the action modal → back out with "cancel".
monkeypatch.setattr(HermesCLI, "_prompt_text_input_modal", _scripted("cancel"), raising=False)
cli._show_billing("/topup")
out = capsys.readouterr().out
assert "Add funds now — a single charge, added to your balance today." in out
assert "Refill when low — charges your card automatically when your balance falls below" in out
# Dollars-only surface: no "credits" word leaks into /topup.
assert "credits" not in out.lower()
def test_topup_automatic_copy_generic_when_amounts_missing(cli, monkeypatch, capsys):
# (5): auto-reload "enabled" but amounts absent (partial response) → generic
# copy, never "charges — automatically … below —.".
from agent.billing_view import AutoReload
cli._app = object()
state = BillingState(
logged_in=True, role="OWNER", balance_usd=Decimal("50"),
cli_billing_enabled=True, charge_presets=(Decimal("25"),),
card=CardInfo(brand="Visa", last4="4242"),
auto_reload=AutoReload(enabled=True, threshold_usd=None, reload_to_usd=None),
portal_url="https://portal/billing",
)
monkeypatch.setattr(bv, "build_billing_state", lambda *a, **kw: state)
monkeypatch.setattr(HermesCLI, "_prompt_text_input_modal", _scripted("cancel"), raising=False)
cli._show_billing("/topup")
out = capsys.readouterr().out
assert (
"Refill when low — charges your card automatically when your balance "
"falls below the amount you set."
) in out
assert "charges — automatically" not in out
def test_buy_flow_no_card_back_abandons(cli, monkeypatch, capsys):
cli._app = object()
nocard = BillingState(
logged_in=True, role="OWNER", cli_billing_enabled=True,
charge_presets=(Decimal("25"),), card=None,
portal_url="https://portal/billing",
)
calls = {"n": 0}
def _no_fetch(*a, **kw):
calls["n"] += 1
return nocard
monkeypatch.setattr(bv, "build_billing_state", _no_fetch)
monkeypatch.setattr(HermesCLI, "_prompt_text_input_modal", _scripted("cancel"), raising=False)
cli._billing_buy_flow(nocard)
out = capsys.readouterr().out
assert "Add a card first" in out
assert "Cancelled. No funds added." in out
assert calls["n"] == 0 # backed out before any re-check