import json import os import tempfile from companion.config import load_config def test_load_config_reads_json_and_expands_tilde(): with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f: json.dump( { "companion": { "name": "SAN", "persona": { "role": "companion", "tone": "reflective", "style": "questioning", "boundaries": [], }, "memory": { "session_turns": 20, "persistent_store": "~/mem.db", "summarize_after": 10, }, "chat": { "streaming": True, "max_response_tokens": 2048, "default_temperature": 0.7, "allow_temperature_override": True, }, }, "vault": { "path": "~/test-vault", "indexing": { "auto_sync": False, "auto_sync_interval_minutes": 1440, "watch_fs_events": False, "file_patterns": ["*.md"], "deny_dirs": [".git"], "deny_patterns": [".*"], }, "chunking_rules": {}, }, "rag": { "embedding": { "provider": "ollama", "model": "dummy", "base_url": "http://localhost:11434", "dimensions": 4, "batch_size": 2, }, "vector_store": { "type": "lancedb", "path": "~/.companion/vectors.lance", }, "search": { "default_top_k": 8, "max_top_k": 20, "similarity_threshold": 0.75, "hybrid_search": { "enabled": False, "keyword_weight": 0.3, "semantic_weight": 0.7, }, "filters": { "date_range_enabled": True, "tag_filter_enabled": True, "directory_filter_enabled": True, }, }, }, "model": { "inference": { "backend": "llama.cpp", "model_path": "", "context_length": 8192, "gpu_layers": 35, "batch_size": 512, "threads": 8, }, "fine_tuning": { "base_model": "", "output_dir": "", "lora_rank": 16, "lora_alpha": 32, "learning_rate": 0.0002, "batch_size": 4, "gradient_accumulation_steps": 4, "num_epochs": 3, "warmup_steps": 100, "save_steps": 500, "eval_steps": 250, "training_data_path": "", "validation_split": 0.1, }, "retrain_schedule": { "auto_reminder": True, "default_interval_days": 90, "reminder_channels": [], }, }, "api": { "host": "127.0.0.1", "port": 7373, "cors_origins": [], "auth": {"enabled": False}, }, "ui": { "web": { "enabled": True, "theme": "obsidian", "features": { "streaming": True, "citations": True, "source_preview": True, }, }, "cli": {"enabled": True, "rich_output": True}, }, "logging": { "level": "INFO", "file": "", "max_size_mb": 100, "backup_count": 5, }, "security": { "local_only": True, "vault_path_traversal_check": True, "sensitive_content_detection": True, "sensitive_patterns": [], "require_confirmation_for_external_apis": True, }, }, f, ) path = f.name try: config = load_config(path) assert config.vault.path == os.path.expanduser("~/test-vault") assert config.rag.vector_store.path == os.path.expanduser( "~/.companion/vectors.lance" ) assert config.companion.memory.persistent_store == os.path.expanduser( "~/mem.db" ) finally: os.unlink(path) def test_tilde_expansion_in_list_values(): """Test that tilde expansion works for list values.""" with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f: json.dump( { "companion": { "name": "SAN", "persona": { "role": "companion", "tone": "reflective", "style": "questioning", "boundaries": [], }, "memory": { "session_turns": 20, "persistent_store": "~/mem.db", "summarize_after": 10, }, "chat": { "streaming": True, "max_response_tokens": 2048, "default_temperature": 0.7, "allow_temperature_override": True, }, }, "vault": { "path": "~/test-vault", "indexing": { "auto_sync": False, "auto_sync_interval_minutes": 1440, "watch_fs_events": False, "file_patterns": ["*.md"], "deny_dirs": ["~/secret", ".git"], "deny_patterns": [".*"], }, "chunking_rules": {}, }, "rag": { "embedding": { "provider": "ollama", "model": "dummy", "base_url": "http://localhost:11434", "dimensions": 4, "batch_size": 2, }, "vector_store": { "type": "lancedb", "path": "~/.companion/vectors.lance", }, "search": { "default_top_k": 8, "max_top_k": 20, "similarity_threshold": 0.75, "hybrid_search": { "enabled": False, "keyword_weight": 0.3, "semantic_weight": 0.7, }, "filters": { "date_range_enabled": True, "tag_filter_enabled": True, "directory_filter_enabled": True, }, }, }, "model": { "inference": { "backend": "llama.cpp", "model_path": "", "context_length": 8192, "gpu_layers": 35, "batch_size": 512, "threads": 8, }, "fine_tuning": { "base_model": "", "output_dir": "", "lora_rank": 16, "lora_alpha": 32, "learning_rate": 0.0002, "batch_size": 4, "gradient_accumulation_steps": 4, "num_epochs": 3, "warmup_steps": 100, "save_steps": 500, "eval_steps": 250, "training_data_path": "", "validation_split": 0.1, }, "retrain_schedule": { "auto_reminder": True, "default_interval_days": 90, "reminder_channels": [], }, }, "api": { "host": "127.0.0.1", "port": 7373, "cors_origins": [], "auth": {"enabled": False}, }, "ui": { "web": { "enabled": True, "theme": "obsidian", "features": { "streaming": True, "citations": True, "source_preview": True, }, }, "cli": {"enabled": True, "rich_output": True}, }, "logging": { "level": "INFO", "file": "", "max_size_mb": 100, "backup_count": 5, }, "security": { "local_only": True, "vault_path_traversal_check": True, "sensitive_content_detection": True, "sensitive_patterns": [], "require_confirmation_for_external_apis": True, }, }, f, ) path = f.name try: config = load_config(path) # Check tilde expansion in list values assert config.vault.indexing.deny_dirs[0] == os.path.expanduser("~/secret") finally: os.unlink(path) def test_load_config_missing_file(): """Test that loading a missing file raises FileNotFoundError.""" with tempfile.TemporaryDirectory() as tmp: missing_path = os.path.join(tmp, "nonexistent.json") try: load_config(missing_path) assert False, "Should have raised an exception" except FileNotFoundError: pass # Expected def test_load_config_malformed_json(): """Test that malformed JSON raises JSONDecodeError.""" with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f: f.write("{invalid json") path = f.name try: load_config(path) assert False, "Should have raised an exception" except json.JSONDecodeError: pass # Expected finally: os.unlink(path) def test_load_config_missing_required_field(): """Test that missing required field raises ValidationError.""" with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f: # Missing required fields like "companion" json.dump({"vault": {"path": "~/test"}}, f) path = f.name try: load_config(path) assert False, "Should have raised an exception" except Exception as e: # Should be a Pydantic validation error assert "companion" in str(e).lower() or "validation" in str(e).lower() finally: os.unlink(path)