- A1: fire the hard filter (Apify is precise); embedding vibe-ranker (embed.py) blends with the floor-free white-box (rank.py) to sift ~90 -> top 18 - A2: Opus curator (curate.py) reads the 18 -> honest <=count + Gemini-voiced report cards; graceful fallback to the white-box + templated cards - LLM via opencode.ai/zen gateway (llm.py): Opus chat + direct-OpenAI embeddings - Run LLM work off the event loop (asyncio.to_thread) so the Redis response publishes - C1: dedicated Postgres (app/db/) persists the per-user feed; get_scout_feed replays it so matches survive navigation/refresh - match contract + report-card fields (schema.py); skills.py; tests/
51 lines
2.4 KiB
Python
51 lines
2.4 KiB
Python
"""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
|