init
This commit is contained in:
@@ -0,0 +1,186 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from resume_agent.agent import (
|
||||
build_profile,
|
||||
revise_tailored_resume,
|
||||
tailor_resume,
|
||||
validate_package,
|
||||
)
|
||||
from resume_agent.models import (
|
||||
BackedText,
|
||||
CareerProfile,
|
||||
ContactInfo,
|
||||
JobAnalysis,
|
||||
MatchAssessment,
|
||||
ResumeSection,
|
||||
TailoredResume,
|
||||
TailoringPackage,
|
||||
)
|
||||
|
||||
CONTACT = ContactInfo(
|
||||
full_name="Ada Example",
|
||||
email="ada@example.com",
|
||||
phone=None,
|
||||
location="London",
|
||||
linkedin=None,
|
||||
website=None,
|
||||
)
|
||||
|
||||
|
||||
class FakeLLM:
|
||||
def __init__(self, responses: list[Any]) -> None:
|
||||
self.responses = responses
|
||||
self.calls: list[tuple[str, str]] = []
|
||||
|
||||
def parse(self, schema: type[Any], instructions: str, input_text: str) -> Any:
|
||||
self.calls.append((instructions, input_text))
|
||||
response = self.responses.pop(0)
|
||||
assert isinstance(response, schema)
|
||||
return response
|
||||
|
||||
|
||||
def profile() -> CareerProfile:
|
||||
return CareerProfile.model_validate(
|
||||
{
|
||||
"contact": CONTACT.model_dump(),
|
||||
"professional_identity": "Backend engineer",
|
||||
"differentiators": ["Reliable distributed systems"],
|
||||
"target_roles": ["Senior Backend Engineer"],
|
||||
"facts": [
|
||||
{
|
||||
"id": "F001",
|
||||
"category": "experience",
|
||||
"statement": "Reduced API latency by 30%.",
|
||||
"source_name": "resume",
|
||||
"source_excerpt": "Reduced API latency by 30%.",
|
||||
}
|
||||
],
|
||||
"skills": ["Python"],
|
||||
"unanswered_questions": [],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def package(evidence_ids: list[str] | None = None) -> TailoringPackage:
|
||||
item = BackedText(
|
||||
text="Reduced API latency by 30%.",
|
||||
evidence_ids=evidence_ids if evidence_ids is not None else ["F001"],
|
||||
)
|
||||
return TailoringPackage(
|
||||
job=JobAnalysis(
|
||||
company="Example Co",
|
||||
role_title="Backend Engineer",
|
||||
mission=None,
|
||||
requirements=[],
|
||||
responsibilities=[],
|
||||
culture_signals=[],
|
||||
ats_keywords=["Python"],
|
||||
),
|
||||
resume=TailoredResume(
|
||||
contact=CONTACT,
|
||||
headline="Backend Engineer",
|
||||
summary=[item],
|
||||
sections=[ResumeSection(title="Experience", items=[item])],
|
||||
),
|
||||
match=MatchAssessment(
|
||||
strong_matches=["API performance"],
|
||||
partial_matches=[],
|
||||
genuine_gaps=[],
|
||||
keywords_used=["Python"],
|
||||
),
|
||||
changes_made=["Prioritized relevant impact."],
|
||||
questions_for_candidate=[],
|
||||
warnings=[],
|
||||
)
|
||||
|
||||
|
||||
def test_build_profile_uses_structured_result() -> None:
|
||||
expected = profile()
|
||||
assert build_profile(FakeLLM([expected]), "resume text") == expected
|
||||
|
||||
|
||||
def test_build_profile_repairs_an_empty_fact_ledger() -> None:
|
||||
empty = profile().model_copy(update={"facts": []})
|
||||
expected = profile()
|
||||
|
||||
assert build_profile(FakeLLM([empty, expected]), "resume text") == expected
|
||||
|
||||
|
||||
def test_build_profile_reports_empty_fact_ledger_after_repair() -> None:
|
||||
empty = profile().model_copy(update={"facts": []})
|
||||
|
||||
with pytest.raises(ValueError, match="after two attempts"):
|
||||
build_profile(FakeLLM([empty, empty]), "resume text")
|
||||
|
||||
|
||||
def test_tailor_runs_draft_and_audit() -> None:
|
||||
expected = package()
|
||||
result = tailor_resume(FakeLLM([expected, expected]), profile(), "long job post")
|
||||
assert result == expected
|
||||
|
||||
|
||||
def test_tailoring_strength_controls_rewriting_without_relaxing_evidence() -> None:
|
||||
expected = package()
|
||||
llm = FakeLLM([expected, expected])
|
||||
|
||||
tailor_resume(llm, profile(), "long job post", tailoring_strength=100)
|
||||
|
||||
draft_instructions, draft_input = llm.calls[0]
|
||||
assert "Tailoring strength: 100/100" in draft_instructions
|
||||
assert "never permits fabricated" in draft_instructions
|
||||
assert '"tailoring_strength": 100' in draft_input
|
||||
|
||||
|
||||
def test_tailoring_strength_rejects_out_of_range_values() -> None:
|
||||
with pytest.raises(ValueError, match="between 0 and 100"):
|
||||
tailor_resume(FakeLLM([]), profile(), "long job post", tailoring_strength=101)
|
||||
|
||||
|
||||
def test_revision_chat_rewrites_and_audits_current_package() -> None:
|
||||
current = package()
|
||||
revised = package()
|
||||
revised.changes_made = ["Made experience bullets more concise."]
|
||||
llm = FakeLLM([revised, revised])
|
||||
|
||||
result = revise_tailored_resume(
|
||||
llm,
|
||||
profile(),
|
||||
current,
|
||||
"Make the experience bullets more concise.",
|
||||
)
|
||||
|
||||
assert result.changes_made == ["Made experience bullets more concise."]
|
||||
assert "candidate_request" in llm.calls[0][1]
|
||||
assert "only source of candidate facts" in llm.calls[0][0]
|
||||
|
||||
|
||||
def test_revision_chat_cannot_change_job_analysis() -> None:
|
||||
current = package()
|
||||
revised = package()
|
||||
revised.job.role_title = "Different role"
|
||||
|
||||
with pytest.raises(ValueError, match="cannot change"):
|
||||
revise_tailored_resume(FakeLLM([revised]), profile(), current, "Rewrite it.")
|
||||
|
||||
|
||||
def test_unknown_evidence_is_rejected() -> None:
|
||||
with pytest.raises(ValueError, match="unknown evidence"):
|
||||
validate_package(profile(), package(["F999"]))
|
||||
|
||||
|
||||
def test_contact_mutation_is_rejected() -> None:
|
||||
result = package()
|
||||
result.resume.contact = ContactInfo(
|
||||
full_name="Someone Else",
|
||||
email="ada@example.com",
|
||||
phone=None,
|
||||
location="London",
|
||||
linkedin=None,
|
||||
website=None,
|
||||
)
|
||||
with pytest.raises(ValueError, match="contact"):
|
||||
validate_package(profile(), result)
|
||||
@@ -0,0 +1,37 @@
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from resume_agent.documents import DocumentError, read_document, read_document_bytes
|
||||
from resume_agent.web import html_to_text
|
||||
|
||||
|
||||
def test_reads_plain_text(tmp_path: Path) -> None:
|
||||
resume = tmp_path / "resume.md"
|
||||
resume.write_text("# Ada\nBuilt reliable systems.", encoding="utf-8")
|
||||
assert "Built reliable systems" in read_document(resume)
|
||||
|
||||
|
||||
def test_rejects_unknown_document_type(tmp_path: Path) -> None:
|
||||
resume = tmp_path / "resume.exe"
|
||||
resume.write_text("not a resume", encoding="utf-8")
|
||||
with pytest.raises(DocumentError, match="Supported resume formats"):
|
||||
read_document(resume)
|
||||
|
||||
|
||||
def test_reads_uploaded_text_bytes() -> None:
|
||||
assert read_document_bytes("resume.txt", b"Built reliable systems.") == (
|
||||
"Built reliable systems."
|
||||
)
|
||||
|
||||
|
||||
def test_rejects_empty_uploaded_text() -> None:
|
||||
with pytest.raises(DocumentError, match="No readable text"):
|
||||
read_document_bytes("resume.md", b"")
|
||||
|
||||
|
||||
def test_html_to_text_removes_scripts() -> None:
|
||||
text = html_to_text("<h1>Engineer</h1><script>malicious()</script><p>Build APIs</p>")
|
||||
assert "Engineer" in text
|
||||
assert "Build APIs" in text
|
||||
assert "malicious" not in text
|
||||
@@ -0,0 +1,123 @@
|
||||
from types import SimpleNamespace
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
from pydantic import BaseModel
|
||||
|
||||
from resume_agent.llm import LLMError, OpenAILLM
|
||||
|
||||
|
||||
class LoggedResult(BaseModel):
|
||||
answer: str
|
||||
|
||||
|
||||
def fake_client(parsed: BaseModel) -> Any:
|
||||
class Completions:
|
||||
def parse(self, **kwargs: Any) -> Any:
|
||||
return SimpleNamespace(
|
||||
choices=[SimpleNamespace(message=SimpleNamespace(parsed=parsed))]
|
||||
)
|
||||
|
||||
return SimpleNamespace(
|
||||
base_url="https://llm.example/v1/",
|
||||
chat=SimpleNamespace(completions=Completions()),
|
||||
)
|
||||
|
||||
|
||||
def test_custom_endpoint_defaults_to_chat(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setenv("RESUME_AGENT_API_KEY", "test-token")
|
||||
monkeypatch.setenv("RESUME_AGENT_BASE_URL", "https://llm.example/v1")
|
||||
monkeypatch.setenv("RESUME_AGENT_MODEL", "example-model")
|
||||
monkeypatch.delenv("RESUME_AGENT_API_STYLE", raising=False)
|
||||
monkeypatch.delenv("RESUME_AGENT_LLM_TIMEOUT_SECONDS", raising=False)
|
||||
|
||||
llm = OpenAILLM()
|
||||
|
||||
assert llm.model == "example-model"
|
||||
assert llm.api_style == "chat"
|
||||
assert llm.timeout_seconds == 1800
|
||||
assert str(llm.client.base_url) == "https://llm.example/v1/"
|
||||
|
||||
|
||||
def test_custom_endpoint_allows_model_discovery(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setenv("RESUME_AGENT_API_KEY", "test-token")
|
||||
monkeypatch.setenv("RESUME_AGENT_BASE_URL", "https://llm.example/v1")
|
||||
monkeypatch.delenv("RESUME_AGENT_MODEL", raising=False)
|
||||
|
||||
llm = OpenAILLM()
|
||||
|
||||
assert llm.model is None
|
||||
|
||||
|
||||
def test_parse_requires_model(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setenv("RESUME_AGENT_API_KEY", "test-token")
|
||||
monkeypatch.setenv("RESUME_AGENT_BASE_URL", "https://llm.example/v1")
|
||||
monkeypatch.delenv("RESUME_AGENT_MODEL", raising=False)
|
||||
|
||||
with pytest.raises(LLMError, match="RESUME_AGENT_MODEL"):
|
||||
OpenAILLM().parse(dict, "instructions", "input") # type: ignore[type-var]
|
||||
|
||||
|
||||
def test_custom_llm_timeout(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setenv("RESUME_AGENT_API_KEY", "test-token")
|
||||
monkeypatch.setenv("RESUME_AGENT_BASE_URL", "https://llm.example/v1")
|
||||
monkeypatch.setenv("RESUME_AGENT_MODEL", "example-model")
|
||||
monkeypatch.setenv("RESUME_AGENT_LLM_TIMEOUT_SECONDS", "2700")
|
||||
|
||||
llm = OpenAILLM()
|
||||
|
||||
assert llm.timeout_seconds == 2700
|
||||
assert llm.client.timeout.read == 2700
|
||||
assert llm.client.timeout.connect == 30
|
||||
|
||||
|
||||
def test_llm_timeout_must_be_positive(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setenv("RESUME_AGENT_API_KEY", "test-token")
|
||||
monkeypatch.setenv("RESUME_AGENT_BASE_URL", "https://llm.example/v1")
|
||||
monkeypatch.setenv("RESUME_AGENT_MODEL", "example-model")
|
||||
monkeypatch.setenv("RESUME_AGENT_LLM_TIMEOUT_SECONDS", "0")
|
||||
|
||||
with pytest.raises(LLMError, match="greater than zero"):
|
||||
OpenAILLM()
|
||||
|
||||
|
||||
def test_model_queries_log_payloads_to_stdout(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
monkeypatch.setenv("RESUME_AGENT_API_KEY", "test-token")
|
||||
monkeypatch.setenv("RESUME_AGENT_BASE_URL", "https://llm.example/v1")
|
||||
monkeypatch.setenv("RESUME_AGENT_MODEL", "example-model")
|
||||
monkeypatch.setenv("RESUME_AGENT_LOG_MODEL_PAYLOADS", "true")
|
||||
llm = OpenAILLM()
|
||||
llm.client = fake_client(LoggedResult(answer="done"))
|
||||
|
||||
with caplog.at_level("INFO", logger="resume_agent.model"):
|
||||
llm.parse(LoggedResult, "System logging test", "Private resume input")
|
||||
|
||||
output = caplog.text
|
||||
assert "event=start" in output
|
||||
assert "System logging test" in output
|
||||
assert "Private resume input" in output
|
||||
assert '"answer": "done"' in output
|
||||
assert "test-token" not in output
|
||||
|
||||
|
||||
def test_model_payload_logging_can_be_disabled(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
monkeypatch.setenv("RESUME_AGENT_API_KEY", "test-token")
|
||||
monkeypatch.setenv("RESUME_AGENT_BASE_URL", "https://llm.example/v1")
|
||||
monkeypatch.setenv("RESUME_AGENT_MODEL", "example-model")
|
||||
monkeypatch.setenv("RESUME_AGENT_LOG_MODEL_PAYLOADS", "false")
|
||||
llm = OpenAILLM()
|
||||
llm.client = fake_client(LoggedResult(answer="done"))
|
||||
|
||||
with caplog.at_level("INFO", logger="resume_agent.model"):
|
||||
llm.parse(LoggedResult, "Hidden system prompt", "Hidden resume input")
|
||||
|
||||
output = caplog.text
|
||||
assert "event=start" in output
|
||||
assert "Hidden system prompt" not in output
|
||||
assert "Hidden resume input" not in output
|
||||
@@ -0,0 +1,372 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from resume_agent.agent import save_json
|
||||
from resume_agent.models import CareerProfile, TailoringPackage
|
||||
from resume_agent.render import render_ohmycv_resume
|
||||
from resume_agent.webapp import create_app
|
||||
|
||||
|
||||
def sample_profile() -> CareerProfile:
|
||||
return CareerProfile.model_validate(
|
||||
{
|
||||
"contact": {
|
||||
"full_name": "Ada Example",
|
||||
"email": "ada@example.com",
|
||||
"phone": None,
|
||||
"location": "London",
|
||||
"linkedin": None,
|
||||
"website": None,
|
||||
},
|
||||
"professional_identity": "Backend engineer",
|
||||
"differentiators": ["Reliable distributed systems"],
|
||||
"target_roles": ["Senior Backend Engineer"],
|
||||
"facts": [
|
||||
{
|
||||
"id": "F001",
|
||||
"category": "experience",
|
||||
"statement": "Reduced API latency by 30%.",
|
||||
"source_name": "resume",
|
||||
"source_excerpt": "Reduced API latency by 30%.",
|
||||
}
|
||||
],
|
||||
"skills": ["Python"],
|
||||
"unanswered_questions": [],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def sample_package() -> TailoringPackage:
|
||||
contact = sample_profile().contact.model_dump()
|
||||
item = {"text": "Reduced API latency by 30%. [F013]", "evidence_ids": ["F001"]}
|
||||
return TailoringPackage.model_validate(
|
||||
{
|
||||
"job": {
|
||||
"company": "Example Co",
|
||||
"role_title": "Backend Engineer",
|
||||
"mission": None,
|
||||
"requirements": [],
|
||||
"responsibilities": [],
|
||||
"culture_signals": [],
|
||||
"ats_keywords": ["Python"],
|
||||
},
|
||||
"resume": {
|
||||
"contact": contact,
|
||||
"headline": "Backend Engineer",
|
||||
"summary": [item],
|
||||
"sections": [{"title": "Experience", "items": [item]}],
|
||||
},
|
||||
"match": {
|
||||
"strong_matches": ["API performance"],
|
||||
"partial_matches": [],
|
||||
"genuine_gaps": [],
|
||||
"keywords_used": ["Python"],
|
||||
},
|
||||
"changes_made": ["Prioritized relevant impact."],
|
||||
"questions_for_candidate": [],
|
||||
"warnings": [],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class FakeLLM:
|
||||
def __init__(self, responses: list[Any]) -> None:
|
||||
self.responses = responses
|
||||
|
||||
def parse(self, schema: type[Any], instructions: str, input_text: str) -> Any:
|
||||
response = self.responses.pop(0)
|
||||
assert isinstance(response, schema)
|
||||
return response
|
||||
|
||||
def list_models(self) -> list[str]:
|
||||
return ["test-model"]
|
||||
|
||||
|
||||
def wait_for_task(client: TestClient, task_id: str) -> dict[str, Any]:
|
||||
for _ in range(100):
|
||||
response = client.get(f"/api/tasks/{task_id}")
|
||||
assert response.status_code == 200
|
||||
task = response.json()
|
||||
if task["status"] != "running":
|
||||
return task
|
||||
time.sleep(0.01)
|
||||
raise AssertionError("Background task did not finish in time.")
|
||||
|
||||
|
||||
def test_root_and_status(tmp_path: Path, monkeypatch: Any) -> None:
|
||||
monkeypatch.delenv("RESUME_AGENT_API_KEY", raising=False)
|
||||
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
|
||||
app = create_app(
|
||||
profile_path=tmp_path / "profile.json",
|
||||
output_dir=tmp_path / "output",
|
||||
llm_factory=lambda: FakeLLM([]), # type: ignore[arg-type]
|
||||
)
|
||||
|
||||
with TestClient(app) as client:
|
||||
assert client.get("/").status_code == 200
|
||||
favicon = client.get("/favicon.ico")
|
||||
assert favicon.status_code == 200
|
||||
assert favicon.headers["content-type"] == "image/x-icon"
|
||||
status = client.get("/api/status").json()
|
||||
assert status["configured"] is False
|
||||
assert status["profile_exists"] is False
|
||||
|
||||
|
||||
def test_build_profile_from_upload(tmp_path: Path) -> None:
|
||||
expected = sample_profile()
|
||||
app = create_app(
|
||||
profile_path=tmp_path / "profile.json",
|
||||
output_dir=tmp_path / "output",
|
||||
llm_factory=lambda: FakeLLM([expected]), # type: ignore[arg-type]
|
||||
)
|
||||
|
||||
with TestClient(app) as client:
|
||||
response = client.post(
|
||||
"/api/profile",
|
||||
files={"resume": ("resume.md", b"# Ada\nReduced API latency by 30%.")},
|
||||
data={"about": ""},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["facts"][0]["id"] == "F001"
|
||||
assert (tmp_path / "profile.json").is_file()
|
||||
|
||||
|
||||
def test_background_profile_upload_returns_pollable_task(tmp_path: Path) -> None:
|
||||
expected = sample_profile()
|
||||
app = create_app(
|
||||
profile_path=tmp_path / "profile.json",
|
||||
output_dir=tmp_path / "output",
|
||||
llm_factory=lambda: FakeLLM([expected]), # type: ignore[arg-type]
|
||||
)
|
||||
|
||||
with TestClient(app) as client:
|
||||
started = client.post(
|
||||
"/api/profile/start",
|
||||
files={"resume": ("resume.md", b"# Ada\nReduced API latency by 30%.")},
|
||||
data={"about": ""},
|
||||
)
|
||||
task = wait_for_task(client, started.json()["task_id"])
|
||||
|
||||
assert started.status_code == 200
|
||||
assert task["status"] == "succeeded"
|
||||
assert task["result"]["facts"][0]["id"] == "F001"
|
||||
|
||||
|
||||
def test_tailor_creates_downloads(tmp_path: Path) -> None:
|
||||
profile_path = tmp_path / "profile.json"
|
||||
output_dir = tmp_path / "output"
|
||||
save_json(sample_profile(), profile_path)
|
||||
expected = sample_package()
|
||||
app = create_app(
|
||||
profile_path=profile_path,
|
||||
output_dir=output_dir,
|
||||
llm_factory=lambda: FakeLLM([expected, expected]), # type: ignore[arg-type]
|
||||
)
|
||||
|
||||
with TestClient(app) as client:
|
||||
response = client.post(
|
||||
"/api/tailor",
|
||||
json={
|
||||
"job_url": None,
|
||||
"job_text": "Backend engineer role requiring Python and API performance. " * 3,
|
||||
},
|
||||
)
|
||||
download = client.get("/api/download/resume.md")
|
||||
ohmycv_download = client.get("/api/download/resume-ohmycv.md")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["job"]["company"] == "Example Co"
|
||||
assert download.status_code == 200
|
||||
assert "Ada Example" in download.text
|
||||
assert "[F013]" not in download.text
|
||||
assert ohmycv_download.status_code == 200
|
||||
assert ohmycv_download.text.startswith('---\nname: "Ada Example"')
|
||||
assert "[F013]" not in ohmycv_download.text
|
||||
|
||||
|
||||
def test_background_tailoring_returns_pollable_task(tmp_path: Path) -> None:
|
||||
profile_path = tmp_path / "profile.json"
|
||||
output_dir = tmp_path / "output"
|
||||
save_json(sample_profile(), profile_path)
|
||||
expected = sample_package()
|
||||
app = create_app(
|
||||
profile_path=profile_path,
|
||||
output_dir=output_dir,
|
||||
llm_factory=lambda: FakeLLM([expected, expected]), # type: ignore[arg-type]
|
||||
)
|
||||
|
||||
with TestClient(app) as client:
|
||||
started = client.post(
|
||||
"/api/tailor/start",
|
||||
json={
|
||||
"job_url": None,
|
||||
"job_text": "Backend engineer role requiring Python and API performance. " * 3,
|
||||
},
|
||||
)
|
||||
task = wait_for_task(client, started.json()["task_id"])
|
||||
|
||||
assert started.status_code == 200
|
||||
assert task["status"] == "succeeded"
|
||||
assert task["result"]["job"]["company"] == "Example Co"
|
||||
assert (output_dir / "resume-ohmycv.md").is_file()
|
||||
|
||||
|
||||
def test_tailor_requires_exactly_one_job_source(tmp_path: Path) -> None:
|
||||
profile_path = tmp_path / "profile.json"
|
||||
save_json(sample_profile(), profile_path)
|
||||
app = create_app(
|
||||
profile_path=profile_path,
|
||||
output_dir=tmp_path / "output",
|
||||
llm_factory=lambda: FakeLLM([]), # type: ignore[arg-type]
|
||||
)
|
||||
|
||||
with TestClient(app) as client:
|
||||
response = client.post(
|
||||
"/api/tailor",
|
||||
json={"job_url": "https://example.com/job", "job_text": "Also pasted"},
|
||||
)
|
||||
|
||||
assert response.status_code == 422
|
||||
|
||||
|
||||
def test_tailor_rejects_out_of_range_strength(tmp_path: Path) -> None:
|
||||
profile_path = tmp_path / "profile.json"
|
||||
save_json(sample_profile(), profile_path)
|
||||
app = create_app(
|
||||
profile_path=profile_path,
|
||||
output_dir=tmp_path / "output",
|
||||
llm_factory=lambda: FakeLLM([]), # type: ignore[arg-type]
|
||||
)
|
||||
|
||||
with TestClient(app) as client:
|
||||
response = client.post(
|
||||
"/api/tailor",
|
||||
json={
|
||||
"job_url": None,
|
||||
"job_text": "Backend engineer role requiring Python. " * 3,
|
||||
"tailoring_strength": 101,
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 422
|
||||
|
||||
|
||||
def test_revision_chat_updates_tailored_outputs(tmp_path: Path) -> None:
|
||||
profile_path = tmp_path / "profile.json"
|
||||
output_dir = tmp_path / "output"
|
||||
current = sample_package()
|
||||
revised = sample_package()
|
||||
revised.changes_made = ["Made the summary more direct."]
|
||||
save_json(sample_profile(), profile_path)
|
||||
save_json(current, output_dir / "tailoring.json")
|
||||
app = create_app(
|
||||
profile_path=profile_path,
|
||||
output_dir=output_dir,
|
||||
llm_factory=lambda: FakeLLM([revised, revised]), # type: ignore[arg-type]
|
||||
)
|
||||
|
||||
with TestClient(app) as client:
|
||||
response = client.post(
|
||||
"/api/revise",
|
||||
json={"message": "Make the summary more direct."},
|
||||
)
|
||||
download = client.get("/api/download/resume-ohmycv.md")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["package"]["changes_made"] == [
|
||||
"Made the summary more direct."
|
||||
]
|
||||
assert "Updated and re-audited" in response.json()["reply"]
|
||||
assert download.status_code == 200
|
||||
assert "[F013]" not in download.text
|
||||
|
||||
|
||||
def test_revision_chat_requires_a_tailored_resume(tmp_path: Path) -> None:
|
||||
app = create_app(
|
||||
profile_path=tmp_path / "profile.json",
|
||||
output_dir=tmp_path / "output",
|
||||
llm_factory=lambda: FakeLLM([]), # type: ignore[arg-type]
|
||||
)
|
||||
|
||||
with TestClient(app) as client:
|
||||
response = client.post("/api/revise", json={"message": "Make it shorter."})
|
||||
|
||||
assert response.status_code == 409
|
||||
|
||||
|
||||
def test_markdown_tailor_preserves_ohmycv_front_matter(tmp_path: Path) -> None:
|
||||
expected_profile = sample_profile()
|
||||
expected_package = sample_package()
|
||||
app = create_app(
|
||||
profile_path=tmp_path / "profile.json",
|
||||
output_dir=tmp_path / "output",
|
||||
cv_dist=tmp_path / "missing-cv",
|
||||
llm_factory=lambda: FakeLLM([expected_profile, expected_package, expected_package]), # type: ignore[arg-type]
|
||||
)
|
||||
source = """---
|
||||
name: Ada Example
|
||||
header:
|
||||
- text: ada@example.com
|
||||
---
|
||||
|
||||
## Experience
|
||||
|
||||
- Reduced API latency by 30%.
|
||||
"""
|
||||
|
||||
with TestClient(app) as client:
|
||||
response = client.post(
|
||||
"/api/markdown/tailor",
|
||||
json={
|
||||
"markdown": source,
|
||||
"about": "",
|
||||
"job_url": None,
|
||||
"job_text": "Backend engineer role requiring Python and API performance. " * 3,
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
markdown = response.json()["markdown"]
|
||||
assert markdown.startswith("---\nname: Ada Example\nheader:")
|
||||
assert "## Professional Summary\n\nReduced API latency" in markdown
|
||||
assert "Reduced API latency by 30%." in markdown
|
||||
assert "[F013]" not in markdown
|
||||
assert (tmp_path / "output/resume-ohmycv.md").read_text() == markdown
|
||||
|
||||
|
||||
def test_ohmycv_render_creates_native_header_when_source_has_none() -> None:
|
||||
markdown = render_ohmycv_resume(sample_package(), "# Old resume")
|
||||
|
||||
assert markdown.startswith('---\nname: "Ada Example"\nheader:')
|
||||
assert 'data-icon=\\"tabler:mail\\"' in markdown
|
||||
assert 'link: "mailto:ada@example.com"' in markdown
|
||||
assert "[F013]" not in markdown
|
||||
|
||||
|
||||
def test_status_reports_built_cv_editor(tmp_path: Path) -> None:
|
||||
cv_dist = tmp_path / "cv"
|
||||
cv_dist.mkdir()
|
||||
(cv_dist / "index.html").write_text("<h1>Oh My CV</h1>", encoding="utf-8")
|
||||
(cv_dist / "200.html").write_text("<h1>Editor fallback</h1>", encoding="utf-8")
|
||||
app = create_app(
|
||||
profile_path=tmp_path / "profile.json",
|
||||
output_dir=tmp_path / "output",
|
||||
cv_dist=cv_dist,
|
||||
llm_factory=lambda: FakeLLM([]), # type: ignore[arg-type]
|
||||
)
|
||||
|
||||
with TestClient(app) as client:
|
||||
assert client.get("/api/status").json()["editor_available"] is True
|
||||
editor = client.get("/cv/")
|
||||
dynamic_editor = client.get("/cv/editor/42")
|
||||
|
||||
assert editor.status_code == 200
|
||||
assert "Oh My CV" in editor.text
|
||||
assert dynamic_editor.status_code == 200
|
||||
assert "Editor fallback" in dynamic_editor.text
|
||||
Reference in New Issue
Block a user