dojozero-client 0.2.1__tar.gz → 0.3.0__tar.gz
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.
- {dojozero_client-0.2.1 → dojozero_client-0.3.0}/.gitignore +7 -2
- {dojozero_client-0.2.1 → dojozero_client-0.3.0}/PKG-INFO +3 -3
- {dojozero_client-0.2.1 → dojozero_client-0.3.0}/README.md +1 -1
- {dojozero_client-0.2.1 → dojozero_client-0.3.0}/pyproject.toml +2 -2
- {dojozero_client-0.2.1 → dojozero_client-0.3.0}/src/dojozero_client/__init__.py +3 -1
- {dojozero_client-0.2.1 → dojozero_client-0.3.0}/src/dojozero_client/_client.py +22 -9
- {dojozero_client-0.2.1 → dojozero_client-0.3.0}/src/dojozero_client/_config.py +2 -2
- {dojozero_client-0.2.1 → dojozero_client-0.3.0}/src/dojozero_client/_daemon.py +72 -26
- {dojozero_client-0.2.1 → dojozero_client-0.3.0}/tests/test_client.py +3 -3
- {dojozero_client-0.2.1 → dojozero_client-0.3.0}/tests/test_daemon.py +103 -73
- {dojozero_client-0.2.1 → dojozero_client-0.3.0}/src/dojozero_client/_cli.py +0 -0
- {dojozero_client-0.2.1 → dojozero_client-0.3.0}/src/dojozero_client/_credentials.py +0 -0
- {dojozero_client-0.2.1 → dojozero_client-0.3.0}/src/dojozero_client/_exceptions.py +0 -0
- {dojozero_client-0.2.1 → dojozero_client-0.3.0}/src/dojozero_client/_rpc.py +0 -0
- {dojozero_client-0.2.1 → dojozero_client-0.3.0}/src/dojozero_client/_strategy.py +0 -0
- {dojozero_client-0.2.1 → dojozero_client-0.3.0}/src/dojozero_client/_transport.py +0 -0
- {dojozero_client-0.2.1 → dojozero_client-0.3.0}/tests/test_config.py +0 -0
- {dojozero_client-0.2.1 → dojozero_client-0.3.0}/tests/test_credentials.py +0 -0
- {dojozero_client-0.2.1 → dojozero_client-0.3.0}/tests/test_rpc.py +0 -0
|
@@ -158,7 +158,8 @@ uv.lock
|
|
|
158
158
|
Thumbs.db
|
|
159
159
|
|
|
160
160
|
# Sample scenario artifacts
|
|
161
|
-
dojozero-store
|
|
161
|
+
dojozero-store/
|
|
162
|
+
store/
|
|
162
163
|
|
|
163
164
|
# Data
|
|
164
165
|
/outputs/
|
|
@@ -174,4 +175,8 @@ package-lock.json
|
|
|
174
175
|
logs/
|
|
175
176
|
|
|
176
177
|
# node modules
|
|
177
|
-
node_modules/
|
|
178
|
+
node_modules/
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
agents/llms/*.yaml
|
|
182
|
+
!agents/llms/default.yaml
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dojozero-client
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary: Python SDK for external agents participating in DojoZero trials. Works with OpenClaw and
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Python SDK for external agents participating in DojoZero trials. Works with OpenClaw and QwenPaw.
|
|
5
5
|
Project-URL: Homepage, https://github.com/agentscope-ai/DojoZero
|
|
6
6
|
Project-URL: Repository, https://github.com/agentscope-ai/DojoZero
|
|
7
7
|
Project-URL: Issues, https://github.com/agentscope-ai/DojoZero/issues
|
|
@@ -149,7 +149,7 @@ asyncio.run(main())
|
|
|
149
149
|
|
|
150
150
|
## Documentation
|
|
151
151
|
|
|
152
|
-
For the full API reference, daemon mode, multiple agent profiles, and Agent Skill integration (OpenClaw /
|
|
152
|
+
For the full API reference, daemon mode, multiple agent profiles, and Agent Skill integration (OpenClaw / QwenPaw / AgentScope), see the [full documentation](https://github.com/agentscope-ai/DojoZero/tree/main/docs).
|
|
153
153
|
|
|
154
154
|
## License
|
|
155
155
|
|
|
@@ -127,7 +127,7 @@ asyncio.run(main())
|
|
|
127
127
|
|
|
128
128
|
## Documentation
|
|
129
129
|
|
|
130
|
-
For the full API reference, daemon mode, multiple agent profiles, and Agent Skill integration (OpenClaw /
|
|
130
|
+
For the full API reference, daemon mode, multiple agent profiles, and Agent Skill integration (OpenClaw / QwenPaw / AgentScope), see the [full documentation](https://github.com/agentscope-ai/DojoZero/tree/main/docs).
|
|
131
131
|
|
|
132
132
|
## License
|
|
133
133
|
|
|
@@ -4,8 +4,8 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "dojozero-client"
|
|
7
|
-
version = "0.
|
|
8
|
-
description = "Python SDK for external agents participating in DojoZero trials. Works with OpenClaw and
|
|
7
|
+
version = "0.3.0"
|
|
8
|
+
description = "Python SDK for external agents participating in DojoZero trials. Works with OpenClaw and QwenPaw."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
11
11
|
license = {text = "MIT"}
|
|
@@ -27,6 +27,7 @@ from dojozero_client._client import (
|
|
|
27
27
|
GatewayInfo,
|
|
28
28
|
Holding,
|
|
29
29
|
Odds,
|
|
30
|
+
PollResult,
|
|
30
31
|
TrialConnection,
|
|
31
32
|
TrialEndedEvent,
|
|
32
33
|
TrialMetadata,
|
|
@@ -60,7 +61,7 @@ from dojozero_client._exceptions import (
|
|
|
60
61
|
)
|
|
61
62
|
from dojozero_client._transport import GatewayTransport, SSEEvent
|
|
62
63
|
|
|
63
|
-
__version__ = "0.
|
|
64
|
+
__version__ = "0.3.0"
|
|
64
65
|
|
|
65
66
|
__all__ = [
|
|
66
67
|
# Main client
|
|
@@ -74,6 +75,7 @@ __all__ = [
|
|
|
74
75
|
"GatewayInfo",
|
|
75
76
|
"Holding",
|
|
76
77
|
"Odds",
|
|
78
|
+
"PollResult",
|
|
77
79
|
"TrialEndedEvent",
|
|
78
80
|
"TrialMetadata",
|
|
79
81
|
"TrialResults",
|
|
@@ -262,6 +262,19 @@ class TrialResults:
|
|
|
262
262
|
)
|
|
263
263
|
|
|
264
264
|
|
|
265
|
+
@dataclass
|
|
266
|
+
class PollResult:
|
|
267
|
+
"""Result of a poll_events call."""
|
|
268
|
+
|
|
269
|
+
events: list[EventEnvelope]
|
|
270
|
+
trial_ended: dict[str, Any] | None = None
|
|
271
|
+
|
|
272
|
+
@property
|
|
273
|
+
def is_trial_ended(self) -> bool:
|
|
274
|
+
"""Check if trial has ended."""
|
|
275
|
+
return self.trial_ended is not None
|
|
276
|
+
|
|
277
|
+
|
|
265
278
|
class TrialConnection:
|
|
266
279
|
"""Connection to a specific trial.
|
|
267
280
|
|
|
@@ -450,24 +463,20 @@ class TrialConnection:
|
|
|
450
463
|
async def poll_events(
|
|
451
464
|
self,
|
|
452
465
|
since: int | None = None,
|
|
453
|
-
limit: int =
|
|
454
|
-
|
|
455
|
-
) -> list[EventEnvelope]:
|
|
466
|
+
limit: int = 100,
|
|
467
|
+
) -> "PollResult":
|
|
456
468
|
"""Poll for recent events.
|
|
457
469
|
|
|
458
470
|
Args:
|
|
459
471
|
since: Get events after this sequence number
|
|
460
|
-
limit: Maximum events to return
|
|
461
|
-
event_types: Optional event type filter
|
|
472
|
+
limit: Maximum events to return (max 100)
|
|
462
473
|
|
|
463
474
|
Returns:
|
|
464
|
-
|
|
475
|
+
PollResult with events and trial_ended info
|
|
465
476
|
"""
|
|
466
477
|
params: dict[str, Any] = {"limit": limit}
|
|
467
478
|
if since is not None:
|
|
468
479
|
params["since"] = since
|
|
469
|
-
if event_types:
|
|
470
|
-
params["event_types"] = ",".join(event_types)
|
|
471
480
|
|
|
472
481
|
response = await self._transport.request(
|
|
473
482
|
"GET",
|
|
@@ -482,7 +491,10 @@ class TrialConnection:
|
|
|
482
491
|
if current_seq > self._last_sequence:
|
|
483
492
|
self._last_sequence = current_seq
|
|
484
493
|
|
|
485
|
-
|
|
494
|
+
# Check for trial ended
|
|
495
|
+
trial_ended = response.get("trialEnded")
|
|
496
|
+
|
|
497
|
+
return PollResult(events=events, trial_ended=trial_ended)
|
|
486
498
|
|
|
487
499
|
async def get_current_odds(self) -> Odds:
|
|
488
500
|
"""Get current betting odds.
|
|
@@ -880,6 +892,7 @@ __all__ = [
|
|
|
880
892
|
"EventEnvelope",
|
|
881
893
|
"GatewayInfo",
|
|
882
894
|
"Odds",
|
|
895
|
+
"PollResult",
|
|
883
896
|
"TrialConnection",
|
|
884
897
|
"TrialEndedEvent",
|
|
885
898
|
"TrialMetadata",
|
|
@@ -42,7 +42,7 @@ TRIALS_DIR = CONFIG_DIR / "trials"
|
|
|
42
42
|
# Default config template with comments
|
|
43
43
|
DEFAULT_CONFIG_TEMPLATE = """\
|
|
44
44
|
# DojoZero Client Configuration
|
|
45
|
-
# See: https://github.com/
|
|
45
|
+
# See: https://github.com/agentscope-ai/dojozero
|
|
46
46
|
|
|
47
47
|
# Dashboard server URL
|
|
48
48
|
# For local development: http://localhost:8000
|
|
@@ -130,7 +130,7 @@ def save_config(
|
|
|
130
130
|
# Write with comments
|
|
131
131
|
content = f"""\
|
|
132
132
|
# DojoZero Client Configuration
|
|
133
|
-
# See: https://github.com/
|
|
133
|
+
# See: https://github.com/agentscope-ai/dojozero
|
|
134
134
|
|
|
135
135
|
# Dashboard server URL
|
|
136
136
|
dashboard_url: {dashboard_url}
|
|
@@ -18,7 +18,7 @@ from datetime import datetime, timezone
|
|
|
18
18
|
from pathlib import Path
|
|
19
19
|
from typing import TYPE_CHECKING, Any
|
|
20
20
|
|
|
21
|
-
from dojozero_client._client import AgentResult, DojoClient, EventEnvelope
|
|
21
|
+
from dojozero_client._client import AgentResult, DojoClient, EventEnvelope, PollResult
|
|
22
22
|
from dojozero_client._config import (
|
|
23
23
|
CONFIG_DIR,
|
|
24
24
|
PID_FILE,
|
|
@@ -550,37 +550,64 @@ class TrialHandler:
|
|
|
550
550
|
return events
|
|
551
551
|
|
|
552
552
|
async def _event_loop(self) -> None:
|
|
553
|
-
"""Main event processing loop."""
|
|
553
|
+
"""Main event processing loop using polling."""
|
|
554
554
|
if not self._trial:
|
|
555
555
|
return
|
|
556
556
|
|
|
557
|
-
|
|
558
|
-
async for event in self._trial.events(
|
|
559
|
-
event_types=self.filters,
|
|
560
|
-
raise_on_trial_end=False,
|
|
561
|
-
):
|
|
562
|
-
if not self._running:
|
|
563
|
-
break
|
|
564
|
-
await self._handle_event(event)
|
|
557
|
+
poll_interval = float(os.environ.get("DOJOZERO_POLL_INTERVAL", "5.0"))
|
|
565
558
|
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
if ended.final_results:
|
|
577
|
-
_write_results(
|
|
578
|
-
self.state_dir / "results.json",
|
|
559
|
+
try:
|
|
560
|
+
while self._running:
|
|
561
|
+
try:
|
|
562
|
+
result: PollResult = await self._trial.poll_events(
|
|
563
|
+
since=self._state.last_event_sequence,
|
|
564
|
+
)
|
|
565
|
+
except Exception as e:
|
|
566
|
+
# 404 or connection error means gateway is gone
|
|
567
|
+
logger.info(
|
|
568
|
+
"Trial %s: Poll failed (%s), treating as trial ended",
|
|
579
569
|
self.trial_id,
|
|
580
|
-
|
|
581
|
-
ended.final_results,
|
|
570
|
+
e,
|
|
582
571
|
)
|
|
583
|
-
|
|
572
|
+
await self._handle_trial_ended("completed", [])
|
|
573
|
+
return
|
|
574
|
+
|
|
575
|
+
for event in result.events:
|
|
576
|
+
if not self._running:
|
|
577
|
+
return
|
|
578
|
+
await self._handle_event(event)
|
|
579
|
+
|
|
580
|
+
if result.is_trial_ended:
|
|
581
|
+
reason = (
|
|
582
|
+
result.trial_ended.get("reason", "completed")
|
|
583
|
+
if result.trial_ended
|
|
584
|
+
else "completed"
|
|
585
|
+
)
|
|
586
|
+
logger.info("Trial %s ended (reason=%s)", self.trial_id, reason)
|
|
587
|
+
# Fetch final results from server
|
|
588
|
+
try:
|
|
589
|
+
results_resp = await self._trial.get_results()
|
|
590
|
+
final_results = [
|
|
591
|
+
AgentResult(
|
|
592
|
+
agent_id=r.agent_id,
|
|
593
|
+
final_balance=r.final_balance,
|
|
594
|
+
net_profit=r.net_profit,
|
|
595
|
+
total_bets=r.total_bets,
|
|
596
|
+
win_rate=r.win_rate,
|
|
597
|
+
roi=r.roi,
|
|
598
|
+
)
|
|
599
|
+
for r in results_resp.results
|
|
600
|
+
]
|
|
601
|
+
except Exception:
|
|
602
|
+
final_results = []
|
|
603
|
+
await self._handle_trial_ended(reason, final_results)
|
|
604
|
+
return
|
|
605
|
+
|
|
606
|
+
# If we got a full page, there may be more — poll again
|
|
607
|
+
# immediately to catch up. Otherwise wait before next poll.
|
|
608
|
+
if len(result.events) < 100:
|
|
609
|
+
await asyncio.sleep(poll_interval)
|
|
610
|
+
|
|
584
611
|
except asyncio.CancelledError:
|
|
585
612
|
pass
|
|
586
613
|
except Exception as e:
|
|
@@ -588,6 +615,25 @@ class TrialHandler:
|
|
|
588
615
|
self._state.status = "error"
|
|
589
616
|
self._save_state()
|
|
590
617
|
|
|
618
|
+
async def _handle_trial_ended(
|
|
619
|
+
self, reason: str, final_results: list[AgentResult]
|
|
620
|
+
) -> None:
|
|
621
|
+
"""Handle trial ended — write results and update state."""
|
|
622
|
+
self._state.status = reason
|
|
623
|
+
if final_results:
|
|
624
|
+
_write_results(
|
|
625
|
+
self.state_dir / "results.json",
|
|
626
|
+
self.trial_id,
|
|
627
|
+
reason,
|
|
628
|
+
final_results,
|
|
629
|
+
)
|
|
630
|
+
# Update balance from our own settled result
|
|
631
|
+
for r in final_results:
|
|
632
|
+
if r.agent_id == self.agent_id:
|
|
633
|
+
self._state.balance = r.final_balance
|
|
634
|
+
break
|
|
635
|
+
self._save_state()
|
|
636
|
+
|
|
591
637
|
async def _handle_event(self, event: EventEnvelope) -> None:
|
|
592
638
|
"""Process an incoming event."""
|
|
593
639
|
# Skip events already seen (replayed on reconnect)
|
|
@@ -210,11 +210,11 @@ class TestReconnection:
|
|
|
210
210
|
"""Test extracting agent_id from error message."""
|
|
211
211
|
import re
|
|
212
212
|
|
|
213
|
-
# Test format: "Agent
|
|
214
|
-
error_msg = "Agent
|
|
213
|
+
# Test format: "Agent qwenpaw-agent already connected"
|
|
214
|
+
error_msg = "Agent qwenpaw-agent already connected"
|
|
215
215
|
match = re.search(r"Agent (\S+) already", error_msg)
|
|
216
216
|
assert match is not None
|
|
217
|
-
assert match.group(1) == "
|
|
217
|
+
assert match.group(1) == "qwenpaw-agent"
|
|
218
218
|
|
|
219
219
|
def test_extract_agent_id_from_json_error(self):
|
|
220
220
|
"""Test extracting agent_id from JSON error."""
|
|
@@ -9,7 +9,12 @@ from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
9
9
|
|
|
10
10
|
import pytest
|
|
11
11
|
|
|
12
|
-
from dojozero_client._client import
|
|
12
|
+
from dojozero_client._client import (
|
|
13
|
+
AgentResult,
|
|
14
|
+
EventEnvelope,
|
|
15
|
+
PollResult,
|
|
16
|
+
TrialResults,
|
|
17
|
+
)
|
|
13
18
|
from dojozero_client._daemon import (
|
|
14
19
|
DaemonState,
|
|
15
20
|
TrialHandler,
|
|
@@ -273,24 +278,8 @@ def _make_agent_result(**overrides: object) -> AgentResult:
|
|
|
273
278
|
return AgentResult(**defaults) # type: ignore[arg-type]
|
|
274
279
|
|
|
275
280
|
|
|
276
|
-
def _make_trial_ended(
|
|
277
|
-
reason: str = "completed",
|
|
278
|
-
results: list[AgentResult] | None = None,
|
|
279
|
-
) -> TrialEndedEvent:
|
|
280
|
-
"""Create a TrialEndedEvent with sensible defaults."""
|
|
281
|
-
if results is None:
|
|
282
|
-
results = [_make_agent_result()]
|
|
283
|
-
return TrialEndedEvent(
|
|
284
|
-
trial_id="test-trial",
|
|
285
|
-
reason=reason,
|
|
286
|
-
timestamp=datetime.now(timezone.utc),
|
|
287
|
-
final_results=results,
|
|
288
|
-
message=f"Trial has {reason}",
|
|
289
|
-
)
|
|
290
|
-
|
|
291
|
-
|
|
292
281
|
class TestTrialHandlerTrialEnd:
|
|
293
|
-
"""Tests for TrialHandler handling trial_ended
|
|
282
|
+
"""Tests for TrialHandler handling trial_ended via polling."""
|
|
294
283
|
|
|
295
284
|
@pytest.mark.asyncio
|
|
296
285
|
async def test_event_loop_handles_completed_trial(self):
|
|
@@ -306,21 +295,40 @@ class TestTrialHandlerTrialEnd:
|
|
|
306
295
|
handler._state.status = "connected"
|
|
307
296
|
handler.state_dir.mkdir(parents=True, exist_ok=True)
|
|
308
297
|
|
|
309
|
-
# Mock trial that yields one event then ends
|
|
310
298
|
mock_trial = AsyncMock()
|
|
311
|
-
|
|
299
|
+
call_count = 0
|
|
300
|
+
|
|
301
|
+
async def mock_poll(**kwargs):
|
|
302
|
+
nonlocal call_count
|
|
303
|
+
call_count += 1
|
|
304
|
+
if call_count == 1:
|
|
305
|
+
# First poll: return an event
|
|
306
|
+
return PollResult(
|
|
307
|
+
events=[
|
|
308
|
+
EventEnvelope(
|
|
309
|
+
trial_id="test-trial",
|
|
310
|
+
sequence=1,
|
|
311
|
+
timestamp=datetime.now(timezone.utc),
|
|
312
|
+
payload={"event_type": "event.odds_update"},
|
|
313
|
+
)
|
|
314
|
+
],
|
|
315
|
+
)
|
|
316
|
+
# Second poll: trial ended
|
|
317
|
+
return PollResult(
|
|
318
|
+
events=[],
|
|
319
|
+
trial_ended={"reason": "completed", "message": ""},
|
|
320
|
+
)
|
|
312
321
|
|
|
313
|
-
|
|
314
|
-
|
|
322
|
+
agent_result = _make_agent_result()
|
|
323
|
+
mock_trial.poll_events = mock_poll
|
|
324
|
+
mock_trial.get_results = AsyncMock(
|
|
325
|
+
return_value=TrialResults(
|
|
315
326
|
trial_id="test-trial",
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
327
|
+
status="completed",
|
|
328
|
+
results=[agent_result],
|
|
329
|
+
ended_at=datetime.now(timezone.utc),
|
|
319
330
|
)
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
mock_trial.events = mock_events
|
|
323
|
-
mock_trial.trial_ended = ended
|
|
331
|
+
)
|
|
324
332
|
handler._trial = mock_trial
|
|
325
333
|
handler._running = True
|
|
326
334
|
|
|
@@ -353,14 +361,20 @@ class TestTrialHandlerTrialEnd:
|
|
|
353
361
|
handler.state_dir.mkdir(parents=True, exist_ok=True)
|
|
354
362
|
|
|
355
363
|
mock_trial = AsyncMock()
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
mock_trial.
|
|
363
|
-
|
|
364
|
+
mock_trial.poll_events = AsyncMock(
|
|
365
|
+
return_value=PollResult(
|
|
366
|
+
events=[],
|
|
367
|
+
trial_ended={"reason": "cancelled", "message": ""},
|
|
368
|
+
)
|
|
369
|
+
)
|
|
370
|
+
mock_trial.get_results = AsyncMock(
|
|
371
|
+
return_value=TrialResults(
|
|
372
|
+
trial_id="test-trial",
|
|
373
|
+
status="cancelled",
|
|
374
|
+
results=[],
|
|
375
|
+
ended_at=None,
|
|
376
|
+
)
|
|
377
|
+
)
|
|
364
378
|
handler._trial = mock_trial
|
|
365
379
|
handler._running = True
|
|
366
380
|
|
|
@@ -383,14 +397,20 @@ class TestTrialHandlerTrialEnd:
|
|
|
383
397
|
handler.state_dir.mkdir(parents=True, exist_ok=True)
|
|
384
398
|
|
|
385
399
|
mock_trial = AsyncMock()
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
mock_trial.
|
|
393
|
-
|
|
400
|
+
mock_trial.poll_events = AsyncMock(
|
|
401
|
+
return_value=PollResult(
|
|
402
|
+
events=[],
|
|
403
|
+
trial_ended={"reason": "failed", "message": ""},
|
|
404
|
+
)
|
|
405
|
+
)
|
|
406
|
+
mock_trial.get_results = AsyncMock(
|
|
407
|
+
return_value=TrialResults(
|
|
408
|
+
trial_id="test-trial",
|
|
409
|
+
status="failed",
|
|
410
|
+
results=[],
|
|
411
|
+
ended_at=None,
|
|
412
|
+
)
|
|
413
|
+
)
|
|
394
414
|
handler._trial = mock_trial
|
|
395
415
|
handler._running = True
|
|
396
416
|
|
|
@@ -401,8 +421,8 @@ class TestTrialHandlerTrialEnd:
|
|
|
401
421
|
assert not (handler.state_dir / "results.json").exists()
|
|
402
422
|
|
|
403
423
|
@pytest.mark.asyncio
|
|
404
|
-
async def
|
|
405
|
-
"""Test
|
|
424
|
+
async def test_event_loop_gateway_404_treats_as_ended(self):
|
|
425
|
+
"""Test poll failure (e.g. gateway 404) is treated as trial ended."""
|
|
406
426
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
407
427
|
with patch("dojozero_client._daemon.TRIALS_DIR", Path(tmpdir)):
|
|
408
428
|
client = MagicMock()
|
|
@@ -415,25 +435,19 @@ class TestTrialHandlerTrialEnd:
|
|
|
415
435
|
handler.state_dir.mkdir(parents=True, exist_ok=True)
|
|
416
436
|
|
|
417
437
|
mock_trial = AsyncMock()
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
yield
|
|
422
|
-
|
|
423
|
-
mock_trial.events = mock_events
|
|
424
|
-
mock_trial.trial_ended = None # No trial_ended event
|
|
438
|
+
mock_trial.poll_events = AsyncMock(
|
|
439
|
+
side_effect=Exception("404 Not Found")
|
|
440
|
+
)
|
|
425
441
|
handler._trial = mock_trial
|
|
426
442
|
handler._running = True
|
|
427
443
|
|
|
428
444
|
await handler._event_loop()
|
|
429
445
|
|
|
430
|
-
|
|
431
|
-
assert handler._state.status == "connected"
|
|
432
|
-
assert not (handler.state_dir / "results.json").exists()
|
|
446
|
+
assert handler._state.status == "completed"
|
|
433
447
|
|
|
434
448
|
@pytest.mark.asyncio
|
|
435
449
|
async def test_event_loop_real_error_sets_error_status(self):
|
|
436
|
-
"""Test
|
|
450
|
+
"""Test unhandled exceptions in event processing set status=error."""
|
|
437
451
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
438
452
|
with patch("dojozero_client._daemon.TRIALS_DIR", Path(tmpdir)):
|
|
439
453
|
client = MagicMock()
|
|
@@ -446,16 +460,26 @@ class TestTrialHandlerTrialEnd:
|
|
|
446
460
|
handler.state_dir.mkdir(parents=True, exist_ok=True)
|
|
447
461
|
|
|
448
462
|
mock_trial = AsyncMock()
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
463
|
+
mock_trial.poll_events = AsyncMock(
|
|
464
|
+
return_value=PollResult(
|
|
465
|
+
events=[
|
|
466
|
+
EventEnvelope(
|
|
467
|
+
trial_id="test-trial",
|
|
468
|
+
sequence=1,
|
|
469
|
+
timestamp=datetime.now(timezone.utc),
|
|
470
|
+
payload={"event_type": "event.test"},
|
|
471
|
+
)
|
|
472
|
+
],
|
|
473
|
+
)
|
|
474
|
+
)
|
|
456
475
|
handler._trial = mock_trial
|
|
457
476
|
handler._running = True
|
|
458
477
|
|
|
478
|
+
# Make _handle_event raise to trigger error path
|
|
479
|
+
handler._handle_event = AsyncMock(
|
|
480
|
+
side_effect=RuntimeError("Unexpected crash")
|
|
481
|
+
)
|
|
482
|
+
|
|
459
483
|
await handler._event_loop()
|
|
460
484
|
|
|
461
485
|
assert handler._state.status == "error"
|
|
@@ -483,16 +507,22 @@ class TestTrialHandlerTrialEnd:
|
|
|
483
507
|
roi=-0.2,
|
|
484
508
|
),
|
|
485
509
|
]
|
|
486
|
-
ended = _make_trial_ended("completed", results=results)
|
|
487
510
|
|
|
488
511
|
mock_trial = AsyncMock()
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
mock_trial.
|
|
512
|
+
mock_trial.poll_events = AsyncMock(
|
|
513
|
+
return_value=PollResult(
|
|
514
|
+
events=[],
|
|
515
|
+
trial_ended={"reason": "completed", "message": ""},
|
|
516
|
+
)
|
|
517
|
+
)
|
|
518
|
+
mock_trial.get_results = AsyncMock(
|
|
519
|
+
return_value=TrialResults(
|
|
520
|
+
trial_id="test-trial",
|
|
521
|
+
status="completed",
|
|
522
|
+
results=results,
|
|
523
|
+
ended_at=datetime.now(timezone.utc),
|
|
524
|
+
)
|
|
525
|
+
)
|
|
496
526
|
handler._trial = mock_trial
|
|
497
527
|
handler._running = True
|
|
498
528
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|