From 02ce8beaaf6ab4e96b72402fedf3440042da48b1 Mon Sep 17 00:00:00 2001 From: agessaman Date: Wed, 8 Jul 2026 08:56:05 -0700 Subject: [PATCH] feat(config, validation): enhance config validation with strict mode and update example checks - Added a `--strict` option to `validate_config.py` to fail on "Unknown section" messages, aimed at CI validation of example configs. - Updated GitHub Actions workflow to validate shipped example configs against the canonical section list using the new strict mode. - Expanded the `CANONICAL_NON_COMMAND_SECTIONS` in `config_validation.py` to include additional sections for improved validation accuracy. - Introduced a regression test to ensure example configs do not trigger unknown section warnings, maintaining consistency with the canonical list. --- .github/workflows/test.yml | 10 +++++ modules/config_validation.py | 11 ++++++ tests/test_config_validation.py | 24 +++++++++++ tests/test_validate_config_cli.py | 66 +++++++++++++++++++++++++++++++ validate_config.py | 17 ++++++++ 5 files changed, 128 insertions(+) create mode 100644 tests/test_validate_config_cli.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4946c6c..28d61ab 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -75,6 +75,16 @@ jobs: - name: Log injection check — fail on new unsanitized logger calls run: python scripts/check_log_injection.py + - name: Config validation — fail on unrecognized sections in shipped configs + run: | + set -e + for f in config.ini.example config.ini.minimal-example config.ini.quickstart config.min.ini config-pymc.ini; do + if [ -f "$f" ]; then + echo "Validating $f" + python validate_config.py --config "$f" --strict + fi + done + typecheck: name: Type check (mypy) runs-on: ubuntu-latest diff --git a/modules/config_validation.py b/modules/config_validation.py index 0566ba2..3773a69 100644 --- a/modules/config_validation.py +++ b/modules/config_validation.py @@ -39,6 +39,7 @@ def _channel_name_is_public(name: str) -> bool: CANONICAL_NON_COMMAND_SECTIONS = frozenset({ "Connection", "Bot", + "Admin", "Channels", "Banned_Users", "Localization", @@ -46,6 +47,7 @@ CANONICAL_NON_COMMAND_SECTIONS = frozenset({ "Plugin_Overrides", "Companion_Purge", "Keywords", + "RandomLine", "Scheduled_Messages", "Logging", "Custom_Syntax", @@ -53,12 +55,21 @@ CANONICAL_NON_COMMAND_SECTIONS = frozenset({ "Weather", "Solar_Config", "Channels_List", + "Data_Retention", "Web_Viewer", "Feed_Manager", "PacketCapture", "MapUploader", "Weather_Service", + "MqttWeather", + "Earthquake_Service", + "Worldcup_Service", + "Rate_Limits", + "Webhook", + "RepeaterPrefixCollision_Service", "DiscordBridge", + "TelegramBridge", + "DARC_MoWaS_Service", }) # Sections required for the bot to start (accessed without has_section guards) diff --git a/tests/test_config_validation.py b/tests/test_config_validation.py index 0e5e775..1165592 100644 --- a/tests/test_config_validation.py +++ b/tests/test_config_validation.py @@ -176,6 +176,30 @@ test = ack ) +class TestExampleConfigsHaveNoUnknownSections: + """Regression test for #215: shipped example configs must not trigger + 'Unknown section' info messages, i.e. CANONICAL_NON_COMMAND_SECTIONS must + stay in sync with the sections actually used in the example files.""" + + def test_example_configs_have_no_unknown_sections(self): + base = Path(__file__).resolve().parent.parent + example_files = [ + "config.ini.example", + "config.ini.minimal-example", + "config.ini.quickstart", + ] + for filename in example_files: + path = base / filename + if not path.exists(): + continue + results = validate_config(str(path)) + unknown = [ + r for r in results + if r[0] == SEVERITY_INFO and "not in canonical list" in r[1] + ] + assert unknown == [], f"{filename} has unrecognized sections: {unknown}" + + class TestPathValidation: """Tests for path writability validation.""" diff --git a/tests/test_validate_config_cli.py b/tests/test_validate_config_cli.py new file mode 100644 index 0000000..5e817c6 --- /dev/null +++ b/tests/test_validate_config_cli.py @@ -0,0 +1,66 @@ +"""Tests for the validate_config.py CLI entry point, in particular --strict.""" + +import subprocess +import sys +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parent.parent + +VALID_CONFIG = """[Connection] +connection_type = serial +serial_port = /dev/ttyUSB0 + +[Bot] +bot_name = TestBot +db_path = {db_path} + +[Channels] +monitor_channels = general +respond_to_dms = true + +[Keywords] +test = ack +""" + +CONFIG_WITH_UNKNOWN_SECTION = VALID_CONFIG + """ +[TotallyMadeUpSection] +foo = bar +""" + + +def _run_validate_config(config_path: Path, *extra_args: str) -> subprocess.CompletedProcess: + return subprocess.run( + [sys.executable, "validate_config.py", "--config", str(config_path), *extra_args], + cwd=REPO_ROOT, + capture_output=True, + text=True, + timeout=30, + ) + + +class TestValidateConfigCli: + def test_unknown_section_does_not_fail_without_strict(self, tmp_path): + config = tmp_path / "config.ini" + config.write_text(CONFIG_WITH_UNKNOWN_SECTION.format(db_path=str(tmp_path / "meshcore_bot.db"))) + result = _run_validate_config(config) + assert result.returncode == 0 + assert "Unknown section [TotallyMadeUpSection]" in result.stderr + + def test_unknown_section_fails_with_strict(self, tmp_path): + config = tmp_path / "config.ini" + config.write_text(CONFIG_WITH_UNKNOWN_SECTION.format(db_path=str(tmp_path / "meshcore_bot.db"))) + result = _run_validate_config(config, "--strict") + assert result.returncode == 1 + assert "Unknown section [TotallyMadeUpSection]" in result.stderr + + def test_valid_config_passes_with_strict(self, tmp_path): + config = tmp_path / "config.ini" + config.write_text(VALID_CONFIG.format(db_path=str(tmp_path / "meshcore_bot.db"))) + result = _run_validate_config(config, "--strict") + assert result.returncode == 0 + + def test_error_still_fails_regardless_of_strict(self, tmp_path): + config = tmp_path / "config.ini" + config.write_text("[Bot]\nbot_name = Test\n") + result = _run_validate_config(config) + assert result.returncode == 1 diff --git a/validate_config.py b/validate_config.py index 8661fa8..9ef15c3 100644 --- a/validate_config.py +++ b/validate_config.py @@ -4,6 +4,12 @@ Validate MeshCore Bot config.ini section names. Run standalone: python validate_config.py [--config config.ini] Exits with 1 if any errors are found, 0 otherwise. Warnings and info are printed but do not affect exit code. + +Use --strict to also fail on "Unknown section" info messages. This is intended for +CI checks against the shipped example configs (config.ini.example, etc.), which should +never contain a section outside the canonical list (see CANONICAL_NON_COMMAND_SECTIONS +in modules/config_validation.py). It is not recommended for validating end-user configs, +since those may legitimately contain sections for local/third-party plugins. """ import argparse @@ -25,6 +31,15 @@ def main() -> int: default="config.ini", help="Path to configuration file (default: config.ini)", ) + parser.add_argument( + "--strict", + action="store_true", + help=( + "Also fail (exit 1) on 'Unknown section' info messages. Intended for " + "CI validation of the shipped example configs; not recommended for " + "end-user configs, which may have local/third-party plugin sections." + ), + ) args = parser.parse_args() results = validate_config(args.config) @@ -37,6 +52,8 @@ def main() -> int: print(f"Warning: {message}", file=sys.stderr) else: print(f"Info: {message}", file=sys.stderr) + if args.strict and "Unknown section" in message: + has_error = True return 1 if has_error else 0