import datetime as dt from projektmatch import rules TODAY = dt.date(2026, 7, 9) def geo_munich(place): return (48.137, 11.575) # ~40 km from Sauerlach def geo_hamburg(place): return (53.551, 9.994) # ~600 km def geo_fail(place): raise OSError("network down") def misc(**kw): base = {"miscType": "other", "startDate": None, "workloadPercent": None, "remotePercent": None, "onsiteLocation": None} base.update(kw) return base def test_security_is_always_no(): assert rules.eval_misc(misc(miscType="security"), TODAY) == rules.NO def test_start_within_8_weeks_yes_after_no_missing_unknown(): assert rules.eval_misc(misc(miscType="start", startDate="2026-08-01"), TODAY) == rules.YES assert rules.eval_misc(misc(miscType="start", startDate="2026-12-01"), TODAY) == rules.NO assert rules.eval_misc(misc(miscType="start"), TODAY) == rules.UNKNOWN assert rules.eval_misc(misc(miscType="start", startDate="asap"), TODAY) == rules.UNKNOWN def test_workload(): assert rules.eval_misc(misc(miscType="workload", workloadPercent=100), TODAY) == rules.YES assert rules.eval_misc(misc(miscType="workload", workloadPercent=50), TODAY) == rules.UNKNOWN assert rules.eval_misc(misc(miscType="workload"), TODAY) == rules.UNKNOWN def test_duration_always_yes(): assert rules.eval_misc(misc(miscType="duration"), TODAY) == rules.YES def test_location_rules(): full_remote = misc(miscType="location", remotePercent=100) assert rules.eval_misc(full_remote, TODAY) == rules.YES hybrid_near = misc(miscType="location", remotePercent=60, onsiteLocation="München") assert rules.eval_misc(hybrid_near, TODAY, geocode_fn=geo_munich) == rules.YES hybrid_far = misc(miscType="location", remotePercent=60, onsiteLocation="Hamburg") assert rules.eval_misc(hybrid_far, TODAY, geocode_fn=geo_hamburg) == rules.NO unclear = misc(miscType="location") assert rules.eval_misc(unclear, TODAY) == rules.NO geo_broken = misc(miscType="location", remotePercent=40, onsiteLocation="Xyzstadt") assert rules.eval_misc(geo_broken, TODAY, geocode_fn=geo_fail) == rules.UNKNOWN def test_calc_match_and_rounding(): rows = [{"kat": "Must", "rating": "yes", "text": "a"}, {"kat": "Must", "rating": "yes", "text": "b"}, {"kat": "Must", "rating": "unknown", "text": "c"}, {"kat": "Nice", "rating": "no", "text": "d"}, {"kat": "Misc", "rating": "yes", "text": "e"}] assert rules.calc_match(rows, "Must") == 75 # (100+100+25)/3 assert rules.calc_match(rows, "Nice") == 0 assert rules.calc_match([], "Nice") is None # commercial rounding, half up (not banker's): (100+25)/2 = 62.5 -> 63 two = [{"kat": "Must", "rating": "yes", "text": "a"}, {"kat": "Must", "rating": "unknown", "text": "b"}] assert rules.calc_match(two, "Must") == 63 def test_build_description_exact_format(): rows = [{"kat": "Misc", "rating": "yes", "text": "Hybrid: 60 % remote"}, {"kat": "Must", "rating": "no", "text": "Mehrjährige LLM-Plattform-Erfahrung"}, {"kat": "Must", "rating": "yes", "text": "RAG-Systeme"}, {"kat": "Nice", "rating": "yes", "text": "vLLM"}] text = rules.build_description(rules.order_rows(rows)) assert text == ( "Must-have-Match: 50 % · Nice-to-have-Match: 100 %\n" "\n" "| Nr. | Kat. | ❔ | Anforderung |\n" "|---|---|---|---|\n" "| 1 | Must | ❌ | Mehrjährige LLM-Plattform-Erfahrung |\n" "| 2 | Must | ✅ | RAG-Systeme |\n" "| 3 | Nice | ✅ | vLLM |\n" "| 4 | Misc | ✅ | Hybrid: 60 % remote |") def test_empty_category_dash(): rows = [{"kat": "Must", "rating": "yes", "text": "a"}] assert rules.build_description(rows).startswith( "Must-have-Match: 100 % · Nice-to-have-Match: –")