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)
210 lines
8.5 KiB
Python
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)
|