Files
meshcore-bot/tests
agessaman 595f37a27b fix(i18n): address review findings on weather localization
- Extracted `modules/alert_format.py` as the single NWS alert formatter, replacing four copies of the event-type abbreviation table and two of the time compactor. `!wx alerts` and the proactive `WeatherService` broadcasts now localize from one code path, so a Russian bot no longer answers `!wx alerts` in English while its proactive alerts are Russian.
- Stopped leaking translation key paths into mesh broadcasts. `Translator.translate` returns the dotted key when a lookup misses in both the locale and the English fallback, which is right for development but reached the air in production: an unclassifiable NWS title rendered as `⚪Hazardous services.weather_service.event_types.Unknown`, an unmapped WMO code as `services.weather_service.weather_descriptions.4`, and an oddly-cased `wind_speed_unit` as `services.weather_service.wind_speed_units.KMH`. `alert_format.translate_or()` carries an English default at each site, and `WeatherService` now normalizes and validates its three `[Weather]` unit settings the way `GlobalWxCommand` already did.
- Fixed alert expiry rendering in every locale. The formatter rendered a timestamp to a string and re-parsed its own output with `(\d+)(AM|PM)` against a hardcoded English month list, so translated months took the wrong branch and truncated mid-string. Times now carry parsed parts and render through a per-locale `common.alerts.time_12h` template — the space before AM/PM was correct (Russian writes "6 дня", not "6дня"); the downstream regex was the bug.
- Restored month abbreviation. `_compact_time` iterated over abbreviations and replaced them in the string instead of mapping full names, so English stopped shortening "June 28" and Russian replaced the "Jun" inside "June", leaving a stray Latin "e" (`июнe 28`). Reuses the existing `common.date_time.month_abbreviations` rather than the duplicate `services.weather_service.months` block.
- Made `!gwx` display units follow `[Weather]` config instead of the response language. Visibility switched on `base_language != 'en'`, so `language = ru` with the default `temperature_unit = fahrenheit` printed Fahrenheit beside kilometers, and `en-GB` was forced to miles. Pressure is a locale convention rather than a metric/imperial split, so each catalog names its own via `commands.gwx.pressure_unit` — previously every non-English locale inherited mmHg from the English catalog, whose `pressure_mmhg` string contained Russian text, giving German and French users Cyrillic pressure units.
- Let localized `H`/`L` labels reach a standard install. `config.ini.example` shipped the three `temperature_*_format` keys uncommented with literal `H:`/`L:`, and a config value always beats the new locale-aware default, so a Russian bot built from the documented example still rendered `H:47°C L:33°C`. The example now uses the `{high_label}`/`{low_label}` placeholders, which were documented in the docstring but not in the file.
- Routed high/low labels through the reply's translator. `_format_high_low` passed `bot.translator`, so with `auto_detect_language` on, an English-default bot answering a Russian sender localized the rest of the line but not `H:`/`L:`. Added `BaseCommand.response_translator` for this, replacing `wx_international`'s reach into the private `_response_translator` ContextVar.
- Fixed a byte-budget overrun in `!gwx`. The guard on the extra conditions block compared a character count against a byte-derived budget while the rest of the function used `_count_display_width`; Cyrillic is two bytes per character, so the block was appended after the budget was spent.
- Reverted nine `commands.gwx` English rewordings that were not localization work, including the configuration hint in `mqtt_weather_no_subscriber` — dropping it left a mis-configured operator with no pointer to the two keys they need.
- Fixed the Russian `visibility` string, which said "км" on the miles key — the same locale/config conflation as the code bug, in the data. Shortened the Russian event-type abbreviations, which were full words consuming a quarter of the 130-byte budget at two bytes per character.
- Added `commands.wx.hourly_not_available`, missing from every catalog so `!wx hourly` printed the raw key path. Predates this branch; found while auditing every translation key the weather modules reference.
- Moved alert strings to `common.alerts.*` and wind directions to `common.wind_directions.*`, since a command and a service both read them.
2026-09-07 15:13:18 -07:00
..

MeshCore Bot Test Suite

This directory contains the test suite for the MeshCore Bot, focusing on graph-based path guessing functionality.

Structure

