|
12 | 12 | from mcp.types import CallToolRequestParams, TextContent |
13 | 13 |
|
14 | 14 | from fast_agent.agents.agent_types import AgentConfig |
| 15 | +from fast_agent.cli.runtime import runner |
15 | 16 | from fast_agent.cli.runtime.agent_setup import ( |
16 | 17 | _apply_shell_cwd_policy_preflight, |
17 | 18 | _build_fan_out_result_paths, |
@@ -130,6 +131,69 @@ def test_should_convert_keyboard_interrupt_to_task_cancel_only_for_interactive_r |
130 | 131 | assert _should_convert_keyboard_interrupt_to_task_cancel(one_shot_request) is False |
131 | 132 |
|
132 | 133 |
|
| 134 | +def test_run_request_closes_loop_when_cleanup_is_interrupted( |
| 135 | + monkeypatch: pytest.MonkeyPatch, |
| 136 | +) -> None: |
| 137 | + class _FakeTask: |
| 138 | + def __init__(self) -> None: |
| 139 | + self.cancelled = False |
| 140 | + |
| 141 | + def done(self) -> bool: |
| 142 | + return False |
| 143 | + |
| 144 | + def cancel(self) -> None: |
| 145 | + self.cancelled = True |
| 146 | + |
| 147 | + class _FakeLoop: |
| 148 | + def __init__(self) -> None: |
| 149 | + self.task = _FakeTask() |
| 150 | + self.main_gather = object() |
| 151 | + self.shutdown_marker = object() |
| 152 | + self.closed = False |
| 153 | + |
| 154 | + def is_running(self) -> bool: |
| 155 | + return False |
| 156 | + |
| 157 | + def create_task(self, coro: Any) -> _FakeTask: |
| 158 | + coro.close() |
| 159 | + return self.task |
| 160 | + |
| 161 | + def run_until_complete(self, awaitable: object) -> None: |
| 162 | + if awaitable is self.task: |
| 163 | + raise KeyboardInterrupt() |
| 164 | + if awaitable is self.main_gather: |
| 165 | + raise KeyboardInterrupt() |
| 166 | + |
| 167 | + def shutdown_asyncgens(self) -> object: |
| 168 | + return self.shutdown_marker |
| 169 | + |
| 170 | + def close(self) -> None: |
| 171 | + self.closed = True |
| 172 | + |
| 173 | + fake_loop = _FakeLoop() |
| 174 | + |
| 175 | + async def _fake_run_agent_request(_request: AgentRunRequest) -> None: |
| 176 | + return None |
| 177 | + |
| 178 | + def _fake_gather(*aws: object, return_exceptions: bool = False) -> object: |
| 179 | + del return_exceptions |
| 180 | + if aws == (fake_loop.task,): |
| 181 | + return fake_loop.main_gather |
| 182 | + return object() |
| 183 | + |
| 184 | + monkeypatch.setattr(runner, "configure_uvloop", lambda: (False, False)) |
| 185 | + monkeypatch.setattr(runner, "ensure_event_loop", lambda: fake_loop) |
| 186 | + monkeypatch.setattr(runner, "set_asyncio_exception_handler", lambda _loop: None) |
| 187 | + monkeypatch.setattr(runner, "run_agent_request", _fake_run_agent_request) |
| 188 | + monkeypatch.setattr(asyncio, "all_tasks", lambda _loop: set()) |
| 189 | + monkeypatch.setattr(asyncio, "gather", _fake_gather) |
| 190 | + |
| 191 | + with pytest.raises(KeyboardInterrupt): |
| 192 | + runner.run_request(_make_request(result_file=None, message="hello")) |
| 193 | + |
| 194 | + assert fake_loop.closed is True |
| 195 | + |
| 196 | + |
133 | 197 | def test_sanitize_result_suffix() -> None: |
134 | 198 | assert _sanitize_result_suffix("openai/gpt-4o") == "openai_gpt-4o" |
135 | 199 | assert _sanitize_result_suffix(" model name ") == "model_name" |
|
0 commit comments