coulomb-social/tests/test_date_backfill_journal.py
tegwick fb4baf6712
Some checks failed
Use-case regression gate / usecase-regressions (push) Has been cancelled
Journal creation-date corrections across SQL and content with crash recovery
Assistant: codex
Assistant-Model: gpt-6.1-sol
Assistant-Session: 01a18e2d-f513-7742-b8e7-12af0b744ff3
2026-10-30 20:05:11 +01:00

133 lines
5.7 KiB
Python

"""Real process exits across the SQL/filesystem boundary; synthetic data only."""
import hashlib
import json
import os
import subprocess
import sys
import pytest
from django.conf import settings
from coulomb_social.apps.spaces.bubble_transfer import source_timestamp
from coulomb_social.apps.spaces.date_backfill_journal import apply_date_patch, reconcile_date_patch
from coulomb_social.apps.spaces.models import Space
from tests.test_source_date_backfill import patch_fixture
pytestmark = pytest.mark.django_db(transaction=True)
@pytest.fixture
def prepared(tmp_path):
plan, original = patch_fixture()
space = Space.objects.create(tenant_id="tenant:synthetic", slug="accepted", title="Local title")
plan["selection"]["space_id"] = str(space.pk)
plan["proposed_changes"][0]["space_id"] = str(space.pk)
Space.objects.filter(pk=space.pk).update(
created_at=source_timestamp(plan["observed_sql_created_at"]))
root = tmp_path / "content"
pages = root / "spaces/accepted/pages"
pages.mkdir(parents=True)
(pages / "index.md").write_bytes(original)
(pages / "index.md").chmod(0o640)
for page in plan["pages"]:
(pages / (page["slug"] + ".md")).write_bytes(b"Synthetic edited bytes")
private = tmp_path / "private"
private.mkdir(mode=0o700)
plan_path = private / "plan.json"
plan_path.write_text(json.dumps(plan))
return root, plan, private / "journal.json", plan_path, original, space
def test_success_preserves_other_bytes_and_sql_fields_and_refuses_replay(prepared):
root, plan, journal, _, original, space = prepared
before = Space.objects.values().get(pk=space.pk)
apply_date_patch(root, plan, journal, backup_sha256="a" * 64)
after = Space.objects.values().get(pk=space.pk)
assert after.pop("created_at") == source_timestamp(plan["source_created_at"])
before.pop("created_at")
assert after == before
assert json.loads(journal.read_bytes())["state"] == "completed"
assert journal.stat().st_mode & 0o777 == 0o600
assert (root / "spaces/accepted/pages/index.md").stat().st_mode & 0o777 == 0o640
assert (root / "spaces/accepted/pages/copy.md").read_bytes() == b"Synthetic edited bytes"
assert original in (root / "spaces/accepted/pages/index.md").read_bytes().replace(
b"source_created_at: 1700000000123\n", b"", 1)
with pytest.raises(ValueError):
apply_date_patch(root, plan, journal, backup_sha256="a" * 64)
@pytest.mark.parametrize("phase", ["intent", "sql", "file", "commit"])
def test_actual_process_death_reconciles_from_committed_sql(prepared, phase):
root, plan, journal, plan_path, original, space = prepared
code = '''
import json, os, sys
from django.conf import settings
settings.DATABASES["default"] = json.loads(sys.argv[1])
import django
django.setup()
from coulomb_social.apps.spaces.date_backfill_journal import apply_date_patch
from pathlib import Path
apply_date_patch(Path(sys.argv[2]), json.loads(Path(sys.argv[3]).read_bytes()),
Path(sys.argv[4]), backup_sha256="a"*64,
checkpoint=lambda phase: os._exit(73) if phase == sys.argv[5] else None)
'''
result = subprocess.run(
[sys.executable, "-c", code, json.dumps(settings.DATABASES["default"], default=str),
str(root), str(plan_path), str(journal), phase],
env={**os.environ, "DJANGO_SETTINGS_MODULE": "coulomb_social.settings.test"},
capture_output=True, timeout=30)
assert result.returncode == 73, result.stderr.decode()
assert json.loads(journal.read_bytes())["state"] == "pending"
committed = phase == "commit"
assert reconcile_date_patch(root, journal) == ("completed" if committed else "rolled_back")
space.refresh_from_db()
assert space.created_at == source_timestamp(
plan["source_created_at"] if committed else plan["observed_sql_created_at"])
data = (root / "spaces/accepted/pages/index.md").read_bytes()
assert (data != original) == committed
for page in plan["pages"]:
assert hashlib.sha256((root / "spaces/accepted/pages" / (
page["slug"] + ".md")).read_bytes()).hexdigest() == page["observed_sha256"]
with pytest.raises(ValueError, match="Pending journal"):
reconcile_date_patch(root, journal)
@pytest.mark.parametrize("drift", ["index", "other_page", "sql"])
def test_recovery_refuses_later_edits_or_unknown_sql(prepared, drift):
root, plan, journal, _, _, space = prepared
def interrupt(phase):
if phase == "file":
raise RuntimeError("interrupted")
with pytest.raises(RuntimeError):
apply_date_patch(root, plan, journal, backup_sha256="a" * 64, checkpoint=interrupt)
if drift == "sql":
Space.objects.filter(pk=space.pk).update(created_at=source_timestamp(1600000000000))
else:
path = root / "spaces/accepted/pages" / (
"index.md" if drift == "index" else "copy.md")
path.write_bytes(b"later local edit")
before = journal.read_bytes()
with pytest.raises(ValueError):
reconcile_date_patch(root, journal)
assert journal.read_bytes() == before
if drift != "sql":
assert path.read_bytes() == b"later local edit"
def test_private_directory_and_destination_symlinks_refused_before_writes(prepared):
root, plan, journal, _, _, _ = prepared
journal.parent.chmod(0o755)
with pytest.raises(ValueError, match="Private"):
apply_date_patch(root, plan, journal, backup_sha256="a" * 64)
assert not journal.exists()
journal.parent.chmod(0o700)
index = root / "spaces/accepted/pages/index.md"
held = index.with_suffix(".held")
index.rename(held)
index.symlink_to(held)
with pytest.raises(ValueError, match="Symlink"):
apply_date_patch(root, plan, journal, backup_sha256="a" * 64)
assert not journal.exists()