feat: implement institutional governance and recovery architecture
Dependency Audit / dependency-audit (push) Failing after 10s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Failing after 9s

This commit is contained in:
2026-08-01 17:46:53 +02:00
parent 3c658fa32d
commit d78b13f9d3
45 changed files with 4388 additions and 262 deletions
+38
View File
@@ -1007,6 +1007,44 @@ class CapabilityFitEvidenceTests(unittest.TestCase):
report["proof_scope"]["target_environment"]["observed_subject_id"],
)
def test_reference_readiness_scopes_require_release_bound_signed_proof(
self,
) -> None:
installed = matching_installed_evidence(self.assessment)
catalog, keyring = signed_catalog(
self.assessment,
release_artifacts=catalog_artifacts_for(installed),
)
readiness_scopes = (
"target_environment",
"accessibility",
"privacy",
"security",
"operations",
"recovery",
)
proof, authority = signed_boundary_evidence(
assessment=self.assessment,
installed=installed,
claims=[boundary_claim(scope, "passed") for scope in readiness_scopes],
allowed_scopes=list(readiness_scopes),
)
report = self.review(
catalog=catalog,
keyring=keyring,
installed_evidence=installed,
boundary_evidence=proof,
authority_keyring=authority,
)
self.assertEqual("current", report["status"])
for scope in readiness_scopes:
with self.subTest(scope=scope):
self.assertTrue(report["proof_scope"][scope]["checked"])
self.assertTrue(report["proof_scope"][scope]["valid"])
self.assertTrue(report["proof_scope"]["reference_readiness"]["valid"])
def test_external_provider_claim_requires_explicit_matching_subject(self) -> None:
catalog, keyring = signed_catalog(self.assessment)
installed = matching_installed_evidence(self.assessment)
@@ -0,0 +1,79 @@
from __future__ import annotations
import hashlib
import json
from pathlib import Path
import unittest
from govoplan_core.core.configuration_packages import (
ConfigurationPackageManifest,
ConfigurationPreflightContext,
configuration_package_claim_issues,
dry_run_configuration_package,
)
ROOT = Path(__file__).resolve().parents[1]
PACKAGE_ROOT = ROOT / "packages"
class ConfigurationPackageArtifactTests(unittest.TestCase):
def test_product_package_manifests_are_portable_and_evidence_backed(self) -> None:
paths = tuple(sorted(PACKAGE_ROOT.glob("*/*/package.json")))
self.assertGreaterEqual(len(paths), 2)
package_ids: set[str] = set()
for path in paths:
manifest = ConfigurationPackageManifest.from_mapping(
json.loads(path.read_text(encoding="utf-8"))
)
self.assertNotIn(manifest.package_id, package_ids)
package_ids.add(manifest.package_id)
self.assertEqual((), configuration_package_claim_issues(manifest))
for evidence in manifest.evidence:
evidence_path = ROOT / evidence.reference
self.assertTrue(
evidence_path.is_file(),
f"Missing evidence {evidence.reference} for {manifest.package_id}",
)
if evidence.checksum is not None:
self.assertEqual(
evidence.checksum,
"sha256:" + hashlib.sha256(evidence_path.read_bytes()).hexdigest(),
f"Stale evidence checksum for {manifest.package_id}: {evidence.reference}",
)
for requirement in (
*manifest.required_modules,
*manifest.optional_modules,
):
repository = ROOT.parent / (
"govoplan-" + requirement.module_id.replace("_", "-")
)
self.assertTrue(
repository.is_dir(),
f"Missing repository for {requirement.module_id}",
)
result = dry_run_configuration_package(
manifest,
(),
ConfigurationPreflightContext(
installed_modules={
item.module_id: item.version or "workspace"
for item in manifest.required_modules
},
capabilities=frozenset(manifest.required_capabilities),
),
)
self.assertFalse(
any(item.severity == "blocker" for item in result.diagnostics),
tuple(item.to_dict() for item in result.diagnostics),
)
self.assertIn("product.governed-communication", package_ids)
self.assertIn("product.governed-data-assurance", package_ids)
self.assertIn("product.service-to-decision", package_ids)
if __name__ == "__main__":
unittest.main()
+273 -8
View File
@@ -31,6 +31,7 @@ from govoplan_deploy.bundle import ( # noqa: E402
)
from govoplan_deploy.cli import _receipt_uses_direct_web_port, main # noqa: E402
import govoplan_deploy.cli as deployment_cli # noqa: E402
from govoplan_deploy.kubernetes import render_kubernetes # noqa: E402
from govoplan_deploy.model import ( # noqa: E402
SpecError,
default_spec,
@@ -38,6 +39,12 @@ from govoplan_deploy.model import ( # noqa: E402
)
from govoplan_deploy.planning import _endpoint_check, build_plan # noqa: E402
import govoplan_deploy.planning as deployment_planning # noqa: E402
from govoplan_deploy.recovery import ( # noqa: E402
DeploymentOperationJournal,
load_operation,
recover_operation,
write_applied_state,
)
def run_cli(arguments: list[str]) -> tuple[int, str, str]:
@@ -49,6 +56,242 @@ def run_cli(arguments: list[str]) -> tuple[int, str, str]:
class DeploymentInstallerTests(unittest.TestCase):
def test_pre_migration_failure_restores_checksum_verified_applied_bundle(
self,
) -> None:
with tempfile.TemporaryDirectory(
prefix="govoplan-deploy-recovery-"
) as directory:
paths = bundle_paths(Path(directory))
paths.root.chmod(0o700)
spec = default_spec()
previous_environment = initial_secrets(spec)
atomic_write(paths.spec, canonical_json(spec.to_dict()), mode=0o600)
write_env(paths.env, previous_environment)
atomic_write(
paths.compose, canonical_json(render_compose(spec)), mode=0o600
)
atomic_write(
paths.receipt, canonical_json({"spec_sha256": "old"}), mode=0o600
)
write_applied_state(paths)
atomic_write(paths.spec, canonical_json({"changed": True}), mode=0o600)
journal = DeploymentOperationJournal.begin(
paths,
plan={"installation_id": spec.installation_id},
)
journal.failed(RuntimeError("pull failed"))
result = recover_operation(paths, operation_id=journal.operation_id)
self.assertEqual("configuration_restored", result.action)
self.assertEqual(spec.to_dict(), json.loads(paths.spec.read_text()))
self.assertEqual(previous_environment, read_env(paths.env))
def test_post_migration_failure_requires_forward_recovery(self) -> None:
with tempfile.TemporaryDirectory(
prefix="govoplan-deploy-recovery-"
) as directory:
paths = bundle_paths(Path(directory))
paths.root.chmod(0o700)
spec = default_spec()
atomic_write(paths.spec, canonical_json(spec.to_dict()), mode=0o600)
write_env(paths.env, initial_secrets(spec))
atomic_write(
paths.compose, canonical_json(render_compose(spec)), mode=0o600
)
write_applied_state(paths)
journal = DeploymentOperationJournal.begin(
paths,
plan={"installation_id": spec.installation_id},
)
journal.migration_started()
journal.failed(RuntimeError("health failed"))
result = recover_operation(paths, operation_id=journal.operation_id)
self.assertEqual("forward_recovery", result.action)
self.assertIn("not restored", result.detail)
def test_recovery_refuses_tampered_migration_boundary(self) -> None:
with tempfile.TemporaryDirectory(
prefix="govoplan-deploy-recovery-"
) as directory:
paths = bundle_paths(Path(directory))
paths.root.chmod(0o700)
spec = default_spec()
journal = DeploymentOperationJournal.begin(
paths,
plan={"installation_id": spec.installation_id},
)
journal.migration_started()
payload = json.loads(journal.path.joinpath("operation.json").read_text())
payload["migration_started"] = False
atomic_write(
journal.path / "operation.json",
canonical_json(payload),
mode=0o600,
)
with self.assertRaisesRegex(ValueError, "migration boundary"):
load_operation(paths, operation_id=journal.operation_id)
def test_recovery_verifies_complete_snapshot_before_mutating_bundle(self) -> None:
with tempfile.TemporaryDirectory(
prefix="govoplan-deploy-recovery-"
) as directory:
paths = bundle_paths(Path(directory))
paths.root.chmod(0o700)
spec = default_spec()
atomic_write(paths.spec, canonical_json(spec.to_dict()), mode=0o600)
write_env(paths.env, initial_secrets(spec))
atomic_write(
paths.compose, canonical_json(render_compose(spec)), mode=0o600
)
write_applied_state(paths)
journal = DeploymentOperationJournal.begin(
paths,
plan={"installation_id": spec.installation_id},
)
changed_spec = canonical_json({"changed": True})
atomic_write(paths.spec, changed_spec, mode=0o600)
atomic_write(
journal.path / "before" / "compose.json", b"tampered", mode=0o600
)
with self.assertRaisesRegex(ValueError, "checksum failed"):
recover_operation(paths, operation_id=journal.operation_id)
self.assertEqual(changed_spec, paths.spec.read_bytes())
def test_recovery_refuses_tampered_desired_plan(self) -> None:
with tempfile.TemporaryDirectory(
prefix="govoplan-deploy-recovery-"
) as directory:
paths = bundle_paths(Path(directory))
paths.root.chmod(0o700)
spec = default_spec()
journal = DeploymentOperationJournal.begin(
paths,
plan={"installation_id": spec.installation_id},
)
payload = json.loads(journal.path.joinpath("operation.json").read_text())
payload["desired"]["installation_id"] = "other-installation"
atomic_write(
journal.path / "operation.json",
canonical_json(payload),
mode=0o600,
)
with self.assertRaisesRegex(ValueError, "plan"):
load_operation(paths, operation_id=journal.operation_id)
def test_kubernetes_export_is_stateless_and_uses_exact_secret_contract(
self,
) -> None:
spec = default_spec(
installation_id="govoplan-cluster",
postgres_mode="external",
redis_mode="external",
storage_mode="s3",
api_replicas=3,
web_replicas=2,
worker_replicas=4,
api_image="registry.example.test/govoplan-api@sha256:" + "a" * 64,
web_image="registry.example.test/govoplan-web@sha256:" + "b" * 64,
)
environment = initial_secrets(
spec,
supplied={
"DATABASE_URL": "postgresql+psycopg://user:db-secret@postgres.example.test/govoplan",
"GOVOPLAN_DATABASE_URL_PGTOOLS": "postgresql://user:db-secret@postgres.example.test/govoplan",
"REDIS_URL": "rediss://:redis-secret@redis.example.test/0",
"FILE_STORAGE_S3_ENDPOINT_URL": "https://s3.example.test",
"FILE_STORAGE_S3_REGION": "eu-test-1",
"FILE_STORAGE_S3_ACCESS_KEY_ID": "object-key",
"FILE_STORAGE_S3_SECRET_ACCESS_KEY": "object-secret",
"FILE_STORAGE_S3_BUCKET": "govoplan",
},
)
manifest = render_kubernetes(spec, environment)
rendered = json.dumps(manifest, sort_keys=True)
kinds = [item["kind"] for item in manifest["items"]]
deployments = {
item["metadata"]["name"]: item
for item in manifest["items"]
if item["kind"] == "Deployment"
}
self.assertNotIn("db-secret", rendered)
self.assertNotIn("redis-secret", rendered)
self.assertNotIn("object-secret", rendered)
self.assertNotIn("PersistentVolumeClaim", kinds)
self.assertNotIn("StatefulSet", kinds)
self.assertEqual(3, deployments["govoplan-cluster-api"]["spec"]["replicas"])
self.assertEqual(4, deployments["govoplan-cluster-worker"]["spec"]["replicas"])
scheduler_command = deployments["govoplan-cluster-scheduler"]["spec"][
"template"
]["spec"]["containers"][0]["command"]
self.assertIn("govoplan_core.commands.fenced_run", scheduler_command)
api_init_command = deployments["govoplan-cluster-api"]["spec"]["template"][
"spec"
]["initContainers"][0]["command"]
self.assertIn("govoplan_core.commands.wait_for_database", api_init_command)
migration = next(item for item in manifest["items"] if item["kind"] == "Job")
self.assertRegex(
migration["metadata"]["name"],
r"^govoplan-cluster-migrate-[0-9a-f]{8}$",
)
self.assertEqual(
"forward-recovery",
migration["metadata"]["annotations"]["govoplan.add-ideas.de/recovery-mode"],
)
config = next(item for item in manifest["items"] if item["kind"] == "ConfigMap")
self.assertEqual("shared", config["data"]["GOVOPLAN_STATE_PROFILE"])
self.assertEqual("3", config["data"]["GOVOPLAN_EXPECTED_API_REPLICAS"])
self.assertEqual(
[{"name": "Host", "value": "127.0.0.1"}],
deployments["govoplan-cluster-api"]["spec"]["template"]["spec"][
"containers"
][0]["readinessProbe"]["httpGet"]["httpHeaders"],
)
def test_kubernetes_export_rejects_mutable_release_images(self) -> None:
spec = default_spec(
installation_id="govoplan-cluster",
postgres_mode="external",
redis_mode="external",
storage_mode="s3",
api_image="registry.example.test/govoplan-api:latest",
web_image="registry.example.test/govoplan-web:latest",
)
environment = initial_secrets(
spec,
supplied={
"DATABASE_URL": "postgresql+psycopg://user:secret@postgres.example.test/govoplan",
"GOVOPLAN_DATABASE_URL_PGTOOLS": "postgresql://user:secret@postgres.example.test/govoplan",
"REDIS_URL": "rediss://:secret@redis.example.test/0",
"FILE_STORAGE_S3_ENDPOINT_URL": "https://s3.example.test",
"FILE_STORAGE_S3_REGION": "eu-test-1",
"FILE_STORAGE_S3_ACCESS_KEY_ID": "object-key",
"FILE_STORAGE_S3_SECRET_ACCESS_KEY": "object-secret",
"FILE_STORAGE_S3_BUCKET": "govoplan",
},
)
with self.assertRaisesRegex(ValueError, "digest-pinned"):
render_kubernetes(spec, environment)
def test_kubernetes_export_rejects_installer_managed_state_services(self) -> None:
spec = default_spec(
installation_id="govoplan-cluster",
storage_mode="garage",
)
with self.assertRaisesRegex(ValueError, "external shared PostgreSQL"):
render_kubernetes(spec, initial_secrets(spec))
def test_default_bundle_has_base_modules_and_managed_dependencies(self) -> None:
spec = default_spec()
compose = render_compose(spec)
@@ -80,6 +323,7 @@ class DeploymentInstallerTests(unittest.TestCase):
self.assertNotIn("scheduler", compose["services"])
self.assertEqual("false", values["CELERY_ENABLED"])
self.assertEqual("true", values["GOVOPLAN_ALLOW_PROCESS_LOCAL_LOGIN_THROTTLE"])
self.assertEqual("local", values["GOVOPLAN_STATE_PROFILE"])
def test_self_hosted_rejects_insecure_or_test_only_choices(self) -> None:
with self.assertRaisesRegex(SpecError, "must use HTTPS"):
@@ -154,7 +398,7 @@ class DeploymentInstallerTests(unittest.TestCase):
)
self.assertEqual("rediss://:secret@redis.example.test/0", values["REDIS_URL"])
def test_s3_requires_complete_private_configuration_and_https_in_production(
def test_s3_requires_complete_private_configuration_and_https_endpoint(
self,
) -> None:
evaluation = default_spec(storage_mode="s3")
@@ -168,17 +412,21 @@ class DeploymentInstallerTests(unittest.TestCase):
"FILE_STORAGE_S3_SECRET_ACCESS_KEY": "secret",
"FILE_STORAGE_S3_BUCKET": "govoplan",
}
self.assertEqual(
"s3",
initial_secrets(evaluation, supplied=supplied)["FILE_STORAGE_BACKEND"],
)
with self.assertRaisesRegex(ValueError, "must use HTTPS"):
initial_secrets(evaluation, supplied=supplied)
supplied["FILE_STORAGE_S3_ENDPOINT_URL"] = "https://s3.example.test"
values = initial_secrets(evaluation, supplied=supplied)
self.assertEqual("s3", values["FILE_STORAGE_BACKEND"])
self.assertEqual("true", values["FILE_STORAGE_S3_ENDPOINT_TRUSTED"])
production = default_spec(
profile="self-hosted",
public_url="https://govoplan.example.test",
storage_mode="s3",
)
with self.assertRaisesRegex(ValueError, "must use HTTPS"):
initial_secrets(production, supplied=supplied)
self.assertEqual(
"s3",
initial_secrets(production, supplied=supplied)["FILE_STORAGE_BACKEND"],
)
def test_managed_garage_bootstraps_private_s3_storage(self) -> None:
spec = default_spec(storage_mode="garage")
@@ -187,6 +435,7 @@ class DeploymentInstallerTests(unittest.TestCase):
self.assertEqual("s3", values["FILE_STORAGE_BACKEND"])
self.assertEqual("true", values["FILE_STORAGE_S3_DEPLOYMENT_MANAGED"])
self.assertEqual("false", values["FILE_STORAGE_S3_ENDPOINT_TRUSTED"])
self.assertEqual(
"http://garage:3900",
values["FILE_STORAGE_S3_ENDPOINT_URL"],
@@ -244,6 +493,16 @@ class DeploymentInstallerTests(unittest.TestCase):
)
self.assertIn("server-template web- 2 web:8080", config)
self.assertIn("server-template api- 3 api:8000", config)
self.assertIn("option httpchk GET /health/ready", config)
self.assertIn("http-check send hdr Host 127.0.0.1", config)
self.assertIn(
"headers={'Host': '127.0.0.1'}",
compose["services"]["api"]["healthcheck"]["test"][-1],
)
self.assertEqual(
"host-shared",
initial_secrets(spec)["GOVOPLAN_STATE_PROFILE"],
)
def test_multi_replica_runtime_requires_shared_redis(self) -> None:
with self.assertRaisesRegex(SpecError, "multiple API replicas"):
@@ -725,11 +984,17 @@ class DeploymentInstallerTests(unittest.TestCase):
for index, command in enumerate(commands)
if "--remove-orphans" in command
)
stop_index = next(
index
for index, command in enumerate(commands)
if "stop" in command and "--timeout" in command
)
self.assertLess(stop_index, migrate_index)
self.assertLess(migrate_index, runtime_index)
self.assertIn("postgres", commands[0])
self.assertIn("redis", commands[0])
wait_for_health.assert_called_once_with(
"http://127.0.0.1:8080/health",
"http://127.0.0.1:8080/health/ready",
timeout_seconds=120.0,
)
receipt = json.loads((root / "receipt.json").read_text(encoding="utf-8"))
@@ -0,0 +1,591 @@
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()
+271
View File
@@ -0,0 +1,271 @@
from __future__ import annotations
from dataclasses import dataclass
from datetime import UTC, datetime, timedelta
import unittest
from sqlalchemy import create_engine
from sqlalchemy.orm import Session
from govoplan_core.core.institutional import (
CAPABILITY_FORM_DEFINITIONS,
CAPABILITY_SERVICE_DEFINITIONS,
FormDefinition,
FormFieldDefinition,
InstitutionalReference,
ServiceBinding,
ServiceDefinition,
TemporalRevision,
service_launch_capability,
)
from govoplan_cases.backend.service_intake import (
CAPABILITY_CASES_SERVICE_INTAKE,
CaseServiceIntake,
)
from govoplan_forms.backend.db.models import FormDefinitionRevision
from govoplan_forms.backend.service import (
SqlFormDefinitionProvider,
record_form_definition,
)
from govoplan_forms_runtime.backend.db.models import (
FormInstanceEvent,
FormInstanceIdentity,
FormInstanceRevision,
)
from govoplan_forms_runtime.backend.service import (
FormRuntimeService,
FormsServiceLauncher,
)
from govoplan_portal.backend.service_directory import PortalServiceDirectory
NOW = datetime(2026, 8, 1, 10, 0, tzinfo=UTC)
def _service() -> ServiceDefinition:
return ServiceDefinition(
reference=InstitutionalReference(
kind="service",
owner_module="portal",
object_id="permit",
tenant_id="tenant-1",
version="5",
),
key="permit.apply",
temporal=TemporalRevision(
revision="5",
valid_from=NOW - timedelta(days=1),
valid_to=NOW + timedelta(days=1),
recorded_at=NOW - timedelta(days=2),
),
title="Apply for a permit",
audience=("resident",),
required_evidence_types=("application",),
bindings=(
ServiceBinding("capability", CAPABILITY_CASES_SERVICE_INTAKE),
ServiceBinding("case", "permit-application"),
ServiceBinding("workflow", "workflow:permit-review"),
),
publication_state="published",
)
class _Provider:
def __init__(self, definition: ServiceDefinition) -> None:
self.definition = definition
def get_service_definition(self, session, principal, *, reference, effective_at=None):
return self.definition
def list_service_definitions(self, session, principal, *, tenant_id, query="", limit=100):
return (self.definition,)
class _Registry:
def __init__(self, definition: ServiceDefinition) -> None:
self.capabilities = {
CAPABILITY_SERVICE_DEFINITIONS: _Provider(definition),
CAPABILITY_CASES_SERVICE_INTAKE: CaseServiceIntake(),
service_launch_capability("case"): object(),
}
def has(self, module_id: str) -> bool:
return module_id in {"portal", "cases"}
def has_capability(self, name: str) -> bool:
return name in self.capabilities
def capability(self, name: str) -> object:
return self.capabilities[name]
def require_capability(self, name: str) -> object:
return self.capabilities[name]
@dataclass
class _Principal:
tenant_id: str = "tenant-1"
account_id: str = "account-1"
class _FormRegistry(_Registry):
def __init__(self, definition: ServiceDefinition) -> None:
super().__init__(definition)
self.capabilities[CAPABILITY_FORM_DEFINITIONS] = (
SqlFormDefinitionProvider()
)
self.capabilities[service_launch_capability("form")] = (
FormsServiceLauncher(self)
)
def has(self, module_id: str) -> bool:
return module_id in {"portal", "forms", "forms_runtime"}
class InstitutionalServiceJourneyTests(unittest.TestCase):
def test_one_service_version_drives_portal_and_case_intake(self) -> None:
definition = _service()
registry = _Registry(definition)
entries = PortalServiceDirectory(registry).list_entries(
None,
None,
tenant_id="tenant-1",
effective_at=NOW,
audiences=("resident",),
)
plan = registry.capability(CAPABILITY_CASES_SERVICE_INTAKE).plan(
entries[0].definition,
case_id="case-1",
effective_at=NOW,
)
self.assertTrue(entries[0].available)
self.assertIs(definition, entries[0].definition)
self.assertEqual(definition.reference, plan.service_ref)
self.assertEqual("5", plan.context.service_ref.version)
self.assertEqual("workflow:permit-review", plan.workflow_refs[0])
def test_portal_launches_exact_form_revision_and_persists_submission(self) -> None:
engine = create_engine("sqlite+pysqlite:///:memory:")
for table in (
FormDefinitionRevision.__table__,
FormInstanceIdentity.__table__,
FormInstanceRevision.__table__,
FormInstanceEvent.__table__,
):
table.create(engine)
session = Session(engine)
principal = _Principal()
try:
form = record_form_definition(
session,
principal,
definition=FormDefinition(
reference=InstitutionalReference(
kind="form",
owner_module="forms",
object_id="permit-application",
tenant_id="tenant-1",
version="3",
),
key="permit-application",
temporal=TemporalRevision(
revision="3",
valid_from=NOW - timedelta(days=1),
valid_to=NOW + timedelta(days=1),
recorded_at=NOW - timedelta(days=2),
change_reason="Publish the permit application.",
),
title="Permit application",
fields=(
FormFieldDefinition(
key="applicant_name",
label="Applicant name",
required=True,
constraints={"min_length": 2},
),
),
publication_state="published",
allow_drafts=True,
handoff_kinds=("case",),
),
)
binding = ServiceBinding(
"form",
f"{form.reference.object_id}/{form.reference.version}",
)
service = ServiceDefinition(
reference=InstitutionalReference(
kind="service",
owner_module="services",
object_id="permit",
tenant_id="tenant-1",
version="6",
),
key="permit.apply",
temporal=TemporalRevision(
revision="6",
valid_from=NOW - timedelta(days=1),
valid_to=NOW + timedelta(days=1),
recorded_at=NOW - timedelta(days=2),
),
title="Apply for a permit",
audience=("public",),
bindings=(binding,),
publication_state="published",
)
registry = _FormRegistry(service)
directory = PortalServiceDirectory(registry)
launched = directory.launch_service(
session,
principal,
reference=service.reference,
requested_at=NOW,
idempotency_key="portal-form-launch-1",
parameters={"applicant_name": "Ada Lovelace"},
)
replay = directory.launch_service(
session,
principal,
reference=service.reference,
requested_at=NOW,
idempotency_key="portal-form-launch-1",
parameters={"applicant_name": "Ada Lovelace"},
)
instance = FormRuntimeService(registry).get_instance(
session,
principal,
instance_id=str(launched.metadata["form_instance_id"]),
)
self.assertEqual("form_submission", launched.target_ref.kind)
self.assertEqual(service.reference, launched.service_ref)
self.assertEqual("3", launched.metadata["form_definition_revision"])
self.assertTrue(replay.replayed)
self.assertEqual(launched.target_ref, replay.target_ref)
self.assertEqual(
(
form.reference.owner_module,
form.reference.object_id,
form.reference.tenant_id,
form.reference.version,
),
(
instance.definition_ref.owner_module,
instance.definition_ref.object_id,
instance.definition_ref.tenant_id,
instance.definition_ref.version,
),
)
self.assertEqual(NOW, instance.definition_ref.valid_at)
self.assertEqual(service.reference, instance.service_ref)
self.assertEqual("Ada Lovelace", instance.values["applicant_name"])
finally:
session.close()
engine.dispose()
if __name__ == "__main__":
unittest.main()
+85 -1
View File
@@ -4,17 +4,101 @@ import sys
import unittest
from pathlib import Path
from govoplan_core.core.modules import ModuleManifest
from govoplan_core.core.provider_governance import (
ExternalProviderDeclaration,
ModuleArchitectureDeclaration,
ModuleMaturityEvidence,
ProviderBehaviorDeclaration,
ProviderObjectDeclaration,
)
META_ROOT = Path(__file__).resolve().parents[1]
RELEASE_ROOT = META_ROOT / "tools" / "release"
if str(RELEASE_ROOT) not in sys.path:
sys.path.insert(0, str(RELEASE_ROOT))
from govoplan_release.catalog_entry_synthesis import validate_initial_entry_closure # noqa: E402
from govoplan_release.catalog_entry_synthesis import ( # noqa: E402
manifest_catalog_entry,
validate_initial_entry_closure,
)
from govoplan_release.selective_catalog import apply_repo_updates # noqa: E402
class ReleaseCatalogEntrySynthesisTests(unittest.TestCase):
def test_catalog_entry_preserves_architecture_and_provider_declarations(
self,
) -> None:
provider = ExternalProviderDeclaration(
id="example.records",
module_id="example",
label="Example records",
maturity="read",
operations=("read",),
objects=(
ProviderObjectDeclaration(
object_type="record",
field_groups=("identity", "content"),
authority_modes=("external_mirror",),
default_authority_mode="external_mirror",
),
),
behavior=ProviderBehaviorDeclaration(
freshness="Reports the acquisition timestamp.",
health="Reports source and parser health.",
max_read_items=100,
classifications=("internal",),
purposes=("release contract test",),
retention="The owning package retention policy applies.",
),
)
architecture = ModuleArchitectureDeclaration(
layer="data_reporting_integration",
kind="integration",
maturity="vertical_slice",
evidence=(
ModuleMaturityEvidence(
kind="test",
reference="tests/test_release_catalog_entry_synthesis.py",
summary="Proves catalog metadata preservation.",
),
ModuleMaturityEvidence(
kind="documentation",
reference="docs/INSTITUTIONAL_GOVERNANCE_TARGET_ARCHITECTURE.md",
summary="Defines the provider declaration contract.",
),
),
known_limits=("Test-only provider.",),
supported_authority_modes=("external_mirror",),
owned_concepts=("example transport",),
target_tested_providers=(provider.id,),
)
entry = manifest_catalog_entry(
manifest=ModuleManifest(
id="example",
name="Example",
version="1.2.3",
architecture=architecture,
external_providers=(provider,),
),
repo="govoplan-example",
package="govoplan-example",
version="1.2.3",
description=None,
root=META_ROOT,
repository_base="git+ssh://git@git.add-ideas.de/GovOPlaN",
)
self.assertEqual("vertical_slice", entry["architecture"]["maturity"])
self.assertEqual(
"external_mirror",
entry["external_providers"][0]["objects"][0][
"default_authority_mode"
],
)
def test_selective_update_synthesizes_initial_entries_from_package_manifests(self) -> None:
payload: dict[str, object] = {
"core_release": {},
+28
View File
@@ -17,6 +17,34 @@ from govoplan_release import git_state # noqa: E402
class ReleaseGitStateTests(unittest.TestCase):
def test_manifest_version_does_not_confuse_interface_versions(self) -> None:
from tempfile import TemporaryDirectory
with TemporaryDirectory() as directory:
root = Path(directory) / "govoplan-example"
manifest = root / "src" / "govoplan_example" / "backend" / "manifest.py"
manifest.parent.mkdir(parents=True)
manifest.write_text(
"\n".join(
(
'MODULE_VERSION = "1.2.3"',
"manifest = ModuleManifest(",
' id="example",',
" version=MODULE_VERSION,",
" provides_interfaces=(",
' ModuleInterfaceProvider(name="example.items", version="9.8.7"),',
" ),",
")",
"",
)
),
encoding="utf-8",
)
versions = git_state.read_manifest_versions(root)
self.assertEqual(("1.2.3",), versions)
def test_git_trusts_only_the_resolved_repository_for_each_command(self) -> None:
repository = Path("/workspace/../workspace/govoplan-core")
completed = subprocess.CompletedProcess([], 0, "", "")