feat(6845): every instance has an id lineage, and every full backup carries it
CI & Build / Python lint (push) Successful in 2s
CI & Build / Plugin hooks (push) Successful in 12s
CI & Build / TypeScript typecheck (push) Successful in 32s
CI & Build / Python tests (push) Successful in 1m37s
CI & Build / integration (push) Successful in 2m7s
CI & Build / Build & push image (push) Successful in 22s

instance_identity is one row made by migration 0127 with a random id, so an
instance has it before its first account. A full backup carries it in its
header as instance_id (the tar manifest header and the JSON top level);
BACKUP_VERSION is 28. The table rides in _NOT_INCLUDED: a restore decides from
the header value rather than copying a row.

Two stores with the same id number their rows the same way. That is what lets
a later step roll a live instance back by original id, refuse a backup whose
ids would land on unrelated rows, and have a fresh-instance restore adopt the
backup's id (adopt_instance_id).

Milestone 582, step 1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-04 19:53:45 -05:00
co-authored by Claude Opus 5.5
parent 3c01e17559
commit 6d8db0bb65
7 changed files with 171 additions and 4 deletions
@@ -0,0 +1,42 @@
"""Real-Postgres proof that every instance has one id lineage, that a full
backup carries it, and that adopting a backup's id replaces this one's."""
import io
import pytest
from grimoire.models import async_session
from grimoire.services import backup
from grimoire.services.backup_file import read_archive, write_archive_to
from grimoire.services.instance import adopt_instance_id, get_instance_id
pytestmark = [pytest.mark.integration, pytest.mark.usefixtures("_dispose_engine")]
async def test_the_migration_gave_this_instance_an_id_and_it_stays_put():
first = await get_instance_id()
assert first and len(first) >= 32
assert await get_instance_id() == first
async def test_a_full_backup_carries_the_instance_id_through_the_tar():
payload = await backup.export_full_backup()
assert payload["instance_id"] == await get_instance_id()
out = io.BytesIO()
write_archive_to(payload, out, app_version="test")
out.seek(0)
assert read_archive(out)["instance_id"] == payload["instance_id"]
async def test_adopting_a_backups_id_replaces_this_instances():
own = await get_instance_id()
try:
async with async_session() as s:
await adopt_instance_id(s, "adopted-lineage")
await s.commit()
assert await get_instance_id() == "adopted-lineage"
finally:
async with async_session() as s:
await adopt_instance_id(s, own)
await s.commit()
assert await get_instance_id() == own
+7 -3
View File
@@ -11,7 +11,7 @@ The full FK-remapping round-trip needs real Postgres and lives in the
"""
from datetime import datetime, timezone
from types import SimpleNamespace
from unittest.mock import patch
from unittest.mock import AsyncMock, patch
import pytest
@@ -22,7 +22,7 @@ def test_backup_version_is_current():
"""The bump is the point of the test — a payload section added without
moving the version produces backups that are structurally different and
indistinguishable by inspection."""
assert backup.BACKUP_VERSION == 27
assert backup.BACKUP_VERSION == 28
def _exportable_note(**over):
@@ -561,11 +561,15 @@ class _CM:
@pytest.mark.asyncio
async def test_export_full_backup_contains_every_declared_section():
with patch("grimoire.services.backup.async_session", lambda: _CM()):
with patch("grimoire.services.backup.async_session", lambda: _CM()), patch(
"grimoire.services.instance.get_instance_id", AsyncMock(return_value="test-lineage"),
):
out = await backup.export_full_backup()
assert out["version"] == backup.BACKUP_VERSION
assert out["scope"] == "full"
# v28: the id lineage a live rollback is checked against.
assert out["instance_id"] == "test-lineage"
assert "api_keys" in out["_not_included"]
# The sections v2 silently dropped, the six v5 added, v6's
# note_supersessions, and v7's code_shapes (all empty here).