Files
llm-connect/tests/test_async.py
Bernd Worsch d71f4114d1 feat: WP-0001 foundation + WP-0002 core extensions
WP-0001 — Foundation & GAAF Baseline
- SCOPE.md, ARCHITECTURE-LAYERS.md, contracts/ tree
- .claude/rules/ stubs filled (architecture, stack, boundary)
- 57 tests (pytest), pyproject.toml with ruff+mypy, CI workflow

WP-0002 — Core Extensions (FR-4 + FR-3)
- FR-4: BudgetTracker (thread-safe) + LLMBudgetExceededError +
  optional RunConfig.budget_tracker + enforcement in all adapters
- FR-3: async_execute_prompt on LLMAdapter ABC (asyncio.to_thread
  fallback) + native asyncio.create_subprocess_exec in ClaudeCodeAdapter

81 tests passing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-01 22:24:14 +00:00

102 lines
3.5 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Tests for async_execute_prompt (FR-3).
"""
import asyncio
import pytest
from llm_connect.models import RunConfig, BudgetTracker
from llm_connect.adapter import MockLLMAdapter
from llm_connect.exceptions import LLMBudgetExceededError
class TestAsyncExecutePrompt:
def test_default_fallback_returns_response(self):
adapter = MockLLMAdapter(mock_response="async result")
config = RunConfig()
response = asyncio.run(adapter.async_execute_prompt("hello", config))
assert response.content == "async result"
def test_gather_multiple_adapters(self):
"""asyncio.gather over N adapters completes without errors."""
adapters = [MockLLMAdapter(mock_response=f"resp-{i}") for i in range(4)]
config = RunConfig()
async def run():
return await asyncio.gather(*[
a.async_execute_prompt("prompt", config) for a in adapters
])
results = asyncio.run(run())
assert len(results) == 4
for i, r in enumerate(results):
assert r.content == f"resp-{i}"
def test_gather_increments_call_counts(self):
adapter = MockLLMAdapter()
config = RunConfig()
async def run():
await asyncio.gather(*[
adapter.async_execute_prompt("p", config) for _ in range(5)
])
asyncio.run(run())
assert adapter.call_count == 5
def test_concurrent_faster_than_sequential(self):
"""Gathering N async calls should not be N× slower than one call."""
import time
adapter = MockLLMAdapter()
config = RunConfig()
async def run_concurrent(n: int):
await asyncio.gather(*[
adapter.async_execute_prompt("p", config) for _ in range(n)
])
# Just verify it completes without deadlock or error — timing is CI-unreliable
asyncio.run(run_concurrent(10))
assert adapter.call_count == 10
def test_async_with_budget_tracker(self):
"""Budget enforcement works through async calls."""
tracker = BudgetTracker(total=10000)
config = RunConfig(budget_tracker=tracker)
adapter = MockLLMAdapter(mock_response="hi")
asyncio.run(adapter.async_execute_prompt("hello", config))
assert tracker.spent > 0
def test_async_exhausted_budget_raises(self):
"""Exhausted budget raises LLMBudgetExceededError in async context."""
tracker = BudgetTracker(total=1)
tracker.consume(1)
config = RunConfig(budget_tracker=tracker)
adapter = MockLLMAdapter()
with pytest.raises(LLMBudgetExceededError):
asyncio.run(adapter.async_execute_prompt("p", config))
def test_async_gather_with_shared_budget(self):
"""Shared budget across concurrent async calls is enforced correctly."""
tracker = BudgetTracker(total=100000)
config = RunConfig(budget_tracker=tracker)
adapters = [MockLLMAdapter(mock_response="hi") for _ in range(4)]
async def run():
await asyncio.gather(*[
a.async_execute_prompt("hello", config) for a in adapters
])
asyncio.run(run())
assert tracker.spent > 0
def test_returns_llm_response_type(self):
from llm_connect.models import LLMResponse
adapter = MockLLMAdapter()
config = RunConfig()
response = asyncio.run(adapter.async_execute_prompt("q", config))
assert isinstance(response, LLMResponse)