File size: 6,145 Bytes
fd65907
 
c425de3
 
 
 
 
 
 
fd65907
 
 
 
 
 
c425de3
 
 
 
 
fd65907
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65ea09d
 
e2b8b62
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65ea09d
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
"""Phase 9 (R22): structured keyword placement into the hardcoded resume.

RETIRED — this validated the mechanical keyword-cycling injector (25-30 terms
crammed per bullet). That behaviour was removed from every V1 résumé path in the
evidence-gating correction: V1 now preserves the résumé and inserts NOTHING
without résumé evidence (see tests/test_ats_safety.py). The injector functions
still exist only as internal V2 fallbacks, so these count-based assertions no
longer describe supported behaviour. Skipped rather than deleted to preserve
history and the rationale.
"""
import os
import re

import pytest

pytestmark = pytest.mark.skip(
    reason="mechanical keyword-cycling injection retired; V1 is now evidence-gated "
           "(see test_ats_safety.py). No keyword is inserted without résumé evidence."
)

from src.latex_resume import inject_keywords, place_keywords_structured


def _resume() -> str:
    try:
        from src.default_resume import DEFAULT_RESUME_LATEX
        if DEFAULT_RESUME_LATEX.strip():
            return DEFAULT_RESUME_LATEX
    except Exception:
        pass
    p = os.path.join(
        ".planning", "phases", "09-hardcoded-resume-keyword-placement",
        "resume-source.tex",
    )
    if os.path.exists(p):
        with open(p, encoding="utf-8") as f:
            txt = f.read()
        if txt.strip():
            return txt
    pytest.skip("resume source not available")


KWS = [f"kw{i:03d}" for i in range(120)]


def _items_in(block: str):
    """Return the list of tagged-item term-strings found in `block`."""
    return re.findall(r"\\resumeItem\{([^\n]*?)\}\s*% ats-item", block)


def test_summary_slot_15_to_20():
    out, _ = inject_keywords(_resume(), KWS)
    m = re.search(r"Additional areas: (.+?)\.\s*% ===== end", out, re.S)
    assert m, "summary injection block missing"
    n = len([t for t in m.group(1).split(",") if t.strip()])
    assert 15 <= n <= 20, f"summary has {n} terms (want 15-20)"


def test_byjus_psm_25_to_30():
    out, _ = inject_keywords(_resume(), KWS)
    # The tagged item inside the Asst PSM block.
    seg = out[out.find("Asst. Product Success Manager"):]
    seg = seg[: seg.find("\\resumeItemListEnd") + 20]
    items = _items_in(seg)
    assert items, "no tagged item in PSM block"
    n = len([t for t in items[-1].split(",") if t.strip()])
    assert 25 <= n <= 30, f"PSM has {n} terms (want 25-30)"


def test_byjus_ps_25_to_30():
    out, _ = inject_keywords(_resume(), KWS)
    seg = out[out.find("Product Specialist -- User Experience"):]
    seg = seg[: seg.find("\\resumeItemListEnd") + 20]
    items = _items_in(seg)
    assert items, "no tagged item in Product Specialist block"
    n = len([t for t in items[-1].split(",") if t.strip()])
    assert 25 <= n <= 30, f"PS has {n} terms (want 25-30)"


def test_ml_edutech_8_to_12():
    out, _ = inject_keywords(_resume(), KWS)
    seg = out[out.find("ML Edutech"):]
    seg = seg[: seg.find("\\resumeItemListEnd") + 20]
    items = _items_in(seg)
    assert items, "no tagged item in ML Edutech block"
    n = len([t for t in items[-1].split(",") if t.strip()])
    assert 8 <= n <= 12, f"ML Edutech has {n} terms (want 8-12)"


def test_skills_other_row_present():
    out, _ = inject_keywords(_resume(), KWS)
    m = re.search(r"\\textbf\{Other\}\{: (.+?)\}\s*% ats-skills-other", out)
    assert m, "Skills Other row missing"
    n = len([t for t in m.group(1).split(",") if t.strip()])
    assert n >= 15, f"Skills Other has {n} terms (want >=15 once filled)"


def test_anchor_before_list_end():
    out, _ = inject_keywords(_resume(), KWS)
    assert re.search(r"% ats-item[^\n]*\n\s*\\resumeItemListEnd", out), \
        "tagged item not anchored immediately before a \\resumeItemListEnd"


def test_append_only_original_bullets_intact():
    src = _resume()
    out, _ = inject_keywords(src, KWS)
    for original in [
        "Increased user retention by 8",
        "Managed",
        "Launched an EdTech app portfolio",
        "Led end-to-end revamp of the",
    ]:
        assert original in out, f"original bullet text lost: {original!r}"


def test_idempotent():
    src = _resume()
    o1, _ = inject_keywords(src, KWS)
    o2, _ = inject_keywords(o1, KWS)
    assert o1 == o2, "placement is not idempotent (injected lines stacked)"


def test_dedupe_skips_present_terms():
    # A term already in the resume must not be re-added.
    out, placed = place_keywords_structured(_resume(), ["Roadmap Planning", "kwX1", "kwX2"])
    assert "Roadmap Planning" not in placed
    assert "kwX1" in placed


def test_tectonic_asset_matches_sanitizer():
    """assets/default_resume.tectonic.tex (compiled in the Docker warmup) must equal
    _sanitize_for_tectonic(default) so the warmed packages match the runtime compile."""
    p = os.path.join("assets", "default_resume.tectonic.tex")
    if not os.path.exists(p):
        pytest.skip("assets/default_resume.tectonic.tex not present")
    from src.default_resume import DEFAULT_RESUME_LATEX
    from src.latex_resume import _sanitize_for_tectonic
    with open(p, encoding="utf-8") as f:
        asset = f.read()
    assert asset.strip() == _sanitize_for_tectonic(DEFAULT_RESUME_LATEX).strip(), \
        "assets/default_resume.tectonic.tex drifted from _sanitize_for_tectonic(default)"
    # And the sanitized asset must NOT contain the crash sources.
    assert "fontawesome5" not in asset and "{FiraMono}" not in asset
    assert "\\faPhone" not in asset and "\\faRupeeSign" not in asset


def test_assets_tex_matches_python_source():
    """assets/default_resume.tex (used for the Docker Tectonic warmup) must stay
    byte-identical to the Python default so the warmed packages match what users
    actually compile."""
    p = os.path.join("assets", "default_resume.tex")
    if not os.path.exists(p):
        pytest.skip("assets/default_resume.tex not present")
    from src.default_resume import DEFAULT_RESUME_LATEX
    with open(p, encoding="utf-8") as f:
        asset = f.read()
    assert asset.strip() == DEFAULT_RESUME_LATEX.strip(), \
        "assets/default_resume.tex drifted from src/default_resume.py"