Some checks failed
Use-case regression gate / usecase-regressions (push) Has been cancelled
Assistant: codex Assistant-Model: gpt-6.1-sol Assistant-Session: 01a18e2d-f513-7742-b8e7-12af0b744ff3
133 lines
5.7 KiB
Python
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()
|