Files
Unraid-AppOps-Gateway-Public/backend/tests/test_repopulse_adoption_journal.py
T

498 lines
18 KiB
Python

from __future__ import annotations
import hashlib
import pytest
from sqlalchemy import inspect
from sqlalchemy.exc import IntegrityError
from sqlmodel import Session, SQLModel, create_engine
from backend.app.db.models import RepoPulseAdoptionJournal
from backend.app.services import repopulse_adoption_journal as journal
from backend.app.services.enrollment_profiles import PROFILE_VERSION
from backend.app.services.repopulse_legacy_adoption_protocol import (
APPLICATION_ID,
CONTROLLER_RECEIPT_VERSION,
LEGACY_VOLUME,
PROTOCOL_VERSION,
REPOSITORY,
advance_intent,
create_intent,
intent_binding_sha256,
)
def _sha(char: str) -> str:
return char * 64
def _passing_preflight() -> dict:
check_codes = (
"legacy_registry",
"controller_state",
"gitea_ancestry",
"runtime_identity_and_health",
"legacy_storage_topology",
"legacy_appdata",
"product_api",
"database_health",
"queue_quiescence",
"database_baseline",
)
return {
"apiVersion": "itworx.repopulse-legacy-adoption-preflight/v1",
"repository": REPOSITORY,
"status": "preflight_passed",
"legacyCompatibility": {
"status": "passed",
"checks": [
{"code": code, "status": "passed", "gate": "legacy_compatibility"}
for code in check_codes
],
},
"targetAcceptance": {"status": "blocked", "checks": []},
"readOnly": True,
"adoptionAuthorized": False,
"evidence": {
"main_revision": "a" * 40,
"live_revision": "b" * 40,
"container_id_sha256": _sha("d"),
"container_image_sha256": _sha("1"),
"container_state": "running",
"container_health": "healthy",
"queue": {"queued": 0, "active": 0, "completed": 569, "failed": 0},
"database": {
"database_bytes": 44_576_768,
"journal_bytes": 0,
"scan_count": 572,
"compacted_scan_count": 96,
},
},
}
def _target_gates() -> dict:
return {
"profileId": "repopulse-authenticated",
"profileVersion": PROFILE_VERSION,
"planSha256": _sha("c"),
**{
key: True
for key in (
"profileReady",
"controllerTrustReady",
"controllerBootstrapSecretImported",
"typedEnrollmentTrustReady",
"targetSecretReady",
"exactPolicyReady",
"authenticatedRouteReady",
"accessListReady",
"mutationBoundaryReady",
"managedRuntimeReady",
"scanSourceMatches",
"scanMountReadOnly",
"scanPermissionPolicyPersistent",
"scanAccessForRuntimeIdentityReady",
"ownedDataTargetReady",
"managedEnvironmentReady",
"legacyEnvironmentRetained",
"maintenanceAuthReady",
"controllerAdoptionCapabilityReady",
"controllerRegistryWriterReady",
"controllerSecretWriterReady",
)
},
}
def _quiescence(intent: dict) -> dict:
return {
"intentId": intent["intentId"],
"globalDeployLockHeld": True,
"projectLockHeld": True,
"maintenanceModeActive": True,
"queueFrozen": True,
"queuedJobs": 0,
"activeJobs": 0,
"maintenanceModeAuthenticated": True,
"containerName": "repopulse",
"containerIdSha256": _sha("d"),
"containerImageSha256": _sha("1"),
"containerState": "exited",
"legacyVolume": LEGACY_VOLUME,
"controllerIntentDurable": True,
"controllerIntentBindingSha256": intent_binding_sha256(intent),
}
def _snapshot(intent: dict) -> dict:
return {
"intentId": intent["intentId"],
"globalDeployLockHeld": True,
"projectLockHeld": True,
"legacyContainerStopped": True,
"legacyVolume": LEGACY_VOLUME,
"legacyVolumeRetained": True,
"sqliteBackupMethod": "sqlite_online_backup",
"quickCheck": "ok",
"integrityCheck": "ok",
"sourceLogicalSha256": _sha("e"),
"snapshotLogicalSha256": _sha("e"),
"snapshotFileSha256": _sha("f"),
"sourceScanCount": 572,
"snapshotScanCount": 572,
"sourceCompactedScanCount": 96,
"snapshotCompactedScanCount": 96,
"sourceQueueSha256": _sha("1"),
"snapshotQueueSha256": _sha("1"),
"sourceAuxiliaryTreeSha256": _sha("3"),
"sourceAuxiliaryFileCount": 42,
"targetDataPathWasAbsent": True,
"snapshotReceiptDurable": True,
"legacyEnvironmentRetained": True,
"managedEnvironmentFilePrivate": True,
"controllerIntentBindingSha256": intent_binding_sha256(intent),
}
def _registration(intent: dict) -> dict:
gates = intent["evidence"]["targetGates"]
snapshot = intent["evidence"]["snapshot"]
return {
"receiptVersion": "itworx.repopulse-legacy-adoption-receipt/v1",
"intentId": intent["intentId"],
"receiptSha256": _sha("2"),
"repository": REPOSITORY,
"applicationId": APPLICATION_ID,
"enrollmentId": "enr-0123456789abcdefabcd",
"profileId": gates["profileId"],
"planSha256": gates["planSha256"],
"snapshotFileSha256": snapshot["snapshotFileSha256"],
"targetLogicalSha256": snapshot["snapshotLogicalSha256"],
"targetScanCount": snapshot["snapshotScanCount"],
"targetCompactedScanCount": snapshot["snapshotCompactedScanCount"],
"targetQueueSha256": snapshot["snapshotQueueSha256"],
"targetAuxiliaryTreeSha256": snapshot["sourceAuxiliaryTreeSha256"],
"targetAuxiliaryFileCount": snapshot["sourceAuxiliaryFileCount"],
"targetQueuedJobs": 0,
"targetActiveJobs": 0,
"targetQuickCheck": "ok",
"targetIntegrityCheck": "ok",
"targetRecoveryStatus": "healthy",
"targetDataUid": 10001,
"targetDataGid": 10001,
"managedDataMountMatches": True,
"copySourceIsVerifiedSnapshot": True,
"registryTargetMatches": True,
"controllerStateReceiptVerified": True,
"deploymentReceiptSchema": "gitea-deploy-receipt.v1",
"deploymentReceiptSha256": _sha("4"),
"deploymentReceiptVerified": True,
"runtimeHealthy": True,
"publishedHostPorts": [],
"globalDeployLockHeld": True,
"projectLockHeld": True,
"legacyVolume": LEGACY_VOLUME,
"legacyVolumeRetained": True,
"legacyEnvironmentRetained": True,
"managedEnvironmentFilePrivate": True,
"scanSourceMatches": True,
"scanMountReadOnly": True,
"scanPermissionPolicyPersistent": True,
"scanAccessForRuntimeIdentityReady": True,
"controllerIntentDurable": True,
"controllerIntentBindingSha256": intent_binding_sha256(intent),
}
def _ownership(intent: dict, registration: dict) -> dict:
return {
"repository": REPOSITORY,
"applicationId": APPLICATION_ID,
"enrollmentId": registration["enrollmentId"],
"profileId": registration["profileId"],
"planSha256": intent["evidence"]["targetGates"]["planSha256"],
"controllerReceiptSha256": registration["receiptSha256"],
"enrollmentPersisted": True,
"exactApplyPolicyPersisted": True,
"exactRollbackPolicyPersisted": True,
"secretReferenceReady": True,
}
@pytest.fixture
def engine(tmp_path):
database_engine = create_engine(f"sqlite:///{tmp_path / 'appops.db'}")
SQLModel.metadata.create_all(database_engine)
yield database_engine
database_engine.dispose()
def _reserve(engine, key: str = "repopulse-adoption-2026") -> dict:
with Session(engine) as session:
return journal.reserve_repopulse_adoption(session, _passing_preflight(), key)
def test_create_all_registers_singleton_table_and_reservation_survives_restart(engine) -> None:
assert inspect(engine).has_table("repopulse_adoption_journal")
first = _reserve(engine)
assert first["apiVersion"] == PROTOCOL_VERSION
assert first["status"] == "in_progress"
assert first["phase"] == "reserved"
assert first["repository"] == REPOSITORY
assert first["applicationId"] == APPLICATION_ID
assert first["revision"] == 1
# A new Session models a process restart: the slot and idempotent replay
# are read back from SQLite, not held in adapter memory.
replay = _reserve(engine)
assert replay == first
with Session(engine) as session:
status = journal.get_repopulse_adoption_status(session)
assert status == first
def test_changed_preflight_and_different_key_conflict_without_replacing_slot(engine) -> None:
first = _reserve(engine)
changed = _passing_preflight()
changed["evidence"]["live_revision"] = "c" * 40
with Session(engine) as session, pytest.raises(journal.RepoPulseAdoptionJournalError) as error:
journal.reserve_repopulse_adoption(session, changed, "repopulse-adoption-2026")
assert error.value.code == "idempotency_conflict"
with Session(engine) as session, pytest.raises(journal.RepoPulseAdoptionJournalError) as error:
journal.reserve_repopulse_adoption(
session, _passing_preflight(), "repopulse-adoption-2027"
)
assert error.value.code == "one_time_adoption_already_reserved"
assert _reserve(engine) == first
def test_reservation_rejects_another_repository_identity_without_creating_a_row(engine) -> None:
other_repository = _passing_preflight()
other_repository["repository"] = "someone-else/repopulse"
with Session(engine) as session, pytest.raises(journal.RepoPulseAdoptionJournalError) as error:
journal.reserve_repopulse_adoption(
session, other_repository, "repopulse-adoption-2026"
)
assert error.value.code == "legacy_preflight_not_compatible"
with Session(engine) as session:
assert journal.get_repopulse_adoption_status(session)["status"] == "unreserved"
intent = create_intent(_passing_preflight(), "repopulse-adoption-2026")
with Session(engine) as session, pytest.raises(IntegrityError):
session.add(
RepoPulseAdoptionJournal(
slot="other",
repository="someone-else/repopulse",
application_id="other-app",
intent_id=intent["intentId"],
idempotency_sha256=intent["idempotencySha256"],
preflight_sha256=intent["preflightSha256"],
phase=intent["phase"],
status=intent["status"],
revision=1,
intent_json=intent,
)
)
session.commit()
def test_controller_intent_confirmation_requires_exact_binding_and_replays(engine) -> None:
_reserve(engine)
with Session(engine) as session:
after_gates = journal.advance_repopulse_adoption(
session, "target_gates", _target_gates(), expected_revision=1
)
assert after_gates["phase"] == "target_gates_ready"
assert after_gates["revision"] == 2
current = create_intent(_passing_preflight(), "repopulse-adoption-2026")
valid_confirmation = _quiescence(current)
invalid_confirmation = {**valid_confirmation, "controllerIntentBindingSha256": _sha("0")}
with Session(engine) as session, pytest.raises(journal.RepoPulseAdoptionJournalError) as error:
journal.confirm_repopulse_controller_intent(
session, invalid_confirmation, expected_revision=2
)
assert error.value.code == "controller_intent_binding_mismatch"
with Session(engine) as session:
confirmed = journal.confirm_repopulse_controller_intent(
session, valid_confirmation, expected_revision=2
)
assert confirmed["phase"] == "quiesced"
assert confirmed["revision"] == 3
with Session(engine) as session:
replay = journal.confirm_repopulse_controller_intent(
session, valid_confirmation, expected_revision=2
)
assert replay == confirmed
def test_compare_and_swap_rejects_stale_checkpoint_without_overwriting_state(engine) -> None:
first = _reserve(engine)
with Session(engine) as session:
updated = journal.advance_repopulse_adoption(
session,
"target_gates",
_target_gates(),
expected_revision=1,
)
assert updated["status"] == "in_progress"
assert updated["revision"] == 2
candidate = create_intent(_passing_preflight(), "repopulse-adoption-2026")
valid_confirmation = _quiescence(candidate)
with Session(engine) as session, pytest.raises(journal.RepoPulseAdoptionJournalError) as error:
journal.confirm_repopulse_controller_intent(
session,
valid_confirmation,
expected_revision=1,
)
assert error.value.code == "journal_revision_conflict"
with Session(engine) as session:
assert journal.get_repopulse_adoption_status(session) == updated
assert updated["intentId"] == first["intentId"]
def test_recovery_resume_checks_controller_binding_and_persists_across_sessions(engine) -> None:
_reserve(engine)
with Session(engine) as session:
recovery = journal.mark_repopulse_adoption_recovery(
session,
"operator_interruption",
{"operatorNote": "resume review", "path": "private/path"},
expected_revision=1,
)
assert "private/path" not in str(recovery)
receipt = {
"apiVersion": CONTROLLER_RECEIPT_VERSION,
"intentId": recovery["intentId"],
"phase": "reserved",
"receiptSha256": hashlib.sha256(b"durable controller journal row").hexdigest(),
"repository": REPOSITORY,
"applicationId": APPLICATION_ID,
"legacyVolumeRetained": True,
"intentDurable": True,
"intentBindingSha256": recovery["intentBindingSha256"],
"registrationEvidence": None,
}
invalid_receipt = {**receipt, "intentBindingSha256": _sha("0")}
with Session(engine) as session, pytest.raises(journal.RepoPulseAdoptionJournalError) as error:
journal.resume_repopulse_adoption(session, invalid_receipt, expected_revision=2)
assert error.value.code == "controller_recovery_receipt_unbound"
with Session(engine) as session:
resumed = journal.resume_repopulse_adoption(
session, receipt, expected_revision=2
)
assert resumed["status"] == "in_progress"
assert resumed["phase"] == "reserved"
assert resumed["revision"] == 3
with Session(engine) as session:
assert journal.get_repopulse_adoption_status(session) == resumed
def test_controller_registration_reconciles_before_appops_ownership_can_advance(engine) -> None:
_reserve(engine)
candidate = create_intent(_passing_preflight(), "repopulse-adoption-2026")
with Session(engine) as session:
current = journal.advance_repopulse_adoption(
session, "target_gates", _target_gates(), expected_revision=1
)
candidate = advance_intent(candidate, "target_gates", _target_gates())
with Session(engine) as session:
current = journal.confirm_repopulse_controller_intent(
session, _quiescence(candidate), expected_revision=2
)
candidate = advance_intent(candidate, "quiescence", _quiescence(candidate))
with Session(engine) as session:
current = journal.advance_repopulse_adoption(
session, "snapshot", _snapshot(candidate), expected_revision=3
)
candidate = advance_intent(candidate, "snapshot", _snapshot(candidate))
early_ownership = {
"repository": REPOSITORY,
"applicationId": APPLICATION_ID,
"enrollmentId": "enr-0123456789abcdefabcd",
"profileId": "repopulse-authenticated",
"planSha256": _sha("c"),
"controllerReceiptSha256": _sha("2"),
"enrollmentPersisted": True,
"exactApplyPolicyPersisted": True,
"exactRollbackPolicyPersisted": True,
"secretReferenceReady": True,
}
with Session(engine) as session, pytest.raises(journal.RepoPulseAdoptionJournalError) as error:
journal.advance_repopulse_adoption(
session, "appops_ownership", early_ownership, expected_revision=4
)
assert error.value.code == "controller_registration_missing"
assert current["phase"] == "snapshot_verified"
registration = _registration(candidate)
with Session(engine) as session:
registered = journal.advance_repopulse_adoption(
session,
"controller_registration",
registration,
expected_revision=4,
)
candidate = advance_intent(candidate, "controller_registration", registration)
assert registered["phase"] == "target_registered"
assert registered["revision"] == 5
with Session(engine) as session:
recovery = journal.mark_repopulse_adoption_recovery(
session,
"appops_commit_uncertain",
{"uncertain": True, "path": "private/path"},
expected_revision=5,
)
assert recovery["status"] == "recovery_required"
assert recovery["phase"] == "target_registered"
assert recovery["legacyVolumePreserved"] is True
assert recovery["controllerReconciliationRequired"] is True
assert "private/path" not in str(recovery)
controller_receipt = {
"apiVersion": CONTROLLER_RECEIPT_VERSION,
"intentId": candidate["intentId"],
"phase": "target_registered",
"receiptSha256": registration["receiptSha256"],
"repository": REPOSITORY,
"applicationId": APPLICATION_ID,
"legacyVolumeRetained": True,
"intentDurable": True,
"intentBindingSha256": intent_binding_sha256(candidate),
"registrationEvidence": registration,
}
with Session(engine) as session:
resumed = journal.resume_repopulse_adoption(
session, controller_receipt, expected_revision=6
)
assert resumed["status"] == "in_progress"
assert resumed["phase"] == "target_registered"
assert resumed["revision"] == 7
ownership = _ownership(candidate, registration)
with Session(engine) as session:
owned = journal.advance_repopulse_adoption(
session, "appops_ownership", ownership, expected_revision=7
)
assert owned["phase"] == "appops_owned"
assert owned["revision"] == 8
assert owned["status"] != "completed"