feat: report broken links (4xx/5xx) in every scan

Neue 4xx/5xx-Seiten (nicht in Baseline) werden bewertet (+20 Pkt) und
lösen ab Schwelle einen Alert aus. Bekannte kaputte Links (in Baseline
vorhanden) erscheinen im Report als [bekannt] ohne Score-Beitrag.

Motivation: erster Crawl fand bredelar.info/www.stadtmarketing-marsberg.de
(404) — CMS-Fehler durch Schema-losen href. Soll dauerhaft sichtbar sein.

Änderungen: checker.find_broken_pages(), differ.compare_snapshots() mit
new_broken_urls/known_broken_urls, score_diff(), Report-Abschnitt,
config-Default new_broken_link:20, 4 neue Tests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Your Name 2026-06-12 01:55:19 +02:00
commit b3ef3a90d4
6 changed files with 101 additions and 0 deletions

View file

@ -153,6 +153,55 @@ class TestCompareSnapshots:
assert "spam.example.com" in result["new_external_domains"]
class TestBrokenLinks:
def _make_page(self, url, text="Inhalt", status=200):
return {
"url": url, "status": status, "text": text,
"links": {"a": [], "script": [], "iframe": [], "link_rel": [], "form": [], "meta_refresh": [], "canonical": None},
"metadata": {"title": "T", "h1": [], "h2": []},
"hidden_content": [], "inline_scripts": [], "jsonld": [], "comment_links": [],
}
def test_new_broken_url_detected(self):
baseline = {"https://b.info/": self._make_page("https://b.info/")}
current = {
"https://b.info/": self._make_page("https://b.info/"),
"https://b.info/missing/": self._make_page("https://b.info/missing/", status=404),
}
result = compare_snapshots(baseline, current, BASE_CFG)
assert len(result["new_broken_urls"]) == 1
assert result["new_broken_urls"][0]["url"] == "https://b.info/missing/"
assert result["new_broken_urls"][0]["status"] == 404
def test_known_broken_not_in_new(self):
broken_page = {"https://b.info/old/": self._make_page("https://b.info/old/", status=404)}
baseline = {**{"https://b.info/": self._make_page("https://b.info/")}, **broken_page}
current = {**{"https://b.info/": self._make_page("https://b.info/")}, **broken_page}
result = compare_snapshots(baseline, current, BASE_CFG)
assert result["new_broken_urls"] == []
assert len(result["known_broken_urls"]) == 1
def test_broken_url_scores(self):
result = score_diff({
"new_internal_urls": [], "missing_internal_urls": [],
"new_external_domains": [], "page_diffs": [], "new_pages_analysis": [],
"new_broken_urls": [{"url": "https://b.info/gone/", "status": 404}],
"known_broken_urls": [],
}, BASE_CFG)
assert result["score"] == 20
assert result["level"] == "yellow"
def test_known_broken_not_scored(self):
result = score_diff({
"new_internal_urls": [], "missing_internal_urls": [],
"new_external_domains": [], "page_diffs": [], "new_pages_analysis": [],
"new_broken_urls": [],
"known_broken_urls": [{"url": "https://b.info/old/", "status": 404}],
}, BASE_CFG)
assert result["score"] == 0
assert result["level"] == "green"
class TestScoreDiff:
def test_green_for_no_changes(self):
result = score_diff({