# allow: no-sut-import — tests scripts/generate_config_docs.py, repo tooling outside the local_deep_research package """ Tests for scripts/generate_config_docs.py Validates that the configuration documentation generator correctly: - Converts setting keys to env var names - Formats default values for markdown - Auto-discovers all env_definitions modules - Extracts all fields from Setting constructors - Generates valid markdown covering all JSON files and env_definitions - Detects stale documentation via --check mode - Handles errors gracefully """ import json import sys from pathlib import Path # Ensure the scripts directory is importable REPO_ROOT = Path(__file__).resolve().parent.parent.parent SCRIPTS_DIR = REPO_ROOT / "scripts" sys.path.insert(0, str(SCRIPTS_DIR)) from generate_config_docs import ( # noqa: E402 format_value, generate_docs, generate_docs_content, get_env_only_settings, get_env_var_name, ) # ── Paths ────────────────────────────────────────────────────────────── DEFAULTS_DIR = REPO_ROOT / "src" / "local_deep_research" / "defaults" ENV_DEFS_DIR = ( REPO_ROOT / "src" / "local_deep_research" / "settings" / "env_definitions" ) OUTPUT_FILE = REPO_ROOT / "docs" / "CONFIGURATION.md" # Expected env_definitions modules (excluding __init__.py and env_settings.py) EXPECTED_ENV_MODULES = { "bootstrap", "db_config", "news_scheduler", "security", "testing", "queue_processor", } # ═══════════════════════════════════════════════════════════════════════ # Unit tests # ═══════════════════════════════════════════════════════════════════════ class TestGetEnvVarName: """Test dotted keys -> LDR_* env var conversion.""" def test_simple_key(self): assert get_env_var_name("app.debug") == "LDR_APP_DEBUG" def test_nested_key(self): assert get_env_var_name("llm.openai.model") == "LDR_LLM_OPENAI_MODEL" def test_already_uppercase(self): assert get_env_var_name("DB.HOST") == "LDR_DB_HOST" def test_single_segment(self): assert get_env_var_name("port") == "LDR_PORT" class TestFormatValue: """Test value formatting for markdown table cells.""" def test_none(self): assert format_value(None) == "null" def test_bool_true(self): assert format_value(True) == "true" def test_bool_false(self): assert format_value(False) == "false" def test_dict(self): result = format_value({"a": 1}) assert result.startswith("`") assert '"a"' in result def test_list(self): result = format_value([1, 2]) assert result.startswith("`") assert "[1, 2]" in result def test_string(self): assert format_value("hello") == "hello" def test_int(self): assert format_value(42) == "42" class TestGetEnvOnlySettings: """Test env_definitions AST extraction.""" def test_returns_all_modules(self): """All env_definitions modules should be represented.""" settings = get_env_only_settings() categories = {s["category"].lower().replace(" ", "_") for s in settings} for module in EXPECTED_ENV_MODULES: # Category is derived from filename: db_config.py -> "Db Config" -> "db_config" assert module in categories, ( f"Module '{module}' not found in categories: {categories}" ) def test_extracts_all_fields(self): """Each setting should have the core fields populated.""" settings = get_env_only_settings() assert len(settings) > 0 required_fields = { "key", "env_var", "description", "default", "type", "required", "min_value", "max_value", "allowed_values", "deprecated_env_var", } for s in settings: missing = required_fields - set(s.keys()) assert not missing, ( f"Setting '{s.get('key', '?')}' is missing fields: {missing}" ) def test_bootstrap_encryption_key_present(self): """The bootstrap encryption key should be discovered.""" settings = get_env_only_settings() keys = {s["key"] for s in settings} assert "bootstrap.encryption_key" in keys def test_db_config_has_constraints(self): """db_config settings should have min/max values extracted.""" settings = get_env_only_settings() cache_size = next( (s for s in settings if s["key"] == "db_config.cache_size_mb"), None, ) assert cache_size is not None, "db_config.cache_size_mb not found" assert cache_size["min_value"] == 1 assert cache_size["max_value"] == 10000 def test_db_config_has_allowed_values(self): """Enum settings should have allowed_values extracted.""" settings = get_env_only_settings() journal = next( (s for s in settings if s["key"] == "db_config.journal_mode"), None, ) assert journal is not None, "db_config.journal_mode not found" assert journal["allowed_values"] is not None assert "WAL" in journal["allowed_values"] def test_deprecated_env_var_extracted(self): """Settings with deprecated_env_var should have it extracted.""" settings = get_env_only_settings() cache_size = next( (s for s in settings if s["key"] == "db_config.cache_size_mb"), None, ) assert cache_size is not None assert cache_size["deprecated_env_var"] == "LDR_DB_CACHE_SIZE_MB" # ═══════════════════════════════════════════════════════════════════════ # Integration tests # ═══════════════════════════════════════════════════════════════════════ class TestGenerateDocsIntegration: """Test the full generation pipeline.""" def test_produces_valid_markdown(self): """Generated content should have the expected markdown structure.""" content = generate_docs_content() assert content.startswith("# Configuration Reference\n") assert "## Pre-Database (Env-Only) Settings" in content assert "## Settings List" in content assert "| Key | Environment Variable |" in content assert "*Generated by scripts/generate_config_docs.py*" in content def test_includes_all_json_files(self): """All JSON config files should contribute settings to the output.""" json_files = sorted(DEFAULTS_DIR.rglob("*.json")) assert len(json_files) >= 18, ( f"Expected at least 18 JSON files, found {len(json_files)}" ) content = generate_docs_content() # Collect all keys from all JSON files all_json_keys = set() for jf in json_files: try: data = json.loads(jf.read_text()) all_json_keys.update(data.keys()) except Exception: continue # Every key should appear in the generated doc for key in all_json_keys: assert f"`{key}`" in content, ( f"Setting key '{key}' not found in generated docs" ) def test_includes_all_env_definitions(self): """All env_definitions modules should be represented.""" content = generate_docs_content() settings = get_env_only_settings() for s in settings: assert s["env_var"] in content, ( f"Env var '{s['env_var']}' not found in generated docs" ) def test_no_env_var_collisions(self): """No duplicate env var names should exist across all settings.""" settings = get_env_only_settings() env_vars = [s["env_var"] for s in settings] duplicates = {v for v in env_vars if env_vars.count(v) > 1} assert not duplicates, f"Duplicate env var names found: {duplicates}" def test_env_only_table_has_expanded_columns(self): """The env-only table should include Type, Required, Constraints columns.""" content = generate_docs_content() header_line = None for line in content.split("\n"): if ( "Environment Variable" in line and "Type" in line and "Required" in line ): header_line = line break assert header_line is not None, ( "Could not find expanded env-only table header" ) assert "Constraints" in header_line assert "Deprecated Alias" in header_line # ═══════════════════════════════════════════════════════════════════════ # Check mode tests # ═══════════════════════════════════════════════════════════════════════ class TestCheckMode: """Test the --check flag behaviour.""" def test_check_mode_passes_when_fresh(self, tmp_path): """Exit 0 when docs match the generated output.""" out = tmp_path / "CONFIGURATION.md" # Generate fresh assert generate_docs(output_path=out, check=False) == 0 # Check should pass assert generate_docs(output_path=out, check=True) == 0 def test_check_mode_fails_when_stale(self, tmp_path): """Exit 1 when docs differ from the generated output.""" out = tmp_path / "CONFIGURATION.md" # Generate, then tamper generate_docs(output_path=out, check=False) out.write_text("stale content\n") assert generate_docs(output_path=out, check=True) == 1 def test_check_mode_fails_when_missing(self, tmp_path): """Exit 1 when the docs file doesn't exist.""" out = tmp_path / "CONFIGURATION.md" assert generate_docs(output_path=out, check=True) == 1 # ═══════════════════════════════════════════════════════════════════════ # Error handling tests # ═══════════════════════════════════════════════════════════════════════ class TestErrorHandling: """Test graceful handling of bad inputs.""" def test_handles_missing_json_gracefully(self, tmp_path): """Corrupted/missing JSON in defaults dir doesn't crash generation.""" # Create a minimal project structure with a bad JSON file defaults = tmp_path / "src" / "local_deep_research" / "defaults" defaults.mkdir(parents=True) (defaults / "bad.json").write_text("{invalid json") (defaults / "good.json").write_text( json.dumps( { "test.key": { "value": "hello", "description": "A test", "type": "STRING", } } ) ) # Also create empty env_definitions dir env_defs = ( tmp_path / "src" / "local_deep_research" / "settings" / "env_definitions" ) env_defs.mkdir(parents=True) content = generate_docs_content(root_dir=tmp_path) # Good file should still be included assert "`test.key`" in content def test_handles_ast_parse_errors_gracefully(self, tmp_path): """Bad Python in env_definitions doesn't crash generation.""" env_defs = ( tmp_path / "src" / "local_deep_research" / "settings" / "env_definitions" ) env_defs.mkdir(parents=True) (env_defs / "broken.py").write_text("def incomplete(") defaults = tmp_path / "src" / "local_deep_research" / "defaults" defaults.mkdir(parents=True) # Should not raise settings = get_env_only_settings(root_dir=tmp_path) assert isinstance(settings, list)