feat: AI Town MVP - 完整三层架构实现

- Python FastAPI 引擎:世界状态模拟、全局能量条、世界事件系统
- Node.js WebSocket 服务器:实时通信、事件队列批处理
- 前端仪表盘:世界状态可视化、行动日志、事件展示

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
empty
2025-12-30 10:29:35 +08:00
commit 8b90d55f02
20 changed files with 2729 additions and 0 deletions

View File

View File

@@ -0,0 +1,173 @@
import pytest
from fastapi.testclient import TestClient
import os
import json
from pathlib import Path
from app.main import app
from app.state import STATE_FILE, get_default_state, save_state
@pytest.fixture(autouse=True)
def reset_state():
"""每个测试前重置状态文件"""
if STATE_FILE.exists():
STATE_FILE.unlink()
save_state(get_default_state())
yield
if STATE_FILE.exists():
STATE_FILE.unlink()
@pytest.fixture
def client():
return TestClient(app)
def test_step_tick_increments(client):
"""测试 tick 递增"""
# 第一次调用
resp1 = client.post("/step", json={"events": []})
assert resp1.status_code == 200
data1 = resp1.json()
assert data1["world_state"]["tick"] == 1
# 第二次调用
resp2 = client.post("/step", json={"events": []})
assert resp2.status_code == 200
data2 = resp2.json()
assert data2["world_state"]["tick"] == 2
def test_mood_bounded(client):
"""测试 town_mood 有界 (-10 到 10)"""
# 发送多个"支持"事件,测试上限
events = [
{"type": "comment", "text": "支持", "user": "test", "ts": i}
for i in range(15)
]
resp = client.post("/step", json={"events": events})
data = resp.json()
assert data["world_state"]["town_mood"] <= 10
# 重置状态
save_state(get_default_state())
# 发送多个"混乱"事件,测试下限
events = [
{"type": "comment", "text": "混乱", "user": "test", "ts": i}
for i in range(15)
]
resp = client.post("/step", json={"events": events})
data = resp.json()
assert data["world_state"]["town_mood"] >= -10
def test_memory_max_length(client):
"""测试 memory 长度不超过 3"""
events = [
{"type": "comment", "text": f"消息{i}", "user": "user1", "ts": i}
for i in range(10)
]
resp = client.post("/step", json={"events": events})
data = resp.json()
for agent_id, agent in data["world_state"]["agents"].items():
assert len(agent["memory"]) <= 3
def test_weather_change(client):
"""测试天气变化"""
# 下雨
resp = client.post("/step", json={
"events": [{"type": "comment", "text": "下雨了", "user": "test", "ts": 1}]
})
assert resp.json()["world_state"]["weather"] == "rainy"
# 天晴
resp = client.post("/step", json={
"events": [{"type": "comment", "text": "天晴了", "user": "test", "ts": 2}]
})
assert resp.json()["world_state"]["weather"] == "sunny"
def test_actions_generated(client):
"""测试 actions 生成"""
resp = client.post("/step", json={"events": []})
data = resp.json()
assert len(data["actions"]) == 2
agent_ids = [a["agent_id"] for a in data["actions"]]
assert "alice" in agent_ids
assert "bob" in agent_ids
for action in data["actions"]:
assert "say" in action
assert "do" in action
def test_global_meter_accumulates(client):
"""测试能量累计"""
# comment 类型 +1
events = [{"type": "comment", "text": "hello", "user": "test", "ts": 1}]
resp = client.post("/step", json={"events": events})
data = resp.json()
assert data["world_state"]["global_meter"]["value"] == 1
# support 类型 +3
events = [{"type": "support", "text": "支持", "user": "test", "ts": 2}]
resp = client.post("/step", json={"events": events})
data = resp.json()
assert data["world_state"]["global_meter"]["value"] == 4
# chaos 类型 +5
events = [{"type": "chaos", "text": "混乱", "user": "test", "ts": 3}]
resp = client.post("/step", json={"events": events})
data = resp.json()
assert data["world_state"]["global_meter"]["value"] == 9
def test_global_event_triggers(client):
"""测试世界级事件触发"""
# 发送足够多的 chaos 事件达到阈值 (100)
# chaos +5, 需要 20 个事件
events = [
{"type": "chaos", "text": f"chaos{i}", "user": "test", "ts": i}
for i in range(20)
]
resp = client.post("/step", json={"events": events})
data = resp.json()
# 验证事件触发
assert data["global_event"]["triggered"] is True
assert data["global_event"]["event"] is not None
assert "name" in data["global_event"]["event"]
assert "description" in data["global_event"]["event"]
# 验证能量重置和冷却设置
assert data["world_state"]["global_meter"]["value"] == 0
assert data["world_state"]["global_meter"]["cooldown"] == 5
def test_global_event_cooldown(client):
"""测试冷却期间不触发事件"""
# 先触发一次事件
events = [
{"type": "chaos", "text": f"chaos{i}", "user": "test", "ts": i}
for i in range(20)
]
resp = client.post("/step", json={"events": events})
assert resp.json()["global_event"]["triggered"] is True
# 冷却期间再次达到阈值,不应触发
events = [
{"type": "chaos", "text": f"chaos{i}", "user": "test", "ts": i}
for i in range(20)
]
resp = client.post("/step", json={"events": events})
data = resp.json()
# 不应触发(冷却中)
assert data["global_event"]["triggered"] is False
# 冷却递减
assert data["world_state"]["global_meter"]["cooldown"] == 4