"""Synthetic observations only; date plans must preserve local edits/copies.""" import copy import hashlib import json import os import subprocess import sys from pathlib import Path import pytest from coulomb_social.apps.spaces.date_backfill import plan_date_backfill def fixtures(): digest = hashlib.sha256(b"Synthetic edited bytes").hexdigest() selection = {"space_id": "destination-id", "space_slug": "accepted", "tenant_id": "tenant:synthetic", "source_space_id": "source"} pages = [ {"slug": "index", "sha256": digest, "meta": {"id": "bubble-space:source", "bubble_type": "space"}}, {"slug": "notes", "sha256": digest, "meta": {"id": "bubble:article", "source_created_at": 1700000001000}}, {"slug": "copy", "sha256": digest, "meta": {"id": "bubble:article", "source_created_at": 1700000001000}}, {"slug": "local-notes", "sha256": digest, "meta": {}}, ] observation = {"schema": "coulomb.source-date-observation.v1", "spaces": [{ "id": "destination-id", "slug": "accepted", "tenant_id": "tenant:synthetic", "created_at": "2026-10-06T12:00:00+00:00", "pages": pages, }]} spaces = {"source": {"Created Date": 1700000000123}} articles = {"article": {"Created Date": 1700000001000}} return observation, spaces, articles, selection def test_plan_only_changes_index_field_and_sql_date_keeps_copies_edits_and_local_pages(): data = fixtures() before = copy.deepcopy(data) plan = plan_date_backfill(*data) assert data == before assert plan["prepared_change_set_valid"] assert [c["operation"] for c in plan["proposed_changes"]] == [ "update_space_created_at", "insert_frontmatter_field"] assert plan["proposed_changes"][1]["preserve_all_existing_metadata_and_body_bytes"] assert plan["proposed_changes"][0]["value"] == "2023-11-14T22:13:20.123000+00:00" assert sum(p["duplicate_provenance"] for p in plan["pages"]) == 2 assert all(not p["proposed_changes"] for p in plan["pages"]) assert plan["pages"][-1]["status"] == "local_page_preserved" assert not plan["ready_to_apply"] and not plan["production_writes"] assert plan["requires_fresh_paired_backup"] and plan["requires_live_hash_and_sql_preconditions"] @pytest.mark.parametrize("field", ["space_id", "space_slug", "tenant_id", "source_space_id"]) def test_ambiguous_or_wrong_destination_is_not_inferred_from_provenance(field): observation, spaces, articles, selection = fixtures() selection[field] = "different" with pytest.raises(ValueError): plan_date_backfill(observation, spaces, articles, selection) def test_existing_index_conflict_requires_review_and_never_proposes_overwriting_it(): observation, spaces, articles, selection = fixtures() observation["spaces"][0]["pages"][0]["meta"]["source_created_at"] = 1600000000000 plan = plan_date_backfill(observation, spaces, articles, selection) assert plan["blockers"] == ["existing_index_date_conflicts_with_source"] assert not plan["prepared_change_set_valid"] assert all(c["operation"] != "insert_frontmatter_field" for c in plan["proposed_changes"]) def test_article_date_gap_is_reported_without_rewriting_missing_source_or_local_copies(): observation, spaces, articles, selection = fixtures() del observation["spaces"][0]["pages"][1]["meta"]["source_created_at"] plan = plan_date_backfill(observation, spaces, articles, selection) assert "article_date_missing" in plan["blockers"] assert all(not p["proposed_changes"] for p in plan["pages"]) def test_numeric_prefixed_original_index_reference_matches_normalized_source(): observation, _, articles, selection = fixtures() identity = "1700000000000x12345678" selection["source_space_id"] = identity observation["spaces"][0]["pages"][0]["meta"]["id"] = ( "bubble-space:1234__LOOKUP__" + identity) plan = plan_date_backfill(observation, {identity: {"Created Date": 1700000000123}}, articles, selection) assert plan["prepared_change_set_valid"] def test_cli_private_plan_hashes_and_no_overwrite_or_checkout_output(tmp_path): observation, spaces, articles, selection = fixtures() capture = tmp_path / "capture" capture.mkdir() for name, data in [("spaces", spaces), ("articles", articles)]: (capture / (name + ".json")).write_text(json.dumps(data)) observed = tmp_path / "observation.json" observed.write_text(json.dumps(observation)) selected = tmp_path / "selection.json" selected.write_text(json.dumps(selection)) output = tmp_path / "plan.json" root = Path(__file__).resolve().parents[1] cmd = [sys.executable, str(root / "scripts/plan-source-date-backfill.py"), str(capture), "--observation", str(observed), "--selection", str(selected), "--output", str(output)] result = subprocess.run(cmd, capture_output=True, text=True) assert result.returncode == 0, result.stderr assert "destination-id" not in result.stdout plan = json.loads(output.read_text()) assert plan["source_sha256"]["observation"] == hashlib.sha256(observed.read_bytes()).hexdigest() assert os.stat(output).st_mode & 0o777 == 0o600 assert subprocess.run(cmd, capture_output=True).returncode == 1 cmd[-1] = str(root / "var/refused-date-plan.json") assert subprocess.run(cmd, capture_output=True).returncode == 1 assert not Path(cmd[-1]).exists()