"""Phase 0 — the `match` contract test. The engine's `match` block must (a) validate against the Pydantic model and (b) serialize to EXACTLY the keys the frontend `scoutJobToMatchRole` reads (scout/lib/jobMatching.ts → `JobMatch`). If either drifts, backend ⇄ frontend have silently diverged — this test fails. """ import pytest from pydantic import ValidationError from app.engine.schema import Growth, MatchDim, MatchResult # The exact field sets the frontend reads. Keep in lockstep with `JobMatch` / `MatchDim`. FRONTEND_MATCH_KEYS = {"score", "fit", "archetype", "one_line", "reason", "breakdown", "growth", "coach_note", "proofReady", "factors"} FRONTEND_DIM_KEYS = {"name", "score", "level", "note"} FRONTEND_GROWTH_KEYS = {"text", "from", "to"} def _golden() -> MatchResult: return MatchResult( score=88, fit="stretch", reason="Matches 5/7 skills · Senior fit", breakdown=[MatchDim(name="Product Management", score=92, level="Strong"), MatchDim(name="SQL", score=48, level="Light")], growth=Growth(text="Add an SQL proof point", **{"from": 88, "to": 95}), proofReady=True, factors={"skill": 0.71, "experience": 0.9, "salary": None}, ) def test_match_block_keys_align_with_frontend(): d = _golden().as_dict() assert set(d) == FRONTEND_MATCH_KEYS, f"match keys drifted: {set(d) ^ FRONTEND_MATCH_KEYS}" assert set(d["breakdown"][0]) == FRONTEND_DIM_KEYS assert set(d["growth"]) == FRONTEND_GROWTH_KEYS # the `from` keyword must serialize under its alias, not `from_` assert d["growth"]["from"] == 88 and d["growth"]["to"] == 95 def test_constrained_enums_reject_bad_values(): with pytest.raises(ValidationError): MatchResult(score=1, fit="maybe", reason="x") # fit ∉ {fit, stretch} with pytest.raises(ValidationError): MatchDim(name="x", score=1, level="Great") # level ∉ {Strong, Solid, Light} def test_minimal_match_is_valid_and_floor_free_shaped(): d = MatchResult(score=50, fit="fit", reason="ok").as_dict() assert d["growth"] is None and d["breakdown"] == [] and d["factors"] == {} # factors may carry None (an *absent* factor) — that's the coverage-aware contract, not a floor d2 = MatchResult(score=50, fit="fit", reason="ok", factors={"salary": None}).as_dict() assert d2["factors"]["salary"] is None