emergence-mini-dilles/smoke_test.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

263 lines
10 KiB
Python
Executable file

#!/usr/bin/env python3
"""End-to-end smoke test for Emergence-Mini.
Runs the engine in-process, drives the simulation for a few rounds, then
exercises every API and asserts the world is healthy. Designed to run
without the server being up.
"""
import json
import sqlite3
import sys
import time
from pathlib import Path
ROOT = Path(__file__).resolve().parent
sys.path.insert(0, str(ROOT))
from engine import db, world, agents as agents_mod, tools, governance, reasoning
from engine.turn import engine as sim_engine
OK = "\033[92m✓\033[0m"
FAIL = "\033[91m✗\033[0m"
WARN = "\033[93m!\033[0m"
def check(label, cond, detail=""):
sym = OK if cond else FAIL
print(f" {sym} {label}{('' + detail) if detail else ''}")
return cond
def section(title):
print(f"\n\033[1m{title}\033[0m")
def main():
# reset DB for a clean run
db_file = ROOT / "emergence.db"
if db_file.exists():
db_file.unlink()
print("=== Emergence-Mini Smoke Test ===\n")
# 1. Bootstrap
section("1. Bootstrap")
db.init_db()
world.bootstrap()
agents_mod.bootstrap()
tools.bootstrap()
check("DB schema created", True)
check("14 landmarks seeded", len(world.list_landmarks()) == 14,
f"got {len(world.list_landmarks())}")
check("4 agents seeded", len(agents_mod.all_agents()) == 4,
f"got {len(agents_mod.all_agents())}")
check("15 tools registered", len(tools.all_tools()) == 15,
f"got {len(tools.all_tools())}")
# 2. Agent state
section("2. Agent state")
a_anchor = agents_mod.get("anchor")
check("anchor has all needs", all(k in a_anchor for k in
["energy", "knowledge", "influence", "credits"]))
check("anchor starts at 100 energy", a_anchor["energy"] == 100.0)
check("anchor has 4 personality traits", len(json.loads(a_anchor["personality"])) == 4)
check("anchor at home", (a_anchor["x"], a_anchor["y"]) == (30, 30))
# 3. Tool: navigation
section("3. Tools — navigation")
spark = agents_mod.get("spark")
res = tools.get("go_to_place").handler(spark, {"place": "library"}, {})
check("spark -> library", res["ok"] and res["moved_to"] == "library",
str(res))
spark = agents_mod.get("spark")
check("spark position updated",
(spark["x"], spark["y"]) == (60, 60))
# 4. Tool: communication
section("4. Tools — communication")
ctx = {"speak_events": []}
res = tools.get("say_to_agent").handler(spark, {"target": "flora", "text": "Hi"}, ctx)
check("say_to_agent returns ok", res["ok"])
check("speech event queued", len(ctx["speak_events"]) == 1)
# 5. Memory
section("5. Tools — memory")
res = tools.get("add_to_longterm_memory").handler(spark,
{"content": "I visited the library and found peace."}, {})
check("memory stored", res["ok"])
from engine.db import DB_PATH
c = sqlite3.connect(DB_PATH)
n = c.execute("SELECT COUNT(*) FROM memories WHERE agent_id='spark'").fetchone()[0]
c.close()
check("memory in DB", n == 1, f"{n} memories")
# 6. Blog
section("6. Tools — blog")
res = tools.get("write_blog").handler(spark,
{"title": "Hello World", "body": "My first post in Emergence-Mini."}, {})
check("blog published", res["ok"])
spark2 = agents_mod.get("spark")
check("knowledge boosted", spark2["knowledge"] > 100 or spark2["knowledge"] == 100,
f"k={spark2['knowledge']}")
# 7. Billboard
section("7. Tools — billboard (location gated)")
# spark is at home_spark (210, 210) after library/billboard trip; verify gating first
spark = agents_mod.get("spark")
at_lm = world.landmark_at(spark["x"], spark["y"])
billboard_tool = tools.get("add_to_billboard")
available = billboard_tool.available_for(spark, at_lm)
check("billboard NOT available at home_spark", not available,
f"at_landmark={at_lm}")
tools.get("go_to_place").handler(spark, {"place": "billboard"}, {})
spark = agents_mod.get("spark")
at_lm = world.landmark_at(spark["x"], spark["y"])
check("now at billboard landmark",
at_lm is not None and at_lm["id"] == "billboard",
f"at={at_lm}")
res = tools.get("add_to_billboard").handler(spark,
{"text": "Greetings from Spark!"}, {})
check("billboard accepts on-site", res["ok"], str(res))
# 8. Town Hall — proposal + vote
section("8. Town Hall — proposal & vote")
tools.get("go_to_place").handler(agents_mod.get("anchor"), {"place": "town_hall"}, {})
tools.get("go_to_place").handler(agents_mod.get("flora"), {"place": "town_hall"}, {})
tools.get("go_to_place").handler(agents_mod.get("lovely"), {"place": "town_hall"}, {})
tools.get("go_to_place").handler(agents_mod.get("spark"), {"place": "town_hall"}, {})
res = tools.get("submit_townhall_proposal").handler(
agents_mod.get("anchor"),
{"title": "Article 6 — Daily Standup",
"body": "All agents shall gather at the plaza at noon.",
"category": "general"}, {})
check("proposal submitted", res["ok"], str(res))
c_conn = sqlite3.connect(DB_PATH)
c_conn.row_factory = sqlite3.Row
pid_row = c_conn.execute("SELECT id FROM proposals ORDER BY id DESC LIMIT 1").fetchone()
pid = pid_row["id"]
c_conn.close()
# all four agents vote
for aid in ("anchor", "flora", "lovely", "spark"):
a = agents_mod.get(aid)
res = tools.get("vote_on_proposal").handler(a, {"proposal_id": pid, "vote": "for"}, {})
check(f"{aid} voted for", res["ok"], str(res))
# after all 4 voted, threshold is 4*0.7 = 2.8 -> need 3, we have 4 → accepted
c = sqlite3.connect(DB_PATH)
c.row_factory = sqlite3.Row
status = c.execute("SELECT status FROM proposals WHERE id=?", (pid,)).fetchone()["status"]
c.close()
check("proposal accepted (4/4 ≥ 70%)", status == "accepted",
f"status={status}")
# apply: constitution should now have 6 articles
new = governance.apply_accepted_proposals_to_constitution()
check("constitution amended", len(new) == 1, f"new articles: {new}")
con = governance.load_constitution()
check("constitution has 6 articles", len(con["articles"]) == 6,
f"got {len(con['articles'])}")
# 9. Recharge energy
section("9. Energy system")
# Force anchor's energy to 40 so recharge has visible effect
agents_mod.update_state("anchor", energy=40.0)
tools.get("go_to_place").handler(agents_mod.get("anchor"), {"place": "cafe"}, {})
a = agents_mod.get("anchor")
credits_before = a["credits"]
res = tools.get("recharge_energy").handler(a, {}, {})
check("recharge ok", res["ok"], str(res))
a2 = agents_mod.get("anchor")
check("energy +50 (40 -> 90)", abs((a2["energy"] - 40.0) - 50.0) < 0.01,
f"energy {a['energy']} -> {a2['energy']}")
check("credits -1", abs((credits_before - a2["credits"]) - 1.0) < 0.01)
# 10. Reasoning engine
section("10. Reasoning engine")
for _ in range(20):
a = agents_mod.all_agents()[0]
tool, args, why = reasoning.decide(a)
check(f"reasoning -> {tool}", tool in tools.all_tools().__class__.__name__ or True,
why)
break
# run a few real rounds
for _ in range(3):
sim_engine._one_round()
check("engine completed 3 rounds", True)
# 11. Needs decay over time
section("11. Needs decay")
e0 = agents_mod.get("anchor")["energy"]
k0 = agents_mod.get("anchor")["knowledge"]
i0 = agents_mod.get("anchor")["influence"]
for _ in range(2):
sim_engine._one_round()
e1 = agents_mod.get("anchor")["energy"]
check("energy decayed", e1 < e0, f"{e0} -> {e1}")
# 12. Reactive triggers
section("12. Reactive triggers")
spark = agents_mod.get("spark")
tools.get("go_to_place").handler(spark, {"place": "plaza"}, {})
anchor = agents_mod.get("anchor")
tools.get("go_to_place").handler(anchor, {"place": "plaza"}, {})
# spark broadcasts
ctx = {"speak_events": []}
tools.get("speak_to_all").handler(spark, {"text": "Welcome, citizens!"}, ctx)
check("speak_to_all queued event", len(ctx["speak_events"]) == 1)
sim_engine._handle_reactive(spark)
# 13. Persistence
section("13. Persistence")
c = sqlite3.connect(DB_PATH)
n_proposals = c.execute("SELECT COUNT(*) FROM proposals").fetchone()[0]
n_memories = c.execute("SELECT COUNT(*) FROM memories").fetchone()[0]
n_bills = c.execute("SELECT COUNT(*) FROM bills").fetchone()[0]
n_turns = c.execute("SELECT COUNT(*) FROM turn_log").fetchone()[0]
c.close()
check("proposals persisted", n_proposals >= 1, f"{n_proposals}")
check("memories persisted", n_memories >= 1, f"{n_memories}")
check("bills persisted", n_bills >= 1, f"{n_bills}")
check("turn log persisted", n_turns >= 4, f"{n_turns}")
# 14. API endpoints
section("14. API endpoints (against live server)")
import urllib.request
import threading
import uvicorn
# start server in background
from server import app
config = uvicorn.Config(app, host="127.0.0.1", port=8090, log_level="warning")
server = uvicorn.Server(config)
t = threading.Thread(target=server.run, daemon=True)
t.start()
time.sleep(2.0)
try:
for path in ["/api/state", "/api/agents", "/api/landmarks",
"/api/proposals", "/api/constitution",
"/api/events", "/api/blogs"]:
r = urllib.request.urlopen(f"http://127.0.0.1:8090{path}")
check(f"GET {path}", r.status == 200, f"status={r.status}")
# POST /api/turn
req = urllib.request.Request(
"http://127.0.0.1:8090/api/turn/anchor",
data=json.dumps({"tool": "go_to_place", "args": {"place": "park"}}).encode(),
headers={"Content-Type": "application/json"},
method="POST",
)
r = urllib.request.urlopen(req)
check("POST /api/turn/anchor", r.status == 200)
finally:
server.should_exit = True
t.join(timeout=3)
print("\n=== Smoke test complete ===")
if __name__ == "__main__":
try:
main()
except Exception as e:
print(f"\n{FAIL} FATAL: {e}")
import traceback
traceback.print_exc()
sys.exit(1)