hermes-agent/tests/hermes_cli/test_provider_groups.py
Teknium 93e6a05efc
feat(model-picker): group multi-endpoint providers under one row (#35227)
* Inspired by Claude Code: /compress here [N] — boundary-aware 'summarize up to here'

Adds a user-chosen compression boundary to the existing /compress command.
/compress here [N] summarizes everything except the most recent N exchanges
(default 2), which are preserved verbatim — letting the user pick the
compression boundary instead of relying on the automatic token-budget heuristic.

Inspired by Claude Code's Rewind 'Summarize up to here' action (v2.1.139,
Week 20, May 2026): https://code.claude.com/docs/en/whats-new/2026-w20

- hermes_cli/partial_compress.py: pure split/parse helpers + seam-alternation
  guard (shared by CLI and gateway).
- cli.py / gateway/run.py: route 'here [N]' / '--keep N' to partial compression;
  compress only the head, re-append the verbatim tail through the seam guard.
- Preserves message-flow role alternation (seam guard merges any illegal
  user->user / assistant->assistant adjacency).
- Reuses the existing _compress_context session-rotation/lock machinery — no
  changes to the compression core.
- Bare /compress (full) and /compress <focus> behavior unchanged.

Tests: 12 helper unit tests + 5 CLI integration tests + E2E (interleaved
tool-call transcript, degenerate/multimodal seams, real handler path).

* feat(model-picker): group multi-endpoint providers under one row

The interactive provider pickers (hermes model, setup wizard, Telegram
/model) listed every provider slug flat, so vendors with several endpoints
(Kimi/Moonshot, MiniMax, xAI Grok, Google Gemini, OpenAI, OpenCode, GitHub
Copilot) each occupied multiple top-level rows. Now related slugs fold into
one top-level row that drills down to the specific endpoint.

- models.py: add PROVIDER_GROUPS table + group_providers() fold (display
  only — CANONICAL_PROVIDERS, slugs, --provider, /model <provider:model>
  all unchanged and individually addressable).
- hermes model (main.py): group rows drill into a member sub-picker, then
  dispatch to the existing _model_flow_* unchanged. setup wizard inherits it.
- Telegram /model: new mpg:<group> callback expands to member mp:<slug>
  buttons; single authenticated member degrades to a direct button.
- Grouping is the single shared fold across all three surfaces.

Validation: 163 targeted tests pass; E2E confirms group->member->model
resolves to the correct concrete slug for all families.
2026-05-30 01:41:33 -07:00

118 lines
4.2 KiB
Python

"""Tests for provider-group folding (display-only picker grouping).
These are invariant tests, not catalog snapshots: they assert how
``group_providers`` folds a flat slug list and how member slugs relate to
``PROVIDER_GROUPS`` / ``CANONICAL_PROVIDERS`` — not the specific set of
vendors, which is expected to change over time.
"""
from hermes_cli.models import (
CANONICAL_PROVIDERS,
PROVIDER_GROUPS,
group_providers,
provider_group_for_slug,
)
def _slugs(rows):
"""Flatten picker rows back to the concrete slugs they expose."""
out = []
for r in rows:
if r["kind"] == "single":
out.append(r["slug"])
else:
out.extend(r["members"])
return out
def test_groups_reference_real_canonical_slugs():
"""Every group member must be an actual provider slug. Guards typos and
stale group entries after a provider is renamed/removed."""
canonical = {p.slug for p in CANONICAL_PROVIDERS}
for gid, (label, members) in PROVIDER_GROUPS.items():
assert label, f"group {gid} has empty label"
assert len(members) >= 1
for m in members:
assert m in canonical, f"group {gid} member {m!r} is not a canonical slug"
def test_member_slugs_are_unique_across_groups():
"""A slug may belong to at most one group."""
seen = {}
for gid, (_label, members) in PROVIDER_GROUPS.items():
for m in members:
assert m not in seen, f"{m!r} in both {seen[m]!r} and {gid!r}"
seen[m] = gid
def test_reverse_index_matches_groups():
for gid, (_label, members) in PROVIDER_GROUPS.items():
for m in members:
assert provider_group_for_slug(m) == gid
assert provider_group_for_slug("openrouter") == ""
assert provider_group_for_slug("") == ""
def test_ungrouped_providers_pass_through_in_order():
rows = group_providers(["nous", "openrouter", "deepseek"])
assert all(r["kind"] == "single" for r in rows)
assert [r["slug"] for r in rows] == ["nous", "openrouter", "deepseek"]
def test_multi_member_group_folds_to_one_row():
rows = group_providers(["minimax", "minimax-oauth", "minimax-cn"])
assert len(rows) == 1
row = rows[0]
assert row["kind"] == "group"
assert row["group_id"] == "minimax"
assert row["members"] == ["minimax", "minimax-oauth", "minimax-cn"]
def test_group_appears_at_first_member_position():
"""The group row takes the slot of its earliest-listed present member,
and later members do not re-emit."""
rows = group_providers(["nous", "minimax", "deepseek", "minimax-cn"])
kinds = [(r["kind"], r.get("group_id") or r.get("slug")) for r in rows]
assert kinds == [
("single", "nous"),
("group", "minimax"),
("single", "deepseek"),
]
# both minimax members folded into the single group row
assert rows[1]["members"] == ["minimax", "minimax-cn"]
def test_single_present_member_degrades_to_single_row():
"""A group with only one present member shows no submenu."""
rows = group_providers(["xai"]) # xai-oauth absent
assert len(rows) == 1
assert rows[0]["kind"] == "single"
assert rows[0]["slug"] == "xai"
def test_member_order_follows_declaration_not_input():
"""Inside a folded group, members are ordered by PROVIDER_GROUPS, not by
the order they appeared in the input list."""
rows = group_providers(["minimax-cn", "minimax", "minimax-oauth"])
assert rows[0]["members"] == ["minimax", "minimax-oauth", "minimax-cn"]
def test_duplicate_slugs_ignored():
rows = group_providers(["nous", "nous", "minimax", "minimax"])
assert [r.get("slug") or r["group_id"] for r in rows] == ["nous", "minimax"]
def test_fold_is_lossless_for_present_slugs():
"""Every input slug (deduped) must still be reachable through the folded
rows — grouping hides nothing."""
flat = [p.slug for p in CANONICAL_PROVIDERS]
rows = group_providers(flat)
assert set(_slugs(rows)) == set(flat)
def test_canonical_fold_row_count_shrinks():
"""Folding the full canonical list produces fewer top-level rows than the
flat list (proves grouping actually consolidates)."""
flat = [p.slug for p in CANONICAL_PROVIDERS]
rows = group_providers(flat)
assert len(rows) < len(flat)