coulomb-social/tests/test_bubble_bundle.py
tegwick ed2c8e2f4d
Some checks failed
Use-case regression gate / usecase-regressions (push) Failing after 2m59s
Prepare lossless all-space review bundles and reconcile contents references
Assistant: codex
Assistant-Model: gpt-6.1-sol
Assistant-Session: 01a17f16-d99c-7ea3-95f0-8212897fba6b
2026-10-28 00:46:38 +01:00

168 lines
8 KiB
Python

"""Synthetic source bodies only; offline preparation must never imply acceptance."""
import hashlib
import json
import os
import subprocess
import sys
from pathlib import Path
import pytest
from coulomb_social.apps.spaces.bubble_bundle import prepare_bundle
from coulomb_social.apps.spaces.import_tree import import_space_tree
from coulomb_social.apps.spaces.models import Space
from tests.test_space_permissions import make_member
def source(created, **kwargs):
return {"Created Date": created, "Modified Date": created, **kwargs}
def capture():
return ({"s": source(1700000000123, _chunk_custom_article="r")}, {
"r": source(1700000001456, content_text="\n\n Root\r\n\n", nomer_text="Synthetic",
chunks_list_custom_article=["c"], space_custom_space="s"),
"c": source(1700000002789, content_text="Child \n", context_custom_article="r"),
})
def test_bundle_deterministic_preserves_bodies_dates_and_ordered_relations():
spaces, articles = capture()
articles["r"]["chunks_list_custom_article"] = ["c", "c"]
report, files = prepare_bundle(spaces, articles)
reversed_report, reversed_files = prepare_bundle(
dict(reversed(list(spaces.items()))), dict(reversed(list(articles.items()))))
assert (report, files) == (reversed_report, reversed_files)
entry = report["spaces"][0]
assert entry["created_at_raw"] == 1700000000123
assert b"[Source root](?page=article-" in files[entry["tree"] + "/pages/index.md"]
for page in entry["articles"]:
raw = articles[page["id"]]["content_text"].encode()
assert files[page["file"]].endswith(b"\n---\n" + raw)
assert page["body_sha256"] == hashlib.sha256(raw).hexdigest()
assert f'source_created_at: {articles[page["id"]]["Created Date"]}'.encode() in (
files[page["file"]])
root = next(a for a in report["articles"] if a["id"] == "r")
assert root["relations"]["chunks_list_custom_article"] == ["c", "c"]
assert not entry["import_ready"]
assert not report["production_imported"]
def test_missing_dates_bodies_and_inventory_only_records_remain_queued_without_trees():
spaces, articles = capture()
del articles["c"]["Created Date"]
articles["orphan"] = {"content_text": "Orphan original"}
report, files = prepare_bundle(spaces, articles, inventory_spaces={"old": {}})
assert len(report["spaces"]) == 2
assert all(s["tree"] is None for s in report["spaces"])
assert not any(p.startswith("review-trees/") for p in files)
assert report["unassigned_article_ids"] == ["orphan"]
assert len([p for p in files if p.startswith("source-bodies/")]) == 3
def test_reference_images_html_relations_and_attachments_are_unresolved_and_private():
spaces, articles = capture()
body = ('[root](https://coulomb.social/pg_chunk/r?q=1#part)\n'
'![image][visual]\n\n[visual]: https://assets.example/synthetic.png\n\n'
'<img src="./local.png" srcset="large.png 2x">'
'<a href="https://coulomb.social.attacker.example/pg_chunk/r">lookalike</a>'
'<iframe src="https://example.invalid/embed"></iframe>')
articles["r"].update(content_text=body, contents_list_custom_article=["missing"],
logo_image="https://assets.example/logo.webp", hasattachments_boolean=True)
report, files = prepare_bundle(spaces, articles)
detail = next(a for a in report["articles"] if a["id"] == "r")
refs = detail["references"]
root = next(r for r in refs if r["classification"] == "bubble_article")
assert root["target_status"] == "unique"
assert root["query"] == "q=1" and root["fragment"] == "part"
assert {r["kind"] for r in refs} == {"asset", "visual", "link"}
assert next(r for r in refs if "attacker.example" in r["source"])["classification"] == (
"external")
assert all(not r["resolved"] for r in refs)
entry = report["spaces"][0]
assert "uncaptured_relation_records" in entry["blockers"]
assert "attachment_records_require_capture" in detail["review_flags"]
assert "srcset_requires_review" in detail["review_flags"]
assert "iframe_requires_review" in detail["review_flags"]
assert any(data.endswith(body.encode()) for data in files.values())
assert report["network_requests"] == 0
def test_destination_names_survive_label_changes_and_additional_records():
spaces, articles = capture()
report, _ = prepare_bundle(spaces, articles)
articles["r"]["nomer_text"] = "../../index"
articles["extra"] = source(1700000001000, space_custom_space="s", content_text="Extra")
later, files = prepare_bundle(spaces, articles)
assert report["spaces"][0]["slug"] == later["spaces"][0]["slug"]
paths = {p["id"]: p["file"] for p in later["spaces"][0]["articles"]}
assert all(p["file"] == paths[p["id"]] for p in report["spaces"][0]["articles"])
assert all(".." not in Path(path).parts for path in files)
def test_shared_source_reference_never_selects_an_arbitrary_destination():
spaces, articles = capture()
spaces["another"] = source(1700000000000, _chunk_custom_article="c")
articles["r"]["content_text"] = "[shared](/pg_chunk/c)"
report, _ = prepare_bundle(spaces, articles)
root = next(a for a in report["articles"] if a["id"] == "r")
reference = root["references"][0]
assert reference["target_status"] == "ambiguous"
assert len(reference["candidate_targets"]) == 2
assert not reference["resolved"]
def test_relation_id_placeholder_does_not_count_as_a_complete_source_record():
spaces, articles = capture()
articles["r"]["contents_list_custom_article"] = ["stub"]
articles["stub"] = {"_id": "stub"}
report, _ = prepare_bundle(spaces, articles)
entry = report["spaces"][0]
root = next(a for a in entry["articles"] if a["id"] == "r")
assert root["incomplete_relation_ids"] == ["stub"]
assert not root["missing_relation_ids"]
assert "incomplete_relation_records" in entry["blockers"]
@pytest.mark.django_db
def test_import_keeps_distinct_space_root_and_child_creation_dates(tmp_path, settings):
spaces, articles = capture()
report, files = prepare_bundle(spaces, articles)
for name, data in files.items():
path = tmp_path / name
path.parent.mkdir(parents=True, exist_ok=True)
path.write_bytes(data)
settings.CONTENT_ROOT = str(tmp_path / "content")
owner = make_member("synthetic-bundle-owner")
entry = report["spaces"][0]
import_space_tree(tmp_path / entry["tree"], tenant_id=owner.tenant_id, member=owner)
space = Space.objects.get(slug=entry["slug"])
assert space.created_at.isoformat() == "2023-11-14T22:13:20.123000+00:00"
for page in entry["articles"]:
actual = Path(settings.CONTENT_ROOT) / "spaces" / entry["slug"] / "pages" / (
Path(page["file"]).name)
assert actual.read_bytes() == files[page["file"]]
def test_cli_private_output_completion_hashes_no_overwrite_or_checkout_output(tmp_path):
repo = Path(__file__).resolve().parents[1]
capture_dir = tmp_path / "capture"
capture_dir.mkdir()
spaces, articles = capture()
for name, data in [("spaces", spaces), ("articles", articles)]:
(capture_dir / (name + ".json")).write_text(json.dumps(data))
output = tmp_path / "bundle"
cmd = [sys.executable, str(repo / "scripts/prepare-bubble-bundle.py"), str(capture_dir),
"--output", str(output)]
result = subprocess.run(cmd, capture_output=True, text=True)
assert result.returncode == 0, result.stderr
receipt = json.loads((output / "reconciliation.json").read_text())
assert receipt["status"] == "completed_offline_preparation"
assert "Synthetic" not in result.stdout
assert os.stat(output).st_mode & 0o777 == 0o700
assert all(os.stat(p).st_mode & 0o777 == 0o600 for p in output.rglob("*") if p.is_file())
assert subprocess.run(cmd, capture_output=True).returncode == 1
cmd[-1] = str(repo / "var/test-bundle-refusal")
assert subprocess.run(cmd, capture_output=True).returncode == 1
assert not Path(cmd[-1]).exists()