Fix release blockers and deployment build
deploy / deploy (push) Canceled after 0s

This commit is contained in:
Jens
2026-07-21 21:22:29 +02:00
parent b8091e59bd
commit a4eced8be5
64 changed files with 1011 additions and 675 deletions
+10 -3
View File
@@ -9,10 +9,12 @@ from apps.jobs.models import AiAnalysisCache
from apps.jobs.services import ai as ai_service
from apps.jobs.services.ai import analyze_job_text
pytestmark = pytest.mark.django_db
def _evaluation_cases() -> list[dict[str, object]]:
path = Path(__file__).resolve().parents[1] / "fixtures" / "ai" / "evaluation_set.json"
return json.loads(path.read_text(encoding="utf-8"))["cases"]
path = Path(__file__).resolve().parents[2] / "fixtures" / "ai" / "evaluation_set.json"
return json.loads(path.read_text(encoding="utf-8-sig"))["cases"]
@pytest.mark.parametrize("case", _evaluation_cases(), ids=lambda case: case["id"])
@@ -31,6 +33,7 @@ def test_ai_evaluation_cases(monkeypatch, case):
return
if case["response_type"] == "timeout":
class FakeClient:
def __init__(self, *args, **kwargs):
pass
@@ -46,6 +49,7 @@ def test_ai_evaluation_cases(monkeypatch, case):
monkeypatch.setattr(httpx, "Client", FakeClient)
else:
class FakeClient:
def __init__(self, *args, **kwargs):
pass
@@ -112,7 +116,10 @@ def test_ai_cache_reuses_result(monkeypatch):
nonlocal calls
calls += 1
return _fake_response(
'{"summary_nl":"Gevalideerde IT-ops advertentie.","features":{"support_ratio":0.03,"consultancy_ratio":0.01,"travel_ratio":0.02,"seniority":"senior","evidence":["IT"]},"warnings":[]}'
'{"summary_nl":"Gevalideerde IT-ops advertentie.",'
'"features":{"support_ratio":0.03,"consultancy_ratio":0.01,'
'"travel_ratio":0.02,"seniority":"senior","evidence":["IT"]},'
'"warnings":[]}'
)
monkeypatch.setattr(httpx, "Client", FakeClient)
+54 -13
View File
@@ -1,4 +1,4 @@
from pathlib import Path
from pathlib import Path
from apps.sources.adapters.ats import (
GreenhouseAdapter,
@@ -20,19 +20,28 @@ def _assert_evidence_fields(job, *, expected_keys):
def test_greenhouse_adapter_listing_extracts_open_jobs_and_skips_closed():
adapter = GreenhouseAdapter()
result = adapter.extract(_load_fixture("fixtures/ats/greenhouse-listing.json"), url="https://boards.greenhouse.io/example")
result = adapter.extract(
_load_fixture("fixtures/ats/greenhouse-listing.json"),
url="https://boards.greenhouse.io/example",
)
assert result.parser_key == "ats-greenhouse"
assert len(result.jobs) == 2
assert {job.external_id for job in result.jobs} == {"gh-open-1", "gh-open-2"}
assert all("closed" not in result.warnings for _ in [0])
assert result.confidence >= 0.89
_assert_evidence_fields(result.jobs[0], expected_keys={"external_id", "url", "location_text", "date_posted", "employment_types"})
_assert_evidence_fields(
result.jobs[0],
expected_keys={"external_id", "url", "location_text", "date_posted", "employment_types"},
)
def test_greenhouse_adapter_detail_supports_changed_markup():
adapter = GreenhouseAdapter()
result = adapter.extract(_load_fixture("fixtures/ats/greenhouse-detail.json"), url="https://boards.greenhouse.io/example/jobs/senior-software-engineer")
result = adapter.extract(
_load_fixture("fixtures/ats/greenhouse-detail.json"),
url="https://boards.greenhouse.io/example/jobs/senior-software-engineer",
)
assert len(result.jobs) == 1
job = result.jobs[0]
@@ -40,12 +49,24 @@ def test_greenhouse_adapter_detail_supports_changed_markup():
assert job.external_id == "gh-open-1"
assert job.url == "https://boards.greenhouse.io/example/jobs/senior-software-engineer"
assert job.description_html
_assert_evidence_fields(job, expected_keys={"external_id", "url", "location_text", "date_posted", "valid_through", "employment_types"})
_assert_evidence_fields(
job,
expected_keys={
"external_id",
"url",
"location_text",
"date_posted",
"valid_through",
"employment_types",
},
)
def test_lever_adapter_listing_extracts_open_jobs_and_skips_closed():
adapter = LeverAdapter()
result = adapter.extract(_load_fixture("fixtures/ats/lever-listing.json"), url="https://jobs.lever.co/example")
result = adapter.extract(
_load_fixture("fixtures/ats/lever-listing.json"), url="https://jobs.lever.co/example"
)
assert result.parser_key == "ats-lever"
assert len(result.jobs) == 2
@@ -55,7 +76,10 @@ def test_lever_adapter_listing_extracts_open_jobs_and_skips_closed():
def test_lever_adapter_detail_supports_changed_markup():
adapter = LeverAdapter()
result = adapter.extract(_load_fixture("fixtures/ats/lever-detail.json"), url="https://jobs.lever.co/example/platform-engineer")
result = adapter.extract(
_load_fixture("fixtures/ats/lever-detail.json"),
url="https://jobs.lever.co/example/platform-engineer",
)
assert len(result.jobs) == 1
job = result.jobs[0]
@@ -67,7 +91,9 @@ def test_lever_adapter_detail_supports_changed_markup():
def test_recruitee_adapter_listing_extracts_open_jobs_and_skips_closed():
adapter = RecruiteeAdapter()
result = adapter.extract(_load_fixture("fixtures/ats/recruitee-listing.json"), url="https://acme.recruitee.com/o/" )
result = adapter.extract(
_load_fixture("fixtures/ats/recruitee-listing.json"), url="https://acme.recruitee.com/o/"
)
assert result.parser_key == "ats-recruitee"
assert len(result.jobs) == 2
@@ -76,7 +102,10 @@ def test_recruitee_adapter_listing_extracts_open_jobs_and_skips_closed():
def test_recruitee_adapter_detail_supports_changed_markup():
adapter = RecruiteeAdapter()
result = adapter.extract(_load_fixture("fixtures/ats/recruitee-detail.json"), url="https://acme.recruitee.com/o/system-engineer")
result = adapter.extract(
_load_fixture("fixtures/ats/recruitee-detail.json"),
url="https://acme.recruitee.com/o/system-engineer",
)
assert len(result.jobs) == 1
job = result.jobs[0]
@@ -87,7 +116,10 @@ def test_recruitee_adapter_detail_supports_changed_markup():
def test_smartrecruiters_adapter_listing_extracts_open_jobs_and_skips_closed():
adapter = SmartRecruitersAdapter()
result = adapter.extract(_load_fixture("fixtures/ats/smartrecruiters-listing.json"), url="https://jobs.smartrecruiters.com/example")
result = adapter.extract(
_load_fixture("fixtures/ats/smartrecruiters-listing.json"),
url="https://jobs.smartrecruiters.com/example",
)
assert result.parser_key == "ats-smartrecruiters"
assert len(result.jobs) == 2
@@ -96,7 +128,10 @@ def test_smartrecruiters_adapter_listing_extracts_open_jobs_and_skips_closed():
def test_smartrecruiters_adapter_detail_supports_changed_markup():
adapter = SmartRecruitersAdapter()
result = adapter.extract(_load_fixture("fixtures/ats/smartrecruiters-detail.json"), url="https://jobs.smartrecruiters.com/example/security-engineer")
result = adapter.extract(
_load_fixture("fixtures/ats/smartrecruiters-detail.json"),
url="https://jobs.smartrecruiters.com/example/security-engineer",
)
assert len(result.jobs) == 1
job = result.jobs[0]
@@ -106,7 +141,10 @@ def test_smartrecruiters_adapter_detail_supports_changed_markup():
def test_workable_adapter_listing_extracts_open_jobs_and_skips_closed():
adapter = WorkableAdapter()
result = adapter.extract(_load_fixture("fixtures/ats/workable-listing.json"), url="https://apply.workable.com/example")
result = adapter.extract(
_load_fixture("fixtures/ats/workable-listing.json"),
url="https://apply.workable.com/example",
)
assert result.parser_key == "ats-workable"
assert len(result.jobs) == 2
@@ -115,7 +153,10 @@ def test_workable_adapter_listing_extracts_open_jobs_and_skips_closed():
def test_workable_adapter_detail_supports_changed_markup():
adapter = WorkableAdapter()
result = adapter.extract(_load_fixture("fixtures/ats/workable-detail.json"), url="https://apply.workable.com/example/data-engineer/")
result = adapter.extract(
_load_fixture("fixtures/ats/workable-detail.json"),
url="https://apply.workable.com/example/data-engineer/",
)
assert len(result.jobs) == 1
job = result.jobs[0]
+12 -7
View File
@@ -38,12 +38,15 @@ def test_discovery_from_html_includes_jsonld_and_feed_links():
assert candidate_urls["https://example.org/werken-bij"] == "html"
assert candidate_urls["https://jobs.example.org/vacatures/"] == "html"
assert candidate_urls["https://jobs.example.org/vacatures/data-engineer"] == "html-jsonld"
assert candidate_urls["https://jobs.example.org/vacatures/data-engineer/solliciteer"] == "html-jsonld"
assert candidate_urls["https://example.org/feed/jobs.xml"] == "html"
assert candidate_urls["https://jobs.example.org/vacatures/data-engineer"].startswith("html-jsonld")
assert any(
c.source_type == Source.Type.RSS and c.reason == "feed" for c in result
assert (
candidate_urls["https://jobs.example.org/vacatures/data-engineer/solliciteer"]
== "html-jsonld"
)
assert candidate_urls["https://example.org/feed/jobs.xml"] == "html"
assert candidate_urls["https://jobs.example.org/vacatures/data-engineer"].startswith(
"html-jsonld"
)
assert any(c.source_type == Source.Type.RSS and c.reason == "feed" for c in result)
assert any(
c.source_type == Source.Type.EMPLOYER and c.url == "https://example.org/werken-bij"
for c in result
@@ -69,14 +72,16 @@ def test_discovery_from_sitemap_prefers_candidates_and_filters_private_or_denied
assert len(index_candidates) == 1
assert index_candidates[0].url == "https://jobs.example.org/sitemap-jobs.xml"
assert len(urlset_candidates) >= 2
assert any(item.url == "https://jobs.example.org/vacatures/data-engineer" for item in urlset_candidates)
assert any(
item.url == "https://jobs.example.org/vacatures/data-engineer" for item in urlset_candidates
)
assert all("linkedin.com" not in item.url for item in index_candidates + urlset_candidates)
def test_discovery_from_email_extracts_employer_domain_root():
raw = Path("fixtures/emails/sample_alert.eml").read_bytes()
result = discover_from_email(raw)
assert [item.url for item in result] == ["https://jobs.example.org"]
assert [item.url for item in result] == ["https://jobs.example.org/"]
assert result[0].source_type == Source.Type.EMPLOYER
+1 -1
View File
@@ -10,7 +10,7 @@ from apps.jobs.services.normalization import CanonicalJobDraft
from apps.sources.models import Source
def _draft_for_test(job: JobPosting, *, **overrides) -> CanonicalJobDraft:
def _draft_for_test(job: JobPosting, **overrides) -> CanonicalJobDraft:
values = {
"source_url": "https://jobs.smartrecruiters.com/example/security-engineer",
"canonical_url": "https://jobs.smartrecruiters.com/example/security-engineer",
+3 -1
View File
@@ -27,7 +27,9 @@ def test_learning_skip_for_implicit_hide(profile, job, user):
def test_learning_marks_explicit_title_hide_as_nonlearning(profile, job, user):
feedback = record_feedback(user=user, job=job, action=Feedback.Action.HIDE, reason="Titel past niet bij mij")
feedback = record_feedback(
user=user, job=job, action=Feedback.Action.HIDE, reason="Titel past niet bij mij"
)
metadata = feedback.metadata.get("learning", {})
assert metadata["status"] == "queued"
assert metadata["reason_code"] == "non_learning_title"
+7 -3
View File
@@ -6,8 +6,8 @@ from apps.sources.services.fetcher import (
ContentRejectedError,
FetchError,
FetchTimeoutError,
RateLimitedError,
PolicyBlockedError,
RateLimitedError,
fetch_url,
)
from apps.sources.services.url_security import ValidatedUrl
@@ -79,7 +79,9 @@ def test_fetcher_rejects_dns_rebinding(monkeypatch):
def validate(url: str, **kwargs):
if "/final" in url:
return ValidatedUrl(url=url, hostname="example.org", port=443, addresses=("198.51.100.12",))
return ValidatedUrl(
url=url, hostname="example.org", port=443, addresses=("198.51.100.12",)
)
return ValidatedUrl(url=url, hostname="example.org", port=443, addresses=("93.184.216.34",))
monkeypatch.setattr("apps.sources.services.fetcher.validate_public_url", validate)
@@ -133,7 +135,9 @@ def test_fetcher_rejects_policy_content_size_and_http(monkeypatch):
client.close()
client = httpx.Client(
transport=httpx.MockTransport(lambda request: (_ for _ in ()).throw(httpx.TimeoutException("timeout")))
transport=httpx.MockTransport(
lambda request: (_ for _ in ()).throw(httpx.TimeoutException("timeout"))
)
)
with pytest.raises(FetchTimeoutError):
fetch_url("https://example.org/slow", client=client)
+1 -2
View File
@@ -127,8 +127,7 @@ def test_validate_csv_geodata_flags_duplicates_and_invalid_coordinates(tmp_path:
validate_csv_geodata(source)
source.write_text(
"postal_code,municipality,region,latitude,longitude\n"
"9000,Gent,Vlaams-Brabant,99,3.73\n"
"postal_code,municipality,region,latitude,longitude\n9000,Gent,Vlaams-Brabant,99,3.73\n"
)
with pytest.raises(CommandError, match="latitude buiten bereik"):
validate_csv_geodata(source)
+1 -3
View File
@@ -34,9 +34,7 @@ def test_manual_import_url_uses_fetch_and_succeeds(monkeypatch, user):
monkeypatch.setattr("apps.sources.services.manual_import.fetch_url", fake_fetch)
monkeypatch.setattr("apps.sources.services.manual_import.process_raw_document", fake_process)
summary = import_manual_source(
actor=user, source_url="https://jobs.example.org/jobs"
)
summary = import_manual_source(actor=user, source_url="https://jobs.example.org/jobs")
source = Source.objects.get(pk=summary.source_id)
assert summary.mode == "url"
+7 -5
View File
@@ -19,7 +19,11 @@ def test_source_lease_blocks_concurrent_workers_and_recovers_when_expired(source
assert acquire_source_lease(source_id=source.pk, worker_token="worker-b", now=now) is None
assert release_source_lease(source_id=source.pk, worker_token="worker-a", now=now)
lease_b = acquire_source_lease(source_id=source.pk, worker_token="worker-b", now=timezone.now())
lease_b = acquire_source_lease(
source_id=source.pk,
worker_token="worker-b",
now=now + timedelta(seconds=source.minimum_interval_seconds),
)
assert lease_b is not None
@@ -51,7 +55,7 @@ def test_source_lease_allows_origin_concurrency(db):
)
source_b = Source.objects.create(
name="Source B",
source_type=Source.Type.EMPLOYER,
source_type=Source.Type.RSS,
base_url="https://shared.example.org/careers/",
domain=shared_domain,
status=Source.Status.ACTIVE,
@@ -76,8 +80,6 @@ def test_failure_backoff_uses_retry_after_and_bounds():
policy=Source.Policy.ALLOW,
)
source.save()
backoff = calculate_failure_backoff_seconds(
source, failure_count=2, retry_after_seconds=120
)
backoff = calculate_failure_backoff_seconds(source, failure_count=2, retry_after_seconds=120)
assert backoff >= 120
assert backoff <= 3600 + 45
+17 -11
View File
@@ -4,10 +4,9 @@ from decimal import Decimal
import pytest
from apps.jobs.models import AiAnalysisCache
from apps.jobs.services.ai import AiAnalysis
from apps.jobs.models import Employer, JobPosting, ScoreRun
from apps.jobs.models import AiAnalysisCache, Employer, JobPosting, ScoreRun
from apps.jobs.services import scoring
from apps.jobs.services.ai import AiAnalysis
from apps.jobs.services.scoring import calculate_score
@@ -78,10 +77,12 @@ def test_distance_boundary_inclusief(matching_job, profile, monkeypatch):
def test_remote_job_heeft_geen_afstandsexclusie(matching_job, profile):
matching_job.workplace_type = JobPosting.Workplace.REMOTE
matching_job.postal_code = None
matching_job.municipality = None
matching_job.raw_location = None
matching_job.save(update_fields=["workplace_type", "postal_code", "municipality", "raw_location"])
matching_job.postal_code = ""
matching_job.municipality = ""
matching_job.raw_location = ""
matching_job.save(
update_fields=["workplace_type", "postal_code", "municipality", "raw_location"]
)
matching_job.latitude = None
matching_job.longitude = None
result = calculate_score(matching_job, profile)
@@ -90,12 +91,14 @@ def test_remote_job_heeft_geen_afstandsexclusie(matching_job, profile):
def test_onbekende_locatie_leidt_niet_tot_automatische_uitsluiting(matching_job, profile):
matching_job.postal_code = None
matching_job.municipality = None
matching_job.postal_code = ""
matching_job.municipality = ""
matching_job.raw_location = "onbekend"
matching_job.latitude = None
matching_job.longitude = None
matching_job.save(update_fields=["postal_code", "municipality", "raw_location", "latitude", "longitude"])
matching_job.save(
update_fields=["postal_code", "municipality", "raw_location", "latitude", "longitude"]
)
result = calculate_score(matching_job, profile)
assert result.evidence["distance_km"] is None
assert result.recommendation != ScoreRun.Recommendation.HIDDEN
@@ -162,7 +165,10 @@ def test_ai_score_influence_is_opt_in_and_bounded(matching_job, profile, monkeyp
)
result = calculate_score(matching_job, profile)
assert result.recommendation in {ScoreRun.Recommendation.STRONG, ScoreRun.Recommendation.POSSIBLE}
assert result.recommendation in {
ScoreRun.Recommendation.STRONG,
ScoreRun.Recommendation.POSSIBLE,
}
assert result.model_version == "local-model"
assert result.evidence["ai"]["status"] == AiAnalysisCache.Status.OK
assert result.evidence["ai"]["weight_applied"] == 20.0
+8 -1
View File
@@ -19,7 +19,14 @@ def test_validate_production_security_skips_when_debug_mode_is_enabled():
@pytest.mark.parametrize(
"secret_key, allowed_hosts, csrf_trusted_origins, session_cookie_secure, csrf_cookie_secure, secure_ssl_redirect",
(
"secret_key",
"allowed_hosts",
"csrf_trusted_origins",
"session_cookie_secure",
"csrf_cookie_secure",
"secure_ssl_redirect",
),
[
("", ["example.org"], ["https://example.org"], True, True, True),
("change-me", ["example.org"], ["https://example.org"], True, True, True),