---
title: Stabilize the flaky trade-post spec that intermittently fails master CI
type: task
status: completed
description: >-
  Root-caused to the league fixture DST roster cap (mdst=3) colliding with the random draft fixture,
  at 0.67% of runs; fixed in league 14ade5fef along with a second timestamp-race flake in the same
  file.
base_uri: user:task/league/stabilize-flaky-trade-post-spec.md
created_at: '2026-08-02T02:14:49.209Z'
entity_id: 3c480cdb-628f-4067-b356-88ccb1a7ddb6
finished_at: '2026-08-03T16:26:23.328Z'
owner_identity_uri: █████████████████████████
priority: Medium
public_read: true
relations:
  - relates [[user:repository/active/league/ABOUT.md]]
tags:
  - user:tag/league-xo-football.md
updated_at: '2026-08-03T16:26:23.749Z'
visibility_analyzed_at: '2026-11-05T23:50:29.013Z'
---

## Context

The `Test` workflow on `mistakia/league` failed on run [30720947242](https://github.com/mistakia/league/actions/runs/30720947242) (commit `b8ef15b75`, "rfa: state the rotation directly in its comments") with:

```
API /trades post one-for-one player trade
AssertionError: expected Response to have status code 200 but got 400
  at test/trade.spec.mjs:128:29
2500 passing, 1 pending, 1 failing
```

Attempt 2 of that same run passed on the same commit with zero code change (2501 passing), so the run's recorded conclusion is now `success`. The commit under test had nothing to do with trades.

Surfaced during the 2026-08-01 CI-failure triage of nine `gh_action_failure` signals against this repo (signal #123968). The other eight were two separate real causes and a live-vendor timeout family; this one is the only remaining unexplained flake.

A 400 from this route means a validation branch rejected the payload, so the likely shapes are order-dependent fixture state (a prior spec leaving a roster, trade, or transaction row behind) or a clock-derived deadline check that only rejects inside part of the day. Both fit "passes on re-run".

## Outcome (2026-08-03) — resolved

**Root cause: the league fixture's DST roster cap, not fixture bleed and not the clock.** The spec picked an arbitrary non-K player from each of teams 1 and 2. `db/fixtures/draft.mjs` fills rosters with `ORDER BY RANDOM()`, and `db/fixtures/league.mjs` leaves `mqb`/`mrb`/`mwr`/`mte` at 0 (unlimited) but `mdst` and `mk` at 3. So an arbitrary pair could hand the receiving team a fourth DST, and `has_bench_space_for_position` → `has_position_capacity` rejected the ACCEPT with 400 `No active roster space available for this position`.

The propose call passes because the accepting team's slot validation runs at accept time (`api/routes/leagues/trade.mjs`, `validate_trade_roster_slots`), which is why the failure landed on line 128 rather than line 96.

Rate measured directly against the fixture: **10 rejections in 1500 draws (0.67%)**, every one of them DST, split 6 on the accepting side and 4 on the proposing side. No other rejection reason appeared in 1500 trials.

Fixed in `mistakia/league` commit `14ade5fef`. Both one-for-one specs now share `select_tradeable_pair`, which matches the two positions and additionally requires `rosters_players.pos` to agree with `player.primary_position` — the fixture writes `pos` from `secondary_position` while the validator reads `primary_position`, so matching on `pos` alone left a residual mismatch. The sibling `trade preserves extension counts` spec had the same-position half already; it now gets the second half too.

**A second, independent flake surfaced in the same file and is also fixed.** `test/trade.spec.mjs` compared a server-stamped transaction timestamp against a later `Math.round(Date.now() / 1000)`, which fails whenever a second boundary falls between the request and the assertion. It reproduced once in the first 30-run loop. The same pattern exists at **46 assertions across 16 spec files** and is untouched — see the observation on `user:repository/active/league/ABOUT.md`.

Verification: 40/40 clean runs of `test/trade.spec.mjs` on an isolated database, plus one full-suite run under CI's two excludes at 2671 passing / 3 pending / 0 failing.

Note both commits carry `[skip ci]` (the second is the ABOUT.md observation, and GitHub reads only the HEAD message), so the spec change did not get a CI run — the local full-suite run under CI's excludes stands in for it.

## Plan

- [x] Reproduce the failure in isolation (step 1 below)
- [x] If clean, reproduce against the full suite (step 2)
- [x] Capture the 400 response body so the validation branch is named (step 3)
- [ ] ~~If still clean, vary the clock across the trade deadline (step 4)~~ — not needed
- [x] Fix the spec or the leaking writer per the Interpretation section
- [x] Record the outcome as an observation on `user:repository/active/league/ABOUT.md`

### Step detail

1. Reproduce by running the spec in a loop against an isolated database on the shared container:

   ```bash
   yarn test:db:up
   docker exec -u postgres league-test-pg psql -U league_test -d league_test \
     -c 'CREATE DATABASE league_test_trades OWNER league_test;'
   for i in $(seq 1 30); do
     LEAGUE_DB_HOST=127.0.0.1 LEAGUE_DB_PORT=5433 LEAGUE_DB_DATABASE=league_test_trades \
       TZ=America/New_York NODE_ENV=test TEST=all \
       node_modules/.bin/mocha --exit --require test/global.mjs --reporter min test/trade.spec.mjs \
       || echo "FAILED on iteration $i"
   done
   ```

2. If it will not reproduce in isolation, run the full suite in a loop instead — cross-spec fixture bleed only shows up with the preceding specs present.

3. Capture the response BODY, not just the status. `test/trade.spec.mjs:128` asserts on the code alone, so the validation message that would name the branch is currently discarded. Add it to the assertion output before hunting further.

4. Vary the clock with `LEAGUE_MOCK_DATE` across a trade-deadline boundary if step 1 and 2 both come up clean — a deadline check is the cheapest remaining explanation and is invisible to a same-day loop.

## Interpretation

- **Reproduces in isolation** — the spec's own fixture is under-specified. Fix the spec.
- **Reproduces only in the full suite** — a preceding spec is leaking state. Find the writer and scope its cleanup; record which spec on this task.
- **Reproduces only at certain mocked times** — a clock-derived validation branch. Pin the clock in the spec per the repo's `LEAGUE_MOCK_DATE` convention.
- **Will not reproduce in 100+ runs** — record that on this task with the loop count and close it as unreproducible rather than leaving it open indefinitely. Re-open if the signal fires again.

Record the outcome as an observation on `user:repository/active/league/ABOUT.md` either way, since that is where this repo's CI-flake findings live.

## Key locations

- Spec: `repository/active/league/test/trade.spec.mjs` (assertion at line 128)
- Route under test: `repository/active/league/api/routes/leagues/trades.mjs`
- Failing run: https://github.com/mistakia/league/actions/runs/30720947242
- Repo entity: `user:repository/active/league/ABOUT.md`
- Test-container and isolated-database conventions: `repository/active/league/CLAUDE.md` § Testing
