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

214 lines
8.2 KiB
Python

from __future__ import annotations
import contextlib
import shutil
import unittest
import uuid
from pathlib import Path
from opc.core.config import OPCConfig, PermissionsV2Config
from opc.core.models import ApprovalAction, ApprovalDecision, PermissionResolution, RiskLevel, Task, TaskResult, TaskStatus
from opc.layer3_agent.runtime_v2.permissions import ToolPermissionResolver
from opc.layer3_agent.runtime_v2.runtime import NativeRuntimeV2
from opc.layer3_agent.runtime_v2.streaming_tool_executor import StreamingToolExecutor
from opc.layer3_agent.runtime_v2.subagents import SubagentManager
from opc.layer3_agent.runtime_v2.tool_planner import ToolPlanner
from opc.layer4_tools.registry import ToolDefinition, ToolRegistry
@contextlib.contextmanager
def _workspace_tempdir() -> Path:
base = Path.cwd() / ".tmp-test" / f"runtime-hooks-{uuid.uuid4().hex}"
base.mkdir(parents=True, exist_ok=True)
try:
yield base
finally:
shutil.rmtree(base, ignore_errors=True)
class _StubLLM:
def __init__(self) -> None:
self.config = type("Cfg", (), {"max_tokens": 2048})()
class RuntimeHookBusTests(unittest.IsolatedAsyncioTestCase):
async def test_pre_hook_permission_gate_blocks_execution(self) -> None:
registry = ToolRegistry()
executed: list[str] = []
async def shell_tool(command: str) -> dict[str, str]:
executed.append(command)
return {"stdout": command}
registry.register(ToolDefinition(
name="shell_exec",
description="shell",
parameters={"type": "object", "properties": {"command": {"type": "string"}}},
func=shell_tool,
concurrency_safe=False,
read_only=False,
requires_confirmation=True,
))
async def approval_callback(tool, arguments, task, on_progress):
_ = (tool, arguments, task, on_progress)
return False, ApprovalDecision(
action=ApprovalAction.ESCALATE,
risk_level=RiskLevel.HIGH,
rationale="Need approval first.",
confidence=1.0,
policy_source="test",
)
runtime = NativeRuntimeV2(
llm=_StubLLM(),
tool_registry=registry,
config=OPCConfig(),
approval_callback=approval_callback,
)
task = Task(id="task-hook", session_id="sess-hook", project_id="proj1")
hook_bus = runtime._build_tool_hook_bus(
runtime_session_id="rt_hook",
task=task,
permission_resolver=ToolPermissionResolver(PermissionsV2Config()),
)
executor = StreamingToolExecutor(
registry=registry,
planner=ToolPlanner(registry),
permission_resolver=ToolPermissionResolver(PermissionsV2Config()),
hook_bus=hook_bus,
)
results = await executor.execute([
{"id": "call-1", "function": "shell_exec", "arguments": {"command": "git commit -m test"}},
], task=task)
self.assertEqual(executed, [])
self.assertFalse(results[0]["result"]["success"])
self.assertEqual(results[0]["permission_decision"].resolution, PermissionResolution.ASK)
self.assertEqual(results[0]["result"]["approval"]["policy_source"], "test")
async def test_parallel_batch_failure_converges_remaining_calls(self) -> None:
registry = ToolRegistry()
executed: list[str] = []
async def fail_read(path: str) -> dict[str, str]:
raise RuntimeError(f"boom:{path}")
async def second_read(path: str) -> dict[str, str]:
executed.append(path)
return {"content": path}
registry.register(ToolDefinition(
name="file_read",
description="read",
parameters={"type": "object", "properties": {"path": {"type": "string"}}},
func=fail_read,
concurrency_safe=True,
read_only=True,
))
registry.register(ToolDefinition(
name="web_fetch",
description="fetch",
parameters={"type": "object", "properties": {"path": {"type": "string"}}},
func=second_read,
concurrency_safe=True,
read_only=True,
))
runtime = NativeRuntimeV2(
llm=_StubLLM(),
tool_registry=registry,
config=OPCConfig(),
)
hook_bus = runtime._build_tool_hook_bus(
runtime_session_id="rt_parallel",
task=Task(id="task-parallel", session_id="sess-parallel", project_id="proj1"),
permission_resolver=ToolPermissionResolver(PermissionsV2Config()),
)
executor = StreamingToolExecutor(
registry=registry,
planner=ToolPlanner(registry, max_parallel_read_tools=1),
permission_resolver=ToolPermissionResolver(PermissionsV2Config()),
hook_bus=hook_bus,
max_parallel_read_tools=1,
converge_on_parallel_failure=True,
)
results = await executor.execute([
{"id": "call-a", "function": "file_read", "arguments": {"path": "a.txt"}},
{"id": "call-b", "function": "web_fetch", "arguments": {"path": "b.txt"}},
])
self.assertFalse(results[0]["result"]["success"])
self.assertFalse(results[1]["result"]["success"])
self.assertTrue(results[1]["result"]["converged"])
self.assertEqual(executed, [])
class SubagentPermissionBridgeTests(unittest.IsolatedAsyncioTestCase):
async def test_permission_bridge_routes_child_approval_through_parent_task(self) -> None:
captured: dict[str, object] = {}
class _ApprovalEngine:
async def authorize_tool_call(self, **kwargs):
captured["approval_kwargs"] = dict(kwargs)
return True, ApprovalDecision(
action=ApprovalAction.AUTO_APPROVE,
risk_level=RiskLevel.LOW,
rationale="approved",
confidence=1.0,
policy_source="human_escalation",
metadata={"human_reply": "approve_session"},
)
approval_engine = _ApprovalEngine()
class _ChildAgent:
async def execute(self, child_task: Task) -> TaskResult:
captured["child_task_metadata"] = dict(child_task.metadata)
bridge = getattr(child_task, "_runtime_permission_bridge")
tool = ToolDefinition(
name="shell_exec",
description="shell",
parameters={"type": "object", "properties": {"command": {"type": "string"}}},
func=lambda **_: None, # type: ignore[arg-type]
concurrency_safe=False,
read_only=False,
requires_confirmation=True,
)
allowed, decision = await bridge(
tool=tool,
arguments={"command": "git status"},
approval_engine=approval_engine,
on_progress=None,
)
captured["allowed"] = allowed
captured["decision"] = decision
return TaskResult(status=TaskStatus.DONE, content="bridge-ok")
manager = SubagentManager(
parent_task=Task(id="parent-task", session_id="parent-session", project_id="proj1"),
config=OPCConfig(),
child_agent_factory=lambda profile, allowed_tools, prompt, overrides: _ChildAgent(),
runtime_session_id="rt_parent",
)
result = await manager.spawn(
profile="implement",
prompt="Need approval",
background=False,
name="bridge-worker",
)
self.assertTrue(result["success"])
self.assertTrue(captured["allowed"])
approval_kwargs = dict(captured["approval_kwargs"])
self.assertEqual(approval_kwargs["task"].id, "parent-task")
self.assertEqual(approval_kwargs["metadata"]["subagent_name"], "bridge-worker")
self.assertEqual(captured["child_task_metadata"]["_permission_bridge_runtime_session_id"], "rt_parent")
if __name__ == "__main__":
unittest.main()