项目文件夹

文件
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

242 行
8.7 KiB
Python

"""Integration tests for the adaptive resume wizard endpoints."""
import json
from unittest.mock import AsyncMock, patch
from httpx import ASGITransport, AsyncClient
from app.main import app
from app.schemas.resume_wizard import ResumeWizardHistoryEntry, ResumeWizardQuestion
from app.services.resume_wizard import (
RESUME_WIZARD_MAX_QUESTIONS,
build_initial_wizard_state,
)
_AI_RESULT = {
"resume_data": {
"personalInfo": {"name": "James"},
"summary": "",
"workExperience": [],
"education": [],
"personalProjects": [],
"additional": {
"technicalSkills": ["Python"],
"languages": [],
"certificationsTraining": [],
"awards": [],
},
"sectionMeta": [],
"customSections": {},
},
"next_question": {"text": "What tools do you use most?", "section": "skills"},
"inferred_skills": ["FastAPI"],
"is_complete": False,
}
async def test_turn_answer_runs_ai_and_returns_next_question(isolated_db) -> None:
transport = ASGITransport(app=app)
state = build_initial_wizard_state()
state.step = "question"
state.current_question.section = "skills"
with patch(
"app.services.resume_wizard.complete_json",
new_callable=AsyncMock,
return_value=_AI_RESULT,
):
async with AsyncClient(transport=transport, base_url="http://test") as client:
response = await client.post(
"/api/v1/resume-wizard/turn",
json={
"state": state.model_dump(mode="json"),
"action": "answer",
"answer": {"text": "I use Python and FastAPI."},
},
)
assert response.status_code == 200
payload = response.json()["state"]
assert payload["current_question"]["text"] == "What tools do you use most?"
assert payload["resume_data"]["additional"]["technicalSkills"] == ["Python", "FastAPI"]
assert payload["asked_count"] == 1
async def test_turn_review_needs_no_llm(isolated_db) -> None:
transport = ASGITransport(app=app)
state = build_initial_wizard_state()
state.step = "question"
state.resume_data.personalInfo.name = "James"
async with AsyncClient(transport=transport, base_url="http://test") as client:
response = await client.post(
"/api/v1/resume-wizard/turn",
json={"state": state.model_dump(mode="json"), "action": "review"},
)
assert response.status_code == 200
payload = response.json()["state"]
assert payload["step"] == "review"
assert payload["warnings"]
async def test_turn_answer_without_answer_is_422(isolated_db) -> None:
transport = ASGITransport(app=app)
state = build_initial_wizard_state()
async with AsyncClient(transport=transport, base_url="http://test") as client:
response = await client.post(
"/api/v1/resume-wizard/turn",
json={"state": state.model_dump(mode="json"), "action": "answer"},
)
assert response.status_code == 422
async def test_finalize_creates_ready_master_resume(isolated_db) -> None:
transport = ASGITransport(app=app)
state = build_initial_wizard_state()
state.resume_data.personalInfo.name = "James"
state.resume_data.personalInfo.email = "james@example.com"
state.resume_data.additional.technicalSkills = ["Python"]
async with AsyncClient(transport=transport, base_url="http://test") as client:
response = await client.post(
"/api/v1/resume-wizard/finalize",
json={"state": state.model_dump(mode="json")},
)
assert response.status_code == 200
payload = response.json()
assert payload["processing_status"] == "ready"
assert payload["is_master"] is True
stored = await isolated_db.get_resume(payload["resume_id"])
assert stored is not None
assert stored["is_master"] is True
assert stored["content_type"] == "json"
assert json.loads(stored["content"])["personalInfo"]["name"] == "James"
async def test_finalize_rejects_when_master_exists(isolated_db, sample_resume) -> None:
await isolated_db.create_resume(
content=json.dumps(sample_resume),
content_type="json",
filename="existing.json",
is_master=True,
processed_data=sample_resume,
processing_status="ready",
)
state = build_initial_wizard_state()
state.resume_data.personalInfo.name = "James"
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as client:
response = await client.post(
"/api/v1/resume-wizard/finalize",
json={"state": state.model_dump(mode="json")},
)
assert response.status_code == 409
assert "already exists" in response.json()["detail"].lower()
async def test_turn_start_returns_initial_state(isolated_db) -> None:
transport = ASGITransport(app=app)
state = build_initial_wizard_state()
async with AsyncClient(transport=transport, base_url="http://test") as client:
response = await client.post(
"/api/v1/resume-wizard/turn",
json={"state": state.model_dump(mode="json"), "action": "start"},
)
assert response.status_code == 200
payload = response.json()["state"]
assert payload["step"] == "intro"
assert payload["current_question"]["section"] == "intro"
assert payload["asked_count"] == 0
async def test_turn_back_restores_previous_question(isolated_db) -> None:
transport = ASGITransport(app=app)
state = build_initial_wizard_state()
state.step = "question"
state.asked_count = 1
state.current_question = ResumeWizardQuestion(text="Skills?", section="skills")
state.resume_data.additional.technicalSkills = ["Python"]
state.history = [
ResumeWizardHistoryEntry(
question="Where have you worked?",
answer="Acme",
section="workExperience",
resume_data_before=build_initial_wizard_state().resume_data,
)
]
async with AsyncClient(transport=transport, base_url="http://test") as client:
response = await client.post(
"/api/v1/resume-wizard/turn",
json={"state": state.model_dump(mode="json"), "action": "back"},
)
assert response.status_code == 200
payload = response.json()["state"]
assert payload["asked_count"] == 0
assert payload["current_question"]["section"] == "workExperience"
# The pre-answer snapshot is restored, dropping the later skills edit.
assert payload["resume_data"]["additional"]["technicalSkills"] == []
async def test_turn_skip_advances_without_modifying_resume_data(isolated_db) -> None:
transport = ASGITransport(app=app)
state = build_initial_wizard_state()
state.step = "question"
state.current_question = ResumeWizardQuestion(text="Education?", section="education")
skip_result = {
"resume_data": {"education": [{"id": 1, "institution": "MIT"}]},
"next_question": {"text": "What skills?", "section": "skills"},
"inferred_skills": [],
"is_complete": False,
}
with patch(
"app.services.resume_wizard.complete_json",
new_callable=AsyncMock,
return_value=skip_result,
):
async with AsyncClient(transport=transport, base_url="http://test") as client:
response = await client.post(
"/api/v1/resume-wizard/turn",
json={"state": state.model_dump(mode="json"), "action": "skip"},
)
assert response.status_code == 200
payload = response.json()["state"]
assert payload["current_question"]["section"] == "skills"
assert payload["resume_data"]["education"] == [] # skip must not apply the model's data
assert payload["asked_count"] == 1
async def test_turn_answer_past_cap_routes_to_review_without_llm(isolated_db) -> None:
transport = ASGITransport(app=app)
state = build_initial_wizard_state()
state.step = "question"
state.current_question.section = "skills"
state.asked_count = RESUME_WIZARD_MAX_QUESTIONS # at the cap
with patch(
"app.services.resume_wizard.complete_json",
new_callable=AsyncMock,
) as mock_complete:
async with AsyncClient(transport=transport, base_url="http://test") as client:
response = await client.post(
"/api/v1/resume-wizard/turn",
json={
"state": state.model_dump(mode="json"),
"action": "answer",
"answer": {"text": "one more thing"},
},
)
assert response.status_code == 200
assert response.json()["state"]["step"] == "review"
mock_complete.assert_not_awaited() # cap guard must skip the LLM call