"""Tests for A22: Native agy CLI Login, Profile Isolation, ~/.gemini Protection, and Model Discovery. Acceptance criteria verification: 1. Hub never modifies ~/.gemini or global Windows Credential Manager during profile operations. 2. launch_native_agy_login spawns agy in target profile's isolated environment. 3. check_profile_native_auth_status detects native credentials without logging secrets. 4. AutoAssigner and wizard support sequential multi-account progression across all Antigravity slots. 5. do_set_model allows valid model selection. """ from __future__ import annotations import json import os import subprocess import time from pathlib import Path from unittest.mock import MagicMock, patch import pytest from antigravity_provider.agy_subprocess import ( check_profile_native_auth_status, launch_native_agy_login, ) from antigravity_provider.paths import get_profile_dir from antigravity_provider.router.action_handler import do_set_model from antigravity_provider.router.auto_assigner import AutoAssigner from antigravity_provider.router.profile_manager import ProfileAuthManager from antigravity_provider.router.router_config import ( RouterConfig, RouterProfileConfig, load_router_config, save_router_config, ) # ── TEST 1: Absolute protection of global ~/.gemini ── @pytest.mark.unit def test_profile_operations_never_touch_user_home_gemini(tmp_path, monkeypatch): """P0-4: Saving, updating, refreshing or probing profiles must NEVER touch ~/.gemini.""" hermes_home = tmp_path / "hermes_home" monkeypatch.setenv("HERMES_HOME", str(hermes_home)) # Mock user home user_home = tmp_path / "user_home" global_gemini = user_home / ".gemini" global_gemini.mkdir(parents=True, exist_ok=True) monkeypatch.setenv("USERPROFILE", str(user_home)) monkeypatch.setenv("HOME", str(user_home)) monkeypatch.setattr(Path, "home", lambda: user_home) # Seed global .gemini with owner's sensitive files accounts_file = global_gemini / "google_accounts.json" accounts_file.write_text(json.dumps({"active": "owner@gmail.com", "old": []}), encoding="utf-8") creds_file = global_gemini / "oauth_creds.json" creds_file.write_text(json.dumps({"access_token": "ya29.owner_token", "expiry_date": 9999999999999}), encoding="utf-8") # Snapshot initial state of ~/.gemini initial_snapshot = { p.name: (p.stat().st_mtime_ns, p.read_bytes()) for p in global_gemini.iterdir() if p.is_file() } # Perform diverse profile operations across multiple Antigravity slots auth_data = { "email": "worker1@gmail.com", "auth_method": "oauth", "token": { "access_token": "ya29.worker1_token", "refresh_token": "1//worker1_refresh", "id_token": "eyJhbGciOiJSUzI1NiJ9.worker1.sig", "scope": "openid email", "token_type": "Bearer", "expiry_date": int(time.time() * 1000) + 3600000, }, } for slot in ["ag-orch-fallback", "ag-w1", "ag-w2"]: ProfileAuthManager.save_profile_auth("antigravity", slot, auth_data) loaded = ProfileAuthManager.load_profile_auth("antigravity", slot) assert loaded is not None # Check native status is_authed, email, data = check_profile_native_auth_status(slot) assert is_authed is True assert email == "worker1@gmail.com" # Set main profile ProfileAuthManager.set_main_profile("antigravity", "ag-w1") # Verify global ~/.gemini is 100% UNTOUCHED current_files = list(global_gemini.iterdir()) assert len(current_files) == len(initial_snapshot), "New files appeared in ~/.gemini!" for p in current_files: assert p.name in initial_snapshot, f"Unexpected file {p.name} in ~/.gemini" init_mtime, init_bytes = initial_snapshot[p.name] assert p.read_bytes() == init_bytes, f"File {p.name} in ~/.gemini was modified!" # ── TEST 2: Native agy login execution and environment isolation ── @pytest.mark.unit def test_launch_native_agy_login_env_isolation(tmp_path, monkeypatch): """P0-2: launch_native_agy_login launches agy in profile's isolated directory.""" monkeypatch.setenv("HERMES_HOME", str(tmp_path / "hermes")) slot = "ag-w2" expected_pdir = get_profile_dir(slot, "antigravity") with patch("subprocess.Popen") as mock_popen, \ patch("antigravity_provider.agy_subprocess.get_agy_exe", return_value="C:\\fake\\agy.exe"): mock_popen.return_value = MagicMock() proc = launch_native_agy_login(slot) assert proc is not None mock_popen.assert_called_once() args, kwargs = mock_popen.call_args # Command is agy executable assert args[0][0] == "C:\\fake\\agy.exe" # Environment points to profile dir env = kwargs.get("env", {}) assert env.get("USERPROFILE") == str(expected_pdir) assert env.get("HOME") == str(expected_pdir) assert env.get("HOMEPATH") == str(expected_pdir) # Profile .gemini directory was created assert (expected_pdir / ".gemini").is_dir() # ── TEST 3: Detection of native agy authentication ── @pytest.mark.unit def test_check_profile_native_auth_status(tmp_path, monkeypatch): """P0-2: check_profile_native_auth_status detects native credentials and syncs auth.json.""" monkeypatch.setenv("HERMES_HOME", str(tmp_path / "hermes")) slot = "ag-w3" pdir = get_profile_dir(slot, "antigravity") gemini_dir = pdir / ".gemini" gemini_dir.mkdir(parents=True, exist_ok=True) # Initially not authenticated is_authed, email, data = check_profile_native_auth_status(slot) assert is_authed is False assert email is None # Simulate agy writing its native credentials oauth_creds = { "access_token": "ya29.native_access_token", "refresh_token": "1//native_refresh_token", "id_token": "eyJhbGciOiJSUzI1NiJ9.native_id.sig", "scope": "openid https://www.googleapis.com/auth/userinfo.email", "token_type": "Bearer", "expiry_date": int(time.time() * 1000) + 3600000, } (gemini_dir / "oauth_creds.json").write_text(json.dumps(oauth_creds), encoding="utf-8") google_accounts = { "active": "native.coder@gmail.com", "old": [], } (gemini_dir / "google_accounts.json").write_text(json.dumps(google_accounts), encoding="utf-8") # Detection succeeds is_authed, email, data = check_profile_native_auth_status(slot) assert is_authed is True assert email == "native.coder@gmail.com" assert data["token"]["access_token"] == "ya29.native_access_token" # auth.json was synced auth_file = pdir / "auth.json" assert auth_file.is_file() saved = json.loads(auth_file.read_text(encoding="utf-8")) assert saved["email"] == "native.coder@gmail.com" # ── TEST 4: Multi-account sequential slot progression ── @pytest.mark.unit def test_multi_account_sequential_slot_progression(tmp_path, monkeypatch): """P0-2: Wizard can iterate through all Antigravity profile slots sequentially.""" monkeypatch.setenv("HERMES_HOME", str(tmp_path / "hermes")) # Initial free slot first_slot = AutoAssigner.find_free_slot("antigravity") assert first_slot == "ag-orch-fallback" # Connect first slot AutoAssigner.ensure_profile_definition("antigravity", first_slot) pdir = get_profile_dir(first_slot, "antigravity") (pdir / "auth.json").write_text(json.dumps({"auth_method": "oauth", "email": "user1@gmail.com"})) # Next free slot second_slot = AutoAssigner.find_free_slot("antigravity") assert second_slot == "ag-w1" # Connect second slot AutoAssigner.ensure_profile_definition("antigravity", second_slot) pdir2 = get_profile_dir(second_slot, "antigravity") (pdir2 / "auth.json").write_text(json.dumps({"auth_method": "oauth", "email": "user2@gmail.com"})) # Next free slot third_slot = AutoAssigner.find_free_slot("antigravity") assert third_slot == "ag-w2" # ── TEST 5: Model selection and assignment (P1-6) ── @pytest.mark.unit def test_do_set_model_allows_valid_model_assignment(tmp_path, monkeypatch): """P1-6: do_set_model permits setting valid models like gemini-3.1-pro-high.""" monkeypatch.setenv("HERMES_HOME", str(tmp_path / "hermes")) cfg = RouterConfig() cfg.profiles["ag-w1"] = RouterProfileConfig( profile_id="ag-w1", provider="antigravity", account_id="ag-w1", preferred_models=["gemini-2.5-pro"], ) save_router_config(cfg) # Set model to gemini-3.1-pro-high ok, msg = do_set_model("ag-w1", "gemini-3.1-pro-high") assert ok is True assert "успешно сохранена" in msg updated = load_router_config() assert updated.profiles["ag-w1"].preferred_models[0] == "gemini-3.1-pro-high"