tests/
├── README.md              # This file
├── conftest.py            # Pytest fixtures and configuration
├── helpers.py             # Test data factories and helper functions
├── unit/                  # Unit tests (isolated, with mocks)
│   ├── test_mesh_graph_edges.py
│   ├── test_mesh_graph_validation.py
│   ├── test_mesh_graph_scoring.py
│   ├── test_mesh_graph_multihop.py
│   └── test_path_command_graph_selection.py
└── integration/          # Integration tests (with real database)
    └── test_path_resolution.py

Running Tests

Run all tests

pytest

Run only unit tests

pytest tests/unit/

Run only integration tests

pytest tests/integration/

Run specific test file

pytest tests/unit/test_mesh_graph_edges.py

Run specific test

pytest tests/unit/test_mesh_graph_edges.py::TestMeshGraphEdges::test_add_new_edge

Run with coverage

pytest --cov=modules --cov-report=html --cov-report=term-missing

Run with verbose output

pytest -v

Run with markers

pytest -m unit          # Run only unit tests
pytest -m integration  # Run only integration tests
pytest -m slow          # Run slow tests

Test Coverage

Unit Tests

test_mesh_graph_edges.py (15 tests)

Tests for MeshGraph edge management:

  • Adding new edges
  • Updating existing edges
  • Public key handling
  • Hop position tracking
  • Geographic distance
  • Edge queries (get, has, outgoing, incoming)
  • Prefix normalization

test_mesh_graph_validation.py (12 tests)

Tests for path validation:

  • Path segment validation
  • Confidence calculation
  • Recency checks
  • Bidirectional edge validation
  • Full path validation
  • Minimum observations filtering

test_mesh_graph_scoring.py (11 tests)

Tests for candidate scoring:

  • Score calculation with various edge combinations
  • Bidirectional bonuses
  • Hop position matching
  • Geographic distance bonuses
  • Minimum observations filtering

test_mesh_graph_multihop.py (12 tests)

Tests for multi-hop path inference:

  • 2-hop and 3-hop path finding
  • Intermediate node discovery
  • Minimum observations filtering
  • Bidirectional path bonuses
  • Score reduction for longer paths

test_path_command_graph_selection.py (8 tests)

Tests for PathCommand._select_repeater_by_graph:

  • Direct edge selection
  • Stored public key bonus
  • Star bias multiplier
  • Multi-hop inference
  • Confidence conversion

Integration Tests

test_path_resolution.py (5 tests)

End-to-end tests for full path resolution:

  • Path resolution with graph edges from database
  • Prefix collision resolution using graph data
  • Edge persistence across graph restarts
  • Graph vs geographic selection
  • Real-world multi-hop scenarios

Test Fixtures

Fixtures are defined in conftest.py:

  • mock_logger: Mock logger for testing
  • test_config: Test configuration with Path_Command settings
  • test_db: In-memory SQLite database for testing
  • mock_bot: Mock bot instance with all necessary attributes
  • mesh_graph: Clean MeshGraph instance for testing
  • populated_mesh_graph: MeshGraph instance with sample edges

Test Helpers

Helper functions in helpers.py:

  • create_test_repeater(): Factory for creating test repeater data
  • create_test_edge(): Factory for creating test edge data
  • create_test_path(): Factory for creating test path data
  • populate_test_graph(): Helper to populate a graph with test edges

Writing New Tests

Unit Test Example

import pytest
from tests.helpers import create_test_edge

@pytest.mark.unit
class TestMyFeature:
    def test_my_feature(self, mesh_graph):
        """Test description."""
        mesh_graph.add_edge('01', '7e')
        assert mesh_graph.has_edge('01', '7e')

Integration Test Example

import pytest

@pytest.mark.integration
class TestMyIntegration:
    def test_my_integration(self, mock_bot, test_db):
        """Test description."""
        # Use real database and bot components
        pass

Test Markers

Tests are marked with:

  • @pytest.mark.unit: Unit tests (isolated, with mocks)
  • @pytest.mark.integration: Integration tests (with real database)
  • @pytest.mark.slow: Slow-running tests

Dependencies

Test dependencies are in requirements.txt:

  • pytest>=7.0.0
  • pytest-asyncio>=0.21.0
  • pytest-mock>=3.10.0
  • pytest-cov>=4.0.0

Configuration

Pytest configuration is in pytest.ini:

  • Test discovery patterns
  • Async test support
  • Output options
  • Test markers

Notes

  • Unit tests use in-memory SQLite databases for speed
  • Integration tests may use real database connections
  • All tests should be deterministic and not depend on external services
  • Tests should clean up after themselves (fixtures handle this automatically)