ring/installer/tests/test_utils.py
Fred Amaral e3ece79ccf
feat(installer): introduce python-based multi-platform installer
refactor(installer): introduce adapter and transformer architecture
fix(security): harden installer against path traversal and symlinks
docs: add comprehensive guides for multi-platform support and migration
fix(beads): improve stop hook logic to check for open issues
2025-11-27 18:20:53 -03:00

987 lines
32 KiB
Python

"""
Tests for utility modules.
Tests fs.py (filesystem utilities), platform_detect.py (platform detection),
and version.py (semver comparison).
"""
import json
import os
import sys
from pathlib import Path
from unittest.mock import patch, MagicMock
import pytest
# ==============================================================================
# Tests for fs.py (Filesystem Utilities)
# ==============================================================================
class TestEnsureDirectory:
"""Tests for ensure_directory() function."""
def test_creates_new_directory(self, tmp_path):
"""ensure_directory() should create a new directory."""
from ring_installer.utils.fs import ensure_directory
new_dir = tmp_path / "new_directory"
assert not new_dir.exists()
result = ensure_directory(new_dir)
assert new_dir.exists()
assert new_dir.is_dir()
assert result == new_dir
def test_creates_nested_directories(self, tmp_path):
"""ensure_directory() should create nested directories."""
from ring_installer.utils.fs import ensure_directory
nested_dir = tmp_path / "level1" / "level2" / "level3"
result = ensure_directory(nested_dir)
assert nested_dir.exists()
assert result == nested_dir
def test_existing_directory_unchanged(self, tmp_path):
"""ensure_directory() should not fail on existing directory."""
from ring_installer.utils.fs import ensure_directory
existing = tmp_path / "existing"
existing.mkdir()
result = ensure_directory(existing)
assert existing.exists()
assert result == existing
def test_raises_if_file_exists(self, tmp_path):
"""ensure_directory() should raise NotADirectoryError if path is a file."""
from ring_installer.utils.fs import ensure_directory
file_path = tmp_path / "a_file"
file_path.write_text("content")
with pytest.raises(NotADirectoryError):
ensure_directory(file_path)
def test_expands_user_path(self, tmp_path):
"""ensure_directory() should expand ~ in paths."""
from ring_installer.utils.fs import ensure_directory
# Create a mock expanduser that returns tmp_path
with patch.object(Path, 'expanduser', return_value=tmp_path / "expanded"):
path = Path("~/test_dir")
# The actual behavior depends on implementation
# This test verifies expanduser is called
class TestBackupExisting:
"""Tests for backup_existing() function."""
def test_creates_backup_of_file(self, tmp_path):
"""backup_existing() should create backup of existing file."""
from ring_installer.utils.fs import backup_existing
original = tmp_path / "original.txt"
original.write_text("original content")
backup_path = backup_existing(original)
assert backup_path is not None
assert backup_path.exists()
assert backup_path.read_text() == "original content"
assert "backup" in backup_path.name
def test_creates_backup_of_directory(self, tmp_path):
"""backup_existing() should create backup of existing directory."""
from ring_installer.utils.fs import backup_existing
original_dir = tmp_path / "original_dir"
original_dir.mkdir()
(original_dir / "file.txt").write_text("content")
backup_path = backup_existing(original_dir)
assert backup_path is not None
assert backup_path.exists()
assert (backup_path / "file.txt").exists()
def test_returns_none_if_not_exists(self, tmp_path):
"""backup_existing() should return None if path doesn't exist."""
from ring_installer.utils.fs import backup_existing
nonexistent = tmp_path / "nonexistent"
result = backup_existing(nonexistent)
assert result is None
def test_uses_custom_backup_dir(self, tmp_path):
"""backup_existing() should use custom backup directory."""
from ring_installer.utils.fs import backup_existing
original = tmp_path / "original.txt"
original.write_text("content")
backup_dir = tmp_path / "backups"
backup_path = backup_existing(original, backup_dir=backup_dir)
assert backup_path.parent == backup_dir
def test_backup_name_includes_timestamp(self, tmp_path):
"""backup_existing() should include timestamp in backup name."""
from ring_installer.utils.fs import backup_existing
original = tmp_path / "original.txt"
original.write_text("content")
backup_path = backup_existing(original)
# Backup name format: original.backup_YYYYMMDD_HHMMSS
assert "backup_" in backup_path.name
# Should have date-like pattern
parts = backup_path.name.split("backup_")
assert len(parts[1]) >= 15 # YYYYMMDD_HHMMSS
class TestCopyWithTransform:
"""Tests for copy_with_transform() function."""
def test_copies_file_without_transform(self, tmp_path):
"""copy_with_transform() should copy file without transformation."""
from ring_installer.utils.fs import copy_with_transform
source = tmp_path / "source.txt"
source.write_text("source content")
target = tmp_path / "target.txt"
result = copy_with_transform(source, target)
assert target.exists()
assert target.read_text() == "source content"
assert result == target
def test_applies_transformation(self, tmp_path):
"""copy_with_transform() should apply transformation function."""
from ring_installer.utils.fs import copy_with_transform
source = tmp_path / "source.txt"
source.write_text("hello world")
target = tmp_path / "target.txt"
result = copy_with_transform(
source,
target,
transform_func=lambda c: c.upper()
)
assert target.read_text() == "HELLO WORLD"
def test_creates_target_directory(self, tmp_path):
"""copy_with_transform() should create target directory if needed."""
from ring_installer.utils.fs import copy_with_transform
source = tmp_path / "source.txt"
source.write_text("content")
target = tmp_path / "nested" / "dir" / "target.txt"
copy_with_transform(source, target)
assert target.exists()
def test_raises_if_source_missing(self, tmp_path):
"""copy_with_transform() should raise FileNotFoundError if source missing."""
from ring_installer.utils.fs import copy_with_transform
source = tmp_path / "nonexistent.txt"
target = tmp_path / "target.txt"
with pytest.raises(FileNotFoundError):
copy_with_transform(source, target)
class TestSafeRemove:
"""Tests for safe_remove() function."""
def test_removes_file(self, tmp_path):
"""safe_remove() should remove a file."""
from ring_installer.utils.fs import safe_remove
file_path = tmp_path / "to_remove.txt"
file_path.write_text("content")
result = safe_remove(file_path)
assert result is True
assert not file_path.exists()
def test_removes_directory(self, tmp_path):
"""safe_remove() should remove a directory and its contents."""
from ring_installer.utils.fs import safe_remove
dir_path = tmp_path / "to_remove"
dir_path.mkdir()
(dir_path / "file.txt").write_text("content")
result = safe_remove(dir_path)
assert result is True
assert not dir_path.exists()
def test_missing_ok_true(self, tmp_path):
"""safe_remove() should not raise if path missing and missing_ok=True."""
from ring_installer.utils.fs import safe_remove
nonexistent = tmp_path / "nonexistent"
result = safe_remove(nonexistent, missing_ok=True)
assert result is False
def test_missing_ok_false_raises(self, tmp_path):
"""safe_remove() should raise if path missing and missing_ok=False."""
from ring_installer.utils.fs import safe_remove
nonexistent = tmp_path / "nonexistent"
with pytest.raises(FileNotFoundError):
safe_remove(nonexistent, missing_ok=False)
class TestGetFileHash:
"""Tests for get_file_hash() function."""
def test_returns_sha256_hash(self, tmp_path):
"""get_file_hash() should return SHA256 hash by default."""
from ring_installer.utils.fs import get_file_hash
file_path = tmp_path / "test.txt"
file_path.write_text("test content")
hash_value = get_file_hash(file_path)
assert len(hash_value) == 64 # SHA256 produces 64 hex chars
assert all(c in "0123456789abcdef" for c in hash_value)
def test_same_content_same_hash(self, tmp_path):
"""get_file_hash() should return same hash for same content."""
from ring_installer.utils.fs import get_file_hash
file1 = tmp_path / "file1.txt"
file2 = tmp_path / "file2.txt"
file1.write_text("identical content")
file2.write_text("identical content")
assert get_file_hash(file1) == get_file_hash(file2)
def test_different_content_different_hash(self, tmp_path):
"""get_file_hash() should return different hash for different content."""
from ring_installer.utils.fs import get_file_hash
file1 = tmp_path / "file1.txt"
file2 = tmp_path / "file2.txt"
file1.write_text("content A")
file2.write_text("content B")
assert get_file_hash(file1) != get_file_hash(file2)
def test_supports_md5_algorithm(self, tmp_path):
"""get_file_hash() should support MD5 algorithm."""
from ring_installer.utils.fs import get_file_hash
file_path = tmp_path / "test.txt"
file_path.write_text("test")
hash_value = get_file_hash(file_path, algorithm="md5")
assert len(hash_value) == 32 # MD5 produces 32 hex chars
def test_raises_if_file_missing(self, tmp_path):
"""get_file_hash() should raise FileNotFoundError if file missing."""
from ring_installer.utils.fs import get_file_hash
with pytest.raises(FileNotFoundError):
get_file_hash(tmp_path / "nonexistent.txt")
class TestFilesAreIdentical:
"""Tests for files_are_identical() function."""
def test_identical_files_returns_true(self, tmp_path):
"""files_are_identical() should return True for identical files."""
from ring_installer.utils.fs import files_are_identical
file1 = tmp_path / "file1.txt"
file2 = tmp_path / "file2.txt"
file1.write_text("same content")
file2.write_text("same content")
assert files_are_identical(file1, file2) is True
def test_different_files_returns_false(self, tmp_path):
"""files_are_identical() should return False for different files."""
from ring_installer.utils.fs import files_are_identical
file1 = tmp_path / "file1.txt"
file2 = tmp_path / "file2.txt"
file1.write_text("content A")
file2.write_text("content B")
assert files_are_identical(file1, file2) is False
def test_missing_file_returns_false(self, tmp_path):
"""files_are_identical() should return False if either file missing."""
from ring_installer.utils.fs import files_are_identical
file1 = tmp_path / "exists.txt"
file1.write_text("content")
file2 = tmp_path / "nonexistent.txt"
assert files_are_identical(file1, file2) is False
assert files_are_identical(file2, file1) is False
class TestListFilesRecursive:
"""Tests for list_files_recursive() function."""
def test_lists_all_files(self, tmp_path):
"""list_files_recursive() should list all files in directory."""
from ring_installer.utils.fs import list_files_recursive
(tmp_path / "file1.txt").write_text("1")
(tmp_path / "file2.md").write_text("2")
nested = tmp_path / "nested"
nested.mkdir()
(nested / "file3.py").write_text("3")
files = list_files_recursive(tmp_path)
assert len(files) == 3
def test_filters_by_extension(self, tmp_path):
"""list_files_recursive() should filter by extension."""
from ring_installer.utils.fs import list_files_recursive
(tmp_path / "file1.txt").write_text("1")
(tmp_path / "file2.md").write_text("2")
(tmp_path / "file3.py").write_text("3")
files = list_files_recursive(tmp_path, extensions=[".md"])
assert len(files) == 1
assert files[0].suffix == ".md"
def test_excludes_patterns(self, tmp_path):
"""list_files_recursive() should exclude patterns."""
from ring_installer.utils.fs import list_files_recursive
(tmp_path / "file.txt").write_text("1")
cache_dir = tmp_path / "__pycache__"
cache_dir.mkdir()
(cache_dir / "cached.pyc").write_text("c")
files = list_files_recursive(tmp_path, exclude_patterns=["__pycache__"])
assert len(files) == 1
assert all("__pycache__" not in str(f) for f in files)
class TestAtomicWrite:
"""Tests for atomic_write() function."""
def test_writes_string_content(self, tmp_path):
"""atomic_write() should write string content."""
from ring_installer.utils.fs import atomic_write
file_path = tmp_path / "output.txt"
atomic_write(file_path, "test content")
assert file_path.read_text() == "test content"
def test_writes_bytes_content(self, tmp_path):
"""atomic_write() should write bytes content."""
from ring_installer.utils.fs import atomic_write
file_path = tmp_path / "output.bin"
atomic_write(file_path, b"\x00\x01\x02")
assert file_path.read_bytes() == b"\x00\x01\x02"
def test_no_partial_writes(self, tmp_path):
"""atomic_write() should not leave partial files on failure."""
from ring_installer.utils.fs import atomic_write
file_path = tmp_path / "output.txt"
# Write initial content
atomic_write(file_path, "initial")
# The atomic write should be all-or-nothing
assert file_path.read_text() == "initial"
# ==============================================================================
# Tests for platform_detect.py (Platform Detection)
# ==============================================================================
class TestPlatformInfo:
"""Tests for PlatformInfo dataclass."""
def test_create_platform_info(self):
"""PlatformInfo should be creatable with basic attributes."""
from ring_installer.utils.platform_detect import PlatformInfo
info = PlatformInfo(
platform_id="test",
name="Test Platform",
installed=True
)
assert info.platform_id == "test"
assert info.name == "Test Platform"
assert info.installed is True
assert info.version is None
assert info.details == {}
def test_platform_info_with_all_fields(self):
"""PlatformInfo should accept all optional fields."""
from ring_installer.utils.platform_detect import PlatformInfo
info = PlatformInfo(
platform_id="test",
name="Test Platform",
installed=True,
version="1.0.0",
install_path=Path("/test"),
config_path=Path("/test/config"),
binary_path=Path("/usr/bin/test"),
details={"extra": "info"}
)
assert info.version == "1.0.0"
assert info.install_path == Path("/test")
assert info.details["extra"] == "info"
class TestDetectInstalledPlatforms:
"""Tests for detect_installed_platforms() function."""
def test_returns_list(self, mock_platform_detection):
"""detect_installed_platforms() should return a list."""
from ring_installer.utils.platform_detect import detect_installed_platforms
result = detect_installed_platforms()
assert isinstance(result, list)
def test_returns_only_installed(self, mock_platform_detection):
"""detect_installed_platforms() should return only installed platforms."""
from ring_installer.utils.platform_detect import PlatformInfo, detect_installed_platforms
# Set Claude as installed
mock_platform_detection["claude"].return_value = PlatformInfo(
platform_id="claude",
name="Claude Code",
installed=True,
version="1.0.0"
)
result = detect_installed_platforms()
assert len(result) == 1
assert result[0].platform_id == "claude"
class TestIsPlatformInstalled:
"""Tests for is_platform_installed() function."""
def test_returns_true_if_installed(self, mock_platform_detection):
"""is_platform_installed() should return True for installed platform."""
from ring_installer.utils.platform_detect import PlatformInfo, is_platform_installed
mock_platform_detection["claude"].return_value = PlatformInfo(
platform_id="claude",
name="Claude Code",
installed=True
)
assert is_platform_installed("claude") is True
def test_returns_false_if_not_installed(self, mock_platform_detection):
"""is_platform_installed() should return False for uninstalled platform."""
from ring_installer.utils.platform_detect import is_platform_installed
assert is_platform_installed("claude") is False
class TestGetPlatformVersion:
"""Tests for get_platform_version() function."""
def test_returns_version_if_installed(self, mock_platform_detection):
"""get_platform_version() should return version for installed platform."""
from ring_installer.utils.platform_detect import PlatformInfo, get_platform_version
mock_platform_detection["cursor"].return_value = PlatformInfo(
platform_id="cursor",
name="Cursor",
installed=True,
version="0.42.0"
)
assert get_platform_version("cursor") == "0.42.0"
def test_returns_none_if_not_installed(self, mock_platform_detection):
"""get_platform_version() should return None for uninstalled platform."""
from ring_installer.utils.platform_detect import get_platform_version
assert get_platform_version("cursor") is None
class TestGetSystemInfo:
"""Tests for get_system_info() function."""
def test_returns_system_info(self):
"""get_system_info() should return system information dict."""
from ring_installer.utils.platform_detect import get_system_info
info = get_system_info()
assert "platform" in info
assert info["platform"] == sys.platform
assert "python_version" in info
assert "home_directory" in info
assert "path" in info
# ==============================================================================
# Tests for version.py (Version Comparison)
# ==============================================================================
class TestVersionParsing:
"""Tests for Version class parsing."""
def test_parse_basic_version(self):
"""Version.parse() should parse basic version string."""
from ring_installer.utils.version import Version
v = Version.parse("1.2.3")
assert v.major == 1
assert v.minor == 2
assert v.patch == 3
assert v.prerelease == ""
assert v.build == ""
def test_parse_with_v_prefix(self):
"""Version.parse() should handle 'v' prefix."""
from ring_installer.utils.version import Version
v = Version.parse("v2.0.0")
assert v.major == 2
assert v.minor == 0
assert v.patch == 0
def test_parse_with_prerelease(self):
"""Version.parse() should parse prerelease identifier."""
from ring_installer.utils.version import Version
v = Version.parse("1.0.0-alpha.1")
assert v.major == 1
assert v.prerelease == "alpha.1"
def test_parse_with_build(self):
"""Version.parse() should parse build metadata."""
from ring_installer.utils.version import Version
v = Version.parse("1.0.0+build.123")
assert v.build == "build.123"
def test_parse_full_version(self):
"""Version.parse() should parse version with prerelease and build."""
from ring_installer.utils.version import Version
v = Version.parse("1.0.0-beta.2+build.456")
assert v.prerelease == "beta.2"
assert v.build == "build.456"
def test_parse_invalid_raises(self):
"""Version.parse() should raise ValueError for invalid version."""
from ring_installer.utils.version import Version
with pytest.raises(ValueError):
Version.parse("invalid")
with pytest.raises(ValueError):
Version.parse("1.2") # Missing patch
with pytest.raises(ValueError):
Version.parse("1.2.3.4") # Too many parts
class TestVersionComparison:
"""Tests for Version comparison operations."""
def test_equal_versions(self):
"""Equal versions should compare as equal."""
from ring_installer.utils.version import Version
v1 = Version.parse("1.0.0")
v2 = Version.parse("1.0.0")
assert v1 == v2
def test_major_comparison(self):
"""Major version should be compared first."""
from ring_installer.utils.version import Version
assert Version.parse("2.0.0") > Version.parse("1.9.9")
assert Version.parse("1.0.0") < Version.parse("2.0.0")
def test_minor_comparison(self):
"""Minor version should be compared when major is equal."""
from ring_installer.utils.version import Version
assert Version.parse("1.2.0") > Version.parse("1.1.9")
assert Version.parse("1.1.0") < Version.parse("1.2.0")
def test_patch_comparison(self):
"""Patch version should be compared when major and minor are equal."""
from ring_installer.utils.version import Version
assert Version.parse("1.0.2") > Version.parse("1.0.1")
assert Version.parse("1.0.0") < Version.parse("1.0.1")
def test_prerelease_lower_than_release(self):
"""Prerelease versions should be lower than release versions."""
from ring_installer.utils.version import Version
assert Version.parse("1.0.0-alpha") < Version.parse("1.0.0")
assert Version.parse("1.0.0") > Version.parse("1.0.0-rc.1")
def test_prerelease_comparison(self):
"""Prerelease identifiers should be compared alphabetically."""
from ring_installer.utils.version import Version
assert Version.parse("1.0.0-alpha") < Version.parse("1.0.0-beta")
assert Version.parse("1.0.0-alpha.1") < Version.parse("1.0.0-alpha.2")
def test_comparison_operators(self):
"""All comparison operators should work."""
from ring_installer.utils.version import Version
v1 = Version.parse("1.0.0")
v2 = Version.parse("1.0.1")
assert v1 < v2
assert v1 <= v2
assert v1 <= v1
assert v2 > v1
assert v2 >= v1
assert v1 >= v1
class TestVersionString:
"""Tests for Version string representation."""
def test_str_basic(self):
"""Version should convert to basic version string."""
from ring_installer.utils.version import Version
v = Version(1, 2, 3)
assert str(v) == "1.2.3"
def test_str_with_prerelease(self):
"""Version should include prerelease in string."""
from ring_installer.utils.version import Version
v = Version(1, 0, 0, prerelease="alpha")
assert str(v) == "1.0.0-alpha"
def test_str_with_build(self):
"""Version should include build metadata in string."""
from ring_installer.utils.version import Version
v = Version(1, 0, 0, build="123")
assert str(v) == "1.0.0+123"
class TestVersionMethods:
"""Tests for Version utility methods."""
def test_is_prerelease(self):
"""is_prerelease() should correctly identify prerelease versions."""
from ring_installer.utils.version import Version
assert Version.parse("1.0.0-alpha").is_prerelease() is True
assert Version.parse("1.0.0").is_prerelease() is False
def test_bump_major(self):
"""bump_major() should increment major version."""
from ring_installer.utils.version import Version
v = Version.parse("1.2.3")
bumped = v.bump_major()
assert str(bumped) == "2.0.0"
def test_bump_minor(self):
"""bump_minor() should increment minor version."""
from ring_installer.utils.version import Version
v = Version.parse("1.2.3")
bumped = v.bump_minor()
assert str(bumped) == "1.3.0"
def test_bump_patch(self):
"""bump_patch() should increment patch version."""
from ring_installer.utils.version import Version
v = Version.parse("1.2.3")
bumped = v.bump_patch()
assert str(bumped) == "1.2.4"
class TestCompareVersions:
"""Tests for compare_versions() function."""
def test_compare_versions(self, version_test_cases):
"""compare_versions() should correctly compare versions."""
from ring_installer.utils.version import compare_versions
for v1, v2, expected in version_test_cases:
result = compare_versions(v1, v2)
assert result == expected, f"compare_versions({v1}, {v2}) = {result}, expected {expected}"
class TestIsUpdateAvailable:
"""Tests for is_update_available() function."""
def test_update_available(self):
"""is_update_available() should return True when update available."""
from ring_installer.utils.version import is_update_available
assert is_update_available("1.0.0", "1.0.1") is True
assert is_update_available("1.0.0", "2.0.0") is True
def test_no_update_available(self):
"""is_update_available() should return False when up to date."""
from ring_installer.utils.version import is_update_available
assert is_update_available("1.0.0", "1.0.0") is False
assert is_update_available("2.0.0", "1.0.0") is False
class TestInstallManifest:
"""Tests for InstallManifest class."""
def test_create_manifest(self):
"""InstallManifest.create() should create manifest with defaults."""
from ring_installer.utils.version import InstallManifest
manifest = InstallManifest.create(
version="1.0.0",
source_path="/path/to/ring",
platform="claude"
)
assert manifest.version == "1.0.0"
assert manifest.source_path == "/path/to/ring"
assert manifest.platform == "claude"
assert manifest.installed_at != "" # Should have timestamp
def test_to_dict(self):
"""InstallManifest.to_dict() should convert to dictionary."""
from ring_installer.utils.version import InstallManifest
manifest = InstallManifest.create(
version="1.0.0",
source_path="/path",
platform="claude",
plugins=["default"],
files={"a.md": "hash"}
)
data = manifest.to_dict()
assert data["version"] == "1.0.0"
assert data["plugins"] == ["default"]
assert data["files"]["a.md"] == "hash"
def test_from_dict(self):
"""InstallManifest.from_dict() should create from dictionary."""
from ring_installer.utils.version import InstallManifest
data = {
"version": "2.0.0",
"installed_at": "2024-01-01T00:00:00",
"source_path": "/path",
"platform": "cursor",
"plugins": ["test"],
"files": {},
"metadata": {}
}
manifest = InstallManifest.from_dict(data)
assert manifest.version == "2.0.0"
assert manifest.platform == "cursor"
def test_save_and_load(self, tmp_path):
"""InstallManifest should save and load from file."""
from ring_installer.utils.version import InstallManifest
manifest = InstallManifest.create(
version="1.0.0",
source_path="/path",
platform="claude",
plugins=["default"],
files={"test.md": "abc123"}
)
manifest_path = tmp_path / ".ring-manifest.json"
manifest.save(manifest_path)
loaded = InstallManifest.load(manifest_path)
assert loaded is not None
assert loaded.version == "1.0.0"
assert loaded.files["test.md"] == "abc123"
def test_load_nonexistent_returns_none(self, tmp_path):
"""InstallManifest.load() should return None for missing file."""
from ring_installer.utils.version import InstallManifest
result = InstallManifest.load(tmp_path / "nonexistent.json")
assert result is None
class TestGetRingVersion:
"""Tests for get_ring_version() function."""
def test_from_marketplace_json(self, tmp_ring_root):
"""get_ring_version() should read from marketplace.json."""
from ring_installer.utils.version import get_ring_version
version = get_ring_version(tmp_ring_root)
assert version == "1.2.3"
def test_from_version_file(self, tmp_path):
"""get_ring_version() should read from VERSION file."""
from ring_installer.utils.version import get_ring_version
(tmp_path / "VERSION").write_text("2.0.0")
version = get_ring_version(tmp_path)
assert version == "2.0.0"
def test_returns_none_if_not_found(self, tmp_path):
"""get_ring_version() should return None if no version found."""
from ring_installer.utils.version import get_ring_version
version = get_ring_version(tmp_path)
assert version is None
class TestGetManifestPath:
"""Tests for get_manifest_path() function."""
def test_returns_manifest_path(self, tmp_path):
"""get_manifest_path() should return .ring-manifest.json path."""
from ring_installer.utils.version import get_manifest_path
path = get_manifest_path(tmp_path)
assert path == tmp_path / ".ring-manifest.json"
class TestCheckForUpdates:
"""Tests for check_for_updates() function."""
def test_detects_update_available(self, tmp_ring_root, tmp_install_dir):
"""check_for_updates() should detect when update is available."""
from ring_installer.utils.version import (
check_for_updates,
InstallManifest,
get_manifest_path
)
# Create old manifest in install dir
old_manifest = InstallManifest.create(
version="1.0.0", # Older than tmp_ring_root's 1.2.3
source_path=str(tmp_ring_root),
platform="claude"
)
old_manifest.save(get_manifest_path(tmp_install_dir))
result = check_for_updates(tmp_ring_root, tmp_install_dir, "claude")
assert result.update_available is True
assert result.is_newer is True
assert result.installed_version == "1.0.0"
assert result.available_version == "1.2.3"
def test_no_update_when_same_version(self, tmp_ring_root, tmp_install_dir):
"""check_for_updates() should detect no update when versions match."""
from ring_installer.utils.version import (
check_for_updates,
InstallManifest,
get_manifest_path
)
# Create manifest with same version
manifest = InstallManifest.create(
version="1.2.3",
source_path=str(tmp_ring_root),
platform="claude"
)
manifest.save(get_manifest_path(tmp_install_dir))
result = check_for_updates(tmp_ring_root, tmp_install_dir, "claude")
assert result.update_available is False
assert result.is_newer is False
class TestSaveInstallManifest:
"""Tests for save_install_manifest() function."""
def test_saves_manifest(self, tmp_path):
"""save_install_manifest() should create manifest file."""
from ring_installer.utils.version import (
save_install_manifest,
get_manifest_path,
InstallManifest
)
manifest = save_install_manifest(
install_path=tmp_path,
source_path=Path("/source"),
platform="cursor",
version="1.0.0",
plugins=["default"],
installed_files={"a.md": "hash1"}
)
# Verify file was created
manifest_path = get_manifest_path(tmp_path)
assert manifest_path.exists()
# Verify content
loaded = InstallManifest.load(manifest_path)
assert loaded.version == "1.0.0"
assert loaded.platform == "cursor"