from pathlib import Path import pytest from lai_automation.config import Settings def settings(**overrides) -> Settings: values = { "database_host": "db", "database_port": 3306, "database_name": "lai", "database_user": "worker", "database_password": "secret", "source_repository": "https://example.test/lai.git", "source_branch": "main", "workspace_root": Path("/tmp/workspaces"), "mode": "dry-run", "allow_direct_main": False, "gitea_url": "https://example.test", "gitea_owner": "owner", "gitea_repository": "lai", "gitea_token": "", "gitea_username": "bot", "agent_command": "aider", "agent_model": "mistral/test", "agent_timeout_seconds": 900, "agent_idle_timeout_seconds": 300, "setup_commands": (), "test_commands": ("php artisan test",), "test_database_host": "test-db", "test_database_port": 3306, "test_database_name": "lai_agent_test", "test_database_user": "test_runner", "test_database_password": "test-secret", "test_customer_database": "customer_agent_test", "test_config_database": "cockpit_agent_test", "poll_seconds": 60, "max_changed_files": 12, "max_changed_lines": 800, "max_agent_attempts": 2, "stale_claim_minutes": 90, "dry_run": True, } values.update(overrides) return Settings(**values) def test_direct_main_requires_explicit_kill_switch() -> None: config = settings(mode="direct-main", dry_run=False, gitea_token="token") with pytest.raises(ValueError, match="ALLOW_DIRECT_MAIN"): config.validate() def test_pr_requires_gitea_token() -> None: config = settings(mode="pr", dry_run=False) with pytest.raises(ValueError, match="GITEA_TOKEN"): config.validate() def test_default_commands_prepare_workspace_and_run_tests(monkeypatch) -> None: monkeypatch.setenv("FAILURE_DB_PASSWORD", "failure-secret") monkeypatch.setenv("TEST_DB_PASSWORD", "test-secret") monkeypatch.delenv("SETUP_COMMANDS", raising=False) monkeypatch.delenv("TEST_COMMANDS", raising=False) config = Settings.from_env() assert "npm ci" in config.setup_commands assert "npm run build" in config.setup_commands assert "php artisan migrate:fresh --force" in config.setup_commands assert config.test_commands == ("php artisan test",) def test_agent_attempts_are_bounded() -> None: with pytest.raises(ValueError, match="MAX_AGENT_ATTEMPTS"): settings(max_agent_attempts=8).validate() settings(max_agent_attempts=7).validate() def test_agent_timeout_is_bounded() -> None: with pytest.raises(ValueError, match="AGENT_TIMEOUT_SECONDS"): settings(agent_timeout_seconds=59).validate() settings(agent_timeout_seconds=60, agent_idle_timeout_seconds=60).validate() def test_agent_idle_timeout_is_bounded_by_process_timeout() -> None: with pytest.raises(ValueError, match="AGENT_IDLE_TIMEOUT_SECONDS"): settings(agent_idle_timeout_seconds=59).validate() with pytest.raises(ValueError, match="AGENT_IDLE_TIMEOUT_SECONDS"): settings(agent_timeout_seconds=120, agent_idle_timeout_seconds=121).validate() settings(agent_timeout_seconds=120, agent_idle_timeout_seconds=60).validate() def test_validation_databases_must_be_distinct_and_explicitly_test_only() -> None: with pytest.raises(ValueError, match="isolated test database"): settings(test_customer_database="customer_production").validate() with pytest.raises(ValueError, match="must be distinct"): settings(test_config_database="customer_agent_test").validate() with pytest.raises(ValueError, match="FAILURE_DB_NAME"): settings( database_name="lai_agent_test", test_database_name="lai_agent_test", ).validate()