Skip to content

test: three timing-sensitive tests still fail on a loaded runner, with no landed fix #522

Description

@ivndev001

Steps

Open any PR whose branch is a few days behind main, or re-run test + coverage (latest ruby/rails) on a loaded runner.

Expected

A PR whose diff touches no lib/ file, and in two cases no test/ file at all, gets a green suite.

Actual

Four of the five PRs open on 2026-09-03 went red on test + coverage, and none of the failures was caused by the PR's diff. Six distinct timing-sensitive methods across five files failed on a loaded runner:

Test File Landed fix on main
SwarmSupervisionTest#test_crash_loop_backoff_grows_and_term_drains_while_pending test/integration/swarm_supervision_test.rb yes — #518
TimeoutHangTest#test_a_genuinely_sleeping_job_past_its_deadline_is_abandoned_and_booked_expired test/integration/timeout_hang_test.rb yes — #518, reinforced by #519
ApiMutationsTest#test_bulk_deduplicates_repeated_keys test/engine/api_mutations_test.rb yes — #520
WebSearchTest#test_search_bounds_round_trips_on_a_huge_no_match_queue test/unit/web_search_test.rb no
LimiterStressTest#test_1000_concurrent_acquires_never_exceed_the_limit test/integration/limiter_stress_test.rb no
WebExtensionsTest#test_search_kinds_filter test/engine/web_extensions_test.rb no

Three fixes landed on main on 2026-09-03/04. The bottom three have no landed fix, and they are the subject of this issue.

Observed failure text, for identification:

  • WebSearchTestExpected 104 to be <= 102 (a bounds assertion that hard-codes ONE queue; see the corrected diagnosis below)
  • LimiterStressTest17 acquires timed out with a 15s wait
  • WebExtensionsTest — expected ["retry"], got []

Version

Filed against main at b29d659010e56f6b5cf0605cd306a3ad421b308c.

Why this is worth its own issue

The three open CI issues do not cover this class, and each was checked:

So the failures are a real, uncovered class: timing-sensitive integration tests that fail on a loaded runner. The cost is not theoretical — it is why four PRs sat red for days while their diffs were fine, and why a rebase is currently the standard remedy for a test result that had nothing to do with the change.

The three root causes are DIFFERENT — do not apply one remedy to all three

An earlier version of this issue folded all three under "make the assertion follow the clock or the
schedule rather than a fixed wall-clock margin". That fits two of them and actively misleads on the
third, which has no clock at all.

  1. WebSearchTest — the bound hard-codes one queue; the driver is how many queues EXIST. The
    original gloss ("assumes the queue did not grow under load") is wrong and the cited code says so:
    queue_scan_stop caps at [SCAN_LIMIT_PER_QUEUE, SCAN_BUDGET - @scanned].min
    (lib/wurk/web/search.rb), so the seeded queue's SIZE cannot move the counter — the test's own
    comment already notes the count is "never proportional to the queue's real size". What moves it is
    search_queues iterating Queue.all, which is SMEMBERS of the shared Keys::QUEUES_SET
    (lib/wurk/queue.rb:33-36): one SMEMBERS + one LLEN + 100 LRANGE = exactly 102 for a single 10k
    queue, which is max_round_trips with ZERO margin. Every additional member of that set adds at
    least one more round trip.

    WHO adds the extra members is NOT established — do not invent an answer. An earlier revision
    of this issue blamed concurrent sibling methods under parallelize_me!. That is false:
    Wurk::Test::UnitCase overrides def self.parallelize_me! with an EMPTY body
    (test/test_helper.rb:199-201), minitest-parallel_fork re-extends any genuinely parallel suite
    with Minitest::Unparallelize anyway, and test/integration/swarm_boot_test.rb:92-97 states both
    in prose. Two hypotheses remain consistent with the repo and neither is confirmed: a leaked
    scheduled poller re-enqueuing due entries and SADDing their queues (the mechanism test: seed the API mutation sorted-set fixtures not-yet-due #520's own
    comment records at test/engine/api_mutations_test.rb:297-309), or two suite runs sharing one
    Redis instance on a loaded runner.

    Remedy: because the writer is unidentified, the fix must hold when the set changes during the
    measured window, not merely when it is larger at the start. Either derive the bound from the
    queues present in a way that tolerates concurrent mutation, or give this test its own Redis
    logical DB / capsule (as DemoWorkloadTest does with Wurk::Test::DEDICATED_DB) so Queue.all
    can only ever see its own queue. Do NOT "isolate Keys::QUEUES_SET" by editing
    lib/wurk/keys.rb — that is a lib/ change, which the diff-scope criterion forbids and which
    would make this slice runtime-affecting. Do NOT widen SCAN_LIMIT_PER_QUEUE or QUEUE_PAGE
    they are a real DoS guard ("a keystroke must never full-walk a multi-million-entry queue") and
    this test is the only thing pinning them.

  2. WebExtensionsTest — a due-NOW seed that a scheduled poller drains first. :189-192 seeds
    ZADD <name> Time.now.to_f, which is exactly the pattern test: seed the API mutation sorted-set fixtures not-yet-due #520 fixed in
    api_mutations_test.rb:296-310 by seeding a future score (+ 3600). That landed comment names
    the mechanism: "a due entry in the worker's shared Redis DB is exactly what a scheduled poller
    still running from an earlier class pops before the request lands." This is the clock/schedule
    remedy, and it is a direct mirror of merged precedent.

  3. LimiterStressTest — genuinely wall-clock dependent. 1000 acquires against limit 5 across 50
    threads under a fixed wait_timeout: 15. This is the hard one; pick and state an approach in the
    PR rather than tuning the number, and show how the replacement is demonstrated red.

Do not simply widen a constant, and do not delete an assertion that is testing something real.

Acceptance criteria

All three tests PASS on a quiet runner today, so bin/check green proves nothing on its own. These
criteria are what go red against current main:

  • WebSearchTest: a case that mutates Keys::QUEUES_SET concurrently with the measured
    window
    — a thread SADDing sibling queues while the search runs — still satisfies the bound
    (red today, and unlike a population fixed before the counter is installed, this is the shape
    that actually flakes; a Queue.all.size snapshot taken at assert time passes the static
    version and still flakes)
  • WebExtensionsTest: a case where a due-now-seeded retry entry is drained before the request is
    made still returns ["retry"] from the kinds filter (red today)
  • LimiterStressTest: the test asserts the ATOMICITY invariant it exists for — max_seen <= limit and limiter.size == 0 — without asserting over_limit == 0, and a case that
    deliberately starves the limiter is added showing that new assertion red
  • no wait/timeout literal in test/integration/limiter_stress_test.rb increases relative to
    origin/mainwait_timeout: 15 at :36 lives inside test/**, so the diff-scope criterion
    below does not reach it, and raising it is the exact "buy green by widening a constant" this
    issue forbids
  • the diff touches only test/**git diff --name-only origin/main...HEAD lists no path
    outside test/. This is what stops "buy green by widening SCAN_LIMIT_PER_QUEUE"
  • the PR body states, per test, what the old assertion depended on and what the new one depends
    on instead
  • no test is deleted or skipped to reach green
  • gates green: bin/check (lint + suite + parity)

Affected paths

test/unit/web_search_test.rb, test/engine/web_extensions_test.rb, test/integration/limiter_stress_test.rb

AFK / HITL

AFK.

Verified against

42cc4e4

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    afkagent ships it solo via worktree+PRarea/cibugSomething isn't workingpriority/p2Medium — Pro parity / useful additionsready-for-agentsize/2h~2 hours — medium, multi-module

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions