mirror of
https://github.com/hanzoai/python-sdk.git
synced 2026-10-27 04:20:09 +00:00
230 lines
6.9 KiB
Python
230 lines
6.9 KiB
Python
"""Test unified Jupyter tool implementation."""
|
|
|
|
import json
|
|
|
|
import pytest
|
|
from hanzo_tools.jupyter.jupyter import JupyterTool
|
|
from mcp.server.fastmcp import Context as MCPContext
|
|
|
|
from hanzo_mcp.tools.common.permissions import PermissionManager
|
|
|
|
|
|
@pytest.fixture
|
|
def permission_manager(tmp_path):
|
|
"""Create a permission manager with tmp_path as allowed."""
|
|
pm = PermissionManager()
|
|
pm.add_allowed_path(str(tmp_path))
|
|
return pm
|
|
|
|
|
|
@pytest.fixture
|
|
def jupyter_tool(permission_manager):
|
|
"""Create JupyterTool instance."""
|
|
return JupyterTool(permission_manager)
|
|
|
|
|
|
@pytest.fixture
|
|
def sample_notebook(tmp_path):
|
|
"""Create a sample notebook for testing."""
|
|
notebook_path = tmp_path / "test.ipynb"
|
|
notebook_content = {
|
|
"cells": [
|
|
{
|
|
"cell_type": "code",
|
|
"source": "print('Hello, World!')",
|
|
"metadata": {},
|
|
"outputs": [
|
|
{
|
|
"output_type": "stream",
|
|
"name": "stdout",
|
|
"text": "Hello, World!\n",
|
|
}
|
|
],
|
|
"execution_count": 1,
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"source": "# Test Notebook\nThis is a test.",
|
|
"metadata": {},
|
|
},
|
|
],
|
|
"metadata": {"language_info": {"name": "python"}},
|
|
"nbformat": 4,
|
|
"nbformat_minor": 5,
|
|
}
|
|
|
|
with open(notebook_path, "w") as f:
|
|
json.dump(notebook_content, f)
|
|
|
|
return notebook_path
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
class TestUnifiedJupyterTool:
|
|
"""Test the unified Jupyter tool."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_read_action(self, jupyter_tool, sample_notebook):
|
|
"""Test reading a notebook."""
|
|
ctx = MCPContext()
|
|
|
|
# Read entire notebook
|
|
result = await jupyter_tool.call(
|
|
ctx, action="read", notebook_path=str(sample_notebook)
|
|
)
|
|
|
|
assert "Notebook with 2 cells" in result
|
|
assert "Hello, World!" in result
|
|
assert "Test Notebook" in result
|
|
assert "[stdout]: Hello, World!" in result
|
|
|
|
# Read specific cell by index
|
|
result = await jupyter_tool.call(
|
|
ctx, action="read", notebook_path=str(sample_notebook), cell_index=0
|
|
)
|
|
|
|
assert "Cell 0 (code)" in result
|
|
assert "print('Hello, World!')" in result
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_action(self, jupyter_tool, tmp_path):
|
|
"""Test creating a new notebook."""
|
|
ctx = MCPContext()
|
|
new_notebook = tmp_path / "new.ipynb"
|
|
|
|
result = await jupyter_tool.call(
|
|
ctx, action="create", notebook_path=str(new_notebook)
|
|
)
|
|
|
|
assert "Successfully created notebook" in result
|
|
assert new_notebook.exists()
|
|
|
|
# Verify it's a valid notebook
|
|
with open(new_notebook) as f:
|
|
nb = json.load(f)
|
|
assert nb["nbformat"] == 4
|
|
assert "cells" in nb
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_edit_action_replace(self, jupyter_tool, sample_notebook):
|
|
"""Test editing a cell (replace mode)."""
|
|
ctx = MCPContext()
|
|
|
|
result = await jupyter_tool.call(
|
|
ctx,
|
|
action="edit",
|
|
notebook_path=str(sample_notebook),
|
|
cell_index=0,
|
|
source="print('Modified!')",
|
|
edit_mode="replace",
|
|
)
|
|
|
|
assert "Successfully updated cell at index 0" in result
|
|
|
|
# Verify the change
|
|
with open(sample_notebook) as f:
|
|
nb = json.load(f)
|
|
# nbformat stores source as list in JSON
|
|
source = nb["cells"][0]["source"]
|
|
if isinstance(source, list):
|
|
source = "".join(source)
|
|
assert source == "print('Modified!')"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_edit_action_insert(self, jupyter_tool, sample_notebook):
|
|
"""Test inserting a new cell."""
|
|
ctx = MCPContext()
|
|
|
|
result = await jupyter_tool.call(
|
|
ctx,
|
|
action="edit",
|
|
notebook_path=str(sample_notebook),
|
|
cell_index=1,
|
|
source="x = 42",
|
|
cell_type="code",
|
|
edit_mode="insert",
|
|
)
|
|
|
|
assert "Successfully inserted new cell at index 1" in result
|
|
|
|
# Verify the notebook now has 3 cells
|
|
with open(sample_notebook) as f:
|
|
nb = json.load(f)
|
|
assert len(nb["cells"]) == 3
|
|
# nbformat stores source as list in JSON
|
|
source = nb["cells"][1]["source"]
|
|
if isinstance(source, list):
|
|
source = "".join(source)
|
|
assert source == "x = 42"
|
|
assert nb["cells"][1]["cell_type"] == "code"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_edit_action_delete(self, jupyter_tool, sample_notebook):
|
|
"""Test deleting a cell."""
|
|
ctx = MCPContext()
|
|
|
|
# First check we have 2 cells
|
|
with open(sample_notebook) as f:
|
|
nb = json.load(f)
|
|
assert len(nb["cells"]) == 2
|
|
|
|
result = await jupyter_tool.call(
|
|
ctx,
|
|
action="edit",
|
|
notebook_path=str(sample_notebook),
|
|
cell_index=1,
|
|
edit_mode="delete",
|
|
)
|
|
|
|
assert "Successfully deleted cell at index 1" in result
|
|
|
|
# Verify we now have 1 cell
|
|
with open(sample_notebook) as f:
|
|
nb = json.load(f)
|
|
assert len(nb["cells"]) == 1
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delete_action_notebook(self, jupyter_tool, sample_notebook):
|
|
"""Test deleting entire notebook."""
|
|
ctx = MCPContext()
|
|
|
|
result = await jupyter_tool.call(
|
|
ctx, action="delete", notebook_path=str(sample_notebook)
|
|
)
|
|
|
|
assert "Successfully deleted notebook" in result
|
|
assert not sample_notebook.exists()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_error_handling(self, jupyter_tool, tmp_path):
|
|
"""Test error handling."""
|
|
ctx = MCPContext()
|
|
|
|
# Non-existent file
|
|
result = await jupyter_tool.call(
|
|
ctx, action="read", notebook_path=str(tmp_path / "nonexistent.ipynb")
|
|
)
|
|
assert "Error:" in result and "does not exist" in result
|
|
|
|
# Invalid action
|
|
result = await jupyter_tool.call(
|
|
ctx, action="invalid", notebook_path=str(tmp_path / "test.ipynb")
|
|
)
|
|
assert "Error: Unknown action" in result
|
|
|
|
# Missing required params for edit (need to test insert mode)
|
|
# First create a notebook to edit
|
|
test_nb = tmp_path / "test_edit.ipynb"
|
|
await jupyter_tool.call(ctx, action="create", notebook_path=str(test_nb))
|
|
|
|
# Try to insert without source
|
|
result = await jupyter_tool.call(
|
|
ctx,
|
|
action="edit",
|
|
notebook_path=str(test_nb),
|
|
cell_index=0,
|
|
edit_mode="insert",
|
|
cell_type="code", # Required for insert
|
|
)
|
|
assert "Error: source is required" in result
|