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,631 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import importlib
|
||||
import json
|
||||
|
||||
import pytest
|
||||
from sqlmodel import Session, SQLModel, create_engine
|
||||
|
||||
from backend.app.core.config import settings
|
||||
from backend.app.db.models import AppSetting
|
||||
from backend.app.schemas.enrollment import EnrollmentPlanV1
|
||||
from backend.app.services import repopulse_adoption_journal as journal
|
||||
from backend.app.services import repopulse_legacy_adoption_protocol as protocol
|
||||
from backend.app.services.secret_store import set_secret
|
||||
|
||||
|
||||
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": "jens/repopulse",
|
||||
"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 _plan_sha(revision: str) -> str:
|
||||
plan = 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")
|
||||
encoded = json.dumps(plan, sort_keys=True, separators=(",", ":")).encode("utf-8")
|
||||
return hashlib.sha256(encoded).hexdigest()
|
||||
|
||||
|
||||
def _document_sha(value: dict) -> str:
|
||||
encoded = (json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=True) + "\n").encode("ascii")
|
||||
return hashlib.sha256(encoded).hexdigest()
|
||||
|
||||
|
||||
def _completion_sha(value: dict) -> str:
|
||||
encoded = json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=True).encode("ascii")
|
||||
return hashlib.sha256(encoded).hexdigest()
|
||||
|
||||
|
||||
def _controller_result(intent_id: str, binding: str) -> dict:
|
||||
plan_sha = _plan_sha("a" * 40)
|
||||
gates = {
|
||||
"profileId": "repopulse-authenticated",
|
||||
"profileVersion": "enrollment-profile/v1",
|
||||
"planSha256": plan_sha,
|
||||
**{
|
||||
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",
|
||||
)
|
||||
},
|
||||
}
|
||||
quiescence = {
|
||||
"intentId": intent_id,
|
||||
"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": "repopulse_data",
|
||||
"controllerIntentDurable": True,
|
||||
"controllerIntentBindingSha256": binding,
|
||||
}
|
||||
snapshot = {
|
||||
"intentId": intent_id,
|
||||
"globalDeployLockHeld": True,
|
||||
"projectLockHeld": True,
|
||||
"legacyContainerStopped": True,
|
||||
"legacyVolume": "repopulse_data",
|
||||
"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": binding,
|
||||
}
|
||||
copied = {
|
||||
"intentId": intent_id,
|
||||
"snapshotFileSha256": _sha("f"),
|
||||
"targetLogicalSha256": _sha("e"),
|
||||
"targetScanCount": 572,
|
||||
"targetCompactedScanCount": 96,
|
||||
"targetQueueSha256": _sha("1"),
|
||||
"sourceAuxiliaryTreeSha256": _sha("3"),
|
||||
"targetAuxiliaryTreeSha256": _sha("3"),
|
||||
"sourceAuxiliaryFileCount": 42,
|
||||
"targetAuxiliaryFileCount": 42,
|
||||
"targetQuickCheck": "ok",
|
||||
"targetIntegrityCheck": "ok",
|
||||
"targetDataUid": 10001,
|
||||
"targetDataGid": 10001,
|
||||
"targetRecoveryStatus": "healthy",
|
||||
"managedDataMountMatches": True,
|
||||
"copySourceIsVerifiedSnapshot": True,
|
||||
"legacyVolumeRetained": True,
|
||||
"legacyEnvironmentRetained": True,
|
||||
"managedEnvironmentFilePrivate": True,
|
||||
}
|
||||
enrollment_id = f"enr-{intent_id[4:]}"
|
||||
registry_receipt = {
|
||||
"apiVersion": "itworx.repopulse-controller-adoption-store-receipt/v1",
|
||||
"intentId": intent_id,
|
||||
"repository": "jens/repopulse",
|
||||
"applicationId": "repopulse",
|
||||
"phase": "registry_committed",
|
||||
"intentBindingSha256": binding,
|
||||
"legacyVolumeRetained": True,
|
||||
"intentDurable": True,
|
||||
"managedEnrollmentId": enrollment_id,
|
||||
"projectsSha256": _sha("2"),
|
||||
"snapshotFileSha256": _sha("f"),
|
||||
}
|
||||
registry_receipt["receiptSha256"] = _document_sha(registry_receipt)
|
||||
deployment_receipt = {
|
||||
"repository": "jens/repopulse",
|
||||
"revision": "a" * 40,
|
||||
"containerRevision": "a" * 40,
|
||||
"containerIdSha256": _sha("4"),
|
||||
"deploymentReceiptSchema": "gitea-deploy-receipt.v1",
|
||||
"deploymentReceiptSha256": _sha("5"),
|
||||
"deploymentReceiptVerified": True,
|
||||
"runtimeHealthy": True,
|
||||
"publishedHostPorts": [],
|
||||
"managedDataMountMatches": True,
|
||||
"targetDataUid": 10001,
|
||||
"targetDataGid": 10001,
|
||||
"targetLogicalSha256": _sha("e"),
|
||||
"targetScanCount": 572,
|
||||
"targetCompactedScanCount": 96,
|
||||
"targetQueueSha256": _sha("1"),
|
||||
"targetAuxiliaryTreeSha256": _sha("3"),
|
||||
"targetAuxiliaryFileCount": 42,
|
||||
"targetQuickCheck": "ok",
|
||||
"targetIntegrityCheck": "ok",
|
||||
"targetRecoveryStatus": "healthy",
|
||||
"targetQueuedJobs": 0,
|
||||
"targetActiveJobs": 0,
|
||||
"legacyVolume": "repopulse_data",
|
||||
"legacyVolumeRetained": True,
|
||||
"legacyEnvironmentRetained": True,
|
||||
"managedEnvironmentFilePrivate": True,
|
||||
"scanSourceMatches": True,
|
||||
"scanMountReadOnly": True,
|
||||
"scanPermissionPolicyPersistent": True,
|
||||
"scanAccessForRuntimeIdentityReady": True,
|
||||
"containerName": "repopulse",
|
||||
"containerImageSha256": _sha("6"),
|
||||
"snapshotFileSha256": _sha("f"),
|
||||
}
|
||||
completion = {
|
||||
"apiVersion": "itworx.repopulse-controller-adoption-completion/v1",
|
||||
"intentId": intent_id,
|
||||
"intentBindingSha256": binding,
|
||||
"controllerRegistrationReceiptSha256": registry_receipt["receiptSha256"],
|
||||
"mainRevision": "a" * 40,
|
||||
"deploymentReceiptSha256": deployment_receipt["deploymentReceiptSha256"],
|
||||
"snapshotFileSha256": snapshot["snapshotFileSha256"],
|
||||
"snapshotLogicalSha256": snapshot["snapshotLogicalSha256"],
|
||||
"snapshotQueueSha256": snapshot["snapshotQueueSha256"],
|
||||
"auxiliaryTreeSha256": copied["targetAuxiliaryTreeSha256"],
|
||||
"containerIdSha256": deployment_receipt["containerIdSha256"],
|
||||
"containerImageSha256": deployment_receipt["containerImageSha256"],
|
||||
"legacyVolume": "repopulse_data",
|
||||
"legacyVolumeRetained": True,
|
||||
}
|
||||
completion["completionSha256"] = _completion_sha(completion)
|
||||
return {
|
||||
"apiVersion": "itworx.repopulse-controller-adoption-execution-result/v1",
|
||||
"repository": "jens/repopulse",
|
||||
"applicationId": "repopulse",
|
||||
"intentId": intent_id,
|
||||
"status": "deployment_verified",
|
||||
"phase": "completion_verified",
|
||||
"legacyVolume": "repopulse_data",
|
||||
"legacyVolumeRetained": True,
|
||||
"targetGates": gates,
|
||||
"exactShaValidation": {
|
||||
"repository": "jens/repopulse",
|
||||
"revision": "a" * 40,
|
||||
"contexts": {"Managed validation / full (push)": "success"},
|
||||
},
|
||||
"quiescence": quiescence,
|
||||
"snapshot": snapshot,
|
||||
"copy": copied,
|
||||
"registryReceipt": registry_receipt,
|
||||
"deploymentReceipt": deployment_receipt,
|
||||
"completion": completion,
|
||||
}
|
||||
|
||||
|
||||
class FakePreflight:
|
||||
def __init__(self) -> None:
|
||||
self.calls = 0
|
||||
|
||||
def run(self) -> dict:
|
||||
self.calls += 1
|
||||
return _passing_preflight()
|
||||
|
||||
|
||||
class FakeBroker:
|
||||
base_url = "https://controller.test"
|
||||
verify_tls = True
|
||||
ca_file = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
reserve_result: dict | None = None,
|
||||
status_result: dict | None = None,
|
||||
) -> None:
|
||||
self.reserve_result = reserve_result
|
||||
self.status_result = status_result
|
||||
self.execute_calls: list[dict] = []
|
||||
self.status_calls = 0
|
||||
self.reserve_calls: list[dict] = []
|
||||
|
||||
def configuration_error(self):
|
||||
return None
|
||||
|
||||
def repopulse_adoption_capabilities(self, *, secret: str) -> dict:
|
||||
return {
|
||||
"apiVersion": "itworx.repopulse-adoption-capabilities/v1",
|
||||
"status": "ready",
|
||||
"repository": "jens/repopulse",
|
||||
"adoptionStore": {"executionEnabled": True},
|
||||
"privateTls": {"status": "ready"},
|
||||
"controller": {
|
||||
"aliases": "ready",
|
||||
"registry": "ready",
|
||||
"secretRoot": "ready",
|
||||
},
|
||||
}
|
||||
|
||||
def repopulse_adoption_reserve(self, **kwargs) -> dict:
|
||||
self.reserve_calls.append(kwargs)
|
||||
if self.reserve_result is not None:
|
||||
return self.reserve_result
|
||||
binding = hashlib.sha256(
|
||||
json.dumps(
|
||||
{
|
||||
"intentId": kwargs["intent_id"],
|
||||
"repository": "jens/repopulse",
|
||||
"applicationId": "repopulse",
|
||||
"preflightSha256": kwargs["preflight_sha256"],
|
||||
"idempotencySha256": kwargs["idempotency_sha256"],
|
||||
},
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
ensure_ascii=True,
|
||||
).encode("ascii")
|
||||
).hexdigest()
|
||||
return {
|
||||
"apiVersion": "itworx.repopulse-adoption-reserve/v1",
|
||||
"status": "reserved",
|
||||
"intentId": kwargs["intent_id"],
|
||||
"phase": "reserved",
|
||||
"intentSha256": _sha("7"),
|
||||
"intentBindingSha256": binding,
|
||||
}
|
||||
|
||||
def repopulse_adoption_execute(self, **kwargs) -> dict:
|
||||
self.execute_calls.append(kwargs)
|
||||
raise AssertionError("AppOps must not invoke the controller cutover")
|
||||
|
||||
def repopulse_adoption_status(self, *, secret: str) -> dict:
|
||||
self.status_calls += 1
|
||||
return self.status_result or {
|
||||
"apiVersion": "itworx.repopulse-adoption-status/v1",
|
||||
"status": "not_reserved",
|
||||
"phase": None,
|
||||
"hashes": {},
|
||||
}
|
||||
|
||||
|
||||
def _runner_type():
|
||||
try:
|
||||
module = importlib.import_module("backend.app.services.repopulse_adoption_runner")
|
||||
except ModuleNotFoundError:
|
||||
pytest.fail("fixed RepoPulse AppOps runner is missing")
|
||||
return module.RepoPulseAdoptionRunner
|
||||
|
||||
|
||||
def _controller_status(intent: dict, controller_result: dict) -> dict:
|
||||
snapshot = controller_result["snapshot"]
|
||||
gates = controller_result["targetGates"]
|
||||
registry = controller_result["registryReceipt"]
|
||||
return {
|
||||
"apiVersion": "itworx.repopulse-adoption-status/v1",
|
||||
"status": "in_progress",
|
||||
"phase": "snapshot_verified",
|
||||
"hashes": {
|
||||
"intentSha256": _sha("7"),
|
||||
"intentBindingSha256": protocol.intent_binding_sha256(intent),
|
||||
"idempotencySha256": intent["idempotencySha256"],
|
||||
"preflightSha256": intent["preflightSha256"],
|
||||
"expectedProjectsSha256": registry["projectsSha256"],
|
||||
"quiescenceSha256": _sha("d"),
|
||||
"planSha256": gates["planSha256"],
|
||||
"snapshotFileSha256": snapshot["snapshotFileSha256"],
|
||||
"snapshotLogicalSha256": snapshot["snapshotLogicalSha256"],
|
||||
"snapshotQueueSha256": snapshot["snapshotQueueSha256"],
|
||||
"targetProjectsSha256": registry["projectsSha256"],
|
||||
"receiptSha256": registry["receiptSha256"],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _session(monkeypatch) -> Session:
|
||||
monkeypatch.setattr(settings, "environment", "production")
|
||||
monkeypatch.setattr(settings, "secret_key", "appops-test-key-with-at-least-32-bytes")
|
||||
monkeypatch.setattr(settings, "secret_key_source", "env")
|
||||
engine = create_engine("sqlite://")
|
||||
SQLModel.metadata.create_all(engine)
|
||||
session = Session(engine)
|
||||
session.add(
|
||||
AppSetting(
|
||||
key="deployment_enrollment:trust",
|
||||
is_secret=False,
|
||||
value_json={
|
||||
"controller_identity": "unraid-controller",
|
||||
"bootstrap_secret_ref": "APPOPS_DEPLOYMENT_BOOTSTRAP",
|
||||
"approved_networks": ["itworx-mcp-hub-npm"],
|
||||
"allowed_domain_suffixes": ["example.com"],
|
||||
"autonomous_profiles": ["repopulse-authenticated"],
|
||||
"repopulse_proxy_access_list_id": 1,
|
||||
},
|
||||
)
|
||||
)
|
||||
session.commit()
|
||||
set_secret(session, "APPOPS_DEPLOYMENT_BOOTSTRAP", "B" * 64)
|
||||
return session
|
||||
|
||||
|
||||
def test_synthetic_prepare_and_finalize_commits_protocol_ownership_and_completion(monkeypatch):
|
||||
runner_type = _runner_type()
|
||||
monkeypatch.setattr(protocol, "EXECUTION_ENABLED", True)
|
||||
session = _session(monkeypatch)
|
||||
preflight = FakePreflight()
|
||||
report = _passing_preflight()
|
||||
candidate = protocol.create_intent(report, "repopulse-adoption-2026")
|
||||
binding = protocol.intent_binding_sha256(candidate)
|
||||
controller_result = _controller_result(candidate["intentId"], binding)
|
||||
broker = FakeBroker()
|
||||
runner = runner_type(session=session, preflight=preflight, broker=broker)
|
||||
|
||||
handoff = runner.prepare_controller_cli_handoff("repopulse-adoption-2026")
|
||||
prepared = journal.get_repopulse_adoption_status(session)
|
||||
broker.status_result = _controller_status(candidate, controller_result)
|
||||
result = runner.finalize(controller_result)
|
||||
|
||||
assert prepared["status"] == "in_progress"
|
||||
assert prepared["phase"] == "reserved"
|
||||
assert handoff["intentId"] == candidate["intentId"]
|
||||
assert result["status"] == "completed"
|
||||
assert result["phase"] == "completed"
|
||||
assert result["revision"] == 7
|
||||
assert preflight.calls == 1
|
||||
assert len(broker.reserve_calls) == 1
|
||||
assert broker.reserve_calls[0]["intent_id"] == candidate["intentId"]
|
||||
assert broker.reserve_calls[0]["preflight_sha256"] == candidate["preflightSha256"]
|
||||
assert broker.execute_calls == []
|
||||
|
||||
|
||||
def test_prepared_and_completed_replays_never_repeat_external_mutations(monkeypatch):
|
||||
runner_type = _runner_type()
|
||||
monkeypatch.setattr(protocol, "EXECUTION_ENABLED", True)
|
||||
session = _session(monkeypatch)
|
||||
report = _passing_preflight()
|
||||
candidate = protocol.create_intent(report, "repopulse-adoption-2026")
|
||||
binding = protocol.intent_binding_sha256(candidate)
|
||||
controller_result = _controller_result(candidate["intentId"], binding)
|
||||
broker = FakeBroker()
|
||||
runner = runner_type(session=session, preflight=FakePreflight(), broker=broker)
|
||||
|
||||
broker.status_result = _controller_status(candidate, controller_result)
|
||||
handoff = runner.prepare_controller_cli_handoff("repopulse-adoption-2026")
|
||||
replay = runner.prepare("repopulse-adoption-2026")
|
||||
completed = runner.finalize(controller_result)
|
||||
completed_replay = runner.prepare("repopulse-adoption-2026")
|
||||
|
||||
assert handoff["intentId"] == candidate["intentId"]
|
||||
assert replay["status"] == "prepared"
|
||||
assert completed["status"] == completed_replay["status"] == "completed"
|
||||
assert completed_replay["revision"] == completed["revision"]
|
||||
assert len(broker.reserve_calls) == 1
|
||||
assert broker.execute_calls == []
|
||||
|
||||
|
||||
def test_controller_cli_handoff_is_exact_in_memory_contract_and_not_printed(monkeypatch, capsys):
|
||||
runner_type = _runner_type()
|
||||
monkeypatch.setattr(protocol, "EXECUTION_ENABLED", True)
|
||||
session = _session(monkeypatch)
|
||||
report = _passing_preflight()
|
||||
candidate = protocol.create_intent(report, "repopulse-adoption-2026")
|
||||
broker = FakeBroker()
|
||||
runner = runner_type(session=session, preflight=FakePreflight(), broker=broker)
|
||||
|
||||
handoff = runner.prepare_controller_cli_handoff("repopulse-adoption-2026")
|
||||
|
||||
assert set(handoff) == {
|
||||
"intentId",
|
||||
"idempotencySha256",
|
||||
"preflightSha256",
|
||||
"preflightProjection",
|
||||
"targetCredential",
|
||||
}
|
||||
assert handoff["intentId"] == candidate["intentId"]
|
||||
assert handoff["idempotencySha256"] == candidate["idempotencySha256"]
|
||||
assert handoff["preflightSha256"] == candidate["preflightSha256"]
|
||||
assert handoff["preflightProjection"] == candidate["evidence"]["preflight"]
|
||||
assert isinstance(handoff["targetCredential"], str)
|
||||
assert 32 <= len(handoff["targetCredential"]) <= 256
|
||||
assert capsys.readouterr().out == ""
|
||||
assert len(broker.reserve_calls) == 1
|
||||
assert broker.execute_calls == []
|
||||
|
||||
|
||||
def test_controller_cli_handoff_cannot_be_reissued(monkeypatch):
|
||||
runner_type = _runner_type()
|
||||
monkeypatch.setattr(protocol, "EXECUTION_ENABLED", True)
|
||||
session = _session(monkeypatch)
|
||||
candidate = protocol.create_intent(_passing_preflight(), "repopulse-adoption-2026")
|
||||
controller_result = _controller_result(
|
||||
candidate["intentId"], protocol.intent_binding_sha256(candidate)
|
||||
)
|
||||
broker = FakeBroker(status_result=_controller_status(candidate, controller_result))
|
||||
runner = runner_type(session=session, preflight=FakePreflight(), broker=broker)
|
||||
|
||||
first = runner.prepare_controller_cli_handoff("repopulse-adoption-2026")
|
||||
with pytest.raises(ValueError, match="controller_cli_handoff_replay_requires_recovery"):
|
||||
runner.prepare_controller_cli_handoff("repopulse-adoption-2026")
|
||||
|
||||
status = journal.get_repopulse_adoption_status(session)
|
||||
assert first["intentId"] == candidate["intentId"]
|
||||
assert status["status"] == "recovery_required"
|
||||
assert status["phase"] == "reserved"
|
||||
assert len(broker.reserve_calls) == 1
|
||||
assert broker.execute_calls == []
|
||||
|
||||
|
||||
def test_operator_runner_hydrates_portainer_settings_without_fastapi_lifespan(monkeypatch):
|
||||
runner_type = _runner_type()
|
||||
monkeypatch.setattr(protocol, "EXECUTION_ENABLED", True)
|
||||
session = _session(monkeypatch)
|
||||
calls = []
|
||||
runner_module = importlib.import_module(
|
||||
"backend.app.services.repopulse_adoption_runner"
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
runner_module,
|
||||
"apply_stored_connector_settings",
|
||||
lambda active_session, connector_ids: calls.append(
|
||||
(active_session, connector_ids)
|
||||
),
|
||||
)
|
||||
|
||||
runner = runner_type(session=session, preflight=FakePreflight(), broker=FakeBroker())
|
||||
assert calls == []
|
||||
runner.prepare("repopulse-adoption-2026")
|
||||
|
||||
assert calls == [(session, {"portainer"})]
|
||||
|
||||
|
||||
def test_execution_flag_remains_fail_closed_in_source(monkeypatch):
|
||||
runner_type = _runner_type()
|
||||
monkeypatch.setattr(protocol, "EXECUTION_ENABLED", False)
|
||||
session = _session(monkeypatch)
|
||||
preflight = FakePreflight()
|
||||
broker = FakeBroker()
|
||||
runner = runner_type(session=session, preflight=preflight, broker=broker)
|
||||
|
||||
assert runner.prepare("repopulse-adoption-2026")["status"] == "disabled"
|
||||
assert preflight.calls == 0
|
||||
assert broker.reserve_calls == []
|
||||
assert broker.execute_calls == []
|
||||
|
||||
|
||||
def test_tampered_completion_is_rejected_and_marked_for_recovery(monkeypatch):
|
||||
runner_type = _runner_type()
|
||||
monkeypatch.setattr(protocol, "EXECUTION_ENABLED", True)
|
||||
session = _session(monkeypatch)
|
||||
candidate = protocol.create_intent(_passing_preflight(), "repopulse-adoption-2026")
|
||||
binding = protocol.intent_binding_sha256(candidate)
|
||||
controller_result = _controller_result(candidate["intentId"], binding)
|
||||
broker = FakeBroker()
|
||||
runner = runner_type(session=session, preflight=FakePreflight(), broker=broker)
|
||||
runner.prepare_controller_cli_handoff("repopulse-adoption-2026")
|
||||
broker.status_result = _controller_status(candidate, controller_result)
|
||||
controller_result["completion"]["completionSha256"] = _sha("0")
|
||||
|
||||
result = runner.finalize(controller_result)
|
||||
|
||||
assert result["status"] == "recovery_required"
|
||||
assert result["phase"] == "reserved"
|
||||
assert broker.status_calls == 1
|
||||
assert broker.execute_calls == []
|
||||
|
||||
|
||||
def test_ambiguous_controller_reserve_queries_status_and_requires_recovery(monkeypatch):
|
||||
runner_type = _runner_type()
|
||||
monkeypatch.setattr(protocol, "EXECUTION_ENABLED", True)
|
||||
session = _session(monkeypatch)
|
||||
broker = FakeBroker(
|
||||
reserve_result={
|
||||
"status": "unreachable",
|
||||
"reason": "secret-bearing raw transport error",
|
||||
}
|
||||
)
|
||||
runner = runner_type(session=session, preflight=FakePreflight(), broker=broker)
|
||||
|
||||
result = runner.prepare("repopulse-adoption-2026")
|
||||
|
||||
assert result["status"] == "recovery_required"
|
||||
assert result["reason"] == "unknown_external_effect"
|
||||
assert broker.status_calls == 1
|
||||
assert len(broker.reserve_calls) == 1
|
||||
assert broker.execute_calls == []
|
||||
assert "secret-bearing raw transport error" not in json.dumps(result)
|
||||
|
||||
replay = runner.prepare("repopulse-adoption-2026")
|
||||
assert replay["status"] == "recovery_required"
|
||||
assert len(broker.reserve_calls) == 1
|
||||
Reference in new issue
Block a user