portfolio-optimiser/tests/test_contracts.py

50 lines
1.9 KiB
Python

"""Step 11 tests — fail-fast contract loaders (malformed config raises at startup, no LLM).
Every malformed contract raises a ValidationError at load — and, critically, no chat-client
is ever touched (the validation is pure config-layer, ordered before any backend exists).
Pattern: tests/test_backends.py (raises) + FakeChatClient.call_count spy.
"""
import pytest
from pydantic import ValidationError
from spikes._harness import FakeChatClient
from portfolio_optimiser.contracts import Contracts, load_contracts
_DS = {"docs_dir": "docs", "top_k": 3}
_TERM = {"max_rounds": 3, "max_tokens": 10000}
_FB = {"decision": "approved", "rationale": "feasible within range"}
def test_valid_contracts_load() -> None:
contracts = load_contracts(_DS, _TERM, _FB)
assert isinstance(contracts, Contracts)
assert contracts.model_map.local["default"] # bundled model_map validated
def test_malformed_data_source_raises() -> None:
with pytest.raises(ValidationError):
load_contracts({"docs_dir": "docs", "top_k": 0}, _TERM, _FB) # top_k must be > 0
def test_malformed_model_map_raises() -> None:
bad_map = {"local": {"proposer": "qwen3:4b"}, "azure": {"default": "x"}} # local missing 'default'
with pytest.raises(ValidationError):
load_contracts(_DS, _TERM, _FB, model_map=bad_map)
def test_malformed_termination_raises() -> None:
with pytest.raises(ValidationError):
load_contracts(_DS, {"max_rounds": 0, "max_tokens": 10000}, _FB) # unbounded
def test_malformed_feedback_raises() -> None:
with pytest.raises(ValidationError):
load_contracts(_DS, _TERM, {"decision": "maybe", "rationale": "x"}) # not a literal
def test_no_chat_client_call_on_malformed_contract() -> None:
spy = FakeChatClient()
with pytest.raises(ValidationError):
load_contracts({"docs_dir": "docs", "top_k": -1}, _TERM, _FB)
assert spy.call_count == 0 # contract validation never constructs or calls a client