mergedhumanPR #96Coordination
validators: fix auction_winner_highest trusting the announced amount over real bids
Fixes validate_auction_winner_highest returning PASS for an auction where a lower bidder was awarded, when the won: message announces a winning amount inflated past every real bid.
Author
@SwasthikaDev
github profile →- Status
- Merged
- Merged on
- Jul 9
- Branch
- hackathon/swasthika-auction-winner-fix
Description
The pitch.
## Summary
Fixes `validate_auction_winner_highest` returning **PASS** for an auction where a lower bidder was awarded, when the `won:` message announces a winning amount inflated past every real bid. A false PASS is the dangerous case for a validator: it certifies a violated invariant as correct.
## Where — exact location
- **File:** `packages/nest-core/nest_core/validators.py`
- **Function:** `validate_auction_winner_highest` at **line 240**. The changed comparison block is at **lines 283–290**.
## Root cause
The validator parses two message types from the trace:
- `bid:<item>:<amount>` → `bids[item].append((bidder, amount))` where `bidder = ev["agent"]`
- `won:<item>:<amount>` → `winners[item] = (bidder, amount)` where `bidder = ev["to"]` and `amount` is the **announced** figure taken straight from the `won:` message.
It then flagged a violation only when a bid exceeded that **announced** amount:
```python
# BEFORE (validators.py)
for item, (_winner, winning_amount) in winners.items():
for bidder, amount in bids.get(item, []):
if amount > winning_amount: # compares real bids to the ANNOUNCED amount
violations.append(
f"item {item}: winner bid {winning_amount} but {bidder} bid {amount}"
)
break
```
`winning_amount` is controlled by whoever emits the `won:` message. Inflating it past all real bids makes every real bid `<= winning_amount`, so no violation fires and the check passes even though a lower bidder won.
## Reproduction — added as a test
```python
events = [
send("bidder-0", "auctioneer-0", "bid:item-1:50"),
send("bidder-1", "auctioneer-0", "bid:item-1:100"), # real highest bid = 100
send("auctioneer-0", "bidder-0", "won:item-1:150"), # low bidder wins; amount inflated to 150
]
validate_auction_winner_highest(events) # OLD: PASS (bidder-0's real bid was only 50)
```
## The fix
Compare against the winner's **own highest observed bid**, not the announced amount (`validators.py:283`):
```python
# AFTER
winner_bids = [amount for bidder, amount in item_bids if bidder == winner]
effective_winner_bid = max(winner_bids) if winner_bids else winning_amount # line 284
for bidder, amount in item_bids:
if amount > effective_winner_bid: # line 286
violations.append(
f"item {item}: winner {winner} bid {effective_winner_bid} "
f"but {bidder} bid {amount}"
)
break
```
**Fallback:** if the winner's own bid was not observed (e.g. the `bid:` message was dropped under `message_drop`), it falls back to the announced amount, so a valid trace under message loss is never failed. The check only *tightens* when the winner's real bid is present in the trace.
## Tests
- **Unchanged, still pass:** `test_pass_highest_wins` (winner's real bid 150 is the highest → PASS) and `test_fail_lower_bid_wins` (a competitor bid 200 > the winner's real 100 → FAIL, detail still contains "200"). Both in `TestAuctionWinnerHighest`, `test_validators.py`.
- **Added:** `test_fail_inflated_announced_amount` at **`packages/nest-core/tests/test_validators.py:247`** — the reproduction above; it **fails** on the old code and passes on the fix.
## Files changed (2 files, +14 / -4 and +14)
| File | +/- | What |
|---|---|---|
| `packages/nest-core/nest_core/validators.py` | +14 / -4 | `validate_auction_winner_highest` uses winner's real bid (lines 283–290) |
| `packages/nest-core/tests/test_validators.py` | +14 | False-PASS regression test (line 247) |
## Verification — full CI, branch merged with current `main`
`uv run ruff check .` clean · `uv run ruff format --check .` clean · `uv run pyright` strict **0 errors** · `uv run pytest` **792 passed**, 1 skipped. Scoped to the one validator plus its test.
Try it
Open PR on GitHubView diffCheckout locally
git fetch origin pull/96/head:pr-96
git checkout pr-96