"""Tests for tool settings functionality.""" import pytest import tempfile import shutil from pathlib import Path import yaml from cmdforge.tool import ensure_settings, save_tool, load_tool, Tool from cmdforge.runner import substitute_variables class TestEnsureSettings: """Tests for ensure_settings helper function.""" def test_creates_settings_from_defaults(self, tmp_path): """Settings.yaml created when defaults.yaml exists.""" tool_dir = tmp_path / "my-tool" tool_dir.mkdir() # Create defaults.yaml defaults_path = tool_dir / "defaults.yaml" defaults_path.write_text("backend: piper\nendpoint: http://localhost:5001\n") # Ensure settings.yaml doesn't exist yet settings_path = tool_dir / "settings.yaml" assert not settings_path.exists() # Call ensure_settings result = ensure_settings(tool_dir) # Check settings.yaml was created assert result == settings_path assert settings_path.exists() assert settings_path.read_text() == defaults_path.read_text() def test_preserves_existing_settings(self, tmp_path): """Existing settings.yaml not overwritten.""" tool_dir = tmp_path / "my-tool" tool_dir.mkdir() # Create defaults.yaml defaults_path = tool_dir / "defaults.yaml" defaults_path.write_text("backend: piper\n") # Create existing settings.yaml with different content settings_path = tool_dir / "settings.yaml" settings_path.write_text("backend: google\napi_key: my-key\n") # Call ensure_settings result = ensure_settings(tool_dir) # Check settings.yaml was not overwritten assert result == settings_path assert settings_path.read_text() == "backend: google\napi_key: my-key\n" def test_no_defaults_returns_none(self, tmp_path): """Returns None when no defaults.yaml exists.""" tool_dir = tmp_path / "my-tool" tool_dir.mkdir() result = ensure_settings(tool_dir) assert result is None assert not (tool_dir / "settings.yaml").exists() def test_existing_settings_no_defaults_returns_path(self, tmp_path): """Returns settings path when settings exists but defaults doesn't.""" tool_dir = tmp_path / "my-tool" tool_dir.mkdir() # Create only settings.yaml settings_path = tool_dir / "settings.yaml" settings_path.write_text("custom: value\n") result = ensure_settings(tool_dir) assert result == settings_path class TestSubstituteVariablesSettings: """Tests for {settings.key} substitution in templates.""" def test_settings_scalar_string(self): """Template substitution works for string settings.""" variables = {"settings": {"backend": "piper"}} result = substitute_variables("Using {settings.backend}", variables) assert result == "Using piper" def test_settings_scalar_int(self): """Template substitution works for int settings.""" variables = {"settings": {"port": 5001}} result = substitute_variables("Port: {settings.port}", variables) assert result == "Port: 5001" def test_settings_scalar_float(self): """Template substitution works for float settings.""" variables = {"settings": {"threshold": 0.75}} result = substitute_variables("Threshold: {settings.threshold}", variables) assert result == "Threshold: 0.75" def test_settings_scalar_bool(self): """Template substitution works for bool settings.""" variables = {"settings": {"enabled": True}} result = substitute_variables("Enabled: {settings.enabled}", variables) assert result == "Enabled: True" def test_settings_nonscalar_unchanged(self, capsys): """Non-scalar settings leave placeholder unchanged and warn.""" variables = {"settings": {"items": ["a", "b", "c"]}} result = substitute_variables("Items: {settings.items}", variables, warn_non_scalar=True) # Placeholder should be unchanged assert result == "Items: {settings.items}" # Should have printed a warning captured = capsys.readouterr() assert "not a scalar value" in captured.err def test_settings_missing_key_unchanged(self): """Missing settings key leaves placeholder unchanged.""" variables = {"settings": {"backend": "piper"}} result = substitute_variables("API: {settings.api_key}", variables) assert result == "API: {settings.api_key}" def test_settings_empty_dict(self): """Empty settings dict leaves placeholders unchanged.""" variables = {"settings": {}} result = substitute_variables("Backend: {settings.backend}", variables) assert result == "Backend: {settings.backend}" def test_mixed_variables_and_settings(self): """Regular variables and settings work together.""" variables = { "input": "Hello", "name": "World", "settings": {"backend": "piper"} } result = substitute_variables( "{input} {name}! Using {settings.backend}.", variables ) assert result == "Hello World! Using piper." def test_escaped_braces_preserved(self): """Escaped braces {{}} work with settings.""" variables = {"settings": {"key": "value"}} result = substitute_variables( "{{literal}} and {settings.key}", variables ) assert result == "{literal} and value" class TestRunnerLoadsSettings: """Integration tests for runner loading settings.""" def test_settings_available_in_code_step(self, monkeypatch, tmp_path): """Settings dict is available in code steps.""" from cmdforge.tool import TOOLS_DIR from cmdforge.runner import run_tool # Patch TOOLS_DIR monkeypatch.setattr("cmdforge.tool.TOOLS_DIR", tmp_path) # Create tool directory with config and settings tool_dir = tmp_path / "test-settings-tool" tool_dir.mkdir() config = { "name": "test-settings-tool", "description": "Test tool", "steps": [ { "type": "code", "code": "result = f\"backend={settings.get('backend', 'none')}\"", "output_var": "result" } ], "output": "{result}" } (tool_dir / "config.yaml").write_text(yaml.dump(config)) settings = {"backend": "piper", "api_key": "test-key"} (tool_dir / "settings.yaml").write_text(yaml.dump(settings)) # Load and run the tool tool = load_tool("test-settings-tool") output, exit_code = run_tool(tool, "test input", {}) assert exit_code == 0 assert "backend=piper" in output