184 lines
6.8 KiB
Python
184 lines
6.8 KiB
Python
"""Tests for Routing Control Center & Web Navigation (A24).
|
|
|
|
Verifies:
|
|
1. AutoAssigner.persist_role_chain saves reordered chains to YAML and survives reload.
|
|
2. ActionExecutor handles 'save_chain', 'reorder_chain', 'edit_route', 'assign_role'.
|
|
3. Model assignment via ActionExecutor('set_model') updates profile and role default.
|
|
4. Web client navigation contains exactly 7 primary sections in order.
|
|
5. Redundant views (team, providers) are removed from navigation and client script.
|
|
"""
|
|
|
|
from pathlib import Path
|
|
from typing import List
|
|
|
|
import pytest
|
|
from antigravity_provider.router.action_handler import ActionExecutor
|
|
from antigravity_provider.router.auto_assigner import AutoAssigner
|
|
from antigravity_provider.router.router_config import (
|
|
RolePolicy,
|
|
RouterConfig,
|
|
RouterProfileConfig,
|
|
load_router_config,
|
|
save_router_config,
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def temp_router_config(tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
|
|
"""Creates an isolated temporary router configuration."""
|
|
cfg_path = tmp_path / "router_profiles.yaml"
|
|
monkeypatch.setenv("HERMES_ROUTER_CONFIG", str(cfg_path))
|
|
|
|
# Initialize sample profiles and roles
|
|
config = RouterConfig(
|
|
enabled=True,
|
|
default_role="manager",
|
|
profiles={
|
|
"ag-w1": RouterProfileConfig(
|
|
profile_id="ag-w1",
|
|
provider="antigravity",
|
|
account_id="ag-dev-1",
|
|
preferred_models=["gemini-2.5-pro"],
|
|
),
|
|
"ag-w2": RouterProfileConfig(
|
|
profile_id="ag-w2",
|
|
provider="antigravity",
|
|
account_id="ag-dev-2",
|
|
preferred_models=["gemini-2.5-flash"],
|
|
),
|
|
"op-1": RouterProfileConfig(
|
|
profile_id="op-1",
|
|
provider="opencode-go",
|
|
account_id="op-primary",
|
|
preferred_models=["glm-4.7"],
|
|
),
|
|
"cl-1": RouterProfileConfig(
|
|
profile_id="cl-1",
|
|
provider="claude",
|
|
account_id="cl-fast",
|
|
preferred_models=["claude-3-5-sonnet"],
|
|
),
|
|
},
|
|
roles={
|
|
"developer-1": RolePolicy(
|
|
role_name="developer-1",
|
|
preferred_chain=["ag-w1", "ag-w2", "op-1"],
|
|
default_model="gemini-2.5-pro",
|
|
),
|
|
"developer-2": RolePolicy(
|
|
role_name="developer-2",
|
|
preferred_chain=["ag-w2", "op-1"],
|
|
default_model="gemini-2.5-flash",
|
|
),
|
|
},
|
|
)
|
|
save_router_config(config)
|
|
return cfg_path
|
|
|
|
|
|
def test_persist_role_chain_reorder_and_persistence(temp_router_config: Path):
|
|
"""Verify persist_role_chain updates preferred_chain and primary_profile_id and survives reload."""
|
|
new_chain: List[str] = ["op-1", "ag-w2", "ag-w1"]
|
|
ok, msg = AutoAssigner.persist_role_chain("developer-1", new_chain)
|
|
|
|
assert ok is True
|
|
assert "успешно сохранена" in msg
|
|
|
|
# Reload from disk
|
|
reloaded = load_router_config()
|
|
role_cfg = reloaded.roles["developer-1"]
|
|
assert role_cfg.preferred_chain == new_chain
|
|
assert role_cfg.preferred_chain[0] == "op-1"
|
|
|
|
|
|
def test_persist_role_chain_canonical_name_mapping(temp_router_config: Path):
|
|
"""Verify persist_role_chain resolves Russian aliases and canonical names."""
|
|
new_chain = ["ag-w2", "ag-w1"]
|
|
ok, msg = AutoAssigner.persist_role_chain("кодер 1", new_chain)
|
|
|
|
assert ok is True
|
|
reloaded = load_router_config()
|
|
assert reloaded.roles["developer-1"].preferred_chain == new_chain
|
|
|
|
|
|
def test_persist_role_chain_validation_errors(temp_router_config: Path):
|
|
"""Verify persist_role_chain rejects invalid profiles and duplicates."""
|
|
# Unknown profile
|
|
ok, msg = AutoAssigner.persist_role_chain("developer-1", ["ag-w1", "non-existent-profile"])
|
|
assert ok is False
|
|
assert "не найден" in msg
|
|
|
|
# Duplicate profile in chain
|
|
ok, msg = AutoAssigner.persist_role_chain("developer-1", ["ag-w1", "ag-w1"])
|
|
assert ok is False
|
|
assert "повторяться" in msg or "дублир" in msg
|
|
|
|
# Non-existent role
|
|
ok, msg = AutoAssigner.persist_role_chain("unknown-role-xyz", ["ag-w1"])
|
|
assert ok is False
|
|
assert "Неизвестная роль" in msg or "неизвестн" in msg.lower()
|
|
|
|
|
|
def test_action_executor_save_chain(temp_router_config: Path):
|
|
"""Verify ActionExecutor executes 'save_chain', 'reorder_chain', and 'edit_route'."""
|
|
executor = ActionExecutor()
|
|
|
|
# save_chain
|
|
res1 = executor.execute("save_chain", {"role_id": "developer-1", "chain": ["ag-w2", "ag-w1"]})
|
|
assert res1["ok"] is True
|
|
|
|
# reorder_chain
|
|
res2 = executor.execute("reorder_chain", {"role_id": "developer-1", "desired_chain": ["op-1", "ag-w1"]})
|
|
assert res2["ok"] is True
|
|
|
|
# edit_route
|
|
res3 = executor.execute("edit_route", {"role_id": "developer-2", "chain": ["op-1", "ag-w2"]})
|
|
assert res3["ok"] is True
|
|
|
|
reloaded = load_router_config()
|
|
assert reloaded.roles["developer-1"].preferred_chain == ["op-1", "ag-w1"]
|
|
assert reloaded.roles["developer-2"].preferred_chain == ["op-1", "ag-w2"]
|
|
|
|
|
|
def test_action_executor_set_model_updates_profile_and_role(temp_router_config: Path):
|
|
"""Verify ActionExecutor('set_model') updates profile preferred_models and role default_model."""
|
|
executor = ActionExecutor()
|
|
|
|
res = executor.execute("set_model", {
|
|
"profile_id": "ag-w1",
|
|
"model": "gemini-3.1-pro",
|
|
"role_id": "developer-1",
|
|
})
|
|
assert res["ok"] is True
|
|
|
|
reloaded = load_router_config()
|
|
assert reloaded.profiles["ag-w1"].preferred_models[0] == "gemini-3.1-pro"
|
|
assert reloaded.roles["developer-1"].default_model == "gemini-3.1-pro"
|
|
|
|
|
|
def test_web_client_7_views_exact_order_and_no_redundant_views():
|
|
"""Verify index.html navigation items match exactly 7 sections in required order."""
|
|
static_dir = Path(__file__).parent.parent / "src" / "antigravity_provider" / "router" / "web" / "static"
|
|
index_html = (static_dir / "index.html").read_text(encoding="utf-8")
|
|
app_js = (static_dir / "app.js").read_text(encoding="utf-8")
|
|
|
|
expected_views = [
|
|
"overview",
|
|
"accounts",
|
|
"routing",
|
|
"analytics",
|
|
"health",
|
|
"logs",
|
|
"settings",
|
|
]
|
|
|
|
# Verify presence in exact order in index.html nav-menu
|
|
nav_positions = [index_html.find(f'data-view="{v}"') for v in expected_views]
|
|
assert all(pos != -1 for pos in nav_positions), "All 7 views must be present in index.html"
|
|
assert nav_positions == sorted(nav_positions), "Views in index.html must be in exact specified order"
|
|
|
|
# Verify absence of removed views
|
|
assert 'data-view="providers"' not in index_html
|
|
assert 'data-view="team"' not in index_html
|
|
assert "renderProvidersView" not in app_js
|
|
assert "renderTeamView" not in app_js
|