239 lines
10 KiB
Python
239 lines
10 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
from opc.core.company_tools import (
|
|
COLLAB_PROFILE_COORDINATOR_DEFAULT,
|
|
COLLAB_PROFILE_MANAGER_DEFAULT,
|
|
COLLAB_PROFILE_WORKER_DEFAULT,
|
|
REVIEW_EXECUTE_TURN_MODE,
|
|
resolve_allowed_collaboration_tools,
|
|
)
|
|
from opc.core.events import EventBus
|
|
from opc.core.models import (
|
|
ExecutionCheckpoint,
|
|
ExecutionMode,
|
|
Task,
|
|
TaskStatus,
|
|
)
|
|
from opc.database.store import OPCStore
|
|
from opc.engine import OPCEngine
|
|
from opc.layer2_organization.communication import CommunicationManager
|
|
from opc.layer2_organization.work_item_links import set_linked_work_item_id
|
|
from opc.layer4_tools.collaboration import build_external_cli_tool_contract_lines, create_collaboration_tools
|
|
from opc.layer4_tools.collaboration_dispatch import dispatch_collaboration_tool
|
|
from opc.layer4_tools.user_input import request_user_input
|
|
|
|
|
|
class RequestUserInputCollabTests(unittest.IsolatedAsyncioTestCase):
|
|
async def test_request_user_input_normalizes_legacy_and_choice_questions(self) -> None:
|
|
result = await request_user_input(
|
|
reason="Need deployment input.",
|
|
questions=[
|
|
"Which region should we deploy to?",
|
|
{
|
|
"id": "deployment_region",
|
|
"header": "Deployment region",
|
|
"question": "Which region should I target?",
|
|
"options": [
|
|
{"label": "US East", "description": "Use us-east-1"},
|
|
{"label": "EU West", "description": "Use eu-west-1"},
|
|
{"label": "Asia", "description": "Use ap-east-1"},
|
|
{"label": "Overflow option"},
|
|
],
|
|
},
|
|
],
|
|
required_fields=["deployment_region"],
|
|
)
|
|
|
|
self.assertTrue(result["requires_user_input"])
|
|
self.assertEqual(
|
|
result["questions"],
|
|
["Which region should we deploy to?", "Which region should I target?"],
|
|
)
|
|
self.assertEqual(result["input_questions"][0]["id"], "question_1")
|
|
self.assertEqual(result["input_questions"][0]["options"], [])
|
|
self.assertTrue(result["input_questions"][0]["allow_freeform"])
|
|
structured = result["input_questions"][1]
|
|
self.assertEqual(structured["id"], "deployment_region")
|
|
self.assertEqual([option["id"] for option in structured["options"]], ["a", "b", "c"])
|
|
self.assertEqual(len(structured["options"]), 3)
|
|
self.assertTrue(structured["allow_freeform"])
|
|
self.assertTrue(structured["required"])
|
|
|
|
def test_company_toolsets_do_not_include_request_user_input(self) -> None:
|
|
for profile in (
|
|
COLLAB_PROFILE_WORKER_DEFAULT,
|
|
COLLAB_PROFILE_MANAGER_DEFAULT,
|
|
COLLAB_PROFILE_COORDINATOR_DEFAULT,
|
|
):
|
|
with self.subTest(profile=profile):
|
|
self.assertNotIn("request_user_input", resolve_allowed_collaboration_tools(profile))
|
|
|
|
review_task = Task(
|
|
metadata={
|
|
"runtime_model": "multi_team_org",
|
|
"current_turn_mode": REVIEW_EXECUTE_TURN_MODE,
|
|
}
|
|
)
|
|
self.assertNotIn(
|
|
"request_user_input",
|
|
resolve_allowed_collaboration_tools(COLLAB_PROFILE_WORKER_DEFAULT, task=review_task),
|
|
)
|
|
|
|
def test_close_human_review_is_only_added_for_delivery_review_followup(self) -> None:
|
|
task = Task(
|
|
metadata={
|
|
"execution_mode": ExecutionMode.COMPANY_MODE.value,
|
|
"runtime_model": "multi_team_org",
|
|
"current_turn_mode": "dispatch_required",
|
|
},
|
|
)
|
|
self.assertNotIn(
|
|
"close_human_review",
|
|
resolve_allowed_collaboration_tools(COLLAB_PROFILE_MANAGER_DEFAULT, task=task),
|
|
)
|
|
task.metadata["human_review_close_allowed"] = True
|
|
self.assertIn(
|
|
"close_human_review",
|
|
resolve_allowed_collaboration_tools(COLLAB_PROFILE_MANAGER_DEFAULT, task=task),
|
|
)
|
|
|
|
def test_external_contract_omits_request_user_input_for_company_collab(self) -> None:
|
|
lines = build_external_cli_tool_contract_lines({"request_user_input"})
|
|
rendered = "\n".join(lines)
|
|
self.assertNotIn("`request_user_input` arguments", rendered)
|
|
self.assertIn("Use the exact argument names", rendered)
|
|
|
|
async def test_company_collaboration_tools_do_not_register_request_user_input(self) -> None:
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
store = OPCStore(Path(tmpdir) / "tasks.db")
|
|
await store.initialize()
|
|
try:
|
|
communication = CommunicationManager(store, EventBus())
|
|
tool_names = {tool.name for tool in create_collaboration_tools(communication)}
|
|
self.assertNotIn("request_user_input", tool_names)
|
|
finally:
|
|
await store.close()
|
|
|
|
async def test_opc_collab_dispatch_request_user_input_is_unknown(self) -> None:
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
store = OPCStore(Path(tmpdir) / "tasks.db")
|
|
await store.initialize()
|
|
try:
|
|
task = Task(
|
|
id="task-cli-input",
|
|
title="CLI scoped work",
|
|
project_id="proj1",
|
|
session_id="child-session",
|
|
parent_session_id="parent-session",
|
|
assigned_to="cto",
|
|
status=TaskStatus.RUNNING,
|
|
metadata={
|
|
"execution_mode": ExecutionMode.COMPANY_MODE.value,
|
|
"work_item_projection_id": "cto_plan",
|
|
"delegation_seat_id": "seat::team::ceo::cto",
|
|
},
|
|
)
|
|
set_linked_work_item_id(task, "work-item-cli")
|
|
await store.save_task(task)
|
|
env = {
|
|
"OPC_COMMS_FROM": "cto",
|
|
"OPC_COMMS_PROJECT": "proj1",
|
|
"OPC_COMMS_SESSION": "parent-session",
|
|
"OPC_WORKSPACE_ROOT": tmpdir,
|
|
"OPC_TASK_ID": task.id,
|
|
"OPC_WORK_ITEM_ID": "work-item-cli",
|
|
"OPC_PROJECT_DB_PATH": str(store.db_path),
|
|
"OPC_ALLOWED_COLLAB_TOOLS": json.dumps(["request_user_input"]),
|
|
}
|
|
|
|
payload, is_error = await dispatch_collaboration_tool(
|
|
"request_user_input",
|
|
{"reason": "Need approval.", "target_task_id": "other-task"},
|
|
env=env,
|
|
)
|
|
self.assertTrue(is_error)
|
|
self.assertIn("unknown tool: request_user_input", payload["error"])
|
|
finally:
|
|
await store.close()
|
|
|
|
async def test_engine_does_not_coerce_external_user_input_markers(self) -> None:
|
|
engine = OPCEngine.__new__(OPCEngine)
|
|
self.assertFalse(hasattr(engine, "_coerce_external_user_input_pause"))
|
|
|
|
async def test_resume_task_checkpoint_stores_plain_user_input(self) -> None:
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
store = OPCStore(Path(tmpdir) / "tasks.db")
|
|
await store.initialize()
|
|
try:
|
|
pause_request = (await request_user_input(
|
|
reason="Need deployment region.",
|
|
questions=[
|
|
{
|
|
"id": "deployment_region",
|
|
"header": "Deployment region",
|
|
"question": "Which region?",
|
|
"options": [{"id": "a", "label": "US East"}],
|
|
}
|
|
],
|
|
))
|
|
task = Task(
|
|
id="task-resume-input",
|
|
title="Resume input",
|
|
project_id="proj1",
|
|
session_id="session-1",
|
|
assigned_to="engineer",
|
|
status=TaskStatus.AWAITING_HUMAN,
|
|
)
|
|
await store.save_task(task)
|
|
checkpoint = ExecutionCheckpoint(
|
|
checkpoint_id="cp-resume-input",
|
|
project_id="proj1",
|
|
session_id="session-1",
|
|
checkpoint_type="task_user_input",
|
|
task_id=task.id,
|
|
payload={
|
|
"task_id": task.id,
|
|
"task_ids": [task.id],
|
|
"execution_mode": ExecutionMode.SINGLE_AGENT.value,
|
|
"pause_request": pause_request,
|
|
"runtime_v2": {"runtime_session_id": "rt-1"},
|
|
},
|
|
)
|
|
await store.save_execution_checkpoint(checkpoint)
|
|
engine = OPCEngine.__new__(OPCEngine)
|
|
engine.store = store
|
|
engine.project_id = "proj1"
|
|
|
|
async def _ensure_checkpoint_runtime_v2_payload(cp: ExecutionCheckpoint) -> ExecutionCheckpoint:
|
|
return cp
|
|
|
|
async def _execute_single_agent(_tasks: list[Task], _agent: str | None) -> str:
|
|
return "resumed"
|
|
|
|
engine._ensure_checkpoint_runtime_v2_payload = _ensure_checkpoint_runtime_v2_payload
|
|
engine._restore_runtime_state_from_checkpoint = lambda _task, _payload: None
|
|
engine._execute_single_agent = _execute_single_agent
|
|
|
|
response = await engine._resume_task_checkpoint(
|
|
checkpoint,
|
|
"Selected US East.",
|
|
)
|
|
|
|
self.assertEqual(response, "resumed")
|
|
refreshed = await store.get_task(task.id)
|
|
self.assertEqual(refreshed.context_snapshot["user_supplied_input"], "Selected US East.")
|
|
self.assertNotIn("user_input_answers", refreshed.context_snapshot)
|
|
checkpoints = await store.get_execution_checkpoints(project_id="proj1")
|
|
self.assertEqual(checkpoints[0].status, "resolved")
|
|
finally:
|
|
await store.close()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|