498 lines
18 KiB
Python
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"
|