main
py 122 lines 3.25 KB
Raw
1 import asyncio
2 import importlib
3 import sys
4 import types
5 from pathlib import Path
6 from types import SimpleNamespace
7
8 import pytest
9
10 PROJECT_ROOT = Path(__file__).resolve().parents[1]
11 if str(PROJECT_ROOT) not in sys.path:
12 sys.path.insert(0, str(PROJECT_ROOT))
13
14 agent_stub = types.ModuleType("agent")
15 agent_stub.LoopData = object
16 original_agent_module = sys.modules.get("agent")
17 sys.modules["agent"] = agent_stub
18
19 try:
20 retry_module = importlib.import_module(
21 "plugins._error_retry.extensions.python._functions.agent.Agent."
22 "handle_exception.end._80_retry_critical_exception"
23 )
24 counter_module = importlib.import_module(
25 "plugins._error_retry.extensions.python._functions.agent.Agent."
26 "monologue.start._10_reset_critical_exception_counter"
27 )
28 finally:
29 if original_agent_module is None:
30 sys.modules.pop("agent", None)
31 else:
32 sys.modules["agent"] = original_agent_module
33
34 DATA_NAME_COUNTER = counter_module.DATA_NAME_COUNTER
35
36
37 class FakeLog:
38 def __init__(self):
39 self.entries = []
40
41 def log(self, **entry):
42 self.entries.append(entry)
43
44
45 class FakeAgent:
46 def __init__(self, counter=0):
47 self._data = {DATA_NAME_COUNTER: counter}
48 self.context = SimpleNamespace(log=FakeLog())
49 self.history = SimpleNamespace(remove_all_embeds=lambda: 0)
50 self.interventions = 0
51 self.warnings = []
52
53 def get_data(self, key):
54 return self._data.get(key)
55
56 def set_data(self, key, value):
57 self._data[key] = value
58
59 async def handle_intervention(self):
60 self.interventions += 1
61
62 def read_prompt(self, prompt, **kwargs):
63 return f"{prompt}: {kwargs['error_message']}"
64
65 def hist_add_warning(self, **warning):
66 self.warnings.append(warning)
67
68
69 async def _no_sleep(_delay):
70 return None
71
72
73 def _set_retry_config(monkeypatch, retries):
74 monkeypatch.setattr(
75 retry_module.plugins,
76 "get_plugin_config",
77 lambda *args, **kwargs: {"retries": retries},
78 )
79 monkeypatch.setattr(retry_module.asyncio, "sleep", _no_sleep)
80
81
82 @pytest.mark.parametrize(
83 ("value", "expected"),
84 [
85 (None, 1),
86 ("", 1),
87 (False, 1),
88 ("3", 3),
89 (2.9, 2),
90 (-4, 0),
91 ],
92 )
93 def test_normalize_max_retries(value, expected):
94 assert retry_module.normalize_max_retries(value) == expected
95
96
97 def test_error_retry_uses_configured_retry_limit(monkeypatch):
98 _set_retry_config(monkeypatch, 2)
99 agent = FakeAgent(counter=1)
100 data = {"exception": RuntimeError("boom")}
101
102 asyncio.run(retry_module.RetryCriticalException(agent=agent).execute(data))
103
104 assert agent.get_data(DATA_NAME_COUNTER) == 2
105 assert data["exception"] is None
106 assert agent.interventions == 1
107 assert len(agent.context.log.entries) == 1
108 assert len(agent.warnings) == 1
109
110
111 def test_zero_configured_retries_disables_retry(monkeypatch):
112 _set_retry_config(monkeypatch, 0)
113 agent = FakeAgent(counter=0)
114 exception = RuntimeError("boom")
115 data = {"exception": exception}
116
117 asyncio.run(retry_module.RetryCriticalException(agent=agent).execute(data))
118
119 assert agent.get_data(DATA_NAME_COUNTER) == 0
120 assert data["exception"] is exception
121 assert agent.interventions == 0
122 assert agent.context.log.entries == []