tests/test_proposal.py
| 1 | """Tests for GovernanceProposal.""" |
| 2 | |
| 3 | from __future__ import annotations |
| 4 | |
| 5 | import time |
| 6 | |
| 7 | from pqc_ai_governance import GovernanceProposal, ProposalKind, ProposalStatus |
| 8 | |
| 9 | |
| 10 | def test_create_populates_fields() -> None: |
| 11 | prop = GovernanceProposal.create( |
| 12 | kind=ProposalKind.AUTHORIZE_MODEL, |
| 13 | subject_id="did:pqaid:abc", |
| 14 | title="Authorize abc", |
| 15 | proposer_did="did:pqaid:alice", |
| 16 | ) |
| 17 | assert prop.proposal_id.startswith("urn:pqc-gov-prop:") |
| 18 | assert prop.kind == ProposalKind.AUTHORIZE_MODEL |
| 19 | assert prop.status == ProposalStatus.OPEN |
| 20 | assert prop.created_at != "" |
| 21 | assert prop.expires_at != "" |
| 22 | assert prop.signature == "" |
| 23 | |
| 24 | |
| 25 | def test_proposal_hash_is_deterministic() -> None: |
| 26 | prop = GovernanceProposal.create( |
| 27 | kind=ProposalKind.UPDATE_POLICY, |
| 28 | subject_id="policy-1", |
| 29 | title="Raise rate limit", |
| 30 | proposer_did="did:pqaid:alice", |
| 31 | parameters={"max_rate_qps": 100, "window": "1m"}, |
| 32 | ) |
| 33 | h1 = prop.proposal_hash() |
| 34 | h2 = prop.proposal_hash() |
| 35 | assert h1 == h2 |
| 36 | assert len(h1) == 64 |
| 37 | |
| 38 | |
| 39 | def test_is_expired_after_ttl_zero() -> None: |
| 40 | prop = GovernanceProposal.create( |
| 41 | kind=ProposalKind.EMERGENCY_FREEZE, |
| 42 | subject_id="*", |
| 43 | title="freeze now", |
| 44 | proposer_did="did:pqaid:alice", |
| 45 | ttl_seconds=0, |
| 46 | ) |
| 47 | time.sleep(0.01) |
| 48 | assert prop.is_expired() is True |
| 49 | |
| 50 | |
| 51 | def test_to_dict_from_dict_roundtrip() -> None: |
| 52 | original = GovernanceProposal.create( |
| 53 | kind=ProposalKind.AUTHORIZE_AGENT, |
| 54 | subject_id="did:pqaid:agent-x", |
| 55 | title="Authorize agent x", |
| 56 | proposer_did="did:pqaid:alice", |
| 57 | description="grant scope", |
| 58 | parameters={"scope": ["read", "write"]}, |
| 59 | ) |
| 60 | d = original.to_dict() |
| 61 | restored = GovernanceProposal.from_dict(d) |
| 62 | assert restored.proposal_id == original.proposal_id |
| 63 | assert restored.kind == original.kind |
| 64 | assert restored.subject_id == original.subject_id |
| 65 | assert restored.parameters == original.parameters |
| 66 | assert restored.proposal_hash() == original.proposal_hash() |
| 67 | |
| 68 | |
| 69 | def test_canonical_bytes_is_deterministic() -> None: |
| 70 | prop = GovernanceProposal.create( |
| 71 | kind=ProposalKind.AUTHORIZE_MODEL, |
| 72 | subject_id="did:pqaid:abc", |
| 73 | title="x", |
| 74 | proposer_did="did:pqaid:alice", |
| 75 | parameters={"b": 2, "a": 1}, |
| 76 | ) |
| 77 | b1 = prop.canonical_bytes() |
| 78 | b2 = prop.canonical_bytes() |
| 79 | assert b1 == b2 |
| 80 | # Keys should be alphabetised deterministically |
| 81 | assert b'"a":1' in b1 |
| 82 | assert b1.index(b'"a":1') < b1.index(b'"b":2') |
| 83 | |