Govbot is an AI-powered governance bot that interprets natural language constitutions and facilitates collective decision-making across social platforms. Core features: - Agentic architecture with constitutional reasoning (RAG) - Platform-agnostic design (Mastodon, Discord, Telegram, etc.) - Action primitives for flexible governance processes - Temporal awareness for multi-day proposals and voting - Audit trail with constitutional citations - Reversible actions with supermajority veto - Works with local (Ollama) and cloud AI models Platform support: - Mastodon: Full implementation with streaming, moderation, and admin skills - Discord/Telegram: Platform abstraction ready for implementation Documentation: - README.md: Architecture and overview - QUICKSTART.md: Getting started guide - PLATFORMS.md: Platform implementation guide for developers - MASTODON_SETUP.md: Complete Mastodon deployment guide - constitution.md: Example governance constitution Technical stack: - Python 3.11+ - SQLAlchemy for state management - llm CLI for model abstraction - Mastodon.py for Mastodon integration - Pydantic for configuration validation Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
173 lines
4.6 KiB
Python
173 lines
4.6 KiB
Python
"""
|
|
Basic tests for Govbot functionality.
|
|
|
|
These tests demonstrate the core features without requiring Mastodon.
|
|
"""
|
|
|
|
import pytest
|
|
from pathlib import Path
|
|
import sys
|
|
import tempfile
|
|
import shutil
|
|
|
|
# Add src to path
|
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
|
|
from src.govbot.db.models import init_db, get_session
|
|
from src.govbot.agent import GovernanceAgent
|
|
from src.govbot.governance.constitution import ConstitutionalReasoner
|
|
from src.govbot.governance.primitives import GovernancePrimitives
|
|
|
|
|
|
@pytest.fixture
|
|
def test_db():
|
|
"""Create a temporary test database"""
|
|
temp_dir = tempfile.mkdtemp()
|
|
db_path = Path(temp_dir) / "test.db"
|
|
|
|
engine = init_db(str(db_path))
|
|
session = get_session(engine)
|
|
|
|
yield session
|
|
|
|
session.close()
|
|
shutil.rmtree(temp_dir)
|
|
|
|
|
|
@pytest.fixture
|
|
def constitution_path():
|
|
"""Get path to test constitution"""
|
|
return str(Path(__file__).parent.parent / "constitution.md")
|
|
|
|
|
|
def test_constitution_loading(constitution_path):
|
|
"""Test that constitution loads and parses correctly"""
|
|
reasoner = ConstitutionalReasoner(constitution_path)
|
|
|
|
assert reasoner.constitution_text
|
|
assert len(reasoner.sections) > 0
|
|
|
|
# Test section listing
|
|
sections = reasoner.list_sections()
|
|
assert any("Proposal" in s for s in sections)
|
|
|
|
|
|
def test_constitutional_query(constitution_path):
|
|
"""Test querying the constitution"""
|
|
reasoner = ConstitutionalReasoner(constitution_path)
|
|
|
|
result = reasoner.query("What are the rules for standard proposals?")
|
|
|
|
assert "answer" in result
|
|
assert "citations" in result
|
|
assert len(result["citations"]) > 0
|
|
|
|
|
|
def test_primitives_store_and_query(test_db):
|
|
"""Test basic primitive operations"""
|
|
primitives = GovernancePrimitives(test_db)
|
|
|
|
# Store a record
|
|
record_id = primitives.store_record(
|
|
record_type="test_record",
|
|
data={"foo": "bar", "count": 42},
|
|
actor="test_user",
|
|
reasoning="Testing storage",
|
|
)
|
|
|
|
assert record_id > 0
|
|
|
|
# Query it back
|
|
records = primitives.query_records(record_type="store_test_record")
|
|
assert len(records) > 0
|
|
|
|
|
|
def test_create_process(test_db):
|
|
"""Test creating a governance process"""
|
|
primitives = GovernancePrimitives(test_db)
|
|
|
|
process_id = primitives.create_process(
|
|
process_type="test_proposal",
|
|
creator="test_user",
|
|
deadline_days=6,
|
|
constitutional_basis="Article 3, Section 1",
|
|
initial_state={"proposal_text": "Test proposal"},
|
|
)
|
|
|
|
assert process_id > 0
|
|
|
|
# Verify it was created
|
|
from src.govbot.db import queries
|
|
|
|
process = queries.get_process(test_db, process_id)
|
|
assert process is not None
|
|
assert process.process_type == "test_proposal"
|
|
assert process.creator == "test_user"
|
|
|
|
|
|
def test_vote_counting(test_db):
|
|
"""Test vote counting and threshold checking"""
|
|
primitives = GovernancePrimitives(test_db)
|
|
|
|
# Create a process
|
|
process_id = primitives.create_process(
|
|
process_type="test_proposal",
|
|
creator="alice",
|
|
deadline_days=6,
|
|
constitutional_basis="Article 3",
|
|
initial_state={"votes": {}},
|
|
)
|
|
|
|
# Cast some votes
|
|
primitives.update_process_state(
|
|
process_id,
|
|
{
|
|
"votes.alice": {"vote": "agree"},
|
|
"votes.bob": {"vote": "agree"},
|
|
"votes.charlie": {"vote": "disagree"},
|
|
},
|
|
)
|
|
|
|
# Count votes
|
|
counts = primitives.count_votes(process_id)
|
|
assert counts["agree"] == 2
|
|
assert counts["disagree"] == 1
|
|
|
|
# Check threshold
|
|
passed = primitives.check_threshold(counts, "simple_majority")
|
|
assert passed is True
|
|
|
|
|
|
def test_agent_proposal_creation(test_db, constitution_path):
|
|
"""Test agent creating a proposal from natural language"""
|
|
agent = GovernanceAgent(test_db, constitution_path, model=None)
|
|
|
|
result = agent.process_request(
|
|
request="I propose we have weekly meetings",
|
|
actor="test_user",
|
|
)
|
|
|
|
assert result["success"] is True
|
|
assert result["process_id"] is not None
|
|
assert "proposal" in result["response"].lower()
|
|
|
|
|
|
def test_proposal_interpretation(constitution_path):
|
|
"""Test interpreting different proposal types"""
|
|
reasoner = ConstitutionalReasoner(constitution_path)
|
|
|
|
# Standard proposal
|
|
info = reasoner.interpret_proposal("We should update the guidelines")
|
|
assert "proposal_type" in info
|
|
assert info["discussion_period_days"] >= 2
|
|
|
|
# Urgent proposal
|
|
info = reasoner.interpret_proposal("URGENT: We need to address this spam attack")
|
|
# Should recognize urgency (though AI interpretation may vary)
|
|
assert "proposal_type" in info
|
|
|
|
|
|
if __name__ == "__main__":
|
|
# Run tests
|
|
pytest.main([__file__, "-v"])
|