"""employment_agent clamps for the new skills / years_experience fields. These are the CV Bank's only structured data, and the model is the only source, so the clamps are what stop a hallucinated skill becoming a searchable fact. """ from employment_agent.decorators import parse_employment_response from employment_agent.prompt import CURRENT_TITLE, EDUCATION, NO_COMPANY RESUME = ( "Ada Lovelace\n" "Backend Engineer at Acme\n" "Skills: Python, FastAPI, PostgreSQL, Docker\n" "BS Computer Science\n" "6 years of experience\n" ) def parse(payload, resume_text=RESUME): return parse_employment_response(payload, resume_text) # -------------------------------------------------------------------------- # Backwards compatibility — the inbox match path predates these fields # -------------------------------------------------------------------------- def test_response_without_the_new_keys_still_parses(): """An older or partial reply must not break inbox matching.""" fields = parse({ "current_employment": "Acme", "education": "BS Computer Science", "current_title": "Backend Engineer", "linkedin_url": "", "phone": "", }) assert fields["skills"] == [] assert fields["years_experience"] is None assert fields["current_employment"] == "Acme" def test_non_list_skills_degrade_to_empty(): assert parse({"skills": "Python, FastAPI"})["skills"] == [] assert parse({"skills": None})["skills"] == [] assert parse({"skills": {"a": 1}})["skills"] == [] # -------------------------------------------------------------------------- # skills # -------------------------------------------------------------------------- def test_skills_present_in_the_resume_are_kept_with_their_own_spelling(): fields = parse({"skills": ["Python", "FastAPI", "PostgreSQL"]}) assert fields["skills"] == ["Python", "FastAPI", "PostgreSQL"] def test_fabricated_skills_are_dropped(): """The model crediting Kubernetes to a CV that never mentions it is the exact defect this clamp exists for.""" fields = parse({"skills": ["Python", "Kubernetes", "Terraform"]}) assert fields["skills"] == ["Python"] def test_skills_are_deduplicated_case_insensitively_keeping_first_spelling(): fields = parse({"skills": ["Python", "python", "PYTHON", "FastAPI"]}) assert fields["skills"] == ["Python", "FastAPI"] def test_blank_and_whitespace_skills_are_removed(): fields = parse({"skills": ["Python", "", " ", "\n", "FastAPI"]}) assert fields["skills"] == ["Python", "FastAPI"] def test_skills_are_trimmed_before_matching(): fields = parse({"skills": [" Python ", " FastAPI"]}) assert fields["skills"] == ["Python", "FastAPI"] def test_dedup_runs_before_the_thirty_cap(): """31 near-duplicates must collapse under the limit rather than push real skills out of it — same ordering rule as ATSScore's keyword arrays.""" resume = "Skills: " + ", ".join(f"skill{i}" for i in range(30)) + ", Python\n" noisy = ["Python"] * 5 + [f"skill{i}" for i in range(30)] fields = parse_employment_response({"skills": noisy}, resume) assert len(fields["skills"]) == 30 assert fields["skills"][0] == "Python" assert fields["skills"].count("Python") == 1 def test_skills_are_capped_at_thirty(): resume = "Skills: " + ", ".join(f"skill{i}" for i in range(50)) fields = parse_employment_response( {"skills": [f"skill{i}" for i in range(50)]}, resume, ) assert len(fields["skills"]) == 30 def test_sentence_length_entries_are_rejected(): """A responsibility is not a skill; a 60-char ceiling keeps chips renderable.""" long_entry = "Responsible for building and maintaining backend services at scale" fields = parse_employment_response({"skills": [long_entry]}, long_entry) assert fields["skills"] == [] def test_non_string_entries_are_ignored(): fields = parse({"skills": ["Python", 42, None, {"x": 1}, ["FastAPI"]]}) assert fields["skills"] == ["Python"] def test_skills_pass_through_when_there_is_no_resume_text_to_check_against(): """Nothing to verify against is not evidence of fabrication.""" fields = parse_employment_response({"skills": ["Python", "Kubernetes"]}, "") assert fields["skills"] == ["Python", "Kubernetes"] # -------------------------------------------------------------------------- # years_experience # -------------------------------------------------------------------------- def test_stated_years_are_kept(): assert parse({"years_experience": 6})["years_experience"] == 6 def test_zero_years_is_a_real_value(): assert parse({"years_experience": 0})["years_experience"] == 0 def test_years_are_bounded_at_sixty(): assert parse({"years_experience": 61})["years_experience"] is None assert parse({"years_experience": 60})["years_experience"] == 60 def test_negative_years_are_rejected(): assert parse({"years_experience": -3})["years_experience"] is None def test_years_as_a_string_are_parsed(): assert parse({"years_experience": "6"})["years_experience"] == 6 assert parse({"years_experience": "6 years"})["years_experience"] == 6 def test_unparseable_years_read_as_unknown_not_zero(): """0 would sort the candidate as a fresh graduate; unknown must stay unknown.""" for value in (None, "", "several", "many years", [], {}, True, False): assert parse({"years_experience": value})["years_experience"] is None def test_float_years_truncate_to_whole_years(): assert parse({"years_experience": 6.8})["years_experience"] == 6 # -------------------------------------------------------------------------- # The pre-existing fields are unaffected by the new clamps # -------------------------------------------------------------------------- def test_existing_sentinels_still_normalize(): fields = parse({ "current_employment": NO_COMPANY, "education": EDUCATION, "current_title": CURRENT_TITLE, "skills": ["Python"], "years_experience": 6, }) assert fields["current_employment"] == NO_COMPANY assert fields["education"] == EDUCATION assert fields["skills"] == ["Python"] def test_city_from_the_model_is_kept_as_returned(): resume = "Ali Khan | Karachi(Malir) | 0321-5551234" fields = parse_employment_response({"city": "Karachi"}, resume) assert fields["city"] == "Karachi" def test_messy_model_city_is_clamped_to_canonical_before_persist(): resume = "Ali Khan | Karachi(Malir) | 0321-5551234" fields = parse_employment_response({"city": "Karachi(Malir)"}, resume) assert fields["city"] == "Karachi" assert parse({"city": "DHA Karachi"})["city"] == "Karachi" assert parse({"city": "Wah Cantt"})["city"] == "Wah" def test_city_sentinel_is_dropped(): assert parse({"city": "no city mentioned"})["city"] is None