File size: 7,972 Bytes
28c12aa
 
 
 
 
 
7d4eb85
 
28c12aa
 
 
 
 
 
 
 
 
7d4eb85
28c12aa
 
 
 
 
7d4eb85
28c12aa
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7d4eb85
 
28c12aa
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
700e896
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28c12aa
 
 
 
 
 
 
 
 
cd5f9d4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
160
161
162
163
164
165
166
167
168
169
170
171
"""Step 02 sub-phase 1 contract β€” load coverage, no-drop, sentinel preservation."""

from __future__ import annotations

from pathlib import Path

import orjson

from preprocess.load import UNKNOWN_SENTINEL, load_candidates

_SCHEMA_PATH = Path(__file__).resolve().parents[1] / "data" / "candidate_schema.json"

# The five document arrays are kept structured (not hoisted); every other schema leaf becomes a column.
_ARRAY_FIELDS = ["career_history", "education", "skills", "certifications", "languages"]


def _write_jsonl(path: Path, records: list[dict]) -> Path:
    path.write_bytes(b"\n".join(orjson.dumps(r) for r in records))
    return path


def test_flatten_covers_every_schema_field(tmp_path: Path) -> None:
    """Adding a field to candidate_schema.json must force a new column β€” this guards that contract."""
    schema = orjson.loads(_SCHEMA_PATH.read_bytes())
    frame = load_candidates(_write_jsonl(tmp_path / "c.jsonl", [{"candidate_id": "CAND_0000001"}]))
    columns = set(frame.columns)

    assert "candidate_id" in columns
    for array_field in _ARRAY_FIELDS:
        assert array_field in columns, f"array field {array_field} missing"
    for profile_field in schema["properties"]["profile"]["properties"]:
        assert profile_field in columns, f"profile.{profile_field} not covered"
    for signal_field in schema["properties"]["redrob_signals"]["properties"]:
        if signal_field == "expected_salary_range_inr_lpa":
            assert {"expected_salary_min", "expected_salary_max"} <= columns
        else:
            assert signal_field in columns, f"redrob_signals.{signal_field} not covered"


def test_blank_and_malformed_lines_are_skipped_not_dropped(tmp_path: Path) -> None:
    """Two valid records survive; a blank line and a malformed line are skipped, never abort the load."""
    valid = [{"candidate_id": "CAND_0000001"}, {"candidate_id": "CAND_0000002"}]
    path = tmp_path / "c.jsonl"
    lines = [orjson.dumps(valid[0]), b"", b"{not valid json", orjson.dumps(valid[1])]
    path.write_bytes(b"\n".join(lines))
    frame = load_candidates(path)
    assert frame.height == 2
    assert frame["candidate_id"].to_list() == ["CAND_0000001", "CAND_0000002"]


def test_unknown_sentinel_is_preserved(tmp_path: Path) -> None:
    """github_activity_score / offer_acceptance_rate keep their -1 'unknown' marker verbatim on load."""
    record = {
        "candidate_id": "CAND_0000001",
        "redrob_signals": {"github_activity_score": -1, "offer_acceptance_rate": -1},
    }
    frame = load_candidates(_write_jsonl(tmp_path / "c.jsonl", [record]))
    assert frame["github_activity_score"][0] == UNKNOWN_SENTINEL
    assert frame["offer_acceptance_rate"][0] == UNKNOWN_SENTINEL


def test_derived_flat_lists_mirror_the_nested_arrays(tmp_path: Path) -> None:
    """Every nested array gets a name-only flat_* projection; assessment map β†’ verified_skills + {name,score}."""
    record = {
        "candidate_id": "CAND_0000001",
        "career_history": [
            {"title": "AI Engineer", "industry": "Tech", "company_size": "51-200"},
            {"title": "ML Engineer", "industry": "Fintech", "company_size": "11-50"},
        ],
        "education": [{"institution": "IIT", "degree": "BTech", "tier": "tier_1"}],
        "skills": [{"name": "Python"}, {"name": "PyTorch"}],
        "languages": [{"language": "English"}, {"language": "Hindi"}],
        "certifications": [{"name": "AWS SAA"}],
        "redrob_signals": {"skill_assessment_scores": {"Python": 90.0}},
    }
    row = load_candidates(_write_jsonl(tmp_path / "c.jsonl", [record])).row(0, named=True)
    assert row["flat_job_titles"] == ["AI Engineer", "ML Engineer"]
    assert row["flat_career_industries"] == ["Tech", "Fintech"]
    assert row["flat_company_sizes"] == ["51-200", "11-50"]
    assert row["flat_institutions"] == ["IIT"]
    assert row["flat_institution_tiers"] == ["tier_1"]
    assert row["flat_degrees"] == ["BTech"]
    assert row["flat_skills"] == ["Python", "PyTorch"]
    assert row["flat_languages"] == ["English", "Hindi"]
    assert row["flat_certifications"] == ["AWS SAA"]
    assert row["verified_skills"] == ["Python"]
    assert row["skill_assessment_scores"] == [{"name": "Python", "score": 90.0}]


