coulomb-social/tests/test_import_reconciliation.py
tegwick 80fe387dcc Complete matching uncertain import checkpoints with retained evidence
Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a192e9-c28b-7533-bf25-e2341364c22a
2026-10-31 18:07:34 +01:00

194 lines
8 KiB
Python

"""Exact checkpoint reconciliation preserves stores and survives process death."""
import io
import json
import os
import subprocess
import sys
import pytest
from django.conf import settings
from django.core.management import call_command
from django.core.management.base import CommandError
from coulomb_social.apps.spaces import import_reconciliation, pageops
from coulomb_social.apps.spaces.models import Space, SpaceMembership
from tests import test_import_corpus
from tests.test_import_inspection import inspect, uncertain
pytestmark = pytest.mark.django_db(transaction=True)
corpus = test_import_corpus.corpus
def complete(manifest, directory, inspection, index=0):
output = io.StringIO()
call_command(
"reconcile_content_import", str(manifest), receipt_dir=str(directory),
unit_index=index, expect_inspection=inspection["inspection_sha256"],
complete_matching_commit=True, stdout=output,
)
return json.loads(output.getvalue())
def stores():
return (
list(Space.objects.order_by("pk").values()),
list(SpaceMembership.objects.order_by("pk").values()),
{str(p): p.read_bytes() for p in pageops.content_root().rglob("*") if p.is_file()},
)
def test_matching_commit_completes_only_checkpoint_then_resume_preserves_later_edits(
corpus, tmp_path, monkeypatch
):
manifest, _, member = corpus
directory = tmp_path / "receipts"
uncertain(manifest, directory, monkeypatch)
before = stores()
prior = json.loads((directory / "checkpoint.json").read_bytes())
report = inspect(manifest, directory)
result = complete(manifest, directory, report)
checkpoint = json.loads((directory / "checkpoint.json").read_bytes())
assert stores() == before
assert result["checkpoint_completed"] and not result["remaining_import_authorized"]
assert not result["external_writer_fence_verified"]
assert not result["sql_modified"] and not result["content_modified"]
assert checkpoint["spaces"][1] == prior["spaces"][1]
reconciled = checkpoint["spaces"][0]
assert reconciled["reconciliation"]["prior_unit"] == prior["spaces"][0]
assert reconciled["reconciliation"]["inspection"] == report
assert (directory / "checkpoint.json").stat().st_mode & 0o777 == 0o600
saved = (directory / "checkpoint.json").read_bytes()
with pytest.raises(CommandError):
complete(manifest, directory, report)
assert (directory / "checkpoint.json").read_bytes() == saved
page = pageops.space_dir("one") / "pages/index.md"
page.write_text("later private edit")
Space.objects.filter(slug="one").update(title="later title")
SpaceMembership.objects.filter(space__slug="one", member=member).update(role="viewer")
output = io.StringIO()
call_command("import_content_corpus", str(manifest), receipt_dir=str(directory), stdout=output)
assert "Imported 1 space(s); skipped 1" in output.getvalue()
assert page.read_text() == "later private edit"
assert Space.objects.get(slug="one").title == "later title"
assert SpaceMembership.objects.get(space__slug="one").role == "viewer"
@pytest.mark.parametrize("change", [
"body", "grant", "metadata", "source", "manifest", "checkpoint", "foreign",
"legacy", "no_sql", "no_files", "staging", "wrong_digest", "pending", "bad_index",
])
def test_changed_or_nonmatching_return_never_modifies_stores_or_checkpoint(
corpus, tmp_path, monkeypatch, change
):
manifest, entries, member = corpus
directory = tmp_path / "receipts"
uncertain(manifest, directory, monkeypatch)
report = inspect(manifest, directory)
checkpoint = directory / "checkpoint.json"
if change == "body":
(pageops.space_dir("one") / "pages/index.md").write_text("later content")
elif change == "grant":
SpaceMembership.objects.filter(member=member).update(role="viewer")
elif change == "metadata":
Space.objects.filter(slug="one").update(description="later metadata")
elif change == "source":
(manifest.parent / entries[0]["tree"] / "pages/index.md").write_text("source drift")
elif change == "manifest":
manifest.write_text(manifest.read_text() + "\n")
elif change in {"checkpoint", "legacy"}:
data = json.loads(checkpoint.read_bytes())
if change == "legacy":
data["spaces"][0].pop("space_id")
checkpoint.write_text(json.dumps(data, indent=2))
elif change in {"no_sql", "foreign"}:
Space.objects.get(slug="one").delete()
if change == "foreign":
Space.objects.create(tenant_id="tenant:a", slug="one", title="foreign")
elif change == "no_files":
import shutil
shutil.rmtree(pageops.space_dir("one"))
elif change == "staging":
(pageops.content_root() / "spaces/.import-unattributed").mkdir()
elif change == "wrong_digest":
report["inspection_sha256"] = "0" * 64
# Test both stale inspection and fresh inspection of nonmatching state.
if change in {"body", "grant", "metadata", "legacy", "foreign", "no_sql", "no_files",
"staging"}:
report = inspect(manifest, directory)
before = stores()
saved = checkpoint.read_bytes()
index = 1 if change == "pending" else -1 if change == "bad_index" else 0
with pytest.raises(CommandError):
complete(manifest, directory, report, index=index)
assert stores() == before and checkpoint.read_bytes() == saved
@pytest.mark.parametrize("failure", ["fsync", "drift"])
def test_failed_sync_or_late_drift_leaves_uncertain_checkpoint(
corpus, tmp_path, monkeypatch, failure
):
manifest, _, _ = corpus
directory = tmp_path / "receipts"
uncertain(manifest, directory, monkeypatch)
report = inspect(manifest, directory)
saved = (directory / "checkpoint.json").read_bytes()
def sync(tree):
if failure == "fsync":
raise OSError("synthetic disk failure")
(tree / "pages/index.md").write_text("external writer")
monkeypatch.setattr(import_reconciliation, "sync_tree", sync)
with pytest.raises(CommandError):
complete(manifest, directory, report)
assert (directory / "checkpoint.json").read_bytes() == saved
@pytest.mark.parametrize("phase", ["before_replace", "after_replace"])
def test_process_exit_retains_started_or_completed_checkpoint_with_evidence(
corpus, tmp_path, monkeypatch, phase
):
manifest, _, _ = corpus
directory = tmp_path / "receipts"
uncertain(manifest, directory, monkeypatch)
report = inspect(manifest, directory)
before = stores()
code = '''
import json,os,sys
from django.conf import settings
settings.DATABASES['default']=json.loads(sys.argv[1])
settings.CONTENT_ROOT=sys.argv[2]
import django
django.setup()
from django.core.management import call_command
from coulomb_social.apps.spaces import import_receipts
original=import_receipts.os.replace
def replace(*args):
if sys.argv[6]=='before_replace':os._exit(74)
original(*args)
os._exit(74)
import_receipts.os.replace=replace
call_command('reconcile_content_import',sys.argv[3],receipt_dir=sys.argv[4],
unit_index=0,expect_inspection=sys.argv[5],complete_matching_commit=True)
'''
result = subprocess.run(
[sys.executable, "-c", code, json.dumps(settings.DATABASES["default"], default=str),
str(pageops.content_root()), str(manifest), str(directory),
report["inspection_sha256"], phase],
env={**os.environ, "DJANGO_SETTINGS_MODULE": "coulomb_social.settings.test"},
capture_output=True, timeout=30,
)
assert result.returncode == 74, result.stderr.decode()
assert stores() == before
unit = json.loads((directory / "checkpoint.json").read_bytes())["spaces"][0]
if phase == "before_replace":
assert unit["state"] == "started"
# A fresh explicit review can complete; no importer automatically retries.
complete(manifest, directory, inspect(manifest, directory))
else:
assert unit["state"] == "completed"
assert unit["reconciliation"]["inspection"] == report
with pytest.raises(CommandError):
complete(manifest, directory, report)