refactor: migrate exec bridge to anyio and harden cancellation (#6)

This commit is contained in:
banteg
2025-12-31 01:51:46 +04:00
committed by GitHub
parent 6687a435c9
commit 8eda3f5e84
9 changed files with 492 additions and 310 deletions
+68 -11
View File
@@ -1,11 +1,13 @@
import asyncio
import anyio
import pytest
from takopi.exec_bridge import CodexExecRunner
from takopi.exec_bridge import CodexExecRunner, EventCallback
def test_run_serialized_serializes_same_session() -> None:
@pytest.mark.anyio
async def test_run_serialized_serializes_same_session() -> None:
runner = CodexExecRunner(codex_cmd="codex", extra_args=[])
gate = asyncio.Event()
gate = anyio.Event()
in_flight = 0
max_in_flight = 0
@@ -19,13 +21,68 @@ def test_run_serialized_serializes_same_session() -> None:
runner.run = run_stub # type: ignore[assignment]
async def run_test() -> None:
t1 = asyncio.create_task(runner.run_serialized("a", "sid"))
t2 = asyncio.create_task(runner.run_serialized("b", "sid"))
await asyncio.sleep(0)
async with anyio.create_task_group() as tg:
tg.start_soon(runner.run_serialized, "a", "sid")
tg.start_soon(runner.run_serialized, "b", "sid")
await anyio.sleep(0)
gate.set()
await asyncio.gather(t1, t2)
asyncio.run(run_test())
assert max_in_flight == 1
@pytest.mark.anyio
async def test_run_serialized_allows_parallel_new_sessions() -> None:
runner = CodexExecRunner(codex_cmd="codex", extra_args=[])
gate = anyio.Event()
in_flight = 0
max_in_flight = 0
async def run_stub(*_args, **_kwargs):
nonlocal in_flight, max_in_flight
in_flight += 1
max_in_flight = max(max_in_flight, in_flight)
await gate.wait()
in_flight -= 1
return ("sid", "ok", True)
runner.run = run_stub # type: ignore[assignment]
async with anyio.create_task_group() as tg:
tg.start_soon(runner.run_serialized, "a", None)
tg.start_soon(runner.run_serialized, "b", None)
with anyio.move_on_after(1):
while max_in_flight < 2:
await anyio.sleep(0)
gate.set()
assert max_in_flight == 2
@pytest.mark.anyio
async def test_new_session_holds_lock_for_resumes() -> None:
runner = CodexExecRunner(codex_cmd="codex", extra_args=[])
finish = anyio.Event()
resume_started = anyio.Event()
async def run_stub(
_prompt: str,
session_id: str | None,
on_event: EventCallback | None = None,
) -> tuple[str, str, bool]:
if session_id is None:
if on_event:
await on_event({"type": "thread.started", "thread_id": "sid"})
await finish.wait()
return ("sid", "ok", True)
resume_started.set()
return ("sid", "ok", True)
runner.run = run_stub # type: ignore[assignment]
async with anyio.create_task_group() as tg:
tg.start_soon(runner.run_serialized, "first", None)
await anyio.sleep(0)
tg.start_soon(runner.run_serialized, "resume", "sid")
await anyio.sleep(0)
assert not resume_started.is_set()
finish.set()