tests/test_tally.py
3.5 KB · 112 lines · python Raw
1 """Tests for VoteTally (Byzantine-aware vote aggregation)."""
2
3 from __future__ import annotations
4
5 import pytest
6
7 from pqc_ai_governance import (
8 ByzantineDetectedError,
9 GovernanceNode,
10 GovernanceProposal,
11 NodeRegistry,
12 SignedVote,
13 Vote,
14 VoteDecision,
15 VoteTally,
16 )
17
18
19 def test_approve_vote_increments_approve_weight(
20 alice: GovernanceNode,
21 registry: NodeRegistry,
22 sample_proposal: GovernanceProposal,
23 ) -> None:
24 tally = VoteTally(proposal=sample_proposal, registry=registry)
25 tally.add(alice.cast_vote(sample_proposal, VoteDecision.APPROVE))
26 assert tally.approve_weight == 1
27 assert tally.reject_weight == 0
28
29
30 def test_reject_vote_increments_reject_weight(
31 dave: GovernanceNode,
32 registry: NodeRegistry,
33 sample_proposal: GovernanceProposal,
34 ) -> None:
35 tally = VoteTally(proposal=sample_proposal, registry=registry)
36 tally.add(dave.cast_vote(sample_proposal, VoteDecision.REJECT))
37 # dave has weight 2
38 assert tally.reject_weight == 2
39 assert tally.approve_weight == 0
40
41
42 def test_invalid_signature_rejected_and_recorded(
43 alice: GovernanceNode,
44 registry: NodeRegistry,
45 sample_proposal: GovernanceProposal,
46 ) -> None:
47 tally = VoteTally(proposal=sample_proposal, registry=registry)
48 signed = alice.cast_vote(sample_proposal, VoteDecision.APPROVE)
49 signed.vote.rationale = "TAMPERED"
50 tally.add(signed)
51 assert tally.approve_weight == 0
52 assert len(tally.invalid_votes) == 1
53 assert tally.invalid_votes[0][1] == "invalid signature"
54
55
56 def test_wrong_proposal_hash_rejected(
57 alice: GovernanceNode,
58 registry: NodeRegistry,
59 sample_proposal: GovernanceProposal,
60 ) -> None:
61 tally = VoteTally(proposal=sample_proposal, registry=registry)
62 bogus = Vote.create(
63 proposal_id=sample_proposal.proposal_id,
64 proposal_hash="0" * 64, # wrong
65 voter_did=alice.did,
66 decision=VoteDecision.APPROVE,
67 )
68 tally.add(SignedVote(vote=bogus, algorithm="x", signature="", public_key=""))
69 assert len(tally.invalid_votes) == 1
70 assert tally.invalid_votes[0][1] == "proposal hash mismatch"
71
72
73 def test_non_member_voter_rejected(
74 registry: NodeRegistry,
75 sample_proposal: GovernanceProposal,
76 ) -> None:
77 from quantumshield.identity.agent import AgentIdentity
78
79 intruder = GovernanceNode(
80 identity=AgentIdentity.create("mallory"), name="mallory"
81 )
82 # Deliberately do NOT register the intruder
83 signed = intruder.cast_vote(sample_proposal, VoteDecision.APPROVE)
84 tally = VoteTally(proposal=sample_proposal, registry=registry)
85 tally.add(signed)
86 assert tally.approve_weight == 0
87 assert len(tally.invalid_votes) == 1
88 assert tally.invalid_votes[0][1] == "non-member voter"
89
90
91 def test_double_vote_same_decision_is_idempotent(
92 alice: GovernanceNode,
93 registry: NodeRegistry,
94 sample_proposal: GovernanceProposal,
95 ) -> None:
96 tally = VoteTally(proposal=sample_proposal, registry=registry)
97 tally.add(alice.cast_vote(sample_proposal, VoteDecision.APPROVE))
98 tally.add(alice.cast_vote(sample_proposal, VoteDecision.APPROVE))
99 assert tally.approve_weight == 1
100 assert len(tally.valid_votes) == 1
101
102
103 def test_double_vote_different_decisions_raises(
104 alice: GovernanceNode,
105 registry: NodeRegistry,
106 sample_proposal: GovernanceProposal,
107 ) -> None:
108 tally = VoteTally(proposal=sample_proposal, registry=registry)
109 tally.add(alice.cast_vote(sample_proposal, VoteDecision.APPROVE))
110 with pytest.raises(ByzantineDetectedError):
111 tally.add(alice.cast_vote(sample_proposal, VoteDecision.REJECT))
112