Initial curated Unraid-AppOps-Gateway source from 3bd6f6fee310125bfbb4d4650389a817f498724d
This commit is contained in:
commit
1dfbca08a1
528 files changed
+131054
No files matched your search
@@ -0,0 +1,606 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
|
||||
import pytest
|
||||
from sqlmodel import Session, SQLModel, create_engine, select
|
||||
|
||||
from backend.app.db.models import (
|
||||
AppSetting,
|
||||
Enrollment,
|
||||
Policy,
|
||||
RepoPulseAdoptionJournal,
|
||||
)
|
||||
from backend.app.schemas.enrollment import EnrollmentPlanV1
|
||||
from backend.app.services import repopulse_adoption_journal as journal
|
||||
from backend.app.services import repopulse_adoption_ownership as ownership
|
||||
from backend.app.services.repopulse_legacy_adoption_protocol import (
|
||||
APPLICATION_ID,
|
||||
CONTROLLER_RECEIPT_VERSION,
|
||||
LEGACY_VOLUME,
|
||||
REPOSITORY,
|
||||
intent_binding_sha256,
|
||||
)
|
||||
from backend.app.services.secret_store import set_secret
|
||||
|
||||
|
||||
def _sha(char: str) -> str:
|
||||
return char * 64
|
||||
|
||||
|
||||
def _plan_payload(revision: str) -> dict:
|
||||
return EnrollmentPlanV1.model_validate(
|
||||
{
|
||||
"application_id": "repopulse",
|
||||
"display_name": "RepoPulse",
|
||||
"environment": "production",
|
||||
"repository": "Jens/RepoPulse",
|
||||
"revision": revision,
|
||||
"profile": "repopulse-authenticated",
|
||||
"target": {
|
||||
"target_identity": "unraid-controller",
|
||||
"container_name": "repopulse",
|
||||
},
|
||||
"storage": {"runtime_uid": 10001, "runtime_gid": 10001},
|
||||
"network": {"docker_network": "itworx-mcp-hub-npm", "internal_port": 8000},
|
||||
"artifact": {
|
||||
"release_version": "1.5.0",
|
||||
"image_repository": "repopulse-managed",
|
||||
"image_tag": "production",
|
||||
"health_path": "/health",
|
||||
},
|
||||
"exposure": {
|
||||
"hostname": "repopulse.example.com",
|
||||
"upstream_service": "repopulse",
|
||||
},
|
||||
"secrets": {"additional_secret_refs": ["REPOPULSE_GITEA_TOKEN"]},
|
||||
}
|
||||
).model_dump(mode="json")
|
||||
|
||||
|
||||
def _plan_sha(revision: str) -> str:
|
||||
payload = _plan_payload(revision)
|
||||
canonical = json.dumps(payload, sort_keys=True, separators=(",", ":")).encode()
|
||||
return hashlib.sha256(canonical).hexdigest()
|
||||
|
||||
|
||||
def _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": "enrollment-profile/v1",
|
||||
"planSha256": _plan_sha("a" * 40),
|
||||
**{
|
||||
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, *, enrollment_id: str | None = None) -> 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": enrollment_id or f"enr-{intent['intentId'][4:]}",
|
||||
"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 _ready_engine():
|
||||
engine = create_engine("sqlite://")
|
||||
SQLModel.metadata.create_all(engine)
|
||||
with Session(engine) as session:
|
||||
session.add(
|
||||
AppSetting(
|
||||
key="deployment_enrollment:trust",
|
||||
is_secret=False,
|
||||
value_json={
|
||||
"controller_identity": "unraid-controller",
|
||||
"bootstrap_secret_ref": "ENROLLMENT_BOOTSTRAP",
|
||||
"approved_networks": ["itworx-mcp-hub-npm"],
|
||||
"allowed_domain_suffixes": ["example.com"],
|
||||
"autonomous_profiles": ["repopulse-authenticated"],
|
||||
"repopulse_proxy_access_list_id": 1,
|
||||
},
|
||||
)
|
||||
)
|
||||
set_secret(
|
||||
session,
|
||||
"ENROLL_REPOPULSE_TARGET_HMAC",
|
||||
"test-target-credential-only-for-fixture",
|
||||
commit=False,
|
||||
)
|
||||
session.commit()
|
||||
return engine
|
||||
|
||||
|
||||
def _register_target(
|
||||
engine, *, enrollment_id: str | None = None
|
||||
) -> tuple[dict, dict, int]:
|
||||
preflight = _preflight()
|
||||
with Session(engine) as session:
|
||||
reserved = journal.reserve_repopulse_adoption(
|
||||
session, preflight, "repopulse-adoption-2026"
|
||||
)
|
||||
intent = journal.protocol.create_intent(preflight, "repopulse-adoption-2026")
|
||||
gates = _target_gates()
|
||||
with Session(engine) as session:
|
||||
journal.advance_repopulse_adoption(
|
||||
session, "target_gates", gates, expected_revision=reserved["revision"]
|
||||
)
|
||||
intent = journal.protocol.advance_intent(intent, "target_gates", gates)
|
||||
quiescence = _quiescence(intent)
|
||||
with Session(engine) as session:
|
||||
journal.confirm_repopulse_controller_intent(
|
||||
session, quiescence, expected_revision=2
|
||||
)
|
||||
intent = journal.protocol.advance_intent(intent, "quiescence", quiescence)
|
||||
snapshot = _snapshot(intent)
|
||||
with Session(engine) as session:
|
||||
journal.advance_repopulse_adoption(
|
||||
session, "snapshot", snapshot, expected_revision=3
|
||||
)
|
||||
intent = journal.protocol.advance_intent(intent, "snapshot", snapshot)
|
||||
registration = _registration(intent, enrollment_id=enrollment_id)
|
||||
with Session(engine) as session:
|
||||
registered = journal.advance_repopulse_adoption(
|
||||
session,
|
||||
"controller_registration",
|
||||
registration,
|
||||
expected_revision=4,
|
||||
)
|
||||
return intent, registration, registered["revision"]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def engine():
|
||||
db = _ready_engine()
|
||||
yield db
|
||||
db.dispose()
|
||||
|
||||
|
||||
def test_ownership_commit_is_atomic_fixed_and_replayable(engine) -> None:
|
||||
intent, registration, revision = _register_target(engine)
|
||||
|
||||
with Session(engine) as session:
|
||||
result = ownership.commit_repopulse_adoption_ownership(
|
||||
session,
|
||||
registration,
|
||||
expected_revision=revision,
|
||||
)
|
||||
|
||||
assert result["phase"] == "appops_owned"
|
||||
assert result["revision"] == revision + 1
|
||||
with Session(engine) as session:
|
||||
row = session.get(Enrollment, registration["enrollmentId"])
|
||||
assert row is not None
|
||||
assert row.repository == "jens/repopulse"
|
||||
assert row.application_id == "repopulse"
|
||||
assert row.revision == "a" * 40
|
||||
assert row.profile == "repopulse-authenticated"
|
||||
assert row.controller_identity == "unraid-controller"
|
||||
assert row.plan_sha256 == registration["planSha256"]
|
||||
assert row.target_secret_ref == "ENROLL_REPOPULSE_TARGET_HMAC"
|
||||
assert row.oidc_secret_ref is None
|
||||
assert row.plan_json == _plan_payload("a" * 40)
|
||||
assert row.provenance_json["createdBy"] == "repopulse-legacy-adoption/v1"
|
||||
assert (
|
||||
row.provenance_json["controllerReceiptSha256"]
|
||||
== registration["receiptSha256"]
|
||||
)
|
||||
policies = session.exec(select(Policy).order_by(Policy.id)).all()
|
||||
assert [policy.id for policy in policies] == [
|
||||
"enrollment-repopulse-deployment-apply",
|
||||
"enrollment-repopulse-deployment-rollback",
|
||||
]
|
||||
assert {
|
||||
(item.action, item.target, item.voice, item.manual, item.risk)
|
||||
for item in policies
|
||||
} == {
|
||||
("deployment.apply", "repopulse", "blocked", "allowed", "high"),
|
||||
("deployment.rollback", "repopulse", "blocked", "allowed", "high"),
|
||||
}
|
||||
assert all(
|
||||
item.provenance_json["adoptionIntentId"] == intent["intentId"]
|
||||
for item in policies
|
||||
)
|
||||
stored_journal = session.get(RepoPulseAdoptionJournal, "repopulse")
|
||||
assert stored_journal.phase == "appops_owned"
|
||||
assert (
|
||||
stored_journal.intent_json["evidence"]["appopsOwnership"][
|
||||
"secretReferenceReady"
|
||||
]
|
||||
is True
|
||||
)
|
||||
|
||||
with Session(engine) as session:
|
||||
replay = ownership.commit_repopulse_adoption_ownership(
|
||||
session,
|
||||
registration,
|
||||
expected_revision=revision + 1,
|
||||
)
|
||||
assert replay["phase"] == "appops_owned"
|
||||
assert replay["revision"] == revision + 1
|
||||
|
||||
|
||||
def test_ownership_commit_rejects_live_controller_receipt_drift(engine) -> None:
|
||||
_, registration, revision = _register_target(engine)
|
||||
changed = {**registration, "deploymentReceiptSha256": _sha("5")}
|
||||
|
||||
with (
|
||||
Session(engine) as session,
|
||||
pytest.raises(ownership.RepoPulseOwnershipError) as error,
|
||||
):
|
||||
ownership.commit_repopulse_adoption_ownership(
|
||||
session, changed, expected_revision=revision
|
||||
)
|
||||
|
||||
assert error.value.code == "idempotency_conflict"
|
||||
with Session(engine) as session:
|
||||
assert session.exec(select(Enrollment)).all() == []
|
||||
assert session.exec(select(Policy)).all() == []
|
||||
|
||||
|
||||
def test_enrollment_identity_is_derived_from_persisted_intent(engine) -> None:
|
||||
_, registration, revision = _register_target(
|
||||
engine, enrollment_id="enr-0123456789abcdefabcd"
|
||||
)
|
||||
|
||||
with (
|
||||
Session(engine) as session,
|
||||
pytest.raises(ownership.RepoPulseOwnershipError) as error,
|
||||
):
|
||||
ownership.commit_repopulse_adoption_ownership(
|
||||
session, registration, expected_revision=revision
|
||||
)
|
||||
|
||||
assert error.value.code == "fixed_enrollment_id_mismatch"
|
||||
with Session(engine) as session:
|
||||
assert session.exec(select(Enrollment)).all() == []
|
||||
assert session.exec(select(Policy)).all() == []
|
||||
|
||||
|
||||
def test_preexisting_exact_policy_id_is_not_adopted(engine) -> None:
|
||||
_, registration, revision = _register_target(engine)
|
||||
with Session(engine) as session:
|
||||
session.add(
|
||||
Policy(
|
||||
id="enrollment-repopulse-deployment-apply",
|
||||
action="deployment.apply",
|
||||
target="repopulse",
|
||||
voice="blocked",
|
||||
manual="denied",
|
||||
risk="high",
|
||||
)
|
||||
)
|
||||
session.commit()
|
||||
|
||||
with (
|
||||
Session(engine) as session,
|
||||
pytest.raises(ownership.RepoPulseOwnershipError) as error,
|
||||
):
|
||||
ownership.commit_repopulse_adoption_ownership(
|
||||
session, registration, expected_revision=revision
|
||||
)
|
||||
assert error.value.code == "ownership_resource_conflict"
|
||||
with Session(engine) as session:
|
||||
assert session.exec(select(Enrollment)).all() == []
|
||||
assert (
|
||||
session.get(Policy, "enrollment-repopulse-deployment-apply").manual
|
||||
== "denied"
|
||||
)
|
||||
assert session.get(Policy, "enrollment-repopulse-deployment-rollback") is None
|
||||
assert (
|
||||
session.get(RepoPulseAdoptionJournal, "repopulse").phase
|
||||
== "target_registered"
|
||||
)
|
||||
|
||||
|
||||
def test_ownership_commit_rejects_missing_fixed_secret_ref(engine) -> None:
|
||||
_, registration, revision = _register_target(engine)
|
||||
with Session(engine) as session:
|
||||
from backend.app.db.models import Secret
|
||||
|
||||
session.delete(session.get(Secret, "ENROLL_REPOPULSE_TARGET_HMAC"))
|
||||
session.commit()
|
||||
|
||||
with (
|
||||
Session(engine) as session,
|
||||
pytest.raises(ownership.RepoPulseOwnershipError) as error,
|
||||
):
|
||||
ownership.commit_repopulse_adoption_ownership(
|
||||
session, registration, expected_revision=revision
|
||||
)
|
||||
assert error.value.code == "target_secret_reference_unavailable"
|
||||
with Session(engine) as session:
|
||||
assert session.exec(select(Enrollment)).all() == []
|
||||
assert session.exec(select(Policy)).all() == []
|
||||
|
||||
|
||||
def test_completion_is_a_separate_idempotent_step_bound_to_live_receipt(engine) -> None:
|
||||
_, registration, revision = _register_target(engine)
|
||||
with Session(engine) as session:
|
||||
owned = ownership.commit_repopulse_adoption_ownership(
|
||||
session, registration, expected_revision=revision
|
||||
)
|
||||
|
||||
with Session(engine) as session:
|
||||
completed = ownership.complete_repopulse_adoption(
|
||||
session,
|
||||
registration,
|
||||
expected_revision=owned["revision"],
|
||||
)
|
||||
assert completed["phase"] == "completed"
|
||||
assert completed["status"] == "completed"
|
||||
assert completed["revision"] == owned["revision"] + 1
|
||||
|
||||
with Session(engine) as session:
|
||||
replay = ownership.complete_repopulse_adoption(
|
||||
session,
|
||||
registration,
|
||||
expected_revision=completed["revision"],
|
||||
)
|
||||
assert replay["revision"] == completed["revision"]
|
||||
assert replay["phase"] == "completed"
|
||||
|
||||
with Session(engine) as session:
|
||||
ownership_replay = ownership.commit_repopulse_adoption_ownership(
|
||||
session, registration, expected_revision=completed["revision"]
|
||||
)
|
||||
assert ownership_replay["revision"] == completed["revision"]
|
||||
assert ownership_replay["phase"] == "completed"
|
||||
|
||||
changed = {**registration, "receiptSha256": _sha("9")}
|
||||
with (
|
||||
Session(engine) as session,
|
||||
pytest.raises(ownership.RepoPulseOwnershipError) as error,
|
||||
):
|
||||
ownership.complete_repopulse_adoption(
|
||||
session, changed, expected_revision=completed["revision"]
|
||||
)
|
||||
assert error.value.code == "idempotency_conflict"
|
||||
|
||||
|
||||
def test_completion_replay_rechecks_owned_policy_rows(engine) -> None:
|
||||
_, registration, revision = _register_target(engine)
|
||||
with Session(engine) as session:
|
||||
owned = ownership.commit_repopulse_adoption_ownership(
|
||||
session, registration, expected_revision=revision
|
||||
)
|
||||
with Session(engine) as session:
|
||||
completed = ownership.complete_repopulse_adoption(
|
||||
session, registration, expected_revision=owned["revision"]
|
||||
)
|
||||
with Session(engine) as session:
|
||||
policy = session.get(Policy, "enrollment-repopulse-deployment-apply")
|
||||
policy.manual = "denied"
|
||||
session.add(policy)
|
||||
session.commit()
|
||||
|
||||
with (
|
||||
Session(engine) as session,
|
||||
pytest.raises(ownership.RepoPulseOwnershipError) as error,
|
||||
):
|
||||
ownership.complete_repopulse_adoption(
|
||||
session, registration, expected_revision=completed["revision"]
|
||||
)
|
||||
assert error.value.code == "ownership_policy_readback_mismatch"
|
||||
|
||||
|
||||
def test_half_write_failure_rolls_back_all_ownership_rows_and_marks_recovery(
|
||||
engine, monkeypatch
|
||||
) -> None:
|
||||
_, registration, revision = _register_target(engine)
|
||||
original_commit = Session.commit
|
||||
calls = 0
|
||||
|
||||
def fail_first_commit(session):
|
||||
nonlocal calls
|
||||
calls += 1
|
||||
if calls == 1:
|
||||
raise RuntimeError("simulated commit interruption")
|
||||
return original_commit(session)
|
||||
|
||||
monkeypatch.setattr(Session, "commit", fail_first_commit)
|
||||
with (
|
||||
Session(engine) as session,
|
||||
pytest.raises(ownership.RepoPulseOwnershipError) as error,
|
||||
):
|
||||
ownership.commit_repopulse_adoption_ownership(
|
||||
session, registration, expected_revision=revision
|
||||
)
|
||||
assert error.value.code == "appops_commit_uncertain"
|
||||
with Session(engine) as session:
|
||||
assert session.exec(select(Enrollment)).all() == []
|
||||
assert session.exec(select(Policy)).all() == []
|
||||
stored = session.get(RepoPulseAdoptionJournal, "repopulse")
|
||||
assert stored.phase == "target_registered"
|
||||
assert stored.status == "recovery_required"
|
||||
assert stored.intent_json["recovery"]["reason"] == "appops_commit_uncertain"
|
||||
|
||||
|
||||
def test_commit_exception_after_durable_commit_is_resolved_by_readback(
|
||||
engine, monkeypatch
|
||||
) -> None:
|
||||
_, registration, revision = _register_target(engine)
|
||||
original_commit = Session.commit
|
||||
calls = 0
|
||||
|
||||
def commit_then_lose_response(session):
|
||||
nonlocal calls
|
||||
calls += 1
|
||||
if calls == 1:
|
||||
original_commit(session)
|
||||
raise RuntimeError("simulated lost commit response")
|
||||
return original_commit(session)
|
||||
|
||||
monkeypatch.setattr(Session, "commit", commit_then_lose_response)
|
||||
with Session(engine) as session:
|
||||
result = ownership.commit_repopulse_adoption_ownership(
|
||||
session, registration, expected_revision=revision
|
||||
)
|
||||
|
||||
assert result["phase"] == "appops_owned"
|
||||
with Session(engine) as session:
|
||||
assert session.get(Enrollment, registration["enrollmentId"]) is not None
|
||||
stored = session.get(RepoPulseAdoptionJournal, "repopulse")
|
||||
assert stored.phase == "appops_owned"
|
||||
assert stored.status == "in_progress"
|
||||
Reference in new issue
Block a user