513 lines
19 KiB
Python
513 lines
19 KiB
Python
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from backend.app.services.enrollment_profiles import PROFILE_VERSION
|
|
from backend.app.services.repopulse_legacy_adoption_protocol import (
|
|
APPLICATION_ID,
|
|
CONTROLLER_RECEIPT_VERSION,
|
|
EXECUTION_ENABLED,
|
|
LEGACY_VOLUME,
|
|
REPOSITORY,
|
|
AdoptionProtocolError,
|
|
advance_intent,
|
|
complete_intent,
|
|
create_intent,
|
|
execution_status,
|
|
intent_binding_sha256,
|
|
mark_recovery_required,
|
|
reserve_or_replay,
|
|
resume_intent,
|
|
)
|
|
|
|
|
|
def _sha(value: str) -> str:
|
|
return value * 64
|
|
|
|
|
|
def _passing_preflight() -> dict:
|
|
checks = [
|
|
{
|
|
"code": code,
|
|
"status": "passed",
|
|
"gate": "legacy_compatibility",
|
|
"detail": "sanitized fixed check",
|
|
}
|
|
for code in (
|
|
"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": checks},
|
|
"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 _intent() -> dict:
|
|
return create_intent(_passing_preflight(), "repopulse-adoption-2026")
|
|
|
|
|
|
def _target_gates() -> dict:
|
|
return {
|
|
"profileId": "repopulse-authenticated",
|
|
"profileVersion": PROFILE_VERSION,
|
|
"planSha256": _sha("c"),
|
|
"profileReady": True,
|
|
"controllerTrustReady": True,
|
|
"controllerBootstrapSecretImported": True,
|
|
"typedEnrollmentTrustReady": True,
|
|
"targetSecretReady": True,
|
|
"exactPolicyReady": True,
|
|
"authenticatedRouteReady": True,
|
|
"accessListReady": True,
|
|
"mutationBoundaryReady": True,
|
|
"managedRuntimeReady": True,
|
|
"scanSourceMatches": True,
|
|
"scanMountReadOnly": True,
|
|
"scanPermissionPolicyPersistent": True,
|
|
"scanAccessForRuntimeIdentityReady": True,
|
|
"ownedDataTargetReady": True,
|
|
"managedEnvironmentReady": True,
|
|
"legacyEnvironmentRetained": True,
|
|
"maintenanceAuthReady": True,
|
|
"controllerAdoptionCapabilityReady": True,
|
|
"controllerRegistryWriterReady": True,
|
|
"controllerSecretWriterReady": True,
|
|
}
|
|
|
|
|
|
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": CONTROLLER_RECEIPT_VERSION,
|
|
"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 _to_snapshot_verified() -> dict:
|
|
intent = advance_intent(_intent(), "target_gates", _target_gates())
|
|
intent = advance_intent(intent, "quiescence", _quiescence(intent))
|
|
return advance_intent(intent, "snapshot", _snapshot(intent))
|
|
|
|
|
|
def _to_target_registered() -> dict:
|
|
intent = _to_snapshot_verified()
|
|
return advance_intent(intent, "controller_registration", _registration(intent))
|
|
|
|
|
|
def test_passing_legacy_preflight_can_reserve_but_never_authorizes_adoption() -> None:
|
|
intent = _intent()
|
|
|
|
assert intent["repository"] == REPOSITORY
|
|
assert intent["applicationId"] == APPLICATION_ID
|
|
assert intent["legacyVolume"] == LEGACY_VOLUME
|
|
assert intent["phase"] == "reserved"
|
|
assert intent["status"] == "in_progress"
|
|
assert "adoptionAuthorized" not in intent
|
|
assert intent["evidence"]["preflight"]["queue"]["queued"] == 0
|
|
|
|
|
|
def test_reservation_is_idempotent_and_binds_the_preflight_facts() -> None:
|
|
first = create_intent(_passing_preflight(), "repopulse-adoption-2026")
|
|
replay = reserve_or_replay(first, _passing_preflight(), "repopulse-adoption-2026")
|
|
changed = _passing_preflight()
|
|
changed["evidence"]["main_revision"] = "c" * 40
|
|
|
|
assert replay == first
|
|
with pytest.raises(AdoptionProtocolError, match="idempotency_conflict"):
|
|
reserve_or_replay(first, changed, "repopulse-adoption-2026")
|
|
with pytest.raises(AdoptionProtocolError, match="one_time_adoption_already_reserved"):
|
|
reserve_or_replay(first, _passing_preflight(), "repopulse-adoption-2027")
|
|
assert intent_binding_sha256(first) == intent_binding_sha256(replay)
|
|
|
|
|
|
def test_modified_persisted_intent_evidence_fails_integrity_check() -> None:
|
|
intent = _intent()
|
|
intent["evidence"]["preflight"]["repository"] = "jens/other"
|
|
|
|
with pytest.raises(AdoptionProtocolError, match="intent_evidence_invalid"):
|
|
advance_intent(intent, "target_gates", _target_gates())
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"mutate,code",
|
|
[
|
|
(lambda report: report.update(repository="jens/other"), "legacy_preflight_not_compatible"),
|
|
(lambda report: report.update(adoptionAuthorized=True), "legacy_preflight_not_compatible"),
|
|
(lambda report: report.update(status="blocked"), "legacy_preflight_not_compatible"),
|
|
(lambda report: report["evidence"]["queue"].update(active=1), "legacy_preflight_queue_not_quiescent"),
|
|
(lambda report: report["legacyCompatibility"]["checks"].pop(), "legacy_preflight_check_incomplete"),
|
|
],
|
|
)
|
|
def test_invalid_or_tampered_preflight_cannot_reserve(mutate, code: str) -> None:
|
|
report = _passing_preflight()
|
|
mutate(report)
|
|
|
|
with pytest.raises(AdoptionProtocolError) as error:
|
|
create_intent(report, "repopulse-adoption-2026")
|
|
assert error.value.code == code
|
|
|
|
|
|
def test_changed_or_unready_profile_trust_secret_acl_or_runtime_gate_blocks() -> None:
|
|
intent = _intent()
|
|
gates = _target_gates()
|
|
gates["accessListReady"] = False
|
|
|
|
with pytest.raises(AdoptionProtocolError, match="target_gates_not_ready"):
|
|
advance_intent(intent, "target_gates", gates)
|
|
with pytest.raises(AdoptionProtocolError, match="adoption_step_out_of_order"):
|
|
advance_intent(intent, "quiescence", _quiescence(intent))
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("field", "value"),
|
|
[("profileId", "internal-webapp"), ("profileVersion", "unreviewed/v9")],
|
|
)
|
|
def test_target_gates_require_the_exact_managed_profile(field: str, value: str) -> None:
|
|
gates = _target_gates()
|
|
gates[field] = value
|
|
|
|
with pytest.raises(AdoptionProtocolError, match="target_gates_not_ready"):
|
|
advance_intent(_intent(), "target_gates", gates)
|
|
|
|
|
|
def test_quiescence_requires_locks_maintenance_drain_stop_and_the_named_volume() -> None:
|
|
intent = advance_intent(_intent(), "target_gates", _target_gates())
|
|
evidence = _quiescence(intent)
|
|
evidence["globalDeployLockHeld"] = False
|
|
|
|
with pytest.raises(AdoptionProtocolError, match="quiescence_not_proven"):
|
|
advance_intent(intent, "quiescence", evidence)
|
|
|
|
evidence = _quiescence(intent)
|
|
evidence["activeJobs"] = 1
|
|
with pytest.raises(AdoptionProtocolError, match="quiescence_not_proven"):
|
|
advance_intent(intent, "quiescence", evidence)
|
|
|
|
evidence = _quiescence(intent)
|
|
evidence["controllerIntentBindingSha256"] = _sha("9")
|
|
with pytest.raises(AdoptionProtocolError, match="controller_intent_binding_mismatch"):
|
|
advance_intent(intent, "quiescence", evidence)
|
|
|
|
evidence = _quiescence(intent)
|
|
evidence["containerIdSha256"] = _sha("9")
|
|
with pytest.raises(AdoptionProtocolError, match="quiescence_not_proven"):
|
|
advance_intent(intent, "quiescence", evidence)
|
|
|
|
|
|
def test_quiesced_snapshot_requires_sqlite_online_backup_and_matching_logical_data() -> None:
|
|
intent = advance_intent(_intent(), "target_gates", _target_gates())
|
|
intent = advance_intent(intent, "quiescence", _quiescence(intent))
|
|
evidence = _snapshot(intent)
|
|
evidence["sqliteBackupMethod"] = "raw_file_copy"
|
|
|
|
with pytest.raises(AdoptionProtocolError, match="sqlite_snapshot_not_verified"):
|
|
advance_intent(intent, "snapshot", evidence)
|
|
|
|
evidence = _snapshot(intent)
|
|
evidence["sourceLogicalSha256"] = _sha("a")
|
|
with pytest.raises(AdoptionProtocolError, match="sqlite_snapshot_not_verified"):
|
|
advance_intent(intent, "snapshot", evidence)
|
|
|
|
|
|
def test_controller_receipt_must_bind_exact_target_keep_legacy_volume_and_have_no_ports() -> None:
|
|
intent = _to_snapshot_verified()
|
|
evidence = _registration(intent)
|
|
evidence["publishedHostPorts"] = [{"ip": "0.0.0.0", "port": 1254}]
|
|
|
|
with pytest.raises(AdoptionProtocolError, match="controller_adoption_receipt_not_verified"):
|
|
advance_intent(intent, "controller_registration", evidence)
|
|
|
|
evidence = _registration(intent)
|
|
evidence["legacyVolumeRetained"] = False
|
|
with pytest.raises(AdoptionProtocolError, match="controller_adoption_receipt_not_verified"):
|
|
advance_intent(intent, "controller_registration", evidence)
|
|
|
|
evidence = _registration(intent)
|
|
evidence["targetDataUid"] = 0
|
|
with pytest.raises(AdoptionProtocolError, match="controller_adoption_receipt_not_verified"):
|
|
advance_intent(intent, "controller_registration", evidence)
|
|
|
|
evidence = _registration(intent)
|
|
evidence["targetActiveJobs"] = 1
|
|
with pytest.raises(AdoptionProtocolError, match="controller_adoption_receipt_not_verified"):
|
|
advance_intent(intent, "controller_registration", evidence)
|
|
|
|
for field, invalid in (
|
|
("targetAuxiliaryTreeSha256", _sha("5")),
|
|
("targetAuxiliaryFileCount", 41),
|
|
("deploymentReceiptSchema", "unknown"),
|
|
("deploymentReceiptSha256", "missing"),
|
|
("deploymentReceiptVerified", False),
|
|
):
|
|
evidence = _registration(intent)
|
|
evidence[field] = invalid
|
|
with pytest.raises(AdoptionProtocolError, match="controller_adoption_receipt_not_verified"):
|
|
advance_intent(intent, "controller_registration", evidence)
|
|
|
|
|
|
def test_exact_controller_and_appops_ownership_are_separate_checkpoints() -> None:
|
|
intent = _to_target_registered()
|
|
registration = intent["evidence"]["controllerRegistration"]
|
|
gates = intent["evidence"]["targetGates"]
|
|
evidence = {
|
|
"repository": REPOSITORY,
|
|
"applicationId": APPLICATION_ID,
|
|
"enrollmentId": registration["enrollmentId"],
|
|
"profileId": registration["profileId"],
|
|
"planSha256": gates["planSha256"],
|
|
"controllerReceiptSha256": registration["receiptSha256"],
|
|
"enrollmentPersisted": True,
|
|
"exactApplyPolicyPersisted": True,
|
|
"exactRollbackPolicyPersisted": True,
|
|
"secretReferenceReady": True,
|
|
}
|
|
appops_owned = advance_intent(intent, "appops_ownership", evidence)
|
|
complete = complete_intent(
|
|
appops_owned,
|
|
{
|
|
"intentId": intent["intentId"],
|
|
"repository": REPOSITORY,
|
|
"applicationId": APPLICATION_ID,
|
|
"enrollmentId": registration["enrollmentId"],
|
|
"controllerReceiptSha256": registration["receiptSha256"],
|
|
"enrollmentStillPersisted": True,
|
|
"exactApplyPolicyStillPersisted": True,
|
|
"exactRollbackPolicyStillPersisted": True,
|
|
"controllerReceiptStillMatches": True,
|
|
"legacyVolumeRetained": True,
|
|
},
|
|
)
|
|
|
|
assert complete["phase"] == "completed"
|
|
assert complete["status"] == "completed"
|
|
assert "appopsOwnership" in complete["evidence"]
|
|
assert "completion" in complete["evidence"]
|
|
|
|
|
|
def test_step_replay_is_idempotent_but_changed_evidence_is_a_conflict() -> None:
|
|
intent = advance_intent(_intent(), "target_gates", _target_gates())
|
|
replay = advance_intent(intent, "target_gates", _target_gates())
|
|
changed = _target_gates()
|
|
changed["planSha256"] = _sha("9")
|
|
|
|
assert replay == intent
|
|
with pytest.raises(AdoptionProtocolError, match="idempotency_conflict"):
|
|
advance_intent(intent, "target_gates", changed)
|
|
|
|
|
|
def test_recovery_marker_keeps_last_good_phase_and_never_deletes_legacy_data() -> None:
|
|
intent = _to_snapshot_verified()
|
|
failed = mark_recovery_required(
|
|
intent,
|
|
"registry_receipt_uncertain",
|
|
{"rawError": "SECRET_OR_PATH_MUST_NOT_BE_STORED"},
|
|
)
|
|
|
|
assert failed["status"] == "recovery_required"
|
|
assert failed["phase"] == "snapshot_verified"
|
|
assert failed["legacyVolumePreserved"] is True
|
|
assert "legacyVolumePreserved" not in failed["recovery"] or failed["recovery"]["legacyVolumePreserved"] is True
|
|
assert "SECRET_OR_PATH_MUST_NOT_BE_STORED" not in str(failed)
|
|
with pytest.raises(AdoptionProtocolError, match="intent_requires_recovery"):
|
|
advance_intent(failed, "controller_registration", _registration(intent))
|
|
|
|
|
|
def test_recovery_reconciles_only_exact_controller_committed_registration() -> None:
|
|
intent = _to_snapshot_verified()
|
|
failed = mark_recovery_required(intent, "appops_commit_uncertain", {"timeout": True})
|
|
registration = _registration(intent)
|
|
receipt = {
|
|
"apiVersion": CONTROLLER_RECEIPT_VERSION,
|
|
"intentId": intent["intentId"],
|
|
"phase": "target_registered",
|
|
"receiptSha256": registration["receiptSha256"],
|
|
"repository": REPOSITORY,
|
|
"applicationId": APPLICATION_ID,
|
|
"legacyVolumeRetained": True,
|
|
"intentDurable": True,
|
|
"intentBindingSha256": intent_binding_sha256(intent),
|
|
"registrationEvidence": registration,
|
|
}
|
|
|
|
resumed = resume_intent(failed, receipt)
|
|
|
|
assert resumed["status"] == "in_progress"
|
|
assert resumed["phase"] == "target_registered"
|
|
assert resumed["evidence"]["controllerRegistration"]["receiptSha256"] == registration["receiptSha256"]
|
|
|
|
|
|
def test_recovery_replays_registered_target_without_repeating_cutover() -> None:
|
|
intent = _to_target_registered()
|
|
failed = mark_recovery_required(intent, "appops_commit_uncertain", {"commit": "timeout"})
|
|
registration = intent["evidence"]["controllerRegistration"]
|
|
receipt = {
|
|
"apiVersion": CONTROLLER_RECEIPT_VERSION,
|
|
"intentId": intent["intentId"],
|
|
"phase": "target_registered",
|
|
"receiptSha256": registration["receiptSha256"],
|
|
"repository": REPOSITORY,
|
|
"applicationId": APPLICATION_ID,
|
|
"legacyVolumeRetained": True,
|
|
"intentDurable": True,
|
|
"intentBindingSha256": intent_binding_sha256(intent),
|
|
"registrationEvidence": registration,
|
|
}
|
|
|
|
resumed = resume_intent(failed, receipt)
|
|
|
|
assert resumed["phase"] == "target_registered"
|
|
assert resumed["status"] == "in_progress"
|
|
assert resumed["receipts"]["controller_registration"] == intent["receipts"]["controller_registration"]
|
|
|
|
|
|
def test_recovery_receipt_for_another_repository_or_intent_is_rejected() -> None:
|
|
intent = mark_recovery_required(_to_snapshot_verified(), "registry_receipt_uncertain", {})
|
|
receipt = {
|
|
"apiVersion": CONTROLLER_RECEIPT_VERSION,
|
|
"intentId": "rpa-00000000000000000000",
|
|
"phase": "target_registered",
|
|
"receiptSha256": _sha("2"),
|
|
"repository": "jens/other",
|
|
"applicationId": APPLICATION_ID,
|
|
"legacyVolumeRetained": True,
|
|
"intentDurable": True,
|
|
"intentBindingSha256": intent_binding_sha256(intent),
|
|
"registrationEvidence": {},
|
|
}
|
|
|
|
with pytest.raises(AdoptionProtocolError, match="controller_recovery_receipt_unbound"):
|
|
resume_intent(intent, receipt)
|
|
|
|
|
|
def test_protocol_activation_reports_enabled_without_claiming_readiness() -> None:
|
|
assert EXECUTION_ENABLED is True
|
|
assert execution_status() == {
|
|
"apiVersion": "itworx.repopulse-legacy-adoption/v1",
|
|
"repository": REPOSITORY,
|
|
"status": "enabled",
|
|
"reason": None,
|
|
"executionEnabled": True,
|
|
}
|