def test_salary_range_is_unnested(tmp_path: Path) -> None:
    """expected_salary_range_inr_lpa.{min,max} flatten to two scalar columns."""
    record = {
        "candidate_id": "CAND_0000001",
        "redrob_signals": {"expected_salary_range_inr_lpa": {"min": 18.0, "max": 32.0}},
    }
    frame = load_candidates(_write_jsonl(tmp_path / "c.jsonl", [record]))
    assert frame["expected_salary_min"][0] == 18.0
    assert frame["expected_salary_max"][0] == 32.0


def test_load_uses_and_writes_cache_for_default_path(tmp_path: Path, monkeypatch) -> None:
    """The DEFAULT_CANDIDATES + limit=None path caches to Parquet; a later call reads the cache, not the JSONL β€”
    proven by counting how many times the JSONL is actually opened+parsed, not just comparing results."""
    import preprocess.load as load_module

    source = _write_jsonl(tmp_path / "candidates.jsonl", [{"candidate_id": "CAND_0000001"}])
    cache = tmp_path / "cache.parquet"
    monkeypatch.setattr(load_module, "DEFAULT_CANDIDATES", source)
    monkeypatch.setattr(load_module, "LOADED_CANDIDATES_CACHE", cache)

    real_open_text = load_module._open_text
    open_count = 0

    def _counting_open_text(path):
        nonlocal open_count
        open_count += 1
        return real_open_text(path)

    monkeypatch.setattr(load_module, "_open_text", _counting_open_text)

    assert not cache.exists()
    first = load_candidates(source)  # limit=None, path == DEFAULT_CANDIDATES β†’ cache-eligible; a real parse
    assert cache.exists()
    assert first["candidate_id"].to_list() == ["CAND_0000001"]
    assert open_count == 1

    second = load_candidates(source)  # must be served from the Parquet cache β€” the JSONL is NOT reopened
    assert second["candidate_id"].to_list() == ["CAND_0000001"]
    assert open_count == 1  # unchanged β€” proves the second call never touched the source file at all


def test_load_cache_not_used_for_custom_path_or_limit(tmp_path: Path, monkeypatch) -> None:
    """A custom path or a limit'd read must NEVER use/pollute the cache β€” it isn't the real full dataset."""
    import preprocess.load as load_module

    default_source = tmp_path / "candidates.jsonl"
    cache = tmp_path / "cache.parquet"
    monkeypatch.setattr(load_module, "DEFAULT_CANDIDATES", default_source)
    monkeypatch.setattr(load_module, "LOADED_CANDIDATES_CACHE", cache)
    _write_jsonl(default_source, [{"candidate_id": "CAND_0000001"}])

    other_path = _write_jsonl(tmp_path / "custom.jsonl", [{"candidate_id": "CAND_OTHER"}])
    load_candidates(other_path)  # different path β†’ cache untouched
    assert not cache.exists()

    load_candidates(default_source, limit=1)  # limited read β†’ cache untouched
    assert not cache.exists()


def test_load_stale_cache_is_ignored(tmp_path: Path, monkeypatch) -> None:
    """A cache older than the source JSONL is treated as stale and a fresh parse runs instead."""
    import os

    import preprocess.load as load_module

    source = tmp_path / "candidates.jsonl"
    cache = tmp_path / "cache.parquet"
    monkeypatch.setattr(load_module, "DEFAULT_CANDIDATES", source)
    monkeypatch.setattr(load_module, "LOADED_CANDIDATES_CACHE", cache)

    _write_jsonl(source, [{"candidate_id": "CAND_OLD"}])
    load_candidates(source)  # writes the cache reflecting CAND_OLD
    assert cache.exists()

    _write_jsonl(source, [{"candidate_id": "CAND_NEW"}])  # source content changes
    newer = cache.stat().st_mtime + 5
    os.utime(source, (newer, newer))  # force the source's mtime strictly past the cache's

    refreshed = load_candidates(source)
    assert refreshed["candidate_id"].to_list() == ["CAND_NEW"]  # stale cache ignored, live reparse happened