Files
OpenOPC/tests/test_tool_argument_recovery.py
2026-07-01 17:56:31 +08:00

108 lines
3.9 KiB
Python

from __future__ import annotations
import unittest
from opc.layer3_agent.runtime_v2.runtime import NativeRuntimeV2
from opc.layer4_tools.registry import ToolDefinition, ToolRegistry
from opc.llm.provider import _parse_tool_arguments
class ToolArgumentParsingTests(unittest.TestCase):
def test_parse_tool_arguments_returns_error_context_for_invalid_json(self) -> None:
parsed, raw, error = _parse_tool_arguments("shell_exec", '{"command": "echo hi"')
self.assertEqual(parsed, '{"command": "echo hi"')
self.assertEqual(raw, '{"command": "echo hi"')
self.assertIsNotNone(error)
self.assertIn("Invalid tool arguments JSON for `shell_exec`", error)
def test_runtime_v2_tool_argument_finalize_preserves_unicode(self) -> None:
runtime = NativeRuntimeV2.__new__(NativeRuntimeV2)
calls = runtime._finalize_tool_calls(
{
0: {
"id": "call-1",
"function": "todo_write",
"arguments_chunks": [
'{"todos":[{"id":"1","title":"梳理需求","status":"in_progress"}]}'
],
}
}
)
self.assertEqual(calls[0]["function"], "todo_write")
self.assertIsNone(calls[0]["arguments_parse_error"])
self.assertEqual(calls[0]["arguments"]["todos"][0]["title"], "梳理需求")
class ToolRegistryUnknownArgumentTests(unittest.IsolatedAsyncioTestCase):
async def test_unknown_argument_returns_structured_error_with_valid_params(self) -> None:
"""Unknown tool arguments must surface as a tool-call error, not be
silently dropped. The error message must list the valid parameters
so the agent can retry with the right names instead of the LLM
hallucination (e.g. `digest` vs `summary`)."""
registry = ToolRegistry()
async def example_stub(summary: str = "", parent: str = "") -> dict[str, str]:
return {"summary": summary, "parent": parent}
registry.register(
ToolDefinition(
name="example_tool",
description="stub",
parameters={
"type": "object",
"properties": {
"summary": {"type": "string"},
"parent": {"type": "string"},
},
},
func=example_stub,
)
)
result = await registry.invoke(
"example_tool",
{"digest": "3 children done"},
)
self.assertFalse(result.get("success", True))
error = str(result.get("error", ""))
self.assertIn("example_tool", error)
self.assertIn("'digest'", error)
# Error message must enumerate valid params so the agent can retry.
self.assertIn("summary", error)
self.assertIn("retry", error.lower())
async def test_known_arguments_still_execute_normally(self) -> None:
"""Guardrail: making unknown-arg errors strict must not reject
legitimate calls or the `task` auto-injection path."""
registry = ToolRegistry()
async def echo_tool(message: str, task=None) -> dict[str, str]:
return {"echoed": message, "task_injected": str(bool(task))}
registry.register(
ToolDefinition(
name="echo",
description="stub",
parameters={
"type": "object",
"properties": {"message": {"type": "string"}},
},
func=echo_tool,
)
)
result = await registry.invoke(
"echo",
{"message": "hello"},
task=object(),
)
self.assertTrue(result.get("success"))
self.assertEqual(result["result"]["echoed"], "hello")
self.assertEqual(result["result"]["task_injected"], "True")
if __name__ == "__main__":
unittest.main()