diff --git a/api/app/doc_render.py b/api/app/doc_render.py index 98544fe..156e934 100644 --- a/api/app/doc_render.py +++ b/api/app/doc_render.py @@ -49,7 +49,6 @@ _MAX_PROJECT_DESC_CHARS = 520 # room for 2-3 sentences (what it is, how it works, a result) _MAX_EDUCATION = 4 _MAX_CERTS = 6 -_MAX_PARAGRAPHS = 4 # Pages a CV must fit: one for a graduate / early-career candidate, two otherwise. _PAGE_TARGET = {"junior": 1, "experienced": 2} @@ -310,7 +309,9 @@ def _letter_contact_lines(contact: dict) -> list[dict]: def build_letter_template_data(letter: dict, fit: tuple[str, ...] = ()) -> dict: """Render-ready letter data. `fit` is a step from _LETTER_FIT_STEPS: layout classes only.""" letter = letter or {} - paragraphs = [p.strip() for p in (letter.get("paragraphs") or []) if p and p.strip()][:_MAX_PARAGRAPHS] + # Every paragraph is kept (the model may write five, the editor can add more); fitting the + # letter to one page is _fitted_html's job, which only ever tightens the layout. + paragraphs = [p.strip() for p in (letter.get("paragraphs") or []) if p and p.strip()] return { "page_target": 1, "body_class": " ".join(fit), diff --git a/api/tests/test_documents.py b/api/tests/test_documents.py index d2e1aef..e4e4560 100644 --- a/api/tests/test_documents.py +++ b/api/tests/test_documents.py @@ -446,13 +446,13 @@ def test_cover_letter_sign_off_follows_the_salutation(llm, salutation, signoff): assert (letter["salutation"], letter["signoff"]) == (salutation, signoff) -def test_cover_letter_renders_four_paragraphs_and_the_contact_block(llm): - paragraphs = _letter_paragraphs() + ["A fifth paragraph the model added."] +def test_cover_letter_renders_every_paragraph_and_the_contact_block(llm): + paragraphs = _letter_paragraphs() + ["A fifth, closing paragraph the model added."] llm.reply = json.dumps({"headline": "Computer Science Graduate", "salutation": "Dear Hiring Manager,", "paragraphs": paragraphs, "signoff": "Yours faithfully,"}) html = doc_render.render_letter_html(_cover_letter()) - assert _letter_body(html) == ["Dear Hiring Manager,", *paragraphs[:4]] + assert _letter_body(html) == ["Dear Hiring Manager,", *paragraphs] # the closing paragraph isn't dropped assert '
Computer Science Graduate
' in html assert ">Bristol." in html and 'href="mailto:sam@example.com"' in html and 'href="tel:+447700900456"' in html assert re.search(r'Yours faithfully,
\s*Sam Okafor
', html) @@ -563,6 +563,19 @@ def test_a_full_length_cover_letter_fits_one_page(pdf_dir): assert _weasy("cover_letter", letter).page_count == 1 +@needs_weasyprint +def test_a_five_paragraph_letter_at_full_length_fits_one_page(pdf_dir): + letter = {"candidate_name": "Sam Okafor", "headline": "Computer Science Graduate", + "contact": {"address_line_1": "1 High Street", "city": "Bristol", "postcode": "BS1 1AA", + "phone": "+44 7700 900456", "email": "sam@example.com"}, + "date": "15 October 2026", "company": "Acme Robotics", "company_location": "Cambridge, UK", + "job_title": "Graduate Software Engineer (2027 start)", + "paragraphs": _letter_paragraphs((60, 75, 120, 70, 55))} # 380 words, the prompt's top end + pdf = _weasy("cover_letter", letter) + assert pdf.page_count == 1 + assert _sentence(55, 4) in " ".join(pdf[0].get_text().split()) # the last paragraph is on the page + + @pytest.mark.parametrize("doc_type, doc, expected", [ ("cv", agent._persona_to_cvdoc(GRADUATE), ["Sam Okafor", "Software Engineering Intern", "Pantry Planner", "BSc (Hons) Computer Science"]),