tests/test_round.py
3.4 KB · 113 lines · python Raw
1 """Tests for ConsensusRound + QuorumPolicy."""
2
3 from __future__ import annotations
4
5 import time
6
7 import pytest
8
9 from pqc_ai_governance import (
10 ConsensusRound,
11 GovernanceNode,
12 GovernanceProposal,
13 NodeRegistry,
14 ProposalExpiredError,
15 ProposalKind,
16 ProposalStatus,
17 QuorumPolicy,
18 VoteDecision,
19 )
20
21
22 def test_quorum_passes_with_four_of_five_approvals(
23 nodes: list[GovernanceNode],
24 registry: NodeRegistry,
25 sample_proposal: GovernanceProposal,
26 ) -> None:
27 rnd = ConsensusRound(proposal=sample_proposal, registry=registry)
28 alice, bob, carol, dave, eve = nodes
29 for voter in (alice, bob, carol, dave):
30 rnd.cast(voter.cast_vote(sample_proposal, VoteDecision.APPROVE))
31 rnd.cast(eve.cast_vote(sample_proposal, VoteDecision.REJECT))
32
33 result = rnd.finalize(alice)
34 assert result.decision == "passed"
35 assert sample_proposal.status == ProposalStatus.PASSED
36
37
38 def test_quorum_fails_with_only_two_of_five(
39 nodes: list[GovernanceNode],
40 registry: NodeRegistry,
41 sample_proposal: GovernanceProposal,
42 ) -> None:
43 rnd = ConsensusRound(proposal=sample_proposal, registry=registry)
44 alice, bob, *_ = nodes
45 rnd.cast(alice.cast_vote(sample_proposal, VoteDecision.APPROVE))
46 rnd.cast(bob.cast_vote(sample_proposal, VoteDecision.APPROVE))
47 result = rnd.finalize(alice)
48 assert result.decision == "rejected"
49 assert "participation" in result.reason
50
51
52 def test_quorum_fails_when_all_abstain(
53 nodes: list[GovernanceNode],
54 registry: NodeRegistry,
55 sample_proposal: GovernanceProposal,
56 ) -> None:
57 rnd = ConsensusRound(proposal=sample_proposal, registry=registry)
58 for voter in nodes:
59 rnd.cast(voter.cast_vote(sample_proposal, VoteDecision.ABSTAIN))
60 result = rnd.finalize(nodes[0])
61 assert result.decision == "rejected"
62 assert "abstain" in result.reason.lower()
63
64
65 def test_expired_proposal_cast_raises(
66 alice: GovernanceNode,
67 registry: NodeRegistry,
68 ) -> None:
69 proposal = GovernanceProposal.create(
70 kind=ProposalKind.EMERGENCY_FREEZE,
71 subject_id="*",
72 title="freeze",
73 proposer_did=alice.did,
74 ttl_seconds=0,
75 )
76 alice.sign_proposal(proposal)
77 rnd = ConsensusRound(proposal=proposal, registry=registry)
78 time.sleep(0.01)
79 with pytest.raises(ProposalExpiredError):
80 rnd.cast(alice.cast_vote(proposal, VoteDecision.APPROVE))
81
82
83 def test_finalize_signs_result(
84 nodes: list[GovernanceNode],
85 registry: NodeRegistry,
86 sample_proposal: GovernanceProposal,
87 ) -> None:
88 rnd = ConsensusRound(proposal=sample_proposal, registry=registry)
89 for voter in nodes:
90 rnd.cast(voter.cast_vote(sample_proposal, VoteDecision.APPROVE))
91 result = rnd.finalize(nodes[0])
92 assert result.signer_did == nodes[0].did
93 assert result.signature != ""
94 assert result.public_key != ""
95 assert result.algorithm != ""
96
97
98 def test_verify_result_true_for_valid(
99 nodes: list[GovernanceNode],
100 registry: NodeRegistry,
101 sample_proposal: GovernanceProposal,
102 ) -> None:
103 rnd = ConsensusRound(
104 proposal=sample_proposal, registry=registry, policy=QuorumPolicy()
105 )
106 for voter in nodes:
107 rnd.cast(voter.cast_vote(sample_proposal, VoteDecision.APPROVE))
108 result = rnd.finalize(nodes[0])
109 assert ConsensusRound.verify_result(result) is True
110 # Tampered result should fail verification
111 result.reason = "TAMPERED"
112 assert ConsensusRound.verify_result(result) is False
113