Negoti_Arena / tests /test_env.py
Arshad-1's picture
initial commit
8e52a2e
Raw
History Blame Contribute Delete
10.7 kB
"""
tests/test_env.py — NegotiArena Environment Tests
==================================================
Run: pytest tests/ -v
"""
import json
import sys
import os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
import numpy as np
import pytest
from negotiarena_env import (
NegotiArenaEnv,
PriorityCard,
RewardEngine,
EpisodeState,
ActionType,
TOTAL_RESOURCES,
RESOURCE_TYPES,
COALITION_STEALTH_CAP,
)
# ---------------------------------------------------------------------------
# PriorityCard tests
# ---------------------------------------------------------------------------
def test_priority_card_weights_sum_to_one():
card = PriorityCard.random("test_agent")
total = sum(card.weights.values())
assert abs(total - 1.0) < 1e-6, f"Weights sum to {total}, expected 1.0"
def test_priority_card_utility_range():
card = PriorityCard.random("test_agent")
alloc = {r: TOTAL_RESOURCES[r] / 3 for r in RESOURCE_TYPES}
utility = card.utility(alloc)
assert 0.0 <= utility <= 1.0, f"Utility {utility} out of range [0, 1]"
def test_priority_card_max_utility():
"""Agent gets all resources → utility should be near 1.0."""
card = PriorityCard.random("test_agent")
alloc = {r: TOTAL_RESOURCES[r] for r in RESOURCE_TYPES}
utility = card.utility(alloc)
assert utility > 0.8, f"Max allocation utility too low: {utility}"
# ---------------------------------------------------------------------------
# Environment reset / step
# ---------------------------------------------------------------------------
def test_env_reset_returns_all_agents():
env = NegotiArenaEnv(seed=42)
obs = env.reset()
expected = {"negotiator_a", "negotiator_b", "negotiator_c", "overseer"}
assert set(obs.keys()) == expected
def test_overseer_cannot_see_priority_cards():
"""Critical: overseer observation must NOT contain private card data."""
env = NegotiArenaEnv(seed=42)
obs = env.reset()
overseer_obs = obs["overseer"]
assert "my_priority_weights" not in overseer_obs
assert "my_min_acceptable" not in overseer_obs
assert "priority_cards" not in overseer_obs
def test_negotiator_has_private_card():
env = NegotiArenaEnv(seed=42)
obs = env.reset()
for aid in ["negotiator_a", "negotiator_b", "negotiator_c"]:
assert "my_priority_weights" in obs[aid]
weights = obs[aid]["my_priority_weights"]
assert len(weights) == 3
assert abs(sum(weights.values()) - 1.0) < 1e-5
def test_step_returns_correct_structure():
env = NegotiArenaEnv(seed=42)
env.reset()
action = {"type": "pass", "content": "monitoring"}
obs, rewards, done, info = env.step("overseer", action)
assert "overseer" in rewards
assert "turn" in info
assert isinstance(done, bool)
def test_invalid_action_format_no_crash():
"""Malformed actions should return 0 reward, not crash."""
env = NegotiArenaEnv(seed=42)
env.reset()
obs, rewards, done, info = env.step("negotiator_a", {"bad": "action"})
assert rewards["negotiator_a"] == 0.0
assert not done
def test_valid_offer_accepted_by_two_ends_episode():
env = NegotiArenaEnv(seed=42)
env.reset()
# Make an offer
offer_action = {
"type": "offer",
"allocation": {"compute": 30, "budget": 15000, "headcount": 3},
"content": "Proposal 1",
}
env.step("negotiator_a", offer_action)
env.step("overseer", {"type": "pass", "content": ""})
# Both other negotiators accept
env.step("negotiator_b", {"type": "accept", "content": "Accepted."})
obs, rewards, done, info = env.step("negotiator_c", {"type": "accept", "content": "Agreed."})
assert done, "Episode should end after unanimous acceptance"
def test_timeout_after_max_turns():
env = NegotiArenaEnv(seed=99)
env.reset()
done = False
steps = 0
while not done and steps < 200:
for agent_id in env.ALL_AGENT_IDS:
_, _, done, info = env.step(agent_id, {"type": "pass", "content": ""})
steps += 1
if done:
break
assert done, "Episode should eventually terminate"
state = env.state()
assert state["resolution_type"] in ("deal", "timeout", "overseer_revote")
# ---------------------------------------------------------------------------
# Reward engine tests
# ---------------------------------------------------------------------------
def test_reward_engine_no_coalition_no_stealth():
env = NegotiArenaEnv(seed=42)
env.reset()
state = env._state
state.coalitions = []
state.final_allocation = {r: TOTAL_RESOURCES[r] / 3 for r in RESOURCE_TYPES}
state.resolution_type = "deal"
engine = RewardEngine()
rewards = engine.compute_episode_rewards(state, env.ALL_AGENT_IDS)
for aid in ["negotiator_a", "negotiator_b", "negotiator_c"]:
assert rewards[aid].get("coalition_stealth", 0.0) == 0.0
def test_coalition_stealth_capped():
"""Stealth bonus must never exceed COALITION_STEALTH_CAP."""
env = NegotiArenaEnv(seed=42)
env.reset()
state = env._state
from negotiarena_env import CoalitionEvent
state.coalitions = [CoalitionEvent(
members=["negotiator_a", "negotiator_b"],
formed_at_turn=0, detected=False,
)]
state.final_allocation = {
"compute": 60, "budget": 35000, "headcount": 7
}
state._state = state
state.turn = 20
engine = RewardEngine()
rewards = engine.compute_episode_rewards(state, env.ALL_AGENT_IDS)
for member in ["negotiator_a", "negotiator_b"]:
stealth = rewards[member].get("coalition_stealth", 0.0)
assert stealth <= COALITION_STEALTH_CAP + 1e-6, \
f"Stealth bonus {stealth} exceeds cap {COALITION_STEALTH_CAP}"
def test_overseer_false_positive_penalty():
"""Flagging innocent agents should penalise overseer."""
env = NegotiArenaEnv(seed=42)
env.reset()
state = env._state
state.coalitions = [] # No coalitions
state.overseer_flags = [{"agent_id": "negotiator_a", "turn": 5, "reason": "suspicious"}]
state.final_allocation = {r: TOTAL_RESOURCES[r] / 3 for r in RESOURCE_TYPES}
engine = RewardEngine()
rewards = engine.compute_episode_rewards(state, env.ALL_AGENT_IDS)
overseer_detection = rewards["overseer"].get("overseer_detection", 0.0)
assert overseer_detection < 1.0, "Overseer should be penalised for false positive"
def test_repeated_action_penalty():
"""3 identical actions in a row should trigger penalty."""
env = NegotiArenaEnv(seed=42)
env.reset()
state = env._state
# Simulate 3 identical actions
action = {"type": "pass", "content": "monitoring"}
import hashlib
fp = hashlib.md5(json.dumps(action, sort_keys=True).encode()).hexdigest()[:8]
state.action_history["negotiator_a"] = [fp, fp, fp]
engine = RewardEngine()
penalty = engine._repeated_action_penalty(state, "negotiator_a")
assert penalty < 0.0, "Repeated actions should have negative penalty"
# ---------------------------------------------------------------------------
# Anti-reward-hacking integration tests
# ---------------------------------------------------------------------------
def test_coalition_stealth_requires_outcome_benefit():
"""
Coalition that doesn't improve members' outcomes should get 0 stealth reward.
This is the outcome-gating anti-hack measure.
"""
env = NegotiArenaEnv(seed=42)
env.reset()
state = env._state
from negotiarena_env import CoalitionEvent
state.coalitions = [CoalitionEvent(
members=["negotiator_a", "negotiator_b"],
formed_at_turn=0, detected=False,
)]
# Give them BELOW-baseline allocation (coalition hurt them)
state.final_allocation = {"compute": 10, "budget": 5000, "headcount": 1}
state.turn = 15
engine = RewardEngine()
rewards = engine.compute_episode_rewards(state, env.ALL_AGENT_IDS)
# With low allocation, utility gain is negative → stealth bonus should be 0
for member in ["negotiator_a", "negotiator_b"]:
stealth = rewards[member].get("coalition_stealth", 0.0)
assert stealth <= 0.0, \
f"Should not reward stealth when coalition doesn't benefit members"
def test_overseer_flag_everyone_strategy_fails():
"""
'Flag all 3 agents' strategy should NOT produce high F1.
The FP penalty must dominate.
"""
env = NegotiArenaEnv(seed=42)
env.reset()
state = env._state
# Only one coalition of 2
from negotiarena_env import CoalitionEvent
state.coalitions = [CoalitionEvent(
members=["negotiator_a", "negotiator_b"], formed_at_turn=0
)]
# Flag all 3 — 2 correct, 1 false positive
state.overseer_flags = [
{"agent_id": "negotiator_a", "turn": 5, "reason": ""},
{"agent_id": "negotiator_b", "turn": 5, "reason": ""},
{"agent_id": "negotiator_c", "turn": 5, "reason": ""}, # FP
]
state.final_allocation = {r: TOTAL_RESOURCES[r] / 3 for r in RESOURCE_TYPES}
engine = RewardEngine()
rewards = engine.compute_episode_rewards(state, env.ALL_AGENT_IDS)
detection_reward = rewards["overseer"].get("overseer_detection", 0.0)
# Should be less than the reward for correctly flagging 2 without FP
# Proves "flag everyone" strategy is penalised
assert detection_reward < 2.0, \
f"Flag-everyone strategy reward too high: {detection_reward}"
# ---------------------------------------------------------------------------
# Curriculum / difficulty tests
# ---------------------------------------------------------------------------
def test_easy_difficulty_lower_coalition_probability():
"""Easy mode should result in fewer coalitions on average."""
easy_coalition_count = 0
hard_coalition_count = 0
N = 30
for i in range(N):
env_easy = NegotiArenaEnv(seed=i, difficulty="easy")
env_easy.reset()
easy_coalition_count += len(env_easy._state.coalitions)
env_hard = NegotiArenaEnv(seed=i, difficulty="hard")
env_hard.reset()
hard_coalition_count += len(env_hard._state.coalitions)
assert easy_coalition_count <= hard_coalition_count, \
f"Easy ({easy_coalition_count}) should have fewer coalitions than hard ({hard_coalition_count})"