Files
ai-town/engine-python/tests/test_step.py
empty 296e65ff95 feat(engine): add faction system for social emergence
- Add Factions model (optimists/fearful/neutral)
- Implement classify_faction() based on stance thresholds
- Add update_factions() to track faction distribution
- Add apply_faction_influence() for faction→mood feedback
- Integrate faction system into tick flow

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-30 11:03:52 +08:00

322 lines
9.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
def test_opinion_generated_on_world_effect(client):
"""测试世界事件触发时生成观点"""
# 触发世界事件
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 len(data["world_state"]["world_effects"]) > 0
# 验证每个 agent 都有 opinion
for agent_id, agent in data["world_state"]["agents"].items():
assert agent["opinion"] is not None
assert "about" in agent["opinion"]
assert "text" in agent["opinion"]
assert "tick" in agent["opinion"]
def test_opinion_only_once_per_effect(client):
"""测试同一事件只生成一次观点"""
# 触发世界事件
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()
# 记录第一次的观点
first_opinions = {
aid: agent["opinion"]["text"]
for aid, agent in data["world_state"]["agents"].items()
}
# 再次调用 step事件仍在持续
resp2 = client.post("/step", json={"events": []})
data2 = resp2.json()
# 观点应该保持不变(不重新生成)
for aid, agent in data2["world_state"]["agents"].items():
if agent["opinion"]:
assert agent["opinion"]["text"] == first_opinions[aid]
def test_opinion_cleared_when_no_effect(client):
"""测试无活跃事件时观点为空"""
resp = client.post("/step", json={"events": []})
data = resp.json()
# 无世界事件时opinion 应为 None
for agent_id, agent in data["world_state"]["agents"].items():
assert agent["opinion"] is None
def test_stance_exists_in_agent(client):
"""测试 agent 包含 stance 字段"""
resp = client.post("/step", json={"events": []})
data = resp.json()
for agent_id, agent in data["world_state"]["agents"].items():
assert "stance" in agent
assert "optimism" in agent["stance"]
assert "fear" in agent["stance"]
assert 0.0 <= agent["stance"]["optimism"] <= 1.0
assert 0.0 <= agent["stance"]["fear"] <= 1.0
def test_stance_changes_on_world_event(client):
"""测试世界事件触发时 stance 变化"""
# 记录初始 stance
resp1 = client.post("/step", json={"events": []})
initial_stances = {
aid: agent["stance"].copy()
for aid, agent in resp1.json()["world_state"]["agents"].items()
}
# 触发世界事件
events = [
{"type": "chaos", "text": f"chaos{i}", "user": "test", "ts": i}
for i in range(20)
]
resp2 = client.post("/step", json={"events": events})
data = resp2.json()
# 验证事件触发
assert data["global_event"]["triggered"] is True
# stance 应该有变化(由于情绪和社交影响)
for aid, agent in data["world_state"]["agents"].items():
# stance 值仍在有效范围内
assert 0.0 <= agent["stance"]["optimism"] <= 1.0
assert 0.0 <= agent["stance"]["fear"] <= 1.0
def test_social_influence_deterministic(client):
"""测试社交影响是确定性的(同输入同输出)"""
events = [{"type": "comment", "text": "hello", "user": "test", "ts": 1}]
# 第一次执行
save_state(get_default_state())
resp1 = client.post("/step", json={"events": events})
stances1 = {
aid: agent["stance"]
for aid, agent in resp1.json()["world_state"]["agents"].items()
}
# 重置并再次执行
save_state(get_default_state())
resp2 = client.post("/step", json={"events": events})
stances2 = {
aid: agent["stance"]
for aid, agent in resp2.json()["world_state"]["agents"].items()
}
# 结果应该相同
for aid in stances1:
assert stances1[aid]["optimism"] == stances2[aid]["optimism"]
assert stances1[aid]["fear"] == stances2[aid]["fear"]
def test_factions_exists_in_world_state(client):
"""测试 world_state 包含 factions 字段"""
resp = client.post("/step", json={"events": []})
data = resp.json()
assert "factions" in data["world_state"]
factions = data["world_state"]["factions"]
assert "optimists" in factions
assert "fearful" in factions
assert "neutral" in factions
def test_factions_count_matches_agents(client):
"""测试派系总数等于 agent 数量"""
resp = client.post("/step", json={"events": []})
data = resp.json()
factions = data["world_state"]["factions"]
total = factions["optimists"] + factions["fearful"] + factions["neutral"]
agent_count = len(data["world_state"]["agents"])
assert total == agent_count