import pytest from audita.core.config import ( AuditaConfig, ConfigOverrides, DEFAULT_GLOSSARY_CONFIDENCE_THRESHOLD, DEFAULT_GRAMMAR_CONFIDENCE_THRESHOLD, DEFAULT_HOMOPHONES_CONFIDENCE_THRESHOLD, DEFAULT_LLM_CONCURRENCY, DEFAULT_LLM_TIMEOUT_SECONDS, DEFAULT_MAX_SECTION_TOKENS, DEFAULT_MIN_SECTION_TOKENS, DEFAULT_NORMALIZE_MAX_SEGMENT_GAP, DEFAULT_SPOKEN_WORD_CONFIDENCE_THRESHOLD, DEFAULT_VALIDATION_MAX_PROMPT_TOKENS, DEFAULT_WORK_DIR_RETENTION, ) from audita.core.errors import AuditaConfigError from audita.modules import DEFAULT_MODULE_KEYS def test_default_config_allows_missing_api_key(): config = AuditaConfig.from_sources(env={}) assert config.api_key is None assert config.llm_concurrency == DEFAULT_LLM_CONCURRENCY assert config.llm_timeout_seconds == DEFAULT_LLM_TIMEOUT_SECONDS assert config.validation_llm_api_key is None assert config.validation_llm_concurrency is None assert config.validation_llm_timeout_seconds is None assert config.validation_model is None assert config.validation_base_url is None assert config.validation_max_retries is None assert config.validation_max_prompt_tokens == DEFAULT_VALIDATION_MAX_PROMPT_TOKENS assert config.target_sections is None assert config.max_section_tokens == DEFAULT_MAX_SECTION_TOKENS assert config.min_section_tokens == DEFAULT_MIN_SECTION_TOKENS assert config.module_keys == DEFAULT_MODULE_KEYS assert config.glossary_confidence_threshold == DEFAULT_GLOSSARY_CONFIDENCE_THRESHOLD assert config.grammar_confidence_threshold == DEFAULT_GRAMMAR_CONFIDENCE_THRESHOLD assert config.homophones_confidence_threshold == DEFAULT_HOMOPHONES_CONFIDENCE_THRESHOLD assert config.spoken_word_confidence_threshold == DEFAULT_SPOKEN_WORD_CONFIDENCE_THRESHOLD assert config.normalize_max_segment_gap == DEFAULT_NORMALIZE_MAX_SEGMENT_GAP assert config.work_dir_retention == DEFAULT_WORK_DIR_RETENTION def test_cli_overrides_take_precedence(): config = AuditaConfig.from_sources( env={"AUDITA_MAX_SECTION_TOKENS": "1000"}, overrides=ConfigOverrides(max_section_tokens=2000, min_section_tokens=1000), ) assert config.max_section_tokens == 2000 def test_min_section_tokens_cli_override_takes_precedence(): config = AuditaConfig.from_sources( env={"AUDITA_MIN_SECTION_TOKENS": "2000"}, overrides=ConfigOverrides(min_section_tokens=6000), ) assert config.min_section_tokens == 6000 def test_llm_concurrency_cli_override_takes_precedence(): config = AuditaConfig.from_sources( env={"AUDITA_LLM_CONCURRENCY": "2"}, overrides=ConfigOverrides(llm_concurrency=4), ) assert config.llm_concurrency == 4 def test_llm_concurrency_env_is_parsed(): config = AuditaConfig.from_sources(env={"AUDITA_LLM_CONCURRENCY": "3"}) assert config.llm_concurrency == 3 def test_llm_timeout_seconds_cli_override_takes_precedence(): config = AuditaConfig.from_sources( env={"AUDITA_LLM_TIMEOUT_SECONDS": "120"}, overrides=ConfigOverrides(llm_timeout_seconds=900.0), ) assert config.llm_timeout_seconds == 900.0 def test_llm_timeout_seconds_env_is_parsed(): config = AuditaConfig.from_sources(env={"AUDITA_LLM_TIMEOUT_SECONDS": "120.5"}) assert config.llm_timeout_seconds == 120.5 def test_validation_llm_concurrency_cli_override_takes_precedence(): config = AuditaConfig.from_sources( env={"AUDITA_VALIDATION_LLM_CONCURRENCY": "2"}, overrides=ConfigOverrides(validation_llm_concurrency=4), ) assert config.validation_llm_concurrency == 4 def test_validation_llm_concurrency_env_is_parsed(): config = AuditaConfig.from_sources(env={"AUDITA_VALIDATION_LLM_CONCURRENCY": "3"}) assert config.validation_llm_concurrency == 3 def test_validation_llm_timeout_seconds_cli_override_takes_precedence(): config = AuditaConfig.from_sources( env={"AUDITA_VALIDATION_LLM_TIMEOUT_SECONDS": "120"}, overrides=ConfigOverrides(validation_llm_timeout_seconds=900.0), ) assert config.validation_llm_timeout_seconds == 900.0 def test_validation_llm_timeout_seconds_env_is_parsed(): config = AuditaConfig.from_sources(env={"AUDITA_VALIDATION_LLM_TIMEOUT_SECONDS": "120.5"}) assert config.validation_llm_timeout_seconds == 120.5 def test_validation_model_env_is_parsed(): config = AuditaConfig.from_sources(env={"AUDITA_VALIDATION_MODEL": "validator-model"}) assert config.validation_model == "validator-model" def test_validation_base_url_env_is_parsed(): config = AuditaConfig.from_sources(env={"AUDITA_VALIDATION_BASE_URL": "http://localhost:9000/v1"}) assert config.validation_base_url == "http://localhost:9000/v1" def test_validation_max_retries_env_is_parsed(): config = AuditaConfig.from_sources(env={"AUDITA_VALIDATION_MAX_RETRIES": "7"}) assert config.validation_max_retries == 7 def test_validation_max_prompt_tokens_cli_override_takes_precedence(): config = AuditaConfig.from_sources( env={"AUDITA_VALIDATION_MAX_PROMPT_TOKENS": "1024"}, overrides=ConfigOverrides(validation_max_prompt_tokens=4096), ) assert config.validation_max_prompt_tokens == 4096 def test_validation_max_prompt_tokens_env_is_parsed(): config = AuditaConfig.from_sources(env={"AUDITA_VALIDATION_MAX_PROMPT_TOKENS": "1024"}) assert config.validation_max_prompt_tokens == 1024 def test_target_sections_cli_override_takes_precedence(): config = AuditaConfig.from_sources( env={"AUDITA_TARGET_SECTIONS": "2"}, overrides=ConfigOverrides(target_sections=5), ) assert config.target_sections == 5 def test_target_sections_env_is_parsed(): config = AuditaConfig.from_sources(env={"AUDITA_TARGET_SECTIONS": "3"}) assert config.target_sections == 3 def test_min_section_tokens_env_is_parsed(): config = AuditaConfig.from_sources(env={"AUDITA_MIN_SECTION_TOKENS": "3000"}) assert config.min_section_tokens == 3000 def test_generic_llm_api_key_env_is_read(): config = AuditaConfig.from_sources(env={"AUDITA_LLM_API_KEY": "generic-key"}) assert config.api_key == "generic-key" def test_generic_llm_api_key_takes_precedence_over_openrouter_env(): config = AuditaConfig.from_sources( env={ "AUDITA_LLM_API_KEY": "generic-key", "OPENROUTER_API_KEY": "legacy-key", } ) assert config.api_key == "generic-key" def test_llm_api_key_cli_override_takes_precedence_over_env(): config = AuditaConfig.from_sources( env={ "AUDITA_LLM_API_KEY": "generic-key", "OPENROUTER_API_KEY": "legacy-key", }, overrides=ConfigOverrides(llm_api_key="cli-key"), ) assert config.api_key == "cli-key" def test_blank_llm_api_key_override_resolves_to_none(): config = AuditaConfig.from_sources( env={"OPENROUTER_API_KEY": "legacy-key"}, overrides=ConfigOverrides(llm_api_key=" "), ) assert config.api_key is None def test_validation_llm_api_key_env_is_read(): config = AuditaConfig.from_sources(env={"AUDITA_VALIDATION_LLM_API_KEY": "validation-key"}) assert config.validation_llm_api_key == "validation-key" def test_blank_validation_llm_api_key_override_disables_primary_fallback(): config = AuditaConfig.from_sources( env={"AUDITA_LLM_API_KEY": "primary-key"}, overrides=ConfigOverrides(validation_llm_api_key=" "), ) assert config.validation_llm_api_key == "" assert config.validation_llm_config().api_key == "" def test_effective_validation_fields_fall_back_to_primary_settings(): config = AuditaConfig.from_sources( env={ "AUDITA_LLM_API_KEY": "primary-key", "AUDITA_MODEL": "primary-model", "AUDITA_BASE_URL": "http://localhost:8000/v1", "AUDITA_LLM_TIMEOUT_SECONDS": "120", "AUDITA_LLM_CONCURRENCY": "5", "AUDITA_MAX_RETRIES": "9", } ) validation = config.validation_llm_config() assert validation.api_key == "primary-key" assert validation.model == "primary-model" assert validation.base_url == "http://localhost:8000/v1" assert validation.llm_timeout_seconds == 120.0 assert validation.llm_concurrency == 5 assert validation.max_retries == 9 def test_effective_validation_fields_use_overrides_when_set(): config = AuditaConfig.from_sources( env={ "AUDITA_LLM_API_KEY": "primary-key", "AUDITA_MODEL": "primary-model", "AUDITA_BASE_URL": "http://localhost:8000/v1", "AUDITA_LLM_TIMEOUT_SECONDS": "120", "AUDITA_LLM_CONCURRENCY": "5", "AUDITA_MAX_RETRIES": "9", "AUDITA_VALIDATION_LLM_API_KEY": "validation-key", "AUDITA_VALIDATION_MODEL": "validation-model", "AUDITA_VALIDATION_BASE_URL": "http://localhost:9000/v1", "AUDITA_VALIDATION_LLM_TIMEOUT_SECONDS": "240", "AUDITA_VALIDATION_LLM_CONCURRENCY": "3", "AUDITA_VALIDATION_MAX_RETRIES": "2", } ) validation = config.validation_llm_config() assert validation.api_key == "validation-key" assert validation.model == "validation-model" assert validation.base_url == "http://localhost:9000/v1" assert validation.llm_timeout_seconds == 240.0 assert validation.llm_concurrency == 3 assert validation.max_retries == 2 def test_module_key_overrides_take_precedence(): config = AuditaConfig.from_sources( env={"AUDITA_MODULES": "grammar"}, overrides=ConfigOverrides(module_keys="homophones,grammar"), ) assert config.module_keys == ("homophones", "grammar") def test_module_key_env_is_parsed_and_trimmed(): config = AuditaConfig.from_sources(env={"AUDITA_MODULES": " glossary , grammar "}) assert config.module_keys == ("glossary", "grammar") def test_invalid_work_dir_retention_is_rejected(): with pytest.raises(AuditaConfigError): AuditaConfig.from_sources(env={"AUDITA_WORK_DIR_RETENTION": "sometimes"}) def test_report_dict_includes_llm_timeout_seconds(): config = AuditaConfig.from_sources(env={"AUDITA_LLM_TIMEOUT_SECONDS": "321"}) assert config.to_report_dict()["llm_timeout_seconds"] == 321.0 def test_report_dict_includes_target_sections(): config = AuditaConfig.from_sources(env={"AUDITA_TARGET_SECTIONS": "7"}) assert config.to_report_dict()["target_sections"] == 7 def test_report_dict_includes_effective_validation_llm_config(): config = AuditaConfig.from_sources( env={ "AUDITA_LLM_API_KEY": "primary-key", "AUDITA_VALIDATION_MODEL": "validation-model", } ) report = config.to_report_dict() assert report["validation_model"] == "validation-model" assert report["validation_max_prompt_tokens"] == DEFAULT_VALIDATION_MAX_PROMPT_TOKENS assert report["effective_validation_llm"]["api_key_configured"] is True assert report["effective_validation_llm"]["model"] == "validation-model" def test_threshold_overrides_take_precedence(): config = AuditaConfig.from_sources( env={ "AUDITA_GLOSSARY_CONFIDENCE_THRESHOLD": "0.6", "AUDITA_GRAMMAR_CONFIDENCE_THRESHOLD": "0.65", "AUDITA_HOMOPHONES_CONFIDENCE_THRESHOLD": "0.7", "AUDITA_SPOKEN_WORD_CONFIDENCE_THRESHOLD": "0.75", }, overrides=ConfigOverrides( glossary_confidence_threshold=0.85, grammar_confidence_threshold=0.88, homophones_confidence_threshold=0.9, spoken_word_confidence_threshold=0.95, ), ) assert config.glossary_confidence_threshold == 0.85 assert config.grammar_confidence_threshold == 0.88 assert config.homophones_confidence_threshold == 0.9 assert config.spoken_word_confidence_threshold == 0.95 @pytest.mark.parametrize( "value", [ "", "grammar,,homophones", "bogus", ], ) def test_invalid_module_sequences_are_rejected(value): with pytest.raises(AuditaConfigError, match="AUDITA_MODULES"): AuditaConfig.from_sources(env={"AUDITA_MODULES": value}) @pytest.mark.parametrize( "env_name", [ "AUDITA_GLOSSARY_CONFIDENCE_THRESHOLD", "AUDITA_GRAMMAR_CONFIDENCE_THRESHOLD", "AUDITA_HOMOPHONES_CONFIDENCE_THRESHOLD", "AUDITA_SPOKEN_WORD_CONFIDENCE_THRESHOLD", ], ) def test_invalid_thresholds_are_rejected(env_name): with pytest.raises(AuditaConfigError): AuditaConfig.from_sources(env={env_name: "1.5"}) @pytest.mark.parametrize("value", ["0", "-1", "many"]) def test_invalid_llm_concurrency_is_rejected(value): with pytest.raises(AuditaConfigError, match="AUDITA_LLM_CONCURRENCY"): AuditaConfig.from_sources(env={"AUDITA_LLM_CONCURRENCY": value}) @pytest.mark.parametrize("value", ["0", "-1", "many"]) def test_invalid_llm_timeout_seconds_is_rejected(value): with pytest.raises(AuditaConfigError, match="AUDITA_LLM_TIMEOUT_SECONDS"): AuditaConfig.from_sources(env={"AUDITA_LLM_TIMEOUT_SECONDS": value}) @pytest.mark.parametrize("value", ["0", "-1", "many"]) def test_invalid_validation_llm_concurrency_is_rejected(value): with pytest.raises(AuditaConfigError, match="AUDITA_VALIDATION_LLM_CONCURRENCY"): AuditaConfig.from_sources(env={"AUDITA_VALIDATION_LLM_CONCURRENCY": value}) @pytest.mark.parametrize("value", ["0", "-1", "many"]) def test_invalid_validation_llm_timeout_seconds_is_rejected(value): with pytest.raises(AuditaConfigError, match="AUDITA_VALIDATION_LLM_TIMEOUT_SECONDS"): AuditaConfig.from_sources(env={"AUDITA_VALIDATION_LLM_TIMEOUT_SECONDS": value}) @pytest.mark.parametrize("value", ["-1", "many"]) def test_invalid_validation_max_retries_is_rejected(value): with pytest.raises(AuditaConfigError, match="AUDITA_VALIDATION_MAX_RETRIES"): AuditaConfig.from_sources(env={"AUDITA_VALIDATION_MAX_RETRIES": value}) @pytest.mark.parametrize("value", ["0", "-1", "many"]) def test_invalid_validation_max_prompt_tokens_is_rejected(value): with pytest.raises(AuditaConfigError, match="AUDITA_VALIDATION_MAX_PROMPT_TOKENS"): AuditaConfig.from_sources(env={"AUDITA_VALIDATION_MAX_PROMPT_TOKENS": value}) @pytest.mark.parametrize("value", ["0", "-1", "many"]) def test_invalid_target_sections_is_rejected(value): with pytest.raises(AuditaConfigError, match="AUDITA_TARGET_SECTIONS"): AuditaConfig.from_sources(env={"AUDITA_TARGET_SECTIONS": value}) @pytest.mark.parametrize("value", ["0", "-1", "many"]) def test_invalid_min_section_tokens_is_rejected(value): with pytest.raises(AuditaConfigError, match="AUDITA_MIN_SECTION_TOKENS"): AuditaConfig.from_sources(env={"AUDITA_MIN_SECTION_TOKENS": value}) def test_min_section_tokens_must_not_exceed_max_section_tokens(): with pytest.raises(AuditaConfigError, match="AUDITA_MIN_SECTION_TOKENS"): AuditaConfig.from_sources( env={ "AUDITA_MIN_SECTION_TOKENS": "9000", "AUDITA_MAX_SECTION_TOKENS": "8000", } )