generated from kgod/ai-review-template
232 lines
9.4 KiB
Python
232 lines
9.4 KiB
Python
from __future__ import annotations
|
|
|
|
from app.resume_expansion import (
|
|
FallbackEntryExpander,
|
|
OpenAIEntryExpander,
|
|
_EXPANSION_REPAIR_PROMPT,
|
|
_system_prompt,
|
|
build_expander,
|
|
)
|
|
from app.resume_expansion_prompts import _repair_prompt
|
|
from app.settings import Settings
|
|
|
|
|
|
def test_light_expansion_prompt_prioritizes_fact_completeness() -> None:
|
|
prompt = _system_prompt("project_experience")
|
|
assert "Completeness first" in prompt
|
|
assert "do not drop meaningful facts for brevity" in prompt
|
|
assert "covered_fact_ids" not in prompt
|
|
|
|
|
|
def test_light_expansion_prompt_keeps_hard_boundaries_and_star() -> None:
|
|
prompt = _system_prompt("work_experience")
|
|
assert "hard_required_facts" in prompt
|
|
assert "quantity with its original object" in prompt
|
|
assert "responsibility level" in prompt
|
|
assert "STAR" in prompt
|
|
assert prompt.index("STAR") < prompt.index("- ")
|
|
|
|
|
|
def test_education_prompt_polishes_without_star_or_bullets() -> None:
|
|
prompt = _system_prompt("education")
|
|
assert "Do not use a STAR" in prompt
|
|
assert "education entries" in prompt
|
|
assert "bullet points" not in prompt
|
|
|
|
|
|
class _SequentialCompletion:
|
|
def __init__(self, outputs: list[dict[str, object] | Exception]) -> None:
|
|
self.outputs = outputs
|
|
self.calls: list[dict[str, object]] = []
|
|
self.call_options: list[dict[str, object]] = []
|
|
self.schema_names: list[str] = []
|
|
self.system_prompts: list[str] = []
|
|
|
|
def complete(self, *, schema, schema_name, system_prompt, payload, **kwargs):
|
|
self.calls.append(payload)
|
|
self.call_options.append(kwargs)
|
|
self.schema_names.append(schema_name)
|
|
self.system_prompts.append(system_prompt)
|
|
value = self.outputs[min(len(self.calls) - 1, len(self.outputs) - 1)]
|
|
if isinstance(value, Exception):
|
|
raise value
|
|
return schema.model_validate({"optimized_description": value["optimized_description"]})
|
|
|
|
|
|
def _output(text: str) -> dict[str, object]:
|
|
return {"optimized_description": text}
|
|
|
|
|
|
def test_missing_coverage_declaration_does_not_add_a_repair_round() -> None:
|
|
completion = _SequentialCompletion([_output("\u5b8c\u6210\u5df2\u786e\u8ba4\u7684\u5de5\u4f5c\u3002")])
|
|
expander = OpenAIEntryExpander(completion)
|
|
entry = {"description": "\u8fdb\u884c\u9700\u6c42\u5206\u6790\u3002\n\u5b8c\u6210\u63a5\u53e3\u8bbe\u8ba1\u3002\n\u6267\u884c\u4e0a\u7ebf\u652f\u6301\u3002"}
|
|
|
|
proposal = expander.expand(entry, context={"entry_type": "project_experience"})
|
|
|
|
assert len(completion.calls) == 1
|
|
assert proposal["changes"] == []
|
|
assert "coverage_targets" not in completion.calls[0]
|
|
assert "covered_fact_ids" not in proposal
|
|
|
|
|
|
def test_hard_fact_omission_repairs_with_atomic_anchor() -> None:
|
|
entry = {"description": "\u4f7f\u7528 FastAPI \u5f00\u53d1\u670d\u52a1\uff0c\u652f\u6301 300 \u540d\u7528\u6237\u3002"}
|
|
completion = _SequentialCompletion([
|
|
_output("\u652f\u6301 300 \u540d\u7528\u6237\u3002"),
|
|
_output("\u4f7f\u7528 FastAPI \u5f00\u53d1\u670d\u52a1\uff0c\u652f\u6301 300 \u540d\u7528\u6237\u3002"),
|
|
])
|
|
expander = OpenAIEntryExpander(completion)
|
|
|
|
proposal = expander.expand(entry, context={"entry_type": "project_experience"})
|
|
|
|
assert len(completion.calls) == 2
|
|
assert completion.calls[1]["rejected_reason"] == "hard_fact_omitted"
|
|
assert completion.calls[1]["omitted_facts"] == ["fastapi"]
|
|
assert proposal["uncovered_facts"] == []
|
|
|
|
|
|
def test_failed_repair_keeps_the_first_pass_candidate() -> None:
|
|
entry = {"description": "\u4f7f\u7528 FastAPI \u5f00\u53d1\u670d\u52a1\uff0c\u652f\u6301 300 \u540d\u7528\u6237\u3002"}
|
|
completion = _SequentialCompletion([
|
|
_output("\u652f\u6301 300 \u540d\u7528\u6237\u3002"),
|
|
RuntimeError("network failure"),
|
|
])
|
|
expander = OpenAIEntryExpander(completion)
|
|
|
|
proposal = expander.expand(entry, context={"entry_type": "project_experience"})
|
|
|
|
assert len(completion.calls) == 2
|
|
assert proposal["optimized_description"].endswith("300 \u540d\u7528\u6237\u3002")
|
|
assert "repair_failed" in proposal["validation_warnings"]
|
|
|
|
|
|
def test_non_education_bullets_are_normalized_locally() -> None:
|
|
completion = _SequentialCompletion([_output("- \u8d1f\u8d23\u9700\u6c42\u5206\u6790\u3002\n2. \u5b8c\u6210\u90e8\u7f72\u4e0a\u7ebf\u3002")])
|
|
expander = OpenAIEntryExpander(completion)
|
|
|
|
proposal = expander.expand(
|
|
{"description": "\u8d1f\u8d23\u9700\u6c42\u5206\u6790\u3002\u5b8c\u6210\u90e8\u7f72\u4e0a\u7ebf\u3002"},
|
|
context={"entry_type": "project_experience"},
|
|
)
|
|
|
|
assert proposal["optimized_description"].splitlines() == [
|
|
"\u2022 \u8d1f\u8d23\u9700\u6c42\u5206\u6790\u3002",
|
|
"\u2022 \u5b8c\u6210\u90e8\u7f72\u4e0a\u7ebf\u3002",
|
|
]
|
|
|
|
|
|
def test_education_never_gets_local_bullets() -> None:
|
|
completion = _SequentialCompletion([_output("\u5b8c\u6210\u6570\u636e\u5e93\u8bfe\u7a0b\u9879\u76ee\uff0cGPA 3.8/4.0\u3002")])
|
|
expander = OpenAIEntryExpander(completion)
|
|
|
|
proposal = expander.expand(
|
|
{"description": "\u5b8c\u6210\u6570\u636e\u5e93\u8bfe\u7a0b\u9879\u76ee\uff0cGPA 3.8/4.0\u3002"},
|
|
context={"entry_type": "education"},
|
|
)
|
|
|
|
assert proposal["optimized_description"] == "\u5b8c\u6210\u6570\u636e\u5e93\u8bfe\u7a0b\u9879\u76ee\uff0cGPA 3.8/4.0\u3002"
|
|
|
|
|
|
def test_repair_prompt_keeps_star_and_dash_bullets() -> None:
|
|
prompt = _repair_prompt("project_experience")
|
|
assert _EXPANSION_REPAIR_PROMPT in prompt
|
|
assert "STAR" in prompt
|
|
assert prompt.index("STAR") < prompt.index("- ")
|
|
assert "bullet points" in prompt
|
|
|
|
def test_entry_expansion_uses_one_attempt_and_a_remaining_repair_budget() -> None:
|
|
entry = {"description": "Built a FastAPI service for 300 users."}
|
|
completion = _SequentialCompletion([
|
|
_output("Supported 300 users."),
|
|
_output("Built a FastAPI service for 300 users."),
|
|
])
|
|
expander = OpenAIEntryExpander(completion, timeout_seconds=30.0)
|
|
|
|
expander.expand(entry, context={"entry_type": "project_experience"})
|
|
|
|
assert completion.call_options[0]["max_attempts"] == 1
|
|
assert completion.call_options[0]["timeout_seconds"] <= 30.0
|
|
assert completion.call_options[1]["max_attempts"] == 1
|
|
assert 0 < completion.call_options[1]["timeout_seconds"] <= completion.call_options[0]["timeout_seconds"]
|
|
|
|
|
|
def test_repair_is_skipped_when_the_first_pass_exhausts_the_budget() -> None:
|
|
entry = {"description": "Built a FastAPI service for 300 users."}
|
|
completion = _SequentialCompletion([_output("Supported 300 users.")])
|
|
expander = OpenAIEntryExpander(completion, timeout_seconds=5.0)
|
|
|
|
proposal = expander.expand(entry, context={"entry_type": "project_experience"})
|
|
|
|
assert len(completion.calls) == 1
|
|
assert proposal["optimized_description"].endswith("Supported 300 users.")
|
|
assert proposal["optimized_description"].splitlines()[0].lstrip("\u2022 ").startswith("Supported")
|
|
assert "repair_skipped_budget" in proposal["validation_warnings"]
|
|
|
|
|
|
def test_repair_that_does_not_reduce_hard_omissions_keeps_first_pass() -> None:
|
|
entry = {"description": "Built a FastAPI service for 300 users."}
|
|
completion = _SequentialCompletion([
|
|
_output("Supported 300 users."),
|
|
_output("Supported 300 users."),
|
|
])
|
|
expander = OpenAIEntryExpander(completion)
|
|
|
|
proposal = expander.expand(entry, context={"entry_type": "project_experience"})
|
|
|
|
assert proposal["optimized_description"].endswith("Supported 300 users.")
|
|
assert "repair_rejected_quality_regression" in proposal["validation_warnings"]
|
|
|
|
|
|
def test_repair_that_loses_a_retained_hard_fact_keeps_first_pass() -> None:
|
|
entry = {"description": "Built a FastAPI service for 300 users."}
|
|
completion = _SequentialCompletion([
|
|
_output("Built a FastAPI service."),
|
|
_output("Supported 300 users."),
|
|
])
|
|
expander = OpenAIEntryExpander(completion)
|
|
|
|
proposal = expander.expand(entry, context={"entry_type": "project_experience"})
|
|
|
|
assert proposal["optimized_description"].endswith("Built a FastAPI service.")
|
|
assert "repair_rejected_quality_regression" in proposal["validation_warnings"]
|
|
|
|
|
|
def test_generic_api_term_does_not_trigger_repair() -> None:
|
|
completion = _SequentialCompletion([_output("Developed the service endpoint.")])
|
|
expander = OpenAIEntryExpander(completion)
|
|
|
|
proposal = expander.expand(
|
|
{"description": "Built an API endpoint."},
|
|
context={"entry_type": "project_experience"},
|
|
)
|
|
|
|
assert len(completion.calls) == 1
|
|
assert proposal["uncovered_facts"] == []
|
|
|
|
|
|
def test_build_expander_honors_rule_fallback_setting() -> None:
|
|
settings = Settings(
|
|
llm_provider="openai",
|
|
openai_api_key="test-key-not-a-secret",
|
|
fallback_to_rules=False,
|
|
)
|
|
|
|
expander = build_expander(settings, _SequentialCompletion([]))
|
|
|
|
assert isinstance(expander, OpenAIEntryExpander)
|
|
|
|
|
|
def test_repair_cannot_flatten_a_structured_first_draft() -> None:
|
|
entry = {"description": "Built a FastAPI and Redis service for 300 users."}
|
|
completion = _SequentialCompletion([
|
|
_output("Built a FastAPI service for 300 users.\nDesigned service modules.\nReleased documentation."),
|
|
_output("Built a FastAPI and Redis service for 300 users."),
|
|
])
|
|
expander = OpenAIEntryExpander(completion)
|
|
|
|
proposal = expander.expand(entry, context={"entry_type": "project_experience"})
|
|
|
|
assert len(proposal["optimized_description"].splitlines()) == 3
|
|
assert "repair_rejected_quality_regression" in proposal["validation_warnings"]
|