jobhunt-platform/packages/artifacts/tests/test_artifacts.py
hermes 8d8a863300 T2: packages/artifacts + packages/llm-gateway + docker-compose.yml + .env.example
packages/artifacts:
- render_cv_pdf(profile, sections) -> bytes: data-driven CV PDF generation
  using fpdf2 with bundled DejaVuSans TTF for unicode (Swedish chars tested).
  Jinja2 template for layout data prep, adapted from build_cv.py approach.
- render_cover_letter(text, profile) -> bytes: simple cover letter PDF.
- hash_bytes(b) -> str: sha256 hex digest.
- next_version(existing) -> int: version numbering helper.
- 16 tests, all passing: PDF validity, Swedish characters, hash stability,
  cover letter rendering, hash correctness, version logic.

packages/llm-gateway:
- Async-first Gateway class with provider config from env.
- Mock mode default when no API key env present (deterministic canned outputs
  per task name, defined in mock.py).
- Telemetry sink injectable (async or sync callable, receives TelemetryRow).
- Budget guard raises BudgetExceeded BEFORE any provider call is made.
- Retry policy: max 2 retries on 429/5xx, then fallback provider for STRONG
  tasks only. CHEAP tasks never use fallback (paid provider protection).
- Schema validation via jsonschema; SchemaValidationError on mismatch.
- Task class routing: CHEAP (score, extract, cv_assist) vs STRONG (critique,
  cl_critique, research). Model routing per task class.
- Paid provider detection heuristic; warns on paid fallback config.
- 31 tests, all passing: mock determinism, schema pass/fail, budget guard
  (mock + real mode), telemetry sink (async/sync/none), config from env,
  provider calls with mocked HTTP (retry, fallback, no-fallback-for-cheap).

docker-compose.yml:
- postgres:16 service, user/pass/db = jobhunt, host port 5433->5432,
  named volume jobhunt_pgdata, healthcheck.

.env.example:
- DATABASE_URL, LLM provider config (primary + fallback), task budgets,
  API and web settings.
2026-07-30 17:59:56 +00:00

146 lines
No EOL
4.8 KiB
Python

"""Tests for the artifacts package."""
from __future__ import annotations
from artifacts import hash_bytes, next_version, render_cover_letter, render_cv_pdf
SAMPLE_PROFILE = {
"full_name": "Joakim Morling",
"headline": "Full-stack Engineer & Architect",
"email": "jcamorling@gmail.com",
"phone": "+46 76 006 7335",
"location": "Malmo, Sweden",
"summary": (
"Full-stack engineer with a passion for building robust systems. "
"Experienced in government platforms and embedded ML. "
"Swedish characters: å ä ö Å Ä Ö are important."
),
}
SAMPLE_SECTIONS = [
{
"kind": "experience",
"title": "Co-founder & Partner",
"org": "Pro Firmitas ApS",
"location": "Copenhagen, DK",
"start_date": "2026-02",
"end_date": None,
"bullets": [
"Co-founded a consulting company building AI-enhanced software",
"Responsible for technical architecture and client delivery",
],
},
{
"kind": "education",
"title": "M.Sc. Computer Science & Engineering",
"org": "Lund University (LTH)",
"location": "Lund, SE",
"start_date": "2018",
"end_date": "2023",
"bullets": [
"Master Thesis: optimized DBSCAN clustering for radar processing",
],
},
]
SWEDISH_PROFILE = {
"full_name": "Åke Öberg",
"headline": "Mjukvaruingenjör - Full Stack",
"email": "ake.oberg@example.se",
"phone": "+46 70 123 4567",
"location": "Malmö, Sverige",
"summary": "Erfaren utvecklare med fokus på å ä ö Å Ä Ö i alla texter.",
}
SWEDISH_SECTIONS = [
{
"kind": "experience",
"title": "Senior Utvecklare",
"org": "Företag AB",
"location": "Göteborg",
"start_date": "2020",
"end_date": None,
"bullets": [
"Byggde system med ångervektor och översättningsmotor",
"Ansvarig för säkerhetsgranskning av ärendehantering",
],
},
]
class TestRenderCvPdf:
def test_returns_valid_pdf_bytes(self) -> None:
result = render_cv_pdf(SAMPLE_PROFILE, SAMPLE_SECTIONS)
assert isinstance(result, (bytes, bytearray))
assert bytes(result).startswith(b"%PDF")
def test_swedish_characters_render(self) -> None:
"""Swedish characters must not raise an encoding error."""
result = render_cv_pdf(SWEDISH_PROFILE, SWEDISH_SECTIONS)
assert bytes(result).startswith(b"%PDF")
def test_content_hash_stable(self) -> None:
"""Same input should produce the same content hash."""
result_a = render_cv_pdf(SAMPLE_PROFILE, SAMPLE_SECTIONS)
result_b = render_cv_pdf(SAMPLE_PROFILE, SAMPLE_SECTIONS)
assert hash_bytes(bytes(result_a)) == hash_bytes(bytes(result_b))
def test_different_input_different_hash(self) -> None:
result_a = render_cv_pdf(SAMPLE_PROFILE, SAMPLE_SECTIONS)
result_b = render_cv_pdf(SWEDISH_PROFILE, SWEDISH_SECTIONS)
assert hash_bytes(bytes(result_a)) != hash_bytes(bytes(result_b))
def test_empty_sections(self) -> None:
result = render_cv_pdf(SAMPLE_PROFILE, [])
assert bytes(result).startswith(b"%PDF")
def test_minimal_profile(self) -> None:
result = render_cv_pdf({"full_name": "Test Person"}, [])
assert bytes(result).startswith(b"%PDF")
class TestRenderCoverLetter:
def test_returns_valid_pdf_bytes(self) -> None:
result = render_cover_letter("Dear Hiring Manager,\n\nI am applying...", SAMPLE_PROFILE)
assert bytes(result).startswith(b"%PDF")
def test_swedish_text(self) -> None:
text = "Hej! Jag söker jobbet. Mvh Åke Öberg."
result = render_cover_letter(text, SWEDISH_PROFILE)
assert bytes(result).startswith(b"%PDF")
def test_content_hash_stable(self) -> None:
text = "Cover letter body text."
result_a = render_cover_letter(text, SAMPLE_PROFILE)
result_b = render_cover_letter(text, SAMPLE_PROFILE)
assert hash_bytes(bytes(result_a)) == hash_bytes(bytes(result_b))
class TestHashBytes:
def test_known_value(self) -> None:
assert hash_bytes(b"hello") == (
"2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
)
def test_empty_bytes(self) -> None:
assert hash_bytes(b"") == (
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
)
def test_different_input_different_hash(self) -> None:
assert hash_bytes(b"a") != hash_bytes(b"b")
class TestNextVersion:
def test_empty_list(self) -> None:
assert next_version([]) == 1
def test_sequential(self) -> None:
assert next_version([1, 2, 3]) == 4
def test_gaps(self) -> None:
assert next_version([1, 3]) == 4
def test_single(self) -> None:
assert next_version([5]) == 6