536 lines
18 KiB
Python
536 lines
18 KiB
Python
"""Tests for settings and session serialization."""
|
|
|
|
import json
|
|
import pytest
|
|
from pathlib import Path
|
|
|
|
from development_hub.settings import Settings
|
|
|
|
|
|
class TestSettingsPersistence:
|
|
"""Test settings persistence and round-trips."""
|
|
|
|
@pytest.fixture
|
|
def isolated_settings(self, tmp_path, monkeypatch):
|
|
"""Create an isolated Settings instance with temp files."""
|
|
# Reset singleton
|
|
Settings._instance = None
|
|
|
|
# Point to temp directory
|
|
settings_file = tmp_path / "settings.json"
|
|
session_file = tmp_path / "session.json"
|
|
|
|
monkeypatch.setattr(Settings, "_settings_file", settings_file)
|
|
monkeypatch.setattr(Settings, "_session_file", session_file)
|
|
|
|
settings = Settings()
|
|
yield settings
|
|
|
|
# Clean up singleton
|
|
Settings._instance = None
|
|
|
|
def test_default_values(self, isolated_settings):
|
|
"""Settings have sensible defaults."""
|
|
settings = isolated_settings
|
|
|
|
assert settings.deploy_docs_after_creation == True
|
|
assert settings.preferred_editor == "auto"
|
|
assert settings.auto_start_docs_server == True
|
|
assert isinstance(settings.project_search_paths, list)
|
|
assert isinstance(settings.project_ignore_folders, list)
|
|
|
|
def test_set_and_get(self, isolated_settings):
|
|
"""Values can be set and retrieved."""
|
|
settings = isolated_settings
|
|
|
|
settings.preferred_editor = "pycharm"
|
|
assert settings.preferred_editor == "pycharm"
|
|
|
|
settings.deploy_docs_after_creation = False
|
|
assert settings.deploy_docs_after_creation == False
|
|
|
|
def test_persistence_round_trip(self, isolated_settings, tmp_path):
|
|
"""Settings persist across instances."""
|
|
settings = isolated_settings
|
|
|
|
# Modify settings
|
|
settings.preferred_editor = "code"
|
|
settings.auto_start_docs_server = False
|
|
settings.set("custom_key", "custom_value")
|
|
|
|
# Reset singleton and create new instance
|
|
Settings._instance = None
|
|
new_settings = Settings()
|
|
|
|
assert new_settings.preferred_editor == "code"
|
|
assert new_settings.auto_start_docs_server == False
|
|
assert new_settings.get("custom_key") == "custom_value"
|
|
|
|
def test_file_created_on_save(self, isolated_settings, tmp_path):
|
|
"""Settings file is created when saving."""
|
|
settings = isolated_settings
|
|
|
|
settings.preferred_editor = "xed"
|
|
|
|
# Check file exists
|
|
assert Settings._settings_file.exists()
|
|
|
|
def test_invalid_json_handled(self, isolated_settings, tmp_path):
|
|
"""Invalid JSON in settings file doesn't crash."""
|
|
# Write invalid JSON
|
|
Settings._settings_file.parent.mkdir(parents=True, exist_ok=True)
|
|
Settings._settings_file.write_text("{ invalid json }")
|
|
|
|
# Reset and reload
|
|
Settings._instance = None
|
|
settings = Settings()
|
|
|
|
# Should fall back to defaults
|
|
assert settings.preferred_editor == "auto"
|
|
|
|
|
|
class TestSessionSerialization:
|
|
"""Test session state serialization."""
|
|
|
|
@pytest.fixture
|
|
def isolated_settings(self, tmp_path, monkeypatch):
|
|
"""Create an isolated Settings instance with temp files."""
|
|
Settings._instance = None
|
|
|
|
settings_file = tmp_path / "settings.json"
|
|
session_file = tmp_path / "session.json"
|
|
|
|
monkeypatch.setattr(Settings, "_settings_file", settings_file)
|
|
monkeypatch.setattr(Settings, "_session_file", session_file)
|
|
|
|
settings = Settings()
|
|
yield settings
|
|
|
|
Settings._instance = None
|
|
|
|
def test_save_load_session_round_trip(self, isolated_settings):
|
|
"""Session state round-trips correctly."""
|
|
settings = isolated_settings
|
|
|
|
state = {
|
|
"window_geometry": {"x": 100, "y": 200, "width": 800, "height": 600},
|
|
"splitter_sizes": [300, 500],
|
|
"panes": [
|
|
{
|
|
"tabs": [
|
|
{"type": "terminal", "cwd": "/home/user/project", "title": "Terminal 1"},
|
|
{"type": "dashboard", "project_key": "my-project"},
|
|
],
|
|
"active_tab": 0,
|
|
}
|
|
],
|
|
"active_pane": 0,
|
|
}
|
|
|
|
settings.save_session(state)
|
|
loaded = settings.load_session()
|
|
|
|
assert loaded == state
|
|
|
|
def test_save_session_creates_directory(self, isolated_settings, tmp_path):
|
|
"""Session save creates config directory if needed."""
|
|
settings = isolated_settings
|
|
|
|
# Ensure parent doesn't exist
|
|
if Settings._session_file.parent.exists():
|
|
import shutil
|
|
shutil.rmtree(Settings._session_file.parent)
|
|
|
|
state = {"test": True}
|
|
settings.save_session(state)
|
|
|
|
assert Settings._session_file.exists()
|
|
|
|
def test_load_empty_session(self, isolated_settings):
|
|
"""Loading non-existent session returns empty dict."""
|
|
settings = isolated_settings
|
|
loaded = settings.load_session()
|
|
assert loaded == {}
|
|
|
|
def test_load_invalid_session(self, isolated_settings):
|
|
"""Invalid JSON in session returns empty dict."""
|
|
settings = isolated_settings
|
|
|
|
Settings._session_file.parent.mkdir(parents=True, exist_ok=True)
|
|
Settings._session_file.write_text("not valid json")
|
|
|
|
loaded = settings.load_session()
|
|
assert loaded == {}
|
|
|
|
def test_session_preserves_types(self, isolated_settings):
|
|
"""Session preserves various data types."""
|
|
settings = isolated_settings
|
|
|
|
state = {
|
|
"string": "hello",
|
|
"number": 42,
|
|
"float": 3.14,
|
|
"bool": True,
|
|
"none": None,
|
|
"list": [1, 2, 3],
|
|
"nested": {"a": {"b": "c"}},
|
|
}
|
|
|
|
settings.save_session(state)
|
|
loaded = settings.load_session()
|
|
|
|
assert loaded["string"] == "hello"
|
|
assert loaded["number"] == 42
|
|
assert loaded["float"] == 3.14
|
|
assert loaded["bool"] == True
|
|
assert loaded["none"] is None
|
|
assert loaded["list"] == [1, 2, 3]
|
|
assert loaded["nested"]["a"]["b"] == "c"
|
|
|
|
def test_session_handles_unicode(self, isolated_settings):
|
|
"""Session handles unicode characters."""
|
|
settings = isolated_settings
|
|
|
|
state = {
|
|
"emoji": "Test",
|
|
"japanese": "Hello",
|
|
"symbols": "angle bracket < > quotes \" '",
|
|
}
|
|
|
|
settings.save_session(state)
|
|
loaded = settings.load_session()
|
|
|
|
assert loaded == state
|
|
|
|
|
|
class TestGitHostSettings:
|
|
"""Test git hosting configuration."""
|
|
|
|
@pytest.fixture
|
|
def isolated_settings(self, tmp_path, monkeypatch):
|
|
"""Create an isolated Settings instance."""
|
|
Settings._instance = None
|
|
|
|
monkeypatch.setattr(Settings, "_settings_file", tmp_path / "settings.json")
|
|
monkeypatch.setattr(Settings, "_session_file", tmp_path / "session.json")
|
|
|
|
settings = Settings()
|
|
yield settings
|
|
|
|
Settings._instance = None
|
|
|
|
def test_git_not_configured_by_default(self, isolated_settings):
|
|
"""Git is not configured initially."""
|
|
settings = isolated_settings
|
|
assert settings.is_git_configured == False
|
|
|
|
def test_git_configured_when_all_set(self, isolated_settings):
|
|
"""Git is configured when all required fields are set."""
|
|
settings = isolated_settings
|
|
|
|
settings.git_host_type = "gitea"
|
|
settings.git_host_url = "https://gitea.example.com"
|
|
settings.git_host_owner = "myuser"
|
|
|
|
assert settings.is_git_configured == True
|
|
|
|
def test_git_not_configured_if_missing_field(self, isolated_settings):
|
|
"""Git not configured if any required field is missing."""
|
|
settings = isolated_settings
|
|
|
|
settings.git_host_type = "github"
|
|
settings.git_host_url = "https://github.com"
|
|
# Missing: git_host_owner
|
|
|
|
assert settings.is_git_configured == False
|
|
|
|
|
|
class TestDocsModeSettings:
|
|
"""Test documentation mode configuration."""
|
|
|
|
@pytest.fixture
|
|
def isolated_settings(self, tmp_path, monkeypatch):
|
|
"""Create an isolated Settings instance."""
|
|
Settings._instance = None
|
|
|
|
monkeypatch.setattr(Settings, "_settings_file", tmp_path / "settings.json")
|
|
monkeypatch.setattr(Settings, "_session_file", tmp_path / "session.json")
|
|
|
|
settings = Settings()
|
|
yield settings
|
|
|
|
Settings._instance = None
|
|
|
|
def test_default_docs_mode(self, isolated_settings):
|
|
"""Default docs mode is auto."""
|
|
settings = isolated_settings
|
|
assert settings.docs_mode == "auto"
|
|
|
|
def test_set_docs_mode(self, isolated_settings):
|
|
"""Can set docs mode."""
|
|
settings = isolated_settings
|
|
|
|
settings.docs_mode = "standalone"
|
|
assert settings.docs_mode == "standalone"
|
|
|
|
settings.docs_mode = "project-docs"
|
|
assert settings.docs_mode == "project-docs"
|
|
|
|
def test_effective_docs_mode_standalone_when_no_project_docs(self, isolated_settings, tmp_path):
|
|
"""Effective mode is standalone when project-docs doesn't exist."""
|
|
settings = isolated_settings
|
|
settings.project_search_paths = [str(tmp_path)]
|
|
|
|
# No project-docs folder exists
|
|
assert settings.effective_docs_mode == "standalone"
|
|
|
|
def test_effective_docs_mode_project_docs_when_exists(self, isolated_settings, tmp_path):
|
|
"""Effective mode is project-docs when folder exists."""
|
|
settings = isolated_settings
|
|
settings.project_search_paths = [str(tmp_path)]
|
|
|
|
# Create project-docs folder
|
|
(tmp_path / "project-docs").mkdir()
|
|
|
|
assert settings.effective_docs_mode == "project-docs"
|
|
|
|
def test_explicit_mode_overrides_auto(self, isolated_settings, tmp_path):
|
|
"""Explicit mode setting overrides auto-detection."""
|
|
settings = isolated_settings
|
|
settings.project_search_paths = [str(tmp_path)]
|
|
|
|
# Create project-docs (would trigger project-docs mode in auto)
|
|
(tmp_path / "project-docs").mkdir()
|
|
|
|
# But explicitly set to standalone
|
|
settings.docs_mode = "standalone"
|
|
assert settings.effective_docs_mode == "standalone"
|
|
|
|
def test_docs_root_standalone_mode(self, isolated_settings):
|
|
"""Docs root in standalone mode uses local share."""
|
|
settings = isolated_settings
|
|
settings.docs_mode = "standalone"
|
|
|
|
docs_root = settings.docs_root
|
|
assert ".local/share/development-hub" in str(docs_root)
|
|
|
|
def test_docusaurus_path_property(self, isolated_settings, tmp_path):
|
|
"""Can set and get docusaurus path."""
|
|
settings = isolated_settings
|
|
|
|
settings.docusaurus_path = tmp_path / "my-docs"
|
|
assert settings.docusaurus_path == tmp_path / "my-docs"
|
|
|
|
def test_pages_url_property(self, isolated_settings):
|
|
"""Can set and get pages URL."""
|
|
settings = isolated_settings
|
|
|
|
settings.pages_url = "https://pages.example.com"
|
|
assert settings.pages_url == "https://pages.example.com"
|
|
|
|
def test_pages_url_derived_from_gitea(self, isolated_settings):
|
|
"""Pages URL can be derived from gitea URL."""
|
|
settings = isolated_settings
|
|
|
|
settings.git_host_type = "gitea"
|
|
settings.git_host_url = "https://gitea.example.com"
|
|
|
|
# When not explicitly set, should derive
|
|
assert "pages.example.com" in settings.pages_url
|
|
|
|
def test_is_docs_enabled_standalone(self, isolated_settings):
|
|
"""Docs are always enabled in standalone mode."""
|
|
settings = isolated_settings
|
|
settings.docs_mode = "standalone"
|
|
|
|
assert settings.is_docs_enabled == True
|
|
|
|
def test_cmdforge_path_property(self, isolated_settings, tmp_path):
|
|
"""Can set and get cmdforge path."""
|
|
settings = isolated_settings
|
|
|
|
settings.cmdforge_path = tmp_path / "CmdForge"
|
|
assert settings.cmdforge_path == tmp_path / "CmdForge"
|
|
|
|
def test_progress_dir_property(self, isolated_settings, tmp_path):
|
|
"""Can set and get progress directory."""
|
|
settings = isolated_settings
|
|
|
|
settings.progress_dir = tmp_path / "progress"
|
|
assert settings.progress_dir == tmp_path / "progress"
|
|
|
|
|
|
class TestWorkspaceExportImport:
|
|
"""Test workspace file export/import."""
|
|
|
|
@pytest.fixture
|
|
def isolated_settings(self, tmp_path, monkeypatch):
|
|
"""Create an isolated Settings instance."""
|
|
Settings._instance = None
|
|
|
|
monkeypatch.setattr(Settings, "_settings_file", tmp_path / "settings.json")
|
|
monkeypatch.setattr(Settings, "_session_file", tmp_path / "session.json")
|
|
|
|
settings = Settings()
|
|
yield settings
|
|
|
|
Settings._instance = None
|
|
|
|
def test_export_workspace_creates_file(self, isolated_settings, tmp_path):
|
|
"""Export creates a YAML workspace file."""
|
|
settings = isolated_settings
|
|
workspace_path = tmp_path / "workspace.yaml"
|
|
|
|
settings.export_workspace(workspace_path)
|
|
|
|
assert workspace_path.exists()
|
|
|
|
def test_export_workspace_contains_required_fields(self, isolated_settings, tmp_path):
|
|
"""Exported workspace contains required fields."""
|
|
import yaml
|
|
|
|
settings = isolated_settings
|
|
settings.project_search_paths = [str(tmp_path / "projects")]
|
|
workspace_path = tmp_path / "workspace.yaml"
|
|
|
|
settings.export_workspace(workspace_path)
|
|
|
|
with open(workspace_path) as f:
|
|
workspace = yaml.safe_load(f)
|
|
|
|
assert "name" in workspace
|
|
assert "version" in workspace
|
|
assert workspace["version"] == 1
|
|
assert "paths" in workspace
|
|
assert "projects_root" in workspace["paths"]
|
|
assert "documentation" in workspace
|
|
assert "features" in workspace
|
|
|
|
def test_export_includes_git_hosting_when_configured(self, isolated_settings, tmp_path):
|
|
"""Exported workspace includes git hosting when configured."""
|
|
import yaml
|
|
|
|
settings = isolated_settings
|
|
settings.git_host_type = "github"
|
|
settings.git_host_url = "https://github.com"
|
|
settings.git_host_owner = "testuser"
|
|
|
|
workspace_path = tmp_path / "workspace.yaml"
|
|
settings.export_workspace(workspace_path)
|
|
|
|
with open(workspace_path) as f:
|
|
workspace = yaml.safe_load(f)
|
|
|
|
assert "git_hosting" in workspace
|
|
assert workspace["git_hosting"]["type"] == "github"
|
|
assert workspace["git_hosting"]["url"] == "https://github.com"
|
|
assert workspace["git_hosting"]["owner"] == "testuser"
|
|
|
|
def test_import_workspace_sets_values(self, isolated_settings, tmp_path):
|
|
"""Import workspace sets settings values."""
|
|
import yaml
|
|
|
|
settings = isolated_settings
|
|
|
|
workspace = {
|
|
"name": "Test Workspace",
|
|
"version": 1,
|
|
"paths": {
|
|
"projects_root": str(tmp_path / "my-projects")
|
|
},
|
|
"documentation": {
|
|
"mode": "standalone"
|
|
}
|
|
}
|
|
|
|
workspace_path = tmp_path / "workspace.yaml"
|
|
with open(workspace_path, "w") as f:
|
|
yaml.dump(workspace, f)
|
|
|
|
results = settings.import_workspace(workspace_path)
|
|
|
|
assert "projects_root" in results["imported"]
|
|
assert settings.project_search_paths == [str(tmp_path / "my-projects")]
|
|
assert settings.docs_mode == "standalone"
|
|
|
|
def test_import_workspace_sets_git_hosting(self, isolated_settings, tmp_path):
|
|
"""Import workspace sets git hosting values."""
|
|
import yaml
|
|
|
|
settings = isolated_settings
|
|
|
|
workspace = {
|
|
"name": "Test",
|
|
"version": 1,
|
|
"paths": {"projects_root": str(tmp_path)},
|
|
"git_hosting": {
|
|
"type": "gitea",
|
|
"url": "https://git.example.com",
|
|
"owner": "myorg",
|
|
"pages_url": "https://pages.example.com"
|
|
}
|
|
}
|
|
|
|
workspace_path = tmp_path / "workspace.yaml"
|
|
with open(workspace_path, "w") as f:
|
|
yaml.dump(workspace, f)
|
|
|
|
settings.import_workspace(workspace_path)
|
|
|
|
assert settings.git_host_type == "gitea"
|
|
assert settings.git_host_url == "https://git.example.com"
|
|
assert settings.git_host_owner == "myorg"
|
|
assert settings.pages_url == "https://pages.example.com"
|
|
|
|
def test_import_marks_setup_completed(self, isolated_settings, tmp_path):
|
|
"""Import workspace marks setup as completed."""
|
|
import yaml
|
|
|
|
settings = isolated_settings
|
|
|
|
workspace = {
|
|
"name": "Test",
|
|
"version": 1,
|
|
"paths": {"projects_root": str(tmp_path)}
|
|
}
|
|
|
|
workspace_path = tmp_path / "workspace.yaml"
|
|
with open(workspace_path, "w") as f:
|
|
yaml.dump(workspace, f)
|
|
|
|
settings.import_workspace(workspace_path)
|
|
|
|
assert settings.get("setup_completed") == True
|
|
|
|
def test_export_import_round_trip(self, isolated_settings, tmp_path):
|
|
"""Export and import preserves settings."""
|
|
settings = isolated_settings
|
|
|
|
# Configure settings
|
|
settings.project_search_paths = [str(tmp_path / "projects")]
|
|
settings.docs_mode = "project-docs"
|
|
settings.git_host_type = "gitlab"
|
|
settings.git_host_url = "https://gitlab.com"
|
|
settings.git_host_owner = "myuser"
|
|
|
|
# Export
|
|
workspace_path = tmp_path / "workspace.yaml"
|
|
settings.export_workspace(workspace_path)
|
|
|
|
# Reset settings
|
|
Settings._instance = None
|
|
new_settings = Settings()
|
|
|
|
# Import
|
|
new_settings.import_workspace(workspace_path)
|
|
|
|
assert new_settings.project_search_paths == [str(tmp_path / "projects")]
|
|
assert new_settings.docs_mode == "project-docs"
|
|
assert new_settings.git_host_type == "gitlab"
|
|
assert new_settings.git_host_url == "https://gitlab.com"
|
|
assert new_settings.git_host_owner == "myuser"
|
|
|
|
|
|
if __name__ == "__main__":
|
|
pytest.main([__file__, "-v"])
|