Files
meshcore-bot/tests
agessaman 4c76ee3dbe fix: address review findings on shlink shortener and template parser
Security: a shlink deployment with short_url_website unset POSTed the operator's
API key to v.gd. _normalize_base falls back to the public default and the shlink
branch guarded only on a missing key, never on a missing base. shlink now requires
an explicit base and refuses a v.gd/is.gd host outright rather than sending an
X-Api-Key there.

Correctness: _shorten_url_with_gd lost its response.ok guard when the backends were
split out, so a 502 whose body starts with http was returned as the short URL and
transmitted over RF. Restored, with a warning. The regression test that should have
caught this passed only because it left mock_resp.text as a MagicMock; it now uses
a realistic proxy maintenance body.

_shorten_url_with_shlink never checked the status either. Shlink reports failures as
RFC 7807 problem details, which parse as JSON and simply lack shortUrl, so a bad API
key was indistinguishable from an unshortenable URL at DEBUG. It now warns on a
non-OK status, a non-JSON body, and a missing shortUrl. Dropped the shortUrlSlug
fallback: that is a request field, not a response field, and returning it puts a
bare slug where a link belongs.

Timeouts and connection errors are back on their own DEBUG handler. They had fallen
through to the broad handler, whose level rose to ERROR in the same diff, so a
routine intermittent uplink logged "Unexpected error shortening URL" on every reply.

Parser: _GREEDY_ARG_FILTERS was tested before the quoted-argument branch, so
prefix_if_nonempty, the one filter already in shipped configs, could not use the
quoted syntax this PR adds. {path_distance|prefix_if_nonempty:"Dist {sender}: "}
rendered raw template text. A quote immediately after the ':' now selects the quoted
grammar; anything else stays greedy, so config.ini.example's
`prefix_if_nonempty: | Path Dist: ` keeps its pipe and its whitespace.

Blocking HTTP on the event loop: the path command rendered its reply prefix inline
from async code, so a slow shortener stalled radio RX, MQTT and every other handler
for the full 5s timeout. Added format_piped_template_async and switched the path
command to it. The test command still renders synchronously through the sync
check_keywords dispatcher; making that async is a separate change, so the filter now
warns once when it runs on the loop.

Naming: one operation should not have two names in an operator-facing DSL. shorten
and shorten_url are aliases in both response templates and feed formats, and
if_nonempty is canonical with if_notempty as an alias, so a filter chain copied
between a feed format and a response_format works either way.

Also: reduced _build_create_shlink_url to the one parameter it uses and dropped its
dead query/startswith lines, renamed the shlink POST callable from `get`, documented
the config argument on format_piped_template, stopped gating the render trace on an
unrelated parameter and logging field values (sender IDs, user phrases) with it, and
moved the changelog entry from Fixed to Added and Changed.
2026-09-07 15:45:49 -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)