feat(fase1): GoalContract + GoalConfig standalone fail-fast loader (F1)

This commit is contained in:
Kjell Tore Guttormsen 2026-07-07 08:01:53 +02:00
commit c6f62d41db
2 changed files with 92 additions and 1 deletions

View file

@ -9,7 +9,13 @@ import pytest
from pydantic import ValidationError
from spikes._harness import FakeChatClient
from portfolio_optimiser.contracts import Contracts, load_contracts
from portfolio_optimiser.contracts import (
Contracts,
GoalConfig,
GoalContract,
load_contracts,
load_goal_config,
)
_DS = {"docs_dir": "docs", "top_k": 3}
_TERM = {"max_rounds": 3, "max_tokens": 10000}
@ -51,3 +57,49 @@ def test_no_chat_client_call_on_malformed_contract() -> None:
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
# --- Step 7: configurable goal contract + standalone fail-fast loader ----------------------------
def test_goal_contract_hard_and_soft_validate() -> None:
hard = GoalContract(absolute_ore=500000)
assert hard.mode == "hard" # default mode
soft = GoalContract(percent=25.0, mode="soft")
assert soft.mode == "soft"
assert soft.percent == 25.0
def test_goal_contract_requires_a_target() -> None:
"""Neither absolute_ore nor percent -> ValidationError (a goal with no target is meaningless)."""
with pytest.raises(ValidationError):
GoalContract()
def test_goal_config_roundtrips_portfolio_and_per_project() -> None:
cfg = GoalConfig(
portfolio=GoalContract(absolute_ore=1_000_000),
per_project={"P1": GoalContract(percent=10.0, mode="soft")},
)
assert cfg.portfolio is not None and cfg.portfolio.absolute_ore == 1_000_000
assert cfg.per_project["P1"].mode == "soft"
# dict round-trip via model_validate preserves both levels
reparsed = GoalConfig.model_validate(cfg.model_dump())
assert reparsed.per_project["P1"].percent == 10.0
def test_load_goal_config_fail_fast_on_missing_file(tmp_path) -> None:
with pytest.raises(FileNotFoundError):
load_goal_config(str(tmp_path / "nope.json"))
def test_load_goal_config_parses_valid_file(tmp_path) -> None:
p = tmp_path / "goals.json"
p.write_text(
'{"portfolio": {"absolute_ore": 750000, "mode": "hard"},'
' "per_project": {"P2": {"percent": 5.0}}}',
encoding="utf-8",
)
cfg = load_goal_config(str(p))
assert cfg.portfolio is not None and cfg.portfolio.absolute_ore == 750000
assert cfg.per_project["P2"].percent == 5.0