diff --git a/app/db/repo.py b/app/db/repo.py index d9b30d7..4be34a9 100644 --- a/app/db/repo.py +++ b/app/db/repo.py @@ -17,14 +17,35 @@ logger = logging.getLogger(__name__) # fields a caller may set on an opportunity's state (status + the generated document ids + activity flags) _STATE_FIELDS = ("status", "tailored_resume_id", "tailored_version_id", "cover_letter_id", "seen", "viewed", "saved") -# the search-defining pref fields — same values → same signature → cursor advances (new jobs); -# change any → new signature → cursor resets to page 1. -_SIG_FIELDS = ("title", "role", "location", "industry", "experience", "years", "work_mode", "remote") +def _effective_title(prefs: dict) -> str: + """Match the board adapter's title precedence for cache identity.""" + target_titles = prefs.get("targetTitles") or [] + if target_titles and str(target_titles[0]).strip(): + return str(target_titles[0]).strip() + title = str(prefs.get("title") or "").strip() + if title: + return title + for role in prefs.get("role") or []: + if role and role != "Open to any": + return str(role) + return "" def search_signature(prefs: dict) -> str: """Stable short hash of the search-defining prefs (ignores transient flags / user_context).""" - payload = {k: prefs.get(k) for k in _SIG_FIELDS} + # Canonicalize the fields exactly as the board adapters consume them. The + # dashboard contract is camelCase; work_mode remains a legacy fallback. + work_mode = prefs.get("workMode") if "workMode" in prefs else prefs.get("work_mode") + payload = { + "effective_title": _effective_title(prefs), + "role": prefs.get("role"), + "location": prefs.get("location"), + "industry": prefs.get("industry"), + "experience": prefs.get("experience"), + "years": prefs.get("years"), + "workMode": work_mode, + "remote": prefs.get("remote"), + } blob = json.dumps(payload, sort_keys=True, default=str) return hashlib.sha1(blob.encode()).hexdigest()[:16] diff --git a/docker-compose.staging.yml b/docker-compose.staging.yml index 355f8ef..3975d9e 100644 --- a/docker-compose.staging.yml +++ b/docker-compose.staging.yml @@ -35,7 +35,7 @@ services: condition: service_healthy redis: condition: service_started - command: uvicorn app.main:app --host 0.0.0.0 --port 8000 + command: sh -c "alembic upgrade head && exec uvicorn app.main:app --host 0.0.0.0 --port 8000" restart: unless-stopped postgres: diff --git a/docker-compose.yml b/docker-compose.yml index b15bf86..e809011 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,6 +10,7 @@ services: environment: # inside the compose network the DB host is `postgres` (overrides the host-dev .env value) DATABASE_URL: postgresql+asyncpg://matchmaking:matchmaking@postgres:5432/matchmaking + command: sh -c "alembic upgrade head && exec uvicorn app.main:app --host 0.0.0.0 --port 8000" depends_on: postgres: condition: service_healthy diff --git a/tests/test_pool.py b/tests/test_pool.py index 121607f..30f6d21 100644 --- a/tests/test_pool.py +++ b/tests/test_pool.py @@ -1,5 +1,6 @@ """Warm-pool gate — the reorder/floor decision that keeps the shelf stocked without over-fetching.""" from app.engine.search import pool_decision +from app.db.repo import search_signature FLOOR, REORDER = 90, 100 # the configured thresholds (POOL_SAFETY_FLOOR / POOL_REORDER_AT) @@ -39,3 +40,29 @@ def test_floor_never_above_reorder_invariant(): from app.config import get_settings s = get_settings() assert s.POOL_SAFETY_FLOOR <= s.POOL_REORDER_AT + + +def test_target_title_changes_search_signature(): + base = { + "title": "Product Manager", + "role": ["Product"], + "targetTitles": ["Backend Engineer"], + "location": ["Bangalore · India"], + "workMode": ["Hybrid"], + } + changed = {**base, "targetTitles": ["Data Analyst"]} + + assert search_signature(base) != search_signature(changed) + + +def test_work_mode_changes_search_signature_using_dashboard_contract(): + base = {"title": "Data Analyst", "location": ["Bangalore · India"], "workMode": ["Hybrid"]} + changed = {**base, "workMode": ["Remote"]} + + assert search_signature(base) != search_signature(changed) + + +def test_legacy_and_camel_case_work_mode_have_same_signature(): + common = {"title": "Data Analyst", "location": ["Bangalore · India"]} + + assert search_signature({**common, "workMode": ["Hybrid"]}) == search_signature({**common, "work_mode": ["Hybrid"]})