fix(ci): A33 stop hanging tests and stabilize CI #1

Merged
ochenstarik-ui merged 11 commits from codex/a33-test-hang-ci-stabilization into main 2026-08-30 18:47:51 +00:00
26 changed files with 510 additions and 275 deletions
Showing only changes of commit b149a6ab73 - Show all commits

View file

@ -15,6 +15,7 @@ from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple
from antigravity_provider.router.profile_manager import ProfileAuthManager, mask_email, mask_id
from antigravity_provider.router.role_registry import RoleRegistry
from antigravity_provider.router.router_config import (
RolePolicy,
RouterConfig,
@ -25,19 +26,7 @@ from antigravity_provider.router.router_config import (
logger = logging.getLogger("hermes.router.auto_assigner")
HUMAN_ROLE_LABELS = {
"orchestrator_primary": "Главный оркестратор",
"orchestrator_fallback": "Резервный оркестратор",
"coder_1": "Кодер 1",
"coder_2": "Кодер 2",
"reviewer": "Ревьюер",
"researcher": "Исследователь",
"fast_agent": "Быстрый агент",
"universal_subagent": "Универсальный субагент",
"spare_1": "Резерв 1",
"spare_2": "Резерв 2",
"cold_spare": "Холодный резерв",
}
HUMAN_ROLE_LABELS = RoleRegistry.get_human_role_labels()
DEFAULT_SLOT_ROLES = {
"codex-orch": ("Главный оркестратор", "orchestrator", "primary"),
@ -67,35 +56,7 @@ DEFAULT_SLOT_ROLES = {
}
CANONICAL_ROLE_MAP = {
"orchestrator": "orchestrator",
"orchestrator_primary": "orchestrator",
"orchestrator_fallback": "orchestrator",
"главный оркестратор": "orchestrator",
"резервный оркестратор": "orchestrator",
"coder": "coder-primary",
"coder_1": "coder-primary",
"coder-1": "coder-primary",
"coder-primary": "coder-primary",
"кодер 1": "coder-primary",
"coder_2": "coder-secondary",
"coder-2": "coder-secondary",
"coder-secondary": "coder-secondary",
"кодер 2": "coder-secondary",
"reviewer": "reviewer",
"ревьюер": "reviewer",
"research": "research",
"researcher": "research",
"исследователь": "research",
"fast": "fast",
"fast_agent": "fast",
"быстрый агент": "fast",
"general": "fast",
"universal_subagent": "fast",
"универсальный субагент": "fast",
"tester": "fast",
"тестировщик": "fast",
}
CANONICAL_ROLE_MAP = RoleRegistry.get_canonical_role_map()
class AutoAssigner:
@ -172,7 +133,7 @@ class AutoAssigner:
candidates = list(provider_slots.get(provider_norm, []))
# Priority based on requested role
if requested_role == "orchestrator":
if requested_role == "manager":
if provider_norm in ("openai-codex", "codex") and "codex-orch" in candidates:
candidates.remove("codex-orch")
candidates.insert(0, "codex-orch")
@ -368,7 +329,7 @@ class AutoAssigner:
"changes": [],
}
canonical_roles = ["orchestrator", "coder-primary", "coder-secondary", "reviewer", "research", "fast"]
canonical_roles = [r for r in RoleRegistry.get_role_ids() if RoleRegistry.is_role_implemented(r)]
changes: List[Dict[str, Any]] = []
# Ensure canonical roles exist in config
@ -418,12 +379,12 @@ class AutoAssigner:
else:
# Case 3: Multiple providers connected -> distribute by role provider preferences
role_provider_preferences = {
"orchestrator": ["codex", "antigravity", "opencode", "claude", "grok", "local"],
"coder-primary": ["codex", "antigravity", "opencode", "claude", "grok", "local"],
"coder-secondary": ["codex", "antigravity", "opencode", "claude", "grok", "local"],
"reviewer": ["codex", "opencode", "antigravity", "claude", "grok", "local"],
"research": ["opencode", "antigravity", "grok", "claude", "codex", "local"],
"fast": ["opencode", "antigravity", "local", "grok", "codex", "claude"],
"manager": ["codex", "antigravity", "opencode", "claude", "grok", "local"],
"developer-1": ["codex", "antigravity", "opencode", "claude", "grok", "local"],
"developer-2": ["codex", "antigravity", "opencode", "claude", "grok", "local"],
"code-reviewer": ["codex", "opencode", "antigravity", "claude", "grok", "local"],
"researcher": ["opencode", "antigravity", "grok", "claude", "codex", "local"],
"tester": ["opencode", "antigravity", "local", "grok", "codex", "claude"],
}
by_prov: Dict[str, List[str]] = {}
@ -503,7 +464,7 @@ class AutoAssigner:
main_codex = ProfileAuthManager.get_main_profile("openai-codex")
team = {
"orchestrator": [],
"manager": [],
"subagents": [],
"spares": [],
"summary": {

View file

@ -0,0 +1,303 @@
"""Hermes Multi-Provider Account Router — Unified Role Registry."""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional, Tuple
STATUS_DECLARED_UNIMPLEMENTED = "DECLARED_UNIMPLEMENTED"
STATUS_DECLARED_UNIMPLEMENTED_LABEL_RU = "Роль объявлена, исполнение не реализовано"
@dataclass(frozen=True)
class RoleDefinition:
role_id: str
display_name_ru: str
short_name_ru: str
description_ru: str
is_implemented: bool = True
unimplemented_reason_ru: Optional[str] = None
capabilities: List[str] = field(default_factory=list)
fallback_capabilities: List[str] = field(default_factory=list)
default_preferred_chain: List[str] = field(default_factory=list)
default_model: Optional[str] = None
max_failover_attempts: int = 3
session_affinity_enabled: bool = True
is_orchestrator: bool = False
tier: str = "core"
CANONICAL_ROLES: Dict[str, RoleDefinition] = {
"manager": RoleDefinition(
role_id="manager",
display_name_ru="Менеджер проекта (Оркестратор)",
short_name_ru="Менеджер",
description_ru="Координация работы субагентов, планирование, контроль исполнения и распределение задач.",
is_implemented=True,
is_orchestrator=True,
capabilities=["orchestrator", "reasoning", "management", "planning"],
fallback_capabilities=["orchestrator", "reasoning"],
default_preferred_chain=["codex-orch", "ag-orch-fallback", "claude-orch", "grok-orch", "opengo-3"],
default_model="gemini-3.7-flash",
max_failover_attempts=3,
tier="core",
),
"developer-1": RoleDefinition(
role_id="developer-1",
display_name_ru="Ведущий разработчик (Кодер 1)",
short_name_ru="Кодер 1",
description_ru="Пишет код, реализует функционал, исправляет ошибки.",
is_implemented=True,
capabilities=["coding", "developer-1", "reasoning", "tools", "structured_output"],
fallback_capabilities=["coding"],
default_preferred_chain=["codex-worker-1", "claude-worker-1", "ag-w1", "opengo-1"],
max_failover_attempts=3,
tier="core",
),
"developer-2": RoleDefinition(
role_id="developer-2",
display_name_ru="Вспомогательный разработчик (Кодер 2)",
short_name_ru="Кодер 2",
description_ru="Проверяет код Разработчика 1 и выдаёт ему задание на исправление.",
is_implemented=True,
capabilities=["coding", "developer-2", "reviewer", "tools"],
fallback_capabilities=["coding", "reviewer"],
default_preferred_chain=["ag-w1", "grok-worker-1", "codex-worker-2", "opengo-3"],
max_failover_attempts=3,
tier="core",
),
"code-reviewer": RoleDefinition(
role_id="code-reviewer",
display_name_ru="Ревьюер кода",
short_name_ru="Код-ревьювер",
description_ru="Анализирует код на ошибки, проблемы безопасности и соответствие стандартам. Работает после Разработчика 2.",
is_implemented=True,
capabilities=["code-reviewer", "reviewer", "coding", "security_analysis"],
fallback_capabilities=["reviewer", "coding"],
default_preferred_chain=["claude-worker-2", "codex-worker-2", "ag-w2", "opengo-2"],
max_failover_attempts=3,
tier="core",
),
"researcher": RoleDefinition(
role_id="researcher",
display_name_ru="Исследователь",
short_name_ru="Исследователь",
description_ru="Изучает данные, кодовую базу, документацию и внешние источники, чтобы собрать информацию для решения задачи.",
is_implemented=True,
capabilities=["researcher", "research", "search", "long_context"],
fallback_capabilities=["research", "search"],
default_preferred_chain=["ag-w3", "grok-worker-2", "opengo-2", "opengo-1"],
max_failover_attempts=3,
tier="core",
),
"tester": RoleDefinition(
role_id="tester",
display_name_ru="Тестировщик (QA)",
short_name_ru="Тестировщик",
description_ru="Создаёт тесты, проверяет корректность работы кода, находит дефекты.",
is_implemented=True,
capabilities=["tester", "testing", "fast", "automation"],
fallback_capabilities=["fast", "testing"],
default_preferred_chain=["ag-w4", "opengo-1", "ag-spare-1"],
max_failover_attempts=3,
tier="qa_doc",
),
"tech-writer": RoleDefinition(
role_id="tech-writer",
display_name_ru="Технический писатель",
short_name_ru="Техписатель",
description_ru="Создаёт документацию, инструкции, README.",
is_implemented=True,
capabilities=["tech-writer", "documentation", "reasoning", "structured_output"],
fallback_capabilities=["documentation", "reasoning"],
default_preferred_chain=["claude-worker-2", "ag-w3", "ag-w2"],
max_failover_attempts=3,
tier="qa_doc",
),
"analyst": RoleDefinition(
role_id="analyst",
display_name_ru="Системный аналитик",
short_name_ru="Аналитик",
description_ru="Проводит глубокий анализ данных, выявляет тренды, строит прогнозы.",
is_implemented=True,
capabilities=["analyst", "reasoning", "research", "planning"],
fallback_capabilities=["reasoning", "research"],
default_preferred_chain=["grok-worker-2", "opengo-2", "ag-w3"],
max_failover_attempts=3,
tier="qa_doc",
),
"guardian": RoleDefinition(
role_id="guardian",
display_name_ru="Надзиратель (агент безопасности)",
short_name_ru="Надзиратель",
description_ru="Проверяет входящие инструкции на промпт-инъекции, анализирует планы и вызовы инструментов, блокирует обход системных правил, не допускает утечки секретов, следит за границами песочницы.",
is_implemented=False,
unimplemented_reason_ru=STATUS_DECLARED_UNIMPLEMENTED_LABEL_RU,
capabilities=["guardian", "security", "sandboxing"],
fallback_capabilities=["guardian", "security"],
default_preferred_chain=[],
max_failover_attempts=0,
tier="governance",
),
"cost-controller": RoleDefinition(
role_id="cost-controller",
display_name_ru="Агент контроля затрат",
short_name_ru="Контроль затрат",
description_ru="Оценивает планируемый расход токенов, сравнивает с остатком бюджета, предлагает упрощения, сверяет факт с прогнозом, останавливает цепочку при исчерпании лимита.",
is_implemented=False,
unimplemented_reason_ru=STATUS_DECLARED_UNIMPLEMENTED_LABEL_RU,
capabilities=["cost-controller", "budget", "analytics"],
fallback_capabilities=["cost-controller", "budget"],
default_preferred_chain=[],
max_failover_attempts=0,
tier="governance",
),
"integration-expert": RoleDefinition(
role_id="integration-expert",
display_name_ru="Специалист по интеграции",
short_name_ru="Интегратор",
description_ru="Работает с API и внешними сервисами, отправляет вебхуки.",
is_implemented=True,
capabilities=["integration-expert", "integration", "coding", "networking"],
fallback_capabilities=["integration", "coding"],
default_preferred_chain=["opengo-3", "codex-worker-1", "ag-w4"],
max_failover_attempts=3,
tier="expert",
),
"security-expert": RoleDefinition(
role_id="security-expert",
display_name_ru="Юрист / специалист по безопасности",
short_name_ru="Безопасник",
description_ru="Проверяет код и данные на уязвимости.",
is_implemented=True,
capabilities=["security-expert", "security", "code-reviewer", "audit"],
fallback_capabilities=["security", "code-reviewer"],
default_preferred_chain=["claude-worker-1", "codex-worker-2", "ag-w2"],
max_failover_attempts=3,
tier="expert",
),
}
_CANONICAL_ROLE_ALIASES: Dict[str, str] = {
"orchestrator": "manager",
"coder-primary": "developer-1",
"coder-secondary": "developer-2",
"reviewer": "code-reviewer",
"research": "researcher",
"fast": "tester",
}
class RoleRegistry:
@classmethod
def get_all_roles(cls) -> Dict[str, RoleDefinition]:
return dict(CANONICAL_ROLES)
@classmethod
def get_role(cls, role_id: str) -> Optional[RoleDefinition]:
canonical_id = cls.resolve_canonical_role(role_id)
return CANONICAL_ROLES.get(canonical_id)
@classmethod
def get_role_ids(cls) -> List[str]:
return list(CANONICAL_ROLES.keys())
@classmethod
def get_executable_role_ids(cls) -> List[str]:
return [r_id for r_id, r_def in CANONICAL_ROLES.items() if r_def.is_implemented]
@classmethod
def get_unimplemented_role_ids(cls) -> List[str]:
return [r_id for r_id, r_def in CANONICAL_ROLES.items() if not r_def.is_implemented]
@classmethod
def is_role_implemented(cls, role_id: str) -> bool:
r_def = cls.get_role(role_id)
return r_def.is_implemented if r_def else False
@classmethod
def get_role_name_ru(cls, role_id: str, default: Optional[str] = None) -> str:
r_def = cls.get_role(role_id)
if r_def:
return r_def.display_name_ru
return default or role_id
@classmethod
def get_role_short_name_ru(cls, role_id: str, default: Optional[str] = None) -> str:
r_def = cls.get_role(role_id)
if r_def:
return r_def.short_name_ru
return default or role_id
@classmethod
def get_role_description_ru(cls, role_id: str) -> str:
r_def = cls.get_role(role_id)
if r_def:
return r_def.description_ru
return ""
@classmethod
def resolve_canonical_role(cls, name_or_alias: str) -> str:
if not name_or_alias:
return "manager"
clean = name_or_alias.strip().lower()
return _CANONICAL_ROLE_ALIASES.get(clean, clean)
@classmethod
def get_canonical_role_map(cls) -> Dict[str, str]:
return dict(_CANONICAL_ROLE_ALIASES)
@classmethod
def get_human_role_labels(cls) -> Dict[str, str]:
labels = {}
for r_id, r_def in CANONICAL_ROLES.items():
labels[r_id] = r_def.short_name_ru
labels.update({
"spare_1": "Резерв 1",
"spare_2": "Резерв 2",
"cold_spare": "Холодный резерв",
})
return labels
@classmethod
def get_default_role_policies(cls) -> Dict[str, "RolePolicy"]:
from antigravity_provider.router.router_config import RolePolicy
policies: Dict[str, "RolePolicy"] = {}
for r_id, r_def in CANONICAL_ROLES.items():
policies[r_id] = RolePolicy(
role_name=r_id,
preferred_chain=list(r_def.default_preferred_chain),
fallback_capabilities=list(r_def.fallback_capabilities),
max_failover_attempts=r_def.max_failover_attempts,
session_affinity_enabled=r_def.session_affinity_enabled,
default_model=r_def.default_model,
)
return policies
@classmethod
def migrate_legacy_roles(cls, current_roles: Dict[str, "RolePolicy"]) -> Tuple[Dict[str, "RolePolicy"], bool]:
from antigravity_provider.router.router_config import RolePolicy
migrated: Dict[str, "RolePolicy"] = {}
was_modified = False
default_policies = cls.get_default_role_policies()
for rname, rpol in current_roles.items():
canonical_id = cls.resolve_canonical_role(rname)
if canonical_id != rname:
was_modified = True
if canonical_id not in migrated:
migrated[canonical_id] = RolePolicy(
role_name=canonical_id,
preferred_chain=list(rpol.preferred_chain),
fallback_capabilities=list(rpol.fallback_capabilities) or list(default_policies[canonical_id].fallback_capabilities),
max_failover_attempts=rpol.max_failover_attempts,
session_affinity_enabled=rpol.session_affinity_enabled,
default_model=rpol.default_model or default_policies[canonical_id].default_model,
)
else:
migrated[rname] = rpol
for canon_id, def_policy in default_policies.items():
if canon_id not in migrated:
migrated[canon_id] = def_policy
was_modified = True
return migrated, was_modified

View file

@ -3,6 +3,7 @@ from __future__ import annotations
import os
from dataclasses import dataclass, field
from antigravity_provider.router.role_registry import RoleRegistry
from pathlib import Path
from typing import Any, Dict, List, Optional
import yaml
@ -272,51 +273,7 @@ def get_default_router_config() -> RouterConfig:
),
}
roles: dict[str, RolePolicy] = {
"orchestrator": RolePolicy(
role_name="orchestrator",
preferred_chain=["codex-orch", "ag-orch-fallback", "opengo-3"],
fallback_capabilities=["orchestrator", "reasoning"],
max_failover_attempts=3,
session_affinity_enabled=True,
default_model="gemini-3.7-flash",
),
"coder-primary": RolePolicy(
role_name="coder-primary",
preferred_chain=["codex-worker-1", "ag-w1", "opengo-3"],
fallback_capabilities=["coding"],
max_failover_attempts=3,
session_affinity_enabled=True,
),
"coder-secondary": RolePolicy(
role_name="coder-secondary",
preferred_chain=["codex-worker-2", "ag-w2", "opengo-2"],
fallback_capabilities=["coding", "reviewer"],
max_failover_attempts=3,
session_affinity_enabled=True,
),
"reviewer": RolePolicy(
role_name="reviewer",
preferred_chain=["codex-worker-2", "opengo-2", "ag-w2"],
fallback_capabilities=["reviewer", "coding"],
max_failover_attempts=3,
session_affinity_enabled=True,
),
"research": RolePolicy(
role_name="research",
preferred_chain=["opengo-1", "ag-w3", "ag-w4"],
fallback_capabilities=["research", "search"],
max_failover_attempts=3,
session_affinity_enabled=True,
),
"fast": RolePolicy(
role_name="fast",
preferred_chain=["opengo-1", "ag-w4", "ag-spare-1"],
fallback_capabilities=["fast"],
max_failover_attempts=3,
session_affinity_enabled=True,
),
}
roles = RoleRegistry.get_default_role_policies()
return RouterConfig(
enabled=True,

View file

@ -8,6 +8,7 @@ Captures, persists, and computes honest empirical measurements for all router ca
- Ring Buffer & File Rotation (bounded memory and disk footprint)
"""
from __future__ import annotations
from antigravity_provider.router.role_registry import RoleRegistry
import collections
import datetime
@ -399,7 +400,7 @@ class TelemetryService:
global_aggs = self.get_aggregates(window_seconds=window_seconds)
providers = set(known_providers or ["antigravity", "openai-codex", "opencode-go"])
roles = set(known_roles or ["orchestrator", "coder-primary", "coder-secondary", "reviewer", "research", "fast"])
roles = set(known_roles or RoleRegistry.get_role_ids())
with self._lock:
for r in self._buffer:

View file

@ -9,6 +9,7 @@ Single Source of Truth for:
- EventLogService
"""
from __future__ import annotations
from antigravity_provider.router.role_registry import RoleRegistry
import json
import logging
@ -644,21 +645,38 @@ class UnifiedHealthService:
config = load_router_config()
self.scan_all(force=False)
ROLE_META = {
"orchestrator": ("Главный оркестратор", "Управление командой, планирование, контроль исполнения"),
"coder-primary": ("Кодер 1", "Основная разработка кода и исправление дефектов"),
"coder-secondary": ("Кодер 2", "Параллельная разработка и вспомогательные модули"),
"reviewer": ("Ревьюер", "Независимое fail-closed ревью и валидация diff"),
"research": ("Исследователь", "Read-only поиск в кодовой базе и сбор фактов"),
"fast": ("Быстрый агент", "Оперативные вызовы, вспомогательные проверки"),
"universal": ("Универсальный агент", "Широкий спектр общих задач"),
}
agents: List[AgentViewModel] = []
for rname, rpol in config.roles.items():
rname_ru, rdesc_ru = ROLE_META.get(rname, (rname, ""))
rname_ru = RoleRegistry.get_role_name_ru(rname, rname)
rdesc_ru = RoleRegistry.get_role_description_ru(rname)
is_implemented = RoleRegistry.is_role_implemented(rname)
chain = rpol.preferred_chain
if not is_implemented:
agents.append(AgentViewModel(
role_id=rname,
role_name_ru=rname_ru,
role_description_ru=rdesc_ru or "Роль объявлена, исполнение не реализовано",
assigned_profile_id=None,
assigned_display_name=None,
provider="N/A",
provider_display_name="N/A",
model="-",
account_identity="Роль объявлена, исполнение не реализовано",
routing_position="Отключено",
status="unimplemented",
status_label_ru="Не реализовано",
is_active=False,
is_main_orchestrator=False,
cooldown_remaining_sec=0,
session_id=None,
active_quota_status="unavailable",
active_quota_label="Не применяется (роль не активна)",
))
continue
if not chain:
continue
@ -703,7 +721,7 @@ class UnifiedHealthService:
status=active_pvm.health_state,
status_label_ru=active_pvm.health_label_ru,
is_active=(active_pvm.health_state == STATUS_HEALTHY),
is_main_orchestrator=(rname == "orchestrator"),
is_main_orchestrator=(rname == "manager"),
cooldown_remaining_sec=active_pvm.cooldown_remaining_sec,
session_id=None,
active_quota_status=active_quota_st,
@ -772,15 +790,7 @@ class UnifiedHealthService:
self.scan_all(force=False)
pipelines: Dict[str, RolePipeline] = {}
ROLE_NAMES = {
"orchestrator": "Главный оркестратор",
"coder-primary": "Кодер 1 (Primary)",
"coder-secondary": "Кодер 2 (Secondary)",
"reviewer": "Ревьюер",
"research": "Исследователь",
"fast": "Быстрый агент",
"universal": "Универсальный агент",
}
for rname, rpol in config.roles.items():
nodes: List[PipelineNode] = []
@ -834,7 +844,7 @@ class UnifiedHealthService:
pipelines[rname] = RolePipeline(
role_id=rname,
role_name_ru=ROLE_NAMES.get(rname, rname),
role_name_ru=RoleRegistry.get_role_name_ru(rname, rname),
default_model=rpol.default_model or "auto",
max_failover=rpol.max_failover_attempts,
session_affinity=rpol.session_affinity_enabled,

View file

@ -62,7 +62,7 @@ class TestA9ConfigMigration(unittest.TestCase):
legacy_cfg = RouterConfig(
enabled=True,
default_role="orchestrator",
default_role="manager",
roles=get_default_router_config().roles,
profiles=legacy_profiles,
)
@ -316,9 +316,9 @@ class TestA9HermesPluginBoundary(unittest.TestCase):
def test_resolve_role_respects_explicit_or_metadata_role(self):
from antigravity_provider.router import get_router_engine
engine = get_router_engine()
self.assertEqual(engine.resolve_role({}, explicit_role="coder-primary"), "coder-primary")
self.assertEqual(engine.resolve_role({"role": "reviewer"}), "reviewer")
self.assertEqual(engine.resolve_role({"metadata": {"role": "research"}}), "research")
self.assertEqual(engine.resolve_role({}, explicit_role="developer-1"), "developer-1")
self.assertEqual(engine.resolve_role({"role": "code-reviewer"}), "code-reviewer")
self.assertEqual(engine.resolve_role({"metadata": {"role": "researcher"}}), "researcher")
class TestA9CodexOAuthTokenRefreshAndSwitching(unittest.TestCase):

View file

@ -89,7 +89,8 @@ def test_single_account_assigned_to_all_six_roles_p0_3():
"""P0-3: A single account can be assigned as primary across all 6 canonical roles without error."""
ProfileAuthManager.save_profile_auth("antigravity", "ag-w1", {"tokens": {"access_token": "token-1"}})
canonical_roles = ["orchestrator", "coder-primary", "coder-secondary", "reviewer", "research", "fast"]
from antigravity_provider.router.role_registry import RoleRegistry
canonical_roles = list(RoleRegistry.get_role_ids())
for rname in canonical_roles:
ok, msg = AutoAssigner.assign_profile_to_role("ag-w1", rname, is_primary=True)
@ -101,7 +102,7 @@ def test_single_account_assigned_to_all_six_roles_p0_3():
assert cfg.roles[rname].preferred_chain[0] == "ag-w1"
# Verify action executor handles assign_role cleanly
res = ActionExecutor.execute("assign_role", {"role_id": "orchestrator", "profile_id": "ag-w1", "is_primary": True})
res = ActionExecutor.execute("assign_role", {"role_id": "manager", "profile_id": "ag-w1", "is_primary": True})
assert res.get("ok") is True
@ -115,8 +116,10 @@ def test_auto_assign_all_single_account_p0_4():
assert result["total_authenticated"] == 1
cfg = load_router_config()
canonical_roles = ["orchestrator", "coder-primary", "coder-secondary", "reviewer", "research", "fast"]
from antigravity_provider.router.role_registry import RoleRegistry
canonical_roles = list(RoleRegistry.get_role_ids())
for rname in canonical_roles:
if RoleRegistry.is_role_implemented(rname):
assert cfg.roles[rname].preferred_chain == ["ag-w1"]
@ -132,11 +135,11 @@ def test_auto_assign_all_two_accounts_same_provider_p0_4():
cfg = load_router_config()
# Primary accounts should alternate between ag-w1 and ag-w2
primaries = [cfg.roles[r].preferred_chain[0] for r in ["orchestrator", "coder-primary", "coder-secondary", "reviewer", "research", "fast"]]
primaries = [cfg.roles[r].preferred_chain[0] for r in ["manager", "developer-1", "developer-2", "code-reviewer", "researcher", "tester"]]
assert "ag-w1" in primaries
assert "ag-w2" in primaries
# Fallback chains should contain the alternate account
for r in ["orchestrator", "coder-primary", "coder-secondary", "reviewer", "research", "fast"]:
for r in ["manager", "developer-1", "developer-2", "code-reviewer", "researcher", "tester"]:
chain = cfg.roles[r].preferred_chain
assert len(chain) == 2
assert set(chain) == {"ag-w1", "ag-w2"}
@ -155,10 +158,10 @@ def test_auto_assign_all_multi_provider_preferences_p0_4():
cfg = load_router_config()
# Orchestrator prefers codex
assert cfg.roles["orchestrator"].preferred_chain[0] == "codex-orch"
assert cfg.roles["manager"].preferred_chain[0] == "codex-orch"
# Research / fast prefers opencode
assert cfg.roles["research"].preferred_chain[0] == "opengo-1"
assert cfg.roles["fast"].preferred_chain[0] == "opengo-1"
assert cfg.roles["researcher"].preferred_chain[0] == "opengo-1"
assert cfg.roles["tester"].preferred_chain[0] == "opengo-1"
@pytest.mark.unit

View file

@ -164,8 +164,8 @@ def test_provider_selection_explanation_and_candidate_matrix(tmp_path, monkeypat
"ag-healthy": RouterProfileConfig(profile_id="ag-healthy", provider="antigravity", enabled=True, preferred_models=["gemini-2.5-pro"]),
},
roles={
"orchestrator": RolePolicy(
role_name="orchestrator",
"manager": RolePolicy(
role_name="manager",
preferred_chain=["ag-disabled", "ag-unauth", "ag-healthy"],
max_failover_attempts=3,
)
@ -188,7 +188,7 @@ def test_provider_selection_explanation_and_candidate_matrix(tmp_path, monkeypat
with patch.object(AntigravityAdapter, "invoke", side_effect=mock_invoke):
res = engine.route_request({"messages": [{"role": "user", "content": "explain"}]}, role="orchestrator")
res = engine.route_request({"messages": [{"role": "user", "content": "explain"}]}, role="manager")
assert "router_metadata" in res
meta = res["router_metadata"]
@ -227,8 +227,8 @@ def test_failover_exhaustion_includes_selection_trace(tmp_path, monkeypatch):
"ag-w1": RouterProfileConfig(profile_id="ag-w1", provider="antigravity", enabled=True, preferred_models=["gemini-2.5-pro"]),
},
roles={
"orchestrator": RolePolicy(
role_name="orchestrator",
"manager": RolePolicy(
role_name="manager",
preferred_chain=["ag-w1"],
max_failover_attempts=1,
)
@ -246,7 +246,7 @@ def test_failover_exhaustion_includes_selection_trace(tmp_path, monkeypatch):
with patch.object(ProfileAuthManager, "get_profile_status", return_value={"authenticated": True, "auth_state": "AUTHENTICATED"}), \
patch.object(AntigravityAdapter, "invoke", side_effect=mock_invoke_fail):
res = engine.route_request({"messages": [{"role": "user", "content": "hello"}]}, role="orchestrator")
res = engine.route_request({"messages": [{"role": "user", "content": "hello"}]}, role="manager")
assert res.get("router_error") is True
assert "selection_trace" in res

View file

@ -69,7 +69,7 @@ def test_provider_call_share_calculation(clean_services):
# 2. Record 45 antigravity calls, 35 codex calls, 20 opencode calls (Total 100)
for _ in range(45):
ts.record_call(
role="orchestrator",
role="manager",
profile_id="ag-orch",
provider="antigravity",
model="gemini-2.5-pro",
@ -81,7 +81,7 @@ def test_provider_call_share_calculation(clean_services):
for _ in range(35):
ts.record_call(
role="coder-primary",
role="developer-1",
profile_id="codex-w1",
provider="openai-codex",
model="gpt-4o",
@ -93,7 +93,7 @@ def test_provider_call_share_calculation(clean_services):
for _ in range(20):
ts.record_call(
role="fast",
role="tester",
profile_id="opengo-1",
provider="opencode-go",
model="deepseek-v4-flash",
@ -122,9 +122,9 @@ def test_provider_call_share_calculation(clean_services):
assert breakdown["by_provider"]["antigravity"]["call_share"] == 0.45
assert breakdown["by_provider"]["openai-codex"]["call_share"] == 0.35
assert breakdown["by_provider"]["opencode-go"]["call_share"] == 0.20
assert breakdown["by_role"]["orchestrator"]["total_calls"] == 45
assert breakdown["by_role"]["coder-primary"]["total_calls"] == 35
assert breakdown["by_role"]["fast"]["total_calls"] == 20
assert breakdown["by_role"]["manager"]["total_calls"] == 45
assert breakdown["by_role"]["developer-1"]["total_calls"] == 35
assert breakdown["by_role"]["tester"]["total_calls"] == 20
@pytest.mark.unit
@ -211,8 +211,8 @@ def test_active_calls_and_hub_snapshot_integration(clean_services, tmp_path, mon
),
},
roles={
"orchestrator": RolePolicy(
role_name="orchestrator",
"manager": RolePolicy(
role_name="manager",
preferred_chain=["ag-w1"],
)
}
@ -227,7 +227,7 @@ def test_active_calls_and_hub_snapshot_integration(clean_services, tmp_path, mon
# Record 1 call
ts.record_call(
role="orchestrator",
role="manager",
profile_id="ag-w1",
provider="antigravity",
model="gemini-2.5-pro",

View file

@ -34,7 +34,7 @@ def isolated_env(tmp_path, monkeypatch):
def test_auth_required_skipped_in_routing(isolated_env):
tmp_path, ht = isolated_env
# Configure 2 profiles for role 'orchestrator'
# Configure 2 profiles for role 'manager'
cfg = RouterConfig(
profiles={
"ag-orch-1": RouterProfileConfig(
@ -49,12 +49,12 @@ def test_auth_required_skipped_in_routing(isolated_env):
),
},
roles={
"orchestrator": RolePolicy(
role_name="orchestrator",
"manager": RolePolicy(
role_name="manager",
preferred_chain=["ag-orch-1", "ag-orch-2"],
),
},
default_role="orchestrator",
default_role="manager",
)
engine = RouterEngine(config=cfg, health=ht)
@ -64,7 +64,7 @@ def test_auth_required_skipped_in_routing(isolated_env):
# Initial state: ag-orch-1 is healthy and selected
with patch.object(AntigravityAdapter, "invoke", side_effect=_mock_invoke):
res = engine.route_request({"prompt": "hi"}, role="orchestrator")
res = engine.route_request({"prompt": "hi"}, role="manager")
assert res.get("router_metadata", {}).get("profile_id") == "ag-orch-1"
# Mark ag-orch-1 as AUTH_REQUIRED
@ -73,7 +73,7 @@ def test_auth_required_skipped_in_routing(isolated_env):
# Routing should skip ag-orch-1 and route to ag-orch-2
with patch.object(AntigravityAdapter, "invoke", side_effect=_mock_invoke):
res = engine.route_request({"prompt": "hi"}, role="orchestrator")
res = engine.route_request({"prompt": "hi"}, role="manager")
assert res.get("router_metadata", {}).get("profile_id") == "ag-orch-2"
@ -94,12 +94,12 @@ def test_save_profile_auth_auto_recovery(isolated_env):
),
},
roles={
"orchestrator": RolePolicy(
role_name="orchestrator",
"manager": RolePolicy(
role_name="manager",
preferred_chain=["ag-orch-1", "ag-orch-2"],
),
},
default_role="orchestrator",
default_role="manager",
)
engine = RouterEngine(config=cfg, health=ht)
@ -128,7 +128,7 @@ def test_save_profile_auth_auto_recovery(isolated_env):
# Router now routes back to primary profile ag-orch-1
with patch.object(AntigravityAdapter, "invoke", side_effect=_mock_invoke):
res = engine.route_request({"prompt": "hi"}, role="orchestrator")
res = engine.route_request({"prompt": "hi"}, role="manager")
assert res.get("router_metadata", {}).get("profile_id") == "ag-orch-1"

View file

@ -254,14 +254,14 @@ class TestLocalLLMConfigAndAutoAssigner:
p1 = cfg.profiles["local-1"]
assert p1.provider == "local"
assert "reviewer" in p1.capabilities
assert "code-reviewer" in p1.capabilities
assert "coding" in p1.capabilities
assert "Qwen3.8-27B-Q4_K_M.gguf" in p1.preferred_models
assert p1.max_concurrency == 1
p2 = cfg.profiles["local-2"]
assert p2.provider == "local"
assert "fast" in p2.capabilities
assert "tester" in p2.capabilities
assert "Qwen3-4B-Instruct-2507-Q4_K_M.gguf" in p2.preferred_models
assert p2.max_concurrency == 1
@ -277,7 +277,7 @@ class TestLocalLLMConfigAndAutoAssigner:
}
legacy_cfg = RouterConfig(
enabled=True,
default_role="orchestrator",
default_role="manager",
roles=get_default_router_config().roles,
profiles=legacy_profiles,
)

View file

@ -54,7 +54,7 @@ def test_set_model_success_and_config_persistence(temp_models_cache):
res = ActionExecutor.execute("set_model", {
"profile_id": profile_id,
"model": target_model,
"role_id": "coder-primary",
"role_id": "developer-1",
})
assert res["ok"] is True
@ -63,7 +63,7 @@ def test_set_model_success_and_config_persistence(temp_models_cache):
# Verify persistence on disk
reloaded = load_router_config()
assert reloaded.profiles[profile_id].preferred_models[0] == target_model
assert reloaded.roles["coder-primary"].default_model == target_model
assert reloaded.roles["developer-1"].default_model == target_model
def test_set_model_rejects_nonexistent_model(temp_models_cache):

View file

@ -45,12 +45,12 @@ def isolated_hub(tmp_path, monkeypatch):
),
},
roles={
"orchestrator": RolePolicy(
role_name="orchestrator",
"manager": RolePolicy(
role_name="manager",
preferred_chain=["ag-orch-1"],
),
},
default_role="orchestrator",
default_role="manager",
)
save_router_config(cfg)
return tmp_path

View file

@ -77,17 +77,17 @@ class TestRouterConfig:
def test_role_policies_chains(self):
config = get_default_router_config()
assert "orchestrator" in config.roles
orch = config.roles["orchestrator"]
assert "manager" in config.roles
orch = config.roles["manager"]
assert orch.preferred_chain == ["codex-orch", "ag-orch-fallback", "opengo-3"]
coder = config.roles["coder-primary"]
coder = config.roles["developer-1"]
assert coder.preferred_chain == ["codex-worker-1", "ag-w1", "opengo-3"]
reviewer = config.roles["reviewer"]
reviewer = config.roles["code-reviewer"]
assert reviewer.preferred_chain == ["codex-worker-2", "opengo-2", "ag-w2"]
research = config.roles["research"]
research = config.roles["researcher"]
assert research.preferred_chain == ["opengo-1", "ag-w3", "ag-w4"]
@ -141,14 +141,14 @@ class TestSessionAffinityAndLeases:
affinity = SessionAffinityTracker()
assert affinity.get_affinity("sess-1") is None
affinity.set_affinity("sess-1", "orchestrator", "codex-orch", "gpt-4o")
affinity.set_affinity("sess-1", "manager", "codex-orch", "gpt-4o")
rec = affinity.get_affinity("sess-1")
assert rec is not None
assert rec.profile_id == "codex-orch"
assert rec.role == "orchestrator"
assert rec.role == "manager"
# Update on failover
affinity.set_affinity("sess-1", "orchestrator", "ag-orch-fallback", "gemini-3.7-flash")
affinity.set_affinity("sess-1", "manager", "ag-orch-fallback", "gemini-3.7-flash")
rec2 = affinity.get_affinity("sess-1")
assert rec2.profile_id == "ag-orch-fallback"
@ -182,14 +182,14 @@ class TestRouterEngineFailover:
# 1. Normal state: codex-orch succeeds
with patch.object(CodexAdapter, "invoke", return_value=mock_codex_response):
resp = engine.route_request({"messages": [{"role": "user", "content": "hello"}]}, role="orchestrator", session_id="sess-orch-1")
resp = engine.route_request({"messages": [{"role": "user", "content": "hello"}]}, role="manager", session_id="sess-orch-1")
assert resp["choices"][0]["message"]["content"] == "from-codex"
assert resp["router_metadata"]["profile_id"] == "codex-orch"
# 2. Simulate quota on codex-orch: should auto-failover to ag-orch-fallback
engine.health.simulate_quota("codex-orch", duration=600)
with patch.object(AntigravityAdapter, "invoke", return_value=mock_ag_response):
resp2 = engine.route_request({"messages": [{"role": "user", "content": "hello again"}]}, role="orchestrator", session_id="sess-orch-2")
resp2 = engine.route_request({"messages": [{"role": "user", "content": "hello again"}]}, role="manager", session_id="sess-orch-2")
assert resp2["choices"][0]["message"]["content"] == "from-antigravity"
assert resp2["router_metadata"]["profile_id"] == "ag-orch-fallback"
assert resp2["router_metadata"]["failover_count"] == 0 # picked directly because codex-orch was marked unhealthy
@ -197,7 +197,7 @@ class TestRouterEngineFailover:
# 3. Simulate quota on both codex-orch and ag-orch-fallback: should failover to opengo-3
engine.health.simulate_quota("ag-orch-fallback", duration=600)
with patch.object(OpenCodeGoAdapter, "invoke", return_value=mock_opengo_response):
resp3 = engine.route_request({"messages": [{"role": "user", "content": "hello third"}]}, role="orchestrator", session_id="sess-orch-3")
resp3 = engine.route_request({"messages": [{"role": "user", "content": "hello third"}]}, role="manager", session_id="sess-orch-3")
assert resp3["choices"][0]["message"]["content"] == "from-opencode"
assert resp3["router_metadata"]["profile_id"] == "opengo-3"
@ -220,13 +220,13 @@ class TestRouterEngineFailover:
# Turn 1: codex-orch fails with quota exhaustion -> failover to ag-orch-fallback
with patch.object(CodexAdapter, "invoke", side_effect=RuntimeError("Quota limit reached")):
with patch.object(AntigravityAdapter, "invoke", return_value=mock_ag):
resp = engine.route_request({"messages": [{"role": "user", "content": "turn 1"}]}, role="orchestrator", session_id="session-user-123")
resp = engine.route_request({"messages": [{"role": "user", "content": "turn 1"}]}, role="manager", session_id="session-user-123")
assert resp["choices"][0]["message"]["content"] == "a1"
assert resp["router_metadata"]["profile_id"] == "ag-orch-fallback"
# Turn 2: same session continues directly on ag-orch-fallback
with patch.object(AntigravityAdapter, "invoke", return_value=mock_ag):
resp2 = engine.route_request({"messages": [{"role": "user", "content": "turn 2"}]}, role="orchestrator", session_id="session-user-123")
resp2 = engine.route_request({"messages": [{"role": "user", "content": "turn 2"}]}, role="manager", session_id="session-user-123")
assert resp2["choices"][0]["message"]["content"] == "a1"
assert resp2["router_metadata"]["profile_id"] == "ag-orch-fallback"
@ -256,9 +256,9 @@ class TestRouterCLI:
rc = print_routing_policy()
assert rc == 0
out = capsys.readouterr().out
assert "orchestrator" in out
assert "coder-primary" in out
assert "reviewer" in out
assert "manager" in out
assert "developer-1" in out
assert "code-reviewer" in out
def test_simulate_quota_cli(self, capsys):
rc = simulate_quota_cli("codex-orch", duration=300)

View file

@ -126,7 +126,7 @@ def test_p0_4_auto_assign_all(tmp_path, monkeypatch):
# Verify only canonical roles exist in config
cfg = load_router_config()
for rname in cfg.roles:
assert rname in {"orchestrator", "coder-primary", "coder-secondary", "reviewer", "research", "fast"}
assert rname in {"manager", "developer-1", "developer-2", "code-reviewer", "researcher", "tester"}
@pytest.mark.unit
@ -148,8 +148,8 @@ def test_p0_5_antigravity_failover_on_quota(tmp_path, monkeypatch):
),
},
roles={
"orchestrator": RolePolicy(
role_name="orchestrator",
"manager": RolePolicy(
role_name="manager",
preferred_chain=["ag-orch-primary", "codex-orch-fallback"],
max_failover_attempts=2,
)
@ -175,7 +175,7 @@ def test_p0_5_antigravity_failover_on_quota(tmp_path, monkeypatch):
with patch.object(AntigravityAdapter, "invoke", side_effect=mock_agy_invoke), \
patch.object(CodexAdapter, "invoke", side_effect=mock_codex_invoke):
res = engine.route_request({"messages": [{"role": "user", "content": "Hello"}]}, role="orchestrator")
res = engine.route_request({"messages": [{"role": "user", "content": "Hello"}]}, role="manager")
# Must receive fallback response, NOT error text as message content!
assert "choices" in res
@ -236,18 +236,18 @@ def test_p0_7_assign_role_action(tmp_path, monkeypatch):
config = get_default_router_config()
save_router_config(config)
# 1. Assign "coder" -> must update "coder-primary"
# 1. Assign "coder" -> must update "developer-1"
ok, msg = AutoAssigner.assign_profile_to_role("ag-w1", "coder", is_primary=True)
assert ok is True
reloaded = load_router_config()
assert reloaded.roles["coder-primary"].preferred_chain[0] == "ag-w1"
assert reloaded.roles["developer-1"].preferred_chain[0] == "ag-w1"
assert "coder" not in reloaded.roles # Must NOT create a non-canonical role
# 2. Assign "researcher" -> must update "research"
# 2. Assign "researcher" -> must update "researcher"
ok, msg = AutoAssigner.assign_profile_to_role("ag-w2", "researcher", is_primary=True)
assert ok is True
reloaded = load_router_config()
assert reloaded.roles["research"].preferred_chain[0] == "ag-w2"
assert reloaded.roles["researcher"].preferred_chain[0] == "ag-w2"
# 3. Unknown role -> must return False and reject
ok, msg = AutoAssigner.assign_profile_to_role("ag-w1", "completely_unknown_role_xyz")
@ -262,7 +262,7 @@ def test_n1_spare_assignment_mode(tmp_path, monkeypatch):
config = get_default_router_config()
# Put ag-w1 in coder-primary
config.roles["coder-primary"].preferred_chain = ["ag-w1", "ag-w2"]
config.roles["developer-1"].preferred_chain = ["ag-w1", "ag-w2"]
save_router_config(config)
# Assign ag-w1 to spare
@ -271,7 +271,7 @@ def test_n1_spare_assignment_mode(tmp_path, monkeypatch):
assert "резерв" in msg.lower() or "spare" in msg.lower()
reloaded = load_router_config()
assert "ag-w1" not in reloaded.roles["coder-primary"].preferred_chain
assert "ag-w1" not in reloaded.roles["developer-1"].preferred_chain
assert "spare" not in reloaded.roles # Canonical role set unchanged
assert reloaded.profiles["ag-w1"].enabled is True
@ -303,12 +303,12 @@ def test_p0_8_wizard_role_application(tmp_path, monkeypatch):
save_router_config(config)
# Apply role
ok, msg = AutoAssigner.assign_profile_to_role("codex-worker-1", "reviewer", is_primary=True)
ok, msg = AutoAssigner.assign_profile_to_role("codex-worker-1", "code-reviewer", is_primary=True)
assert ok is True
reloaded = load_router_config()
assert "codex-worker-1" in reloaded.roles["reviewer"].preferred_chain
assert reloaded.roles["reviewer"].preferred_chain[0] == "codex-worker-1"
assert "codex-worker-1" in reloaded.roles["code-reviewer"].preferred_chain
assert reloaded.roles["code-reviewer"].preferred_chain[0] == "codex-worker-1"
@pytest.mark.unit
@ -392,8 +392,8 @@ def test_r4_settings_runtime_influence(tmp_path, monkeypatch):
"codex-orch-fallback": RouterProfileConfig(profile_id="codex-orch-fallback", provider="openai-codex", enabled=True),
},
roles={
"orchestrator": RolePolicy(
role_name="orchestrator",
"manager": RolePolicy(
role_name="manager",
preferred_chain=["ag-orch-primary", "codex-orch-fallback"],
max_failover_attempts=2,
)
@ -413,7 +413,7 @@ def test_r4_settings_runtime_influence(tmp_path, monkeypatch):
with patch.object(AntigravityAdapter, "invoke", side_effect=mock_agy_quota), \
patch.object(CodexAdapter, "invoke", mock_codex):
res = engine.route_request({"messages": [{"role": "user", "content": "Hello"}]}, role="orchestrator")
res = engine.route_request({"messages": [{"role": "user", "content": "Hello"}]}, role="manager")
assert "error" in res or "choices" in res
# Codex fallback must NOT have been called because auto_failover was False!
assert mock_codex.call_count == 0
@ -425,7 +425,7 @@ def test_r4_settings_runtime_influence(tmp_path, monkeypatch):
with patch.object(AntigravityAdapter, "invoke", side_effect=mock_agy_quota), \
patch.object(CodexAdapter, "invoke", mock_codex):
res = engine.route_request({"messages": [{"role": "user", "content": "Hello"}]}, role="orchestrator")
res = engine.route_request({"messages": [{"role": "user", "content": "Hello"}]}, role="manager")
assert res["choices"][0]["message"]["content"] == "Fallback OK"
assert mock_codex.call_count == 1

View file

@ -91,7 +91,7 @@ def test_event_bus_pub_sub():
# UI thread dispatch with root.after mock
mock_root = MagicMock()
bus.publish_to_ui(mock_root, EVENT_ROUTING_UPDATED, {"role": "coder-primary"})
bus.publish_to_ui(mock_root, EVENT_ROUTING_UPDATED, {"role": "developer-1"})
assert mock_root.after.called
@ -124,16 +124,16 @@ def test_refresh_scheduler_dedup_and_delays():
def test_session_affinity_ttl_and_lru():
tracker = SessionAffinityTracker(ttl_seconds=2, max_entries=3)
tracker.set_affinity("s1", "coder-primary", "ag-w1", "gemini-2.5-pro")
tracker.set_affinity("s2", "coder-primary", "ag-w2", "gemini-2.5-pro")
tracker.set_affinity("s3", "reviewer", "codex-w1", "gpt-4o")
tracker.set_affinity("s1", "developer-1", "ag-w1", "gemini-2.5-pro")
tracker.set_affinity("s2", "developer-1", "ag-w2", "gemini-2.5-pro")
tracker.set_affinity("s3", "code-reviewer", "codex-w1", "gpt-4o")
assert tracker.get_affinity("s1") is not None
assert tracker.get_affinity("s2") is not None
assert tracker.get_affinity("s3") is not None
# Exceed capacity -> triggers LRU eviction
tracker.set_affinity("s4", "fast", "ag-w3", "gemini-2.5-flash")
tracker.set_affinity("s4", "tester", "ag-w3", "gemini-2.5-flash")
assert len(tracker._sessions) <= 3
# Test TTL expiration
@ -154,7 +154,7 @@ def test_model_registry_capability_filtering():
assert m_gemini_flash is not None
# Reviewer requires security_analysis and coding
req_reviewer = reg.get_role_requirements("reviewer")
req_reviewer = reg.get_role_requirements("code-reviewer")
ok_pro, score_pro, _ = reg.evaluate_model_score(m_gemini_pro, req_reviewer)
ok_flash, score_flash, reason_flash = reg.evaluate_model_score(m_gemini_flash, req_reviewer)
@ -163,7 +163,7 @@ def test_model_registry_capability_filtering():
assert ok_flash is False
# Fast role prioritizes latency
req_fast = reg.get_role_requirements("fast")
req_fast = reg.get_role_requirements("tester")
ok_f_flash, score_f_flash, _ = reg.evaluate_model_score(m_gemini_flash, req_fast)
ok_f_pro, score_f_pro, _ = reg.evaluate_model_score(m_gemini_pro, req_fast)
@ -174,7 +174,7 @@ def test_model_registry_capability_filtering():
# ── TEST 6: RouterEngine Dynamic Selection Trace & Same-Account Fallback ──
def test_router_engine_selection_trace_and_same_account_fallback():
cfg = RouterConfig(
default_role="coder-primary",
default_role="developer-1",
profiles={
"ag-w1": RouterProfileConfig(
profile_id="ag-w1",
@ -190,8 +190,8 @@ def test_router_engine_selection_trace_and_same_account_fallback():
),
},
roles={
"coder-primary": RolePolicy(
role_name="coder-primary",
"developer-1": RolePolicy(
role_name="developer-1",
preferred_chain=["ag-w1", "codex-w1"],
),
},
@ -205,10 +205,10 @@ def test_router_engine_selection_trace_and_same_account_fallback():
mock_adapter.invoke.return_value = {"content": "code generated", "model": "google-antigravity/gemini-2.5-pro"}
mock_adapter_getter.return_value = mock_adapter
res = engine.route_request({"messages": [{"role": "user", "content": "hello"}]}, role="coder-primary")
res = engine.route_request({"messages": [{"role": "user", "content": "hello"}]}, role="developer-1")
assert "router_metadata" in res
meta = res["router_metadata"]
assert meta["role"] == "coder-primary"
assert meta["role"] == "developer-1"
assert meta["profile_id"] == "ag-w1"
assert "selection_trace" in meta
assert meta["selection_trace"]["selected_model"] == "google-antigravity/gemini-2.5-pro"

View file

@ -39,7 +39,7 @@ def test_telemetry_recording_latency_tokens_outcome(temp_telemetry_service):
svc = temp_telemetry_service
rec = svc.record_call(
role="orchestrator",
role="manager",
profile_id="ag-orch",
provider="antigravity",
model="gemini-2.5-pro",
@ -51,7 +51,7 @@ def test_telemetry_recording_latency_tokens_outcome(temp_telemetry_service):
failover_count=0,
)
assert rec.role == "orchestrator"
assert rec.role == "manager"
assert rec.profile_id == "ag-orch"
assert rec.provider == "antigravity"
assert rec.model == "gemini-2.5-pro"
@ -82,7 +82,7 @@ def test_telemetry_aggregates_calculation(temp_telemetry_service):
outcome = "success" if i <= 8 else "error"
error_cat = None if outcome == "success" else "quota_exhausted"
svc.record_call(
role="coder-primary",
role="developer-1",
profile_id="codex-w1",
provider="openai-codex",
model="gpt-4o",
@ -141,7 +141,7 @@ def test_telemetry_no_tokens_when_usage_missing(temp_telemetry_service):
svc = temp_telemetry_service
rec = svc.record_call(
role="fast",
role="tester",
profile_id="opengo-1",
provider="opencode-go",
model="deepseek-v4-flash",
@ -170,7 +170,7 @@ def test_telemetry_no_secrets_and_no_request_response_content(temp_telemetry_ser
# Record a normal call
rec = svc.record_call(
role="orchestrator",
role="manager",
profile_id="ag-w1",
provider="antigravity",
model="gemini-2.5-pro",
@ -228,7 +228,7 @@ def test_telemetry_cost_calculation_with_and_without_pricing(temp_telemetry_serv
# 1. Without pricing -> cost_usd is None
rec1 = svc.record_call(
role="coder-primary",
role="developer-1",
profile_id="ag-w1",
provider="antigravity",
model="gemini-2.5-pro",
@ -245,7 +245,7 @@ def test_telemetry_cost_calculation_with_and_without_pricing(temp_telemetry_serv
})
rec2 = svc.record_call(
role="coder-primary",
role="developer-1",
profile_id="ag-w1",
provider="antigravity",
model="gemini-2.5-pro",
@ -279,8 +279,8 @@ def test_router_engine_records_telemetry_end_to_end(tmp_path, monkeypatch):
),
},
roles={
"orchestrator": RolePolicy(
role_name="orchestrator",
"manager": RolePolicy(
role_name="manager",
preferred_chain=["ag-w1"],
)
}
@ -304,7 +304,7 @@ def test_router_engine_records_telemetry_end_to_end(tmp_path, monkeypatch):
with patch.object(TelemetryService, "get", return_value=svc), \
patch.object(AntigravityAdapter, "invoke", side_effect=mock_invoke):
res = engine.route_request({"messages": [{"role": "user", "content": "hello"}]}, role="orchestrator")
res = engine.route_request({"messages": [{"role": "user", "content": "hello"}]}, role="manager")
assert "router_metadata" in res
aggs = svc.get_aggregates()

View file

@ -32,7 +32,7 @@ def temp_router_config(tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
# Initialize sample profiles and roles
config = RouterConfig(
enabled=True,
default_role="orchestrator",
default_role="manager",
profiles={
"ag-w1": RouterProfileConfig(
profile_id="ag-w1",
@ -60,13 +60,13 @@ def temp_router_config(tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
),
},
roles={
"coder-primary": RolePolicy(
role_name="coder-primary",
"developer-1": RolePolicy(
role_name="developer-1",
preferred_chain=["ag-w1", "ag-w2", "op-1"],
default_model="gemini-2.5-pro",
),
"coder-secondary": RolePolicy(
role_name="coder-secondary",
"developer-2": RolePolicy(
role_name="developer-2",
preferred_chain=["ag-w2", "op-1"],
default_model="gemini-2.5-flash",
),
@ -79,14 +79,14 @@ def temp_router_config(tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
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("coder-primary", new_chain)
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["coder-primary"]
role_cfg = reloaded.roles["developer-1"]
assert role_cfg.preferred_chain == new_chain
assert role_cfg.preferred_chain[0] == "op-1"
@ -98,18 +98,18 @@ def test_persist_role_chain_canonical_name_mapping(temp_router_config: Path):
assert ok is True
reloaded = load_router_config()
assert reloaded.roles["coder-primary"].preferred_chain == new_chain
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("coder-primary", ["ag-w1", "non-existent-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("coder-primary", ["ag-w1", "ag-w1"])
ok, msg = AutoAssigner.persist_role_chain("developer-1", ["ag-w1", "ag-w1"])
assert ok is False
assert "повторяться" in msg or "дублир" in msg
@ -124,20 +124,20 @@ def test_action_executor_save_chain(temp_router_config: Path):
executor = ActionExecutor()
# save_chain
res1 = executor.execute("save_chain", {"role_id": "coder-primary", "chain": ["ag-w2", "ag-w1"]})
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": "coder-primary", "desired_chain": ["op-1", "ag-w1"]})
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": "coder-secondary", "chain": ["op-1", "ag-w2"]})
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["coder-primary"].preferred_chain == ["op-1", "ag-w1"]
assert reloaded.roles["coder-secondary"].preferred_chain == ["op-1", "ag-w2"]
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):
@ -147,13 +147,13 @@ def test_action_executor_set_model_updates_profile_and_role(temp_router_config:
res = executor.execute("set_model", {
"profile_id": "ag-w1",
"model": "gemini-3.1-pro",
"role_id": "coder-primary",
"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["coder-primary"].default_model == "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():

View file

@ -177,16 +177,16 @@ def test_route_and_agent_delta_events():
bus.subscribe("AGENT_UPDATED", _on_agent)
try:
store.apply_delta_route_changed("coder-primary", "ag-w1", failover_reason="Testing failover")
store.apply_delta_route_changed("developer-1", "ag-w1", failover_reason="Testing failover")
assert len(route_events) >= 1
assert route_events[-1]["role_id"] == "coder-primary"
assert route_events[-1]["role_id"] == "developer-1"
assert route_events[-1]["failover_reason"] == "Testing failover"
assert "generation" in route_events[-1]
assert "seq" in route_events[-1]
assert len(agent_events) >= 1
assert agent_events[-1]["role_id"] == "coder-primary"
assert agent_events[-1]["agent"].role_id == "coder-primary"
assert agent_events[-1]["role_id"] == "developer-1"
assert agent_events[-1]["agent"].role_id == "developer-1"
finally:
bus.unsubscribe(EVENT_ROUTING_UPDATED, _on_route)
bus.unsubscribe("AGENT_UPDATED", _on_agent)

View file

@ -159,7 +159,7 @@ def test_single_scheduler_refresh_waits_for_quota_before_delta(monkeypatch: pyte
def test_model_score_rejects_exhausted_pool_and_prefers_more_quota() -> None:
registry = ModelRegistry.get()
descriptor = registry.get_model("gemini-2.5-pro")
requirements = registry.get_role_requirements("research")
requirements = registry.get_role_requirements("researcher")
assert descriptor is not None
ok_empty, _, reason = registry.evaluate_model_score(

View file

@ -34,7 +34,7 @@ def test_grok_wizard_definition_and_routing_flow():
"""Verify Grok profile definition and routing pipeline addition."""
config = RouterConfig(
profiles={},
roles={"coder-primary": RolePolicy(role_name="coder-primary", preferred_chain=[])},
roles={"developer-1": RolePolicy(role_name="developer-1", preferred_chain=[])},
)
with patch("antigravity_provider.router.auto_assigner.load_router_config", return_value=config), \
patch("antigravity_provider.router.auto_assigner.save_router_config", return_value=True), \
@ -47,7 +47,7 @@ def test_grok_wizard_definition_and_routing_flow():
ok_route, msg_route = ensure_profile_in_routing("grok-worker-1")
assert ok_route, f"Routing failed: {msg_route}"
assert "grok-worker-1" in config.roles["coder-primary"].preferred_chain
assert "grok-worker-1" in config.roles["developer-1"].preferred_chain
@pytest.mark.unit
@ -55,7 +55,7 @@ def test_claude_wizard_definition_and_routing_flow():
"""Verify Claude profile definition and routing pipeline addition."""
config = RouterConfig(
profiles={},
roles={"orchestrator": RolePolicy(role_name="orchestrator", preferred_chain=[])},
roles={"manager": RolePolicy(role_name="manager", preferred_chain=[])},
)
with patch("antigravity_provider.router.auto_assigner.load_router_config", return_value=config), \
patch("antigravity_provider.router.auto_assigner.save_router_config", return_value=True), \
@ -68,7 +68,7 @@ def test_claude_wizard_definition_and_routing_flow():
ok_route, msg_route = ensure_profile_in_routing("claude-orch")
assert ok_route, f"Routing failed: {msg_route}"
assert "claude-orch" in config.roles["orchestrator"].preferred_chain
assert "claude-orch" in config.roles["manager"].preferred_chain
@pytest.mark.unit

View file

@ -43,8 +43,8 @@ def _profile(plan_code: str = "PRO", plan_source: str = "provider_api") -> Profi
account_identity="user@example.test",
provider="antigravity",
provider_display_name="Google Antigravity",
assigned_roles=["coder-primary"],
primary_role="coder-primary",
assigned_roles=["developer-1"],
primary_role="developer-1",
is_main_account=True,
is_main_orchestrator=False,
auth_state="AUTHENTICATED",
@ -143,7 +143,7 @@ def test_quota_missing_is_not_rendered_as_zero_and_reason_is_visible(ui_root) ->
@pytest.mark.ui
def test_agent_quota_and_failover_reason_are_bound_to_their_models(ui_root) -> None:
agent = AgentViewModel(
role_id="coder-primary",
role_id="developer-1",
role_name_ru="Кодер 1",
role_description_ru="Основной кодер",
assigned_profile_id="account-2",
@ -163,7 +163,7 @@ def test_agent_quota_and_failover_reason_are_bound_to_their_models(ui_root) -> N
)
team_card = AgentCardWidget(ui_root)
pipeline = RolePipeline(
role_id="coder-primary",
role_id="developer-1",
role_name_ru="Кодер 1",
default_model="gpt-5",
max_failover=2,
@ -214,7 +214,7 @@ def test_agent_quota_and_failover_reason_are_bound_to_their_models(ui_root) -> N
def test_dashboard_agent_quota_measurement_drives_progress_percent() -> None:
agent = AgentViewModel(
role_id="coder-primary",
role_id="developer-1",
role_name_ru="Кодер 1",
role_description_ru="Основной кодер",
assigned_profile_id="ag-w1",

View file

@ -130,7 +130,7 @@ def test_restored_account_buttons_invoke_each_action(ui_root) -> None:
@pytest.mark.ui
def test_assign_role_error_stays_visible_in_open_modal(ui_root, monkeypatch) -> None:
policy = SimpleNamespace(preferred_chain=[])
monkeypatch.setattr(app_module, "load_router_config", lambda: SimpleNamespace(roles={"orchestrator": policy}))
monkeypatch.setattr(app_module, "load_router_config", lambda: SimpleNamespace(roles={"manager": policy}))
monkeypatch.setattr(
app_module.AutoAssigner,
"assign_profile_to_role",

View file

@ -37,8 +37,8 @@ def _profile(index: int) -> ProfileViewModel:
account_identity=f"user-{index}@example.test",
provider="antigravity",
provider_display_name="Google Antigravity",
assigned_roles=["coder" if index else "orchestrator"],
primary_role="coder" if index else "orchestrator",
assigned_roles=["coder" if index else "manager"],
primary_role="coder" if index else "manager",
is_main_account=index == 0,
is_main_orchestrator=index == 0,
auth_state="AUTHENTICATED",

View file

@ -29,12 +29,12 @@ def _config():
"coder": SimpleNamespace(provider="antigravity", preferred_models=["gemini"]),
}
roles = {
"orchestrator": SimpleNamespace(preferred_chain=["orch"]),
"coder-primary": SimpleNamespace(preferred_chain=["coder"]),
"coder-secondary": SimpleNamespace(preferred_chain=["coder"]),
"reviewer": SimpleNamespace(preferred_chain=["coder"]),
"research": SimpleNamespace(preferred_chain=["coder"]),
"fast": SimpleNamespace(preferred_chain=["coder"]),
"manager": SimpleNamespace(preferred_chain=["orch"]),
"developer-1": SimpleNamespace(preferred_chain=["coder"]),
"developer-2": SimpleNamespace(preferred_chain=["coder"]),
"code-reviewer": SimpleNamespace(preferred_chain=["coder"]),
"researcher": SimpleNamespace(preferred_chain=["coder"]),
"tester": SimpleNamespace(preferred_chain=["coder"]),
}
return SimpleNamespace(roles=roles, profiles=profiles)
@ -83,16 +83,16 @@ def test_graph_layout_zoom_and_viewport_survive_restart(tmp_path):
def test_validation_finds_cycle_unreachable_and_missing_profile():
config = _config()
config.roles["reviewer"].preferred_chain = ["ghost"]
config.roles["code-reviewer"].preferred_chain = ["ghost"]
graph = RoutingGraph(
nodes=[
GraphNode("orchestrator", 0, 0),
GraphNode("coder-primary", 1, 0),
GraphNode("reviewer", 2, 0),
GraphNode("manager", 0, 0),
GraphNode("developer-1", 1, 0),
GraphNode("code-reviewer", 2, 0),
],
edges=[
GraphEdge("orchestrator", "coder-primary"),
GraphEdge("coder-primary", "orchestrator"),
GraphEdge("manager", "developer-1"),
GraphEdge("developer-1", "manager"),
],
)
codes = {issue.code for issue in validate_graph(graph, config)}
@ -108,16 +108,16 @@ def test_profile_edge_updates_yaml_via_auto_assigner(monkeypatch, tmp_path):
lambda profile, role, is_primary: calls.append((profile, role, is_primary)) or (True, "ok"),
)
controller = RoutingGraphController(RoutingGraphStore(tmp_path / "graph.json"))
ok, _message = controller.add_edge("orchestrator", "coder-primary", "FALLBACK", "orch")
ok, _message = controller.add_edge("manager", "developer-1", "FALLBACK", "orch")
assert ok
assert calls == [("orch", "coder-primary", False)]
assert calls == [("orch", "developer-1", False)]
def test_undo_redo_and_dirty_state(monkeypatch, tmp_path):
monkeypatch.setattr(graph_module, "load_router_config", _config)
controller = RoutingGraphController(RoutingGraphStore(tmp_path / "graph.json"))
original = controller.graph.nodes[0].x
controller.move_node("orchestrator", original + 100, 50)
controller.move_node("manager", original + 100, 50)
assert controller.dirty
assert controller.undo()
assert controller.graph.nodes[0].x == original
@ -255,8 +255,8 @@ def test_role_chain_order_and_removal_persist_through_auto_assigner(monkeypatch)
config = SimpleNamespace(
profiles={key: SimpleNamespace() for key in ("a", "b", "c")},
roles={
"orchestrator": SimpleNamespace(preferred_chain=["a", "b", "c"]),
"reviewer": SimpleNamespace(preferred_chain=["b"]),
"manager": SimpleNamespace(preferred_chain=["a", "b", "c"]),
"code-reviewer": SimpleNamespace(preferred_chain=["b"]),
},
)
calls = []
@ -279,11 +279,11 @@ def test_role_chain_order_and_removal_persist_through_auto_assigner(monkeypatch)
monkeypatch.setattr(team_module, "load_router_config", lambda: config)
monkeypatch.setattr(team_module.AutoAssigner, "assign_profile_to_role", assign)
ok, _message = team_module.persist_role_chain("orchestrator", ["c", "a"])
ok, _message = team_module.persist_role_chain("manager", ["c", "a"])
assert ok
assert config.roles["orchestrator"].preferred_chain == ["c", "a"]
assert config.roles["reviewer"].preferred_chain == ["b"]
assert config.roles["manager"].preferred_chain == ["c", "a"]
assert config.roles["code-reviewer"].preferred_chain == ["b"]
assert ("b", "spare", False) in calls

View file

@ -96,7 +96,7 @@ def test_main_account_vs_orchestrator_separation():
service.scan_all()
agents = service.get_agent_view_models()
orch_agent = next((a for a in agents if a.role_id == "orchestrator"), None)
orch_agent = next((a for a in agents if a.role_id == "manager"), None)
assert orch_agent is not None
assert orch_agent.is_main_orchestrator is True