"""decnet.config_ini — INI file loader, precedence, section routing.""" from __future__ import annotations import os from pathlib import Path import pytest from decnet.config_ini import load_ini_config def _write_ini(tmp_path: Path, body: str) -> Path: p = tmp_path / "decnet.ini" p.write_text(body) return p def _scrub(monkeypatch: pytest.MonkeyPatch, *names: str) -> None: for n in names: monkeypatch.delenv(n, raising=False) def test_missing_file_is_noop(monkeypatch, tmp_path): _scrub(monkeypatch, "DECNET_MODE", "DECNET_AGENT_PORT") result = load_ini_config(tmp_path / "does-not-exist.ini") assert result is None assert "DECNET_AGENT_PORT" not in os.environ def test_inline_comments_stripped_from_values(monkeypatch, tmp_path): """The module docstring teaches inline ``#`` comments — the parser must accept them. Hit live on the first VPS deploy 2026-04-28: a ``mode = master # or "agent"`` line caused the value to be parsed as ``master # or "agent"`` and downstream validators rejected it.""" _scrub(monkeypatch, "DECNET_MODE", "DECNET_API_PORT") ini = _write_ini(tmp_path, """ [decnet] mode = master # inline hash comment [master] api-port = 8000 ; inline semi comment """) load_ini_config(ini) assert os.environ["DECNET_MODE"] == "master" assert os.environ["DECNET_API_PORT"] == "8000" def test_agent_section_only_loaded_when_mode_agent(monkeypatch, tmp_path): _scrub( monkeypatch, "DECNET_MODE", "DECNET_DISALLOW_MASTER", "DECNET_AGENT_PORT", "DECNET_MASTER_HOST", "DECNET_API_PORT", "DECNET_SWARMCTL_PORT", ) ini = _write_ini(tmp_path, """ [decnet] mode = agent [agent] agent-port = 8765 master-host = 192.168.1.50 [master] api-port = 9999 swarmctl-port = 8770 """) load_ini_config(ini) assert os.environ["DECNET_MODE"] == "agent" assert os.environ["DECNET_AGENT_PORT"] == "8765" assert os.environ["DECNET_MASTER_HOST"] == "192.168.1.50" # [master] section values must NOT leak into an agent host's env assert "DECNET_API_PORT" not in os.environ assert "DECNET_SWARMCTL_PORT" not in os.environ def test_master_section_loaded_when_mode_master(monkeypatch, tmp_path): _scrub( monkeypatch, "DECNET_MODE", "DECNET_API_PORT", "DECNET_SWARMCTL_PORT", "DECNET_AGENT_PORT", ) ini = _write_ini(tmp_path, """ [decnet] mode = master [agent] agent-port = 8765 [master] api-port = 8000 swarmctl-port = 8770 """) load_ini_config(ini) assert os.environ["DECNET_MODE"] == "master" assert os.environ["DECNET_API_PORT"] == "8000" assert os.environ["DECNET_SWARMCTL_PORT"] == "8770" assert "DECNET_AGENT_PORT" not in os.environ def test_env_wins_over_ini(monkeypatch, tmp_path): _scrub(monkeypatch, "DECNET_MODE") monkeypatch.setenv("DECNET_AGENT_PORT", "7777") ini = _write_ini(tmp_path, """ [decnet] mode = agent [agent] agent-port = 8765 """) load_ini_config(ini) # Real env var must beat INI value assert os.environ["DECNET_AGENT_PORT"] == "7777" def test_common_keys_always_exported(monkeypatch, tmp_path): _scrub(monkeypatch, "DECNET_MODE", "DECNET_DISALLOW_MASTER", "DECNET_LOG_DIRECTORY") ini = _write_ini(tmp_path, """ [decnet] mode = agent disallow-master = true log-directory = /var/log/decnet """) load_ini_config(ini) assert os.environ["DECNET_MODE"] == "agent" assert os.environ["DECNET_DISALLOW_MASTER"] == "true" assert os.environ["DECNET_LOG_DIRECTORY"] == "/var/log/decnet" def test_invalid_mode_raises(monkeypatch, tmp_path): _scrub(monkeypatch, "DECNET_MODE") ini = _write_ini(tmp_path, """ [decnet] mode = supervisor """) with pytest.raises(ValueError, match="mode must be"): load_ini_config(ini) def test_decnet_config_env_var_overrides_default_path(monkeypatch, tmp_path): _scrub(monkeypatch, "DECNET_MODE", "DECNET_API_PORT") ini = _write_ini(tmp_path, """ [decnet] mode = master [master] api-port = 9001 """) monkeypatch.setenv("DECNET_CONFIG", str(ini)) # Call with no explicit path — loader reads $DECNET_CONFIG loaded = load_ini_config() assert loaded == ini assert os.environ["DECNET_API_PORT"] == "9001" # ─── Domain sections ──────────────────────────────────────────────────────── def test_domain_sections_load_regardless_of_mode(monkeypatch, tmp_path): """[api], [web], [database], etc. load on both master and agent — setdefault makes unused keys harmless on the other role.""" _scrub( monkeypatch, "DECNET_MODE", "DECNET_API_HOST", "DECNET_API_PORT", "DECNET_WEB_PORT", "DECNET_ADMIN_USER", "DECNET_DB_TYPE", "DECNET_DB_URL", "DECNET_BUS_ENABLED", "DECNET_BUS_GROUP", "DECNET_SWARM_SYSLOG_PORT", "DECNET_SYSTEM_LOGS", "DECNET_INGEST_LOG_FILE", "DECNET_BATCH_SIZE", "DECNET_BATCH_MAX_WAIT_MS", "DECNET_DEVELOPER_TRACING", "DECNET_OTEL_ENDPOINT", ) ini = _write_ini(tmp_path, """ [decnet] mode = agent [api] host = 0.0.0.0 port = 8001 [web] port = 9090 admin-user = superman cors-origins = https://dash.example.com [database] type = mysql url = mysql+asyncmy://decnet@db/decnet [bus] enabled = false group = custom [swarm] syslog-port = 7514 [logging] system-log = /tmp/decnet.log ingest-log = /tmp/decnet.ingest.log [ingester] batch-size = 500 batch-max-wait-ms = 1000 [tracing] enabled = true otel-endpoint = http://otel.internal:4317 """) load_ini_config(ini) assert os.environ["DECNET_API_HOST"] == "0.0.0.0" assert os.environ["DECNET_API_PORT"] == "8001" assert os.environ["DECNET_WEB_PORT"] == "9090" assert os.environ["DECNET_ADMIN_USER"] == "superman" assert os.environ["DECNET_CORS_ORIGINS"] == "https://dash.example.com" assert os.environ["DECNET_DB_TYPE"] == "mysql" assert os.environ["DECNET_DB_URL"] == "mysql+asyncmy://decnet@db/decnet" assert os.environ["DECNET_BUS_ENABLED"] == "false" assert os.environ["DECNET_BUS_GROUP"] == "custom" assert os.environ["DECNET_SWARM_SYSLOG_PORT"] == "7514" assert os.environ["DECNET_SYSTEM_LOGS"] == "/tmp/decnet.log" assert os.environ["DECNET_INGEST_LOG_FILE"] == "/tmp/decnet.ingest.log" assert os.environ["DECNET_BATCH_SIZE"] == "500" assert os.environ["DECNET_BATCH_MAX_WAIT_MS"] == "1000" assert os.environ["DECNET_DEVELOPER_TRACING"] == "true" assert os.environ["DECNET_OTEL_ENDPOINT"] == "http://otel.internal:4317" def test_domain_section_env_wins_over_ini(monkeypatch, tmp_path): """Real env var beats the INI for a domain-section key, same as the role-specific section contract.""" _scrub(monkeypatch, "DECNET_MODE") monkeypatch.setenv("DECNET_API_PORT", "5555") ini = _write_ini(tmp_path, """ [decnet] mode = master [api] port = 8000 """) load_ini_config(ini) assert os.environ["DECNET_API_PORT"] == "5555" def test_domain_unknown_key_logs_warning(monkeypatch, tmp_path, caplog): """Typos in a domain section should be visible to the operator — a silent drop is how you spend an afternoon debugging 'why isn't my setting taking effect'.""" _scrub(monkeypatch, "DECNET_MODE") ini = _write_ini(tmp_path, """ [decnet] mode = master [api] host = 127.0.0.1 # typo: hostt instead of host hostt = 0.0.0.0 """) import logging as _logging with caplog.at_level(_logging.WARNING, logger="decnet.config_ini"): load_ini_config(ini) assert any( "unknown key [api] hostt" in rec.getMessage() for rec in caplog.records ), f"expected warning about unknown key, got: {[r.getMessage() for r in caplog.records]}" def test_domain_absent_section_is_noop(monkeypatch, tmp_path): """INI with only [decnet] present doesn't touch any domain env var.""" _scrub( monkeypatch, "DECNET_MODE", "DECNET_API_PORT", "DECNET_WEB_PORT", "DECNET_DB_TYPE", "DECNET_BUS_ENABLED", ) ini = _write_ini(tmp_path, """ [decnet] mode = master """) load_ini_config(ini) assert os.environ["DECNET_MODE"] == "master" assert "DECNET_API_PORT" not in os.environ assert "DECNET_WEB_PORT" not in os.environ assert "DECNET_DB_TYPE" not in os.environ assert "DECNET_BUS_ENABLED" not in os.environ def test_domain_section_does_not_override_role_section(monkeypatch, tmp_path): """If both [master] (role) and [swarm] (domain) define swarmctl-port, whichever the loader applies first wins via setdefault — and the role section runs first, so the [swarm] value is dropped silently. This locks in the precedence order as part of the contract.""" _scrub(monkeypatch, "DECNET_MODE", "DECNET_SWARMCTL_PORT") ini = _write_ini(tmp_path, """ [decnet] mode = master [master] swarmctl-port = 9001 [swarm] swarmctl-port = 9999 """) load_ini_config(ini) # [master] loaded first, [swarm] lost via setdefault assert os.environ["DECNET_SWARMCTL_PORT"] == "9001"