Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a192e9-c28b-7533-bf25-e2341364c22a
194 lines
8 KiB
Python
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)
|