generated from coulomb/repo-seed
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>
This commit is contained in:
101
tests/test_async.py
Normal file
101
tests/test_async.py
Normal file
@@ -0,0 +1,101 @@
|
||||
"""
|
||||
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)
|
||||
Reference in New Issue
Block a user