fix(profiles): пустые слоты появлялись сами, потому что запрос пути создавал каталог

Владелец спросил, откуда 25 каталогов профилей, когда заводил единицы.
Проверено исполнением: обращение к get_profile_dir создавало каталог. Спросили
четыре пути — появились четыре каталога.

В коде зашит список «стандартных» слотов — ag-orch-primary, ag-orch-fallback,
ag-1..ag-20, ag-w1..ag-w10 — в двух местах: agy_subprocess и
model_discovery_service. Любой обход этого списка материализовал их все. Отсюда
ag-w1..ag-w4, ag-cold-*, ag-spare-* — владелец их не создавал.

Спросить, где профиль жил бы, и завести его — разные действия. Создание теперь
запрашивается явно, create=True, и это делают только те, кто действительно
заводит профиль.

Тесты и сценарий проверки, опиравшиеся на побочное создание, приведены к новому
поведению: они проверяют изоляцию пути, а не то, что каталог возник сам.

669 passed, 2 skipped; ruff чисто; релизный гейт пройден.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Hermes Team 2026-09-01 15:27:33 +07:00
parent b309972e49
commit 6cb0d7c636
8 changed files with 69 additions and 12 deletions

View file

@ -18,7 +18,7 @@ namespace HermesHubSetup
// Подставляется сборщиком из фактического git-коммита. Раньше здесь
// жил зашитый "8cddc9f", то есть манифест сообщал неправду о том, из
// какого кода собран установщик.
public const string BuildCommit = "0ad946e";
public const string BuildCommit = "b309972";
public const string MIN_HERMES_VERSION = "0.20.0";
public const string MAX_TESTED_HERMES = "0.20.4";

Binary file not shown.

Binary file not shown.

View file

@ -138,10 +138,14 @@ def run_checks() -> int:
# 7. Antigravity profile isolation
print("7. Checking Antigravity profile environment directory isolation...")
# Проверяем изоляцию пути, а не побочное создание каталога: запрос пути
# каталогов больше не плодит, иначе любая проверка засоряла бы диск
# десятком пустых слотов.
pdir = get_profile_env_dir("ag-w2")
assert pdir.exists()
assert "ag-w2" in str(pdir)
print(f" [PASS] Profile directory isolated at {pdir}")
assert "agy_profiles" in str(pdir)
assert not pdir.exists(), "запрос пути не должен создавать каталог"
print(f" [PASS] Profile directory isolated at {pdir} (не создан)")
passed += 1
# 8. Error classification

View file

