Files
srbhr--resume-matcher/apps/backend/tests/integration/test_regenerate_endpoints.py
T
wehub-resource-sync 5bdf4cc89a
Publish Docker Image / publish (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:39:36 +08:00

331 lines
12 KiB
Python

import unittest
from unittest.mock import AsyncMock, MagicMock, patch
from fastapi import HTTPException
from pydantic import ValidationError
from app.routers import enrichment as enrichment_router
from app.schemas.enrichment import RegenerateItemInput, RegenerateRequest, RegeneratedItem
class TestRegenerateSchemas(unittest.TestCase):
def test_regenerate_request_instruction_max_length(self) -> None:
item = RegenerateItemInput(
item_id="skills",
item_type="skills",
title="Skills",
current_content=["Python"],
)
RegenerateRequest(
resume_id="resume_1",
items=[item],
instruction="x" * 2000,
output_language="en",
)
with self.assertRaises(ValidationError):
RegenerateRequest(
resume_id="resume_1",
items=[item],
instruction="x" * 2001,
output_language="en",
)
class TestRegenerateEndpoints(unittest.IsolatedAsyncioTestCase):
async def test_regenerate_processes_multiple_items_in_parallel(self) -> None:
resume_id = "resume_1"
request = RegenerateRequest(
resume_id=resume_id,
items=[
RegenerateItemInput(
item_id="exp_0",
item_type="experience",
title="Senior Software Engineer",
subtitle="Google",
current_content=["Old"],
),
RegenerateItemInput(
item_id="skills",
item_type="skills",
title="Technical Skills",
current_content=["Python"],
),
],
instruction="Improve wording",
output_language="en",
)
mock_db = AsyncMock()
mock_db.get_resume.return_value = {"processed_data": {"workExperience": [], "additional": {}}}
exp_item = RegeneratedItem(
item_id="exp_0",
item_type="experience",
title="Senior Software Engineer",
subtitle="Google",
original_content=["Old"],
new_content=["New"],
diff_summary="Summary",
)
skills_item = RegeneratedItem(
item_id="skills",
item_type="skills",
title="Technical Skills",
original_content=["Python"],
new_content=["Python", "TypeScript"],
diff_summary="Summary",
)
with (
patch.object(enrichment_router, "db", mock_db),
patch.object(
enrichment_router,
"_regenerate_experience_or_project",
AsyncMock(return_value=exp_item),
) as mock_regenerate_item,
patch.object(
enrichment_router,
"_regenerate_skills",
AsyncMock(return_value=skills_item),
) as mock_regenerate_skills,
):
response = await enrichment_router.regenerate_items(request)
self.assertEqual(
[item.item_id for item in response.regenerated_items],
["exp_0", "skills"],
)
mock_regenerate_item.assert_awaited()
mock_regenerate_skills.assert_awaited()
async def test_regenerate_allows_partial_success_with_errors(self) -> None:
resume_id = "resume_1"
request = RegenerateRequest(
resume_id=resume_id,
items=[
RegenerateItemInput(
item_id="exp_0",
item_type="experience",
title="Senior Software Engineer",
subtitle="Google",
current_content=["Old"],
),
RegenerateItemInput(
item_id="skills",
item_type="skills",
title="Technical Skills",
current_content=["Python"],
),
],
instruction="Improve wording",
output_language="en",
)
mock_db = AsyncMock()
mock_db.get_resume.return_value = {"processed_data": {"workExperience": [], "additional": {}}}
skills_item = RegeneratedItem(
item_id="skills",
item_type="skills",
title="Technical Skills",
original_content=["Python"],
new_content=["Python", "TypeScript"],
diff_summary="Summary",
)
with (
patch.object(enrichment_router, "db", mock_db),
patch.object(
enrichment_router,
"_regenerate_experience_or_project",
AsyncMock(side_effect=RuntimeError("boom")),
),
patch.object(
enrichment_router,
"_regenerate_skills",
AsyncMock(return_value=skills_item),
),
):
response = await enrichment_router.regenerate_items(request)
self.assertEqual([item.item_id for item in response.regenerated_items], ["skills"])
self.assertEqual([err.item_id for err in response.errors], ["exp_0"])
async def test_apply_regenerated_falls_back_to_metadata_matching(self) -> None:
resume_id = "resume_1"
processed_data = {
"workExperience": [
{
"title": "Some Other Role",
"company": "OtherCo",
"description": ["Keep me"],
},
{
"title": "Senior Software Engineer",
"company": "Google",
"description": ["Old bullet"],
},
],
"personalProjects": [],
"additional": {"technicalSkills": ["Python"]},
}
mock_db = AsyncMock()
mock_db.get_resume.return_value = {"processed_data": processed_data}
mock_db.update_resume.return_value = None
regenerated_items = [
RegeneratedItem(
item_id="exp_0", # stale index: exp_0 no longer points to the matching entry
item_type="experience",
title="Senior Software Engineer",
subtitle="Google",
original_content=["Old bullet"],
new_content=["New bullet"],
diff_summary="Summary",
)
]
with patch.object(enrichment_router, "db", mock_db):
result = await enrichment_router.apply_regenerated_items(resume_id, regenerated_items)
self.assertEqual(result["updated_items"], 1)
update_payload = mock_db.update_resume.call_args.args[1]
updated = update_payload["processed_data"]
self.assertEqual(updated["workExperience"][0]["description"], ["Keep me"])
self.assertEqual(updated["workExperience"][1]["description"], ["New bullet"])
async def test_apply_regenerated_disambiguates_duplicates_by_original_content(self) -> None:
resume_id = "resume_1"
processed_data = {
"workExperience": [
{"title": "Engineer", "company": "Google", "description": ["Bullet A"]},
{"title": "Engineer", "company": "Google", "description": ["Bullet B"]},
],
"personalProjects": [],
"additional": {"technicalSkills": ["Python"]},
}
mock_db = AsyncMock()
mock_db.get_resume.return_value = {"processed_data": processed_data}
mock_db.update_resume.return_value = None
regenerated_items = [
RegeneratedItem(
item_id="exp_0", # could point to a different duplicate after reordering
item_type="experience",
title="Engineer",
subtitle="Google",
original_content=["Bullet B"],
new_content=["Bullet B (rewritten)"],
diff_summary="Summary",
)
]
with patch.object(enrichment_router, "db", mock_db):
result = await enrichment_router.apply_regenerated_items(resume_id, regenerated_items)
self.assertEqual(result["updated_items"], 1)
updated = mock_db.update_resume.call_args.args[1]["processed_data"]
self.assertEqual(updated["workExperience"][0]["description"], ["Bullet A"])
self.assertEqual(updated["workExperience"][1]["description"], ["Bullet B (rewritten)"])
async def test_apply_regenerated_refuses_when_items_do_not_match(self) -> None:
resume_id = "resume_1"
processed_data = {
"workExperience": [
{"title": "Engineer", "company": "Acme", "description": ["Old"]},
],
"personalProjects": [],
"additional": {"technicalSkills": ["Python"]},
}
mock_db = AsyncMock()
mock_db.get_resume.return_value = {"processed_data": processed_data}
regenerated_items = [
RegeneratedItem(
item_id="exp_0",
item_type="experience",
title="Different Title",
subtitle="Different Co",
original_content=["Old"],
new_content=["New"],
diff_summary="Summary",
)
]
with patch.object(enrichment_router, "db", mock_db):
with self.assertRaises(HTTPException) as ctx:
await enrichment_router.apply_regenerated_items(resume_id, regenerated_items)
self.assertEqual(ctx.exception.status_code, 409)
mock_db.update_resume.assert_not_called()
async def test_apply_regenerated_updates_skills_for_additional_and_legacy_paths(self) -> None:
resume_id = "resume_1"
base_item = RegeneratedItem(
item_id="skills",
item_type="skills",
title="Technical Skills",
original_content=["Python"],
new_content=["Python", "TypeScript"],
diff_summary="Summary",
)
# additional.technicalSkills path
mock_db_additional = AsyncMock()
mock_db_additional.get_resume.return_value = {
"processed_data": {"additional": {"technicalSkills": ["Python"]}}
}
mock_db_additional.update_resume.return_value = None
with patch.object(enrichment_router, "db", mock_db_additional):
result = await enrichment_router.apply_regenerated_items(resume_id, [base_item])
self.assertEqual(result["updated_items"], 1)
updated = mock_db_additional.update_resume.call_args.args[1]["processed_data"]
self.assertEqual(updated["additional"]["technicalSkills"], ["Python", "TypeScript"])
# legacy technicalSkills path
mock_db_legacy = AsyncMock()
mock_db_legacy.get_resume.return_value = {"processed_data": {"technicalSkills": ["Python"]}}
mock_db_legacy.update_resume.return_value = None
with patch.object(enrichment_router, "db", mock_db_legacy):
result = await enrichment_router.apply_regenerated_items(resume_id, [base_item])
self.assertEqual(result["updated_items"], 1)
updated = mock_db_legacy.update_resume.call_args.args[1]["processed_data"]
self.assertEqual(updated["technicalSkills"], ["Python", "TypeScript"])
async def test_apply_regenerated_skills_fails_when_no_supported_path_exists(self) -> None:
resume_id = "resume_1"
mock_db = AsyncMock()
mock_db.get_resume.return_value = {"processed_data": {"workExperience": []}}
regenerated_items = [
RegeneratedItem(
item_id="skills",
item_type="skills",
title="Technical Skills",
original_content=["Python"],
new_content=["Python", "TypeScript"],
diff_summary="Summary",
)
]
with patch.object(enrichment_router, "db", mock_db):
with self.assertRaises(HTTPException) as ctx:
await enrichment_router.apply_regenerated_items(resume_id, regenerated_items)
self.assertEqual(ctx.exception.status_code, 409)
mock_db.update_resume.assert_not_called()