152 lines
4.9 KiB
Python
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)
|