gr-mcp/tests/integration/test_mcp_block_dev.py
Ryan Malloy 5db7d71d2b feat: add AI-assisted block development tools
Implements complete workflow for generating GNU Radio blocks from
descriptions:

Block Generation:
- generate_sync_block, generate_basic_block, generate_interp_block,
  generate_decim_block tools for creating different block types
- Template-based code generation with customizable work logic
- Automatic validation via AST parsing and signature checking

Protocol Analysis:
- Parse protocol specifications into structured models
- Generate decoder pipelines matching modulation to demodulator blocks
- Templates for BLE, Zigbee, LoRa, POCSAG, ADS-B protocols

OOT Export:
- Export generated blocks to full OOT module structure
- Generate CMakeLists.txt, block YAML, Python modules
- gr_modtool-compatible output

Dynamic Tool Registration:
- enable_block_dev_mode/disable_block_dev_mode for context management
- Tools only registered when needed (reduces LLM context usage)

Includes comprehensive test coverage and end-to-end demo.
2026-02-09 12:36:54 -07:00

273 lines
10 KiB
Python

"""Integration tests for McpBlockDevProvider."""
import pytest
from fastmcp import Client, FastMCP
from gnuradio_mcp.providers.mcp_block_dev import McpBlockDevProvider
@pytest.fixture
def mcp_app():
"""Create a FastMCP app with block dev provider."""
app = FastMCP("Block Dev Test")
McpBlockDevProvider.create(app)
return app
class TestDynamicBlockDevMode:
"""Tests for dynamic block development mode registration."""
@pytest.mark.asyncio
async def test_block_dev_mode_starts_disabled(self, mcp_app):
"""Block dev mode should be disabled by default."""
async with Client(mcp_app) as client:
result = await client.call_tool(name="get_block_dev_mode")
assert result.data.enabled is False
assert result.data.tools_registered == []
@pytest.mark.asyncio
async def test_enable_block_dev_mode_registers_tools(self, mcp_app):
"""Enabling block dev mode should register generation tools."""
async with Client(mcp_app) as client:
result = await client.call_tool(name="enable_block_dev_mode")
assert result.data.enabled is True
assert "generate_sync_block" in result.data.tools_registered
assert "validate_block_code" in result.data.tools_registered
assert "parse_block_prompt" in result.data.tools_registered
@pytest.mark.asyncio
async def test_disable_block_dev_mode_removes_tools(self, mcp_app):
"""Disabling block dev mode should remove generation tools."""
async with Client(mcp_app) as client:
# Enable first
await client.call_tool(name="enable_block_dev_mode")
# Then disable
result = await client.call_tool(name="disable_block_dev_mode")
assert result.data.enabled is False
assert result.data.tools_registered == []
@pytest.mark.asyncio
async def test_enable_block_dev_mode_idempotent(self, mcp_app):
"""Enabling block dev mode multiple times should be idempotent."""
async with Client(mcp_app) as client:
result1 = await client.call_tool(name="enable_block_dev_mode")
result2 = await client.call_tool(name="enable_block_dev_mode")
assert result1.data.enabled == result2.data.enabled
assert set(result1.data.tools_registered) == set(result2.data.tools_registered)
@pytest.mark.asyncio
async def test_disable_block_dev_mode_idempotent(self, mcp_app):
"""Disabling block dev mode multiple times should be idempotent."""
async with Client(mcp_app) as client:
result1 = await client.call_tool(name="disable_block_dev_mode")
result2 = await client.call_tool(name="disable_block_dev_mode")
assert result1.data.enabled is False
assert result2.data.enabled is False
class TestBlockDevTools:
"""Tests for block development tools when enabled."""
@pytest.mark.asyncio
async def test_generate_sync_block_creates_valid_code(self, mcp_app):
"""Generate a sync block and verify it validates."""
async with Client(mcp_app) as client:
# Enable block dev mode
await client.call_tool(name="enable_block_dev_mode")
# Generate a block
result = await client.call_tool(
name="generate_sync_block",
arguments={
"name": "test_gain",
"description": "Multiply by gain factor",
"inputs": [{"dtype": "float", "vlen": 1}],
"outputs": [{"dtype": "float", "vlen": 1}],
"parameters": [{"name": "gain", "dtype": "float", "default": 1.0}],
"work_template": "gain",
},
)
assert result.data.is_valid is True
assert "gr.sync_block" in result.data.source_code
assert "self.gain" in result.data.source_code
@pytest.mark.asyncio
async def test_generate_basic_block_creates_valid_code(self, mcp_app):
"""Generate a basic block and verify it validates."""
async with Client(mcp_app) as client:
await client.call_tool(name="enable_block_dev_mode")
result = await client.call_tool(
name="generate_basic_block",
arguments={
"name": "packet_extract",
"description": "Extract packets",
"inputs": [{"dtype": "byte", "vlen": 1}],
"outputs": [{"dtype": "byte", "vlen": 1}],
"parameters": [],
"work_logic": "self.consume_each(1); return 1",
},
)
assert result.data.is_valid is True
assert "gr.basic_block" in result.data.source_code
assert "general_work" in result.data.source_code
@pytest.mark.asyncio
async def test_validate_block_code_success(self, mcp_app):
"""Validate syntactically correct code."""
async with Client(mcp_app) as client:
await client.call_tool(name="enable_block_dev_mode")
valid_code = '''
import numpy
from gnuradio import gr
class blk(gr.sync_block):
def __init__(self):
gr.sync_block.__init__(
self, name="test", in_sig=[numpy.float32], out_sig=[numpy.float32]
)
def work(self, input_items, output_items):
output_items[0][:] = input_items[0]
return len(output_items[0])
'''
result = await client.call_tool(
name="validate_block_code",
arguments={"source_code": valid_code},
)
assert result.data.is_valid is True
@pytest.mark.asyncio
async def test_validate_block_code_syntax_error(self, mcp_app):
"""Validate code with syntax errors."""
async with Client(mcp_app) as client:
await client.call_tool(name="enable_block_dev_mode")
invalid_code = """
def broken(:
pass
"""
result = await client.call_tool(
name="validate_block_code",
arguments={"source_code": invalid_code},
)
assert result.data.is_valid is False
@pytest.mark.asyncio
async def test_generate_interp_block(self, mcp_app):
"""Generate an interpolating block."""
async with Client(mcp_app) as client:
await client.call_tool(name="enable_block_dev_mode")
result = await client.call_tool(
name="generate_interp_block",
arguments={
"name": "upsample_2x",
"description": "Upsample by 2",
"inputs": [{"dtype": "float", "vlen": 1}],
"outputs": [{"dtype": "float", "vlen": 1}],
"interpolation": 2,
"parameters": [],
},
)
assert result.data.is_valid is True
assert "gr.interp_block" in result.data.source_code
@pytest.mark.asyncio
async def test_generate_decim_block(self, mcp_app):
"""Generate a decimating block."""
async with Client(mcp_app) as client:
await client.call_tool(name="enable_block_dev_mode")
result = await client.call_tool(
name="generate_decim_block",
arguments={
"name": "downsample_4x",
"description": "Downsample by 4",
"inputs": [{"dtype": "float", "vlen": 1}],
"outputs": [{"dtype": "float", "vlen": 1}],
"decimation": 4,
"parameters": [],
},
)
assert result.data.is_valid is True
assert "gr.decim_block" in result.data.source_code
class TestBlockDevResources:
"""Tests for block dev prompt template resources."""
@pytest.mark.asyncio
async def test_sync_block_prompt_resource(self, mcp_app):
"""Verify sync block prompt resource is available."""
async with Client(mcp_app) as client:
resources = await client.list_resources()
resource_uris = [r.uri for r in resources]
# Check that our prompt resources are registered
assert any("sync-block" in str(uri) for uri in resource_uris)
@pytest.mark.asyncio
async def test_basic_block_prompt_resource(self, mcp_app):
"""Verify basic block prompt resource is available."""
async with Client(mcp_app) as client:
resources = await client.list_resources()
resource_uris = [r.uri for r in resources]
assert any("basic-block" in str(uri) for uri in resource_uris)
@pytest.mark.asyncio
async def test_decoder_chain_prompt_resource(self, mcp_app):
"""Verify decoder chain prompt resource is available."""
async with Client(mcp_app) as client:
resources = await client.list_resources()
resource_uris = [r.uri for r in resources]
assert any("decoder-chain" in str(uri) for uri in resource_uris)
@pytest.mark.asyncio
async def test_common_patterns_prompt_resource(self, mcp_app):
"""Verify common patterns prompt resource is available."""
async with Client(mcp_app) as client:
resources = await client.list_resources()
resource_uris = [r.uri for r in resources]
assert any("common-patterns" in str(uri) for uri in resource_uris)
class TestToolNotAvailableWhenDisabled:
"""Tests that tools are not available when block dev mode is disabled."""
@pytest.mark.asyncio
async def test_generate_sync_block_not_available_when_disabled(self, mcp_app):
"""generate_sync_block should not be callable when mode is disabled."""
async with Client(mcp_app) as client:
# Ensure disabled
await client.call_tool(name="disable_block_dev_mode")
# List available tools
tools = await client.list_tools()
tool_names = [t.name for t in tools]
# Generation tools should not be in the list
assert "generate_sync_block" not in tool_names
assert "validate_block_code" not in tool_names
assert "generate_basic_block" not in tool_names
# But mode control tools should be
assert "get_block_dev_mode" in tool_names
assert "enable_block_dev_mode" in tool_names
assert "disable_block_dev_mode" in tool_names