mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-10-30 04:15:45 +08:00
241 lines
7.5 KiB
Python
241 lines
7.5 KiB
Python
"""Regression tests for local run controls, using genuine SDK contexts."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
|
|
import pytest
|
|
|
|
from components.runner.default import RunDeadline, RunInterruptChecker
|
|
from tests.test_runner import make_context
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_slow_ledger_does_not_delay_chunks_or_deadline():
|
|
calls = 0
|
|
|
|
class SlowLedger:
|
|
async def run_get(self, run_id):
|
|
nonlocal calls
|
|
calls += 1
|
|
await asyncio.sleep(10)
|
|
|
|
context = make_context()
|
|
context.context.available_apis.run_get = True
|
|
async with RunInterruptChecker(SlowLedger(), context) as checker:
|
|
for _ in range(200):
|
|
assert await checker.wait_for(asyncio.sleep(0, result="chunk"), deadline=RunDeadline(0.1)) == "chunk"
|
|
assert calls == 1
|
|
with pytest.raises(asyncio.TimeoutError):
|
|
await checker.wait_for(asyncio.sleep(10), deadline=RunDeadline(0.02))
|
|
assert checker._watcher is None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_operation_timeout_is_not_mistaken_for_poll_timeout():
|
|
checker = RunInterruptChecker(None, make_context())
|
|
operation_error = asyncio.TimeoutError("operation itself timed out")
|
|
|
|
async def operation():
|
|
raise operation_error
|
|
|
|
with pytest.raises(asyncio.TimeoutError) as caught:
|
|
await checker.wait_for(operation(), deadline=RunDeadline(0.02))
|
|
|
|
assert caught.value is operation_error
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_operation_timeout_propagates_without_deadline_when_polling():
|
|
class RunLedgerFixture:
|
|
async def run_get(self, run_id):
|
|
# Give the enclosing test timeout a chance to stop a regression loop.
|
|
await asyncio.sleep(0)
|
|
return {"run_id": run_id, "status": "running"}
|
|
|
|
context = make_context()
|
|
context.context.available_apis.run_get = True
|
|
checker = RunInterruptChecker(RunLedgerFixture(), context)
|
|
operation_error = asyncio.TimeoutError("operation itself timed out")
|
|
|
|
async def operation():
|
|
raise operation_error
|
|
|
|
with pytest.raises(asyncio.TimeoutError) as caught:
|
|
await asyncio.wait_for(checker.wait_for(operation()), timeout=0.25)
|
|
|
|
assert caught.value is operation_error
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_watcher_shares_boundary_observation_but_next_boundary_is_fresh():
|
|
started = asyncio.Event()
|
|
release = asyncio.Event()
|
|
calls = 0
|
|
|
|
class Ledger:
|
|
async def run_get(self, run_id):
|
|
nonlocal calls
|
|
calls += 1
|
|
started.set()
|
|
await release.wait()
|
|
return {"status": "cancelled" if calls > 1 else "running"}
|
|
|
|
context = make_context()
|
|
context.context.available_apis.run_get = True
|
|
checker = RunInterruptChecker(Ledger(), context)
|
|
first = asyncio.create_task(checker.is_cancelled(force=True))
|
|
await started.wait()
|
|
second = asyncio.create_task(checker.is_cancelled(force=True, coalesce=True))
|
|
await asyncio.sleep(0)
|
|
release.set()
|
|
assert await asyncio.gather(first, second) == [False, False]
|
|
assert calls == 1
|
|
assert await checker.is_cancelled(force=True)
|
|
assert calls == 2
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_watcher_shares_initial_boundary_and_still_cancels_slow_operation():
|
|
from components.runner.default import RunCancelledError
|
|
|
|
calls = 0
|
|
stopped = asyncio.Event()
|
|
|
|
class Ledger:
|
|
async def run_get(self, run_id):
|
|
nonlocal calls
|
|
calls += 1
|
|
return {"status": "cancelled" if calls > 1 else "running"}
|
|
|
|
async def operation():
|
|
try:
|
|
await asyncio.Event().wait()
|
|
finally:
|
|
stopped.set()
|
|
|
|
context = make_context()
|
|
context.context.available_apis.run_get = True
|
|
checker = RunInterruptChecker(Ledger(), context, interval_seconds=0.1)
|
|
async with checker:
|
|
assert not await checker.is_cancelled(force=True)
|
|
await asyncio.sleep(0)
|
|
assert calls == 1
|
|
with pytest.raises(RunCancelledError):
|
|
await asyncio.wait_for(checker.wait_for(operation()), timeout=0.5)
|
|
assert calls == 2
|
|
assert stopped.is_set()
|
|
assert checker._watcher is None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_external_cancel_closes_inflight_ledger_request():
|
|
started = asyncio.Event()
|
|
stopped = asyncio.Event()
|
|
|
|
class Ledger:
|
|
async def run_get(self, run_id):
|
|
started.set()
|
|
try:
|
|
await asyncio.Event().wait()
|
|
finally:
|
|
stopped.set()
|
|
|
|
context = make_context()
|
|
context.context.available_apis.run_get = True
|
|
checker = RunInterruptChecker(Ledger(), context)
|
|
poll = asyncio.create_task(checker.is_cancelled(force=True))
|
|
await started.wait()
|
|
poll.cancel()
|
|
with pytest.raises(asyncio.CancelledError):
|
|
await poll
|
|
assert stopped.is_set()
|
|
assert not checker._check_lock.locked()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_forced_boundary_refreshes_without_waiting_for_stale_watcher():
|
|
started = asyncio.Event()
|
|
release = asyncio.Event()
|
|
cancelled = False
|
|
calls = 0
|
|
|
|
class Ledger:
|
|
async def run_get(self, run_id):
|
|
nonlocal calls
|
|
calls += 1
|
|
snapshot = {"status": "cancelled" if cancelled else "running"}
|
|
if calls == 1:
|
|
started.set()
|
|
await release.wait()
|
|
return snapshot
|
|
|
|
context = make_context()
|
|
context.context.available_apis.run_get = True
|
|
checker = RunInterruptChecker(Ledger(), context)
|
|
watcher = asyncio.create_task(checker.is_cancelled(force=True, coalesce=True))
|
|
try:
|
|
await started.wait()
|
|
cancelled = True
|
|
assert await asyncio.wait_for(checker.is_cancelled(force=True), timeout=0.2)
|
|
assert not watcher.done()
|
|
assert calls == 2
|
|
finally:
|
|
release.set()
|
|
await watcher
|
|
# A late running snapshot cannot clear the observed cancellation.
|
|
assert await checker.is_cancelled()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_background_and_boundary_polls_never_exceed_two_inflight_requests():
|
|
inflight = 0
|
|
peak = 0
|
|
|
|
class Ledger:
|
|
async def run_get(self, run_id):
|
|
nonlocal inflight, peak
|
|
inflight += 1
|
|
peak = max(peak, inflight)
|
|
try:
|
|
await asyncio.sleep(0.01)
|
|
return {"status": "running"}
|
|
finally:
|
|
inflight -= 1
|
|
|
|
context = make_context()
|
|
context.context.available_apis.run_get = True
|
|
checker = RunInterruptChecker(Ledger(), context)
|
|
await asyncio.gather(*(checker.is_cancelled(force=True, coalesce=bool(index % 2)) for index in range(20)))
|
|
assert peak <= 2
|
|
assert inflight == 0
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_watcher_retries_when_overlapping_boundary_failed():
|
|
started = asyncio.Event()
|
|
release = asyncio.Event()
|
|
calls = 0
|
|
|
|
class Ledger:
|
|
async def run_get(self, run_id):
|
|
nonlocal calls
|
|
calls += 1
|
|
if calls == 1:
|
|
started.set()
|
|
await release.wait()
|
|
raise OSError("ledger unavailable")
|
|
return {"status": "cancelled"}
|
|
|
|
context = make_context()
|
|
context.context.available_apis.run_get = True
|
|
checker = RunInterruptChecker(Ledger(), context)
|
|
boundary = asyncio.create_task(checker.is_cancelled(force=True))
|
|
await started.wait()
|
|
watcher = asyncio.create_task(checker.is_cancelled(force=True, coalesce=True))
|
|
await asyncio.sleep(0)
|
|
release.set()
|
|
assert await watcher
|
|
await boundary
|
|
assert calls == 2
|