fischer-agentkit/tests/unit/test_auction.py

696 lines
25 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.

"""AuctionHouse 与 WealthTracker 单元测试"""
import threading
import pytest
from agentkit.marketplace.auction import AuctionHouse, AuctionResult, Bid
from agentkit.marketplace.wealth import WealthTracker
# ---- Fixtures ----
@pytest.fixture
def wealth_tracker():
return WealthTracker()
@pytest.fixture
def auction_house():
return AuctionHouse()
@pytest.fixture
def auction_house_with_tracker():
tracker = WealthTracker()
return AuctionHouse(wealth_tracker=tracker), tracker
def make_bid(
agent_name: str = "agent_a",
architecture: str = "react",
estimated_steps: int = 5,
estimated_cost: float = 10.0,
confidence: float = 0.8,
payment_offer: float = 1.0,
capabilities: list[str] | None = None,
) -> Bid:
return Bid(
agent_name=agent_name,
architecture=architecture,
estimated_steps=estimated_steps,
estimated_cost=estimated_cost,
confidence=confidence,
payment_offer=payment_offer,
capabilities=capabilities or [],
)
# ---- AuctionHouse 测试 ----
class TestAuctionHouseSingleBidder:
"""单一竞价者自动获胜"""
@pytest.mark.asyncio
async def test_single_bidder_wins(self, auction_house):
bid = make_bid(agent_name="solo_agent")
result = await auction_house.run_auction("do something", [bid])
assert result.winner is not None
assert result.winner.agent_name == "solo_agent"
assert result.total_bidders == 1
class TestAuctionHouseMultipleBidders:
"""多竞价者,最高分获胜"""
@pytest.mark.asyncio
async def test_highest_score_wins(self, auction_house):
bid_low = make_bid(
agent_name="low_agent",
confidence=0.5,
estimated_cost=10.0,
)
bid_high = make_bid(
agent_name="high_agent",
confidence=0.9,
estimated_cost=10.0,
)
result = await auction_house.run_auction("do something", [bid_low, bid_high])
assert result.winner is not None
assert result.winner.agent_name == "high_agent"
class TestAuctionHouseNoBidders:
"""无竞价者返回 None winner"""
@pytest.mark.asyncio
async def test_no_bidders_returns_none(self, auction_house):
result = await auction_house.run_auction("do something", [])
assert result.winner is None
assert result.total_bidders == 0
assert result.all_bids == []
class TestAuctionHouseWealthFactor:
"""财富因子影响评分"""
@pytest.mark.asyncio
async def test_wealth_factor_affects_scoring(self):
tracker = WealthTracker()
# Give agent_rich more wealth
tracker.reward("agent_rich", 500.0)
house = AuctionHouse(wealth_tracker=tracker)
# Same confidence and cost, but different wealth
bid_rich = make_bid(agent_name="agent_rich", confidence=0.8, estimated_cost=10.0)
bid_poor = make_bid(agent_name="agent_poor", confidence=0.8, estimated_cost=10.0)
result = await house.run_auction("do something", [bid_rich, bid_poor])
assert result.winner is not None
assert result.winner.agent_name == "agent_rich"
class TestAuctionHouseZeroCost:
"""零 estimated_cost 处理max 与 0.001"""
@pytest.mark.asyncio
async def test_zero_estimated_cost_handled(self, auction_house):
bid = make_bid(agent_name="zero_cost_agent", confidence=0.8, estimated_cost=0.0)
result = await auction_house.run_auction("do something", [bid])
assert result.winner is not None
assert result.winner.agent_name == "zero_cost_agent"
def test_score_bid_zero_cost(self, auction_house):
bid = make_bid(agent_name="zero_cost_agent", confidence=0.8, estimated_cost=0.0)
score = auction_house.score_bid(bid)
# score = (0.8 / max(0.0, 0.001)) * 1.1 = (0.8 / 0.001) * 1.1 = 880.0
expected = (0.8 / 0.001) * 1.1
assert abs(score - expected) < 0.01
class TestBidScoringFormula:
"""竞价评分公式验证"""
def test_score_formula(self):
tracker = WealthTracker()
# Default wealth = 100, so wealth_factor = 1.0 + (100 / 1000.0) = 1.1
house = AuctionHouse(wealth_tracker=tracker)
bid = make_bid(agent_name="test_agent", confidence=0.9, estimated_cost=5.0)
score = house.score_bid(bid)
wealth_factor = 1.0 + (100.0 / 1000.0) # 1.1
expected = (0.9 / 5.0) * wealth_factor
assert abs(score - expected) < 0.0001
def test_score_formula_with_custom_wealth(self):
tracker = WealthTracker(initial_wealth=200.0)
tracker.reward("rich_agent", 300.0)
# wealth = 500, factor = 1.0 + 500/1000 = 1.5
house = AuctionHouse(wealth_tracker=tracker)
bid = make_bid(agent_name="rich_agent", confidence=0.6, estimated_cost=3.0)
score = house.score_bid(bid)
wealth_factor = 1.0 + (500.0 / 1000.0) # 1.5
expected = (0.6 / 3.0) * wealth_factor
assert abs(score - expected) < 0.0001
# ---- WealthTracker 测试 ----
class TestWealthTrackerInitialWealth:
"""初始财富默认值"""
def test_default_initial_wealth(self):
tracker = WealthTracker()
assert tracker.get_wealth("unknown_agent") == 100.0
def test_custom_initial_wealth(self):
tracker = WealthTracker(initial_wealth=50.0)
assert tracker.get_wealth("unknown_agent") == 50.0
class TestWealthTrackerReward:
"""奖励增加财富"""
def test_reward_increases_wealth(self, wealth_tracker):
wealth_tracker.reward("agent_a", 50.0)
assert wealth_tracker.get_wealth("agent_a") == 150.0
def test_reward_multiple_times(self, wealth_tracker):
wealth_tracker.reward("agent_a", 30.0)
wealth_tracker.reward("agent_a", 20.0)
assert wealth_tracker.get_wealth("agent_a") == 150.0
class TestWealthTrackerPenalize:
"""惩罚减少财富"""
def test_penalize_decreases_wealth(self, wealth_tracker):
wealth_tracker.penalize("agent_a", 30.0)
assert wealth_tracker.get_wealth("agent_a") == 70.0
def test_penalize_below_zero(self, wealth_tracker):
wealth_tracker.penalize("agent_a", 150.0)
assert wealth_tracker.get_wealth("agent_a") == -50.0
class TestWealthTrackerBankrupt:
"""破产检查wealth <= -100"""
def test_bankrupt_at_negative_100(self, wealth_tracker):
wealth_tracker.penalize("agent_a", 200.0)
assert wealth_tracker.get_wealth("agent_a") == -100.0
assert wealth_tracker.is_bankrupt("agent_a") is True
def test_bankrupt_below_negative_100(self, wealth_tracker):
wealth_tracker.penalize("agent_a", 250.0)
assert wealth_tracker.is_bankrupt("agent_a") is True
def test_not_bankrupt_above_negative_100(self, wealth_tracker):
wealth_tracker.penalize("agent_a", 150.0)
# wealth = -50, which is > -100
assert wealth_tracker.is_bankrupt("agent_a") is False
def test_not_bankrupt_at_default(self, wealth_tracker):
assert wealth_tracker.is_bankrupt("agent_a") is False
class TestWealthTrackerReset:
"""重置恢复初始财富"""
def test_reset_restores_initial_wealth(self, wealth_tracker):
wealth_tracker.reward("agent_a", 500.0)
wealth_tracker.reset("agent_a")
assert wealth_tracker.get_wealth("agent_a") == 100.0
def test_reset_with_custom_initial(self):
tracker = WealthTracker(initial_wealth=200.0)
tracker.penalize("agent_a", 50.0)
tracker.reset("agent_a")
assert tracker.get_wealth("agent_a") == 200.0
class TestWealthTrackerRankings:
"""排名按财富降序"""
def test_rankings_sorted_descending(self, wealth_tracker):
wealth_tracker.reward("agent_a", 100.0) # 200
wealth_tracker.reward("agent_b", 300.0) # 400
wealth_tracker.penalize("agent_c", 50.0) # 50
rankings = wealth_tracker.get_rankings()
assert rankings[0][0] == "agent_b"
assert rankings[1][0] == "agent_a"
assert rankings[2][0] == "agent_c"
def test_rankings_empty(self, wealth_tracker):
assert wealth_tracker.get_rankings() == []
class TestWealthTrackerWealthFactor:
"""财富因子计算"""
def test_wealth_factor_default(self, wealth_tracker):
# wealth = 100, factor = 1.0 + 100/1000 = 1.1
factor = wealth_tracker.get_wealth_factor("agent_a")
assert abs(factor - 1.1) < 0.0001
def test_wealth_factor_with_wealth(self, wealth_tracker):
wealth_tracker.reward("agent_a", 400.0) # wealth = 500
factor = wealth_tracker.get_wealth_factor("agent_a")
# factor = 1.0 + 500/1000 = 1.5
assert abs(factor - 1.5) < 0.0001
def test_wealth_factor_negative_wealth(self, wealth_tracker):
wealth_tracker.penalize("agent_a", 150.0) # wealth = -50
factor = wealth_tracker.get_wealth_factor("agent_a")
# factor = 1.0 + (-50)/1000 = 0.95
assert abs(factor - 0.95) < 0.0001
# ---- Auction 默认禁用验证 ----
class TestAuctionDefaultDisabled:
"""拍卖机制默认禁用"""
def test_auction_not_in_default_config(self):
"""验证默认配置中不包含 auction_enabled"""
from agentkit.server.config import ServerConfig
config = ServerConfig()
# marketplace section should not exist or auction_enabled should be False
marketplace_cfg = getattr(config, "marketplace", None)
if marketplace_cfg is not None:
auction_enabled = getattr(marketplace_cfg, "auction_enabled", False)
assert auction_enabled is False
# If marketplace doesn't exist at all, auction is implicitly disabled
# ---- Vickrey Auction 测试 ----
class TestVickreySingleBidder:
"""Vickrey 拍卖单一竞价者获胜支付自身成本利润为0"""
@pytest.mark.asyncio
async def test_single_bidder_wins_pays_own_cost(self):
tracker = WealthTracker()
house = AuctionHouse(wealth_tracker=tracker)
bid = make_bid(agent_name="solo_agent", estimated_cost=5.0)
result = await house.run_vickrey_auction("task", [bid])
assert result.winner is not None
assert result.winner.agent_name == "solo_agent"
assert result.total_bidders == 1
# Winner pays own cost, so profit = 0 → wealth unchanged
assert tracker.get_wealth("solo_agent") == 100.0
@pytest.mark.asyncio
async def test_single_bidder_selection_reason(self):
house = AuctionHouse()
bid = make_bid(agent_name="solo_agent", estimated_cost=10.0)
result = await house.run_vickrey_auction("task", [bid])
assert "sole eligible bidder" in result.selection_reason
class TestVickreyTwoBidders:
"""Vickrey 拍卖:两个竞价者,最低价赢,支付第二低价"""
@pytest.mark.asyncio
async def test_lowest_cost_wins_pays_second(self):
tracker = WealthTracker()
house = AuctionHouse(wealth_tracker=tracker)
bid_cheap = make_bid(agent_name="cheap_agent", estimated_cost=5.0)
bid_expensive = make_bid(agent_name="expensive_agent", estimated_cost=10.0)
result = await house.run_vickrey_auction("task", [bid_cheap, bid_expensive])
assert result.winner is not None
assert result.winner.agent_name == "cheap_agent"
# Winner pays second-lowest = 10.0, profit = 10.0 - 5.0 = 5.0
assert tracker.get_wealth("cheap_agent") == 100.0 + 5.0
@pytest.mark.asyncio
async def test_second_price_not_own_price(self):
tracker = WealthTracker()
house = AuctionHouse(wealth_tracker=tracker)
bid_a = make_bid(agent_name="agent_a", estimated_cost=3.0)
bid_b = make_bid(agent_name="agent_b", estimated_cost=7.0)
result = await house.run_vickrey_auction("task", [bid_a, bid_b])
# agent_a wins, pays 7.0 (not 3.0), profit = 7.0 - 3.0 = 4.0
assert tracker.get_wealth("agent_a") == 100.0 + 4.0
# Loser pays nothing
assert tracker.get_wealth("agent_b") == 100.0
@pytest.mark.asyncio
async def test_selection_reason_contains_second_price(self):
house = AuctionHouse()
bid_a = make_bid(agent_name="agent_a", estimated_cost=3.0)
bid_b = make_bid(agent_name="agent_b", estimated_cost=7.0)
result = await house.run_vickrey_auction("task", [bid_a, bid_b])
assert "7.0" in result.selection_reason
assert "agent_b" in result.selection_reason
class TestVickreyThreeBidders:
"""Vickrey 拍卖:三个竞价者,最低价赢,支付第二低价"""
@pytest.mark.asyncio
async def test_lowest_wins_pays_second_lowest(self):
tracker = WealthTracker()
house = AuctionHouse(wealth_tracker=tracker)
bid_a = make_bid(agent_name="agent_a", estimated_cost=3.0)
bid_b = make_bid(agent_name="agent_b", estimated_cost=7.0)
bid_c = make_bid(agent_name="agent_c", estimated_cost=12.0)
result = await house.run_vickrey_auction("task", [bid_a, bid_b, bid_c])
assert result.winner is not None
assert result.winner.agent_name == "agent_a"
# Winner pays second-lowest = 7.0, profit = 7.0 - 3.0 = 4.0
assert tracker.get_wealth("agent_a") == 100.0 + 4.0
# Third bidder's cost doesn't affect payment
assert result.total_bidders == 3
@pytest.mark.asyncio
async def test_middle_bidder_wins_when_cheapest_bankrupt(self):
tracker = WealthTracker()
# Make agent_a bankrupt
tracker.penalize("agent_a", 300.0)
assert tracker.is_bankrupt("agent_a") is True
house = AuctionHouse(wealth_tracker=tracker)
bid_a = make_bid(agent_name="agent_a", estimated_cost=3.0)
bid_b = make_bid(agent_name="agent_b", estimated_cost=7.0)
bid_c = make_bid(agent_name="agent_c", estimated_cost=12.0)
result = await house.run_vickrey_auction("task", [bid_a, bid_b, bid_c])
# agent_a is bankrupt, so agent_b wins, pays 12.0
assert result.winner is not None
assert result.winner.agent_name == "agent_b"
# profit = 12.0 - 7.0 = 5.0
assert tracker.get_wealth("agent_b") == 100.0 + 5.0
class TestVickreyCapabilityFiltering:
"""Vickrey 拍卖:能力过滤"""
def test_filter_by_capabilities_basic(self):
house = AuctionHouse()
bids = [
make_bid(agent_name="a", capabilities=["search", "analysis"]),
make_bid(agent_name="b", capabilities=["search"]),
make_bid(agent_name="c", capabilities=["analysis", "coding"]),
]
filtered = house.filter_by_capabilities(bids, ["search"])
assert len(filtered) == 2
assert {b.agent_name for b in filtered} == {"a", "b"}
def test_filter_by_capabilities_requires_all(self):
house = AuctionHouse()
bids = [
make_bid(agent_name="a", capabilities=["search", "analysis"]),
make_bid(agent_name="b", capabilities=["search"]),
make_bid(agent_name="c", capabilities=["analysis", "coding"]),
]
filtered = house.filter_by_capabilities(bids, ["search", "analysis"])
assert len(filtered) == 1
assert filtered[0].agent_name == "a"
def test_filter_by_capabilities_case_insensitive(self):
house = AuctionHouse()
bids = [
make_bid(agent_name="a", capabilities=["Search", "Analysis"]),
]
filtered = house.filter_by_capabilities(bids, ["search", "analysis"])
assert len(filtered) == 1
def test_filter_by_capabilities_no_match(self):
house = AuctionHouse()
bids = [
make_bid(agent_name="a", capabilities=["search"]),
]
filtered = house.filter_by_capabilities(bids, ["coding"])
assert len(filtered) == 0
def test_filter_by_capabilities_empty_requirements(self):
house = AuctionHouse()
bids = [
make_bid(agent_name="a", capabilities=["search"]),
]
filtered = house.filter_by_capabilities(bids, [])
assert len(filtered) == 1
@pytest.mark.asyncio
async def test_vickrey_with_capability_filtering(self):
tracker = WealthTracker()
house = AuctionHouse(wealth_tracker=tracker)
bids = [
make_bid(agent_name="a", estimated_cost=5.0, capabilities=["search", "analysis"]),
make_bid(agent_name="b", estimated_cost=3.0, capabilities=["search"]),
make_bid(agent_name="c", estimated_cost=8.0, capabilities=["search", "analysis"]),
]
# Require both "search" and "analysis" → only a and c eligible
result = await house.run_vickrey_auction("task", bids, required_capabilities=["search", "analysis"])
assert result.winner is not None
assert result.winner.agent_name == "a"
# a wins (cost=5.0), pays second-lowest among eligible = c's cost = 8.0
assert tracker.get_wealth("a") == 100.0 + (8.0 - 5.0)
class TestVickreyBankruptAgent:
"""Vickrey 拍卖:破产 Agent 被排除"""
@pytest.mark.asyncio
async def test_bankrupt_agent_excluded(self):
tracker = WealthTracker()
tracker.penalize("bankrupt_agent", 300.0) # wealth = -200, bankrupt
house = AuctionHouse(wealth_tracker=tracker)
bid_bankrupt = make_bid(agent_name="bankrupt_agent", estimated_cost=1.0)
bid_ok = make_bid(agent_name="ok_agent", estimated_cost=10.0)
result = await house.run_vickrey_auction("task", [bid_bankrupt, bid_ok])
assert result.winner is not None
assert result.winner.agent_name == "ok_agent"
# Only 1 eligible bidder → pays own cost, profit = 0
assert tracker.get_wealth("ok_agent") == 100.0
@pytest.mark.asyncio
async def test_all_bankrupt_returns_none(self):
tracker = WealthTracker()
tracker.penalize("a", 300.0)
tracker.penalize("b", 300.0)
house = AuctionHouse(wealth_tracker=tracker)
bids = [
make_bid(agent_name="a", estimated_cost=1.0),
make_bid(agent_name="b", estimated_cost=2.0),
]
result = await house.run_vickrey_auction("task", bids)
assert result.winner is None
assert "bankrupt" in result.selection_reason.lower() or "eligible" in result.selection_reason.lower()
class TestVickreyNoBidders:
"""Vickrey 拍卖:无竞价者"""
@pytest.mark.asyncio
async def test_no_bidders_returns_none(self):
house = AuctionHouse()
result = await house.run_vickrey_auction("task", [])
assert result.winner is None
assert result.total_bidders == 0
assert result.all_bids == []
@pytest.mark.asyncio
async def test_no_eligible_after_capability_filter(self):
house = AuctionHouse()
bid = make_bid(agent_name="a", estimated_cost=5.0, capabilities=["search"])
result = await house.run_vickrey_auction("task", [bid], required_capabilities=["coding"])
assert result.winner is None
assert result.total_bidders == 1
class TestVickreyWealthTrackerUpdate:
"""Vickrey 拍卖WealthTracker 正确更新"""
@pytest.mark.asyncio
async def test_winner_earns_payment_minus_cost(self):
tracker = WealthTracker(initial_wealth=50.0)
house = AuctionHouse(wealth_tracker=tracker)
bid_a = make_bid(agent_name="a", estimated_cost=4.0)
bid_b = make_bid(agent_name="b", estimated_cost=9.0)
await house.run_vickrey_auction("task", [bid_a, bid_b])
# a wins, pays 9.0, profit = 9.0 - 4.0 = 5.0
assert tracker.get_wealth("a") == 50.0 + 5.0
# b pays nothing
assert tracker.get_wealth("b") == 50.0
@pytest.mark.asyncio
async def test_single_bidder_zero_profit(self):
tracker = WealthTracker(initial_wealth=100.0)
house = AuctionHouse(wealth_tracker=tracker)
# Single bidder: pays own cost, profit = 0
bid = make_bid(agent_name="a", estimated_cost=10.0)
await house.run_vickrey_auction("task", [bid])
assert tracker.get_wealth("a") == 100.0
class TestVickreyBackwardCompat:
"""Vickrey 拍卖:原有 score_bid 方法仍然可用"""
def test_score_bid_still_works(self):
tracker = WealthTracker()
house = AuctionHouse(wealth_tracker=tracker)
bid = make_bid(agent_name="test_agent", confidence=0.9, estimated_cost=5.0)
score = house.score_bid(bid)
wealth_factor = 1.0 + (100.0 / 1000.0)
expected = (0.9 / 5.0) * wealth_factor
assert abs(score - expected) < 0.0001
@pytest.mark.asyncio
async def test_run_auction_still_works(self):
house = AuctionHouse()
bid_low = make_bid(agent_name="low_agent", confidence=0.5, estimated_cost=10.0)
bid_high = make_bid(agent_name="high_agent", confidence=0.9, estimated_cost=10.0)
result = await house.run_auction("do something", [bid_low, bid_high])
assert result.winner is not None
assert result.winner.agent_name == "high_agent"
# ---- Bid Validation 测试 ----
class TestBidValidation:
"""Bid __post_init__ 验证"""
def test_negative_estimated_cost_raises(self):
with pytest.raises(ValueError, match="estimated_cost must be non-negative"):
Bid(
agent_name="a",
architecture="react",
estimated_steps=5,
estimated_cost=-1.0,
confidence=0.8,
payment_offer=1.0,
capabilities=[],
)
def test_confidence_above_one_raises(self):
with pytest.raises(ValueError, match="confidence must be between"):
Bid(
agent_name="a",
architecture="react",
estimated_steps=5,
estimated_cost=10.0,
confidence=1.5,
payment_offer=1.0,
capabilities=[],
)
def test_confidence_below_zero_raises(self):
with pytest.raises(ValueError, match="confidence must be between"):
Bid(
agent_name="a",
architecture="react",
estimated_steps=5,
estimated_cost=10.0,
confidence=-0.1,
payment_offer=1.0,
capabilities=[],
)
def test_negative_payment_offer_raises(self):
with pytest.raises(ValueError, match="payment_offer must be non-negative"):
Bid(
agent_name="a",
architecture="react",
estimated_steps=5,
estimated_cost=10.0,
confidence=0.8,
payment_offer=-5.0,
capabilities=[],
)
def test_zero_values_allowed(self):
bid = Bid(
agent_name="a",
architecture="react",
estimated_steps=5,
estimated_cost=0.0,
confidence=0.0,
payment_offer=0.0,
capabilities=[],
)
assert bid.estimated_cost == 0.0
assert bid.confidence == 0.0
assert bid.payment_offer == 0.0
def test_boundary_confidence_one_allowed(self):
bid = Bid(
agent_name="a",
architecture="react",
estimated_steps=5,
estimated_cost=10.0,
confidence=1.0,
payment_offer=1.0,
capabilities=[],
)
assert bid.confidence == 1.0
# ---- WealthTracker Thread Safety 测试 ----
class TestWealthTrackerThreadSafety:
"""WealthTracker 线程安全"""
def test_concurrent_reward_penalize(self):
tracker = WealthTracker(initial_wealth=1000.0)
errors: list[Exception] = []
def worker(action: str, name: str, amount: float, count: int):
try:
for _ in range(count):
if action == "reward":
tracker.reward(name, amount)
else:
tracker.penalize(name, amount)
except Exception as e:
errors.append(e)
threads = [
threading.Thread(target=worker, args=("reward", "agent_a", 1.0, 100)),
threading.Thread(target=worker, args=("penalize", "agent_a", 1.0, 100)),
threading.Thread(target=worker, args=("reward", "agent_b", 2.0, 50)),
]
for t in threads:
t.start()
for t in threads:
t.join()
assert not errors
# agent_a: 1000 + 100*1.0 - 100*1.0 = 1000
assert tracker.get_wealth("agent_a") == 1000.0
# agent_b: 1000 + 50*2.0 = 1100
assert tracker.get_wealth("agent_b") == 1100.0
# ---- Wealth Factor Lower Bound 测试 ----
class TestWealthFactorLowerBound:
"""get_wealth_factor 下限保护"""
def test_extremely_negative_wealth_clamped(self):
tracker = WealthTracker(initial_wealth=100.0)
tracker.penalize("agent_a", 5000.0)
# wealth = 100 - 5000 = -4900, factor would be 1.0 + (-4900/1000) = -3.9
# But with lower bound: max(0.01, -3.9) = 0.01
factor = tracker.get_wealth_factor("agent_a")
assert factor == 0.01
def test_slightly_negative_wealth_not_clamped(self):
tracker = WealthTracker(initial_wealth=100.0)
tracker.penalize("agent_a", 150.0)
# wealth = -50, factor = 1.0 + (-50/1000) = 0.95
factor = tracker.get_wealth_factor("agent_a")
assert abs(factor - 0.95) < 0.0001