Files
liqiang b119135836
Build latest book artifacts / build (push) Canceled after 0s
dependency resolution / resolve (3.11) (push) Canceled after 0s
dependency resolution / resolve (3.13) (push) Canceled after 0s
deploy-pages / build (push) Canceled after 0s
deploy-pages / deploy (push) Canceled after 0s
i18n consistency check / check (push) Canceled after 0s
provider adoption tests / test (chapter2/context-compression) (push) Canceled after 0s
provider adoption tests / test (chapter2/prompt-injection) (push) Canceled after 0s
provider adoption tests / test (chapter2/system-hint) (push) Canceled after 0s
provider adoption tests / test (chapter3/log-sanitization) (push) Canceled after 0s
web-search-agent tests / test (push) Canceled after 0s
web-search-agent tests / agentbook (push) Canceled after 0s
ai-agent-book 精选快照(<2MB 代码与文档,来自 github.com/bojieli/ai-agent-book)
2026-08-20 13:12:50 +00:00

288 lines
11 KiB
Python

import json
import sys
from types import SimpleNamespace
from unittest.mock import patch
import numpy as np
import pytest
import demo
from werewolf.agent import PlayerAgent
from werewolf.game import Judge, create_players
from werewolf.human import HumanPlayerAgent, LiveVoiceSession
from werewolf.roles import Faction, Role
from werewolf.strategy_audit import evaluate_strategy, strategy_acceptance_passes, validate_strategy_result
class NoAudio:
def __init__(self):
self.private_prompts = []
def say(self, speaker, text, round_no, allow_barge_in=False):
self.private_prompts.append(text)
def test_exact_acceptance_roster_has_one_human_and_required_roles():
players = create_players(seed=42, players=7, wolves=2, human_seat=1, voice=NoAudio())
assert sum(isinstance(p, HumanPlayerAgent) for p in players) == 1
roles = [p.role for p in players]
assert roles.count(Role.WEREWOLF) == 2
assert roles.count(Role.SEER) == 1
assert roles.count(Role.WITCH) == 1
assert players[0].role in set(Role)
@pytest.mark.parametrize("seat_count", [6, 7, 8])
def test_every_allowed_live_roster_has_one_human_and_five_to_seven_ai(seat_count):
players = create_players(
seed=seat_count,
players=seat_count,
wolves=2,
human_seat=1,
voice=NoAudio(),
)
roles = [player.role for player in players]
assert sum(isinstance(player, HumanPlayerAgent) for player in players) == 1
assert sum(not isinstance(player, HumanPlayerAgent) for player in players) == seat_count - 1
assert 5 <= seat_count - 1 <= 7
assert roles.count(Role.WEREWOLF) == 2
assert roles.count(Role.SEER) == 1
assert roles.count(Role.WITCH) == 1
assert roles.count(Role.VILLAGER) == seat_count - 4
def test_spoken_player_number_parser():
candidates = ["P2", "P3", "P4"]
assert HumanPlayerAgent._spoken_target("我投三号玩家", candidates, True) == "P3"
assert HumanPlayerAgent._spoken_target("player 4", candidates, True) == "P4"
assert HumanPlayerAgent._spoken_target("I choose player three", candidates, True) == "P3"
assert HumanPlayerAgent._spoken_target("我弃票", candidates, True) is None
assert HumanPlayerAgent._spoken_target("I choose to abstain", candidates, True) is None
assert not HumanPlayerAgent._explicit_none("P1 is not")
def test_live_human_terminal_never_prints_god_view(capsys):
voice = NoAudio()
players = create_players(seed=42, players=7, wolves=2, human_seat=1, voice=voice)
judge = Judge(players, seed=42)
judge.assign_identities()
output = capsys.readouterr().out
assert "上帝视角身份表已隐藏" in output
assert "P2:" not in output
assert any("您的身份" in prompt for prompt in voice.private_prompts)
judge._print_private(["P2"], "SECRET_WOLF_ACTION")
assert "SECRET_WOLF_ACTION" not in capsys.readouterr().out
judge._print_private(["P1"], "MY_PRIVATE_ACTION")
assert "MY_PRIVATE_ACTION" in capsys.readouterr().out
def test_live_default_without_consent_stops_before_game_or_voice_construction(monkeypatch):
monkeypatch.setattr(sys, "argv", ["demo.py"])
with patch("demo.run_game") as run_game, patch(
"werewolf.human.LiveVoiceSession"
) as live_session:
with pytest.raises(SystemExit) as exc:
demo.main()
assert exc.value.code == 2
run_game.assert_not_called()
live_session.assert_not_called()
def test_human_role_is_actually_randomized_by_the_shared_shuffle():
observed = {
create_players(seed=seed, players=7, wolves=2, human_seat=1, voice=NoAudio())[0].role
for seed in range(40)
}
assert observed == set(Role)
class PassivePlayer(PlayerAgent):
def choose_target(self, prompt, candidates, players, allow_none=False):
if self.role == Role.SEER and candidates:
return candidates[0]
return None
def speak(self, players):
return "我暂时没有可验证的判断。"
def vote(self, candidates, players):
return None
def test_three_cycles_count_only_after_night_day_vote_and_round_cap_is_not_fake_win(capsys):
roles = [Role.WEREWOLF, Role.WEREWOLF, Role.SEER, Role.WITCH,
Role.VILLAGER, Role.VILLAGER, Role.VILLAGER]
players = [PassivePlayer(f"P{i + 1}", role, offline=True) for i, role in enumerate(roles)]
judge = Judge(players, seed=7, max_rounds=3)
winner = judge.run()
assert judge.completed_rounds == 3
assert len([r for r in judge.audit.records if r.category == "公开-死讯"]) == 3
assert len([r for r in judge.audit.records if r.category == "公开-放逐"]) == 3
assert winner == Faction.UNDECIDED
output = capsys.readouterr().out
assert "本局未决" in output
assert "获胜阵营:未决" not in output
def test_deterministic_winner_conditions():
roles = [Role.WEREWOLF, Role.WEREWOLF, Role.SEER, Role.WITCH,
Role.VILLAGER, Role.VILLAGER]
players = [PlayerAgent(f"P{i + 1}", role, offline=True) for i, role in enumerate(roles)]
judge = Judge(players)
players[0].alive = players[1].alive = False
assert judge._check_winner() == Faction.GOOD
players[0].alive = players[1].alive = True
players[4].alive = players[5].alive = False
assert judge._check_winner() == Faction.WEREWOLF
players[5].alive = True
assert judge._check_winner() is None
def test_strategy_acceptance_requires_all_named_criteria_and_evidence():
valid = {
"criteria": {
name: {"status": "pass", "evidence": f"quoted evidence for {name}"}
for name in (
"werewolf_concealment", "seer_timing_and_evidence",
"villager_logical_reasoning", "role_consistency",
)
},
"overall_pass": True,
}
assert strategy_acceptance_passes(validate_strategy_result(valid))
valid_fail = {
"criteria": {
name: {"status": "fail", "evidence": f"counterevidence for {name}"}
for name in (
"werewolf_concealment", "seer_timing_and_evidence",
"villager_logical_reasoning", "role_consistency",
)
},
"overall_pass": False,
}
checked_fail = validate_strategy_result(valid_fail)
assert checked_fail["schema_valid"] is True
assert checked_fail["overall_pass"] is False
assert not strategy_acceptance_passes(checked_fail)
malformed = {"criteria": {"role_consistency": {"status": "pass"}}, "overall_pass": True}
checked = validate_strategy_result(malformed)
assert checked["schema_valid"] is False
assert checked["overall_pass"] is False
assert not strategy_acceptance_passes(checked)
@pytest.mark.parametrize("malformed", [None, [], "not-json-object", 7])
def test_strategy_validation_fails_closed_for_non_object_provider_output(malformed):
checked = validate_strategy_result(malformed)
assert checked["schema_valid"] is False
assert checked["overall_pass"] is False
assert not strategy_acceptance_passes(checked)
def test_strategy_audit_retains_invalid_schema_and_tries_next_backend(monkeypatch):
from werewolf import strategy_audit as audit_module
criteria = {
name: {"status": "pass", "evidence": f"quoted evidence for {name}"}
for name in (
"werewolf_concealment", "seer_timing_and_evidence",
"villager_logical_reasoning", "role_consistency",
)
}
malformed = {
"criteria": dict(list(criteria.items())[:3]),
"role_consistency": criteria["role_consistency"],
}
valid = {"criteria": criteria, "overall_pass": True}
class Completion:
def __init__(self, payload, response_id):
self.payload = payload
self.response_id = response_id
def create(self, **kwargs):
return SimpleNamespace(
id=self.response_id,
model="reported-model",
usage=SimpleNamespace(model_dump=lambda: {"prompt_tokens": 10, "completion_tokens": 5}),
choices=[SimpleNamespace(message=SimpleNamespace(content=__import__("json").dumps(self.payload)))],
)
def client(payload, response_id):
return SimpleNamespace(chat=SimpleNamespace(completions=Completion(payload, response_id)))
monkeypatch.setattr(audit_module, "_backends", lambda: [
(client(malformed, "bad-schema"), "judge-a", "provider-a"),
(client(valid, "valid-schema"), "judge-b", "provider-b"),
])
judge = SimpleNamespace(
players=[SimpleNamespace(name="P1", role=Role.VILLAGER)],
action_history=[{"actor": "P1", "role": "村民", "action": "vote", "target": "P2"}],
)
result = evaluate_strategy(judge)
assert result["schema_valid"] is True
assert result["overall_pass"] is True
assert result["provider"] == "provider-b"
assert [attempt["response_id"] for attempt in result["judge_attempts"]] == [
"bad-schema", "valid-schema"
]
assert result["judge_attempts"][0]["schema_valid"] is False
# Invalid-attempt provenance must remain acyclic when attached to the
# accepted result so the retained acceptance report can be serialized.
json.dumps(result)
def test_barge_in_cancels_playback_and_transcribes_without_real_audio(monkeypatch, tmp_path):
class FakeProcess:
def __init__(self):
self.terminated = False
def poll(self):
return 0 if self.terminated else None
def terminate(self):
self.terminated = True
class FakeInputStream:
def __init__(self, **kwargs):
self.frames = iter([
np.full((1024, 1), 0.2, dtype="float32"),
np.full((1024, 1), 0.2, dtype="float32"),
np.zeros((1024, 1), dtype="float32"),
])
def __enter__(self):
return self
def __exit__(self, *args):
return False
def read(self, _block):
return next(self.frames), False
session = LiveVoiceSession.__new__(LiveVoiceSession)
session.allow_interruptions = True
session.sample_rate = 1024
session.threshold = 0.05
session.silence_seconds = 1.0
session.max_utterance = 1.0
session.player = "fake-player"
events = []
session._tts = lambda *args: tmp_path / "synthetic.mp3"
session._event = lambda kind, **data: events.append((kind, data))
session._write_wav = lambda frames, path: None
session._transcribe = lambda path, kind: "我来打断"
proc = FakeProcess()
monkeypatch.setitem(
sys.modules, "sounddevice", SimpleNamespace(InputStream=FakeInputStream)
)
monkeypatch.setattr("werewolf.human.subprocess.Popen", lambda *args, **kwargs: proc)
result = session.say("P2", "一段 AI 发言", 1, allow_barge_in=True)
assert result == "我来打断"
assert proc.terminated is True
assert events == [("barge_in", {"interrupted_speaker": "P2"})]