Initial commit
This commit is contained in:
@@ -0,0 +1,304 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from opc.layer4_tools.collaboration_rpc import (
|
||||
OPC_COLLAB_RPC_HOST,
|
||||
OPC_COLLAB_RPC_PATH,
|
||||
OPC_COLLAB_RPC_PORT,
|
||||
OPC_COLLAB_RPC_TOKEN,
|
||||
OPC_COLLAB_RPC_TRANSPORT,
|
||||
call_collaboration_rpc,
|
||||
resolve_collaboration_rpc_transport,
|
||||
rpc_env_available,
|
||||
rpc_env_configured,
|
||||
start_collaboration_rpc_server,
|
||||
)
|
||||
|
||||
|
||||
class CollaborationRpcTransportTests(unittest.IsolatedAsyncioTestCase):
|
||||
def test_rpc_env_available_supports_fifo_legacy_and_tcp_contracts(self) -> None:
|
||||
self.assertTrue(
|
||||
rpc_env_available(
|
||||
{
|
||||
OPC_COLLAB_RPC_PATH: "/tmp/openopc/requests.fifo",
|
||||
OPC_COLLAB_RPC_TOKEN: "token",
|
||||
}
|
||||
)
|
||||
)
|
||||
self.assertTrue(
|
||||
rpc_env_available(
|
||||
{
|
||||
OPC_COLLAB_RPC_TRANSPORT: "fifo",
|
||||
OPC_COLLAB_RPC_PATH: "/tmp/openopc/requests.fifo",
|
||||
OPC_COLLAB_RPC_TOKEN: "token",
|
||||
}
|
||||
)
|
||||
)
|
||||
self.assertTrue(
|
||||
rpc_env_available(
|
||||
{
|
||||
OPC_COLLAB_RPC_TRANSPORT: "tcp",
|
||||
OPC_COLLAB_RPC_HOST: "127.0.0.1",
|
||||
OPC_COLLAB_RPC_PORT: "49321",
|
||||
OPC_COLLAB_RPC_TOKEN: "token",
|
||||
}
|
||||
)
|
||||
)
|
||||
self.assertFalse(
|
||||
rpc_env_available(
|
||||
{
|
||||
OPC_COLLAB_RPC_TRANSPORT: "tcp",
|
||||
OPC_COLLAB_RPC_HOST: "127.0.0.1",
|
||||
OPC_COLLAB_RPC_TOKEN: "token",
|
||||
}
|
||||
)
|
||||
)
|
||||
self.assertFalse(rpc_env_configured({}))
|
||||
self.assertTrue(rpc_env_configured({OPC_COLLAB_RPC_TRANSPORT: "tcp"}))
|
||||
|
||||
async def test_collab_cli_does_not_fallback_when_rpc_env_is_malformed(self) -> None:
|
||||
from opc import cli_collab
|
||||
|
||||
with patch.dict(
|
||||
"os.environ",
|
||||
{
|
||||
OPC_COLLAB_RPC_TRANSPORT: "unknown",
|
||||
OPC_COLLAB_RPC_TOKEN: "token",
|
||||
},
|
||||
clear=True,
|
||||
), patch(
|
||||
"opc.layer4_tools.collaboration_dispatch.dispatch_collaboration_tool",
|
||||
side_effect=AssertionError("direct dispatch should not run when RPC env is configured"),
|
||||
):
|
||||
payload, is_error = await cli_collab._dispatch("manager_board_read", {})
|
||||
|
||||
self.assertTrue(is_error)
|
||||
self.assertEqual(payload["error_type"], "infrastructure")
|
||||
self.assertIn("transport is unsupported", payload["error"])
|
||||
self.assertFalse(
|
||||
rpc_env_available(
|
||||
{
|
||||
OPC_COLLAB_RPC_TRANSPORT: "unknown",
|
||||
OPC_COLLAB_RPC_TOKEN: "token",
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
def test_collab_cli_rejects_inline_args_json_on_windows_external_rpc_env(self) -> None:
|
||||
from opc import cli_collab
|
||||
|
||||
parser = cli_collab._build_parser()
|
||||
opts = parser.parse_args(["delegate_work", "--args-json", '{"items": []}'])
|
||||
|
||||
with patch.object(cli_collab.os, "name", "nt"), patch.dict(
|
||||
"os.environ",
|
||||
{OPC_COLLAB_RPC_TRANSPORT: "tcp"},
|
||||
clear=True,
|
||||
):
|
||||
with self.assertRaises(SystemExit) as raised:
|
||||
cli_collab._collect_tool_args(opts)
|
||||
|
||||
self.assertIn("--args-json is disabled on Windows", str(raised.exception))
|
||||
|
||||
async def test_collab_cli_args_json_file_preserves_unicode_on_windows_rpc_env(self) -> None:
|
||||
from opc import cli_collab
|
||||
|
||||
title = "\u8d22\u52a1\u3001\u4f30\u503c\u4e0e 10x \u53ef\u884c\u6027\u7b5b\u9009"
|
||||
seen: list[dict[str, object]] = []
|
||||
|
||||
async def _dispatch(tool_name: str, args: dict[str, object]):
|
||||
seen.append({"tool": tool_name, "args": dict(args)})
|
||||
return {"ok": True}, False
|
||||
|
||||
server = await start_collaboration_rpc_server(_dispatch, transport="tcp")
|
||||
self.assertIsNotNone(server)
|
||||
assert server is not None
|
||||
try:
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
args_path = Path(tmpdir) / "args.json"
|
||||
args_path.write_text(
|
||||
json.dumps(
|
||||
{"items": [{"role_id": "cmo", "title": title}]},
|
||||
ensure_ascii=False,
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
parser = cli_collab._build_parser()
|
||||
opts = parser.parse_args(["delegate_work", "--args-json-file", str(args_path)])
|
||||
with patch.object(cli_collab.os, "name", "nt"), patch.dict(
|
||||
"os.environ",
|
||||
server.client_env,
|
||||
clear=True,
|
||||
):
|
||||
tool_args = cli_collab._collect_tool_args(opts)
|
||||
payload, is_error = await cli_collab._dispatch(opts.tool, tool_args)
|
||||
finally:
|
||||
await server.close()
|
||||
|
||||
self.assertFalse(is_error)
|
||||
self.assertEqual(payload["ok"], True)
|
||||
self.assertEqual(seen[0]["tool"], "delegate_work")
|
||||
received = seen[0]["args"]["items"][0]["title"] # type: ignore[index]
|
||||
self.assertEqual(received, title)
|
||||
|
||||
def test_collab_cli_rejects_args_stdin_on_windows_external_rpc_env(self) -> None:
|
||||
from opc import cli_collab
|
||||
|
||||
parser = cli_collab._build_parser()
|
||||
opts = parser.parse_args(["delegate_work", "--args-stdin"])
|
||||
|
||||
with patch.object(cli_collab.os, "name", "nt"), patch.dict(
|
||||
"os.environ",
|
||||
{OPC_COLLAB_RPC_TRANSPORT: "tcp"},
|
||||
clear=True,
|
||||
):
|
||||
with self.assertRaises(SystemExit) as raised:
|
||||
cli_collab._collect_tool_args(opts)
|
||||
|
||||
self.assertIn("--args-stdin is disabled on Windows", str(raised.exception))
|
||||
|
||||
def test_collab_cli_args_json_file_accepts_utf8_sig(self) -> None:
|
||||
from opc import cli_collab
|
||||
|
||||
title = "\u8d22\u52a1\u3001\u4f30\u503c\u4e0e 10x \u53ef\u884c\u6027\u7b5b\u9009"
|
||||
parser = cli_collab._build_parser()
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
args_path = Path(tmpdir) / "args.json"
|
||||
args_path.write_text(
|
||||
json.dumps(
|
||||
{"items": [{"role_id": "cmo", "title": title}]},
|
||||
ensure_ascii=False,
|
||||
),
|
||||
encoding="utf-8-sig",
|
||||
)
|
||||
opts = parser.parse_args(["delegate_work", "--args-json-file", str(args_path)])
|
||||
payload = cli_collab._collect_tool_args(opts)
|
||||
|
||||
self.assertEqual(payload["items"][0]["title"], title)
|
||||
|
||||
def test_forced_fifo_transport_fails_cleanly_when_unavailable(self) -> None:
|
||||
with patch("opc.layer4_tools.collaboration_rpc.fifo_rpc_supported", return_value=False):
|
||||
with self.assertRaisesRegex(RuntimeError, "FIFO collaboration RPC is unavailable"):
|
||||
resolve_collaboration_rpc_transport("fifo")
|
||||
|
||||
async def test_fifo_rpc_call_fails_cleanly_when_platform_has_no_fifo(self) -> None:
|
||||
with patch("opc.layer4_tools.collaboration_rpc.fifo_rpc_supported", return_value=False), patch(
|
||||
"opc.layer4_tools.collaboration_rpc.os.mkfifo",
|
||||
side_effect=AssertionError("mkfifo should not be called"),
|
||||
create=True,
|
||||
):
|
||||
payload, is_error = await call_collaboration_rpc(
|
||||
"manager_board_read",
|
||||
{"include_children": True},
|
||||
env={
|
||||
OPC_COLLAB_RPC_TRANSPORT: "fifo",
|
||||
OPC_COLLAB_RPC_PATH: "/tmp/openopc/requests.fifo",
|
||||
OPC_COLLAB_RPC_TOKEN: "token",
|
||||
},
|
||||
)
|
||||
|
||||
self.assertTrue(is_error)
|
||||
self.assertEqual(payload["error_type"], "infrastructure")
|
||||
self.assertIn("FIFO collaboration broker RPC is unavailable", payload["error"])
|
||||
|
||||
async def test_auto_transport_uses_tcp_when_fifo_unavailable(self) -> None:
|
||||
async def _dispatch(tool_name: str, args: dict[str, object]):
|
||||
return {"tool": tool_name, "args": args}, False
|
||||
|
||||
with patch("opc.layer4_tools.collaboration_rpc.fifo_rpc_supported", return_value=False), patch(
|
||||
"opc.layer4_tools.collaboration_rpc.os.mkfifo",
|
||||
side_effect=AssertionError("mkfifo should not be called"),
|
||||
create=True,
|
||||
):
|
||||
server = await start_collaboration_rpc_server(_dispatch)
|
||||
|
||||
self.assertIsNotNone(server)
|
||||
assert server is not None
|
||||
try:
|
||||
self.assertEqual(server.transport, "tcp")
|
||||
self.assertEqual(server.client_env[OPC_COLLAB_RPC_TRANSPORT], "tcp")
|
||||
self.assertEqual(server.client_env[OPC_COLLAB_RPC_HOST], "127.0.0.1")
|
||||
self.assertTrue(int(server.client_env[OPC_COLLAB_RPC_PORT]) > 0)
|
||||
finally:
|
||||
await server.close()
|
||||
|
||||
async def test_tcp_rpc_roundtrip_routes_to_dispatch(self) -> None:
|
||||
calls: list[tuple[str, dict[str, object]]] = []
|
||||
|
||||
async def _dispatch(tool_name: str, args: dict[str, object]):
|
||||
calls.append((tool_name, dict(args)))
|
||||
return {"ok": True, "tool": tool_name, "args": args}, False
|
||||
|
||||
server = await start_collaboration_rpc_server(_dispatch, transport="tcp")
|
||||
self.assertIsNotNone(server)
|
||||
assert server is not None
|
||||
try:
|
||||
payload, is_error = await call_collaboration_rpc(
|
||||
"delegate_work",
|
||||
{"items": [{"role_id": "cto", "title": "Build"}]},
|
||||
env=server.client_env,
|
||||
)
|
||||
finally:
|
||||
await server.close()
|
||||
|
||||
self.assertFalse(is_error)
|
||||
self.assertEqual(payload["tool"], "delegate_work")
|
||||
self.assertEqual([name for name, _args in calls], ["delegate_work"])
|
||||
|
||||
async def test_tcp_rpc_invalid_token_returns_typed_error(self) -> None:
|
||||
async def _dispatch(tool_name: str, args: dict[str, object]):
|
||||
return {"should_not_run": True}, False
|
||||
|
||||
server = await start_collaboration_rpc_server(_dispatch, transport="tcp")
|
||||
self.assertIsNotNone(server)
|
||||
assert server is not None
|
||||
try:
|
||||
env = dict(server.client_env)
|
||||
env[OPC_COLLAB_RPC_TOKEN] = "wrong-token"
|
||||
payload, is_error = await call_collaboration_rpc(
|
||||
"send_dm",
|
||||
{"to_agent": "cto"},
|
||||
env=env,
|
||||
)
|
||||
finally:
|
||||
await server.close()
|
||||
|
||||
self.assertTrue(is_error)
|
||||
self.assertEqual(payload["error_type"], "infrastructure")
|
||||
self.assertIn("token rejected", payload["error"])
|
||||
self.assertEqual(payload["tool_name"], "send_dm")
|
||||
|
||||
async def test_tcp_rpc_malformed_json_returns_infrastructure_error(self) -> None:
|
||||
async def _dispatch(tool_name: str, args: dict[str, object]):
|
||||
return {"should_not_run": True}, False
|
||||
|
||||
server = await start_collaboration_rpc_server(_dispatch, transport="tcp")
|
||||
self.assertIsNotNone(server)
|
||||
assert server is not None
|
||||
writer: asyncio.StreamWriter | None = None
|
||||
try:
|
||||
reader, writer = await asyncio.open_connection(server.host, server.port)
|
||||
writer.write(b"{not-json}\n")
|
||||
await writer.drain()
|
||||
raw = await reader.readline()
|
||||
finally:
|
||||
if writer is not None:
|
||||
writer.close()
|
||||
await writer.wait_closed()
|
||||
await server.close()
|
||||
|
||||
response = json.loads(raw.decode("utf-8"))
|
||||
self.assertTrue(response["is_error"])
|
||||
self.assertEqual(response["result"]["error_type"], "infrastructure")
|
||||
self.assertIn("invalid JSON", response["result"]["error"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user