fix(installer): установка на Windows падала с кодом 12 из-за устаревшей проверки
Владелец получил «Ошибка установки (Код: 12)». Код 12 — провал скрипта scripts/verify_multi_provider_router.py, который виндовый установщик запускает после развёртывания. На Linux он не запускается, поэтому там всё вставало. Скрипт пережил три изменения продукта и не был под них обновлён: 1. Требовал роль "orchestrator". A28 переименовал её в "manager", и проверка падала на первом же шаге. Теперь актуальное имя спрашивается у реестра ролей, а не помнится в скрипте. 2. Требовал непустую цепочку у КАЖДОЙ роли. A28 добавил роли, объявленные без реализации — guardian и cost-controller, — у них аккаунтов ещё нет. Установка падала из-за роли, которой никто не пользуется. Теперь пустая цепочка допустима и лишь отмечается, а обязательна она только у оркестрирующей роли: без неё маршрутизация действительно не работает. 3. Зашивал порядок цепочки codex -> antigravity -> opengo-3 и конкретные идентификаторы профилей. Но порядок — выбор владельца, он меняет его мышью, и любая перестановка роняла установку. Проверка переписана на механизм: берётся настоящая цепочка, роняются все профили кроме последнего достижимого, и проверяется, что маршрутизатор дошёл именно до него. Учтён предел max_failover_attempts — за него цепочка не проходится. Карта адаптеров дополнена claude, grok и local: раньше в ней были только три провайдера, и хвост цепочки из остальных не покрывался. Проверено на конфигурации владельца: 10/10 CHECKS PASSED, код возврата 0. 486 passed, ruff чисто. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
d4c4facc94
commit
380c218547
1 changed files with 84 additions and 19 deletions
|
|
@ -6,6 +6,7 @@ import os
|
|||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
from contextlib import ExitStack
|
||||
from unittest.mock import patch
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||
|
|
@ -30,6 +31,9 @@ from antigravity_provider.router.router_engine import RouterEngine, get_router_e
|
|||
from antigravity_provider.router.adapters.antigravity_adapter import AntigravityAdapter, get_profile_env_dir
|
||||
from antigravity_provider.router.adapters.codex_adapter import CodexAdapter
|
||||
from antigravity_provider.router.adapters.opencode_adapter import OpenCodeGoAdapter
|
||||
from antigravity_provider.router.adapters.claude_adapter import ClaudeAdapter
|
||||
from antigravity_provider.router.adapters.grok_adapter import GrokAdapter
|
||||
from antigravity_provider.router.adapters.local_adapter import LocalLLMAdapter
|
||||
|
||||
|
||||
def run_checks() -> int:
|
||||
|
|
@ -61,17 +65,43 @@ def run_checks() -> int:
|
|||
|
||||
# 2. Role Fallback Chains
|
||||
print("2. Checking role fallback policies...")
|
||||
assert "orchestrator" in config.roles, "Роль orchestrator отсутствует"
|
||||
# Имя оркестрирующей роли меняется вместе с реестром: в A28 orchestrator
|
||||
# стал manager. Дословная проверка старого имени пережила миграцию и
|
||||
# роняла установку на Windows с кодом 12 — на Linux этот скрипт не
|
||||
# запускается, поэтому там всё ставилось. Спрашиваем актуальное имя у
|
||||
# реестра, а не помним его в скрипте.
|
||||
assert config.roles, "В конфигурации нет ни одной роли"
|
||||
try:
|
||||
from antigravity_provider.router.role_registry import RoleRegistry
|
||||
|
||||
orchestrating_role = RoleRegistry.resolve_canonical_role("orchestrator")
|
||||
except Exception:
|
||||
orchestrating_role = "orchestrator"
|
||||
assert orchestrating_role in config.roles, (
|
||||
f"Оркестрирующая роль {orchestrating_role!r} отсутствует; есть: {sorted(config.roles)}"
|
||||
)
|
||||
# Цепочки настраиваются владельцем и меняются — дословно их сверять нельзя.
|
||||
# Проверяем то, что действительно ломает маршрутизацию: цепочка непуста и
|
||||
# каждый профиль в ней существует.
|
||||
# Пустая цепочка — не поломка сама по себе. В A28 появились роли,
|
||||
# объявленные без реализации (guardian, cost-controller): аккаунтов у них
|
||||
# ещё нет, и требовать цепочку — значит ронять установку из-за роли,
|
||||
# которой никто не пользуется. Ломает маршрутизацию другое: ссылка на
|
||||
# несуществующий профиль и пустая цепочка у ОРКЕСТРИРУЮЩЕЙ роли.
|
||||
empty_chains = []
|
||||
for role_name, policy in config.roles.items():
|
||||
chain = policy.preferred_chain or []
|
||||
assert chain, f"У роли {role_name} пустая цепочка отказоустойчивости"
|
||||
if not chain:
|
||||
empty_chains.append(role_name)
|
||||
for pid in chain:
|
||||
assert pid in config.profiles, (
|
||||
f"Роль {role_name} ссылается на несуществующий профиль {pid}"
|
||||
)
|
||||
assert config.roles[orchestrating_role].preferred_chain, (
|
||||
f"У оркестрирующей роли {orchestrating_role!r} пустая цепочка — маршрутизация работать не будет"
|
||||
)
|
||||
if empty_chains:
|
||||
print(f" [INFO] Без аккаунтов пока: {', '.join(sorted(empty_chains))}")
|
||||
print(f" [PASS] Цепочки {len(config.roles)} ролей ссылаются только на существующие профили")
|
||||
passed += 1
|
||||
|
||||
|
|
@ -151,26 +181,61 @@ def run_checks() -> int:
|
|||
engine = RouterEngine(config=config)
|
||||
engine.health.clear_cooldown()
|
||||
|
||||
mock_codex = {"id": "c1", "choices": [{"message": {"role": "assistant", "content": "from-codex"}}]}
|
||||
mock_ag = {"id": "a1", "choices": [{"message": {"role": "assistant", "content": "from-antigravity"}}]}
|
||||
mock_opengo = {"id": "o1", "choices": [{"message": {"role": "assistant", "content": "from-opencode"}}]}
|
||||
# Проверяется МЕХАНИЗМ отказоустойчивости, а не расстановка аккаунтов.
|
||||
#
|
||||
# Прежняя версия зашивала порядок codex -> antigravity -> opengo-3 и
|
||||
# конкретные идентификаторы профилей. Но порядок в цепочке — это выбор
|
||||
# владельца, он его меняет мышью в интерфейсе. Любая перестановка роняла
|
||||
# проверку, а с ней и установку на Windows с кодом 12.
|
||||
#
|
||||
# Здесь: берём настоящую цепочку оркестрирующей роли, роняем все профили
|
||||
# кроме последнего и убеждаемся, что маршрутизатор дошёл именно до него.
|
||||
# Учитываем предел попыток: если цепочка длиннее, до её хвоста
|
||||
# маршрутизатор просто не дойдёт, и ожидать этого нельзя.
|
||||
full_chain = list(config.roles[orchestrating_role].preferred_chain)
|
||||
max_attempts = getattr(config.roles[orchestrating_role], "max_failover_attempts", 0) or len(full_chain)
|
||||
chain = full_chain[:max_attempts]
|
||||
assert len(chain) >= 2, (
|
||||
f"В цепочке роли {orchestrating_role!r} меньше двух профилей — "
|
||||
"отказоустойчивость проверить нечем"
|
||||
)
|
||||
last_pid = chain[-1]
|
||||
last_provider = config.profiles[last_pid].provider
|
||||
|
||||
# Simulate codex failure -> route to Antigravity fallback
|
||||
with patch.object(CodexAdapter, "invoke", side_effect=RuntimeError("Insufficient quota")):
|
||||
with patch.object(AntigravityAdapter, "invoke", return_value=mock_ag):
|
||||
res = engine.route_request({"messages": [{"role": "user", "content": "test"}]}, role="orchestrator", session_id="s1")
|
||||
assert res["choices"][0]["message"]["content"] == "from-antigravity"
|
||||
assert res["router_metadata"]["profile_id"] == "ag-orch-fallback"
|
||||
adapter_by_provider = {
|
||||
"openai-codex": CodexAdapter,
|
||||
"antigravity": AntigravityAdapter,
|
||||
"opencode-go": OpenCodeGoAdapter,
|
||||
"claude": ClaudeAdapter,
|
||||
"grok": GrokAdapter,
|
||||
"local": LocalLLMAdapter,
|
||||
}
|
||||
|
||||
# Simulate both codex and ag failure -> route to OpenCode Go
|
||||
with patch.object(CodexAdapter, "invoke", side_effect=RuntimeError("Insufficient quota")):
|
||||
with patch.object(AntigravityAdapter, "invoke", side_effect=RuntimeError("Individual quota reached")):
|
||||
with patch.object(OpenCodeGoAdapter, "invoke", return_value=mock_opengo):
|
||||
res2 = engine.route_request({"messages": [{"role": "user", "content": "test2"}]}, role="orchestrator", session_id="s2")
|
||||
assert res2["choices"][0]["message"]["content"] == "from-opencode"
|
||||
assert res2["router_metadata"]["profile_id"] == "opengo-3"
|
||||
expected = {"id": "ok", "choices": [{"message": {"role": "assistant", "content": "from-last-in-chain"}}]}
|
||||
failing = {cls for pid in chain[:-1]
|
||||
if (cls := adapter_by_provider.get(config.profiles[pid].provider)) is not None}
|
||||
winner = adapter_by_provider.get(last_provider)
|
||||
|
||||
print(" [PASS] 3-tier role failover chain (Codex -> Antigravity -> OpenCode Go) verified")
|
||||
if winner is None or winner in failing:
|
||||
print(f" [SKIP] Последний профиль цепочки ({last_pid}, {last_provider}) "
|
||||
"не покрыт заглушками адаптеров")
|
||||
else:
|
||||
with ExitStack() as stack:
|
||||
for cls in failing:
|
||||
stack.enter_context(patch.object(cls, "invoke", side_effect=RuntimeError("Insufficient quota")))
|
||||
stack.enter_context(patch.object(winner, "invoke", return_value=expected))
|
||||
res = engine.route_request(
|
||||
{"messages": [{"role": "user", "content": "test"}]},
|
||||
role=orchestrating_role,
|
||||
session_id="verify-failover",
|
||||
)
|
||||
assert res["choices"][0]["message"]["content"] == "from-last-in-chain", (
|
||||
f"Отказоустойчивость не дошла до последнего профиля цепочки: {res}"
|
||||
)
|
||||
assert res["router_metadata"]["profile_id"] == last_pid, (
|
||||
f"Ожидался профиль {last_pid}, получен {res['router_metadata']['profile_id']}"
|
||||
)
|
||||
print(f" [PASS] Отказоустойчивость прошла цепочку {' -> '.join(chain)}")
|
||||
passed += 1
|
||||
|
||||
# 10. Passthrough & Graceful fallback
|
||||
|
|
|
|||
Loading…
Reference in a new issue