Skip to content

fix(test): reset the create-rate-limit bucket between session-control tests - #7852

Closed
timwukp wants to merge 1 commit into
kirodotdev:mainfrom
timwukp:fix/session-control-test-create-budget-isolation
Closed

fix(test): reset the create-rate-limit bucket between session-control tests#7852
timwukp wants to merge 1 commit into
kirodotdev:mainfrom
timwukp:fix/session-control-test-create-budget-isolation

Conversation

@timwukp

@timwukp timwukp commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Problem / Motivation

Backend Tests (Windows) (3) fails intermittently on main with two tests refused a session create:

test/test_session_control.py::test_the_audit_write_does_not_run_on_the_event_loop
test/test_session_control.py::test_the_created_agent_name_is_sanitized_before_storage
  kiro_crew.dashboard.session_control.SessionControlError: too many sessions created recently; retry shortly

Observed on main in 2 of the 9 ci.yml runs that completed that job in a ~5.5-hour window — runs 33604886863 (be2ee947) and 33599188871 (aaad3571), with byte-identical annotations.

It is not actually intermittent. create_rate_limit._buckets is process-wide module state keyed (verb, caller_key). Every test in test/test_session_control.py builds its caller as _slot(state, "chat-1"), so _key() returns the same caller_key for all of them, and the file has 36 create_session( call sites against a budget of 20 per 300 s (MAX_SESSION_CREATES_PER_WINDOW / WINDOW_SECS). The file runs in ~1.5 s — three orders of magnitude inside the window — so the creates accumulate and the 21st onward is refused.

Running the file whole fails deterministically:

$ pytest test/test_session_control.py
2 failed, 140 passed in 4.69s

What makes it look intermittent in CI is pytest-split: it distributes this file across 4 groups, so whether any one group carries more than 20 creates depends on the split, and the split shifts as tests are added. That also explains why the same shard number passes on 3.10/3.12 while Windows fails — nothing about the failure is platform-specific, only which tests land together.

The file already resets one piece of process-wide state (stop_retry) in an autouse fixture; this bucket was simply missed.

Why it matters

The failure is attributed to whichever test happens to be past the boundary rather than to the cause, so it reads as a defect in test_the_audit_write_does_not_run_on_the_event_loop — a test about SEL construction threading — when nothing in that test is broken. A reader who trusts the failure location debugs the wrong subsystem.

It also costs real throughput: the job is part of the PR Readiness aggregate, so an unrelated PR inherits a red readiness signal roughly a fifth of the time, and re-running is a ~78% coin flip rather than a fix. Fork contributors cannot even do that — gh run rerun --failed returns Must have admin rights to Repository.

And it degrades the guard's own test value: once the bucket is saturated, later tests in the file exercise the rate-limit refusal path instead of the behaviour they were written for, so a regression in that behaviour would not be caught.

What changed (motivation → approach → change)

Motivation: the tests must be independent of each other; a create budget consumed by an earlier test is not a property any of these tests is asserting.

Approach: reset the bucket per test rather than raising the budget or spreading the caller keys. Raising MAX_SESSION_CREATES_PER_WINDOW would weaken a production security control to accommodate a test artifact — that budget is deliberately sized (the module docstring explains it bounds an auto-approved verb against a creation loop). Giving each test a distinct caller key would work but touches 93 call sites and would silently stop covering the shared-caller case. Resetting is what the repo already does elsewhere: test_create_rate_limit.py and test_chat_folder_cap.py both call create_rate_limit.reset_for_tests(), and reset_for_tests() exists for exactly this ("module state would otherwise leak across tests").

Change: one import plus one autouse fixture in test/test_session_control.py, placed beside the existing _fresh_stop_windows fixture and shaped identically. No production code changes; no test assertions changed.

test/test_session_control.py | 25 +++++++++++++++++++++++++
1 file changed, 25 insertions(+)

Tests

Reproduced first, then fixed — the before/after is the evidence:

before:  2 failed, 140 passed in 4.69s
after:   142 passed in 1.46s

Confirmed no interaction with the two files that already reset this bucket:

$ pytest test/test_session_control.py test/test_create_rate_limit.py test/test_chat_folder_cap.py
154 passed in 1.66s

And under CI's own sharding, all four groups green:

--splits 4 --group 1   36 passed, 106 deselected
--splits 4 --group 2   36 passed, 106 deselected
--splits 4 --group 3   36 passed, 106 deselected
--splits 4 --group 4   34 passed, 108 deselected

No new test is added: the fixture's correctness is demonstrated by the 2 pre-existing failures it clears. A test asserting "the bucket is empty at test start" would assert the fixture against itself.

Manual verification

Run on macOS arm64 (Python 3.12) against main at 8ebf6a87. Because the repo's dev extras carry a pre-existing pin conflict (kirocrew:dev pins pytest-asyncio==0.20.3 while [dev] wants >=0.21), the suite was run against the package's own install_requires rather than the dev extra:

PYTHONPATH=src:test uv run --no-project --python 3.12 \
  --with . --with pytest==9.0.3 --with pytest-asyncio==0.20.3 --with hypothesis \
  --with pytest-timeout --with pytest-xdist==3.5.0 --with pytest-split==0.11.0 \
  python -m pytest test/test_session_control.py -q -o addopts=""

I did not reproduce on Windows. The mechanism is platform-independent (module-level state, a monotonic window, and the split), and the identical annotation on Linux main runs supports that, but I am stating the gap rather than implying I checked it.

Related Issues

None — this flake does not have an issue filed. Raising one to immediately supersede it with this PR seemed like noise; happy to file one if maintainers prefer the paper trail.

Noticed while investigating #7522 / #7553, where this same job was the only red on a fork PR whose diff cannot reach session_control. That is context, not a dependency: this stands alone and touches no file either of those does.

Pattern harvest

Rule candidate: review-prompt

Pattern: a test file that exercises a guard backed by process-wide module state must reset that state per test, or the guard's own budget leaks between tests and the failure surfaces on an unrelated test past the boundary. The tell is a module-level mutable (_buckets, _last_sweep) plus a reset_for_tests() helper that some test files call and others do not.

The generalizable check: for each module exposing reset_for_tests(), every test file that transitively drives it should reset it. create_rate_limit had 2 of 3 callers doing so. That is mechanically checkable and would have caught this before it reached CI.

A second, sharper observation: pytest-split converts order-dependent failures into apparently flaky ones, which is worse than a hard failure because the usual response is a re-run. Anything that fails deterministically when a file runs whole but intermittently under sharding is this class, and the fix is never a re-run.

Checklist

  • At most two commits (one is the norm), with a Conventional Commits title (feat|fix|docs|refactor|perf|test|chore|ci|build|revert: ...)
  • Existing tests pass and new tests added for new functionality — no new functionality; 2 pre-existing failures cleared, 142/142 green
  • Self-review completed; code follows project style guidelines
  • Documentation updated (if applicable) — N/A: test-isolation fix, no user-facing or developer-facing doc surface
  • No secrets, credentials, or internal references in the diff

… tests

The bucket is process-wide module state keyed per caller. Every test in this
file builds its caller as _slot(state, "chat-1"), so all 36 create_session
call sites share one bucket key against a budget of 20 per 300s window. The
file runs in ~1.5s, far inside the window, so the creates accumulate and a
later test is refused a create it is the first to ask for.

Running the file whole fails deterministically on
test_the_created_agent_name_is_sanitized_before_storage and
test_the_audit_write_does_not_run_on_the_event_loop. It reads as an
intermittent CI failure only because pytest-split distributes the file
across groups, so whether a group crosses the budget depends on the split.

Adds an autouse reset fixture, matching test_create_rate_limit.py and
test_chat_folder_cap.py which already reset this bucket.
@timwukp
timwukp requested a review from a team as a code owner September 2, 2026 11:04
@timwukp
timwukp requested a review from smeyffret September 2, 2026 11:04
@github-actions github-actions Bot added fork Pull request from a fork (external contributor) readiness: action required A blocking check or review needs attention labels Sep 2, 2026
@github-actions github-actions Bot added readiness: checking Automated validation is still running and removed readiness: action required A blocking check or review needs attention labels Sep 2, 2026
@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

GPT 5.6 Review (fork) — ✅ no blocking findings

Reviewed 7b07d58695ea70d2b3b32864b72bf9f1ebef1291 via the fork AI-review pipeline; updated in place on each push.

Review details

No findings.
[GPT-REVIEWED] 7b07d58

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Opus 4.8 Review (fork) — ✅ no blocking findings

Reviewed 7b07d58695ea70d2b3b32864b72bf9f1ebef1291 via the fork AI-review pipeline; updated in place on each push.

Review details

The function exists and does exactly what the fixture relies on. The diff is a test-only autouse fixture that resets process-wide bucket state. No candidates to falsify, and nothing in the diff introduces a defect, removes a guard, or touches a rule-governed surface.

No findings.

[OPUS-REVIEWED] 7b07d58

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Design Review (Fable 5, fork) — 🔴 BLOCK (blocking)

Design-level review of 7b07d58695ea70d2b3b32864b72bf9f1ebef1291 via the fork AI-review pipeline — updated in place on each push. A BLOCK verdict blocks PR readiness; PASS/CONCERNS are advisory.

Design-Verdict: BLOCK

Superseded: #7840 already landed this exact fix in test/conftest.py, so this PR adds a dead duplicate with a now-false docstring.

Blockers

  • The problem no longer exists on the base this would merge into. The PR fixes "create_rate_limit._buckets is process-wide module state" leaking across test_session_control.py — but main commit 77db85951 (PR test: isolate the creation rate limiter's module-level buckets between tests #7840, fixing test: session-create rate limiter's module-level buckets leak across tests, flaking CI shards #7836) already clears those same buckets in an autouse fixture at test/conftest.py:1244, which covers every test in test/, this file included. Merging adds a second autouse reset whose 20-line docstring claims "without this reset the creates ACCUMULATE and a later test is refused" — false once conftest runs — so future readers get two competing owners of this isolation and a misleading causal story, contradicting the repo's own rule that isolation belongs to one decided floor (AGENTS.md testing section). Fix: close as superseded, or rebase onto current main and confirm the before/after delta is now zero; if anything remains, it is the conftest fixture's gap, not a per-file one.

Suggestions

  • The genuinely unlanded part of this PR is its "pattern harvest": a mechanical check that every test file driving a reset_for_tests() module resets it — that is a separate follow-up worth filing, not this diff.

[DESIGN-REVIEWED] 7b07d58

@github-actions github-actions Bot added readiness: action required A blocking check or review needs attention merge conflict Branch has merge conflicts with its base — author must resolve before merge and removed readiness: checking Automated validation is still running labels Sep 2, 2026
@bolichen97

Copy link
Copy Markdown
Collaborator

Closing — already landed on main

Verified relationship: already on main``

I decomposed #7852 into its four parts (one added import; one autouse fixture decorator+name; the fixture body; the docstring) and checked each against main before reading the claim's conclusion. The strongest test is a whole-file comparison: git show pr/7852:test/test_session_control.py vs git show 1a765b88:test/test_session_control.py differs ONLY inside the _fresh_create_budget docstring — the import from kiro_crew.dashboard import create_rate_limit (main line 25), the @pytest.fixture(autouse=True) decorator, the fixture NAME _fresh_create_budget, its placement immediately after _fresh_stop_windows, and the body create_rate_limit.reset_for_tests() / yield / create_rate_limit.reset_for_tests() are byte-identical and already present. That landed in 760d8f5 ("test: deflake two suites that leaked process-wide module state (#7898)", Joe Guo), which git merge-base --is-ancestor confirms is an ancestor of the pinned 1a765b8 and of today's origin/main (dc35c19); git log -S'_fresh_create_budget' shows one commit and no revert. #7898 is in fact a strict superset — it also deflakes test/test_external_logout_detection.py, which #7852 does not touch. Independently, 77db859 (#7840, Will Laws) added an autouse _reset_create_rate_limit_buckets at test/conftest.py:1245 that clears create_rate_limit._buckets under create_rate_limit._lock around every test in the test/ testpath, so this file is isolated even without a per-file fixture; reset_for_tests() at src/kiro_crew/dashboard/create_rate_limit.py:120 is the same mechanism (clears _buckets, resets _last_sweep, under _lock), not a similarly-named one. #7852's diff is 1 file, +25/-0: no production change, no new test (the body explicitly declines one), no config key, error code, platform branch, or doc file — so there is no part left that main structurally cannot deliver. The branch is a single commit off merge-base 8ebf6a8 (not stacked), and 8ebf6a8 predates both landings, which is why the cached diff still looks non-empty. The Design Review (Fable 5) BLOCK on this PR alleged exactly this supersession citing #7840/conftest.py:1244; I confirmed it independently and it is understated, since #7898 landed the same per-file fixture under the same name.

This was adjudicated twice, independently; the second reviewer reached the same ruling (already on main``). Their strongest corroborating fact:

Byte-identical code from the identical pre-image blob: PR #7852's test/test_session_control.py hunk (index 63b5b10ec5..) and main commit 760d8f570945 ("test: deflake two suites that leaked process-wide module state (#7898)", index 63b5b10ec..) both add from kiro_crew.dashboard import create_rate_limit plus @pytest.fixture(autouse=True) / def _fresh_create_budget(): / create_rate_limit.reset_for_tests() / yield / create_rate_limit.reset_for_tests() at the same position after _fresh_stop_windows; docstring-stripped comparison of the two + sets returns True. That fixture is live on main at test/test_session_control.py:57-69, with the import at line 25. — Second, broader coverage of the very same state: 77db859511b9 ("test: isolate the creation rate limiter's module-level buckets between tests (#7840)", fixes #7836) added autouse _reset_create_rate_limit_buckets at test/conftest.py:1245-1266, executing with create_rate_limit._lock: create_rate_limit._buckets.clear() before and after every test in test/ — the same body reset_for_tests() runs (src/kiro_crew/dashboard/create_rate_limit.py:120-125: with _lock: _buckets.clear(); _last_sweep = 0.0). git merge-base --is-ancestor confirms both are ancestors of the pinned 1a765b88 and of current origin/main. — Third, no remainder to inherit: git log 8ebf6a878..pr/7852 is the single commit 7b07d5869 touching only test/test_session_control.py (+25/-0), so the PR has no production change, no added test, and no doc beyond the fixture's own docstring.

Evidence

  1. diff <(git show 1a765b88:test/test_session_control.py) <(git show pr/7852:test/test_session_control.py) yields exactly one hunk, entirely inside the _fresh_create_budget docstring; main lines 25 (from kiro_crew.dashboard import create_rate_limit), 57-58 (@pytest.fixture(autouse=True) / def _fresh_create_budget():) and 68-70 (create_rate_limit.reset_for_tests() / yield / create_rate_limit.reset_for_tests()) already carry every executable line the PR adds. 2) git show 760d8f570945 (test: deflake two suites that leaked process-wide module state #7898) adds precisely that import and that same-named fixture to test/test_session_control.py (+16) plus test/test_external_logout_detection.py (+17), and is an ancestor of 1a765b8 and of dc35c19. 3) git show 77db85951 (test: isolate the creation rate limiter's module-level buckets between tests #7840) adds autouse _reset_create_rate_limit_buckets at test/conftest.py:1245, clearing create_rate_limit._buckets inside with create_rate_limit._lock: before and after every test in test/, covering this file file-wide; src/kiro_crew/dashboard/create_rate_limit.py:120 reset_for_tests() is the same clear-under-lock mechanism.

Why this one and not the other

Yes — main is the correct survivor, and it is the better implementation on both counts. 760d8f5 (#7898) is a strict superset of #7852's file-level change: identical fixture plus a second deflake in test/test_external_logout_detection.py that #7852 does not attempt. 77db859 (#7840) is broader still, isolating the bucket for every test under test/ rather than one file. Merging #7852 would add a third reset of the same bucket on this file's path and, because the only delta is docstring text, would land as a conflict rather than new behaviour. One correction to the claim's periphery, though it is outside my subject: the claimed "FULL overlap with open #4533" is content-true but must not be read as grounds to close #4533 — its fixture is named _fresh_create_buckets (not _fresh_create_budget), it is incidental to a 26-file feature (atomic auto-skill promotion) that main does not carry, and the right disposition there is dropping that one hunk on rebase. Likewise #4904's inline create_rate_limit.reset_for_tests() inside a single new test body is a merge-conflict relation only.

Carry this over first

This closure is about redundancy, and these items are the exception: they are not on main and not in the surviving PR, so they need a home before the topic is finished. Please don't let them go with the branch.

One doc line, optional. #7852's _fresh_create_budget docstring in test/test_session_control.py is the only surviving delta and is arguably the more accurate of the two: it attributes the apparent intermittency to pytest-split group distribution — which is what CI actually uses (--splits "$SHARD_COUNT" --group at .github/workflows/ci.yml:716,722,841) and what explains why the same shard number reds on Windows while passing on 3.10/3.12 — where main's landed docstring says "xdist distribution". It also states the concrete budget ("20 creates per 300 s") and cross-references test_create_rate_limit.py / test_chat_folder_cap.py as the two files that already reset this bucket. Carrying those two sentences into main's docstring is a comment-only edit with no behavioural effect. Separately, the PR body's "Pattern harvest" proposal — mechanically check that every test file transitively driving a module exposing reset_for_tests() resets it — is a genuinely unlanded idea, but it is prose in the body, not code in the diff, and belongs in its own issue/PR.


From a repository-wide duplicate/overlap audit of every pull request open against main (2026-09-02, 330 PRs, one reviewer per PR). Each PR was read as its full merge-base diff plus its description and every comment and review, then compared against each candidate PR's own diff and against origin/main at 1a765b88ceb7. Findings that implied a closure were re-adjudicated independently, including an adversarial pass whose only job was to refute them; the reasoning above is what survived. If it is wrong, reopening costs nothing — please say so, and treat the reasoning rather than the outcome as the thing to correct.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

fork Pull request from a fork (external contributor) merge conflict Branch has merge conflicts with its base — author must resolve before merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants