Files
2026-07-13 13:12:33 +08:00

152 lines
4.9 KiB
Python

from __future__ import annotations
import pytest
import structlog.testing
from opensquilla.engine.types import ToolCall
from opensquilla.tools.dispatch import build_tool_handler
from opensquilla.tools.registry import ToolRegistry
from opensquilla.tools.types import (
CallerKind,
InteractionMode,
ToolContext,
ToolSpec,
current_tool_context,
)
def _build_registry() -> ToolRegistry:
registry = ToolRegistry()
async def denied() -> str:
raise ValueError("bad argument")
async def pending() -> str:
return (
'{\"status\": \"approval_required\", \"approval_id\": \"abc123\",'
' \"command\": \"rm /tmp/secret\", \"warning\": \"destructive\"}'
)
async def required_echo(value: str) -> str:
return value
registry.register(ToolSpec(name="denied", description="denied", parameters={}), denied)
registry.register(ToolSpec(name="pending", description="pending", parameters={}), pending)
registry.register(
ToolSpec(
name="required_echo",
description="required echo",
parameters={"value": {"type": "string"}},
required=["value"],
),
required_echo,
)
return registry
@pytest.mark.asyncio
async def test_dispatch_tool_failed_log_includes_surface_context() -> None:
handler = build_tool_handler(_build_registry())
token = current_tool_context.set(
ToolContext(
is_owner=False,
caller_kind=CallerKind.WEB,
session_key="agent:main:demo",
agent_id="main",
)
)
try:
with structlog.testing.capture_logs() as captured:
result = await handler(
ToolCall(tool_use_id="tc-1", tool_name="denied", arguments={})
)
assert result.is_error is True
assert any(event["event"] == "dispatch.tool_failed" for event in captured)
event = next(event for event in captured if event["event"] == "dispatch.tool_failed")
assert event["tool"] == "denied"
assert event["tool_use_id"] == "tc-1"
assert event["agent_id"] == "main"
assert event["session_key"] == "agent:main:demo"
finally:
current_tool_context.reset(token)
@pytest.mark.asyncio
async def test_dispatch_missing_required_log_records_shape_guidance_flag(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setenv("OPENSQUILLA_MISSING_REQUIRED_ARGUMENT_SHAPE_GUIDANCE", "1")
handler = build_tool_handler(_build_registry())
token = current_tool_context.set(
ToolContext(
is_owner=True,
caller_kind=CallerKind.CLI,
session_key="agent:main:shape-guidance",
agent_id="main",
)
)
try:
with structlog.testing.capture_logs() as captured:
result = await handler(
ToolCall(
tool_use_id="tc-shape",
tool_name="required_echo",
arguments={},
)
)
assert result.is_error is True
event = next(
event
for event in captured
if event["event"] == "dispatch.invalid_tool_arguments"
)
assert event["shape_guidance_enabled"] is True
assert event["argument_keys"] == []
assert event["missing"] == ["value"]
assert event["session_key"] == "agent:main:shape-guidance"
finally:
current_tool_context.reset(token)
@pytest.mark.asyncio
async def test_dispatch_unsupported_surface_log_includes_approval_id() -> None:
handler = build_tool_handler(_build_registry())
token = current_tool_context.set(
ToolContext(
is_owner=False,
caller_kind=CallerKind.CRON,
interaction_mode=InteractionMode.UNATTENDED,
session_key="cron:system",
agent_id="cron",
)
)
try:
with structlog.testing.capture_logs() as captured:
result = await handler(
ToolCall(tool_use_id="tc-2", tool_name="pending", arguments={}),
)
assert result.is_error is False
assert result.execution_status is not None
assert result.execution_status["status"] == "unknown"
assert result.execution_status["reason"] == "approval_pending"
assert any(
event["event"] == "dispatch.approval_required_unsupported_surface"
for event in captured
)
event = next(
event
for event in captured
if event["event"] == "dispatch.approval_required_unsupported_surface"
)
assert event["tool"] == "pending"
assert event["surface"] == "cron"
assert event["approval_id"] == "abc123"
assert event["tool_use_id"] == "tc-2"
assert event["agent_id"] == "cron"
assert event["session_key"] == "cron:system"
finally:
current_tool_context.reset(token)