emergence-mini-dilles/tests/test_governance.py
Jeuners ddf9598518 Emergence-Mini: minimaler Klon von Emergence-World
4 Agenten, 14 Landmarks, 15 Tools, 240x240 Grid, SQLite-Persistenz.
Round-Robin Turn-Manager mit Reactive Triggern, Town-Hall-Voting
(70%-Threshold) mit Live-Constitution-Amendment.

- engine/: db, world, agents, needs, tools, reasoning, governance, turn
- web/: Canvas-basierte Live-View mit WebSocket-Stream
- server.py: FastAPI + WebSocket auf 127.0.0.1:8080
- tests/: 70 Unit + Integration Tests (pytest), alle gruen
- smoke_test.py: 50+ End-to-End-Checks
- README: Quickstart, Architektur, Security, Tests, Lizenz
- .gitignore: DB, Cache, Logs

Basiert auf https://github.com/EmergenceAI/Emergence-World
(Lizenz: CC-BY-NC-4.0, Research-only)
2026-06-15 01:07:38 +02:00

210 lines
8.5 KiB
Python

"""Governance tests: 70% threshold, vote counting, amendment apply."""
import sqlite3
import pytest
def test_load_seed_constitution(tmp_db):
"""Fresh DB should load the 5-article seed constitution from the JSON file."""
from engine import governance
con = governance.load_constitution()
assert len(con["articles"]) == 5
assert con["articles"][0]["title"] == "Non-Finality"
assert con["articles"][4]["title"] == "ComputeCredit Economy"
def test_pass_threshold_constant():
from engine import governance
assert governance.PASS_THRESHOLD == 0.7
def test_submit_and_vote_proposal(tmp_db):
from engine import tools, agents as agents_mod
# All agents at town_hall
for aid in ("anchor", "flora", "lovely", "spark"):
tools.get("go_to_place").handler(agents_mod.get(aid), {"place": "town_hall"}, {})
# anchor submits
res = tools.get("submit_townhall_proposal").handler(
agents_mod.get("anchor"),
{"title": "Article 6 — Test", "body": "Test body", "category": "general"},
{}
)
assert res["ok"]
def test_maybe_close_threshold_unreachable(tmp_db):
"""Direct test of maybe_close_proposal: 1/4 for, 3 remaining.
Max possible for = 1+3=4. Threshold is 0.7*4=2.8 -> 4 >= 2.8 -> not unreachable.
"""
from engine import tools, agents as agents_mod, governance
for aid in ("anchor", "flora", "lovely", "spark"):
tools.get("go_to_place").handler(agents_mod.get(aid), {"place": "town_hall"}, {})
tools.get("submit_townhall_proposal").handler(
agents_mod.get("anchor"),
{"title": "X", "body": "y", "category": "general"}, {}
)
c = sqlite3.connect(str(tmp_db))
c.row_factory = sqlite3.Row
pid = c.execute("SELECT MAX(id) AS id FROM proposals").fetchone()["id"]
c.close()
res = tools.get("vote_on_proposal").handler(
agents_mod.get("anchor"), {"proposal_id": pid, "vote": "for"}, {}
)
assert res["ok"]
# still active (1 for, 3 remaining, max 4 >= 2.8)
assert res["proposal_result"]["status"] == "active"
def test_maybe_close_unreachable_at_2_against(tmp_db):
"""2/4 against. Remaining = 2. Max for = 2. 2 < 2.8 -> unreachable -> rejected."""
from engine import tools, agents as agents_mod
for aid in ("anchor", "flora", "lovely", "spark"):
tools.get("go_to_place").handler(agents_mod.get(aid), {"place": "town_hall"}, {})
tools.get("submit_townhall_proposal").handler(
agents_mod.get("anchor"),
{"title": "X", "body": "y", "category": "general"}, {}
)
c = sqlite3.connect(str(tmp_db))
c.row_factory = sqlite3.Row
pid = c.execute("SELECT MAX(id) AS id FROM proposals").fetchone()["id"]
c.close()
# 2 against votes
for aid in ("anchor", "flora"):
res = tools.get("vote_on_proposal").handler(
agents_mod.get(aid), {"proposal_id": pid, "vote": "against"}, {}
)
assert res["ok"]
# status should now be rejected (max possible for = 2 < 2.8)
assert res["proposal_result"]["status"] == "rejected"
# 3rd vote fails because proposal is closed
res = tools.get("vote_on_proposal").handler(
agents_mod.get("lovely"), {"proposal_id": pid, "vote": "for"}, {}
)
assert not res["ok"]
def test_vote_threshold_unreachable_rejects(tmp_db):
"""If 3/4 vote against, the remaining vote cannot reach 70% -> auto-rejected."""
from engine import tools, agents as agents_mod
for aid in ("anchor", "flora", "lovely", "spark"):
tools.get("go_to_place").handler(agents_mod.get(aid), {"place": "town_hall"}, {})
tools.get("submit_townhall_proposal").handler(
agents_mod.get("anchor"),
{"title": "Bad Idea", "body": "no", "category": "general"},
{}
)
c = sqlite3.connect(str(tmp_db))
c.row_factory = sqlite3.Row
pid = c.execute("SELECT MAX(id) AS id FROM proposals").fetchone()["id"]
c.close()
# The 2nd "against" vote already triggers auto-reject (max-for = 2 < 2.8)
res = tools.get("vote_on_proposal").handler(
agents_mod.get("anchor"), {"proposal_id": pid, "vote": "against"}, {}
)
assert res["ok"] # still active
res = tools.get("vote_on_proposal").handler(
agents_mod.get("flora"), {"proposal_id": pid, "vote": "against"}, {}
)
# 2nd vote closes it as rejected
assert res["proposal_result"]["status"] == "rejected"
# 3rd vote fails because proposal is closed
res = tools.get("vote_on_proposal").handler(
agents_mod.get("lovely"), {"proposal_id": pid, "vote": "for"}, {}
)
assert not res["ok"]
def test_unanimous_acceptance_amends_constitution(tmp_db):
from engine import tools, agents as agents_mod, governance
for aid in ("anchor", "flora", "lovely", "spark"):
tools.get("go_to_place").handler(agents_mod.get(aid), {"place": "town_hall"}, {})
tools.get("submit_townhall_proposal").handler(
agents_mod.get("anchor"),
{"title": "Article 6 — Daily Standup",
"body": "All agents gather at noon.", "category": "general"},
{}
)
c = sqlite3.connect(str(tmp_db))
c.row_factory = sqlite3.Row
pid = c.execute("SELECT MAX(id) AS id FROM proposals").fetchone()["id"]
c.close()
for aid in ("anchor", "flora", "lovely", "spark"):
tools.get("vote_on_proposal").handler(
agents_mod.get(aid), {"proposal_id": pid, "vote": "for"}, {}
)
new = governance.apply_accepted_proposals_to_constitution()
assert new == [6]
con = governance.load_constitution()
assert len(con["articles"]) == 6
assert con["articles"][5]["title"] == "Article 6 — Daily Standup"
def test_constitution_versioning(tmp_db):
from engine import tools, agents as agents_mod, governance
# Apply two amendments, expect version 1, 2
for aid in ("anchor", "flora", "lovely", "spark"):
tools.get("go_to_place").handler(agents_mod.get(aid), {"place": "town_hall"}, {})
for title in ("Article 6 — One", "Article 7 — Two"):
tools.get("submit_townhall_proposal").handler(
agents_mod.get("anchor"),
{"title": title, "body": "b", "category": "general"},
{}
)
c = sqlite3.connect(str(tmp_db))
c.row_factory = sqlite3.Row
pid = c.execute("SELECT MAX(id) AS id FROM proposals").fetchone()["id"]
c.close()
for aid in ("anchor", "flora", "lovely", "spark"):
tools.get("vote_on_proposal").handler(
agents_mod.get(aid), {"proposal_id": pid, "vote": "for"}, {}
)
governance.apply_accepted_proposals_to_constitution()
c = sqlite3.connect(str(tmp_db))
versions = [r[0] for r in c.execute("SELECT DISTINCT version FROM constitution ORDER BY version").fetchall()]
c.close()
assert versions == [1, 2]
con = governance.load_constitution()
assert len(con["articles"]) == 7
def test_3_of_4_majority_accepted():
"""Standalone: 3/4 = 75% > 70% -> accepted."""
from engine import tools, agents as agents_mod, db, governance
import tempfile, shutil
from pathlib import Path
# new temp db
tmpdir = tempfile.mkdtemp()
old = db.DB_PATH
db.DB_PATH = Path(tmpdir) / "x.db"
try:
db.init_db()
from engine import world
db.set_world_state("landmarks_seeded", False)
db.set_world_state("agents_seeded", False)
world.bootstrap()
agents_mod.bootstrap()
tools.bootstrap()
for aid in ("anchor", "flora", "lovely", "spark"):
tools.get("go_to_place").handler(agents_mod.get(aid), {"place": "town_hall"}, {})
tools.get("submit_townhall_proposal").handler(
agents_mod.get("anchor"),
{"title": "X", "body": "y", "category": "general"}, {}
)
c = sqlite3.connect(str(db.DB_PATH))
pid = c.execute("SELECT MAX(id) AS id FROM proposals").fetchone()[0]
c.close()
# 3 for, 1 against
for aid in ("anchor", "flora", "lovely"):
tools.get("vote_on_proposal").handler(
agents_mod.get(aid), {"proposal_id": pid, "vote": "for"}, {}
)
# spark abstains (no vote cast)
c = sqlite3.connect(str(db.DB_PATH))
c.row_factory = sqlite3.Row
status = c.execute("SELECT status FROM proposals WHERE id=?", (pid,)).fetchone()["status"]
c.close()
# 3/4 = 75% > 70%, but threshold_unreachable logic kicks in:
# f=3, cast=3, remaining=1 -> 3+1=4 >= 0.7*4=2.8 -> still active
assert status == "active"
finally:
db.DB_PATH = old
shutil.rmtree(tmpdir, ignore_errors=True)