agent-relay 3.2.3 → 3.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +265 -108
- package/package.json +11 -10
- package/packages/acp-bridge/package.json +2 -2
- package/packages/config/package.json +1 -1
- package/packages/hooks/package.json +4 -4
- package/packages/memory/package.json +2 -2
- package/packages/openclaw/package.json +2 -2
- package/packages/policy/package.json +2 -2
- package/packages/sdk/ADAPTER_REVIEW.md +109 -0
- package/packages/sdk/dist/communicate/a2a-bridge.d.ts +25 -0
- package/packages/sdk/dist/communicate/a2a-bridge.d.ts.map +1 -0
- package/packages/sdk/dist/communicate/a2a-bridge.js +89 -0
- package/packages/sdk/dist/communicate/a2a-bridge.js.map +1 -0
- package/packages/sdk/dist/communicate/a2a-server.d.ts +31 -0
- package/packages/sdk/dist/communicate/a2a-server.d.ts.map +1 -0
- package/packages/sdk/dist/communicate/a2a-server.js +220 -0
- package/packages/sdk/dist/communicate/a2a-server.js.map +1 -0
- package/packages/sdk/dist/communicate/a2a-transport.d.ts +48 -0
- package/packages/sdk/dist/communicate/a2a-transport.d.ts.map +1 -0
- package/packages/sdk/dist/communicate/a2a-transport.js +302 -0
- package/packages/sdk/dist/communicate/a2a-transport.js.map +1 -0
- package/packages/sdk/dist/communicate/a2a-types.d.ts +107 -0
- package/packages/sdk/dist/communicate/a2a-types.d.ts.map +1 -0
- package/packages/sdk/dist/communicate/a2a-types.js +209 -0
- package/packages/sdk/dist/communicate/a2a-types.js.map +1 -0
- package/packages/sdk/dist/communicate/adapters/claude-sdk.d.ts +28 -0
- package/packages/sdk/dist/communicate/adapters/claude-sdk.d.ts.map +1 -0
- package/packages/sdk/dist/communicate/adapters/claude-sdk.js +47 -0
- package/packages/sdk/dist/communicate/adapters/claude-sdk.js.map +1 -0
- package/packages/sdk/dist/communicate/adapters/crewai.d.ts +42 -0
- package/packages/sdk/dist/communicate/adapters/crewai.d.ts.map +1 -0
- package/packages/sdk/dist/communicate/adapters/crewai.js +95 -0
- package/packages/sdk/dist/communicate/adapters/crewai.js.map +1 -0
- package/packages/sdk/dist/communicate/adapters/google-adk.d.ts +53 -0
- package/packages/sdk/dist/communicate/adapters/google-adk.d.ts.map +1 -0
- package/packages/sdk/dist/communicate/adapters/google-adk.js +77 -0
- package/packages/sdk/dist/communicate/adapters/google-adk.js.map +1 -0
- package/packages/sdk/dist/communicate/adapters/index.d.ts +7 -0
- package/packages/sdk/dist/communicate/adapters/index.d.ts.map +1 -0
- package/packages/sdk/dist/communicate/adapters/index.js +7 -0
- package/packages/sdk/dist/communicate/adapters/index.js.map +1 -0
- package/packages/sdk/dist/communicate/adapters/langgraph.d.ts +40 -0
- package/packages/sdk/dist/communicate/adapters/langgraph.d.ts.map +1 -0
- package/packages/sdk/dist/communicate/adapters/langgraph.js +77 -0
- package/packages/sdk/dist/communicate/adapters/langgraph.js.map +1 -0
- package/packages/sdk/dist/communicate/adapters/openai-agents.d.ts +25 -0
- package/packages/sdk/dist/communicate/adapters/openai-agents.d.ts.map +1 -0
- package/packages/sdk/dist/communicate/adapters/openai-agents.js +70 -0
- package/packages/sdk/dist/communicate/adapters/openai-agents.js.map +1 -0
- package/packages/sdk/dist/communicate/adapters/pi.d.ts +45 -0
- package/packages/sdk/dist/communicate/adapters/pi.d.ts.map +1 -0
- package/packages/sdk/dist/communicate/adapters/pi.js +59 -0
- package/packages/sdk/dist/communicate/adapters/pi.js.map +1 -0
- package/packages/sdk/dist/communicate/core.d.ts +58 -0
- package/packages/sdk/dist/communicate/core.d.ts.map +1 -0
- package/packages/sdk/dist/communicate/core.js +128 -0
- package/packages/sdk/dist/communicate/core.js.map +1 -0
- package/packages/sdk/dist/communicate/index.d.ts +4 -0
- package/packages/sdk/dist/communicate/index.d.ts.map +1 -0
- package/packages/sdk/dist/communicate/index.js +4 -0
- package/packages/sdk/dist/communicate/index.js.map +1 -0
- package/packages/sdk/dist/communicate/transport.d.ts +36 -0
- package/packages/sdk/dist/communicate/transport.d.ts.map +1 -0
- package/packages/sdk/dist/communicate/transport.js +371 -0
- package/packages/sdk/dist/communicate/transport.js.map +1 -0
- package/packages/sdk/dist/communicate/types.d.ts +58 -0
- package/packages/sdk/dist/communicate/types.d.ts.map +1 -0
- package/packages/sdk/dist/communicate/types.js +66 -0
- package/packages/sdk/dist/communicate/types.js.map +1 -0
- package/packages/sdk/dist/workflows/builder.d.ts +35 -5
- package/packages/sdk/dist/workflows/builder.d.ts.map +1 -1
- package/packages/sdk/dist/workflows/builder.js +81 -7
- package/packages/sdk/dist/workflows/builder.js.map +1 -1
- package/packages/sdk/dist/workflows/cli.js +14 -1
- package/packages/sdk/dist/workflows/cli.js.map +1 -1
- package/packages/sdk/dist/workflows/runner.d.ts +10 -2
- package/packages/sdk/dist/workflows/runner.d.ts.map +1 -1
- package/packages/sdk/dist/workflows/runner.js +95 -1
- package/packages/sdk/dist/workflows/runner.js.map +1 -1
- package/packages/sdk/dist/workflows/types.d.ts +11 -0
- package/packages/sdk/dist/workflows/types.d.ts.map +1 -1
- package/packages/sdk/examples/communicate/claude_sdk_example.ts +5 -0
- package/packages/sdk/examples/communicate/pi_example.ts +8 -0
- package/packages/sdk/package.json +48 -2
- package/packages/sdk/src/__tests__/builder-deterministic.test.ts +132 -0
- package/packages/sdk/src/__tests__/communicate/a2a-bridge.test.ts +211 -0
- package/packages/sdk/src/__tests__/communicate/a2a-server.test.ts +359 -0
- package/packages/sdk/src/__tests__/communicate/a2a-transport.test.ts +537 -0
- package/packages/sdk/src/__tests__/communicate/a2a-types.test.ts +297 -0
- package/packages/sdk/src/__tests__/communicate/adapters/claude-sdk.test.ts +163 -0
- package/packages/sdk/src/__tests__/communicate/adapters/crewai.test.ts +219 -0
- package/packages/sdk/src/__tests__/communicate/adapters/e2e-crewai.test.ts +101 -0
- package/packages/sdk/src/__tests__/communicate/adapters/e2e-google-adk.test.ts +166 -0
- package/packages/sdk/src/__tests__/communicate/adapters/e2e-langgraph.test.ts +181 -0
- package/packages/sdk/src/__tests__/communicate/adapters/e2e-openai-agents.test.ts +137 -0
- package/packages/sdk/src/__tests__/communicate/adapters/e2e-pi.test.ts +140 -0
- package/packages/sdk/src/__tests__/communicate/adapters/google-adk.test.ts +200 -0
- package/packages/sdk/src/__tests__/communicate/adapters/langgraph.test.ts +162 -0
- package/packages/sdk/src/__tests__/communicate/adapters/openai-agents.test.ts +166 -0
- package/packages/sdk/src/__tests__/communicate/adapters/pi.test.ts +140 -0
- package/packages/sdk/src/__tests__/communicate/core.test.ts +574 -0
- package/packages/sdk/src/__tests__/communicate/integration/cross-framework.test.ts +353 -0
- package/packages/sdk/src/__tests__/communicate/transport.test.ts +613 -0
- package/packages/sdk/src/__tests__/start-from.test.ts +346 -0
- package/packages/sdk/src/communicate/a2a-bridge.ts +111 -0
- package/packages/sdk/src/communicate/a2a-server.ts +277 -0
- package/packages/sdk/src/communicate/a2a-transport.ts +395 -0
- package/packages/sdk/src/communicate/a2a-types.ts +338 -0
- package/packages/sdk/src/communicate/adapters/claude-sdk.ts +85 -0
- package/packages/sdk/src/communicate/adapters/crewai.ts +141 -0
- package/packages/sdk/src/communicate/adapters/google-adk.ts +139 -0
- package/packages/sdk/src/communicate/adapters/index.ts +6 -0
- package/packages/sdk/src/communicate/adapters/langgraph.ts +112 -0
- package/packages/sdk/src/communicate/adapters/openai-agents.ts +113 -0
- package/packages/sdk/src/communicate/adapters/pi.ts +105 -0
- package/packages/sdk/src/communicate/core.ts +157 -0
- package/packages/sdk/src/communicate/index.ts +3 -0
- package/packages/sdk/src/communicate/transport.ts +489 -0
- package/packages/sdk/src/communicate/types.ts +106 -0
- package/packages/sdk/src/examples/workflows/fix-dashboard-user-registration.yaml +182 -0
- package/packages/sdk/src/workflows/builder.ts +97 -9
- package/packages/sdk/src/workflows/cli.ts +16 -1
- package/packages/sdk/src/workflows/runner.ts +110 -1
- package/packages/sdk/src/workflows/types.ts +14 -0
- package/packages/sdk/tsconfig.build.json +1 -7
- package/packages/sdk/tsconfig.json +1 -7
- package/packages/sdk-py/README.md +67 -25
- package/packages/sdk-py/examples/communicate/agno_example.py +8 -0
- package/packages/sdk-py/examples/communicate/claude_sdk_example.py +6 -0
- package/packages/sdk-py/examples/communicate/crewai_example.py +7 -0
- package/packages/sdk-py/examples/communicate/google_adk_example.py +7 -0
- package/packages/sdk-py/examples/communicate/openai_agents_example.py +8 -0
- package/packages/sdk-py/examples/communicate/swarms_example.py +7 -0
- package/packages/sdk-py/pyproject.toml +12 -1
- package/packages/sdk-py/src/agent_relay/__init__.py +8 -0
- package/packages/sdk-py/src/agent_relay/builder.py +65 -26
- package/packages/sdk-py/src/agent_relay/communicate/__init__.py +6 -0
- package/packages/sdk-py/src/agent_relay/communicate/a2a_bridge.py +138 -0
- package/packages/sdk-py/src/agent_relay/communicate/a2a_server.py +242 -0
- package/packages/sdk-py/src/agent_relay/communicate/a2a_transport.py +366 -0
- package/packages/sdk-py/src/agent_relay/communicate/a2a_types.py +294 -0
- package/packages/sdk-py/src/agent_relay/communicate/adapters/__init__.py +10 -0
- package/packages/sdk-py/src/agent_relay/communicate/adapters/agno.py +74 -0
- package/packages/sdk-py/src/agent_relay/communicate/adapters/claude_sdk.py +78 -0
- package/packages/sdk-py/src/agent_relay/communicate/adapters/crewai.py +143 -0
- package/packages/sdk-py/src/agent_relay/communicate/adapters/google_adk.py +69 -0
- package/packages/sdk-py/src/agent_relay/communicate/adapters/openai_agents.py +86 -0
- package/packages/sdk-py/src/agent_relay/communicate/adapters/pi.py +175 -0
- package/packages/sdk-py/src/agent_relay/communicate/adapters/swarms.py +44 -0
- package/packages/sdk-py/src/agent_relay/communicate/core.py +293 -0
- package/packages/sdk-py/src/agent_relay/communicate/transport.py +502 -0
- package/packages/sdk-py/src/agent_relay/communicate/types.py +89 -0
- package/packages/sdk-py/src/agent_relay/types.py +2 -1
- package/packages/sdk-py/tests/communicate/__init__.py +0 -0
- package/packages/sdk-py/tests/communicate/adapters/__init__.py +0 -0
- package/packages/sdk-py/tests/communicate/adapters/e2e_test_agno.py +154 -0
- package/packages/sdk-py/tests/communicate/adapters/e2e_test_claude_sdk.py +428 -0
- package/packages/sdk-py/tests/communicate/adapters/e2e_test_crewai.py +234 -0
- package/packages/sdk-py/tests/communicate/adapters/e2e_test_google_adk.py +182 -0
- package/packages/sdk-py/tests/communicate/adapters/e2e_test_langgraph.py +262 -0
- package/packages/sdk-py/tests/communicate/adapters/e2e_test_openai_agents.py +88 -0
- package/packages/sdk-py/tests/communicate/adapters/e2e_test_pi.py +156 -0
- package/packages/sdk-py/tests/communicate/adapters/e2e_test_swarms.py +239 -0
- package/packages/sdk-py/tests/communicate/adapters/test_agno.py +140 -0
- package/packages/sdk-py/tests/communicate/adapters/test_claude_sdk.py +147 -0
- package/packages/sdk-py/tests/communicate/adapters/test_crewai.py +136 -0
- package/packages/sdk-py/tests/communicate/adapters/test_google_adk.py +125 -0
- package/packages/sdk-py/tests/communicate/adapters/test_openai_agents.py +99 -0
- package/packages/sdk-py/tests/communicate/adapters/test_pi.py +270 -0
- package/packages/sdk-py/tests/communicate/adapters/test_swarms.py +113 -0
- package/packages/sdk-py/tests/communicate/conftest.py +555 -0
- package/packages/sdk-py/tests/communicate/integration/__init__.py +1 -0
- package/packages/sdk-py/tests/communicate/integration/test_cross_framework.py +331 -0
- package/packages/sdk-py/tests/communicate/integration/test_end_to_end.py +151 -0
- package/packages/sdk-py/tests/communicate/test_a2a_bridge.py +363 -0
- package/packages/sdk-py/tests/communicate/test_a2a_server.py +346 -0
- package/packages/sdk-py/tests/communicate/test_a2a_transport.py +561 -0
- package/packages/sdk-py/tests/communicate/test_a2a_types.py +342 -0
- package/packages/sdk-py/tests/communicate/test_auto_detect.py +67 -0
- package/packages/sdk-py/tests/communicate/test_core.py +331 -0
- package/packages/sdk-py/tests/communicate/test_transport.py +373 -0
- package/packages/sdk-py/tests/communicate/test_types.py +285 -0
- package/packages/sdk-py/tests/test_builder_deterministic.py +118 -0
- package/packages/telemetry/package.json +1 -1
- package/packages/trajectory/package.json +2 -2
- package/packages/user-directory/package.json +2 -2
- package/packages/utils/package.json +2 -2
- package/packages/sdk/dist/__tests__/completion-pipeline.test.d.ts +0 -14
- package/packages/sdk/dist/__tests__/completion-pipeline.test.d.ts.map +0 -1
- package/packages/sdk/dist/__tests__/completion-pipeline.test.js +0 -1476
- package/packages/sdk/dist/__tests__/completion-pipeline.test.js.map +0 -1
- package/packages/sdk/dist/__tests__/contract-fixtures.test.d.ts +0 -2
- package/packages/sdk/dist/__tests__/contract-fixtures.test.d.ts.map +0 -1
- package/packages/sdk/dist/__tests__/contract-fixtures.test.js +0 -152
- package/packages/sdk/dist/__tests__/contract-fixtures.test.js.map +0 -1
- package/packages/sdk/dist/__tests__/e2e-owner-review.test.d.ts +0 -16
- package/packages/sdk/dist/__tests__/e2e-owner-review.test.d.ts.map +0 -1
- package/packages/sdk/dist/__tests__/e2e-owner-review.test.js +0 -640
- package/packages/sdk/dist/__tests__/e2e-owner-review.test.js.map +0 -1
- package/packages/sdk/dist/__tests__/facade.test.d.ts +0 -2
- package/packages/sdk/dist/__tests__/facade.test.d.ts.map +0 -1
- package/packages/sdk/dist/__tests__/facade.test.js +0 -305
- package/packages/sdk/dist/__tests__/facade.test.js.map +0 -1
- package/packages/sdk/dist/__tests__/integration.test.d.ts +0 -2
- package/packages/sdk/dist/__tests__/integration.test.d.ts.map +0 -1
- package/packages/sdk/dist/__tests__/integration.test.js +0 -205
- package/packages/sdk/dist/__tests__/integration.test.js.map +0 -1
- package/packages/sdk/dist/__tests__/pty.test.d.ts +0 -2
- package/packages/sdk/dist/__tests__/pty.test.d.ts.map +0 -1
- package/packages/sdk/dist/__tests__/pty.test.js +0 -20
- package/packages/sdk/dist/__tests__/pty.test.js.map +0 -1
- package/packages/sdk/dist/__tests__/quickstart.test.d.ts +0 -2
- package/packages/sdk/dist/__tests__/quickstart.test.d.ts.map +0 -1
- package/packages/sdk/dist/__tests__/quickstart.test.js +0 -176
- package/packages/sdk/dist/__tests__/quickstart.test.js.map +0 -1
- package/packages/sdk/dist/__tests__/spawn-from-env.test.d.ts +0 -2
- package/packages/sdk/dist/__tests__/spawn-from-env.test.d.ts.map +0 -1
- package/packages/sdk/dist/__tests__/spawn-from-env.test.js +0 -222
- package/packages/sdk/dist/__tests__/spawn-from-env.test.js.map +0 -1
- package/packages/sdk/dist/__tests__/unit.test.d.ts +0 -2
- package/packages/sdk/dist/__tests__/unit.test.d.ts.map +0 -1
- package/packages/sdk/dist/__tests__/unit.test.js +0 -357
- package/packages/sdk/dist/__tests__/unit.test.js.map +0 -1
- package/packages/sdk-py/agent_relay/__init__.py +0 -21
- package/packages/sdk-py/agent_relay/models.py +0 -398
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
"""Wave 1.3 tests for the communicate Relay core."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import importlib
|
|
7
|
+
from inspect import isawaitable
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
from agent_relay.communicate.types import Message, RelayConfig
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _core_module():
|
|
15
|
+
return importlib.import_module("agent_relay.communicate.core")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
async def _wait_for(predicate, timeout: float = 1.0) -> None:
|
|
19
|
+
deadline = asyncio.get_running_loop().time() + timeout
|
|
20
|
+
while asyncio.get_running_loop().time() < deadline:
|
|
21
|
+
if predicate():
|
|
22
|
+
return
|
|
23
|
+
await asyncio.sleep(0.01)
|
|
24
|
+
raise AssertionError("Timed out waiting for async condition.")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@pytest.fixture
|
|
28
|
+
def core_harness(monkeypatch):
|
|
29
|
+
core = _core_module()
|
|
30
|
+
registered_atexit: list[object] = []
|
|
31
|
+
|
|
32
|
+
class FakeTransport:
|
|
33
|
+
instances: list["FakeTransport"] = []
|
|
34
|
+
|
|
35
|
+
def __init__(self, agent_name: str, config: RelayConfig) -> None:
|
|
36
|
+
self.agent_name = agent_name
|
|
37
|
+
self.config = config
|
|
38
|
+
self.connect_count = 0
|
|
39
|
+
self.disconnect_count = 0
|
|
40
|
+
self.send_dm_calls: list[tuple[str, str]] = []
|
|
41
|
+
self.post_message_calls: list[tuple[str, str]] = []
|
|
42
|
+
self.reply_calls: list[tuple[str, str]] = []
|
|
43
|
+
self.list_agents_calls = 0
|
|
44
|
+
self.list_agents_result = ["Review-Core", "Impl-Core"]
|
|
45
|
+
self.ws_callback = None
|
|
46
|
+
self.connected = False
|
|
47
|
+
FakeTransport.instances.append(self)
|
|
48
|
+
|
|
49
|
+
async def connect(self) -> None:
|
|
50
|
+
self.connect_count += 1
|
|
51
|
+
await asyncio.sleep(0)
|
|
52
|
+
self.connected = True
|
|
53
|
+
|
|
54
|
+
async def disconnect(self) -> None:
|
|
55
|
+
self.disconnect_count += 1
|
|
56
|
+
self.connected = False
|
|
57
|
+
|
|
58
|
+
async def send_dm(self, to: str, text: str) -> str:
|
|
59
|
+
self.send_dm_calls.append((to, text))
|
|
60
|
+
return "message-send"
|
|
61
|
+
|
|
62
|
+
async def post_message(self, channel: str, text: str) -> str:
|
|
63
|
+
self.post_message_calls.append((channel, text))
|
|
64
|
+
return "message-post"
|
|
65
|
+
|
|
66
|
+
async def reply(self, message_id: str, text: str) -> str:
|
|
67
|
+
self.reply_calls.append((message_id, text))
|
|
68
|
+
return "message-reply"
|
|
69
|
+
|
|
70
|
+
async def list_agents(self) -> list[str]:
|
|
71
|
+
self.list_agents_calls += 1
|
|
72
|
+
return list(self.list_agents_result)
|
|
73
|
+
|
|
74
|
+
def on_ws_message(self, callback) -> None:
|
|
75
|
+
self.ws_callback = callback
|
|
76
|
+
|
|
77
|
+
async def emit_message(self, message: Message) -> None:
|
|
78
|
+
if self.ws_callback is None:
|
|
79
|
+
raise AssertionError("Relay did not register a transport WS callback.")
|
|
80
|
+
result = self.ws_callback(message)
|
|
81
|
+
if isawaitable(result):
|
|
82
|
+
await result
|
|
83
|
+
|
|
84
|
+
monkeypatch.setattr(core, "RelayTransport", FakeTransport)
|
|
85
|
+
monkeypatch.setattr(core.atexit, "register", lambda callback: registered_atexit.append(callback))
|
|
86
|
+
|
|
87
|
+
return core, FakeTransport, registered_atexit
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def test_init_is_lazy_and_registers_atexit_cleanup(core_harness):
|
|
91
|
+
core, FakeTransport, registered_atexit = core_harness
|
|
92
|
+
|
|
93
|
+
relay = core.Relay("CoreTester", RelayConfig(auto_cleanup=True))
|
|
94
|
+
|
|
95
|
+
assert sum(transport.connect_count for transport in FakeTransport.instances) == 0
|
|
96
|
+
assert len(registered_atexit) == 1
|
|
97
|
+
assert getattr(registered_atexit[0], "__self__", None) is relay
|
|
98
|
+
assert getattr(registered_atexit[0], "__name__", None) == "close_sync"
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_init_skips_atexit_cleanup_when_disabled(core_harness):
|
|
102
|
+
core, _FakeTransport, registered_atexit = core_harness
|
|
103
|
+
|
|
104
|
+
core.Relay("CoreTester", RelayConfig(auto_cleanup=False))
|
|
105
|
+
|
|
106
|
+
assert registered_atexit == []
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@pytest.mark.asyncio
|
|
110
|
+
async def test_send_lazy_connects_and_delegates_to_transport(core_harness):
|
|
111
|
+
core, FakeTransport, _registered_atexit = core_harness
|
|
112
|
+
relay = core.Relay("CoreTester", RelayConfig(auto_cleanup=False))
|
|
113
|
+
transport = FakeTransport.instances[0]
|
|
114
|
+
|
|
115
|
+
result = await relay.send("Impl-Core", "hello")
|
|
116
|
+
|
|
117
|
+
assert result is None
|
|
118
|
+
assert transport.connect_count == 1
|
|
119
|
+
assert transport.send_dm_calls == [("Impl-Core", "hello")]
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@pytest.mark.asyncio
|
|
123
|
+
async def test_post_lazy_connects_and_delegates_to_transport(core_harness):
|
|
124
|
+
core, FakeTransport, _registered_atexit = core_harness
|
|
125
|
+
relay = core.Relay("CoreTester", RelayConfig(auto_cleanup=False))
|
|
126
|
+
transport = FakeTransport.instances[0]
|
|
127
|
+
|
|
128
|
+
result = await relay.post("core-py", "status update")
|
|
129
|
+
|
|
130
|
+
assert result is None
|
|
131
|
+
assert transport.connect_count == 1
|
|
132
|
+
assert transport.post_message_calls == [("core-py", "status update")]
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@pytest.mark.asyncio
|
|
136
|
+
async def test_reply_lazy_connects_and_delegates_to_transport(core_harness):
|
|
137
|
+
core, FakeTransport, _registered_atexit = core_harness
|
|
138
|
+
relay = core.Relay("CoreTester", RelayConfig(auto_cleanup=False))
|
|
139
|
+
transport = FakeTransport.instances[0]
|
|
140
|
+
|
|
141
|
+
result = await relay.reply("message-123", "thread response")
|
|
142
|
+
|
|
143
|
+
assert result is None
|
|
144
|
+
assert transport.connect_count == 1
|
|
145
|
+
assert transport.reply_calls == [("message-123", "thread response")]
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@pytest.mark.asyncio
|
|
149
|
+
async def test_inbox_drains_pending_buffer_and_does_not_poll_transport(core_harness):
|
|
150
|
+
core, FakeTransport, _registered_atexit = core_harness
|
|
151
|
+
relay = core.Relay("CoreTester", RelayConfig(auto_cleanup=False))
|
|
152
|
+
transport = FakeTransport.instances[0]
|
|
153
|
+
relay._pending.extend(
|
|
154
|
+
[
|
|
155
|
+
Message(sender="Review-Core", text="one", message_id="message-1"),
|
|
156
|
+
Message(sender="Impl-Core", text="two", message_id="message-2"),
|
|
157
|
+
]
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
first = await relay.inbox()
|
|
161
|
+
second = await relay.inbox()
|
|
162
|
+
|
|
163
|
+
assert first == [
|
|
164
|
+
Message(sender="Review-Core", text="one", message_id="message-1"),
|
|
165
|
+
Message(sender="Impl-Core", text="two", message_id="message-2"),
|
|
166
|
+
]
|
|
167
|
+
assert second == []
|
|
168
|
+
assert relay._pending == []
|
|
169
|
+
assert transport.connect_count == 1
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
@pytest.mark.asyncio
|
|
173
|
+
async def test_on_message_registers_callback_and_unsubscribe_restores_buffering(core_harness):
|
|
174
|
+
core, FakeTransport, _registered_atexit = core_harness
|
|
175
|
+
relay = core.Relay("CoreTester", RelayConfig(auto_cleanup=False))
|
|
176
|
+
transport = FakeTransport.instances[0]
|
|
177
|
+
|
|
178
|
+
received: list[Message] = []
|
|
179
|
+
unsubscribe = relay.on_message(lambda message: received.append(message))
|
|
180
|
+
|
|
181
|
+
await _wait_for(lambda: transport.connect_count == 1)
|
|
182
|
+
|
|
183
|
+
callback_message = Message(sender="Review-Core", text="callback", message_id="message-cb")
|
|
184
|
+
buffered_message = Message(sender="Impl-Core", text="buffered", message_id="message-buffer")
|
|
185
|
+
|
|
186
|
+
await transport.emit_message(callback_message)
|
|
187
|
+
unsubscribe()
|
|
188
|
+
await transport.emit_message(buffered_message)
|
|
189
|
+
inbox_messages = await relay.inbox()
|
|
190
|
+
|
|
191
|
+
assert received == [callback_message]
|
|
192
|
+
# "both" case: callback messages are also buffered per spec Section 5.3
|
|
193
|
+
assert inbox_messages == [callback_message, buffered_message]
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
@pytest.mark.asyncio
|
|
197
|
+
async def test_pending_buffer_caps_at_ten_thousand_and_drops_oldest_with_warning(core_harness):
|
|
198
|
+
core, FakeTransport, _registered_atexit = core_harness
|
|
199
|
+
relay = core.Relay("CoreTester", RelayConfig(auto_cleanup=False))
|
|
200
|
+
transport = FakeTransport.instances[0]
|
|
201
|
+
|
|
202
|
+
await relay.agents()
|
|
203
|
+
|
|
204
|
+
with pytest.warns(UserWarning, match="10,000|10000|buffer"):
|
|
205
|
+
for index in range(10_001):
|
|
206
|
+
await transport.emit_message(
|
|
207
|
+
Message(
|
|
208
|
+
sender="Review-Core",
|
|
209
|
+
text=f"message-{index}",
|
|
210
|
+
message_id=f"message-{index}",
|
|
211
|
+
)
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
messages = await relay.inbox()
|
|
215
|
+
|
|
216
|
+
assert len(messages) == 10_000
|
|
217
|
+
assert messages[0].message_id == "message-1"
|
|
218
|
+
assert messages[-1].message_id == "message-10000"
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
@pytest.mark.asyncio
|
|
222
|
+
async def test_agents_returns_transport_agent_list(core_harness):
|
|
223
|
+
core, FakeTransport, _registered_atexit = core_harness
|
|
224
|
+
relay = core.Relay("CoreTester", RelayConfig(auto_cleanup=False))
|
|
225
|
+
transport = FakeTransport.instances[0]
|
|
226
|
+
transport.list_agents_result = ["Review-Core", "Impl-Core", "CoreTester"]
|
|
227
|
+
|
|
228
|
+
agents = await relay.agents()
|
|
229
|
+
|
|
230
|
+
assert agents == ["Review-Core", "Impl-Core", "CoreTester"]
|
|
231
|
+
assert transport.connect_count == 1
|
|
232
|
+
assert transport.list_agents_calls == 1
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
@pytest.mark.asyncio
|
|
236
|
+
async def test_close_disconnects_transport(core_harness):
|
|
237
|
+
core, FakeTransport, _registered_atexit = core_harness
|
|
238
|
+
relay = core.Relay("CoreTester", RelayConfig(auto_cleanup=False))
|
|
239
|
+
transport = FakeTransport.instances[0]
|
|
240
|
+
|
|
241
|
+
await relay.send("Impl-Core", "hello")
|
|
242
|
+
await relay.close()
|
|
243
|
+
|
|
244
|
+
assert transport.disconnect_count == 1
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def test_sync_wrappers_delegate_to_async_methods(core_harness):
|
|
248
|
+
core, FakeTransport, _registered_atexit = core_harness
|
|
249
|
+
relay = core.Relay("CoreTester", RelayConfig(auto_cleanup=False))
|
|
250
|
+
transport = FakeTransport.instances[0]
|
|
251
|
+
relay._pending.append(Message(sender="Review-Core", text="sync inbox", message_id="message-sync"))
|
|
252
|
+
transport.list_agents_result = ["Review-Core", "Impl-Core"]
|
|
253
|
+
|
|
254
|
+
relay.send_sync("Impl-Core", "sync hello")
|
|
255
|
+
relay.post_sync("core-py", "sync update")
|
|
256
|
+
inbox_messages = relay.inbox_sync()
|
|
257
|
+
agents = relay.agents_sync()
|
|
258
|
+
relay.close_sync()
|
|
259
|
+
|
|
260
|
+
assert transport.send_dm_calls == [("Impl-Core", "sync hello")]
|
|
261
|
+
assert transport.post_message_calls == [("core-py", "sync update")]
|
|
262
|
+
assert inbox_messages == [Message(sender="Review-Core", text="sync inbox", message_id="message-sync")]
|
|
263
|
+
assert agents == ["Review-Core", "Impl-Core"]
|
|
264
|
+
assert transport.disconnect_count == 1
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
@pytest.mark.asyncio
|
|
268
|
+
async def test_async_context_manager_closes_transport_on_exit(core_harness):
|
|
269
|
+
core, FakeTransport, _registered_atexit = core_harness
|
|
270
|
+
|
|
271
|
+
async with core.Relay("CoreTester", RelayConfig(auto_cleanup=False)) as relay:
|
|
272
|
+
await relay.send("Impl-Core", "inside context")
|
|
273
|
+
transport = FakeTransport.instances[0]
|
|
274
|
+
assert transport.connect_count == 1
|
|
275
|
+
|
|
276
|
+
assert transport.disconnect_count == 1
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
@pytest.mark.asyncio
|
|
280
|
+
async def test_concurrent_inbox_calls_do_not_lose_messages(core_harness):
|
|
281
|
+
core, FakeTransport, _registered_atexit = core_harness
|
|
282
|
+
relay = core.Relay("CoreTester", RelayConfig(auto_cleanup=False))
|
|
283
|
+
relay._pending.extend(
|
|
284
|
+
[
|
|
285
|
+
Message(sender="one", text="first", message_id="message-1"),
|
|
286
|
+
Message(sender="two", text="second", message_id="message-2"),
|
|
287
|
+
Message(sender="three", text="third", message_id="message-3"),
|
|
288
|
+
]
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
results = await asyncio.gather(relay.inbox(), relay.inbox())
|
|
292
|
+
|
|
293
|
+
combined = [message.message_id for batch in results for message in batch]
|
|
294
|
+
assert sorted(combined) == ["message-1", "message-2", "message-3"]
|
|
295
|
+
assert relay._pending == []
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
@pytest.mark.asyncio
|
|
299
|
+
async def test_multiple_relay_instances_are_independent(core_harness):
|
|
300
|
+
core, FakeTransport, _registered_atexit = core_harness
|
|
301
|
+
first = core.Relay("FirstRelay", RelayConfig(auto_cleanup=False))
|
|
302
|
+
second = core.Relay("SecondRelay", RelayConfig(auto_cleanup=False))
|
|
303
|
+
first_transport, second_transport = FakeTransport.instances
|
|
304
|
+
|
|
305
|
+
await first.agents()
|
|
306
|
+
await second.agents()
|
|
307
|
+
|
|
308
|
+
await first_transport.emit_message(
|
|
309
|
+
Message(sender="Review-Core", text="first only", message_id="message-first")
|
|
310
|
+
)
|
|
311
|
+
await second_transport.emit_message(
|
|
312
|
+
Message(sender="Impl-Core", text="second only", message_id="message-second")
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
assert await first.inbox() == [
|
|
316
|
+
Message(sender="Review-Core", text="first only", message_id="message-first")
|
|
317
|
+
]
|
|
318
|
+
assert await second.inbox() == [
|
|
319
|
+
Message(sender="Impl-Core", text="second only", message_id="message-second")
|
|
320
|
+
]
|
|
321
|
+
assert first_transport is not second_transport
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def test_communicate_package_reexports_public_core_symbols():
|
|
325
|
+
core = _core_module()
|
|
326
|
+
communicate = importlib.reload(importlib.import_module("agent_relay.communicate"))
|
|
327
|
+
|
|
328
|
+
assert communicate.Relay is core.Relay
|
|
329
|
+
assert communicate.Message is Message
|
|
330
|
+
assert communicate.RelayConfig is RelayConfig
|
|
331
|
+
assert hasattr(communicate, "on_relay")
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
"""Tests for the RelayTransport HTTP/WS client against real Relaycast API surface."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import importlib
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
from agent_relay.communicate.types import (
|
|
11
|
+
Message,
|
|
12
|
+
RelayAuthError,
|
|
13
|
+
RelayConfigError,
|
|
14
|
+
RelayConnectionError,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
_ORIGINAL_ASYNCIO_SLEEP = asyncio.sleep
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _transport_module():
|
|
21
|
+
return importlib.import_module("agent_relay.communicate.transport")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _transport_class():
|
|
25
|
+
return _transport_module().RelayTransport
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
async def _wait_for(predicate, timeout: float = 1.0) -> None:
|
|
29
|
+
deadline = asyncio.get_running_loop().time() + timeout
|
|
30
|
+
while asyncio.get_running_loop().time() < deadline:
|
|
31
|
+
if predicate():
|
|
32
|
+
return
|
|
33
|
+
await _ORIGINAL_ASYNCIO_SLEEP(0.01)
|
|
34
|
+
|
|
35
|
+
raise AssertionError("Timed out waiting for async condition.")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@pytest.mark.asyncio
|
|
39
|
+
async def test_register_agent_and_unregister_agent_manage_identity(relay_server):
|
|
40
|
+
RelayTransport = _transport_class()
|
|
41
|
+
transport = RelayTransport("TransportTester", relay_server.make_config())
|
|
42
|
+
|
|
43
|
+
await transport.register_agent()
|
|
44
|
+
|
|
45
|
+
assert transport.agent_id in relay_server.registered_agents
|
|
46
|
+
assert transport.token == relay_server.registered_agents[transport.agent_id]["token"]
|
|
47
|
+
register_payload = relay_server.requests["register_agent"][-1]["json"]
|
|
48
|
+
assert register_payload["name"] == "TransportTester"
|
|
49
|
+
|
|
50
|
+
agent_id = transport.agent_id
|
|
51
|
+
await transport.unregister_agent()
|
|
52
|
+
|
|
53
|
+
assert relay_server.request_count("unregister_agent") == 1
|
|
54
|
+
assert agent_id not in relay_server.registered_agents
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@pytest.mark.asyncio
|
|
58
|
+
async def test_connect_and_disconnect_manage_registration_and_websocket(relay_server):
|
|
59
|
+
RelayTransport = _transport_class()
|
|
60
|
+
transport = RelayTransport("TransportTester", relay_server.make_config())
|
|
61
|
+
|
|
62
|
+
await transport.connect()
|
|
63
|
+
|
|
64
|
+
assert relay_server.request_count("register_agent") == 1
|
|
65
|
+
assert transport.agent_id is not None
|
|
66
|
+
await relay_server.wait_for_ws_connections(transport.agent_id, count=1)
|
|
67
|
+
assert relay_server.websocket_connected(transport.agent_id)
|
|
68
|
+
|
|
69
|
+
agent_id = transport.agent_id
|
|
70
|
+
await transport.disconnect()
|
|
71
|
+
|
|
72
|
+
assert relay_server.request_count("unregister_agent") == 1
|
|
73
|
+
assert agent_id not in relay_server.registered_agents
|
|
74
|
+
assert not relay_server.websocket_connected(agent_id)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@pytest.mark.asyncio
|
|
78
|
+
async def test_send_dm_posts_to_correct_endpoint(relay_server):
|
|
79
|
+
RelayTransport = _transport_class()
|
|
80
|
+
transport = RelayTransport("TransportTester", relay_server.make_config())
|
|
81
|
+
await transport.connect()
|
|
82
|
+
|
|
83
|
+
try:
|
|
84
|
+
message_id = await transport.send_dm("Review-Core", "hello")
|
|
85
|
+
finally:
|
|
86
|
+
await transport.disconnect()
|
|
87
|
+
|
|
88
|
+
assert message_id.startswith("message-")
|
|
89
|
+
dm_req = relay_server.requests["send_dm"][-1]
|
|
90
|
+
assert dm_req["json"]["to"] == "Review-Core"
|
|
91
|
+
assert dm_req["json"]["text"] == "hello"
|
|
92
|
+
assert dm_req["path"] == "/v1/dm"
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@pytest.mark.asyncio
|
|
96
|
+
async def test_post_message_posts_to_channel_endpoint(relay_server):
|
|
97
|
+
RelayTransport = _transport_class()
|
|
98
|
+
transport = RelayTransport("TransportTester", relay_server.make_config())
|
|
99
|
+
await transport.connect()
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
message_id = await transport.post_message("core-py", "status update")
|
|
103
|
+
finally:
|
|
104
|
+
await transport.disconnect()
|
|
105
|
+
|
|
106
|
+
assert message_id.startswith("message-")
|
|
107
|
+
ch_req = relay_server.requests["post_message"][-1]
|
|
108
|
+
assert ch_req["json"]["text"] == "status update"
|
|
109
|
+
assert "/v1/channels/core-py/messages" in ch_req["path"]
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@pytest.mark.asyncio
|
|
113
|
+
async def test_reply_posts_to_replies_endpoint(relay_server):
|
|
114
|
+
RelayTransport = _transport_class()
|
|
115
|
+
transport = RelayTransport("TransportTester", relay_server.make_config())
|
|
116
|
+
await transport.connect()
|
|
117
|
+
|
|
118
|
+
try:
|
|
119
|
+
message_id = await transport.reply("message-123", "thread reply")
|
|
120
|
+
finally:
|
|
121
|
+
await transport.disconnect()
|
|
122
|
+
|
|
123
|
+
assert message_id.startswith("message-")
|
|
124
|
+
reply_req = relay_server.requests["reply"][-1]
|
|
125
|
+
assert reply_req["json"]["text"] == "thread reply"
|
|
126
|
+
assert "/v1/messages/message-123/replies" in reply_req["path"]
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@pytest.mark.asyncio
|
|
130
|
+
async def test_check_inbox_returns_message_objects_and_drains_server_inbox(relay_server):
|
|
131
|
+
RelayTransport = _transport_class()
|
|
132
|
+
transport = RelayTransport("TransportTester", relay_server.make_config())
|
|
133
|
+
await transport.connect()
|
|
134
|
+
|
|
135
|
+
try:
|
|
136
|
+
queued = relay_server.queue_inbox_message(
|
|
137
|
+
transport.agent_id,
|
|
138
|
+
sender="Impl-Core",
|
|
139
|
+
text="transport ready",
|
|
140
|
+
channel="core-py",
|
|
141
|
+
thread_id="thread-1",
|
|
142
|
+
message_id="message-inbox-1",
|
|
143
|
+
timestamp=1710300000.5,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
messages = await transport.check_inbox()
|
|
147
|
+
empty = await transport.check_inbox()
|
|
148
|
+
finally:
|
|
149
|
+
await transport.disconnect()
|
|
150
|
+
|
|
151
|
+
assert len(messages) == 1
|
|
152
|
+
assert messages[0].sender == "Impl-Core"
|
|
153
|
+
assert messages[0].text == "transport ready"
|
|
154
|
+
assert messages[0].message_id == "message-inbox-1"
|
|
155
|
+
assert empty == []
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@pytest.mark.asyncio
|
|
159
|
+
async def test_list_agents_returns_online_agent_names(relay_server):
|
|
160
|
+
RelayTransport = _transport_class()
|
|
161
|
+
relay_server.add_agent("Review-Core")
|
|
162
|
+
transport = RelayTransport("TransportTester", relay_server.make_config())
|
|
163
|
+
await transport.connect()
|
|
164
|
+
|
|
165
|
+
try:
|
|
166
|
+
agents = await transport.list_agents()
|
|
167
|
+
finally:
|
|
168
|
+
await transport.disconnect()
|
|
169
|
+
|
|
170
|
+
assert set(agents) == {"Review-Core", "TransportTester"}
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
@pytest.mark.asyncio
|
|
174
|
+
async def test_websocket_messages_are_decoded_and_delivered_to_callback(relay_server):
|
|
175
|
+
RelayTransport = _transport_class()
|
|
176
|
+
transport = RelayTransport("TransportTester", relay_server.make_config())
|
|
177
|
+
|
|
178
|
+
received: list[Message] = []
|
|
179
|
+
delivered = asyncio.Event()
|
|
180
|
+
|
|
181
|
+
async def on_message(message: Message) -> None:
|
|
182
|
+
received.append(message)
|
|
183
|
+
delivered.set()
|
|
184
|
+
|
|
185
|
+
transport.on_ws_message(on_message)
|
|
186
|
+
await transport.connect()
|
|
187
|
+
|
|
188
|
+
try:
|
|
189
|
+
await relay_server.push_ws_message(
|
|
190
|
+
transport.agent_id,
|
|
191
|
+
sender="Review-Core",
|
|
192
|
+
text="looks good",
|
|
193
|
+
channel="core-py",
|
|
194
|
+
message_id="message-ws-1",
|
|
195
|
+
)
|
|
196
|
+
await asyncio.wait_for(delivered.wait(), timeout=1.0)
|
|
197
|
+
finally:
|
|
198
|
+
await transport.disconnect()
|
|
199
|
+
|
|
200
|
+
assert len(received) == 1
|
|
201
|
+
assert received[0].sender == "Review-Core"
|
|
202
|
+
assert received[0].text == "looks good"
|
|
203
|
+
assert received[0].channel == "core-py"
|
|
204
|
+
assert received[0].message_id == "message-ws-1"
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
@pytest.mark.asyncio
|
|
208
|
+
async def test_transport_reconnects_after_websocket_disconnect(relay_server, monkeypatch):
|
|
209
|
+
transport_module = _transport_module()
|
|
210
|
+
RelayTransport = transport_module.RelayTransport
|
|
211
|
+
sleep_calls: list[float] = []
|
|
212
|
+
|
|
213
|
+
async def fake_sleep(delay: float) -> None:
|
|
214
|
+
sleep_calls.append(delay)
|
|
215
|
+
await _ORIGINAL_ASYNCIO_SLEEP(0)
|
|
216
|
+
|
|
217
|
+
monkeypatch.setattr(transport_module.asyncio, "sleep", fake_sleep)
|
|
218
|
+
|
|
219
|
+
transport = RelayTransport("TransportTester", relay_server.make_config())
|
|
220
|
+
received: list[Message] = []
|
|
221
|
+
delivered = asyncio.Event()
|
|
222
|
+
|
|
223
|
+
def on_message(message: Message) -> None:
|
|
224
|
+
received.append(message)
|
|
225
|
+
delivered.set()
|
|
226
|
+
|
|
227
|
+
transport.on_ws_message(on_message)
|
|
228
|
+
await transport.connect()
|
|
229
|
+
|
|
230
|
+
try:
|
|
231
|
+
agent_id = transport.agent_id
|
|
232
|
+
await relay_server.close_ws(agent_id)
|
|
233
|
+
await relay_server.wait_for_ws_connections(agent_id, count=2)
|
|
234
|
+
|
|
235
|
+
await relay_server.push_ws_message(
|
|
236
|
+
agent_id,
|
|
237
|
+
sender="Impl-Core",
|
|
238
|
+
text="reconnected",
|
|
239
|
+
message_id="message-reconnect-1",
|
|
240
|
+
)
|
|
241
|
+
await asyncio.wait_for(delivered.wait(), timeout=1.0)
|
|
242
|
+
finally:
|
|
243
|
+
await transport.disconnect()
|
|
244
|
+
|
|
245
|
+
assert received[-1].sender == "Impl-Core"
|
|
246
|
+
assert received[-1].text == "reconnected"
|
|
247
|
+
assert [delay for delay in sleep_calls if delay >= 1][:1] == [1]
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
@pytest.mark.asyncio
|
|
251
|
+
@pytest.mark.parametrize(
|
|
252
|
+
("workspace", "api_key", "missing_name"),
|
|
253
|
+
[
|
|
254
|
+
("test-workspace", None, "RELAY_API_KEY"),
|
|
255
|
+
(None, "test-key", "RELAY_WORKSPACE"),
|
|
256
|
+
],
|
|
257
|
+
)
|
|
258
|
+
async def test_connect_requires_workspace_and_api_key(
|
|
259
|
+
relay_server,
|
|
260
|
+
monkeypatch,
|
|
261
|
+
workspace,
|
|
262
|
+
api_key,
|
|
263
|
+
missing_name,
|
|
264
|
+
):
|
|
265
|
+
RelayTransport = _transport_class()
|
|
266
|
+
monkeypatch.delenv("RELAY_WORKSPACE", raising=False)
|
|
267
|
+
monkeypatch.delenv("RELAY_API_KEY", raising=False)
|
|
268
|
+
|
|
269
|
+
transport = RelayTransport(
|
|
270
|
+
"TransportTester",
|
|
271
|
+
relay_server.make_config(workspace=workspace, api_key=api_key),
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
with pytest.raises(RelayConfigError, match=missing_name):
|
|
275
|
+
await transport.connect()
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
@pytest.mark.asyncio
|
|
279
|
+
async def test_register_agent_raises_relay_auth_error_on_401(relay_server):
|
|
280
|
+
RelayTransport = _transport_class()
|
|
281
|
+
relay_server.queue_http_error("register_agent", status=401, message="Unauthorized")
|
|
282
|
+
transport = RelayTransport("TransportTester", relay_server.make_config())
|
|
283
|
+
|
|
284
|
+
with pytest.raises(RelayAuthError, match="Unauthorized"):
|
|
285
|
+
await transport.register_agent()
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
@pytest.mark.asyncio
|
|
289
|
+
async def test_send_dm_raises_connection_error_on_client_error(relay_server):
|
|
290
|
+
RelayTransport = _transport_class()
|
|
291
|
+
transport = RelayTransport("TransportTester", relay_server.make_config())
|
|
292
|
+
await transport.connect()
|
|
293
|
+
|
|
294
|
+
try:
|
|
295
|
+
relay_server.queue_http_error("send_dm", status=404, message="Recipient not found")
|
|
296
|
+
|
|
297
|
+
with pytest.raises(RelayConnectionError, match="Recipient not found") as exc_info:
|
|
298
|
+
await transport.send_dm("Missing-Agent", "hello")
|
|
299
|
+
finally:
|
|
300
|
+
await transport.disconnect()
|
|
301
|
+
|
|
302
|
+
assert exc_info.value.status_code == 404
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
@pytest.mark.asyncio
|
|
306
|
+
async def test_send_dm_retries_transient_server_errors_before_succeeding(
|
|
307
|
+
relay_server,
|
|
308
|
+
monkeypatch,
|
|
309
|
+
):
|
|
310
|
+
transport_module = _transport_module()
|
|
311
|
+
RelayTransport = transport_module.RelayTransport
|
|
312
|
+
sleep_calls: list[float] = []
|
|
313
|
+
|
|
314
|
+
async def fake_sleep(delay: float) -> None:
|
|
315
|
+
sleep_calls.append(delay)
|
|
316
|
+
await _ORIGINAL_ASYNCIO_SLEEP(0)
|
|
317
|
+
|
|
318
|
+
monkeypatch.setattr(transport_module.asyncio, "sleep", fake_sleep)
|
|
319
|
+
|
|
320
|
+
transport = RelayTransport("TransportTester", relay_server.make_config())
|
|
321
|
+
await transport.connect()
|
|
322
|
+
|
|
323
|
+
try:
|
|
324
|
+
relay_server.queue_http_error(
|
|
325
|
+
"send_dm",
|
|
326
|
+
status=503,
|
|
327
|
+
message="Temporary failure",
|
|
328
|
+
repeat=2,
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
message_id = await transport.send_dm("Review-Core", "retry me")
|
|
332
|
+
finally:
|
|
333
|
+
await transport.disconnect()
|
|
334
|
+
|
|
335
|
+
assert message_id.startswith("message-")
|
|
336
|
+
assert relay_server.request_count("send_dm") == 3
|
|
337
|
+
assert [delay for delay in sleep_calls if delay >= 1][:2] == [1, 2]
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
@pytest.mark.asyncio
|
|
341
|
+
async def test_send_dm_raises_after_exhausting_server_error_retries(
|
|
342
|
+
relay_server,
|
|
343
|
+
monkeypatch,
|
|
344
|
+
):
|
|
345
|
+
transport_module = _transport_module()
|
|
346
|
+
RelayTransport = transport_module.RelayTransport
|
|
347
|
+
sleep_calls: list[float] = []
|
|
348
|
+
|
|
349
|
+
async def fake_sleep(delay: float) -> None:
|
|
350
|
+
sleep_calls.append(delay)
|
|
351
|
+
await _ORIGINAL_ASYNCIO_SLEEP(0)
|
|
352
|
+
|
|
353
|
+
monkeypatch.setattr(transport_module.asyncio, "sleep", fake_sleep)
|
|
354
|
+
|
|
355
|
+
transport = RelayTransport("TransportTester", relay_server.make_config())
|
|
356
|
+
await transport.connect()
|
|
357
|
+
|
|
358
|
+
try:
|
|
359
|
+
relay_server.queue_http_error(
|
|
360
|
+
"send_dm",
|
|
361
|
+
status=503,
|
|
362
|
+
message="Still failing",
|
|
363
|
+
repeat=3,
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
with pytest.raises(RelayConnectionError, match="Still failing") as exc_info:
|
|
367
|
+
await transport.send_dm("Review-Core", "retry me")
|
|
368
|
+
finally:
|
|
369
|
+
await transport.disconnect()
|
|
370
|
+
|
|
371
|
+
assert exc_info.value.status_code == 503
|
|
372
|
+
assert relay_server.request_count("send_dm") == 3
|
|
373
|
+
assert [delay for delay in sleep_calls if delay >= 1][:2] == [1, 2]
|