v2.2.0 (#395)
Co-authored-by: Quentin Torroba <quentin.torroba@mistral.ai> Co-authored-by: Clément Siriex <clement.sirieix@mistral.ai> Co-authored-by: Kim-Adeline Miguel <kimadeline.miguel@mistral.ai> Co-authored-by: Michel Thomazo <michel.thomazo@mistral.ai> Co-authored-by: Clément Drouin <clement.drouin@mistral.ai>
This commit is contained in:
parent
51fecc67d9
commit
ec7f3b25ea
107 changed files with 8002 additions and 535 deletions
57
tests/core/test_config_paths.py
Normal file
57
tests/core/test_config_paths.py
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from vibe.core.paths.config_paths import resolve_local_skills_dirs
|
||||
|
||||
|
||||
class TestResolveLocalSkillsDirs:
|
||||
def test_returns_empty_list_when_dir_not_trusted(self, tmp_path: Path) -> None:
|
||||
(tmp_path / ".vibe" / "skills").mkdir(parents=True)
|
||||
with patch("vibe.core.paths.config_paths.trusted_folders_manager") as mock_tfm:
|
||||
mock_tfm.is_trusted.return_value = False
|
||||
assert resolve_local_skills_dirs(tmp_path) == []
|
||||
|
||||
def test_returns_empty_list_when_trusted_but_no_skills_dirs(
|
||||
self, tmp_path: Path
|
||||
) -> None:
|
||||
with patch("vibe.core.paths.config_paths.trusted_folders_manager") as mock_tfm:
|
||||
mock_tfm.is_trusted.return_value = True
|
||||
assert resolve_local_skills_dirs(tmp_path) == []
|
||||
|
||||
def test_returns_vibe_skills_only_when_only_it_exists(self, tmp_path: Path) -> None:
|
||||
vibe_skills = tmp_path / ".vibe" / "skills"
|
||||
vibe_skills.mkdir(parents=True)
|
||||
with patch("vibe.core.paths.config_paths.trusted_folders_manager") as mock_tfm:
|
||||
mock_tfm.is_trusted.return_value = True
|
||||
result = resolve_local_skills_dirs(tmp_path)
|
||||
assert result == [vibe_skills]
|
||||
|
||||
def test_returns_agents_skills_only_when_only_it_exists(
|
||||
self, tmp_path: Path
|
||||
) -> None:
|
||||
agents_skills = tmp_path / ".agents" / "skills"
|
||||
agents_skills.mkdir(parents=True)
|
||||
with patch("vibe.core.paths.config_paths.trusted_folders_manager") as mock_tfm:
|
||||
mock_tfm.is_trusted.return_value = True
|
||||
result = resolve_local_skills_dirs(tmp_path)
|
||||
assert result == [agents_skills]
|
||||
|
||||
def test_returns_both_in_order_when_both_exist(self, tmp_path: Path) -> None:
|
||||
vibe_skills = tmp_path / ".vibe" / "skills"
|
||||
agents_skills = tmp_path / ".agents" / "skills"
|
||||
vibe_skills.mkdir(parents=True)
|
||||
agents_skills.mkdir(parents=True)
|
||||
with patch("vibe.core.paths.config_paths.trusted_folders_manager") as mock_tfm:
|
||||
mock_tfm.is_trusted.return_value = True
|
||||
result = resolve_local_skills_dirs(tmp_path)
|
||||
assert result == [vibe_skills, agents_skills]
|
||||
|
||||
def test_ignores_vibe_skills_when_file_not_dir(self, tmp_path: Path) -> None:
|
||||
(tmp_path / ".vibe").mkdir()
|
||||
(tmp_path / ".vibe" / "skills").write_text("", encoding="utf-8")
|
||||
with patch("vibe.core.paths.config_paths.trusted_folders_manager") as mock_tfm:
|
||||
mock_tfm.is_trusted.return_value = True
|
||||
result = resolve_local_skills_dirs(tmp_path)
|
||||
assert result == []
|
||||
280
tests/core/test_file_logging.py
Normal file
280
tests/core/test_file_logging.py
Normal file
|
|
@ -0,0 +1,280 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from logging.handlers import RotatingFileHandler
|
||||
from pathlib import Path
|
||||
from textwrap import dedent
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from vibe.core.utils import StructuredLogFormatter, apply_logging_config
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_log_dir(tmp_path: Path):
|
||||
"""Mock LOG_DIR and LOG_FILE to use tmp_path for testing."""
|
||||
mock_dir = MagicMock()
|
||||
mock_dir.path = tmp_path
|
||||
mock_file = MagicMock()
|
||||
mock_file.path = tmp_path / "vibe.log"
|
||||
with (
|
||||
patch("vibe.core.utils.LOG_DIR", mock_dir),
|
||||
patch("vibe.core.utils.LOG_FILE", mock_file),
|
||||
):
|
||||
yield tmp_path
|
||||
|
||||
|
||||
class TestStructuredFormatter:
|
||||
def test_format_contains_required_fields(self) -> None:
|
||||
formatter = StructuredLogFormatter()
|
||||
record = logging.LogRecord(
|
||||
name="test_logger",
|
||||
level=logging.INFO,
|
||||
pathname="test.py",
|
||||
lineno=1,
|
||||
msg="Test message",
|
||||
args=(),
|
||||
exc_info=None,
|
||||
)
|
||||
|
||||
output = formatter.format(record)
|
||||
|
||||
parts = output.split(" ", 4)
|
||||
assert len(parts) == 5
|
||||
assert "T" in parts[0]
|
||||
assert parts[1].isdigit()
|
||||
assert parts[2].isdigit()
|
||||
assert parts[3] == "INFO"
|
||||
assert parts[4] == "Test message"
|
||||
|
||||
def test_format_includes_exception(self) -> None:
|
||||
formatter = StructuredLogFormatter()
|
||||
try:
|
||||
raise ValueError("test error")
|
||||
except ValueError:
|
||||
import sys
|
||||
|
||||
exc_info = sys.exc_info()
|
||||
|
||||
record = logging.LogRecord(
|
||||
name="test_logger",
|
||||
level=logging.ERROR,
|
||||
pathname="test.py",
|
||||
lineno=1,
|
||||
msg="Error occurred",
|
||||
args=(),
|
||||
exc_info=exc_info,
|
||||
)
|
||||
|
||||
output = formatter.format(record)
|
||||
|
||||
assert "Error occurred" in output
|
||||
assert "ValueError" in output
|
||||
assert "test error" in output
|
||||
|
||||
def test_format_escapes_newlines_in_message(self) -> None:
|
||||
formatter = StructuredLogFormatter()
|
||||
multiline_msg = dedent("""
|
||||
Line one
|
||||
Line two
|
||||
Line three""")
|
||||
record = logging.LogRecord(
|
||||
name="test_logger",
|
||||
level=logging.INFO,
|
||||
pathname="test.py",
|
||||
lineno=1,
|
||||
msg=multiline_msg,
|
||||
args=(),
|
||||
exc_info=None,
|
||||
)
|
||||
|
||||
output = formatter.format(record)
|
||||
|
||||
assert "\n" not in output
|
||||
assert "Line one\\nLine two\\nLine three" in output
|
||||
|
||||
def test_format_escapes_newlines_in_exception(self) -> None:
|
||||
formatter = StructuredLogFormatter()
|
||||
try:
|
||||
raise ValueError("test error")
|
||||
except ValueError:
|
||||
import sys
|
||||
|
||||
exc_info = sys.exc_info()
|
||||
|
||||
record = logging.LogRecord(
|
||||
name="test_logger",
|
||||
level=logging.ERROR,
|
||||
pathname="test.py",
|
||||
lineno=1,
|
||||
msg="Error occurred",
|
||||
args=(),
|
||||
exc_info=exc_info,
|
||||
)
|
||||
|
||||
output = formatter.format(record)
|
||||
|
||||
assert "\n" not in output
|
||||
assert "ValueError" in output
|
||||
assert "test error" in output
|
||||
assert "\\n" in output
|
||||
|
||||
def test_format_output_is_single_line(self) -> None:
|
||||
formatter = StructuredLogFormatter()
|
||||
try:
|
||||
error_msg = dedent("""
|
||||
multi
|
||||
line
|
||||
error""")
|
||||
raise RuntimeError(error_msg)
|
||||
except RuntimeError:
|
||||
import sys
|
||||
|
||||
exc_info = sys.exc_info()
|
||||
|
||||
multiline_msg = dedent("""
|
||||
Something
|
||||
went
|
||||
wrong""")
|
||||
record = logging.LogRecord(
|
||||
name="test_logger",
|
||||
level=logging.ERROR,
|
||||
pathname="test.py",
|
||||
lineno=1,
|
||||
msg=multiline_msg,
|
||||
args=(),
|
||||
exc_info=exc_info,
|
||||
)
|
||||
|
||||
output = formatter.format(record)
|
||||
lines = output.split("\n")
|
||||
|
||||
assert len(lines) == 1
|
||||
|
||||
|
||||
class TestApplyLoggingConfig:
|
||||
def test_adds_handler_to_logger(
|
||||
self, mock_log_dir: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
monkeypatch.setenv("LOG_LEVEL", "DEBUG")
|
||||
test_logger = logging.getLogger("test_apply_logging")
|
||||
initial_handler_count = len(test_logger.handlers)
|
||||
|
||||
apply_logging_config(test_logger)
|
||||
|
||||
assert len(test_logger.handlers) == initial_handler_count + 1
|
||||
|
||||
def test_creates_log_file(
|
||||
self, mock_log_dir: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
monkeypatch.setenv("LOG_LEVEL", "DEBUG")
|
||||
test_logger = logging.getLogger("test_log_file")
|
||||
test_logger.setLevel(logging.DEBUG)
|
||||
|
||||
apply_logging_config(test_logger)
|
||||
test_logger.info("Test log entry")
|
||||
|
||||
log_file = mock_log_dir / "vibe.log"
|
||||
assert log_file.exists()
|
||||
|
||||
def test_log_entry_format(
|
||||
self, mock_log_dir: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
monkeypatch.setenv("LOG_LEVEL", "DEBUG")
|
||||
test_logger = logging.getLogger("test_format")
|
||||
test_logger.setLevel(logging.DEBUG)
|
||||
|
||||
apply_logging_config(test_logger)
|
||||
test_logger.warning("Test warning message")
|
||||
|
||||
log_file = mock_log_dir / "vibe.log"
|
||||
content = log_file.read_text()
|
||||
|
||||
assert "WARNING" in content
|
||||
assert "Test warning message" in content
|
||||
|
||||
def test_respects_log_level(
|
||||
self, mock_log_dir: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
monkeypatch.setenv("LOG_LEVEL", "WARNING")
|
||||
test_logger = logging.getLogger("test_level_filter")
|
||||
test_logger.setLevel(logging.DEBUG)
|
||||
|
||||
apply_logging_config(test_logger)
|
||||
test_logger.debug("Debug message")
|
||||
test_logger.info("Info message")
|
||||
test_logger.warning("Warning message")
|
||||
|
||||
log_file = mock_log_dir / "vibe.log"
|
||||
content = log_file.read_text()
|
||||
|
||||
assert "Debug message" not in content
|
||||
assert "Info message" not in content
|
||||
assert "Warning message" in content
|
||||
|
||||
def test_creates_log_directory_if_missing(
|
||||
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
log_dir = tmp_path / "nested" / "logs"
|
||||
mock_dir = MagicMock()
|
||||
mock_dir.path = log_dir
|
||||
mock_file = MagicMock()
|
||||
mock_file.path = log_dir / "vibe.log"
|
||||
with (
|
||||
patch("vibe.core.utils.LOG_DIR", mock_dir),
|
||||
patch("vibe.core.utils.LOG_FILE", mock_file),
|
||||
):
|
||||
monkeypatch.setenv("LOG_LEVEL", "DEBUG")
|
||||
test_logger = logging.getLogger("test_mkdir")
|
||||
|
||||
apply_logging_config(test_logger)
|
||||
|
||||
assert log_dir.exists()
|
||||
|
||||
def test_debug_mode_overrides_log_level(
|
||||
self, mock_log_dir: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
monkeypatch.setenv("LOG_LEVEL", "WARNING")
|
||||
monkeypatch.setenv("DEBUG_MODE", "true")
|
||||
test_logger = logging.getLogger("test_debug_mode")
|
||||
test_logger.setLevel(logging.DEBUG)
|
||||
|
||||
apply_logging_config(test_logger)
|
||||
test_logger.debug("Debug message")
|
||||
|
||||
log_file = mock_log_dir / "vibe.log"
|
||||
content = log_file.read_text()
|
||||
|
||||
assert "Debug message" in content
|
||||
|
||||
def test_invalid_log_level_defaults_to_warning(
|
||||
self, mock_log_dir: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
monkeypatch.setenv("LOG_LEVEL", "INVALID")
|
||||
test_logger = logging.getLogger("test_invalid_level")
|
||||
test_logger.setLevel(logging.DEBUG)
|
||||
|
||||
apply_logging_config(test_logger)
|
||||
test_logger.info("Info message")
|
||||
test_logger.warning("Warning message")
|
||||
|
||||
log_file = mock_log_dir / "vibe.log"
|
||||
content = log_file.read_text()
|
||||
|
||||
assert "Info message" not in content
|
||||
assert "Warning message" in content
|
||||
|
||||
def test_log_max_bytes_from_env(
|
||||
self, mock_log_dir: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
monkeypatch.setenv("LOG_MAX_BYTES", "5242880") # 5 MB
|
||||
monkeypatch.setenv("LOG_LEVEL", "DEBUG")
|
||||
test_logger = logging.getLogger("test_max_bytes")
|
||||
|
||||
apply_logging_config(test_logger)
|
||||
|
||||
# Verify handler was added with correct maxBytes
|
||||
handler = test_logger.handlers[-1]
|
||||
assert isinstance(handler, RotatingFileHandler)
|
||||
assert handler.maxBytes == 5242880
|
||||
304
tests/core/test_proxy_setup.py
Normal file
304
tests/core/test_proxy_setup.py
Normal file
|
|
@ -0,0 +1,304 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from vibe.core.paths.global_paths import GLOBAL_ENV_FILE
|
||||
from vibe.core.proxy_setup import (
|
||||
SUPPORTED_PROXY_VARS,
|
||||
ProxySetupError,
|
||||
get_current_proxy_settings,
|
||||
parse_proxy_command,
|
||||
set_proxy_var,
|
||||
unset_proxy_var,
|
||||
)
|
||||
|
||||
|
||||
def _write_env_file(content: str) -> None:
|
||||
GLOBAL_ENV_FILE.path.parent.mkdir(parents=True, exist_ok=True)
|
||||
GLOBAL_ENV_FILE.path.write_text(content, encoding="utf-8")
|
||||
|
||||
|
||||
class TestProxySetupError:
|
||||
def test_inherits_from_exception(self) -> None:
|
||||
assert issubclass(ProxySetupError, Exception)
|
||||
|
||||
def test_preserves_message(self) -> None:
|
||||
error = ProxySetupError("test message")
|
||||
assert str(error) == "test message"
|
||||
|
||||
|
||||
class TestSupportedProxyVars:
|
||||
def test_contains_all_expected_keys(self) -> None:
|
||||
expected_keys = {
|
||||
"HTTP_PROXY",
|
||||
"HTTPS_PROXY",
|
||||
"ALL_PROXY",
|
||||
"NO_PROXY",
|
||||
"SSL_CERT_FILE",
|
||||
"SSL_CERT_DIR",
|
||||
}
|
||||
assert set(SUPPORTED_PROXY_VARS.keys()) == expected_keys
|
||||
|
||||
def test_all_keys_are_uppercase(self) -> None:
|
||||
for key in SUPPORTED_PROXY_VARS:
|
||||
assert key == key.upper()
|
||||
|
||||
def test_all_values_are_non_empty_strings(self) -> None:
|
||||
for value in SUPPORTED_PROXY_VARS.values():
|
||||
assert isinstance(value, str)
|
||||
assert len(value) > 0
|
||||
|
||||
|
||||
class TestGetCurrentProxySettings:
|
||||
def test_returns_all_none_when_env_file_does_not_exist(self) -> None:
|
||||
result = get_current_proxy_settings()
|
||||
|
||||
assert all(value is None for value in result.values())
|
||||
|
||||
def test_returns_dict_with_all_supported_keys(self) -> None:
|
||||
result = get_current_proxy_settings()
|
||||
|
||||
assert set(result.keys()) == set(SUPPORTED_PROXY_VARS.keys())
|
||||
|
||||
def test_returns_values_from_env_file(self) -> None:
|
||||
_write_env_file(
|
||||
"HTTP_PROXY=http://proxy:8080\nHTTPS_PROXY=https://proxy:8443\n"
|
||||
)
|
||||
|
||||
result = get_current_proxy_settings()
|
||||
|
||||
assert result["HTTP_PROXY"] == "http://proxy:8080"
|
||||
assert result["HTTPS_PROXY"] == "https://proxy:8443"
|
||||
|
||||
def test_returns_none_for_unset_keys(self) -> None:
|
||||
_write_env_file("HTTP_PROXY=http://proxy:8080\n")
|
||||
|
||||
result = get_current_proxy_settings()
|
||||
|
||||
assert result["HTTP_PROXY"] == "http://proxy:8080"
|
||||
assert result["HTTPS_PROXY"] is None
|
||||
assert result["ALL_PROXY"] is None
|
||||
|
||||
def test_ignores_non_proxy_vars_in_env_file(self) -> None:
|
||||
_write_env_file("HTTP_PROXY=http://proxy:8080\nOTHER_VAR=ignored\n")
|
||||
|
||||
result = get_current_proxy_settings()
|
||||
|
||||
assert "OTHER_VAR" not in result
|
||||
assert result["HTTP_PROXY"] == "http://proxy:8080"
|
||||
|
||||
def test_handles_values_with_special_characters(self) -> None:
|
||||
_write_env_file("HTTP_PROXY=http://user:p@ss@proxy:8080\n")
|
||||
|
||||
result = get_current_proxy_settings()
|
||||
|
||||
assert result["HTTP_PROXY"] == "http://user:p@ss@proxy:8080"
|
||||
|
||||
def test_returns_all_none_when_env_file_read_fails(
|
||||
self, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
_write_env_file("HTTP_PROXY=http://proxy:8080\n")
|
||||
|
||||
def raise_error(*args, **kwargs):
|
||||
raise OSError("Permission denied")
|
||||
|
||||
monkeypatch.setattr("vibe.core.proxy_setup.dotenv_values", raise_error)
|
||||
|
||||
result = get_current_proxy_settings()
|
||||
|
||||
assert all(value is None for value in result.values())
|
||||
|
||||
|
||||
class TestSetProxyVar:
|
||||
def test_sets_valid_proxy_var(self) -> None:
|
||||
set_proxy_var("HTTP_PROXY", "http://proxy:8080")
|
||||
|
||||
result = get_current_proxy_settings()
|
||||
assert result["HTTP_PROXY"] == "http://proxy:8080"
|
||||
|
||||
@pytest.mark.parametrize("key", SUPPORTED_PROXY_VARS.keys())
|
||||
def test_sets_all_supported_vars(self, key: str) -> None:
|
||||
set_proxy_var(key, "test-value")
|
||||
|
||||
result = get_current_proxy_settings()
|
||||
assert result[key] == "test-value"
|
||||
|
||||
def test_uppercases_key_before_validation(self) -> None:
|
||||
set_proxy_var("http_proxy", "http://proxy:8080")
|
||||
|
||||
result = get_current_proxy_settings()
|
||||
assert result["HTTP_PROXY"] == "http://proxy:8080"
|
||||
|
||||
def test_raises_error_for_unknown_key(self) -> None:
|
||||
with pytest.raises(ProxySetupError) as exc_info:
|
||||
set_proxy_var("UNKNOWN_KEY", "value")
|
||||
|
||||
assert "Unknown key 'UNKNOWN_KEY'" in str(exc_info.value)
|
||||
|
||||
def test_error_message_contains_supported_keys(self) -> None:
|
||||
with pytest.raises(ProxySetupError) as exc_info:
|
||||
set_proxy_var("UNKNOWN_KEY", "value")
|
||||
|
||||
error_msg = str(exc_info.value)
|
||||
assert "HTTP_PROXY" in error_msg
|
||||
assert "HTTPS_PROXY" in error_msg
|
||||
|
||||
def test_creates_env_file_if_missing(self) -> None:
|
||||
assert not GLOBAL_ENV_FILE.path.exists()
|
||||
|
||||
set_proxy_var("HTTP_PROXY", "http://proxy:8080")
|
||||
|
||||
assert GLOBAL_ENV_FILE.path.exists()
|
||||
|
||||
def test_overwrites_existing_value(self) -> None:
|
||||
set_proxy_var("HTTP_PROXY", "http://old:8080")
|
||||
set_proxy_var("HTTP_PROXY", "http://new:8080")
|
||||
|
||||
result = get_current_proxy_settings()
|
||||
assert result["HTTP_PROXY"] == "http://new:8080"
|
||||
|
||||
def test_preserves_other_values(self) -> None:
|
||||
set_proxy_var("HTTP_PROXY", "http://proxy:8080")
|
||||
set_proxy_var("HTTPS_PROXY", "https://proxy:8443")
|
||||
|
||||
result = get_current_proxy_settings()
|
||||
assert result["HTTP_PROXY"] == "http://proxy:8080"
|
||||
assert result["HTTPS_PROXY"] == "https://proxy:8443"
|
||||
|
||||
def test_handles_value_with_spaces(self) -> None:
|
||||
set_proxy_var("NO_PROXY", "localhost, 127.0.0.1, .local")
|
||||
|
||||
result = get_current_proxy_settings()
|
||||
assert result["NO_PROXY"] == "localhost, 127.0.0.1, .local"
|
||||
|
||||
def test_handles_url_with_credentials(self) -> None:
|
||||
set_proxy_var("HTTP_PROXY", "http://user:password@proxy:8080")
|
||||
|
||||
result = get_current_proxy_settings()
|
||||
assert result["HTTP_PROXY"] == "http://user:password@proxy:8080"
|
||||
|
||||
|
||||
class TestUnsetProxyVar:
|
||||
def test_removes_existing_var(self) -> None:
|
||||
set_proxy_var("HTTP_PROXY", "http://proxy:8080")
|
||||
unset_proxy_var("HTTP_PROXY")
|
||||
|
||||
result = get_current_proxy_settings()
|
||||
assert result["HTTP_PROXY"] is None
|
||||
|
||||
def test_uppercases_key_before_validation(self) -> None:
|
||||
set_proxy_var("HTTP_PROXY", "http://proxy:8080")
|
||||
unset_proxy_var("http_proxy")
|
||||
|
||||
result = get_current_proxy_settings()
|
||||
assert result["HTTP_PROXY"] is None
|
||||
|
||||
def test_raises_error_for_unknown_key(self) -> None:
|
||||
with pytest.raises(ProxySetupError) as exc_info:
|
||||
unset_proxy_var("UNKNOWN_KEY")
|
||||
|
||||
assert "Unknown key 'UNKNOWN_KEY'" in str(exc_info.value)
|
||||
|
||||
def test_error_message_contains_supported_keys(self) -> None:
|
||||
with pytest.raises(ProxySetupError) as exc_info:
|
||||
unset_proxy_var("UNKNOWN_KEY")
|
||||
|
||||
error_msg = str(exc_info.value)
|
||||
assert "HTTP_PROXY" in error_msg
|
||||
|
||||
def test_no_op_when_env_file_does_not_exist(self) -> None:
|
||||
assert not GLOBAL_ENV_FILE.path.exists()
|
||||
|
||||
unset_proxy_var("HTTP_PROXY")
|
||||
|
||||
assert not GLOBAL_ENV_FILE.path.exists()
|
||||
|
||||
def test_no_op_when_key_not_in_file(self) -> None:
|
||||
set_proxy_var("HTTPS_PROXY", "https://proxy:8443")
|
||||
unset_proxy_var("HTTP_PROXY")
|
||||
|
||||
result = get_current_proxy_settings()
|
||||
assert result["HTTP_PROXY"] is None
|
||||
assert result["HTTPS_PROXY"] == "https://proxy:8443"
|
||||
|
||||
def test_preserves_other_values(self) -> None:
|
||||
set_proxy_var("HTTP_PROXY", "http://proxy:8080")
|
||||
set_proxy_var("HTTPS_PROXY", "https://proxy:8443")
|
||||
unset_proxy_var("HTTP_PROXY")
|
||||
|
||||
result = get_current_proxy_settings()
|
||||
assert result["HTTP_PROXY"] is None
|
||||
assert result["HTTPS_PROXY"] == "https://proxy:8443"
|
||||
|
||||
@pytest.mark.parametrize("key", SUPPORTED_PROXY_VARS.keys())
|
||||
def test_all_supported_vars_can_be_unset(self, key: str) -> None:
|
||||
set_proxy_var(key, "test-value")
|
||||
unset_proxy_var(key)
|
||||
|
||||
result = get_current_proxy_settings()
|
||||
assert result[key] is None
|
||||
|
||||
|
||||
class TestParseProxyCommand:
|
||||
def test_parses_key_only(self) -> None:
|
||||
key, value = parse_proxy_command("HTTP_PROXY")
|
||||
|
||||
assert key == "HTTP_PROXY"
|
||||
assert value is None
|
||||
|
||||
def test_parses_key_and_value(self) -> None:
|
||||
key, value = parse_proxy_command("HTTP_PROXY http://proxy:8080")
|
||||
|
||||
assert key == "HTTP_PROXY"
|
||||
assert value == "http://proxy:8080"
|
||||
|
||||
def test_uppercases_key(self) -> None:
|
||||
key, value = parse_proxy_command("http_proxy")
|
||||
|
||||
assert key == "HTTP_PROXY"
|
||||
|
||||
def test_preserves_value_case(self) -> None:
|
||||
key, value = parse_proxy_command("HTTP_PROXY http://Proxy:8080")
|
||||
|
||||
assert value == "http://Proxy:8080"
|
||||
|
||||
def test_strips_leading_whitespace(self) -> None:
|
||||
key, value = parse_proxy_command(" HTTP_PROXY")
|
||||
|
||||
assert key == "HTTP_PROXY"
|
||||
|
||||
def test_strips_trailing_whitespace(self) -> None:
|
||||
key, value = parse_proxy_command("HTTP_PROXY ")
|
||||
|
||||
assert key == "HTTP_PROXY"
|
||||
assert value is None
|
||||
|
||||
def test_splits_on_first_space_only(self) -> None:
|
||||
key, value = parse_proxy_command("NO_PROXY localhost, 127.0.0.1, .local")
|
||||
|
||||
assert key == "NO_PROXY"
|
||||
assert value == "localhost, 127.0.0.1, .local"
|
||||
|
||||
def test_raises_error_for_empty_string(self) -> None:
|
||||
with pytest.raises(ProxySetupError) as exc_info:
|
||||
parse_proxy_command("")
|
||||
|
||||
assert "No key provided" in str(exc_info.value)
|
||||
|
||||
def test_raises_error_for_whitespace_only(self) -> None:
|
||||
with pytest.raises(ProxySetupError) as exc_info:
|
||||
parse_proxy_command(" ")
|
||||
|
||||
assert "No key provided" in str(exc_info.value)
|
||||
|
||||
def test_handles_tab_as_separator(self) -> None:
|
||||
key, value = parse_proxy_command("HTTP_PROXY\thttp://proxy:8080")
|
||||
|
||||
assert key == "HTTP_PROXY"
|
||||
assert value == "http://proxy:8080"
|
||||
|
||||
def test_handles_multiple_spaces_as_separator(self) -> None:
|
||||
key, value = parse_proxy_command("HTTP_PROXY http://proxy:8080")
|
||||
|
||||
assert key == "HTTP_PROXY"
|
||||
assert value == "http://proxy:8080"
|
||||
269
tests/core/test_telemetry_send.py
Normal file
269
tests/core/test_telemetry_send.py
Normal file
|
|
@ -0,0 +1,269 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from typing import Any
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.conftest import build_test_vibe_config
|
||||
from tests.stubs.fake_tool import FakeTool, FakeToolArgs
|
||||
from vibe.core.agent_loop import ToolDecision, ToolExecutionResponse
|
||||
from vibe.core.config import Backend
|
||||
from vibe.core.llm.format import ResolvedToolCall
|
||||
from vibe.core.telemetry.send import DATALAKE_EVENTS_URL, TelemetryClient
|
||||
from vibe.core.tools.base import BaseTool, ToolPermission
|
||||
from vibe.core.utils import get_user_agent
|
||||
|
||||
_original_send_telemetry_event = TelemetryClient.send_telemetry_event
|
||||
from vibe.core.tools.builtins.write_file import WriteFile, WriteFileArgs
|
||||
|
||||
|
||||
def _make_resolved_tool_call(
|
||||
tool_name: str, args_dict: dict[str, Any]
|
||||
) -> ResolvedToolCall:
|
||||
if tool_name == "write_file":
|
||||
validated = WriteFileArgs(
|
||||
path="foo.txt", content="x", overwrite=args_dict.get("overwrite", False)
|
||||
)
|
||||
cls: type[BaseTool] = WriteFile
|
||||
else:
|
||||
validated = FakeToolArgs()
|
||||
cls = FakeTool
|
||||
return ResolvedToolCall(
|
||||
tool_name=tool_name, tool_class=cls, validated_args=validated, call_id="call_1"
|
||||
)
|
||||
|
||||
|
||||
def _run_telemetry_tasks() -> None:
|
||||
loop = asyncio.new_event_loop()
|
||||
try:
|
||||
loop.run_until_complete(asyncio.sleep(0))
|
||||
finally:
|
||||
loop.close()
|
||||
|
||||
|
||||
class TestTelemetryClient:
|
||||
def test_send_telemetry_event_does_nothing_when_api_key_is_none(
|
||||
self, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
config = build_test_vibe_config(disable_telemetry=False)
|
||||
env_key = config.get_provider_for_model(
|
||||
config.get_active_model()
|
||||
).api_key_env_var
|
||||
monkeypatch.delenv(env_key, raising=False)
|
||||
client = TelemetryClient(config_getter=lambda: config)
|
||||
assert client._get_mistral_api_key() is None
|
||||
client._client = MagicMock()
|
||||
client._client.post = AsyncMock()
|
||||
|
||||
client.send_telemetry_event("vibe/test", {})
|
||||
_run_telemetry_tasks()
|
||||
|
||||
client._client.post.assert_not_called()
|
||||
|
||||
def test_send_telemetry_event_does_nothing_when_disabled(
|
||||
self, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
config = build_test_vibe_config(disable_telemetry=True)
|
||||
env_key = config.get_provider_for_model(
|
||||
config.get_active_model()
|
||||
).api_key_env_var
|
||||
monkeypatch.setenv(env_key, "sk-test")
|
||||
client = TelemetryClient(config_getter=lambda: config)
|
||||
client._client = MagicMock()
|
||||
client._client.post = AsyncMock()
|
||||
|
||||
client.send_telemetry_event("vibe/test", {})
|
||||
_run_telemetry_tasks()
|
||||
|
||||
client._client.post.assert_not_called()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_send_telemetry_event_posts_when_enabled(
|
||||
self, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
monkeypatch.setattr(
|
||||
TelemetryClient, "send_telemetry_event", _original_send_telemetry_event
|
||||
)
|
||||
config = build_test_vibe_config(disable_telemetry=False)
|
||||
env_key = config.get_provider_for_model(
|
||||
config.get_active_model()
|
||||
).api_key_env_var
|
||||
monkeypatch.setenv(env_key, "sk-test")
|
||||
client = TelemetryClient(config_getter=lambda: config)
|
||||
mock_post = AsyncMock(return_value=MagicMock(status_code=204))
|
||||
client._client = MagicMock()
|
||||
client._client.post = mock_post
|
||||
client._client.aclose = AsyncMock()
|
||||
|
||||
client.send_telemetry_event("vibe/test_event", {"key": "value"})
|
||||
await client.aclose()
|
||||
|
||||
mock_post.assert_called_once_with(
|
||||
DATALAKE_EVENTS_URL,
|
||||
json={"event": "vibe/test_event", "properties": {"key": "value"}},
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": "Bearer sk-test",
|
||||
"User-Agent": get_user_agent(Backend.MISTRAL),
|
||||
},
|
||||
)
|
||||
|
||||
def test_send_tool_call_finished_payload_shape(
|
||||
self, telemetry_events: list[dict[str, Any]]
|
||||
) -> None:
|
||||
config = build_test_vibe_config(disable_telemetry=False)
|
||||
client = TelemetryClient(config_getter=lambda: config)
|
||||
tool_call = _make_resolved_tool_call("todo", {})
|
||||
decision = ToolDecision(
|
||||
verdict=ToolExecutionResponse.EXECUTE, approval_type=ToolPermission.ALWAYS
|
||||
)
|
||||
|
||||
client.send_tool_call_finished(
|
||||
tool_call=tool_call,
|
||||
status="success",
|
||||
decision=decision,
|
||||
agent_profile_name="default",
|
||||
)
|
||||
|
||||
assert len(telemetry_events) == 1
|
||||
event_name = telemetry_events[0]["event_name"]
|
||||
assert event_name == "vibe/tool_call_finished"
|
||||
properties = telemetry_events[0]["properties"]
|
||||
assert properties["tool_name"] == "todo"
|
||||
assert properties["status"] == "success"
|
||||
assert properties["decision"] == "execute"
|
||||
assert properties["approval_type"] == "always"
|
||||
assert properties["agent_profile_name"] == "default"
|
||||
assert properties["nb_files_created"] == 0
|
||||
assert properties["nb_files_modified"] == 0
|
||||
|
||||
def test_send_tool_call_finished_nb_files_created_write_file_new(
|
||||
self, telemetry_events: list[dict[str, Any]]
|
||||
) -> None:
|
||||
config = build_test_vibe_config(disable_telemetry=False)
|
||||
client = TelemetryClient(config_getter=lambda: config)
|
||||
tool_call = _make_resolved_tool_call("write_file", {"overwrite": False})
|
||||
|
||||
client.send_tool_call_finished(
|
||||
tool_call=tool_call,
|
||||
status="success",
|
||||
decision=None,
|
||||
agent_profile_name="default",
|
||||
result={"file_existed": False},
|
||||
)
|
||||
|
||||
assert telemetry_events[0]["properties"]["nb_files_created"] == 1
|
||||
assert telemetry_events[0]["properties"]["nb_files_modified"] == 0
|
||||
|
||||
def test_send_tool_call_finished_nb_files_modified_write_file_overwrite(
|
||||
self, telemetry_events: list[dict[str, Any]]
|
||||
) -> None:
|
||||
config = build_test_vibe_config(disable_telemetry=False)
|
||||
client = TelemetryClient(config_getter=lambda: config)
|
||||
tool_call = _make_resolved_tool_call("write_file", {"overwrite": True})
|
||||
|
||||
client.send_tool_call_finished(
|
||||
tool_call=tool_call,
|
||||
status="success",
|
||||
decision=None,
|
||||
agent_profile_name="default",
|
||||
result={"file_existed": True},
|
||||
)
|
||||
|
||||
assert telemetry_events[0]["properties"]["nb_files_created"] == 0
|
||||
assert telemetry_events[0]["properties"]["nb_files_modified"] == 1
|
||||
|
||||
def test_send_tool_call_finished_decision_none(
|
||||
self, telemetry_events: list[dict[str, Any]]
|
||||
) -> None:
|
||||
config = build_test_vibe_config(disable_telemetry=False)
|
||||
client = TelemetryClient(config_getter=lambda: config)
|
||||
tool_call = _make_resolved_tool_call("todo", {})
|
||||
|
||||
client.send_tool_call_finished(
|
||||
tool_call=tool_call,
|
||||
status="skipped",
|
||||
decision=None,
|
||||
agent_profile_name="default",
|
||||
)
|
||||
|
||||
assert telemetry_events[0]["properties"]["decision"] is None
|
||||
assert telemetry_events[0]["properties"]["approval_type"] is None
|
||||
|
||||
def test_send_user_copied_text_payload(
|
||||
self, telemetry_events: list[dict[str, Any]]
|
||||
) -> None:
|
||||
config = build_test_vibe_config(disable_telemetry=False)
|
||||
client = TelemetryClient(config_getter=lambda: config)
|
||||
|
||||
client.send_user_copied_text("hello world")
|
||||
|
||||
assert len(telemetry_events) == 1
|
||||
assert telemetry_events[0]["event_name"] == "vibe/user_copied_text"
|
||||
assert telemetry_events[0]["properties"]["text_length"] == 11
|
||||
|
||||
def test_send_user_cancelled_action_payload(
|
||||
self, telemetry_events: list[dict[str, Any]]
|
||||
) -> None:
|
||||
config = build_test_vibe_config(disable_telemetry=False)
|
||||
client = TelemetryClient(config_getter=lambda: config)
|
||||
|
||||
client.send_user_cancelled_action("interrupt_agent")
|
||||
|
||||
assert len(telemetry_events) == 1
|
||||
assert telemetry_events[0]["event_name"] == "vibe/user_cancelled_action"
|
||||
assert telemetry_events[0]["properties"]["action"] == "interrupt_agent"
|
||||
|
||||
def test_send_auto_compact_triggered_payload(
|
||||
self, telemetry_events: list[dict[str, Any]]
|
||||
) -> None:
|
||||
config = build_test_vibe_config(disable_telemetry=False)
|
||||
client = TelemetryClient(config_getter=lambda: config)
|
||||
|
||||
client.send_auto_compact_triggered()
|
||||
|
||||
assert len(telemetry_events) == 1
|
||||
assert telemetry_events[0]["event_name"] == "vibe/auto_compact_triggered"
|
||||
|
||||
def test_send_slash_command_used_payload(
|
||||
self, telemetry_events: list[dict[str, Any]]
|
||||
) -> None:
|
||||
config = build_test_vibe_config(disable_telemetry=False)
|
||||
client = TelemetryClient(config_getter=lambda: config)
|
||||
|
||||
client.send_slash_command_used("help", "builtin")
|
||||
client.send_slash_command_used("my_skill", "skill")
|
||||
|
||||
assert len(telemetry_events) == 2
|
||||
assert telemetry_events[0]["event_name"] == "vibe/slash_command_used"
|
||||
assert telemetry_events[0]["properties"]["command"] == "help"
|
||||
assert telemetry_events[0]["properties"]["command_type"] == "builtin"
|
||||
assert telemetry_events[1]["properties"]["command"] == "my_skill"
|
||||
assert telemetry_events[1]["properties"]["command_type"] == "skill"
|
||||
|
||||
def test_send_new_session_payload(
|
||||
self, telemetry_events: list[dict[str, Any]]
|
||||
) -> None:
|
||||
config = build_test_vibe_config(disable_telemetry=False)
|
||||
client = TelemetryClient(config_getter=lambda: config)
|
||||
|
||||
client.send_new_session(
|
||||
has_agents_md=True,
|
||||
nb_skills=2,
|
||||
nb_mcp_servers=1,
|
||||
nb_models=3,
|
||||
entrypoint="cli",
|
||||
)
|
||||
|
||||
assert len(telemetry_events) == 1
|
||||
event_name = telemetry_events[0]["event_name"]
|
||||
assert event_name == "vibe/new_session"
|
||||
properties = telemetry_events[0]["properties"]
|
||||
assert properties["has_agents_md"] is True
|
||||
assert properties["nb_skills"] == 2
|
||||
assert properties["nb_mcp_servers"] == 1
|
||||
assert properties["nb_models"] == 3
|
||||
assert properties["entrypoint"] == "cli"
|
||||
assert "version" in properties
|
||||
|
|
@ -8,7 +8,12 @@ import pytest
|
|||
import tomli_w
|
||||
|
||||
from vibe.core.paths.global_paths import TRUSTED_FOLDERS_FILE
|
||||
from vibe.core.trusted_folders import TrustedFoldersManager
|
||||
from vibe.core.trusted_folders import (
|
||||
AGENTS_MD_FILENAMES,
|
||||
TrustedFoldersManager,
|
||||
has_agents_md_file,
|
||||
has_trustable_content,
|
||||
)
|
||||
|
||||
|
||||
class TestTrustedFoldersManager:
|
||||
|
|
@ -203,3 +208,48 @@ class TestTrustedFoldersManager:
|
|||
manager.add_trusted(tmp_path)
|
||||
|
||||
assert manager.is_trusted(tmp_path) is True
|
||||
|
||||
|
||||
class TestHasAgentsMdFile:
|
||||
def test_returns_false_for_empty_directory(self, tmp_path: Path) -> None:
|
||||
assert has_agents_md_file(tmp_path) is False
|
||||
|
||||
def test_returns_true_when_agents_md_exists(self, tmp_path: Path) -> None:
|
||||
(tmp_path / "AGENTS.md").write_text("# Agents", encoding="utf-8")
|
||||
assert has_agents_md_file(tmp_path) is True
|
||||
|
||||
def test_returns_true_when_vibe_md_exists(self, tmp_path: Path) -> None:
|
||||
(tmp_path / "VIBE.md").write_text("# Vibe", encoding="utf-8")
|
||||
assert has_agents_md_file(tmp_path) is True
|
||||
|
||||
def test_returns_true_when_dot_vibe_md_exists(self, tmp_path: Path) -> None:
|
||||
(tmp_path / ".vibe.md").write_text("# Vibe", encoding="utf-8")
|
||||
assert has_agents_md_file(tmp_path) is True
|
||||
|
||||
def test_returns_false_when_only_other_files_exist(self, tmp_path: Path) -> None:
|
||||
(tmp_path / "README.md").write_text("", encoding="utf-8")
|
||||
(tmp_path / ".vibe").mkdir()
|
||||
assert has_agents_md_file(tmp_path) is False
|
||||
|
||||
def test_agents_md_filenames_constant(self) -> None:
|
||||
assert AGENTS_MD_FILENAMES == ["AGENTS.md", "VIBE.md", ".vibe.md"]
|
||||
|
||||
|
||||
class TestHasTrustableContent:
|
||||
def test_returns_true_when_vibe_dir_exists(self, tmp_path: Path) -> None:
|
||||
(tmp_path / ".vibe").mkdir()
|
||||
assert has_trustable_content(tmp_path) is True
|
||||
|
||||
def test_returns_true_when_agents_dir_exists(self, tmp_path: Path) -> None:
|
||||
(tmp_path / ".agents").mkdir()
|
||||
assert has_trustable_content(tmp_path) is True
|
||||
|
||||
def test_returns_true_when_agents_md_filename_exists(self, tmp_path: Path) -> None:
|
||||
for name in AGENTS_MD_FILENAMES:
|
||||
(tmp_path / name).write_text("", encoding="utf-8")
|
||||
assert has_trustable_content(tmp_path) is True
|
||||
(tmp_path / name).unlink()
|
||||
|
||||
def test_returns_false_when_no_trustable_content(self, tmp_path: Path) -> None:
|
||||
(tmp_path / "other.txt").write_text("", encoding="utf-8")
|
||||
assert has_trustable_content(tmp_path) is False
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue