File size: 6,417 Bytes
6d3a6a8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cc74dfc
 
 
 
 
6d3a6a8
 
 
 
 
 
 
 
 
 
3cc1867
6d3a6a8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7b41b0f
6d3a6a8
7b41b0f
 
 
 
 
 
 
 
 
6d3a6a8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""
ANTI-CHEAT regression — prove the system is not gaming its own scorer.

Runs the REAL uploaded resume through the full pipeline (stub LLM = honest floor)
and checks the INDEPENDENT validator (not candidate_fit) agrees:

  1. A PM JD reaches READY_90_PLUS for the PM resume (both scores >= 90).
  2. A cybersecurity PM JD is REVIEW_REQUIRED with security tools flagged.
  3. A backend-engineer JD does NOT become CLEAN_90_PLUS for a PM (engineering
     hard skills not auto-claimed; download blocked or quality != CLEAN).
  4. A JD needing a specific cert does not get that cert invented.
  5. A 12-year/Director JD is NOT matched as READY for a mid-level candidate
     (independent seniority check fails).
  6. Final score is from parsed EXPORTED text (independent re-parses the file).
  7. If the renderer drops a keyword, the independent score reflects it.
"""
import os, sys, io, shutil
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8")
sys.path.insert(0, os.path.abspath("."))

from src.resume_customizer import ResumeCustomizer, _read_docx_text
from src.llm_client import LLMClient
from src.ats_validator import validate_resume
from src.resume_parser_v2 import parse_resume_pdf
# Isolate the vault so prior user confirmations don't change anti-cheat results.
import src.candidate_vault as _cv
_cv._VAULT_PATH = "data/_test_vault_anticheat.json"
if os.path.exists(_cv._VAULT_PATH):
    os.remove(_cv._VAULT_PATH)

src_pdf = r"C:\Users\Nxtwave\Desktop\resume\Saiteja_Tirunagari_Resume A 26 - Copy.pdf"
dst = "data/resume/resume.pdf"
os.makedirs("data/resume", exist_ok=True)
if not os.path.exists(dst) and os.path.exists(src_pdf):
    shutil.copyfile(src_pdf, dst)
if os.path.exists("data/resume/_parsed.json"):
    os.remove("data/resume/_parsed.json")


def stub(self, cfg, resume_dict, jd_text, job_title, company, assessment, **kwargs):
    o = dict(resume_dict)
    o["summary"] = (f"Strong-fit candidate for {job_title} at {company}: "
                    + (resume_dict.get("summary") or "PM 5+ years."))
    return o


LLMClient.tailor_resume_v4 = stub
rc = ResumeCustomizer.__new__(ResumeCustomizer)
rc.llm = LLMClient.__new__(LLMClient); rc.resume_text = ""
rc.output_dir = "data/output/resumes/_anticheat"; os.makedirs(rc.output_dir, exist_ok=True)
rc.fast_model_cfg = {"model": "fake", "api_key": "fake", "base_url": "https://fake"}
rc._pending_summary_inject = []

ok = True
def check(name, cond, detail=""):
    global ok; ok = ok and cond
    print(f"  [{'PASS' if cond else 'FAIL'}] {name}  {detail}")


def gen(jf, co):
    jd = open(f"tests/fixtures/jds/{jf}.txt", encoding="utf-8").read()
    job = {"title": "Product Manager", "company": co, "description": jd, "_raw_assessment": {}}
    rc._generate_resume_v4(job, cfg=rc.fast_model_cfg,
                           filepath=os.path.join(rc.output_dir, f"{co}.docx"))
    return jd, job, job.get("_v2_report", {})


print("1. PM JD -> READY_90_PLUS, both scores >= 90:")
jd, job, r = gen("generic_pm_3_7yrs", "PM")
internal = r.get("estimated_scores", {}).get("jd_match", 0)
indep = r.get("independent_jd_match", 0)
check("internal >= 90", internal >= 90, f"internal={internal}")
check("independent >= 90", indep >= 90, f"independent={indep}")
check("download allowed", job.get("download_allowed") is True)

print("2. Cybersecurity PM -> security tooling is HIGH-risk (confirm), not auto-faked:")
jd, job, r = gen("sumo_logic_pm", "Sec")
_sec = ("siem", "soar", "xdr", "secops", "threat intelligence",
        "threat detection", "security operations")
_high = [t.lower() for t in r.get("high_risk_terms_for_confirmation", [])]
check("security terms classified HIGH-risk (need confirmation)",
      any(t in _high for t in _sec), str(_high)[:90])
parsed_sec = _read_docx_text(os.path.join(rc.output_dir, "Sec.docx")).lower()
auto_claimed = [t for t in _sec if t in parsed_sec]
check("security tooling NOT auto-claimed in resume (HIGH not auto-included)",
      not auto_claimed, f"auto-claimed={auto_claimed}")

print("3. Backend-engineer JD must NOT be CLEAN_90_PLUS for a PM:")
jd, job, r = gen("backend_engineer", "Backend")
q = r.get("quality_flag", "")
indep = r.get("independent_jd_match", 0)
check("quality is not CLEAN_90_PLUS", q != "CLEAN_90_PLUS", f"quality={q} independent={indep}")
parsed = _read_docx_text(os.path.join(rc.output_dir, "Backend.docx")).lower()
check("did not auto-claim 'java'/'spring boot' as evidenced",
      "spring boot" not in parsed or "java" not in parsed,
      "engineering hard skills not stuffed")

print("4. Specific cert not invented:")
# Backend JD has no cert; use a cert check on parsed text — no fake 'AWS Certified'
check("no invented 'aws certified'/'cissp'",
      "aws certified" not in parsed and "cissp" not in parsed)

print("5. 12-year/Director JD not READY for mid-level candidate:")
jd, job, r = gen("senior_pm_10yrs", "SrPM")
indep = r.get("independent_jd_match", 0)
_base = parse_resume_pdf(dst).to_flat_text()
val = validate_resume(jd, _read_docx_text(os.path.join(rc.output_dir, "SrPM.docx")),
                      base_resume_text=_base)
check("independent seniority check fails (12y vs candidate)", not val.seniority_ok,
      f"seniority_ok={val.seniority_ok} notes={val.notes}")
check("not downloadable as READY", job.get("download_allowed") is not True,
      f"download={job.get('download_allowed')} status={job.get('_v2_status')}")

print("6. Score is from parsed EXPORTED text:")
# validate_resume re-parses the file independently; if it scores, it parsed.
check("independent validator scored parsed export", isinstance(indep, int))

print("7. Dropping a keyword lowers the independent score:")
full = _read_docx_text(os.path.join(rc.output_dir, "PM.docx"))
jd_pm = open("tests/fixtures/jds/generic_pm_3_7yrs.txt", encoding="utf-8").read()
base_score = validate_resume(jd_pm, full).independent_jd_match
# Simulate a renderer that dropped the Skills + most of Experience (keep only
# the header + summary). The independent score MUST fall.
lines = [l for l in full.splitlines() if l.strip()]
truncated = "\n".join(lines[: max(4, int(len(lines) * 0.25))])
drop_score = validate_resume(jd_pm, truncated).independent_jd_match
check("truncated resume scores lower", drop_score < base_score,
      f"{base_score} -> {drop_score}")

print("\n" + ("✓ ALL ANTI-CHEAT CHECKS PASS" if ok else "✗ SOME ANTI-CHEAT CHECKS FAILED"))
sys.exit(0 if ok else 1)