224 lines
11 KiB
Python
224 lines
11 KiB
Python
"""Unit tests for verify_diff_result() — local quality checks."""
|
|
|
|
import copy
|
|
import pytest
|
|
|
|
from app.schemas.models import ResumeChange
|
|
from app.services.improver import verify_diff_result
|
|
|
|
|
|
class TestVerifyNoWarnings:
|
|
"""Tests that pass cleanly with no warnings."""
|
|
|
|
def test_clean_result_no_warnings(self, sample_resume, sample_job_keywords):
|
|
result = copy.deepcopy(sample_resume)
|
|
result["summary"] = "Updated summary."
|
|
applied = [
|
|
ResumeChange(
|
|
path="summary",
|
|
action="replace",
|
|
original=sample_resume["summary"],
|
|
value="Updated summary.",
|
|
reason="test",
|
|
)
|
|
]
|
|
warnings = verify_diff_result(sample_resume, result, applied, sample_job_keywords)
|
|
assert len(warnings) == 0
|
|
|
|
|
|
class TestVerifyEmptyChanges:
|
|
"""Check 1: No changes applied."""
|
|
|
|
def test_warns_on_empty_applied_changes(self, sample_resume, sample_job_keywords):
|
|
warnings = verify_diff_result(sample_resume, sample_resume, [], sample_job_keywords)
|
|
assert len(warnings) == 1
|
|
assert "no changes" in warnings[0].lower()
|
|
|
|
def test_returns_early_on_empty(self, sample_resume, sample_job_keywords):
|
|
"""When no changes applied, skip other checks."""
|
|
warnings = verify_diff_result(sample_resume, sample_resume, [], sample_job_keywords)
|
|
# Should only have the "no changes" warning, not section count etc.
|
|
assert len(warnings) == 1
|
|
|
|
|
|
class TestVerifySectionCounts:
|
|
"""Check 2: Section counts preserved."""
|
|
|
|
def test_warns_on_dropped_work_experience(self, sample_resume, sample_job_keywords):
|
|
result = copy.deepcopy(sample_resume)
|
|
result["workExperience"] = result["workExperience"][:1] # Drop one
|
|
applied = [ResumeChange(path="summary", action="replace", original="x", value="y", reason="z")]
|
|
warnings = verify_diff_result(sample_resume, result, applied, sample_job_keywords)
|
|
assert any("work experience" in w.lower() for w in warnings)
|
|
|
|
def test_warns_on_dropped_education(self, sample_resume, sample_job_keywords):
|
|
result = copy.deepcopy(sample_resume)
|
|
result["education"] = []
|
|
applied = [ResumeChange(path="summary", action="replace", original="x", value="y", reason="z")]
|
|
warnings = verify_diff_result(sample_resume, result, applied, sample_job_keywords)
|
|
assert any("education" in w.lower() for w in warnings)
|
|
|
|
def test_warns_on_dropped_projects(self, sample_resume, sample_job_keywords):
|
|
result = copy.deepcopy(sample_resume)
|
|
result["personalProjects"] = []
|
|
applied = [ResumeChange(path="summary", action="replace", original="x", value="y", reason="z")]
|
|
warnings = verify_diff_result(sample_resume, result, applied, sample_job_keywords)
|
|
assert any("project" in w.lower() for w in warnings)
|
|
|
|
def test_no_warning_when_counts_match(self, sample_resume, sample_job_keywords):
|
|
result = copy.deepcopy(sample_resume)
|
|
result["summary"] = "Changed."
|
|
applied = [ResumeChange(path="summary", action="replace", original="x", value="Changed.", reason="z")]
|
|
warnings = verify_diff_result(sample_resume, result, applied, sample_job_keywords)
|
|
section_warnings = [w for w in warnings if "section count" in w.lower()]
|
|
assert len(section_warnings) == 0
|
|
|
|
|
|
class TestVerifyIdentityFields:
|
|
"""Check 3: Identity fields unchanged."""
|
|
|
|
def test_warns_on_company_change(self, sample_resume, sample_job_keywords):
|
|
result = copy.deepcopy(sample_resume)
|
|
result["workExperience"][0]["company"] = "Different Corp"
|
|
applied = [ResumeChange(path="summary", action="replace", original="x", value="y", reason="z")]
|
|
warnings = verify_diff_result(sample_resume, result, applied, sample_job_keywords)
|
|
assert any("company" in w.lower() or "identity" in w.lower() for w in warnings)
|
|
|
|
def test_warns_on_title_change(self, sample_resume, sample_job_keywords):
|
|
result = copy.deepcopy(sample_resume)
|
|
result["workExperience"][0]["title"] = "VP of Engineering"
|
|
applied = [ResumeChange(path="summary", action="replace", original="x", value="y", reason="z")]
|
|
warnings = verify_diff_result(sample_resume, result, applied, sample_job_keywords)
|
|
assert any("title" in w.lower() or "identity" in w.lower() for w in warnings)
|
|
|
|
def test_warns_on_institution_change(self, sample_resume, sample_job_keywords):
|
|
result = copy.deepcopy(sample_resume)
|
|
result["education"][0]["institution"] = "Stanford"
|
|
applied = [ResumeChange(path="summary", action="replace", original="x", value="y", reason="z")]
|
|
warnings = verify_diff_result(sample_resume, result, applied, sample_job_keywords)
|
|
assert any("institution" in w.lower() or "identity" in w.lower() for w in warnings)
|
|
|
|
|
|
class TestVerifyWordCount:
|
|
"""Check 4: Word count ratio."""
|
|
|
|
def test_warns_on_word_count_explosion(self, sample_resume, sample_job_keywords):
|
|
result = copy.deepcopy(sample_resume)
|
|
# Make descriptions very long
|
|
long_text = "word " * 200
|
|
result["workExperience"][0]["description"] = [long_text] * 5
|
|
result["workExperience"][1]["description"] = [long_text] * 5
|
|
applied = [ResumeChange(path="summary", action="replace", original="x", value="y", reason="z")]
|
|
warnings = verify_diff_result(sample_resume, result, applied, sample_job_keywords)
|
|
assert any("word count" in w.lower() for w in warnings)
|
|
|
|
def test_no_warning_on_normal_growth(self, sample_resume, sample_job_keywords):
|
|
result = copy.deepcopy(sample_resume)
|
|
# Add one bullet — modest growth
|
|
result["workExperience"][0]["description"].append("One extra bullet point here")
|
|
applied = [
|
|
ResumeChange(
|
|
path="workExperience[0].description",
|
|
action="append",
|
|
original=None,
|
|
value="One extra bullet point here",
|
|
reason="z",
|
|
)
|
|
]
|
|
warnings = verify_diff_result(sample_resume, result, applied, sample_job_keywords)
|
|
word_warnings = [w for w in warnings if "word count" in w.lower()]
|
|
assert len(word_warnings) == 0
|
|
|
|
|
|
class TestVerifyInventedMetrics:
|
|
"""Check 5: Invented metrics detection."""
|
|
|
|
def test_warns_on_invented_percentage(self, sample_resume, sample_job_keywords):
|
|
applied = [
|
|
ResumeChange(
|
|
path="workExperience[0].description[0]",
|
|
action="replace",
|
|
original="Built REST APIs",
|
|
value="Built REST APIs improving throughput by 40%",
|
|
reason="Added metric",
|
|
)
|
|
]
|
|
result = copy.deepcopy(sample_resume)
|
|
result["workExperience"][0]["description"][0] = "Built REST APIs improving throughput by 40%"
|
|
warnings = verify_diff_result(sample_resume, result, applied, sample_job_keywords)
|
|
assert any("metric" in w.lower() or "40%" in w for w in warnings)
|
|
|
|
def test_no_warning_on_preserved_metric(self, sample_resume, sample_job_keywords):
|
|
"""If the original already had the metric, no warning."""
|
|
applied = [
|
|
ResumeChange(
|
|
path="workExperience[0].description[0]",
|
|
action="replace",
|
|
original="Built REST APIs serving 50K requests/day using Python and FastAPI",
|
|
value="Designed REST APIs serving 50K requests/day with Python and FastAPI",
|
|
reason="Rephrased",
|
|
)
|
|
]
|
|
result = copy.deepcopy(sample_resume)
|
|
result["workExperience"][0]["description"][0] = applied[0].value
|
|
warnings = verify_diff_result(sample_resume, result, applied, sample_job_keywords)
|
|
metric_warnings = [w for w in warnings if "metric" in w.lower()]
|
|
assert len(metric_warnings) == 0
|
|
|
|
def test_warns_on_invented_dollar_amount(self, sample_resume, sample_job_keywords):
|
|
applied = [
|
|
ResumeChange(
|
|
path="workExperience[1].description[0]",
|
|
action="replace",
|
|
original="Developed payment processing system handling $2M monthly",
|
|
value="Developed payment processing system handling $5M monthly",
|
|
reason="Inflated",
|
|
)
|
|
]
|
|
result = copy.deepcopy(sample_resume)
|
|
result["workExperience"][1]["description"][0] = applied[0].value
|
|
warnings = verify_diff_result(sample_resume, result, applied, sample_job_keywords)
|
|
assert any("$5M" in w or "metric" in w.lower() for w in warnings)
|
|
|
|
|
|
class TestVerifyMultipleWarnings:
|
|
"""Edge case: multiple warnings from a single verification run."""
|
|
|
|
def test_multiple_warnings_all_reported(self, sample_resume, sample_job_keywords):
|
|
"""A result with section count drift AND identity change should produce both warnings."""
|
|
result = copy.deepcopy(sample_resume)
|
|
# Trigger section count warning: drop a work experience entry
|
|
result["workExperience"] = result["workExperience"][:1]
|
|
# Trigger identity field warning: change company name on remaining entry
|
|
result["workExperience"][0]["company"] = "Different Corp"
|
|
applied = [
|
|
ResumeChange(path="summary", action="replace", original="x", value="y", reason="z")
|
|
]
|
|
warnings = verify_diff_result(sample_resume, result, applied, sample_job_keywords)
|
|
section_warnings = [w for w in warnings if "section count" in w.lower() or "work experience" in w.lower()]
|
|
identity_warnings = [w for w in warnings if "identity" in w.lower() or "company" in w.lower()]
|
|
assert len(section_warnings) >= 1
|
|
assert len(identity_warnings) >= 1
|
|
assert len(warnings) >= 2
|
|
|
|
def test_metric_warning_plus_word_count_warning(self, sample_resume, sample_job_keywords):
|
|
"""Invented metric and word count explosion in the same result."""
|
|
result = copy.deepcopy(sample_resume)
|
|
long_text = "Improved revenue by 99% " + ("extra words " * 200)
|
|
result["workExperience"][0]["description"] = [long_text] * 5
|
|
result["workExperience"][1]["description"] = [long_text] * 5
|
|
applied = [
|
|
ResumeChange(
|
|
path="workExperience[0].description[0]",
|
|
action="replace",
|
|
original="Built REST APIs serving 50K requests/day using Python and FastAPI",
|
|
value=long_text,
|
|
reason="over-elaborate",
|
|
)
|
|
]
|
|
warnings = verify_diff_result(sample_resume, result, applied, sample_job_keywords)
|
|
has_metric = any("metric" in w.lower() or "99%" in w for w in warnings)
|
|
has_word_count = any("word count" in w.lower() for w in warnings)
|
|
assert has_metric
|
|
assert has_word_count
|