coulomb-social/tests/test_rendering_audit.py
tegwick a9526e7fb3 Preserve original ordered-list numbering during sanitization
Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a192e9-c28b-7533-bf25-e2341364c22a
2026-10-31 18:21:13 +01:00

108 lines
5 KiB
Python

"""Rendering review uses synthetic notes and never accepts private corpus fidelity."""
import hashlib
import json
import os
import subprocess
import sys
from pathlib import Path
from coulomb_social.apps.spaces.content import render_markdown
from coulomb_social.apps.spaces.rendering_audit import audit_body, review_card
def test_audit_uses_product_renderer_and_reports_custom_tags_attributes_and_schemes():
body = ('# Synthetic\n\n<topic>Placeholder</topic><br>Text\n'
'<img src="assets/picture.png" onerror="synthetic()">'
'<a href="javascript:synthetic()">Unsafe</a>')
audit, rendered = audit_body(body)
assert rendered == render_markdown(body)
assert audit["sanitization_changed_html"]
assert audit["markup_changed_by_sanitization"]
assert not audit["serialization_only_change"]
assert audit["removed_tag_counts"] == {"topic": 1}
assert {a["attribute"] for a in audit["removed_attributes"]} == {"onerror", "href"}
assert "assets/picture.png" in rendered and "<br>" in rendered
assert audit["body_sha256"] == hashlib.sha256(body.encode()).hexdigest()
assert not audit["original_bubble_render_compared"]
assert not audit["productive_fidelity_accepted"]
assert audit["reference_occurrences_before"] == 2
assert audit["reference_occurrences_after"] == 1
assert not audit["reference_attributes_preserved"]
assert audit["removed_reference_occurrences"] == 1
def test_link_entities_duplicates_and_relative_assets_preserve_exact_reference_attributes():
body = ('[first](https://example.invalid/?a=1&b=2#part)\n\n'
'[second](https://example.invalid/?a=1&b=2#part)\n\n'
'<a href="https://example.invalid/?a=1&amp;b=2#part">third</a>'
'<img src="assets/synthetic.png">')
audit, _ = audit_body(body)
assert audit['reference_occurrences_before'] == 4
assert audit['reference_occurrences_after'] == 4
assert audit['reference_attributes_preserved']
assert audit['removed_reference_occurrences'] == 0
assert audit['added_reference_occurrences'] == 0
def test_serialized_line_break_difference_is_not_reported_as_lost_markup():
audit, _ = audit_body("First<br/>Second")
assert audit["serialization_only_change"]
assert not audit["markup_changed_by_sanitization"]
assert not audit["visible_text_changed_by_sanitization"]
def test_continued_numbered_lists_keep_their_starts_through_sanitization():
body = "2. Second item\n3. Third item\n\nSeparate paragraph.\n\n8. Eighth item"
audit, rendered = audit_body(body)
assert '<ol start="2">' in rendered
assert '<ol start="8">' in rendered
assert not audit["markup_changed_by_sanitization"]
assert not audit["visible_text_changed_by_sanitization"]
assert not audit["productive_fidelity_accepted"]
def test_list_start_permission_does_not_allow_event_handlers_or_custom_markup():
body = ('<ol start="4" onclick="synthetic()"><li>Fourth item</li></ol>'
'<topic name="synthetic">Placeholder</topic>'
'<a href="javascript:synthetic()">Link</a>')
audit, rendered = audit_body(body)
assert '<ol start="4">' in rendered
assert 'onclick' not in rendered and 'javascript:' not in rendered
assert '<topic' not in rendered and 'Placeholder' in rendered
assert {a["attribute"] for a in audit["removed_attributes"]} == {
"onclick", "name", "href",
}
def test_review_card_escapes_source_and_blocks_resources_and_interaction():
body = '<img src="https://example.invalid/synthetic.png"><script>synthetic()</script>'
audit, rendered = audit_body(body)
card = review_card("</h1><script>synthetic()</script>", body, rendered, audit,
source_id='a/"?b').decode()
assert "<script>" not in card
assert "&lt;script&gt;" in card
assert "default-src 'none'" in card
assert "base-uri 'none'" in card and "form-action 'none'" in card
assert "<div inert>" in card
assert 'https://coulomb.social/pg_chunk/a%2F%22%3Fb' in card
def test_cli_keeps_missing_bodies_and_private_permissions_and_refuses_overwrite(tmp_path):
repo = Path(__file__).resolve().parents[1]
source = tmp_path / "articles.json"
source.write_text(json.dumps({"a": {"content_text": "Synthetic", "nomer_text": "Private"},
"b": {"_id": "b"}}))
output = tmp_path / "audit"
cmd = [sys.executable, str(repo / "scripts/audit-bubble-rendering.py"), str(source),
"--output", str(output)]
result = subprocess.run(cmd, capture_output=True, text=True)
assert result.returncode == 0, result.stderr
assert "Private" not in result.stdout
report = json.loads((output / "audit.json").read_text())
assert len(report["records"]) == 2
assert report["records"][1]["status"] == "body_missing"
assert (output / "index.html").is_file()
assert os.stat(output).st_mode & 0o777 == 0o700
assert all(os.stat(p).st_mode & 0o777 == 0o600 for p in output.iterdir())
assert subprocess.run(cmd, capture_output=True).returncode == 1