forked from ChelseaKR/habitable
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtest_site_content.py
More file actions
444 lines (386 loc) · 18.1 KB
/
Copy pathtest_site_content.py
File metadata and controls
444 lines (386 loc) · 18.1 KB
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
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright 2026 Chelsea Kelly-Reif
"""People-first SEO, link, and claim contracts for the public content guides."""
from __future__ import annotations
import json
import re
from datetime import date
from html.parser import HTMLParser
from pathlib import Path
from urllib.parse import urlparse
import pytest
_SITE = Path(__file__).resolve().parent.parent / "site"
_BASE = "https://habitable.chelseakr.com/"
_PAGE_META = {
"how-it-works": (
"How Habitable Evidence Works | Offline Repair Records",
"Learn how Habitable Evidence organizes repair conditions, notices, responses, sealed "
"captures, and whole-unit packet disclosures in one offline record.",
),
"documentation-checklist": (
"Safe Repair Documentation Checklist | Habitable Evidence",
"Use this safety-first checklist to organize repair photos, notices, responses, and "
"recurring conditions without posting private tenant data online.",
),
"guides/preserve-maintenance-request-records": (
"Preserve Maintenance Request Records | Habitable Evidence",
"Preserve maintenance request records, attachments, confirmations, replies, status "
"history, and exported copies before a phone or portal changes.",
),
"guides/housing-inspection-records": (
"Housing Inspection Records Guide | Habitable Evidence",
"Learn which housing complaint, inspection, violation, reinspection, and closure "
"records to preserve—and what official portal status cannot prove.",
),
"tenant-unions": (
"Tenant Union Evaluation Guide | Habitable Evidence",
"A bounded, synthetic-data evaluation plan for tenant unions reviewing Habitable Evidence "
"workflows, privacy boundaries, and adoption gates.",
),
"templates/tenant-union-building-condition-survey": (
"Tenant Union Building Condition Survey | Habitable",
"Download a tenant union building survey CSV to organize condition scope, recurrence, "
"repair requests, access, consent, storage, and campaign follow-up.",
),
"legal-aid-reviewers": (
"Legal Aid Evidence Packet Review | Habitable Evidence",
"Review Habitable Evidence packet structure, integrity checks, disclosure, accessibility, "
"and limits with synthetic data before any real-case use.",
),
"inspectors-code-enforcement": (
"Housing Inspector Review Guide | Habitable Evidence",
"See how a synthetic Habitable Evidence packet presents conditions, timelines, notices, "
"and integrity records for inspector or code-enforcement review.",
),
"trust-limitations": (
"Trust, Security & Legal Limits | Habitable Evidence",
"Understand what Habitable Evidence integrity checks can establish, what remains unproven, "
"and which security, privacy, and legal gates remain open.",
),
}
class _ContentParser(HTMLParser):
def __init__(self) -> None:
super().__init__(convert_charrefs=True)
self.html_attrs: dict[str, str] = {}
self.meta: list[dict[str, str]] = []
self.links: list[dict[str, str]] = []
self.anchors: list[str] = []
self.images: list[dict[str, str]] = []
self.ids: set[str] = set()
self.json_ld: list[str] = []
self.title_parts: list[str] = []
self.h1_parts: list[str] = []
self.visible_parts: list[str] = []
self.main_count = 0
self.h1_count = 0
self.collection_controls: list[str] = []
self.non_json_scripts: list[dict[str, str]] = []
self._in_title = False
self._in_h1 = False
self._in_body = False
self._ignored_depth = 0
self._json_parts: list[str] | None = None
def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
values = {name: value or "" for name, value in attrs}
self._record_document_node(tag, values)
self._record_parser_state(tag)
self._record_public_surface_constraint(tag, values)
def _record_document_node(self, tag: str, values: dict[str, str]) -> None:
if tag == "html":
self.html_attrs = values
elif tag == "meta":
self.meta.append(values)
elif tag == "link":
self.links.append(values)
elif tag == "a" and values.get("href"):
self.anchors.append(values["href"])
elif tag == "img":
self.images.append(values)
def _record_parser_state(self, tag: str) -> None:
if tag == "main":
self.main_count += 1
elif tag == "title":
self._in_title = True
elif tag == "h1":
self.h1_count += 1
self._in_h1 = True
elif tag == "body":
self._in_body = True
def _record_public_surface_constraint(self, tag: str, values: dict[str, str]) -> None:
if values.get("id"):
self.ids.add(values["id"])
if tag in {"form", "input", "textarea", "select", "button"}:
self.collection_controls.append(tag)
if tag in {"script", "style"}:
self._ignored_depth += 1
if tag == "script":
if values.get("type") == "application/ld+json":
self._json_parts = []
else:
self.non_json_scripts.append(values)
def handle_endtag(self, tag: str) -> None:
if tag == "title":
self._in_title = False
elif tag == "h1":
self._in_h1 = False
elif tag == "body":
self._in_body = False
if tag in {"script", "style"}:
self._ignored_depth -= 1
if tag == "script" and self._json_parts is not None:
self.json_ld.append("".join(self._json_parts))
self._json_parts = None
def handle_data(self, data: str) -> None:
if self._in_title:
self.title_parts.append(data)
if self._in_h1:
self.h1_parts.append(data)
if self._json_parts is not None:
self._json_parts.append(data)
elif self._in_body and self._ignored_depth == 0 and data.strip():
self.visible_parts.append(data.strip())
def _parse(path: Path) -> _ContentParser:
parser = _ContentParser()
parser.feed(path.read_text(encoding="utf-8"))
return parser
def _meta_values(parser: _ContentParser, attribute: str) -> dict[str, str]:
return {
item[attribute]: item["content"]
for item in parser.meta
if item.get(attribute) and "content" in item
}
def _resolve_local(source: Path, href: str) -> Path | None:
parsed = urlparse(href)
if parsed.scheme or parsed.netloc:
return None
target = (source.parent / parsed.path).resolve()
if target.is_dir() or parsed.path.endswith("/"):
target /= "index.html"
return target
@pytest.mark.parametrize("slug", _PAGE_META)
def test_content_page_has_unique_consistent_metadata(slug: str) -> None:
expected_title, expected_description = _PAGE_META[slug]
canonical = f"{_BASE}{slug}/"
parser = _parse(_SITE / slug / "index.html")
named = _meta_values(parser, "name")
open_graph = _meta_values(parser, "property")
title = "".join(parser.title_parts).strip()
assert parser.html_attrs["lang"] == "en"
assert title == expected_title
assert 50 <= len(title) <= 60
assert named["description"] == expected_description
assert 120 <= len(named["description"]) <= 160
assert named["robots"] == "index, follow, max-image-preview:large"
assert [link for link in parser.links if link.get("rel") == "canonical"] == [
{"rel": "canonical", "href": canonical}
]
assert open_graph["og:type"] == "article"
assert open_graph["og:site_name"] == "Habitable Evidence"
assert open_graph["og:url"] == canonical
assert open_graph["og:title"] == expected_title
assert open_graph["og:description"] == expected_description
assert open_graph["og:image"].startswith(f"{_BASE}img/")
assert open_graph["og:image:alt"]
assert named["twitter:card"] == "summary"
assert named["twitter:title"] == expected_title
assert named["twitter:description"] == expected_description
assert named["twitter:image"] == open_graph["og:image"]
@pytest.mark.parametrize("slug", _PAGE_META)
def test_content_page_uses_only_truthful_article_and_breadcrumb_schema(slug: str) -> None:
_, expected_description = _PAGE_META[slug]
canonical = f"{_BASE}{slug}/"
parser = _parse(_SITE / slug / "index.html")
assert len(parser.json_ld) == 1
document = json.loads(parser.json_ld[0])
assert document["@context"] == "https://schema.org"
graph = document["@graph"]
assert [item["@type"] for item in graph] == ["Article", "BreadcrumbList"]
article = graph[0]
assert article["description"] == expected_description
assert article["mainEntityOfPage"] == canonical
assert article["inLanguage"] == "en"
assert article["author"] == {
"@type": "Person",
"name": "Chelsea Kelly-Reif",
"url": "https://chelseakr.github.io/",
}
assert date.fromisoformat(article["datePublished"]) <= date.today()
assert date.fromisoformat(article["dateModified"]) <= date.today()
assert "review" not in article
assert "aggregateRating" not in article
crumbs = graph[1]["itemListElement"]
assert [crumb["@type"] for crumb in crumbs] == ["ListItem", "ListItem"]
assert [crumb["position"] for crumb in crumbs] == [1, 2]
assert crumbs[0]["item"] == _BASE
assert crumbs[-1]["item"] == canonical
@pytest.mark.parametrize("slug", _PAGE_META)
def test_content_page_is_semantic_static_and_claim_safe(slug: str) -> None:
parser = _parse(_SITE / slug / "index.html")
visible = " ".join(parser.visible_parts)
normalized = re.sub(r"\s+", " ", visible).casefold()
assert parser.main_count == 1
assert parser.h1_count == 1
assert len(parser.h1_parts) > 0
assert parser.collection_controls == []
assert parser.non_json_scripts == []
assert "not legal advice" in normalized or "legal boundary" in normalized
assert "synthetic" in normalized
assert "court-ready" not in normalized
assert "admissib" not in normalized
assert "successful pilot" not in normalized
assert "completed pilot" not in normalized
assert "has been independently audited" not in normalized
assert "guaranteed" not in normalized
assert "submit tenant data" in normalized or "do not send tenant" in normalized
for image in parser.images:
target = _resolve_local(_SITE / slug / "index.html", image["src"])
assert target is not None, f"content image must deploy locally: {image['src']}"
assert target.is_file(), f"missing image: {target}"
assert image.get("width") and image.get("height")
assert "alt" in image
def test_public_content_links_resolve_and_pages_are_cross_linked() -> None:
pages = [_SITE / "index.html", *(_SITE / slug / "index.html" for slug in _PAGE_META)]
inbound: dict[str, set[Path]] = {slug: set() for slug in _PAGE_META}
for page in pages:
parser = _parse(page)
for href in parser.anchors:
target = _resolve_local(page, href)
if target is None:
continue
assert _SITE.resolve() in {target, *target.parents}, f"link escapes site: {href}"
assert target.is_file(), f"broken link from {page.relative_to(_SITE)}: {href}"
for slug in _PAGE_META:
if target == (_SITE / slug / "index.html").resolve() and target != page.resolve():
inbound[slug].add(page)
homepage_links = set(_parse(_SITE / "index.html").anchors)
for slug, sources in inbound.items():
assert f"{slug}/" in homepage_links, f"homepage does not link to {slug}"
assert len(sources) >= 3, f"{slug} needs multiple useful internal paths"
@pytest.mark.parametrize("slug", _PAGE_META)
def test_content_assets_resolve(slug: str) -> None:
page = _SITE / slug / "index.html"
parser = _parse(page)
root_prefix = "../" * len(Path(slug).parts)
local_assets = [
item["href"] for item in parser.links if item.get("rel") in {"icon", "stylesheet"}
]
assert local_assets == [f"{root_prefix}img/icon.svg", f"{root_prefix}content.css"]
for href in local_assets:
target = _resolve_local(page, href)
assert target is not None and target.is_file()
def test_metadata_is_unique_across_content_guides() -> None:
titles = {title for title, _ in _PAGE_META.values()}
descriptions = {description for _, description in _PAGE_META.values()}
assert len(titles) == len(_PAGE_META)
assert len(descriptions) == len(_PAGE_META)
def test_public_export_copy_does_not_advertise_blocked_selection() -> None:
pages = {slug: _parse(_SITE / slug / "index.html") for slug in _PAGE_META}
parts = [_meta_values(parser, "name").get("description", "") for parser in pages.values()]
parts.extend(" ".join(parser.visible_parts) for parser in pages.values())
combined = " ".join(parts).casefold()
stale_claims = (
"selective exports",
"selective export controls",
"selected captures",
"selected images",
"use the minimum scope",
"selective disclosure",
)
assert not any(claim in combined for claim in stale_claims)
how_it_works = " ".join(pages["how-it-works"].visible_parts).casefold()
assert "current packets include every issue, timeline entry, and capture" in how_it_works
assert "embedding sealed originals is optional" in how_it_works
trust = " ".join(pages["trust-limitations"].visible_parts).casefold()
assert "if the whole-unit scope is too broad, do not export" in trust
@pytest.mark.parametrize("slug", _PAGE_META)
def test_inline_emphasis_does_not_join_visible_words(slug: str) -> None:
"""Closing inline markup must not collapse adjacent words in rendered copy."""
html = (_SITE / slug / "index.html").read_text(encoding="utf-8")
assert not re.search(r"</(?:strong|em|a|span)>[A-Za-z]", html)
def test_public_issue_links_carry_a_visible_privacy_warning() -> None:
for slug in _PAGE_META:
parser = _parse(_SITE / slug / "index.html")
if not any("github.com/ChelseaKR/habitable/issues/new" in href for href in parser.anchors):
continue
visible = " ".join(parser.visible_parts).casefold()
assert "public github issue" in visible
assert "never include tenant" in visible or "do not include client" in visible
def test_maintenance_request_guide_preserves_the_full_record_without_overclaiming() -> None:
slug = "guides/preserve-maintenance-request-records"
page = _SITE / slug / "index.html"
parser = _parse(page)
visible = re.sub(r"\s+", " ", " ".join(parser.visible_parts)).casefold()
required_record_stages = {
"request input",
"attachments",
"confirmation or ticket",
"status history",
"replies and visits",
"closure and what followed",
"exported copy",
}
assert all(stage in visible for stage in required_record_stages)
assert "phones are lost, damaged, replaced, or reset" in visible
assert "portal migration" in visible
assert "does not prove that a recipient received or read the request" in visible
assert "does not by itself establish that notice was legally sufficient" in visible
assert parser.collection_controls == []
assert parser.non_json_scripts == []
authoritative_sources = {
"https://oag.ca.gov/node/554793",
"https://www.masslegalhelp.org/housing-apartments-shelter/tenants-rights/keep-records-now-avoid-problems-later",
"https://consumer.ftc.gov/articles/sample-customer-complaint-letter",
}
assert authoritative_sources <= set(parser.anchors)
def test_maintenance_request_guide_has_three_contextual_inbound_paths() -> None:
target = (_SITE / "guides" / "preserve-maintenance-request-records" / "index.html").resolve()
expected_sources = {
_SITE / "index.html",
_SITE / "how-it-works" / "index.html",
_SITE / "documentation-checklist" / "index.html",
}
for source in expected_sources:
linked_targets = {
resolved
for href in _parse(source).anchors
if (resolved := _resolve_local(source, href)) is not None
}
assert target in linked_targets, f"missing contextual link from {source.relative_to(_SITE)}"
def test_inspection_records_guide_preserves_source_and_status_boundaries() -> None:
slug = "guides/housing-inspection-records"
page = _SITE / slug / "index.html"
parser = _parse(page)
visible = re.sub(r"\s+", " ", " ".join(parser.visible_parts)).casefold()
assert visible.count("reviewed 10 july 2026") >= 3
assert "california · statewide" in visible
assert "new york city · citywide" in visible
assert "complaint or service-request id" in visible
assert "inspection report" in visible
assert "citation" in visible
assert "correspondence" in visible
assert "reinspection" in visible
assert "closure" in visible
assert "does not prove current physical conditions, legal compliance, receipt" in visible
assert "or completion of a remedy" in visible
official_sources = {
"https://www.nyc.gov/site/hpd/about/hpd-online.page?no_journeys=true",
"https://www.nyc.gov/site/hpd/services-and-information/report-a-maintenance-issue.page",
"https://leginfo.legislature.ca.gov/faces/codes_displayText.xhtml?article=2."
"&chapter=5.&division=13.&lawCode=HSC&part=1.5.&title=",
}
assert official_sources <= set(parser.anchors)
inbound_sources = {
_SITE / "index.html",
_SITE / "how-it-works" / "index.html",
_SITE / "documentation-checklist" / "index.html",
_SITE / "inspectors-code-enforcement" / "index.html",
_SITE / "legal-aid-reviewers" / "index.html",
}
expected_target = page.resolve()
for source in inbound_sources:
local_targets = {
target
for href in _parse(source).anchors
if (target := _resolve_local(source, href)) is not None
}
assert expected_target in local_targets, f"missing contextual link from {source}"