Files
hive/tools/tests/test_chart_tools_smoke.py
T
2026-05-01 14:57:06 -07:00

151 lines
4.9 KiB
Python

"""End-to-end smoke for chart-tools: spec → PNG file on disk.
Tests run with ``asyncio_mode=auto`` (see tools/pyproject.toml), so any
``async def test_*`` is automatically run via the pytest-asyncio plugin.
chart_render is async because FastMCP runs tool handlers inside the
running event loop; we await it the same way the framework does.
"""
from __future__ import annotations
import struct
from pathlib import Path
import pytest
EXPECTED_TOOLS = {"chart_render"}
def test_register_chart_tools_lands_all(mcp):
from chart_tools import register_chart_tools
names = register_chart_tools(mcp)
assert set(names) == EXPECTED_TOOLS, (
f"missing: {EXPECTED_TOOLS - set(names)}, extra: {set(names) - EXPECTED_TOOLS}"
)
def test_all_tools_have_chart_prefix(mcp):
from chart_tools import register_chart_tools
for n in register_chart_tools(mcp):
assert n.startswith("chart_"), f"{n!r} missing chart_ prefix"
@pytest.fixture
def chart_tool(mcp, tmp_path, monkeypatch):
monkeypatch.setenv("HIVE_HOME", str(tmp_path))
from chart_tools.tools import register_tools
register_tools(mcp)
return mcp._tool_manager._tools["chart_render"].fn
def _is_png(b: bytes) -> bool:
"""Verify the magic bytes + at least one IHDR chunk."""
return b[:8] == b"\x89PNG\r\n\x1a\n" and b"IHDR" in b[:32]
def _png_dims(path: Path) -> tuple[int, int]:
"""Read width/height from the IHDR chunk so we can assert the
screenshot honored the requested dpi/scale."""
data = path.read_bytes()
# IHDR is at offset 16 (after 8-byte signature + 4-byte length + 4-byte 'IHDR')
width = struct.unpack(">I", data[16:20])[0]
height = struct.unpack(">I", data[20:24])[0]
return width, height
async def test_render_echarts_bar_chart(chart_tool, tmp_path):
"""The flagship test: agent calls chart_render with an ECharts spec
→ file lands on disk, returns a non-empty envelope including the
spec echo so the chat can render it live."""
spec = {
"title": {"text": "Smoke test"},
"xAxis": {"type": "category", "data": ["A", "B", "C"]},
"yAxis": {"type": "value"},
"series": [{"type": "bar", "data": [12, 24, 6]}],
}
result = await chart_tool(
kind="echarts",
spec=spec,
title="smoke",
width=600,
height=400,
dpi=96, # keep test fast: 1x scale
)
assert "error" not in result, result
assert result["kind"] == "echarts"
assert result["spec"] == spec, "spec must be echoed back so the chat can re-render"
assert result["width"] == 600 and result["height"] == 400
assert result["bytes"] > 1000, "PNG should be at least 1KB"
path = Path(result["file_path"])
assert path.exists(), f"file not written: {path}"
assert _is_png(path.read_bytes()), "file is not a valid PNG"
w, h = _png_dims(path)
# At dpi=96 (1x), the PNG should be roughly viewport-sized.
assert 500 <= w <= 700, f"unexpected PNG width {w}"
assert 300 <= h <= 500, f"unexpected PNG height {h}"
# File should land under HIVE_HOME/charts/
assert path.parent.name == "charts"
assert "smoke" in path.name
async def test_render_echarts_invalid_kind_returns_error(chart_tool):
result = await chart_tool(kind="bogus", spec={}, title="x")
assert "error" in result
assert result["kind"] == "bogus" # echoed for the chat to surface
async def test_render_echarts_accepts_string_spec(chart_tool):
"""Regression: agent sometimes passes the spec as a JSON STRING
instead of a dict (the actual failure shown to the user on
2026-05-01: 'Cannot create property \"series\" on string ...').
chart_render must parse string specs transparently.
"""
import json as _json
spec_dict = {
"title": {"text": "String-spec regression"},
"xAxis": {"type": "category", "data": ["A", "B", "C"]},
"yAxis": {"type": "value"},
"series": [{"type": "bar", "data": [1, 2, 3]}],
}
result = await chart_tool(
kind="echarts",
spec=_json.dumps(spec_dict), # ← STRING not dict
title="string-spec",
width=600,
height=400,
dpi=96,
)
assert "error" not in result, result
assert result["bytes"] > 1000
# Spec should be echoed back as the parsed dict, not the original string.
assert isinstance(result["spec"], dict)
assert result["spec"] == spec_dict
async def test_render_mermaid_flowchart(chart_tool, tmp_path):
"""Mermaid path: agent passes raw mermaid source as the spec str."""
src = """flowchart LR
A[Start] --> B{ok?}
B -- yes --> C[Done]
B -- no --> D[Retry]
"""
result = await chart_tool(
kind="mermaid",
spec=src,
title="flow",
width=600,
height=400,
dpi=96,
)
assert "error" not in result, result
assert result["spec"] == src
path = Path(result["file_path"])
assert path.exists() and _is_png(path.read_bytes())