- ToolAnnotations (readOnlyHint, openWorldHint) on all 14 tools - ctx.info/warning/debug logging for data quality, fetch status, assessments - ctx.sample() summaries in conditions snapshot, deployment briefing, anomaly detection - New test_client_capabilities diagnostic tool for MCP feature discovery
176 lines
5.4 KiB
Python
176 lines
5.4 KiB
Python
"""Tests for chart rendering and visualization tool registration."""
|
|
|
|
import pytest
|
|
from fastmcp import Client
|
|
|
|
from mcnoaa_tides.charts.conditions import render_conditions_html, render_conditions_png
|
|
from mcnoaa_tides.charts.tides import (
|
|
_parse_observed,
|
|
_parse_predictions,
|
|
render_tide_chart_html,
|
|
render_tide_chart_png,
|
|
)
|
|
|
|
# Mock data (matches conftest.py fixtures)
|
|
MOCK_PREDICTIONS = {
|
|
"predictions": [
|
|
{"t": "2026-02-21 04:30", "v": "4.521", "type": "H"},
|
|
{"t": "2026-02-21 10:42", "v": "-0.123", "type": "L"},
|
|
{"t": "2026-02-21 16:55", "v": "5.012", "type": "H"},
|
|
{"t": "2026-02-21 23:08", "v": "0.234", "type": "L"},
|
|
]
|
|
}
|
|
MOCK_WATER_LEVEL = {
|
|
"data": [
|
|
{"t": "2026-02-21 00:00", "v": "2.34", "s": "0.003", "f": "0,0,0,0", "q": "p"},
|
|
{"t": "2026-02-21 00:06", "v": "2.38", "s": "0.003", "f": "0,0,0,0", "q": "p"},
|
|
]
|
|
}
|
|
MOCK_WIND = {
|
|
"data": [
|
|
{"t": "2026-02-21 00:00", "s": "12.5", "d": "225.00", "dr": "SW", "g": "18.2", "f": "0,0"}
|
|
]
|
|
}
|
|
MOCK_AIR_TEMP = {"data": [{"t": "2026-02-21 00:00", "v": "42.3", "f": "0,0,0"}]}
|
|
MOCK_WATER_TEMP = {"data": [{"t": "2026-02-21 00:00", "v": "38.7", "f": "0,0,0"}]}
|
|
MOCK_PRESSURE = {"data": [{"t": "2026-02-21 00:00", "v": "1013.2", "f": "0,0,0"}]}
|
|
|
|
|
|
# -- Parsing helpers --
|
|
|
|
|
|
def test_parse_predictions():
|
|
preds = MOCK_PREDICTIONS["predictions"]
|
|
times, values, markers = _parse_predictions(preds)
|
|
assert len(times) == 4
|
|
assert len(values) == 4
|
|
assert len(markers) == 4
|
|
assert markers[0]["type"] == "H"
|
|
assert markers[1]["type"] == "L"
|
|
assert values[0] == pytest.approx(4.521)
|
|
|
|
|
|
def test_parse_observed():
|
|
obs = MOCK_WATER_LEVEL["data"]
|
|
times, values = _parse_observed(obs)
|
|
assert len(times) == 2
|
|
assert values[0] == pytest.approx(2.34)
|
|
|
|
|
|
def test_parse_observed_skips_blank_values():
|
|
data = [
|
|
{"t": "2026-02-21 00:00", "v": "2.34"},
|
|
{"t": "2026-02-21 00:06", "v": ""},
|
|
{"t": "2026-02-21 00:12", "v": None},
|
|
]
|
|
times, values = _parse_observed(data)
|
|
assert len(times) == 1
|
|
|
|
|
|
# -- Tide chart rendering --
|
|
|
|
|
|
def test_tide_chart_png_returns_bytes():
|
|
preds = MOCK_PREDICTIONS["predictions"]
|
|
result = render_tide_chart_png(preds, station_name="Test Station")
|
|
assert isinstance(result, bytes)
|
|
assert len(result) > 0
|
|
# PNG magic bytes
|
|
assert result[:4] == b"\x89PNG"
|
|
|
|
|
|
def test_tide_chart_png_with_observed():
|
|
preds = MOCK_PREDICTIONS["predictions"]
|
|
obs = MOCK_WATER_LEVEL["data"]
|
|
result = render_tide_chart_png(preds, observed=obs, station_name="Providence")
|
|
assert isinstance(result, bytes)
|
|
assert result[:4] == b"\x89PNG"
|
|
|
|
|
|
def test_tide_chart_html_returns_string():
|
|
preds = MOCK_PREDICTIONS["predictions"]
|
|
result = render_tide_chart_html(preds, station_name="Test Station")
|
|
assert isinstance(result, str)
|
|
assert "<html>" in result.lower() or "<!doctype" in result.lower()
|
|
assert "plotly" in result.lower()
|
|
|
|
|
|
def test_tide_chart_html_with_observed():
|
|
preds = MOCK_PREDICTIONS["predictions"]
|
|
obs = MOCK_WATER_LEVEL["data"]
|
|
result = render_tide_chart_html(preds, observed=obs, station_name="Providence")
|
|
assert isinstance(result, str)
|
|
assert "Observed" in result
|
|
|
|
|
|
# -- Conditions dashboard rendering --
|
|
|
|
|
|
def _build_snapshot(**overrides) -> dict:
|
|
"""Build a snapshot dict from mock data."""
|
|
snapshot = {
|
|
"station_id": "8454000",
|
|
"predictions": MOCK_PREDICTIONS,
|
|
"water_level": MOCK_WATER_LEVEL,
|
|
"wind": MOCK_WIND,
|
|
"air_temperature": MOCK_AIR_TEMP,
|
|
"water_temperature": MOCK_WATER_TEMP,
|
|
"air_pressure": MOCK_PRESSURE,
|
|
}
|
|
snapshot.update(overrides)
|
|
return snapshot
|
|
|
|
|
|
def test_conditions_png_returns_bytes():
|
|
snapshot = _build_snapshot()
|
|
result = render_conditions_png(snapshot, station_name="Providence")
|
|
assert isinstance(result, bytes)
|
|
assert result[:4] == b"\x89PNG"
|
|
|
|
|
|
def test_conditions_png_partial_data():
|
|
"""Dashboard should render even with missing products."""
|
|
snapshot = _build_snapshot()
|
|
del snapshot["wind"]
|
|
del snapshot["air_temperature"]
|
|
result = render_conditions_png(snapshot, station_name="Providence")
|
|
assert isinstance(result, bytes)
|
|
assert result[:4] == b"\x89PNG"
|
|
|
|
|
|
def test_conditions_png_empty_snapshot():
|
|
"""Dashboard with no data produces a placeholder image."""
|
|
result = render_conditions_png({"station_id": "8454000"})
|
|
assert isinstance(result, bytes)
|
|
assert result[:4] == b"\x89PNG"
|
|
|
|
|
|
def test_conditions_html_returns_string():
|
|
snapshot = _build_snapshot()
|
|
result = render_conditions_html(snapshot, station_name="Providence")
|
|
assert isinstance(result, str)
|
|
assert "plotly" in result.lower()
|
|
|
|
|
|
def test_conditions_html_empty_snapshot():
|
|
result = render_conditions_html({"station_id": "8454000"})
|
|
assert isinstance(result, str)
|
|
assert "No data available" in result
|
|
|
|
|
|
# -- Tool registration --
|
|
|
|
|
|
async def test_visualization_tools_registered(mcp_client: Client):
|
|
"""The 2 new visualization tools should appear in the tool list."""
|
|
tools = await mcp_client.list_tools()
|
|
names = {t.name for t in tools}
|
|
assert "visualize_tides" in names
|
|
assert "visualize_conditions" in names
|
|
|
|
|
|
async def test_total_tool_count(mcp_client: Client):
|
|
"""Verify total tool count (9 base + 4 SmartPot + 1 diagnostics = 14)."""
|
|
tools = await mcp_client.list_tools()
|
|
assert len(tools) == 14
|