@ -111,9 +111,21 @@ def get_compatibility_path() -> Path:
return get_config_dir() / "compatibility.json"
def get_profile_dir(profile_id: str, provider: Optional[str] = None) -> Path:
"""Return isolated storage directory for a profile.
def get_profile_dir(
profile_id: str, provider: Optional[str] = None, create: bool = False
) -> Path:
"""Вернуть каталог профиля. По умолчанию НИЧЕГО не создаёт.
Раньше эта функция создавала каталог при каждом обращении. В коде зашит
список «стандартных» слотов ag-orch-primary, ag-orch-fallback, ag-1..ag-20,
ag-w1..ag-w10, и любой обход этого списка материализовал их на диске. У
владельца накопилось 25 каталогов, из которых он заводил единицы: остальные
появились сами, просто потому что кто-то спросил путь.
Спрашивать, где профиль жил бы, и создавать его разные действия. Создание
теперь запрашивается явно через create=True: так делают те, кто
действительно заводит профиль.
Accepts both (profile_id) and (provider, profile_id) or (profile_id, provider) gracefully.
"""
# If first argument looks like a provider or swapped, resolve cleanly
@ -151,7 +163,8 @@ def get_profile_dir(profile_id: str, provider: Optional[str] = None) -> Path:
folder_prefix = f"{prov_lower}_profiles"
d = get_hermes_home() / folder_prefix / p_id
d.mkdir(parents=True, exist_ok=True)
if create:
d.mkdir(parents=True, exist_ok=True)
return d

View file

@ -138,3 +138,30 @@ def test_immediate_exit_is_reported_as_failure(linux, monkeypatch, tmp_path):
assert ok is False
assert "окно не открылось" in msg
assert data.get("exit_code") == 1
# ── Пустые слоты появлялись сами ──
def test_asking_for_a_path_creates_nothing(tmp_path, monkeypatch):
"""У владельца накопилось 25 каталогов профилей при единицах заведённых.
Причина: get_profile_dir создавал каталог при каждом обращении, а в коде
зашит список «стандартных» слотов ag-orch-primary, ag-orch-fallback,
ag-1..ag-20, ag-w1..ag-w10. Любой обход этого списка материализовал их все.
Спросить, где профиль жил бы, и завести его разные действия.
"""
from antigravity_provider import paths
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
root = tmp_path / "agy_profiles"
for pid in ("ag-orch-primary", "ag-w7", "ag-19", "ag-cold-3"):
paths.get_profile_dir(pid, "antigravity")
assert not root.exists() or not any(root.iterdir()), (
"запрос пути не должен оставлять каталоги на диске"
)
created = paths.get_profile_dir("ag-1", "antigravity", create=True)
assert created.is_dir()
assert sorted(p.name for p in root.iterdir()) == ["ag-1"]

View file

@ -116,7 +116,7 @@ def test_check_profile_native_auth_status(tmp_path, monkeypatch):
monkeypatch.setenv("HERMES_HOME", str(tmp_path / "hermes"))
slot = "ag-w3"
pdir = get_profile_dir(slot, "antigravity")
pdir = get_profile_dir(slot, "antigravity", create=True)
gemini_dir = pdir / ".gemini"
gemini_dir.mkdir(parents=True, exist_ok=True)
@ -169,7 +169,7 @@ def test_multi_account_sequential_slot_progression(tmp_path, monkeypatch):
# Connect first slot
AutoAssigner.ensure_profile_definition("antigravity", first_slot)
pdir = get_profile_dir(first_slot, "antigravity")
pdir = get_profile_dir(first_slot, "antigravity", create=True)
(pdir / "auth.json").write_text(json.dumps({"auth_method": "oauth", "email": "user1@gmail.com"}))
# Next free slot
@ -178,7 +178,7 @@ def test_multi_account_sequential_slot_progression(tmp_path, monkeypatch):
# Connect second slot
AutoAssigner.ensure_profile_definition("antigravity", second_slot)
pdir2 = get_profile_dir(second_slot, "antigravity")
pdir2 = get_profile_dir(second_slot, "antigravity", create=True)
(pdir2 / "auth.json").write_text(json.dumps({"auth_method": "oauth", "email": "user2@gmail.com"}))
# Next free slot

View file

@ -238,10 +238,23 @@ class TestRouterEngineFailover:
class TestAntigravityIsolation:
"""Test environment isolation for Antigravity profiles."""
def test_profile_env_dir_creation(self):
def test_profile_env_dir_is_isolated_and_not_created_on_lookup(self):
"""Запрос пути не должен создавать каталог.
Прежде создавал, и любой обход списка стандартных слотов
(ag-orch-primary, ag-1..ag-20, ag-w1..ag-w10) материализовал их все:
у владельца накопилось 25 каталогов при единицах заведённых вручную.
"""
pdir = get_profile_env_dir("ag-w1")
assert pdir.exists()
assert "ag-w1" in str(pdir)
assert "agy_profiles" in str(pdir)
assert not pdir.exists(), "спросили путь — каталог появиться не должен"
from antigravity_provider.paths import get_profile_dir
created = get_profile_dir("ag-w1", "antigravity", create=True)
assert created.exists()
assert created == pdir
class TestRouterCLI: