mirror of
https://github.com/MadsLorentzen/ai-job-search.git
synced 2026-09-17 00:26:26 +00:00
97 lines
3.9 KiB
Python
97 lines
3.9 KiB
Python
"""Guards for the /rank command spec.
|
|||
|
|
|
||
|
|
The command is a markdown spec (the spec IS the implementation), so these
|
||
|
|
tests pin the invariants that would break silently: the header format that
|
||
|
|
lint_skills.py enforces, and the persistence of scoring-agent gaps/strengths
|
||
|
|
into seen_jobs.json (previously computed in Step 2 and thrown away after
|
||
|
|
Step 5's terminal output).
|
||
|
|
"""
|
||
|
|
import subprocess
|
||
|
|
import sys
|
||
|
|
import unittest
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
try:
|
||
|
|
import yaml # noqa: F401 - only probing availability for the lint integration test
|
||
|
|
_HAVE_YAML = True
|
||
|
|
except ImportError:
|
||
|
|
_HAVE_YAML = False
|
||
|
|
|
||
|
|
REPO = Path(__file__).resolve().parent.parent
|
||
|
|
COMMAND = REPO / ".claude" / "commands" / "rank.md"
|
||
|
|
SCRAPER_SKILL = REPO / ".claude" / "skills" / "job-scraper" / "SKILL.md"
|
||
|
|
|
||
|
|
|
||
|
|
def _sections(text: str) -> dict[str, str]:
|
||
|
|
"""Split a command spec into {heading: body} by '##' headers.
|
||
|
|
|
||
|
|
Splitting this way lets a fork's extra sections (e.g. this fork's
|
||
|
|
'## Blocker logging') sit between the ones under test without shifting
|
||
|
|
which text a given assertion sees.
|
||
|
|
"""
|
||
|
|
parts = text.split("\n## ")
|
||
|
|
result = {}
|
||
|
|
for part in parts[1:]:
|
||
|
|
heading, _, body = part.partition("\n")
|
||
|
|
result[heading.strip()] = body
|
||
|
|
return result
|
||
|
|
|
||
|
|
|
||
|
|
class RankCommandSpec(unittest.TestCase):
|
||
|
|
def test_command_file_exists_with_lint_compliant_header(self):
|
||
|
|
self.assertTrue(COMMAND.is_file(), "command spec missing")
|
||
|
|
first_line = COMMAND.read_text(encoding="utf-8").splitlines()[0]
|
||
|
|
self.assertTrue(
|
||
|
|
first_line.startswith("# /rank"),
|
||
|
|
f"header must start with '# /rank' (lint_skills.py enforces it), got: {first_line!r}",
|
||
|
|
)
|
||
|
|
|
||
|
|
def test_step4_persists_gaps_and_strengths(self):
|
||
|
|
sections = _sections(COMMAND.read_text(encoding="utf-8"))
|
||
|
|
step4 = sections.get("Step 4: Update State", "")
|
||
|
|
self.assertIn('"gaps"', step4, "Step 4 must persist the gaps array into seen_jobs.json")
|
||
|
|
self.assertIn('"strengths"', step4, "Step 4 must persist the strengths array into seen_jobs.json")
|
||
|
|
|
||
|
|
def test_step4_documents_verbatim_no_accumulate_and_untrusted_data_rules(self):
|
||
|
|
sections = _sections(COMMAND.read_text(encoding="utf-8"))
|
||
|
|
step4 = sections.get("Step 4: Update State", "")
|
||
|
|
self.assertIn("verbatim", step4, "Step 4 must require storing gaps/strengths verbatim, never reformatted")
|
||
|
|
self.assertIn("replaces", step4, "Step 4 must state that --all re-scoring replaces, not accumulates, the arrays")
|
||
|
|
self.assertIn("untrusted data", step4, "Step 4 must restate that stored gaps/strengths are untrusted data")
|
||
|
|
|
||
|
|
def test_important_rules_link_honest_scoring_to_persistence(self):
|
||
|
|
sections = _sections(COMMAND.read_text(encoding="utf-8"))
|
||
|
|
rules = sections.get("Important Rules", "")
|
||
|
|
self.assertIn(
|
||
|
|
"persisted with it",
|
||
|
|
rules,
|
||
|
|
"Rule 5 must note that gaps are persisted (Step 4), not just printed (Step 5)",
|
||
|
|
)
|
||
|
|
|
||
|
|
def test_job_scraper_schema_note_mentions_strengths_and_gaps(self):
|
||
|
|
text = SCRAPER_SKILL.read_text(encoding="utf-8")
|
||
|
|
self.assertIn("strengths", text)
|
||
|
|
self.assertIn("gaps", text)
|
||
|
|
self.assertIn(
|
||
|
|
"readers tolerate their absence",
|
||
|
|
text,
|
||
|
|
"schema note must say old entries lacking strengths/gaps are tolerated, never backfilled",
|
||
|
|
)
|
||
|
|
|
||
|
|
@unittest.skipUnless(
|
||
|
|
_HAVE_YAML,
|
||
|
|
"PyYAML not installed (the CI Python-test job omits it; the lint job runs lint_skills.py directly)",
|
||
|
|
)
|
||
|
|
def test_lint_skills_passes(self):
|
||
|
|
result = subprocess.run(
|
||
|
|
[sys.executable, str(REPO / "tools" / "lint_skills.py")],
|
||
|
|
cwd=REPO,
|
||
|
|
capture_output=True,
|
||
|
|
text=True,
|
||
|
|
)
|
||
|
|
self.assertEqual(result.returncode, 0, f"lint_skills.py failed:\n{result.stdout}{result.stderr}")
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
unittest.main()
|