"""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