from __future__ import annotations import json from typing import Any from fastapi.testclient import TestClient BASE = "/ai-api/resume-agent" def active_component(body: dict[str, Any]) -> dict[str, Any]: turns = body.get("turns") or ([body["turn"]] if body.get("turn") else []) for turn in reversed(turns): for block in reversed(turn["blocks"]): if block["type"] == "component" and block["lifecycle"] == "active": return block raise AssertionError("response has no active component") def event( client: TestClient, session_id: str, body: dict[str, Any], event_name: str, payload: dict[str, Any] | None = None, ): block = active_component(body) return client.post( f"{BASE}/sessions/{session_id}/component-events", json={"component_id": block["id"], "event": event_name, "payload": payload or {}}, ) def start_manual_profile( client: TestClient, *, job_type: str = "campus", target_position: str | None = "Backend Engineer", ) -> tuple[str, dict[str, Any]]: created = client.post(f"{BASE}/sessions", json={}) assert created.status_code == 201 body = created.json() session_id = body["session_id"] assert body["stage"] == "PRIVACY_CONSENT" source = event(client, session_id, body, "accept", {"accepted": True}) assert source.status_code == 200 assert source.json()["stage"] == "RESUME_SOURCE_SELECT" phone_selector = event(client, session_id, source.json(), "select", {"value": "manual"}) assert phone_selector.status_code == 200 assert phone_selector.json()["stage"] == "PHONE_SELECTION" phone_input = event(client, session_id, phone_selector.json(), "select", {"source": "other"}) assert phone_input.status_code == 200 assert phone_input.json()["stage"] == "MANUAL_PHONE_INPUT" personal = event(client, session_id, phone_input.json(), "submit", {"phone": "13800138000"}) assert personal.status_code == 200 assert personal.json()["stage"] == "PERSONAL_INFO" job_selector = event( client, session_id, personal.json(), "submit", {"name": "Zhang San", "email": "zhangsan@example.com", "city": "Shanghai"}, ) assert job_selector.status_code == 200 assert job_selector.json()["stage"] == "JOB_TYPE_SELECT" target = event(client, session_id, job_selector.json(), "select", {"job_type": job_type}) assert target.status_code == 200 assert target.json()["stage"] == "TARGET_POSITION" if target_position is None: ready = event(client, session_id, target.json(), "skip") else: ready = event(client, session_id, target.json(), "submit", {"target_position": target_position}) assert ready.status_code == 200, ready.text assert ready.json()["stage"] == "MINIMUM_READY" return session_id, ready.json() ANCHOR_CARD_VALUES = { "school": "Example University", "major": "Computer Science", "degree": "Bachelor", "start_date": "2021-09", "end_date_or_present": "2025-06", } def fill_anchor( client: TestClient, session_id: str, body: dict[str, Any], values: dict[str, str], ) -> dict[str, Any]: response = event(client, session_id, body, "submit", values) assert response.status_code == 200, response.text return response.json() def campus_ready(client: TestClient) -> tuple[str, dict[str, Any]]: return start_manual_profile(client, job_type="campus") def test_privacy_precedes_resume_source_selection(client: TestClient) -> None: created = client.post(f"{BASE}/sessions", json={}) session_id = created.json()["session_id"] source = event(client, session_id, created.json(), "accept", {"accepted": True}) assert source.status_code == 200 body = source.json() assert body["stage"] == "RESUME_SOURCE_SELECT" options = active_component(body)["data"]["options"] assert {option["value"] for option in options} == {"import", "manual"} def test_manual_phone_is_strict_and_retryable(client: TestClient) -> None: created = client.post(f"{BASE}/sessions", json={}).json() session_id = created["session_id"] source = event(client, session_id, created, "accept", {"accepted": True}).json() phone_selector = event(client, session_id, source, "select", {"value": "manual"}).json() phone_input = event(client, session_id, phone_selector, "select", {"source": "other"}).json() invalid = event(client, session_id, phone_input, "submit", {"phone": "+8613800138000"}) assert invalid.status_code == 422 assert invalid.json()["error"]["code"] == "invalid_phone" valid = event(client, session_id, phone_input, "submit", {"phone": "13900139000"}) assert valid.status_code == 200 assert valid.json()["stage"] == "PERSONAL_INFO" def test_target_position_creates_a_basic_resume_without_core_experience(client: TestClient) -> None: session_id, ready = start_manual_profile(client, job_type="social") assert ready["gate"]["allowed"] is True assert ready["missing_fields"] == [] created = client.post(f"{BASE}/sessions/{session_id}/create", json={"idempotency_key": "basic-resume"}) assert created.status_code == 200, created.text result = created.json() assert result["created"] is True assert result["stage"] == "BUILDER_CONVERSATION" assert result["resume"]["content"]["sections"] == [] def test_full_campus_creation_is_idempotent_and_masks_phone(client: TestClient) -> None: session_id, ready = campus_ready(client) first = client.post(f"{BASE}/sessions/{session_id}/create", json={"idempotency_key": "create-once"}) assert first.status_code == 200, first.text result = first.json() assert result["created"] is True assert result["stage"] == "BUILDER_CONVERSATION" assert result["resume"]["content"]["basics"]["masked_phone"] == "138****8000" assert "13800138000" not in json.dumps(result, ensure_ascii=False) second = client.post(f"{BASE}/sessions/{session_id}/create", json={"idempotency_key": "another-key"}) assert second.status_code == 200 assert second.json()["created"] is False assert second.json()["resume_id"] == result["resume_id"] def test_target_position_exploration_can_create_without_core_experience(client: TestClient) -> None: session_id, ready = start_manual_profile(client, job_type="campus", target_position=None) assert ready["stage"] == "MINIMUM_READY" assert ready["gate"]["allowed"] is True assert session_id