generated from kgod/ai-review-template
自内部仓库剥离深度优化与 RAG 知识库后的交付版本: - Builder 对话式简历生成(FSM + 意图路由 LLM 兜底增强) - 条目级轻度优化:事实覆盖门禁 + STAR/bullet 修复链,功能/简介/成果与技术栈同级保护 - 简历导入:DOCX/PDF 解析、结构归一、手机号脱敏 - PostgreSQL 运行时 + Alembic 迁移链 Co-Authored-By: Claude <noreply@anthropic.com>
172 lines
8.1 KiB
Python
172 lines
8.1 KiB
Python
"""Builder follow-up flows: editing, continuation, selection, streaming, revision."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
|
|
from fastapi.testclient import TestClient
|
|
|
|
from builder_flow_helpers import (
|
|
BASE,
|
|
active_component,
|
|
confirm_card,
|
|
create_builder_session,
|
|
event,
|
|
finish_education,
|
|
send_message,
|
|
start_education,
|
|
)
|
|
|
|
|
|
def test_editing_existing_entry_merges_facts_without_refilling_identity(client: TestClient) -> None:
|
|
session_id, body = create_builder_session(client)
|
|
start_education(client, session_id, body)
|
|
created = finish_education(client, session_id, body, "GPA 3.7/4.0,完成数据库课程项目。")
|
|
confirm_card(client, session_id, created)
|
|
|
|
edit_start = send_message(client, session_id, "修改 Example University 教育经历")
|
|
assert "无需重填" in edit_start["turn"]["content"]
|
|
assert not any(block["type"] == "component" for block in edit_start["turn"]["blocks"])
|
|
|
|
revised = send_message(client, session_id, "补充获得两次校级奖学金。")
|
|
card = active_component(revised)
|
|
assert card["data"]["component"] == "experience_confirm_card"
|
|
assert "GPA 3.7/4.0" in card["data"]["value"]["description"]
|
|
assert "两次校级奖学金" in card["data"]["value"]["description"]
|
|
|
|
|
|
def test_recent_confirmed_entry_accepts_natural_follow_up_without_refilling_identity(client: TestClient) -> None:
|
|
session_id, body = create_builder_session(client)
|
|
start_education(client, session_id, body)
|
|
proposal = finish_education(client, session_id, body, "GPA 3.7/4.0,完成数据库课程项目。")
|
|
confirm_card(client, session_id, proposal, use_optimized=True)
|
|
|
|
revised = send_message(client, session_id, "对了,我还获得过校级一等奖学金。")
|
|
assert "已收到这条补充信息" in revised["turn"]["content"]
|
|
card = active_component(revised)
|
|
assert card["data"]["component"] == "experience_confirm_card"
|
|
assert "GPA 3.7/4.0" in card["data"]["value"]["description"]
|
|
assert "校级一等奖学金" in card["data"]["value"]["description"]
|
|
assert not any(
|
|
block["data"].get("component") == "record_fields"
|
|
for block in revised["turn"]["blocks"]
|
|
if block["type"] == "component"
|
|
)
|
|
assert not any(
|
|
block["data"].get("module") == "builder_next_section"
|
|
for block in revised["turn"]["blocks"]
|
|
if block["type"] == "component"
|
|
)
|
|
|
|
|
|
def test_multiple_same_type_entries_offer_a_selection_card(client: TestClient) -> None:
|
|
session_id, body = create_builder_session(client)
|
|
start_education(client, session_id, body, school="First University")
|
|
first = finish_education(client, session_id, body, "GPA 3.6/4.0,完成课程项目。")
|
|
after_first = confirm_card(client, session_id, first)
|
|
start_education(client, session_id, after_first, school="Second University")
|
|
second = finish_education(client, session_id, after_first, "获得学业奖学金,参与实验室实践。")
|
|
confirm_card(client, session_id, second)
|
|
|
|
response = send_message(client, session_id, "修改教育经历")
|
|
card = active_component(response)
|
|
assert card["data"]["component"] == "choice_chips"
|
|
assert card["data"]["module"] == "builder_entry_select"
|
|
assert len(card["data"]["options"]) == 2
|
|
|
|
|
|
def test_builder_message_stream_emits_gap_and_rewrite_statuses(client: TestClient) -> None:
|
|
session_id, body = create_builder_session(client)
|
|
start_education(client, session_id, body)
|
|
with client.stream(
|
|
"POST",
|
|
f"{BASE}/sessions/{session_id}/messages/stream",
|
|
json={"content": "GPA 3.8/4.0,专业前 10%,完成数据库课程项目。"},
|
|
) as response:
|
|
assert response.status_code == 200
|
|
raw = b"".join(response.iter_bytes()).decode("utf-8")
|
|
|
|
events = [line.removeprefix("event: ") for line in raw.splitlines() if line.startswith("event: ")]
|
|
frames = [line.removeprefix("data: ") for line in raw.splitlines() if line.startswith("data: ")]
|
|
statuses = [json.loads(frame)["phase"] for event_name, frame in zip(events, frames) if event_name == "status"]
|
|
assert statuses == ["structuring", "checking_gaps", "rewriting"]
|
|
assert "delta" in events
|
|
assert events[-1] == "complete"
|
|
assert json.loads(frames[-1])["stage"] == "BUILDER_CONVERSATION"
|
|
|
|
|
|
def test_recent_entry_stream_emits_structuring_and_rewriting_only(client: TestClient) -> None:
|
|
session_id, body = create_builder_session(client)
|
|
start_education(client, session_id, body)
|
|
proposal = finish_education(client, session_id, body, "GPA 3.7/4.0,完成数据库课程项目。")
|
|
confirm_card(client, session_id, proposal)
|
|
|
|
with client.stream(
|
|
"POST",
|
|
f"{BASE}/sessions/{session_id}/messages/stream",
|
|
json={"content": "对了,我还获得过校级一等奖学金。"},
|
|
) as response:
|
|
assert response.status_code == 200
|
|
raw = b"".join(response.iter_bytes()).decode("utf-8")
|
|
|
|
events = [line.removeprefix("event: ") for line in raw.splitlines() if line.startswith("event: ")]
|
|
frames = [line.removeprefix("data: ") for line in raw.splitlines() if line.startswith("data: ")]
|
|
statuses = [json.loads(frame)["phase"] for event_name, frame in zip(events, frames) if event_name == "status"]
|
|
assert statuses == ["structuring", "rewriting"]
|
|
assert events[-1] == "complete"
|
|
|
|
|
|
def test_existing_education_supplement_routes_to_the_only_saved_entry(client: TestClient) -> None:
|
|
session_id, body = create_builder_session(client)
|
|
start_education(client, session_id, body)
|
|
proposal = finish_education(client, session_id, body, "GPA 3.7/4.0,专业前 20%,完成数据库课程项目。")
|
|
confirm_card(client, session_id, proposal)
|
|
|
|
response = send_message(client, session_id, "我要补充已经填写过的教育经历")
|
|
assert "无需重填" in response["turn"]["content"]
|
|
assert not any(
|
|
block["data"].get("component") == "record_fields"
|
|
for block in response["turn"]["blocks"]
|
|
if block["type"] == "component"
|
|
)
|
|
|
|
|
|
def test_existing_education_supplement_offers_a_choice_for_multiple_entries(client: TestClient) -> None:
|
|
session_id, body = create_builder_session(client)
|
|
start_education(client, session_id, body, school="First University")
|
|
first = finish_education(client, session_id, body, "GPA 3.6/4.0,完成课程项目。")
|
|
after_first = confirm_card(client, session_id, first)
|
|
start_education(client, session_id, after_first, school="Second University")
|
|
second = finish_education(client, session_id, after_first, "GPA 3.8/4.0,获得学业奖学金并完成机器学习课程项目。")
|
|
confirm_card(client, session_id, second)
|
|
|
|
response = send_message(client, session_id, "补充已经填写过的教育经历")
|
|
card = active_component(response)
|
|
assert card["data"]["component"] == "choice_chips"
|
|
assert card["data"]["module"] == "builder_entry_select"
|
|
assert len(card["data"]["options"]) == 2
|
|
|
|
|
|
def test_explicit_new_education_entry_still_shows_an_identity_card(client: TestClient) -> None:
|
|
session_id, body = create_builder_session(client)
|
|
start_education(client, session_id, body)
|
|
proposal = finish_education(client, session_id, body, "GPA 3.7/4.0,完成数据库课程项目。")
|
|
after_confirm = confirm_card(client, session_id, proposal)
|
|
|
|
response = send_message(client, session_id, "新增一段教育经历")
|
|
card = active_component(response)
|
|
assert card["data"]["component"] == "record_fields"
|
|
assert card["data"]["record_type"] == "education"
|
|
|
|
|
|
def test_revision_correction_is_not_treated_as_a_no_information_answer(client: TestClient) -> None:
|
|
session_id, body = create_builder_session(client)
|
|
start_education(client, session_id, body)
|
|
proposal = finish_education(client, session_id, body, "GPA 3.7/4.0,完成数据库课程项目。")
|
|
revision = event(client, session_id, proposal, "edit", {})
|
|
assert revision.status_code == 200, revision.text
|
|
|
|
response = send_message(client, session_id, "我没有说要跳过这个经历")
|
|
assert "已理解你的调整说明" in response["turn"]["content"]
|
|
assert active_component(response)["data"]["component"] == "experience_confirm_card"
|