mirror of
https://github.com/pocketpaw/pocketpaw.git
synced 2026-05-13 21:21:53 +00:00
- Auto-fix 155 errors (import sorting, annotations, deprecated imports) - Format 87 files with ruff format for line length compliance - Fix 15 F401 unused imports (add __all__ for re-exports, remove truly unused) - Fix 7 F841 unused variables (prefix with _) - Fix 2 F821 undefined names (add missing imports) - Fix 3 E402 module-level imports not at top - Fix 2 UP042 str+Enum → StrEnum - Fix 1 E712 == False comparison - Fix remaining 51 E501 line-too-long in string literals and expressions Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
591 lines
20 KiB
Python
591 lines
20 KiB
Python
# Tests for pocket tools — CreatePocketTool, AddWidgetTool, RemoveWidgetTool.
|
|
# Updated: 2026-04-01 — Added UISpec v1.0, multi-pane, and required-fields tests.
|
|
# Validates all three pocket formats: UISpec, multi-pane, and flat widgets.
|
|
|
|
import json
|
|
|
|
import pytest
|
|
|
|
from pocketpaw.tools.builtin.pocket import (
|
|
AddWidgetTool,
|
|
CreatePocketTool,
|
|
RemoveWidgetTool,
|
|
_convert_legacy_widget,
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def create_tool():
|
|
return CreatePocketTool()
|
|
|
|
|
|
@pytest.fixture
|
|
def add_tool():
|
|
return AddWidgetTool()
|
|
|
|
|
|
@pytest.fixture
|
|
def remove_tool():
|
|
return RemoveWidgetTool()
|
|
|
|
|
|
def _extract_spec(result: str) -> dict:
|
|
"""Extract the JSON spec from the tool result.
|
|
|
|
Tools return: ``{json_with_pocket_event}\\n\\nhuman message``.
|
|
"""
|
|
json_part = result.split("\n\n", 1)[0]
|
|
data = json.loads(json_part)
|
|
assert data.get("pocket_event") == "created", f"Expected pocket_event=created, got: {data}"
|
|
return data["spec"]
|
|
|
|
|
|
def _extract_mutation(result: str) -> dict:
|
|
"""Extract the JSON mutation from the tool result."""
|
|
json_part = result.split("\n\n", 1)[0]
|
|
data = json.loads(json_part)
|
|
assert data.get("pocket_event") == "mutation", f"Expected pocket_event=mutation, got: {data}"
|
|
return data["mutation"]
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# CreatePocketTool tests
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestCreatePocketTool:
|
|
async def test_returns_universal_spec(self, create_tool):
|
|
result = await create_tool.execute(
|
|
title="Test Dashboard",
|
|
description="A test pocket",
|
|
category="research",
|
|
widgets=[
|
|
{
|
|
"type": "metric",
|
|
"title": "Users",
|
|
"size": "sm",
|
|
"data": {"value": "1000", "label": "Users"},
|
|
},
|
|
],
|
|
)
|
|
spec = _extract_spec(result)
|
|
|
|
assert spec["version"] == "2.0"
|
|
assert spec["intent"] == "dashboard"
|
|
assert spec["title"] == "Test Dashboard"
|
|
assert spec["description"] == "A test pocket"
|
|
|
|
async def test_has_lifecycle(self, create_tool):
|
|
result = await create_tool.execute(
|
|
title="Lifecycle Test",
|
|
description="desc",
|
|
category="data",
|
|
widgets=[],
|
|
)
|
|
spec = _extract_spec(result)
|
|
|
|
assert "lifecycle" in spec
|
|
assert spec["lifecycle"]["type"] == "persistent"
|
|
assert spec["lifecycle"]["id"].startswith("ai-")
|
|
|
|
async def test_has_metadata(self, create_tool):
|
|
result = await create_tool.execute(
|
|
title="Meta Test",
|
|
description="desc",
|
|
category="business",
|
|
color="#FF453A",
|
|
widgets=[],
|
|
)
|
|
spec = _extract_spec(result)
|
|
|
|
assert spec["metadata"]["category"] == "business"
|
|
assert spec["metadata"]["color"] == "#FF453A"
|
|
assert spec["metadata"]["pocket_version"] == "2.0"
|
|
assert "created_at" in spec["metadata"]
|
|
|
|
async def test_has_dashboard_layout(self, create_tool):
|
|
result = await create_tool.execute(
|
|
title="Layout Test",
|
|
description="desc",
|
|
category="research",
|
|
columns=4,
|
|
widgets=[],
|
|
)
|
|
spec = _extract_spec(result)
|
|
|
|
assert spec["display"]["columns"] == 4
|
|
assert spec["dashboard_layout"]["type"] == "grid"
|
|
assert spec["dashboard_layout"]["columns"] == 4
|
|
assert spec["dashboard_layout"]["gap"] == 10
|
|
|
|
async def test_metric_widget(self, create_tool):
|
|
result = await create_tool.execute(
|
|
title="Metrics",
|
|
description="desc",
|
|
category="data",
|
|
widgets=[
|
|
{
|
|
"type": "metric",
|
|
"title": "Revenue",
|
|
"size": "sm",
|
|
"data": {"value": "$10B", "label": "Revenue", "trend": "+15%"},
|
|
},
|
|
],
|
|
)
|
|
spec = _extract_spec(result)
|
|
|
|
assert len(spec["widgets"]) == 1
|
|
w = spec["widgets"][0]
|
|
assert w["type"] == "metric"
|
|
assert w["title"] == "Revenue"
|
|
assert w["size"] == "sm"
|
|
assert w["data"]["value"] == "$10B"
|
|
assert w["data"]["trend"] == "+15%"
|
|
|
|
async def test_chart_widget(self, create_tool):
|
|
result = await create_tool.execute(
|
|
title="Charts",
|
|
description="desc",
|
|
category="data",
|
|
widgets=[
|
|
{
|
|
"type": "chart",
|
|
"title": "Sales",
|
|
"size": "md",
|
|
"data": [{"label": "Jan", "value": 100}, {"label": "Feb", "value": 200}],
|
|
"props": {"type": "bar", "height": 200},
|
|
},
|
|
],
|
|
)
|
|
spec = _extract_spec(result)
|
|
|
|
w = spec["widgets"][0]
|
|
assert w["type"] == "chart"
|
|
assert w["props"]["type"] == "bar"
|
|
assert len(w["data"]) == 2
|
|
|
|
async def test_table_widget(self, create_tool):
|
|
result = await create_tool.execute(
|
|
title="Tables",
|
|
description="desc",
|
|
category="data",
|
|
widgets=[
|
|
{
|
|
"type": "table",
|
|
"title": "Orders",
|
|
"size": "lg",
|
|
"data": {"columns": ["ID", "Amount"], "data": [["1", "$50"], ["2", "$75"]]},
|
|
},
|
|
],
|
|
)
|
|
spec = _extract_spec(result)
|
|
|
|
w = spec["widgets"][0]
|
|
assert w["type"] == "table"
|
|
assert w["data"]["columns"] == ["ID", "Amount"]
|
|
assert len(w["data"]["data"]) == 2
|
|
|
|
async def test_widget_ids_generated(self, create_tool):
|
|
result = await create_tool.execute(
|
|
title="IDs Test",
|
|
description="desc",
|
|
category="research",
|
|
widgets=[
|
|
{"type": "metric", "title": "A", "data": {"value": "1"}},
|
|
{"type": "metric", "title": "B", "data": {"value": "2"}},
|
|
],
|
|
)
|
|
spec = _extract_spec(result)
|
|
|
|
ids = [w["id"] for w in spec["widgets"]]
|
|
assert len(set(ids)) == 2 # unique IDs
|
|
assert all(id.startswith("ai-") for id in ids)
|
|
|
|
async def test_legacy_name_param(self, create_tool):
|
|
"""Backward compat: 'name' param maps to 'title'."""
|
|
result = await create_tool.execute(
|
|
name="Legacy Name",
|
|
description="desc",
|
|
category="research",
|
|
widgets=[],
|
|
)
|
|
spec = _extract_spec(result)
|
|
assert spec["title"] == "Legacy Name"
|
|
|
|
async def test_multiple_widget_types(self, create_tool):
|
|
result = await create_tool.execute(
|
|
title="Multi",
|
|
description="desc",
|
|
category="research",
|
|
widgets=[
|
|
{"type": "metric", "title": "KPI", "data": {"value": "99%"}},
|
|
{"type": "chart", "title": "Trend", "data": [{"label": "A", "value": 1}]},
|
|
{"type": "table", "title": "Data", "data": {"columns": ["X"], "data": [["y"]]}},
|
|
{"type": "feed", "title": "News", "data": {"items": [{"text": "hello"}]}},
|
|
],
|
|
)
|
|
spec = _extract_spec(result)
|
|
assert len(spec["widgets"]) == 4
|
|
types = [w["type"] for w in spec["widgets"]]
|
|
assert types == ["metric", "chart", "table", "feed"]
|
|
|
|
async def test_result_contains_message(self, create_tool):
|
|
result = await create_tool.execute(
|
|
title="Msg Test",
|
|
description="desc",
|
|
category="research",
|
|
widgets=[
|
|
{"type": "metric", "title": "X", "data": {"value": "1"}},
|
|
],
|
|
)
|
|
assert "Created pocket **Msg Test** with 1 widgets" in result
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Legacy widget conversion tests
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestLegacyWidgetConversion:
|
|
async def test_legacy_stats_to_metrics(self, create_tool):
|
|
"""Legacy stats display with multiple stats should become multiple metric widgets."""
|
|
result = await create_tool.execute(
|
|
title="Legacy Stats",
|
|
description="desc",
|
|
category="research",
|
|
widgets=[
|
|
{
|
|
"name": "Overview",
|
|
"span": "col-span-2",
|
|
"display": {
|
|
"type": "stats",
|
|
"stats": [
|
|
{"label": "Revenue", "value": "$10B", "trend": "+15%"},
|
|
{"label": "Users", "value": "50K"},
|
|
],
|
|
},
|
|
},
|
|
],
|
|
)
|
|
spec = _extract_spec(result)
|
|
# 2 stats → 2 metric widgets
|
|
assert len(spec["widgets"]) == 2
|
|
assert all(w["type"] == "metric" for w in spec["widgets"])
|
|
assert spec["widgets"][0]["data"]["value"] == "$10B"
|
|
|
|
async def test_legacy_chart_to_chart(self, create_tool):
|
|
result = await create_tool.execute(
|
|
title="Legacy Chart",
|
|
description="desc",
|
|
category="data",
|
|
widgets=[
|
|
{
|
|
"name": "Revenue",
|
|
"display": {
|
|
"type": "chart",
|
|
"bars": [{"label": "Q1", "value": 100}],
|
|
"chartType": "bar",
|
|
},
|
|
},
|
|
],
|
|
)
|
|
spec = _extract_spec(result)
|
|
assert len(spec["widgets"]) == 1
|
|
assert spec["widgets"][0]["type"] == "chart"
|
|
assert spec["widgets"][0]["props"]["type"] == "bar"
|
|
|
|
async def test_legacy_table_to_table(self, create_tool):
|
|
result = await create_tool.execute(
|
|
title="Legacy Table",
|
|
description="desc",
|
|
category="data",
|
|
widgets=[
|
|
{
|
|
"name": "People",
|
|
"display": {
|
|
"type": "table",
|
|
"headers": ["Name", "Role"],
|
|
"rows": [{"cells": ["Alice", "CEO"]}],
|
|
},
|
|
},
|
|
],
|
|
)
|
|
spec = _extract_spec(result)
|
|
w = spec["widgets"][0]
|
|
assert w["type"] == "table"
|
|
assert w["data"]["columns"] == ["Name", "Role"]
|
|
|
|
async def test_legacy_feed_to_feed(self, create_tool):
|
|
result = await create_tool.execute(
|
|
title="Legacy Feed",
|
|
description="desc",
|
|
category="research",
|
|
widgets=[
|
|
{
|
|
"name": "News",
|
|
"display": {
|
|
"type": "feed",
|
|
"feedItems": [{"text": "Breaking news", "type": "info"}],
|
|
},
|
|
},
|
|
],
|
|
)
|
|
spec = _extract_spec(result)
|
|
w = spec["widgets"][0]
|
|
assert w["type"] == "feed"
|
|
assert w["data"]["items"][0]["text"] == "Breaking news"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# UISpec v1.0 and multi-pane tests
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestCreatePocketUISpec:
|
|
async def test_ui_param_produces_v1_spec(self, create_tool):
|
|
result = await create_tool.execute(
|
|
title="UISpec Pocket",
|
|
description="Rich layout",
|
|
category="research",
|
|
ui={
|
|
"type": "flex",
|
|
"props": {"direction": "column", "gap": "16px"},
|
|
"children": [{"type": "heading", "props": {"text": "Title", "level": 3}}],
|
|
},
|
|
)
|
|
spec = _extract_spec(result)
|
|
assert spec["version"] == "1.0"
|
|
assert "ui" in spec
|
|
assert spec["ui"]["type"] == "flex"
|
|
assert "widgets" not in spec
|
|
|
|
async def test_ui_takes_precedence_over_widgets(self, create_tool):
|
|
result = await create_tool.execute(
|
|
title="Both",
|
|
description="desc",
|
|
category="research",
|
|
ui={"type": "flex", "props": {}, "children": []},
|
|
widgets=[{"type": "metric", "title": "X", "data": {"value": "1"}}],
|
|
)
|
|
spec = _extract_spec(result)
|
|
assert spec["version"] == "1.0"
|
|
assert "ui" in spec
|
|
assert "widgets" not in spec
|
|
|
|
async def test_empty_ui_falls_back_to_widgets(self, create_tool):
|
|
result = await create_tool.execute(
|
|
title="Fallback",
|
|
description="desc",
|
|
category="data",
|
|
ui={},
|
|
widgets=[{"type": "metric", "title": "X", "size": "sm", "data": {"value": "1"}}],
|
|
)
|
|
spec = _extract_spec(result)
|
|
assert spec["version"] == "2.0"
|
|
assert "widgets" in spec
|
|
|
|
async def test_multi_pane_spec(self, create_tool):
|
|
result = await create_tool.execute(
|
|
title="Multi Pane",
|
|
description="desc",
|
|
category="data",
|
|
layout="quad",
|
|
panes={
|
|
"tl": {"type": "flex", "props": {}, "children": []},
|
|
"tr": {"type": "heading", "props": {"text": "Charts", "level": 4}},
|
|
},
|
|
)
|
|
spec = _extract_spec(result)
|
|
assert spec["version"] == "1.0"
|
|
assert "panes" in spec
|
|
assert spec["layout"] == "quad"
|
|
assert len(spec["panes"]) == 2
|
|
assert "ui" not in spec
|
|
assert "widgets" not in spec
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _convert_legacy_widget unit tests
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestConvertLegacyWidget:
|
|
def test_stats_single(self):
|
|
widgets = _convert_legacy_widget(
|
|
{"name": "KPI", "display": {"type": "stats", "stats": [{"label": "X", "value": "1"}]}},
|
|
"w0",
|
|
)
|
|
assert len(widgets) == 1
|
|
assert widgets[0]["id"] == "w0"
|
|
assert widgets[0]["type"] == "metric"
|
|
|
|
def test_stats_multiple(self):
|
|
widgets = _convert_legacy_widget(
|
|
{
|
|
"name": "KPIs",
|
|
"display": {
|
|
"type": "stats",
|
|
"stats": [
|
|
{"label": "A", "value": "1"},
|
|
{"label": "B", "value": "2"},
|
|
],
|
|
},
|
|
},
|
|
"w0",
|
|
)
|
|
assert len(widgets) == 2
|
|
assert widgets[0]["id"] == "w0-s0"
|
|
assert widgets[1]["id"] == "w0-s1"
|
|
|
|
def test_terminal(self):
|
|
widgets = _convert_legacy_widget(
|
|
{
|
|
"name": "Logs",
|
|
"display": {
|
|
"type": "terminal",
|
|
"termLines": [{"text": "hello", "type": "stdout"}],
|
|
"termTitle": "Server Log",
|
|
},
|
|
},
|
|
"w0",
|
|
)
|
|
assert len(widgets) == 1
|
|
assert widgets[0]["type"] == "terminal"
|
|
assert widgets[0]["props"]["title"] == "Server Log"
|
|
|
|
def test_metric_single(self):
|
|
widgets = _convert_legacy_widget(
|
|
{
|
|
"name": "KPI",
|
|
"display": {
|
|
"type": "metric",
|
|
"metric": {"label": "Revenue", "value": "$10B", "trend": "+5%"},
|
|
},
|
|
},
|
|
"w0",
|
|
)
|
|
assert len(widgets) == 1
|
|
assert widgets[0]["data"]["value"] == "$10B"
|
|
assert widgets[0]["data"]["trend"] == "+5%"
|
|
|
|
def test_activity_to_feed(self):
|
|
widgets = _convert_legacy_widget(
|
|
{
|
|
"name": "Activity",
|
|
"display": {"type": "activity", "items": [{"text": "logged in"}]},
|
|
},
|
|
"w0",
|
|
)
|
|
assert len(widgets) == 1
|
|
assert widgets[0]["type"] == "feed"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# AddWidgetTool tests
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestAddWidgetTool:
|
|
async def test_add_widget_returns_mutation(self, add_tool):
|
|
result = await add_tool.execute(
|
|
pocket_id="ai-abc12345",
|
|
widget={"type": "metric", "title": "New KPI", "data": {"value": "42"}},
|
|
)
|
|
mutation = _extract_mutation(result)
|
|
|
|
assert mutation["action"] == "add_widget"
|
|
assert mutation["pocket_id"] == "ai-abc12345"
|
|
assert mutation["widget"]["type"] == "metric"
|
|
assert mutation["widget"]["title"] == "New KPI"
|
|
assert mutation["widget"]["data"]["value"] == "42"
|
|
|
|
async def test_add_widget_with_position(self, add_tool):
|
|
result = await add_tool.execute(
|
|
pocket_id="ai-abc12345",
|
|
widget={"type": "chart", "title": "Sales", "data": [{"label": "A", "value": 1}]},
|
|
position=2,
|
|
)
|
|
mutation = _extract_mutation(result)
|
|
assert mutation["position"] == 2
|
|
|
|
async def test_add_widget_generates_id(self, add_tool):
|
|
result = await add_tool.execute(
|
|
pocket_id="ai-abc12345",
|
|
widget={"type": "text", "title": "Note", "data": {"content": "hello"}},
|
|
)
|
|
mutation = _extract_mutation(result)
|
|
assert mutation["widget"]["id"].startswith("ai-abc12345-w")
|
|
|
|
async def test_add_widget_message(self, add_tool):
|
|
result = await add_tool.execute(
|
|
pocket_id="ai-abc12345",
|
|
widget={"type": "metric", "title": "Speed", "data": {"value": "fast"}},
|
|
)
|
|
assert "Added widget **Speed**" in result
|
|
assert "ai-abc12345" in result
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# RemoveWidgetTool tests
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestRemoveWidgetTool:
|
|
async def test_remove_widget_returns_mutation(self, remove_tool):
|
|
result = await remove_tool.execute(
|
|
pocket_id="ai-abc12345",
|
|
widget_id="ai-abc12345-w2",
|
|
)
|
|
mutation = _extract_mutation(result)
|
|
|
|
assert mutation["action"] == "remove_widget"
|
|
assert mutation["pocket_id"] == "ai-abc12345"
|
|
assert mutation["widget_id"] == "ai-abc12345-w2"
|
|
|
|
async def test_remove_widget_message(self, remove_tool):
|
|
result = await remove_tool.execute(
|
|
pocket_id="ai-abc12345",
|
|
widget_id="ai-abc12345-w0",
|
|
)
|
|
assert "Removed widget" in result
|
|
assert "ai-abc12345-w0" in result
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Tool metadata tests
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestToolMetadata:
|
|
def test_create_pocket_name(self, create_tool):
|
|
assert create_tool.name == "create_pocket"
|
|
|
|
def test_add_widget_name(self, add_tool):
|
|
assert add_tool.name == "add_widget"
|
|
|
|
def test_remove_widget_name(self, remove_tool):
|
|
assert remove_tool.name == "remove_widget"
|
|
|
|
def test_all_standard_trust(self, create_tool, add_tool, remove_tool):
|
|
assert create_tool.trust_level == "standard"
|
|
assert add_tool.trust_level == "standard"
|
|
assert remove_tool.trust_level == "standard"
|
|
|
|
def test_create_pocket_params_required_fields(self, create_tool):
|
|
params = create_tool.parameters
|
|
assert "title" in params["required"]
|
|
assert "description" in params["required"]
|
|
assert "category" in params["required"]
|
|
assert "widgets" not in params["required"]
|
|
assert "ui" in params["properties"]
|
|
|
|
def test_add_widget_params(self, add_tool):
|
|
params = add_tool.parameters
|
|
assert "pocket_id" in params["required"]
|
|
assert "widget" in params["required"]
|
|
|
|
def test_remove_widget_params(self, remove_tool):
|
|
params = remove_tool.parameters
|
|
assert "pocket_id" in params["required"]
|
|
assert "widget_id" in params["required"]
|