fix(installer): ошибка 12 при установке — проверка требовала конфигурацию от 20 августа

Владелец получил «Ошибка установки (Код: 12)» на чистой машине.

Причина: scripts/verify_multi_provider_router.py, который установщик
запускает после развёртывания, требовал РОВНО 16 профилей и дословно
заданные цепочки ролей. Миграция из A9 законно доводит конфигурацию до
22 профилей, добавляя claude и grok. Проверено: скрипт падал с
«Expected 16 profiles, got 22», то есть установка обрывалась на любой
машине, где миграция отработала.

Проверки переписаны структурными: есть ли профили у каждого провайдера,
непусты ли цепочки ролей и ссылаются ли они только на существующие
профили. Смысл проверки — работоспособна ли маршрутизация, а не совпадает
ли конфигурация с зафиксированной когда-то. Скрипт проходит 10/10.

Отдельно: код 12 возвращался и при отказе проверки, и из общего catch —
владелец видел число без причины. Непредвиденный сбой отделён в код 15,
обе ветки теперь пишут пояснение в интерфейс установщика.

Тесты: 373 passed, ruff чисто. Установщик пересобран.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Hermes Team 2026-08-24 09:35:36 +07:00
parent 8f62adb760
commit 36b449bc6c
5 changed files with 37 additions and 16 deletions

View file

@ -396,6 +396,12 @@ namespace HermesHubSetup
p.WaitForExit(10000); p.WaitForExit(10000);
if (p.ExitCode != 0) if (p.ExitCode != 0)
{ {
// Код 12 сам по себе ничего не объясняет: раньше он
// возвращался и отсюда, и из общего catch. Владелец
// видел «Ошибка установки (Код: 12)» без причины.
if (progressCallback != null)
progressCallback("Проверка маршрутизации не прошла. Подробности: "
+ verifyScript, 0);
return 12; // Router Verification failed return 12; // Router Verification failed
} }
} }
@ -406,8 +412,11 @@ namespace HermesHubSetup
} }
catch (Exception ex) catch (Exception ex)
{ {
if (progressCallback != null) progressCallback("Error: " + ex.Message, 0); // Отдельный код для непредвиденного сбоя: смешивать его с
return 12; // отказом проверки маршрутизации значит лишать владельца
// возможности отличить одно от другого.
if (progressCallback != null) progressCallback("Ошибка установки: " + ex.Message, 0);
return 15;
} }
} }

Binary file not shown.

Binary file not shown.

View file

@ -43,24 +43,36 @@ def run_checks() -> int:
# 1. Config inventory # 1. Config inventory
print("1. Checking profile inventory and provider counts...") print("1. Checking profile inventory and provider counts...")
config = get_default_router_config() config = get_default_router_config()
assert len(config.profiles) == 16, f"Expected 16 profiles, got {len(config.profiles)}" # Проверка структурная, а не пересчёт. Раньше здесь стояло «ровно 16
codex_count = sum(1 for p in config.profiles.values() if p.provider == "openai-codex") # профилей» и дословные цепочки от 20 августа. Миграция законно довела
ag_count = sum(1 for p in config.profiles.values() if p.provider == "antigravity") # конфигурацию до 22 профилей, добавив claude и grok, — и установка стала
opengo_count = sum(1 for p in config.profiles.values() if p.provider == "opencode-go") # падать с кодом 12 на любой машине, где миграция отработала. Смысл этой
assert codex_count == 3, f"Expected 3 Codex profiles, got {codex_count}" # проверки в том, работоспособна ли маршрутизация, а не совпадает ли
assert ag_count == 10, f"Expected 10 Antigravity profiles, got {ag_count}" # конфигурация с зафиксированной когда-то.
assert opengo_count == 3, f"Expected 3 OpenCode Go profiles, got {opengo_count}" counts = {}
print(f" [PASS] 16 profiles registered ({codex_count} Codex, {ag_count} Antigravity [7 active, 3 cold], {opengo_count} OpenCode Go)") for prof in config.profiles.values():
counts[prof.provider] = counts.get(prof.provider, 0) + 1
assert config.profiles, "В конфигурации нет ни одного профиля"
for required in ("openai-codex", "antigravity", "opencode-go"):
assert counts.get(required), f"Нет ни одного профиля провайдера {required}"
summary = ", ".join(f"{prov}: {n}" for prov, n in sorted(counts.items()))
print(f" [PASS] Профилей: {len(config.profiles)} ({summary})")
passed += 1 passed += 1
# 2. Role Fallback Chains # 2. Role Fallback Chains
print("2. Checking role fallback policies...") print("2. Checking role fallback policies...")
assert "orchestrator" in config.roles assert "orchestrator" in config.roles, "Роль orchestrator отсутствует"
assert config.roles["orchestrator"].preferred_chain == ["codex-orch", "ag-orch-fallback", "opengo-3"] # Цепочки настраиваются владельцем и меняются — дословно их сверять нельзя.
assert config.roles["coder-primary"].preferred_chain == ["codex-worker-1", "ag-w1", "opengo-3"] # Проверяем то, что действительно ломает маршрутизацию: цепочка непуста и
assert config.roles["reviewer"].preferred_chain == ["codex-worker-2", "opengo-2", "ag-w2"] # каждый профиль в ней существует.
assert config.roles["research"].preferred_chain == ["opengo-1", "ag-w3", "ag-w4"] for role_name, policy in config.roles.items():
print(" [PASS] All 6 logical role fallback chains validated") chain = policy.preferred_chain or []
assert chain, f"У роли {role_name} пустая цепочка отказоустойчивости"
for pid in chain:
assert pid in config.profiles, (
f"Роль {role_name} ссылается на несуществующий профиль {pid}"
)
print(f" [PASS] Цепочки {len(config.roles)} ролей ссылаются только на существующие профили")
passed += 1 passed += 1
# 3. Model family extraction # 3. Model family extraction

0
tests/fixtures/scratch_state.lock vendored Normal file
View file