feat: add institutional governance and recovery contracts

This commit is contained in:
2026-08-01 17:46:54 +02:00
parent b65b48832b
commit 7192d32e65
61 changed files with 12539 additions and 168 deletions
@@ -0,0 +1,242 @@
from __future__ import annotations
import unittest
from govoplan_core.core.configuration_packages import (
ConfigurationModuleRequirement,
ConfigurationPackageEvidence,
ConfigurationPackageManifest,
ConfigurationPackageParent,
ConfigurationPreflightContext,
ConfigurationProviderExpectation,
dry_run_configuration_package,
validate_configuration_package_derivation,
)
def _evidence(*kinds: str) -> tuple[ConfigurationPackageEvidence, ...]:
return tuple(
ConfigurationPackageEvidence(
kind=kind, # type: ignore[arg-type]
reference=f"evidence/{kind}.json",
summary=f"Evidence for {kind}.",
checksum=f"sha256:{'a' * 64}",
)
for kind in kinds
)
class ConfigurationPackageArchitectureTests(unittest.TestCase):
def test_legacy_package_defaults_to_product_and_round_trips(self) -> None:
package = ConfigurationPackageManifest.from_mapping(
{"package_id": "example", "name": "Example", "version": "1.0.0"}
)
self.assertEqual(package.package_class, "product")
self.assertEqual(
ConfigurationPackageManifest.from_mapping(package.to_dict()),
package,
)
def test_reference_package_requires_operational_evidence(self) -> None:
with self.assertRaisesRegex(ValueError, "accessibility"):
ConfigurationPackageManifest(
package_id="reference.example",
name="Example reference",
version="1.0.0",
package_class="reference",
evidence=_evidence("target_test", "recovery"),
)
def test_reference_evidence_requires_content_checksums(self) -> None:
with self.assertRaisesRegex(ValueError, "without checksums"):
ConfigurationPackageManifest(
package_id="reference.unbound",
name="Unbound reference",
version="1.0.0",
package_class="reference",
evidence=tuple(
ConfigurationPackageEvidence(
kind=kind, # type: ignore[arg-type]
reference=f"evidence/{kind}.json",
summary=f"{kind} evidence.",
checksum=(
None
if kind == "target_test"
else f"sha256:{'b' * 64}"
),
)
for kind in (
"target_test",
"recovery",
"security",
"operations",
"accessibility",
"privacy",
"documentation",
)
),
)
def test_integration_package_preflight_checks_provider_contract_and_health(self) -> None:
package = ConfigurationPackageManifest(
package_id="integration.example",
name="Example integration",
version="1.0.0",
package_class="integration",
evidence=_evidence(
"target_test", "recovery", "operations", "documentation"
),
provider_expectations=(
ConfigurationProviderExpectation(
provider_id="connectors.example",
authority_mode="external_mirror",
minimum_maturity="read",
),
),
)
healthy = dry_run_configuration_package(
package,
(),
ConfigurationPreflightContext(
external_provider_declarations={
"connectors.example": {
"authority_modes": ["external_mirror"],
"maturity": "synchronize",
}
},
external_provider_states={
"connectors.example": {"health": "healthy"}
},
),
)
unhealthy = dry_run_configuration_package(
package,
(),
ConfigurationPreflightContext(
external_provider_declarations={
"connectors.example": {
"authority_modes": ["external_authoritative"],
"maturity": "discover",
}
},
external_provider_states={
"connectors.example": {"health": "error"}
},
),
)
self.assertFalse(
any(item.severity == "blocker" for item in healthy.diagnostics)
)
self.assertEqual(
{
"external_provider_authority_mode_unsupported",
"external_provider_maturity_insufficient",
"external_provider_unhealthy",
},
{item.code for item in unhealthy.diagnostics},
)
def test_provider_preflight_selects_the_requested_runtime_binding(self) -> None:
package = ConfigurationPackageManifest(
package_id="integration.calendar",
name="Calendar integration",
version="1.0.0",
package_class="integration",
evidence=_evidence(
"target_test", "recovery", "operations", "documentation"
),
provider_expectations=(
ConfigurationProviderExpectation(
provider_id="calendar.caldav_sync",
authority_mode="governed_sync",
minimum_maturity="synchronize",
binding_ref="calendar:sync-source:required",
freshness_expectation="current",
recovery_expectation="ready",
),
),
)
context = ConfigurationPreflightContext(
external_provider_declarations={
"calendar.caldav_sync": {
"authority_modes": ["governed_sync"],
"maturity": "synchronize",
"behavior": {},
}
},
external_provider_states={
"calendar.caldav_sync": {
"health": "warning",
"bindings": [
{
"binding_ref": "calendar:sync-source:other",
"authority_mode": "governed_sync",
"health": "error",
"freshness": "stale",
"recovery": "attention",
},
{
"binding_ref": "calendar:sync-source:required",
"authority_mode": "governed_sync",
"health": "healthy",
"freshness": "current",
"recovery": "ready",
},
],
}
},
)
result = dry_run_configuration_package(package, (), context)
self.assertFalse(
any(item.severity == "blocker" for item in result.diagnostics)
)
def test_derived_package_may_tighten_but_not_remove_parent_constraints(self) -> None:
parent = ConfigurationPackageManifest(
package_id="product.base",
name="Base",
version="1.0.0",
required_modules=(
ConfigurationModuleRequirement("connectors", "1.0.0"),
),
required_capabilities=("connectors.profiles",),
provider_expectations=(
ConfigurationProviderExpectation(
provider_id="connectors.example",
authority_mode="external_mirror",
minimum_maturity="read",
),
),
)
child = ConfigurationPackageManifest(
package_id="sector.example",
name="Sector example",
version="1.0.0",
package_class="sector",
parents=(
ConfigurationPackageParent("product.base", "1.0.0"),
),
evidence=_evidence("documentation"),
)
codes = {
item.code
for item in validate_configuration_package_derivation(child, parent)
}
self.assertEqual(
{
"package_parent_module_constraint_loosened",
"package_parent_capability_constraint_loosened",
"package_parent_provider_constraint_removed",
},
codes,
)
if __name__ == "__main__":
unittest.main()
+15
View File
@@ -15,12 +15,14 @@ class ExternalReferenceContractTests(unittest.TestCase):
object_type="work_package",
object_id="42",
maturity="synchronize",
authority_mode="governed_sync",
canonical_url="https://projects.example.test/work_packages/42",
)
self.assertEqual("openproject-main:work_package:42", reference.identity_key)
self.assertTrue(reference.supports("read"))
self.assertFalse(reference.supports("replace"))
self.assertEqual("governed_sync", reference.to_dict()["authority_mode"])
def test_reference_rejects_credentials_in_urls(self) -> None:
with self.assertRaises(ExternalReferenceValidationError):
@@ -31,6 +33,19 @@ class ExternalReferenceContractTests(unittest.TestCase):
canonical_url="https://user:secret@example.test/wiki/Main_Page",
)
def test_authority_mode_must_match_maturity(self) -> None:
with self.assertRaisesRegex(
ExternalReferenceValidationError,
"Governed-sync references require synchronize maturity",
):
ExternalObjectReference(
system="openproject",
object_type="work_package",
object_id="7",
maturity="read",
authority_mode="governed_sync",
)
if __name__ == "__main__":
unittest.main()
+58
View File
@@ -0,0 +1,58 @@
from __future__ import annotations
from datetime import datetime, timezone
import subprocess
from govoplan_core.commands import fenced_run
from govoplan_core.core.runtime_coordination import LeaseClaim
class _IgnoringProcess:
def __init__(self) -> None:
self.terminated = False
self.killed = False
self.wait_count = 0
def poll(self):
return None if not self.killed else -9
def terminate(self) -> None:
self.terminated = True
def kill(self) -> None:
self.killed = True
def wait(self, *, timeout: float):
self.wait_count += 1
if not self.killed:
raise subprocess.TimeoutExpired("command", timeout)
return -9
def test_fenced_process_is_killed_when_it_ignores_termination() -> None:
process = _IgnoringProcess()
fenced_run._terminate_process(process, timeout_seconds=0.01)
assert process.terminated is True
assert process.killed is True
assert process.wait_count == 2
def test_lease_release_is_best_effort_after_database_loss(monkeypatch) -> None:
claim = LeaseClaim(
installation_id="installation-1",
resource_key="scheduler",
holder_node_id="scheduler-1",
holder_incarnation="incarnation-1",
fencing_token=1,
expires_at=datetime(2026, 8, 1, 12, 0, tzinfo=timezone.utc),
)
monkeypatch.setattr(
fenced_run,
"get_database",
lambda: (_ for _ in ()).throw(RuntimeError("database unavailable")),
)
fenced_run._release(claim)
+43
View File
@@ -186,6 +186,44 @@ class InstallConfigTests(unittest.TestCase):
).file_storage_s3_deployment_managed
)
def test_external_s3_requires_explicit_https_deployment_trust(self) -> None:
base = {
"APP_ENV": "development",
"FILE_STORAGE_BACKEND": "s3",
"FILE_STORAGE_S3_ENDPOINT_URL": "https://s3.example.test",
"FILE_STORAGE_S3_REGION": "eu-test-1",
"FILE_STORAGE_S3_ACCESS_KEY_ID": "access",
"FILE_STORAGE_S3_SECRET_ACCESS_KEY": "secret",
"FILE_STORAGE_S3_BUCKET": "files",
"FILE_STORAGE_S3_DEPLOYMENT_MANAGED": "false",
}
rejected = validate_runtime_configuration(base, profile="development")
accepted = validate_runtime_configuration(
{**base, "FILE_STORAGE_S3_ENDPOINT_TRUSTED": "true"},
profile="development",
)
insecure = validate_runtime_configuration(
{
**base,
"FILE_STORAGE_S3_ENDPOINT_URL": "http://s3.example.test",
"FILE_STORAGE_S3_ENDPOINT_TRUSTED": "true",
},
profile="development",
)
self.assertIn(
"FILE_STORAGE_S3_ENDPOINT_TRUSTED",
{issue.key for issue in rejected.errors},
)
self.assertNotIn(
"FILE_STORAGE_S3_ENDPOINT_TRUSTED",
{issue.key for issue in accepted.errors},
)
self.assertIn(
"FILE_STORAGE_S3_ENDPOINT_URL",
{issue.key for issue in insecure.errors},
)
def test_connector_deployment_allowlists_require_exact_names_and_absolute_paths(self) -> None:
result = validate_runtime_configuration(
{
@@ -215,6 +253,7 @@ class InstallConfigTests(unittest.TestCase):
self.assertIn("GOVOPLAN_HTTP_HSTS_SECONDS=31536000", self_hosted)
self.assertIn("AUTH_LOGIN_THROTTLE_IDENTITY_LIMIT=10", self_hosted)
self.assertIn("FILE_STORAGE_S3_DEPLOYMENT_MANAGED=false", self_hosted)
self.assertIn("FILE_STORAGE_S3_ENDPOINT_TRUSTED=false", self_hosted)
self.assertIn("GOVOPLAN_INSTALL_PROFILE=production-like", production_like)
self.assertNotIn("MASTER_KEY_B64=<generate", production_like)
self.assertIn("CALENDAR_OUTBOX_TERMINAL_RETENTION_DAYS=90", production_like)
@@ -229,6 +268,10 @@ class InstallConfigTests(unittest.TestCase):
"FILE_STORAGE_S3_DEPLOYMENT_MANAGED=false",
production_like,
)
self.assertIn(
"FILE_STORAGE_S3_ENDPOINT_TRUSTED=false",
production_like,
)
if __name__ == "__main__":
+816
View File
@@ -0,0 +1,816 @@
from __future__ import annotations
from dataclasses import replace
from datetime import UTC, datetime, timedelta
import unittest
from govoplan_core.core.automation import ActionExecutionRequest, AutomationInvocation
from govoplan_core.core.events import PlatformEvent
from govoplan_core.core.external_references import ExternalObjectReference
from govoplan_core.core.institutional import (
ActorRepresentationReference,
DecisionEffectReference,
EvidenceReference,
ExternalSourceReference,
FormDefinition,
FormFieldDefinition,
FormalDecision,
GovernedContextEnvelope,
InformationGovernanceReference,
InstitutionalContextError,
InstitutionalReference,
LegalBasisReference,
MandateDefinition,
MandateResolution,
MandateResolutionRequest,
PartyRepresentation,
PartySubjectReference,
ProcedureParty,
ServiceBinding,
ServiceAvailabilityRequirement,
ServiceDefinition,
ServiceLaunchRequest,
ServiceLaunchResult,
TemporalRevision,
derive_service_restriction,
resolve_mandate_candidates,
revise_procedure_party,
revise_formal_decision,
revise_mandate_definition,
revoke_party_representation,
)
NOW = datetime(2026, 8, 1, 10, 0, tzinfo=UTC)
def _reference(
kind: str,
object_id: str,
*,
tenant_id: str = "tenant-1",
) -> InstitutionalReference:
owners = {
"institution": "tenancy",
"organization_unit": "organizations",
"function": "organizations",
"function_assignment": "idm",
"mandate": "cases",
"jurisdiction": "cases",
"service": "portal",
"form": "forms",
"form_submission": "forms_runtime",
"case": "cases",
"party": "cases",
"decision": "committee",
"work_item": "workflow_engine",
"workflow": "workflow_engine",
"approval": "workflow_engine",
"record": "audit",
}
return InstitutionalReference(
kind=kind,
owner_module=owners[kind],
object_id=object_id,
tenant_id=tenant_id,
version="3",
valid_at=NOW,
label=f"Protected label for {object_id}",
)
def _context() -> GovernedContextEnvelope:
external = ExternalObjectReference(
system="gis.example",
object_type="administrative_area",
object_id="area-7",
maturity="read",
authority_mode="external_mirror",
canonical_url="https://gis.example/areas/7",
observed_at=NOW,
)
return GovernedContextEnvelope(
tenant_id="tenant-1",
temporal=TemporalRevision(
revision="7",
valid_from=NOW - timedelta(days=1),
valid_to=NOW + timedelta(days=1),
recorded_at=NOW,
change_reason="Case state changed.",
),
actor=ActorRepresentationReference(
tenant_id="tenant-1",
account_id="account-1",
identity_id="identity-1",
represented_function_ref=_reference("function", "function-1"),
function_assignment_ref=_reference(
"function_assignment", "assignment-1"
),
mandate_ref=_reference("mandate", "mandate-1"),
),
institution_ref=_reference("institution", "institution-1"),
organization_unit_ref=_reference("organization_unit", "unit-1"),
function_ref=_reference("function", "function-1"),
mandate_ref=_reference("mandate", "mandate-1"),
jurisdiction_refs=(_reference("jurisdiction", "jurisdiction-1"),),
service_ref=_reference("service", "service-1"),
case_ref=_reference("case", "case-1"),
party_refs=(_reference("party", "party-1"),),
work_item_ref=_reference("work_item", "work-item-1"),
workflow_ref=_reference("workflow", "workflow-1"),
approval_refs=(_reference("approval", "approval-1"),),
decision_ref=_reference("decision", "decision-1"),
record_refs=(_reference("record", "record-1"),),
legal_bases=(
LegalBasisReference(
kind="law",
authority="Example legislature",
reference="law-1/section-3",
version="2026-01",
effective_from=NOW - timedelta(days=30),
title="Protected legal title",
inspection_url="https://law.example/section-3",
),
),
evidence=(
EvidenceReference(
kind="document",
owner_module="files",
evidence_id="file-1",
tenant_id="tenant-1",
version="4",
checksum="sha256:abcd",
captured_at=NOW,
inspection_url="https://govoplan.example/files/file-1",
),
),
information_governance=InformationGovernanceReference(
classification="confidential",
purposes=("case decision",),
legal_basis_refs=("law-1/section-3",),
retention_policy_ref="records:case-default",
disclosure_state="restricted",
),
external_sources=(
ExternalSourceReference(
reference=external,
authority_mode="external_mirror",
maturity="read",
freshness_state="current",
health_state="ok",
),
),
)
class InstitutionalContractTests(unittest.TestCase):
def test_context_round_trip_preserves_versioned_references(self) -> None:
context = _context()
restored = GovernedContextEnvelope.from_mapping(
context.to_dict(include_inspection=True)
)
self.assertEqual(context.tenant_id, restored.tenant_id)
self.assertEqual("mandate-1", restored.mandate_ref.object_id)
self.assertEqual(
"external_mirror", restored.external_sources[0].authority_mode
)
self.assertEqual("workflow-1", restored.workflow_ref.object_id)
self.assertEqual("approval-1", restored.approval_refs[0].object_id)
self.assertEqual("record-1", restored.record_refs[0].object_id)
self.assertTrue(restored.temporal.effective_at(NOW))
def test_cross_tenant_reference_is_rejected(self) -> None:
with self.assertRaisesRegex(InstitutionalContextError, "different tenant"):
GovernedContextEnvelope(
tenant_id="tenant-1",
temporal=TemporalRevision(revision="1", recorded_at=NOW),
case_ref=_reference("case", "case-2", tenant_id="tenant-2"),
)
def test_default_serialization_redacts_labels_and_inspection_links(self) -> None:
payload = _context().to_dict()
self.assertIsNone(payload["case_ref"]["label"])
self.assertIsNone(payload["legal_bases"][0]["title"])
self.assertIsNone(payload["legal_bases"][0]["inspection_url"])
self.assertIsNone(payload["evidence"][0]["inspection_url"])
def test_event_and_action_envelopes_carry_reference_context(self) -> None:
context = _context()
event = PlatformEvent(
type="committee.decision.recorded",
module_id="committee",
institutional_context=context,
)
action = ActionExecutionRequest(
tenant_id="tenant-1",
action_key="committee.decision.publish",
input={"decision_ref": "decision-1"},
idempotency_key="decision-1:publish:1",
invocation=AutomationInvocation(kind="workflow"),
institutional_context=context,
)
self.assertEqual(
"decision-1",
event.to_dict()["institutional_context"]["decision_ref"]["object_id"],
)
self.assertIs(context, action.institutional_context)
def test_formal_decision_redacts_reasoning_but_keeps_evidence_refs(self) -> None:
context = _context()
decision = FormalDecision(
reference=_reference("decision", "decision-1"),
temporal=context.temporal,
decision_type="permit",
subject_refs=(_reference("case", "case-1"),),
state="decided",
authority_context=context,
fact_evidence=context.evidence,
legal_bases=context.legal_bases,
operative_result="Permit granted.",
reasoning="Protected reasoning.",
)
redacted = decision.to_dict()
protected = decision.to_dict(include_protected=True)
self.assertIsNone(redacted["reasoning"])
self.assertEqual("Protected reasoning.", protected["reasoning"])
self.assertEqual(
"file-1", redacted["fact_evidence"][0]["evidence_id"]
)
restored = FormalDecision.from_mapping(protected)
self.assertEqual("Protected reasoning.", restored.reasoning)
self.assertEqual("decision-1", restored.reference.object_id)
def test_semantic_provider_dtos_round_trip_without_domain_tables(self) -> None:
context = _context()
mandate = MandateDefinition(
reference=_reference("mandate", "mandate-1"),
temporal=context.temporal,
task_types=("committee.formal_decision",),
authority_types=("permit",),
organization_unit_refs=(
_reference("organization_unit", "unit-1"),
),
function_refs=(_reference("function", "function-1"),),
jurisdiction_refs=context.jurisdiction_refs,
legal_bases=context.legal_bases,
evidence=context.evidence,
)
resolution = MandateResolution(
competent=True,
mandates=(mandate,),
explanation="Authority matched.",
evidence=context.evidence,
)
service = ServiceDefinition(
reference=_reference("service", "service-1"),
key="permit.service",
temporal=context.temporal,
title="Permit service",
audience=("residents",),
legal_bases=context.legal_bases,
channels=("portal", "mail"),
responsible_organization_ref=_reference(
"organization_unit", "unit-1"
),
responsible_function_ref=_reference("function", "function-1"),
mandate_ref=_reference("mandate", "mandate-1"),
jurisdiction_refs=context.jurisdiction_refs,
bindings=(ServiceBinding("workflow", "workflow:permit"),),
publication_state="published",
)
represented = _reference("party", "party-applicant")
representative = _reference("party", "party-agent")
party = ProcedureParty(
reference=represented,
procedure_ref=_reference("case", "case-1"),
role="applicant",
subject=PartySubjectReference(
kind="identity",
provider="identity",
subject_id="identity-1",
tenant_id="tenant-1",
version="2",
),
temporal=context.temporal,
preferred_channels=("portal",),
permitted_channels=("portal", "mail"),
delivery_recipient=True,
representations=(
PartyRepresentation(
representative_party_ref=representative,
represented_party_ref=represented,
power_ref="power-1",
permitted_actions=("submit", "receive"),
temporal=context.temporal,
evidence=context.evidence,
),
),
contact_snapshot_refs=("addresses:snapshot-1",),
evidence=context.evidence,
)
restored_resolution = MandateResolution.from_mapping(
resolution.to_dict(include_inspection=True)
)
restored_service = ServiceDefinition.from_mapping(service.to_dict())
restored_party = ProcedureParty.from_mapping(
party.to_dict(include_inspection=True)
)
self.assertTrue(restored_resolution.competent)
self.assertEqual(
"committee.formal_decision",
restored_resolution.mandates[0].task_types[0],
)
self.assertEqual("workflow:permit", restored_service.bindings[0].reference)
self.assertEqual("identity-1", restored_party.subject.subject_id)
self.assertEqual(
"party-agent",
restored_party.representations[0].representative_party_ref.object_id,
)
def test_invalid_semantic_states_are_rejected(self) -> None:
with self.assertRaisesRegex(InstitutionalContextError, "disclosure state"):
InformationGovernanceReference(
classification="internal",
purposes=("test",),
disclosure_state="invalid", # type: ignore[arg-type]
)
def test_service_launch_contract_requires_exact_same_tenant_targets(self) -> None:
service_ref = _reference("service", "service-1")
binding = ServiceBinding("case", "permit")
request = ServiceLaunchRequest(
service_ref=service_ref,
binding=binding,
idempotency_key="launch-1",
requested_at=NOW,
parameters={"title": "Permit application"},
)
result = ServiceLaunchResult(
service_ref=service_ref,
binding=binding,
state="started",
target_ref=_reference("case", "case-1"),
href="/cases/case-1",
metadata={"case_number": "PERMIT-1"},
)
self.assertEqual("launch-1", request.idempotency_key)
self.assertEqual("case-1", result.to_dict()["target_ref"]["object_id"])
with self.assertRaisesRegex(InstitutionalContextError, "another tenant"):
ServiceLaunchResult(
service_ref=service_ref,
binding=binding,
state="started",
target_ref=_reference("case", "case-2", tenant_id="tenant-2"),
metadata={},
)
def test_form_definition_contract_round_trips_exact_schema(self) -> None:
definition = FormDefinition(
reference=_reference("form", "permit-application"),
key="permit-application",
temporal=TemporalRevision(
revision="3",
recorded_at=NOW,
change_reason="Add delivery preference.",
),
title="Permit application",
description="Apply for a permit.",
fields=(
FormFieldDefinition(
key="email",
label="Email address",
value_type="email",
required=True,
constraints={"max_length": 255},
),
FormFieldDefinition(
key="delivery",
label="Delivery",
value_type="choice",
options=("portal", "mail"),
),
),
max_attachments=3,
signature_requirement="optional",
policy_refs=("policy:permit-intake",),
handoff_kinds=("case",),
)
restored = FormDefinition.from_mapping(definition.to_dict())
self.assertEqual("3", restored.reference.version)
self.assertEqual("email", restored.fields[0].key)
self.assertEqual(("portal", "mail"), restored.fields[1].options)
with self.assertRaisesRegex(InstitutionalContextError, "unique"):
replace(restored, fields=(restored.fields[0], restored.fields[0]))
def test_service_launch_redirect_rejects_credentialed_urls(self) -> None:
with self.assertRaisesRegex(InstitutionalContextError, "credential-free"):
ServiceLaunchResult(
service_ref=_reference("service", "service-1"),
binding=ServiceBinding("external", "external-entry"),
state="redirect",
href="https://user:secret@example.test/start",
metadata={},
)
with self.assertRaisesRegex(InstitutionalContextError, "must be a boolean"):
ServiceBinding.from_mapping(
{
"kind": "workflow",
"reference": "workflow:permit",
"required": "false",
}
)
def test_service_derivation_can_only_tighten_parent_constraints(self) -> None:
parent = ServiceDefinition(
reference=_reference("service", "permit-template"),
key="permit.apply",
temporal=TemporalRevision(
revision="7",
valid_from=NOW - timedelta(days=1),
valid_to=NOW + timedelta(days=10),
recorded_at=NOW - timedelta(days=2),
),
title="Permit service",
audience=("resident", "business"),
prerequisites=("registered-address",),
required_evidence_types=("application",),
channels=("portal", "mail"),
bindings=(ServiceBinding("case", "permit-case"),),
availability_requirements=(
ServiceAvailabilityRequirement("policy", "permit-access"),
),
publication_state="published",
)
temporal = TemporalRevision(
revision="8",
valid_from=NOW,
valid_to=NOW + timedelta(days=5),
recorded_at=NOW,
change_reason="Tenant specialization.",
)
reference = replace(
_reference("service", "permit-tenant"),
version="8",
)
derived = derive_service_restriction(
parent,
reference=reference,
temporal=temporal,
audience=("resident",),
channels=("portal",),
add_prerequisites=("tenant-residency",),
add_required_evidence_types=("identity",),
add_bindings=(ServiceBinding("workflow", "permit-review"),),
)
self.assertEqual(parent.reference, derived.derived_from_ref)
self.assertEqual(("resident",), derived.audience)
self.assertEqual(("portal",), derived.channels)
self.assertEqual(
("registered-address", "tenant-residency"),
derived.prerequisites,
)
self.assertIn(ServiceBinding("case", "permit-case"), derived.bindings)
self.assertEqual(
parent.availability_requirements,
derived.availability_requirements,
)
self.assertEqual(
parent.availability_requirements,
ServiceDefinition.from_mapping(
derived.to_dict()
).availability_requirements,
)
with self.assertRaisesRegex(InstitutionalContextError, "widen.*audience"):
derive_service_restriction(
parent,
reference=reference,
temporal=temporal,
audience=("resident", "visitor"),
)
with self.assertRaisesRegex(InstitutionalContextError, "beyond"):
derive_service_restriction(
parent,
reference=reference,
temporal=replace(temporal, valid_to=None),
)
with self.assertRaisesRegex(
InstitutionalContextError,
"publication state",
):
derive_service_restriction(
replace(parent, publication_state="suspended"),
reference=reference,
temporal=temporal,
publication_state="published",
)
def test_mandate_resolution_is_effective_scoped_and_deterministic(self) -> None:
context = _context()
mandate = MandateDefinition(
reference=_reference("mandate", "mandate-1"),
temporal=TemporalRevision(
revision="1",
valid_from=NOW - timedelta(days=1),
valid_to=NOW + timedelta(days=1),
recorded_at=NOW - timedelta(days=2),
),
task_types=("committee.formal_decision",),
authority_types=("permit",),
organization_unit_refs=(
_reference("organization_unit", "unit-1"),
),
function_refs=(_reference("function", "function-1"),),
jurisdiction_refs=context.jurisdiction_refs,
subject_types=("permit",),
evidence=context.evidence,
)
request = MandateResolutionRequest(
tenant_id="tenant-1",
effective_at=NOW,
task_type="committee.formal_decision",
authority_type="permit",
organization_unit_ref=replace(
_reference("organization_unit", "unit-1"), version="9"
),
function_ref=replace(
_reference("function", "function-1"), version="9"
),
jurisdiction_refs=tuple(
replace(item, version="9") for item in context.jurisdiction_refs
),
subject_type="permit",
)
resolution = resolve_mandate_candidates(
request,
(
replace(
mandate,
reference=_reference("mandate", "retired"),
status="retired",
),
mandate,
),
)
self.assertTrue(resolution.competent)
self.assertEqual("mandate-1", resolution.mandates[0].reference.object_id)
self.assertEqual("file-1", resolution.evidence[0].evidence_id)
ambiguous = resolve_mandate_candidates(
request,
(
mandate,
replace(
mandate,
reference=_reference("mandate", "mandate-2"),
),
),
)
self.assertFalse(ambiguous.competent)
self.assertEqual(2, len(ambiguous.conflict_refs))
missing_jurisdiction = resolve_mandate_candidates(
replace(request, jurisdiction_refs=()),
(mandate,),
)
self.assertFalse(missing_jurisdiction.competent)
def test_mandate_resolution_rejects_cross_tenant_candidates(self) -> None:
request = MandateResolutionRequest(
tenant_id="tenant-1",
effective_at=NOW,
task_type="committee.formal_decision",
authority_type="permit",
)
candidate = MandateDefinition(
reference=_reference("mandate", "mandate-2", tenant_id="tenant-2"),
temporal=TemporalRevision(revision="1", recorded_at=NOW),
task_types=("committee.formal_decision",),
authority_types=("permit",),
)
with self.assertRaisesRegex(InstitutionalContextError, "cross tenants"):
resolve_mandate_candidates(request, (candidate,))
def test_mandate_lifecycle_revisions_are_linked_by_stable_identity(self) -> None:
current = MandateDefinition(
reference=_reference("mandate", "mandate-1"),
temporal=TemporalRevision(revision="1", recorded_at=NOW),
task_types=("committee.formal_decision",),
authority_types=("permit",),
status="active",
)
temporal = TemporalRevision(
revision="2",
recorded_at=NOW + timedelta(minutes=1),
change_reason="Authority temporarily suspended.",
)
suspended = revise_mandate_definition(
current,
expected_revision="1",
temporal=temporal,
status="suspended",
suspension_reason="Delegation is under review.",
)
self.assertEqual("active", current.status)
self.assertEqual("suspended", suspended.status)
self.assertEqual("2", suspended.reference.version)
self.assertEqual(current.reference.object_id, suspended.reference.object_id)
with self.assertRaisesRegex(InstitutionalContextError, "stale"):
revise_mandate_definition(
current,
expected_revision="0",
temporal=temporal,
status="suspended",
suspension_reason="Review.",
)
with self.assertRaisesRegex(InstitutionalContextError, "not allowed"):
revise_mandate_definition(
suspended,
expected_revision="2",
temporal=replace(
temporal,
revision="3",
change_reason="Invalid reset.",
),
status="draft",
)
def test_formal_decision_revisions_are_linked_and_occ_guarded(self) -> None:
context = _context()
reference = replace(
_reference("decision", "decision-1"),
version=context.temporal.revision,
)
context = replace(context, decision_ref=reference)
current = FormalDecision(
reference=reference,
temporal=context.temporal,
decision_type="permit",
subject_refs=(_reference("case", "case-1"),),
state="decided",
authority_context=context,
fact_evidence=context.evidence,
legal_bases=context.legal_bases,
operative_result="Permit granted.",
reasoning="Protected reasoning.",
requested_effects=(
DecisionEffectReference("notify", "requested"),
),
)
with self.assertRaisesRegex(
InstitutionalContextError,
"service account",
):
replace(current, assurance_level="automated_under_mandate")
revision = TemporalRevision(
revision="8",
valid_from=NOW + timedelta(minutes=1),
recorded_at=NOW + timedelta(minutes=1),
change_reason="Decision entered into force.",
)
effective = revise_formal_decision(
current,
expected_revision="7",
temporal=revision,
state="effective",
)
self.assertEqual("decided", current.state)
self.assertEqual("8", effective.reference.version)
self.assertEqual("7", effective.supersedes_ref.version)
self.assertEqual(
effective.reference,
effective.authority_context.decision_ref,
)
self.assertEqual(current.requested_effects, effective.requested_effects)
with self.assertRaisesRegex(InstitutionalContextError, "stale"):
revise_formal_decision(
current,
expected_revision="6",
temporal=revision,
state="effective",
)
with self.assertRaisesRegex(InstitutionalContextError, "not allowed"):
revise_formal_decision(
effective,
expected_revision="8",
temporal=replace(
revision,
revision="9",
change_reason="Invalid regression.",
),
state="proposed",
)
corrected = revise_formal_decision(
effective,
expected_revision="8",
temporal=replace(
revision,
revision="9",
recorded_at=NOW + timedelta(minutes=2),
change_reason="Corrected operative wording.",
),
state="corrected",
operative_result="Permit granted subject to condition A.",
)
self.assertEqual(effective.reference, corrected.correction_of_ref)
self.assertEqual(effective.reference, corrected.supersedes_ref)
def test_party_corrections_and_representation_revocation_are_versioned(self) -> None:
context = _context()
represented = _reference("party", "party-applicant")
representative = _reference("party", "party-agent")
representation = PartyRepresentation(
representative_party_ref=representative,
represented_party_ref=represented,
power_ref="power-1",
permitted_actions=("submit", "receive"),
temporal=TemporalRevision(
revision="1",
valid_from=NOW - timedelta(days=1),
recorded_at=NOW - timedelta(days=1),
),
evidence=context.evidence,
)
current = ProcedureParty(
reference=represented,
procedure_ref=_reference("case", "case-1"),
role="applicant",
subject=PartySubjectReference(
kind="identity",
provider="identity",
subject_id="identity-1",
tenant_id="tenant-1",
version="2",
),
temporal=TemporalRevision(revision="1", recorded_at=NOW),
permitted_channels=("mail", "postbox"),
preferred_channels=("mail",),
delivery_recipient=True,
representations=(representation,),
contact_snapshot_refs=("addresses:snapshot-1",),
evidence=context.evidence,
)
revision = TemporalRevision(
revision="2",
valid_from=NOW,
recorded_at=NOW + timedelta(minutes=1),
change_reason="Correct preferred delivery channel.",
)
corrected = revise_procedure_party(
current,
expected_revision="1",
temporal=revision,
preferred_channels=("postbox",),
)
revoked_power = revoke_party_representation(
representation,
expected_revision="1",
temporal=TemporalRevision(
revision="2",
valid_from=NOW - timedelta(days=1),
valid_to=NOW + timedelta(minutes=2),
recorded_at=NOW + timedelta(minutes=2),
change_reason="Power was revoked.",
),
revoked_at=NOW + timedelta(minutes=2),
)
self.assertEqual(("mail",), current.preferred_channels)
self.assertEqual(("postbox",), corrected.preferred_channels)
self.assertEqual("2", corrected.reference.version)
self.assertIsNone(representation.revoked_at)
self.assertEqual(NOW + timedelta(minutes=2), revoked_power.revoked_at)
with self.assertRaisesRegex(InstitutionalContextError, "stale"):
revise_procedure_party(
current,
expected_revision="0",
temporal=revision,
)
if __name__ == "__main__":
unittest.main()
+55
View File
@@ -0,0 +1,55 @@
from __future__ import annotations
import unittest
from unittest.mock import MagicMock, patch
from govoplan_core.db.migration_lock import (
deployment_migration_lock,
migration_lock_key,
)
class MigrationLockTests(unittest.TestCase):
def test_lock_key_is_stable_and_namespaced(self) -> None:
first = migration_lock_key("installation-a", "release")
self.assertEqual(first, migration_lock_key("installation-a", "release"))
self.assertNotEqual(first, migration_lock_key("installation-b", "release"))
self.assertNotEqual(first, migration_lock_key("installation-a", "dev"))
self.assertGreaterEqual(first, -(2**63))
self.assertLess(first, 2**63)
def test_non_postgres_migration_lock_is_a_noop(self) -> None:
entered = False
with deployment_migration_lock(
"sqlite+pysqlite:///:memory:",
installation_id="installation-a",
migration_track="release",
):
entered = True
self.assertTrue(entered)
def test_postgres_lock_is_held_for_the_guarded_operation(self) -> None:
result = MagicMock()
result.scalar_one.return_value = True
connection = MagicMock()
connection.execute.return_value = result
engine = MagicMock()
engine.connect.return_value = connection
with patch(
"govoplan_core.db.migration_lock.create_engine",
return_value=engine,
):
with deployment_migration_lock(
"postgresql://user:secret@db.example.test/govoplan",
installation_id="installation-a",
migration_track="release",
):
self.assertEqual(1, connection.execute.call_count)
self.assertEqual(2, connection.execute.call_count)
connection.close.assert_called_once_with()
engine.dispose.assert_called_once_with()
if __name__ == "__main__":
unittest.main()
+35
View File
@@ -1261,6 +1261,41 @@ finally:
self.assertIn("module_active", {issue.code for issue in preflight.issues})
self.assertIn("module_desired", {issue.code for issue in preflight.issues})
def test_module_installer_blocks_node_local_package_mutation_in_shared_profile(
self,
) -> None:
plan = ModuleInstallPlan(
items=(
ModuleInstallPlanItem(
module_id="files",
action="update",
source="manual",
python_package="govoplan-files",
python_ref="govoplan-files==1.2.3",
),
)
)
with patch.dict(os.environ, {"GOVOPLAN_STATE_PROFILE": "shared"}):
preflight = module_install_preflight(
plan=plan,
available={
"files": ModuleManifest(
id="files",
name="Files",
version="1.2.2",
)
},
current_enabled=("files",),
desired_enabled=("files",),
maintenance_mode=True,
)
self.assertFalse(preflight.allowed)
self.assertIn(
"immutable_cluster_release_required",
{issue.code for issue in preflight.issues},
)
def test_module_installer_preflight_blocks_incompatible_manifest(self) -> None:
root = Path(tempfile.mkdtemp(prefix="govoplan-installer-compat-", dir=_TEST_ROOT))
settings = _settings(root)
+169
View File
@@ -0,0 +1,169 @@
from __future__ import annotations
from io import BytesIO
from pathlib import Path
import tempfile
import unittest
from govoplan_core.core.object_storage import (
LocalFilesystemStorageBackend,
S3StorageBackend,
StorageBackendError,
StorageObjectMissing,
normalize_storage_key,
)
class _S3Error(RuntimeError):
def __init__(self, code: str, status: int) -> None:
super().__init__(code)
self.response = {
"Error": {"Code": code},
"ResponseMetadata": {"HTTPStatusCode": status},
}
class _FakeS3Client:
def __init__(self) -> None:
self.objects: dict[str, bytes] = {}
self.head_error: Exception | None = None
def put_object(self, *, Key: str, Body: bytes, **_kwargs) -> None:
self.objects[Key] = Body
def get_object(self, *, Key: str, **_kwargs):
try:
payload = self.objects[Key]
except KeyError as exc:
raise _S3Error("NoSuchKey", 404) from exc
return {"Body": BytesIO(payload), "ContentLength": len(payload)}
def head_object(self, *, Key: str, **_kwargs):
if self.head_error is not None:
raise self.head_error
try:
payload = self.objects[Key]
except KeyError as exc:
raise _S3Error("NotFound", 404) from exc
return {"ContentLength": len(payload)}
def delete_object(self, *, Key: str, **_kwargs) -> None:
self.objects.pop(Key, None)
class ObjectStorageTests(unittest.TestCase):
def test_local_backend_round_trip_pagination_and_safe_keys(self) -> None:
with tempfile.TemporaryDirectory(prefix="govoplan-object-store-") as directory:
backend = LocalFilesystemStorageBackend(Path(directory))
backend.put_bytes("campaign/a.eml", b"a")
backend.put_bytes("campaign/b.eml", b"bb")
first = backend.list_objects(prefix="campaign/", limit=1)
second = backend.list_objects(
prefix="campaign/",
after=first.next_cursor,
limit=1,
)
self.assertEqual(b"a", backend.get_bytes("campaign/a.eml"))
self.assertEqual(1, backend.stat("campaign/a.eml").size_bytes)
self.assertEqual(
("campaign/a.eml",), tuple(item.key for item in first.objects)
)
self.assertEqual("campaign/a.eml", first.next_cursor)
self.assertEqual(
("campaign/b.eml",), tuple(item.key for item in second.objects)
)
self.assertIsNone(second.next_cursor)
with self.assertRaises(StorageBackendError):
normalize_storage_key("../outside")
with self.assertRaises(StorageObjectMissing):
backend.get_bytes("campaign/missing.eml")
def test_local_listing_is_globally_sorted_and_ignores_symlinks(self) -> None:
with (
tempfile.TemporaryDirectory(prefix="govoplan-object-store-") as directory,
tempfile.TemporaryDirectory(prefix="govoplan-object-outside-") as outside,
):
root = Path(directory)
backend = LocalFilesystemStorageBackend(root)
backend.put_bytes("a/x.txt", b"nested")
backend.put_bytes("a.txt", b"sibling")
backend.put_bytes("b.txt", b"last")
outside_file = Path(outside) / "secret.txt"
outside_file.write_bytes(b"outside")
(root / "linked-file.txt").symlink_to(outside_file)
(root / "linked-directory").symlink_to(
Path(outside), target_is_directory=True
)
first = backend.list_objects(prefix="", limit=2)
second = backend.list_objects(
prefix="",
after=first.next_cursor,
limit=2,
)
self.assertEqual(
("a.txt", "a/x.txt"),
tuple(item.key for item in first.objects),
)
self.assertEqual("a/x.txt", first.next_cursor)
self.assertEqual(
("b.txt",),
tuple(item.key for item in second.objects),
)
self.assertIsNone(second.next_cursor)
with self.assertRaises(StorageBackendError):
backend.exists("../outside")
def test_s3_backend_distinguishes_missing_objects_from_backend_failure(
self,
) -> None:
backend = S3StorageBackend(
bucket="files",
endpoint_url="http://garage:3900",
region_name="garage",
access_key_id="key",
secret_access_key="secret",
deployment_managed=True,
)
client = _FakeS3Client()
backend._client = client
backend.put_bytes("campaign/message.eml", b"message/rfc822")
self.assertTrue(backend.exists("campaign/message.eml"))
self.assertEqual(b"message/rfc822", backend.get_bytes("campaign/message.eml"))
self.assertFalse(backend.exists("campaign/missing.eml"))
client.head_error = _S3Error("AccessDenied", 403)
with self.assertRaises(StorageBackendError):
backend.exists("campaign/message.eml")
def test_external_s3_requires_explicit_https_deployment_trust(self) -> None:
trusted = S3StorageBackend(
bucket="files",
endpoint_url="https://s3.internal.example.test",
region_name="eu-test-1",
access_key_id="key",
secret_access_key="secret",
endpoint_trusted=True,
)
trusted._client = _FakeS3Client()
trusted.put_bytes("files/document.txt", b"document")
self.assertEqual(b"document", trusted.get_bytes("files/document.txt"))
rejected = S3StorageBackend(
bucket="files",
endpoint_url="http://s3.internal.example.test",
region_name="eu-test-1",
access_key_id="key",
secret_access_key="secret",
endpoint_trusted=True,
)
with self.assertRaisesRegex(StorageBackendError, "HTTPS origin"):
rejected.client
if __name__ == "__main__":
unittest.main()
+536
View File
@@ -0,0 +1,536 @@
from __future__ import annotations
from datetime import UTC, datetime
import unittest
from govoplan_core.core.modules import ModuleManifest
from govoplan_core.core.provider_governance import (
ArchitectureDeclarationError,
ExternalProviderDeclaration,
ExternalProviderRuntimeState,
ExternalProviderStateContext,
ExternalProviderStateProviderRegistration,
ModuleArchitectureDeclaration,
ModuleArchitectureDocumentation,
ModuleMaturityEvidence,
ProviderBehaviorDeclaration,
ProviderObjectDeclaration,
collect_external_provider_states,
declared_module_architecture,
external_provider_from_mapping,
module_architecture_from_mapping,
module_architecture_issues,
)
from govoplan_core.core.registry import PlatformRegistry, RegistryError
def _architecture(
*,
providers: tuple[str, ...] = (),
modes: tuple[str, ...] = ("external_mirror",),
) -> ModuleArchitectureDeclaration:
return ModuleArchitectureDeclaration(
layer="data_reporting_integration",
kind="integration",
maturity="vertical_slice",
evidence=(
ModuleMaturityEvidence(
kind="test",
reference="tests/test_provider.py",
summary="Exercises the bounded provider contract.",
),
ModuleMaturityEvidence(
kind="documentation",
reference="docs/PROVIDER.md",
summary="Documents authority and outage behavior.",
),
),
known_limits=("Only bounded test objects are supported.",),
supported_authority_modes=modes,
owned_concepts=("external transport",),
non_owned_concepts=("domain records",),
target_tested_providers=providers,
)
def _mirror_provider() -> ExternalProviderDeclaration:
return ExternalProviderDeclaration(
id="connectors.read_mirror",
module_id="connectors",
label="Read-only mirror",
maturity="read",
operations=("discover", "search", "read", "preview"),
objects=(
ProviderObjectDeclaration(
object_type="register_entry",
field_groups=("identity", "content", "source_metadata"),
authority_modes=("external_authoritative", "external_mirror"),
default_authority_mode="external_mirror",
),
),
behavior=ProviderBehaviorDeclaration(
revision_tokens="ETag and source revision are retained.",
concurrency="Reads can require an expected source revision.",
freshness="The latest successful acquisition time is reported.",
health="Transport and parsing health are reported separately.",
max_read_items=500,
evidence="Immutable snapshots retain acquisition provenance.",
correction="Refresh creates a new snapshot; old evidence is retained.",
reconciliation="Compare the latest source revision and content digest.",
outage="The last governed snapshot remains readable as stale.",
classifications=("internal",),
purposes=("governed import",),
retention="Owning datasource retention policy applies.",
secret_handling="Credentials stay in credential envelopes.",
),
capability_names=("connectors.readMirror",),
)
def _writable_provider() -> ExternalProviderDeclaration:
return ExternalProviderDeclaration(
id="calendar.caldav_sync",
module_id="calendar",
label="CalDAV synchronization",
maturity="synchronize",
operations=("discover", "read", "write", "delete", "synchronize", "preview"),
objects=(
ProviderObjectDeclaration(
object_type="calendar_event",
field_groups=("identity", "schedule", "participants", "recurrence"),
authority_modes=("external_authoritative", "governed_sync"),
default_authority_mode="governed_sync",
),
),
behavior=ProviderBehaviorDeclaration(
revision_tokens="ETag and sync token.",
concurrency="Conditional requests reject stale ETags.",
freshness="Last successful sync and pending changes are reported.",
health="Discovery, authentication, and collection health are separate.",
max_read_items=1000,
idempotency="Stable UID and operation keys suppress duplicate effects.",
retry="Bounded retry is limited to classified transient failures.",
timeout_seconds=30,
conflicts="Conflicts remain explicit until policy or a user resolves them.",
outcome_unknown="Timed-out writes are outcome-unknown, never blindly retried.",
outcome_unknown_supported=True,
evidence="Request, response token, and reconciliation evidence are retained.",
audit_event_types=("calendar.sync.requested", "calendar.sync.reconciled"),
correction="A later governed write or tombstone corrects remote state.",
rollback="Remote writes are not assumed rollback-safe.",
compensation="A compensating update can be requested after reconciliation.",
reconciliation="Read by UID and compare ETag before retrying.",
outage="Local state remains marked stale and pending effects stay queued.",
classifications=("confidential",),
purposes=("calendar synchronization",),
retention="Calendar and audit retention policies apply independently.",
secret_handling="Credentials are referenced through credential envelopes.",
),
capability_names=("calendar.sync",),
)
def _state_registration(
*,
module_id: str,
provider_id: str,
) -> ExternalProviderStateProviderRegistration:
return ExternalProviderStateProviderRegistration(
module_id=module_id,
provider_id=provider_id,
provider=lambda _context: (),
)
class ProviderGovernanceContractTests(unittest.TestCase):
def test_concise_declaration_keeps_repository_evidence_explicit(self) -> None:
architecture = declared_module_architecture(
layer="human_work_procedure",
kind="domain",
maturity="vertical_slice",
documentation_ref="docs/DOMAIN.md",
test_ref="tests/test_service.py",
known_limits=("Only the bounded service slice is implemented.",),
owned_concepts=("domain record",),
non_owned_concepts=("identity",),
)
self.assertEqual("vertical_slice", architecture.maturity)
self.assertEqual(
{"documentation", "test"},
{item.kind for item in architecture.evidence},
)
self.assertEqual(("domain record",), architecture.owned_concepts)
def test_declarations_round_trip_through_catalog_mappings(self) -> None:
provider = _writable_provider()
architecture = _architecture(
providers=(provider.id,),
modes=("external_authoritative", "governed_sync"),
)
self.assertEqual(
architecture,
module_architecture_from_mapping(architecture.to_dict()),
)
self.assertEqual(
provider,
external_provider_from_mapping(provider.to_dict()),
)
def test_read_only_mirror_declaration_is_registry_validated(self) -> None:
provider = _mirror_provider()
registry = PlatformRegistry()
registry.register(
ModuleManifest(
id="connectors",
name="Connectors",
version="1.0.0",
architecture=_architecture(
providers=(provider.id,),
modes=("external_authoritative", "external_mirror"),
),
external_providers=(provider,),
external_provider_state_providers=(
_state_registration(
module_id="connectors",
provider_id=provider.id,
),
),
capability_factories={"connectors.readMirror": lambda _context: object()},
)
)
snapshot = registry.validate()
self.assertEqual("connectors", snapshot.manifests[0].id)
self.assertEqual((provider,), registry.external_provider_declarations())
def test_writable_outcome_unknown_provider_is_explicit(self) -> None:
provider = _writable_provider()
registry = PlatformRegistry()
registry.register(
ModuleManifest(
id="calendar",
name="Calendar",
version="1.0.0",
architecture=_architecture(
providers=(provider.id,),
modes=("external_authoritative", "governed_sync"),
),
external_providers=(provider,),
external_provider_state_providers=(
_state_registration(
module_id="calendar",
provider_id=provider.id,
),
),
capability_factories={"calendar.sync": lambda _context: object()},
)
)
registry.validate()
payload = provider.to_dict()
self.assertTrue(payload["behavior"]["outcome_unknown_supported"])
self.assertIn("reconciliation", payload["behavior"])
def test_provider_requires_a_sanitized_runtime_state_registration(self) -> None:
provider = _mirror_provider()
registry = PlatformRegistry()
registry.register(
ModuleManifest(
id="connectors",
name="Connectors",
version="1.0.0",
architecture=_architecture(
providers=(provider.id,),
modes=("external_authoritative", "external_mirror"),
),
external_providers=(provider,),
capability_factories={
"connectors.readMirror": lambda _context: object()
},
)
)
with self.assertRaisesRegex(
RegistryError,
"require sanitized runtime state providers",
):
registry.validate()
def test_runtime_binding_states_are_validated_and_aggregated(self) -> None:
observed_at = datetime(2026, 8, 1, 12, 0, tzinfo=UTC)
def states(_context: ExternalProviderStateContext):
return (
ExternalProviderRuntimeState(
provider_id="calendar.caldav_sync",
binding_ref="calendar:sync-source:one",
authority_mode="governed_sync",
observed_at=observed_at,
configured=True,
active=True,
health="healthy",
freshness="current",
conflict="clear",
recovery="ready",
),
ExternalProviderRuntimeState(
provider_id="calendar.caldav_sync",
binding_ref="calendar:sync-source:two",
authority_mode="external_mirror",
observed_at=observed_at,
configured=True,
active=True,
health="warning",
freshness="stale",
conflict="pending",
recovery="attention",
),
)
payload = collect_external_provider_states(
(
ExternalProviderStateProviderRegistration(
module_id="calendar",
provider_id="calendar.caldav_sync",
provider=states,
),
),
ExternalProviderStateContext(session=None, tenant_id="tenant-1"),
)["calendar.caldav_sync"]
self.assertEqual("warning", payload["health"])
self.assertEqual("stale", payload["freshness"])
self.assertEqual("pending", payload["conflict"])
self.assertEqual("attention", payload["recovery"])
self.assertEqual(2, len(payload["bindings"]))
def test_runtime_state_registration_requires_declared_provider(self) -> None:
registry = PlatformRegistry()
registry.register(
ModuleManifest(
id="calendar",
name="Calendar",
version="1.0.0",
architecture=_architecture(),
external_provider_state_providers=(
ExternalProviderStateProviderRegistration(
module_id="calendar",
provider_id="calendar.caldav_sync",
provider=lambda _context: (),
),
),
)
)
with self.assertRaisesRegex(RegistryError, "undeclared provider"):
registry.validate()
def test_runtime_state_failure_is_sanitized(self) -> None:
def fail(_context: ExternalProviderStateContext):
raise RuntimeError("secret transport detail")
payload = collect_external_provider_states(
(
ExternalProviderStateProviderRegistration(
module_id="calendar",
provider_id="calendar.caldav_sync",
provider=fail,
),
),
ExternalProviderStateContext(session=None),
)["calendar.caldav_sync"]
self.assertEqual("error", payload["health"])
self.assertNotIn("secret transport detail", str(payload))
def test_supported_claim_requires_evidence_not_only_a_string(self) -> None:
registry = PlatformRegistry()
registry.register(
ModuleManifest(
id="example",
name="Example",
version="1.0.0",
architecture=ModuleArchitectureDeclaration(
layer="domain_capability",
kind="domain",
maturity="supported",
evidence=(),
owned_concepts=("example",),
),
)
)
with self.assertRaisesRegex(RegistryError, "missing evidence"):
registry.validate()
def test_supported_and_lts_claims_require_reference_packages(self) -> None:
for maturity in ("supported", "lts"):
with self.subTest(maturity=maturity):
declaration = ModuleArchitectureDeclaration(
layer="domain_capability",
kind="domain",
maturity=maturity,
evidence=tuple(
ModuleMaturityEvidence(
kind=kind,
reference=f"evidence/{kind}.json",
summary=f"{kind} evidence.",
)
for kind in (
"test",
"documentation",
"reference_process",
"target_test",
"recovery",
"security",
"operations",
"accessibility",
"privacy",
"upgrade",
*(("compatibility",) if maturity == "lts" else ()),
)
),
owned_concepts=("example",),
documentation=ModuleArchitectureDocumentation(
upgrade=("docs/UPGRADE.md",),
recovery=("docs/RECOVERY.md",),
security=("docs/SECURITY.md",),
operations=("docs/OPERATIONS.md",),
),
)
self.assertEqual(
(f"{maturity} maturity requires a reference package",),
module_architecture_issues(declaration, has_migrations=False),
)
def test_reference_ready_claim_requires_target_human_and_operator_evidence(self) -> None:
declaration = ModuleArchitectureDeclaration(
layer="domain_capability",
kind="domain",
maturity="reference_ready",
evidence=(
ModuleMaturityEvidence(
kind="test",
reference="tests/test_reference.py",
summary="Automated journey proof.",
),
ModuleMaturityEvidence(
kind="documentation",
reference="docs/REFERENCE.md",
summary="Reference composition documentation.",
),
ModuleMaturityEvidence(
kind="reference_process",
reference="docs/JOURNEY.md",
summary="Reference process definition.",
),
ModuleMaturityEvidence(
kind="recovery",
reference="evidence/recovery.json",
summary="Recovery drill evidence.",
),
),
known_limits=("One provider profile remains bounded.",),
owned_concepts=("example",),
reference_packages=("reference.example",),
)
issues = module_architecture_issues(declaration, has_migrations=False)
self.assertIn("accessibility", issues[0])
self.assertIn("operations", issues[0])
self.assertIn("privacy", issues[0])
self.assertIn("security", issues[0])
self.assertIn("target_test", issues[0])
self.assertIn("missing documentation", issues[1])
def test_target_tested_provider_requires_provider_evidence(self) -> None:
evidence = tuple(
ModuleMaturityEvidence(
kind=kind,
reference=f"evidence/{kind}.json",
summary=f"{kind} evidence.",
)
for kind in (
"test",
"documentation",
"reference_process",
"target_test",
"recovery",
"security",
"operations",
"accessibility",
"privacy",
)
)
declaration = ModuleArchitectureDeclaration(
layer="data_reporting_integration",
kind="integration",
maturity="reference_ready",
evidence=evidence,
known_limits=("One bounded target profile is assessed.",),
reference_packages=("reference.integration",),
target_tested_providers=("connectors.example",),
documentation=ModuleArchitectureDocumentation(
recovery=("docs/RECOVERY.md",),
security=("docs/SECURITY.md",),
operations=("docs/OPERATIONS.md",),
),
)
self.assertEqual(
(
"maturity 'reference_ready' is missing evidence: provider",
),
module_architecture_issues(declaration, has_migrations=False),
)
def test_provider_references_must_exist_in_manifest(self) -> None:
provider = _mirror_provider()
registry = PlatformRegistry()
registry.register(
ModuleManifest(
id="connectors",
name="Connectors",
version="1.0.0",
architecture=_architecture(
providers=(provider.id,),
modes=("external_authoritative", "external_mirror"),
),
external_providers=(provider,),
)
)
with self.assertRaisesRegex(RegistryError, "undeclared capabilities"):
registry.validate()
def test_governed_sync_requires_read_and_write_semantics(self) -> None:
with self.assertRaisesRegex(
ArchitectureDeclarationError,
"read and write/synchronize",
):
ExternalProviderDeclaration(
id="broken.sync",
module_id="broken",
label="Broken sync",
maturity="synchronize",
operations=("synchronize",),
objects=(
ProviderObjectDeclaration(
object_type="object",
field_groups=("content",),
authority_modes=("governed_sync",),
default_authority_mode="governed_sync",
),
),
behavior=_writable_provider().behavior,
)
if __name__ == "__main__":
unittest.main()
+288
View File
@@ -0,0 +1,288 @@
from __future__ import annotations
from datetime import datetime, timezone
from sqlalchemy import create_engine
from sqlalchemy.orm import Session
import pytest
from govoplan_core.core.recovery import (
RecoveryCheckpoint,
RecoveryGuaranteeError,
RecoveryMode,
RecoveryOperation,
RecoveryPlan,
RecoveryStatus,
plan_recovery_operation,
prepare_recovery_operation,
record_recovery_checkpoint,
start_recovery_operation,
transition_recovery_operation,
verify_recovery_evidence_chain,
)
from govoplan_core.core.runtime_coordination import DistributedLease, acquire_lease
from govoplan_core.db.base import Base
def _session() -> tuple[Session, object]:
engine = create_engine("sqlite+pysqlite:///:memory:")
Base.metadata.create_all(
engine,
tables=[
DistributedLease.__table__,
RecoveryOperation.__table__,
RecoveryCheckpoint.__table__,
],
)
return Session(engine, expire_on_commit=False), engine
def test_recovery_plan_requires_mode_specific_evidence() -> None:
with pytest.raises(RecoveryGuaranteeError, match="compensation steps"):
RecoveryPlan(
mode=RecoveryMode.COMPENSATION,
verification_steps=("verify",),
).validate()
with pytest.raises(RecoveryGuaranteeError, match="backup reference"):
RecoveryPlan(
mode=RecoveryMode.SNAPSHOT_RESTORE,
verification_steps=("verify",),
).validate()
with pytest.raises(RecoveryGuaranteeError, match="approval reference"):
RecoveryPlan(
mode=RecoveryMode.IRREVERSIBLE,
verification_steps=("verify",),
).validate()
def test_recovery_plan_hashes_backup_and_approval_references() -> None:
plan = RecoveryPlan(
mode=RecoveryMode.SNAPSHOT_RESTORE,
verification_steps=("restore drill passed",),
backup_reference="backup://postgres/2026-08-01",
approval_reference="approval://change/42",
)
assert plan.as_dict()["backup_reference"] == "backup://postgres/2026-08-01"
assert plan.as_dict()["approval_reference"] == "approval://change/42"
def test_recovery_operation_records_verifiable_checkpoint_chain() -> None:
session, engine = _session()
try:
operation = plan_recovery_operation(
session,
installation_id="installation-1",
module_id="files",
operation_type="move-tree",
idempotency_key="move-42",
request={"source": "a", "target": "b"},
recovery_plan=RecoveryPlan(
mode=RecoveryMode.COMPENSATION,
compensation_steps=("restore source links",),
verification_steps=("compare object inventory",),
),
)
prepare_recovery_operation(
session,
operation,
evidence={"inventory_sha256": "a" * 64},
)
start_recovery_operation(session, operation)
transition_recovery_operation(
session,
operation,
status=RecoveryStatus.RECOVERY_REQUIRED,
kind="side-effect-failed",
summary="Target index update failed",
evidence={"completed_steps": 1},
failure_summary="target index unavailable",
)
transition_recovery_operation(
session,
operation,
status=RecoveryStatus.RECOVERING,
kind="compensation-started",
summary="Compensation started",
)
transition_recovery_operation(
session,
operation,
status=RecoveryStatus.RECOVERED,
kind="compensation-verified",
summary="Source inventory restored",
evidence={
"verified": True,
"checks": {"object_inventory": "matched"},
},
)
session.commit()
assert operation.status == RecoveryStatus.RECOVERED.value
assert operation.checkpoint_count == 6
assert verify_recovery_evidence_chain(session, operation.id) is True
finally:
session.close()
engine.dispose()
def test_recovery_transition_cannot_skip_preparation() -> None:
session, engine = _session()
try:
operation = plan_recovery_operation(
session,
installation_id="installation-1",
module_id="core",
operation_type="configuration-change",
idempotency_key="config-1",
request={"revision": 2},
recovery_plan=RecoveryPlan(
mode=RecoveryMode.ATOMIC,
verification_steps=("read current revision",),
),
)
with pytest.raises(RecoveryGuaranteeError, match="not allowed"):
start_recovery_operation(session, operation)
finally:
session.close()
engine.dispose()
def test_non_atomic_failure_requires_explicit_recovery_and_verified_completion() -> (
None
):
session, engine = _session()
try:
operation = plan_recovery_operation(
session,
installation_id="installation-1",
module_id="campaign",
operation_type="artifact-publish",
idempotency_key="publish-1",
request={"version": "version-1"},
recovery_plan=RecoveryPlan(
mode=RecoveryMode.COMPENSATION,
compensation_steps=("delete published objects",),
verification_steps=("verify object inventory",),
),
)
prepare_recovery_operation(
session,
operation,
evidence={"preconditions": "verified"},
)
start_recovery_operation(session, operation)
with pytest.raises(RecoveryGuaranteeError, match="cannot be marked failed"):
transition_recovery_operation(
session,
operation,
status=RecoveryStatus.FAILED,
kind="failed",
summary="Object publication failed",
)
transition_recovery_operation(
session,
operation,
status=RecoveryStatus.RECOVERY_REQUIRED,
kind="recovery-required",
summary="Published objects require compensation",
)
transition_recovery_operation(
session,
operation,
status=RecoveryStatus.RECOVERING,
kind="compensation-started",
summary="Removing published objects",
)
with pytest.raises(RecoveryGuaranteeError, match="verified evidence"):
transition_recovery_operation(
session,
operation,
status=RecoveryStatus.RECOVERED,
kind="compensation-finished",
summary="Objects removed",
)
finally:
session.close()
engine.dispose()
def test_recovery_evidence_rejects_plaintext_secrets() -> None:
session, engine = _session()
try:
with pytest.raises(RecoveryGuaranteeError, match="plaintext secrets"):
plan_recovery_operation(
session,
installation_id="installation-1",
module_id="mail",
operation_type="transport-change",
idempotency_key="transport-1",
request={"transport_id": "smtp-1"},
metadata={"password": "do-not-store"},
recovery_plan=RecoveryPlan(
mode=RecoveryMode.ATOMIC,
verification_steps=("read persisted transport",),
),
)
finally:
session.close()
engine.dispose()
def test_fenced_recovery_rejects_low_level_mutation_without_claim() -> None:
session, engine = _session()
try:
claim = acquire_lease(
session,
installation_id="installation-1",
resource_key="files:move-tree",
holder_node_id="worker-1",
holder_incarnation="worker-1-incarnation",
ttl_seconds=300,
now=datetime.now(timezone.utc),
)
assert claim is not None
operation = plan_recovery_operation(
session,
installation_id="installation-1",
module_id="files",
operation_type="move-tree",
idempotency_key="move-fenced-1",
request={"source": "a", "target": "b"},
recovery_plan=RecoveryPlan(
mode=RecoveryMode.COMPENSATION,
compensation_steps=("restore source links",),
verification_steps=("compare object inventory",),
),
lease_claim=claim,
)
with pytest.raises(RecoveryGuaranteeError, match="distributed lease fence"):
transition_recovery_operation(
session,
operation,
status=RecoveryStatus.PREPARED,
kind="prepared",
summary="Attempted without fence",
evidence={"inventory_sha256": "a" * 64},
)
with pytest.raises(RecoveryGuaranteeError, match="distributed lease fence"):
record_recovery_checkpoint(
session,
operation,
kind="manual-note",
summary="Attempted without fence",
evidence={},
)
prepared = prepare_recovery_operation(
session,
operation,
evidence={"inventory_sha256": "a" * 64},
lease_claim=claim,
)
assert prepared.status == RecoveryStatus.PREPARED.value
finally:
session.close()
engine.dispose()
+125
View File
@@ -0,0 +1,125 @@
from __future__ import annotations
import asyncio
from types import SimpleNamespace
from govoplan_core.core.runtime_coordination import (
RuntimeCoordinationError,
RuntimeIdentity,
)
from govoplan_core.server.runtime_agent import RuntimeNodeAgent
class _Session:
def __enter__(self):
return self
def __exit__(self, *_args) -> None:
return None
def commit(self) -> None:
return None
class _Database:
def SessionLocal(self) -> _Session:
return _Session()
class _Consumer:
def __init__(self) -> None:
self.cancelled: list[str] = []
self.added: list[str] = []
def cancel_task_queue(self, queue: str) -> None:
self.cancelled.append(queue)
def add_task_queue(self, queue: str) -> None:
self.added.append(queue)
def test_api_runtime_agent_fails_readiness_on_heartbeat_error() -> None:
agent = RuntimeNodeAgent(
settings=SimpleNamespace(
installation_id="installation-1",
runtime_role="api",
runtime_node_id="api-1",
runtime_heartbeat_seconds=15,
celery_queues="",
),
software_version="0.1.14",
module_ids=("core",),
)
agent.coordination_healthy = True
def fail() -> None:
raise RuntimeCoordinationError("database unavailable")
agent._heartbeat = fail
assert asyncio.run(agent._heartbeat_once()) is False
assert agent.coordination_healthy is False
agent._heartbeat = lambda: None
assert asyncio.run(agent._heartbeat_once()) is True
assert agent.coordination_healthy is True
def test_worker_disables_consumers_without_reclaiming_stale_identity(
monkeypatch,
) -> None:
from govoplan_core import celery_app
identity = RuntimeIdentity(
installation_id="installation-1",
node_id="worker-1",
incarnation="stale-incarnation",
role="worker",
software_version="0.1.14",
composition_hash="a" * 64,
queues=("default", "mail"),
)
consumer = _Consumer()
original_state = (
celery_app._worker_identity,
celery_app._worker_consumer,
celery_app._worker_draining,
)
celery_app._worker_identity = identity
celery_app._worker_consumer = consumer
celery_app._worker_draining = False
monkeypatch.setattr(celery_app, "get_database", lambda: _Database())
def reject_heartbeat(*_args, **_kwargs):
raise RuntimeCoordinationError("stale node incarnation")
monkeypatch.setattr(
celery_app,
"heartbeat_runtime_node",
reject_heartbeat,
)
monkeypatch.setattr(
celery_app,
"register_runtime_node",
lambda *_args, **_kwargs: (_ for _ in ()).throw(
AssertionError("heartbeat failures must not reclaim an identity")
),
)
try:
celery_app._heartbeat_worker_runtime()
assert consumer.cancelled == ["default", "mail"]
assert celery_app._worker_draining is True
monkeypatch.setattr(
celery_app,
"heartbeat_runtime_node",
lambda *_args, **_kwargs: SimpleNamespace(state="active"),
)
celery_app._heartbeat_worker_runtime()
assert consumer.added == ["default", "mail"]
assert celery_app._worker_draining is False
finally:
(
celery_app._worker_identity,
celery_app._worker_consumer,
celery_app._worker_draining,
) = original_state
+130
View File
@@ -0,0 +1,130 @@
from __future__ import annotations
from datetime import datetime, timedelta, timezone
import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import Session
from govoplan_core.core.runtime_coordination import (
DistributedLease,
RuntimeIdentity,
RuntimeNode,
RuntimeNodeState,
StaleFence,
acquire_lease,
assert_lease_fence,
heartbeat_runtime_node,
list_runtime_nodes,
register_runtime_node,
request_runtime_node_drain,
)
from govoplan_core.db.base import Base
def _session() -> tuple[Session, object]:
engine = create_engine("sqlite+pysqlite:///:memory:")
Base.metadata.create_all(
engine,
tables=[RuntimeNode.__table__, DistributedLease.__table__],
)
return Session(engine, expire_on_commit=False), engine
def _identity(*, node_id: str, incarnation: str) -> RuntimeIdentity:
return RuntimeIdentity(
installation_id="installation-1",
node_id=node_id,
incarnation=incarnation,
role="worker",
software_version="0.1.14",
composition_hash="a" * 64,
queues=("default",),
)
def test_node_incarnation_guards_heartbeat_and_drain_state() -> None:
session, engine = _session()
now = datetime(2026, 8, 1, 10, 0, tzinfo=timezone.utc)
first = _identity(node_id="worker-1", incarnation="first")
replacement = _identity(node_id="worker-1", incarnation="replacement")
try:
register_runtime_node(session, first, now=now)
heartbeat_runtime_node(session, first, now=now + timedelta(seconds=5))
register_runtime_node(session, replacement, now=now + timedelta(seconds=10))
with pytest.raises(RuntimeError, match="stale node incarnation"):
heartbeat_runtime_node(session, first, now=now + timedelta(seconds=15))
node = request_runtime_node_drain(
session,
installation_id="installation-1",
node_id="worker-1",
reason="rolling update",
now=now + timedelta(seconds=20),
)
assert node.state == RuntimeNodeState.DRAINING.value
rows = list_runtime_nodes(
session,
installation_id="installation-1",
stale_after_seconds=60,
now=now + timedelta(seconds=25),
)
assert rows[0]["incarnation"] == "replacement"
assert rows[0]["state"] == "draining"
assert rows[0]["stale"] is False
finally:
session.close()
engine.dispose()
def test_expired_lease_reassignment_increments_fence() -> None:
session, engine = _session()
now = datetime(2026, 8, 1, 10, 0, tzinfo=timezone.utc)
try:
first = acquire_lease(
session,
installation_id="installation-1",
resource_key="migration:release",
holder_node_id="node-a",
holder_incarnation="a1",
ttl_seconds=30,
now=now,
)
assert first is not None and first.fencing_token == 1
blocked = acquire_lease(
session,
installation_id="installation-1",
resource_key="migration:release",
holder_node_id="node-b",
holder_incarnation="b1",
ttl_seconds=30,
now=now + timedelta(seconds=10),
)
assert blocked is None
replacement = acquire_lease(
session,
installation_id="installation-1",
resource_key="migration:release",
holder_node_id="node-b",
holder_incarnation="b1",
ttl_seconds=30,
now=now + timedelta(seconds=31),
)
assert replacement is not None
assert replacement.fencing_token == 2
with pytest.raises(StaleFence):
assert_lease_fence(
session,
first,
now=now + timedelta(seconds=32),
)
assert_lease_fence(
session,
replacement,
now=now + timedelta(seconds=32),
)
finally:
session.close()
engine.dispose()
+43
View File
@@ -0,0 +1,43 @@
from __future__ import annotations
import unittest
from unittest.mock import patch
from govoplan_core.commands.wait_for_database import main
class WaitForDatabaseTests(unittest.TestCase):
def test_waits_until_exact_configured_heads_are_visible(self) -> None:
with (
patch(
"govoplan_core.commands.wait_for_database.configured_migration_heads",
return_value=("head-a", "head-b"),
),
patch(
"govoplan_core.commands.wait_for_database.database_migration_heads",
side_effect=[("head-a",), ("head-a", "head-b")],
),
patch("govoplan_core.commands.wait_for_database.time.sleep"),
):
result = main(["--timeout-seconds", "1", "--poll-seconds", "0.01"])
self.assertEqual(0, result)
def test_returns_temporary_failure_when_heads_do_not_match(self) -> None:
with (
patch(
"govoplan_core.commands.wait_for_database.configured_migration_heads",
return_value=("head-b",),
),
patch(
"govoplan_core.commands.wait_for_database.database_migration_heads",
return_value=("head-a",),
),
):
result = main(["--timeout-seconds", "0"])
self.assertEqual(75, result)
if __name__ == "__main__":
unittest.main()