Files
govoplan/tests/test_institutional_governance_journey.py
T
zemion d78b13f9d3
Dependency Audit / dependency-audit (push) Failing after 10s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Failing after 9s
feat: implement institutional governance and recovery architecture
2026-08-01 17:46:53 +02:00

592 lines
21 KiB
Python

from __future__ import annotations
from dataclasses import dataclass, replace
from datetime import UTC, datetime, timedelta
import unittest
from sqlalchemy import create_engine
from sqlalchemy.orm import Session
from govoplan_core.core.institutional import (
ActorRepresentationReference,
DecisionEffectReference,
EvidenceReference,
InformationGovernanceReference,
InstitutionalReference,
LegalBasisReference,
MandateDefinition,
PartyRepresentation,
PartySubjectReference,
ProcedureParty,
ServiceBinding,
ServiceDefinition,
TemporalRevision,
service_launch_capability,
)
from govoplan_cases.backend.party_context import CasePartyContext
from govoplan_cases.backend.db.models import (
CaseAccessGrant,
CaseIdentity,
CaseRecordRevision,
CaseStatusDefinition,
CaseTimelineEntry,
CaseTypeDefinition,
)
from govoplan_cases.backend.service import (
create_case_from_intake,
get_case,
upsert_case_status,
upsert_case_type,
)
from govoplan_cases.backend.service_intake import CaseServiceIntake
from govoplan_committee.backend.db.models import (
CommitteeDecisionProjection,
CommitteeWorkspaceEvent,
CommitteeWorkspaceRevision,
)
from govoplan_committee.backend.decision_path import (
CommitteeDecisionPath,
CommitteeDecisionProposal,
)
from govoplan_committee.backend.workspace import (
CommitteeWorkspaceRecord,
SqlCommitteeWorkspace,
get_workspace_object,
record_workspace_object,
)
from govoplan_portal.backend.service_directory import PortalServiceDirectory
from govoplan_decisions.backend.db.models import FormalDecisionRevision
from govoplan_decisions.backend.service import SqlDecisionRegistry
from govoplan_mandates.backend.db.models import MandateRevision
from govoplan_mandates.backend.service import SqlMandateResolver, record_mandate
from govoplan_parties.backend.db.models import ProcedurePartyRevision
from govoplan_parties.backend.service import SqlPartyResolver, record_procedure_party
from govoplan_services.backend.db.models import ServiceDefinitionRevision
from govoplan_services.backend.service import SqlServiceDefinitionProvider, record_service_definition
NOW = datetime(2026, 8, 1, 10, 0, tzinfo=UTC)
def _reference(
kind: str,
object_id: str,
*,
owner: str,
version: str = "1",
) -> InstitutionalReference:
return InstitutionalReference(
kind=kind, # type: ignore[arg-type]
owner_module=owner,
object_id=object_id,
tenant_id="tenant-1",
version=version,
valid_at=NOW,
)
def _evidence(kind: str, evidence_id: str, owner: str) -> EvidenceReference:
return EvidenceReference(
kind=kind, # type: ignore[arg-type]
owner_module=owner,
evidence_id=evidence_id,
tenant_id="tenant-1",
version="1",
checksum=f"sha256:{evidence_id}",
captured_at=NOW,
)
@dataclass
class _Principal:
tenant_id: str = "tenant-1"
account_id: str = "account-1"
class _Registry:
def __init__(self, capabilities: dict[str, object]) -> None:
self.capabilities = capabilities
def has(self, module_id: str) -> bool:
return module_id in {
"portal",
"cases",
"committee",
"postbox",
"services",
"parties",
"mandates",
"decisions",
}
def has_capability(self, name: str) -> bool:
return name in self.capabilities
def capability(self, name: str) -> object:
return self.capabilities[name]
class InstitutionalGovernanceJourneyTests(unittest.TestCase):
def test_service_to_formal_outcome_retains_governed_context(self) -> None:
engine = create_engine("sqlite+pysqlite:///:memory:")
for table in (
ServiceDefinitionRevision.__table__,
CaseStatusDefinition.__table__,
CaseTypeDefinition.__table__,
CaseIdentity.__table__,
CaseRecordRevision.__table__,
CaseAccessGrant.__table__,
CaseTimelineEntry.__table__,
ProcedurePartyRevision.__table__,
MandateRevision.__table__,
FormalDecisionRevision.__table__,
CommitteeWorkspaceRevision.__table__,
CommitteeWorkspaceEvent.__table__,
CommitteeDecisionProjection.__table__,
):
table.create(engine)
session = Session(engine)
principal = _Principal()
self.addCleanup(engine.dispose)
self.addCleanup(session.close)
organization = _reference(
"organization_unit",
"permit-office",
owner="organizations",
)
function = _reference("function", "permit-officer", owner="organizations")
jurisdiction = _reference(
"jurisdiction",
"city-1",
owner="organizations",
)
mandate_ref = _reference(
"mandate",
"permit-mandate",
owner="mandates",
version="4",
)
legal_basis = LegalBasisReference(
kind="law",
authority="Example legislature",
reference="permit-law:3",
version="2026-01",
effective_from=NOW - timedelta(days=100),
)
service = ServiceDefinition(
reference=_reference(
"service",
"permit-service",
owner="services",
version="5",
),
key="permit.apply",
temporal=TemporalRevision(
revision="5",
valid_from=NOW - timedelta(days=1),
valid_to=NOW + timedelta(days=30),
recorded_at=NOW - timedelta(days=2),
change_reason="Service published.",
),
title="Apply for a permit",
audience=("resident",),
legal_bases=(legal_basis,),
required_evidence_types=("application", "identity"),
channels=("portal", "postbox"),
responsible_organization_ref=organization,
responsible_function_ref=function,
mandate_ref=mandate_ref,
jurisdiction_refs=(jurisdiction,),
bindings=(
ServiceBinding("case", "permit-application"),
ServiceBinding("workflow", "workflow:permit-review"),
ServiceBinding("result", "decision:permit"),
),
remedy_refs=("review:administrative-court",),
publication_state="published",
)
record_service_definition(session, principal, definition=service)
service_registry = _Registry(
{
"services.definitions": SqlServiceDefinitionProvider(),
service_launch_capability("case"): object(),
}
)
entry = PortalServiceDirectory(service_registry).list_entries(
session,
principal,
tenant_id="tenant-1",
effective_at=NOW,
audiences=("resident",),
)[0]
self.assertTrue(entry.available)
intake = CaseServiceIntake().plan(
entry.definition,
case_id="case-1",
effective_at=NOW,
)
application_evidence = _evidence("document", "application-1", "files")
upsert_case_status(
session,
principal,
status_key="intake",
label="Intake",
)
upsert_case_status(
session,
principal,
status_key="decided",
label="Decided",
category="decided",
)
upsert_case_type(
session,
principal,
type_key="permit-application",
label="Permit application",
initial_status_key="intake",
allowed_status_keys=("intake", "decided"),
)
case_record = create_case_from_intake(
session,
principal,
plan=intake,
case_number="PERMIT-2026-0001",
title="Permit application",
status_key=None,
opened_at=NOW,
recorded_at=NOW,
change_reason="Portal application received.",
idempotency_key="journey-case-create",
evidence_refs=(application_evidence,),
deadline_at=NOW + timedelta(days=30),
)
applicant = _reference("party", "applicant", owner="parties")
representative = _reference("party", "representative", owner="parties")
address_evidence = _evidence(
"snapshot",
"address-snapshot-1",
"addresses",
)
representative_party = ProcedureParty(
reference=representative,
procedure_ref=case_record.reference,
role="representative",
subject=PartySubjectReference(
kind="identity",
provider="identity",
subject_id="identity-2",
tenant_id="tenant-1",
version="2",
),
temporal=TemporalRevision(
revision="1",
valid_from=NOW - timedelta(days=1),
recorded_at=NOW - timedelta(days=1),
change_reason="Representative added to the procedure.",
),
permitted_channels=("postbox",),
preferred_channels=("postbox",),
delivery_recipient=True,
representations=(
PartyRepresentation(
representative_party_ref=representative,
represented_party_ref=applicant,
power_ref="power-1",
permitted_actions=("submit", "receive"),
temporal=TemporalRevision(
revision="1",
valid_from=NOW - timedelta(days=1),
recorded_at=NOW - timedelta(days=1),
change_reason="Representation power recorded.",
),
evidence=(address_evidence,),
),
),
contact_snapshot_refs=("addresses:snapshot-1",),
evidence=(address_evidence,),
)
record_procedure_party(session, principal, party=representative_party)
party_context = CasePartyContext(
_Registry({"parties.resolver": SqlPartyResolver()})
)
parties = party_context.resolve(
session,
principal,
case_ref=case_record.reference,
effective_at=NOW,
)
delivery_target = party_context.delivery_targets(
parties,
channel="postbox",
)[0]
mandate = MandateDefinition(
reference=mandate_ref,
temporal=TemporalRevision(
revision="4",
valid_from=NOW - timedelta(days=30),
valid_to=NOW + timedelta(days=30),
recorded_at=NOW - timedelta(days=31),
change_reason="Permit authority delegated.",
),
task_types=("committee.formal_decision",),
authority_types=("permit",),
organization_unit_refs=(organization,),
function_refs=(function,),
jurisdiction_refs=(jurisdiction,),
legal_bases=(legal_basis,),
evidence=(_evidence("record", "mandate-record-4", "mandates"),),
authority_ceiling="permit:standard",
)
record_mandate(session, principal, definition=mandate)
workspace = SqlCommitteeWorkspace()
body = CommitteeWorkspaceRecord(
tenant_id="tenant-1",
object_kind="body",
object_id="permit-board",
revision=1,
state="active",
title="Permit board",
recorded_at=NOW,
change_reason="Permit board configured.",
attributes={
"organization_unit_ref": organization.to_dict(),
"function_refs": [function.to_dict()],
"quorum": {"minimum_count": 1},
},
)
meeting = CommitteeWorkspaceRecord(
tenant_id="tenant-1",
object_kind="meeting",
object_id="meeting-1",
revision=1,
state="open",
title="Permit board meeting",
parent_id=body.object_id,
recorded_at=NOW,
change_reason="Meeting opened.",
attributes={
"starts_at": NOW.isoformat(),
"ends_at": (NOW + timedelta(hours=1)).isoformat(),
},
context=case_record.context,
)
agenda = CommitteeWorkspaceRecord(
tenant_id="tenant-1",
object_kind="agenda_item",
object_id="item-1",
revision=1,
state="deliberating",
title="Permit application",
parent_id=meeting.object_id,
recorded_at=NOW,
change_reason="Agenda item entered deliberation.",
attributes={
"position": 1,
"subject_refs": [case_record.reference.to_dict()],
},
context=case_record.context,
evidence=(application_evidence,),
)
approval_ref = _reference("approval", "approval-1", owner="approvals")
vote = CommitteeWorkspaceRecord(
tenant_id="tenant-1",
object_kind="vote",
object_id="vote-1",
revision=1,
state="closed",
title="Vote on permit application",
parent_id=agenda.object_id,
recorded_at=NOW,
change_reason="Vote result accepted.",
attributes={
"method": "recorded",
"choices": ["yes", "no"],
"eligible_count": 3,
"cast_count": 3,
"counts": {"yes": 3, "no": 0},
"quorum_met": True,
"approval_ref": approval_ref.to_dict(),
},
context=case_record.context,
evidence=(_evidence("record", "vote-result-1", "committee"),),
)
for record, key in (
(body, "journey-body"),
(meeting, "journey-meeting"),
(agenda, "journey-agenda"),
(vote, "journey-vote"),
):
record_workspace_object(
session,
principal,
record=record,
idempotency_key=key,
)
decision_registry = SqlDecisionRegistry()
decision_result = CommitteeDecisionPath(
_Registry(
{
"mandates.resolver": SqlMandateResolver(),
"decisions.registry": decision_registry,
"committee.workspace": workspace,
}
)
).decide(
session,
principal,
proposal=CommitteeDecisionProposal(
tenant_id="tenant-1",
decision_id="decision-1",
revision="1",
effective_at=NOW,
meeting_ref="meeting-1",
agenda_item_ref="item-1",
decision_type="permit",
subject_refs=(case_record.reference,),
organization_unit_ref=organization,
function_ref=function,
actor=ActorRepresentationReference(
tenant_id="tenant-1",
account_id="account-1",
identity_id="identity-1",
represented_function_ref=function,
mandate_ref=mandate_ref,
),
approval_refs=(
approval_ref,
),
fact_evidence=(application_evidence, address_evidence),
legal_bases=(legal_basis,),
operative_result="Permit granted.",
reasoning="The application satisfies the effective rule.",
case_ref=case_record.reference,
jurisdiction_refs=(jurisdiction,),
party_refs=(applicant, representative),
record_refs=(
_reference("record", "record-1", owner="audit"),
),
remedy_refs=service.remedy_refs,
review_refs=("review:administrative-court",),
information_governance=InformationGovernanceReference(
classification="restricted",
purposes=("permit-decision", "party-delivery"),
legal_basis_refs=("permit-law:3@2026-01",),
retention_policy_ref="records:permit",
disclosure_state="partly_disclosable",
),
),
observed_effects=(
DecisionEffectReference(
effect_key="postbox.deliver_decision",
state="confirmed",
resource_refs=(
f"postbox:{delivery_target.party_ref.object_id}",
),
audit_event_refs=("audit:delivery-1",),
evidence_refs=(address_evidence.evidence_id,),
),
),
)
decided_agenda = replace(
agenda,
revision=2,
state="decided",
recorded_at=NOW + timedelta(minutes=1),
change_reason="Formal Decision recorded.",
attributes={
**dict(agenda.attributes),
"decision_ref": decision_result.decision.reference.to_dict(),
},
)
record_workspace_object(
session,
principal,
record=decided_agenda,
expected_revision=1,
idempotency_key="journey-agenda-decided",
)
closed_meeting = replace(
meeting,
revision=2,
state="closed",
recorded_at=NOW + timedelta(hours=1),
change_reason="All agenda items completed.",
)
record_workspace_object(
session,
principal,
record=closed_meeting,
expected_revision=1,
idempotency_key="journey-meeting-closed",
)
minute = CommitteeWorkspaceRecord(
tenant_id="tenant-1",
object_kind="minute",
object_id="minute-1",
revision=1,
state="accepted",
title="Accepted permit board minutes",
parent_id=meeting.object_id,
recorded_at=NOW + timedelta(hours=2),
change_reason="Minutes approved.",
attributes={
"content_ref": _reference(
"record",
"meeting-minutes-1",
owner="records",
).to_dict(),
"approval_ref": _reference(
"approval",
"minutes-approval-1",
owner="approvals",
).to_dict(),
},
context=case_record.context,
evidence=(_evidence("record", "minutes-proof-1", "records"),),
)
record_workspace_object(
session,
principal,
record=minute,
idempotency_key="journey-minute",
)
reconstruction = decision_result.reconstruction_payload()["decision"]
persisted = decision_registry.get_decision(
session,
principal,
reference=decision_result.decision.reference,
)
self.assertTrue(decision_result.persisted_by_decision_registry)
self.assertEqual(decision_result.decision, persisted)
self.assertEqual(
case_record,
get_case(session, principal, case_id="case-1"),
)
self.assertEqual(
"decided",
get_workspace_object(
session,
principal,
object_kind="agenda_item",
object_id="item-1",
).state,
)
self.assertEqual(service.reference, intake.context.service_ref)
self.assertEqual("applicant", delivery_target.represented_party_refs[0].object_id)
self.assertEqual("permit-mandate", reconstruction["authority_context"]["mandate_ref"]["object_id"])
self.assertEqual("city-1", reconstruction["authority_context"]["jurisdiction_refs"][0]["object_id"])
self.assertEqual("confirmed", reconstruction["observed_effects"][0]["state"])
self.assertEqual("audit:delivery-1", reconstruction["observed_effects"][0]["audit_event_refs"][0])
self.assertEqual("application-1", reconstruction["fact_evidence"][0]["evidence_id"])
self.assertEqual("The application satisfies the effective rule.", reconstruction["reasoning"])
self.assertEqual("review:administrative-court", reconstruction["review_refs"][0])
if __name__ == "__main__":
unittest.main()