Skip to content

ThreadTokenUsageTest is wall-clock-bounded but missing from ConcurrencyStressCollection — reddens unrelated PRs #2001

Closed

Description

@rbuergi

ThreadTokenUsageTest judges a cancel on a 10-second wall clock while sharing a 4-vCPU runner with three other AI.Test classes. It is not in ConcurrencyStressCollection, and it should be — or its wait should stop being wall-clocked.

Seen on #1991's CI (run 32490339312, shard 3), where it was the one failure in that PR's shard that the PR could not have caused.

What failed

MeshWeaver.AI.Test.ThreadTokenUsageTest.CancelledRound_RecordsTokensConsumedBeforeCancel_OnCellAndThread [FAIL]
  Expected the observable to emit a value matching the predicate within 10s,
  but it did not. The observable emitted nothing at all.
    at ThreadTokenUsageTest.cs:262

Line 262 is the usage-satellite wait after the cancel:

await WaitForUsage(threadPath, "usage_cancel_model",
    u => u.InputTokens == InTokens && u.OutputTokens == OutTokens, 10_000);

It ran out of time. It did not read a wrong value — the preceding WaitForThread(... Cancelled, 20_000) had already passed, so the cancel itself landed; only the satellite had not appeared inside its own, tighter 10s budget.

Why it is structural, not luck

8807a6d8c ("Give the five machine-hungry AI.Test classes the box, and parallelise the rest", 2026-08-10) turned parallelism on for the whole assembly:

{ "parallelizeAssembly": false, "parallelizeTestCollections": true,
  "maxParallelThreads": 4, "methodTimeout": 30000 }

…and serialised exactly five classes into ConcurrencyStressCollection: AgentChatClientDeadlockTest, CrossHubPatchAtomicityTest, DelegationDeadlockTest, InboxToolIntegrationTest, PatchWorkspaceAckTest.

ThreadTokenUsageTest carries no [Collection] attribute, so since that commit it runs up to 4-wide on a 4-vCPU CI runner — while still judging a cancel-then-satellite round trip on a fixed 10 000 ms.

That commit's own message names this failure mode precisely:

a handful of classes deliberately saturate the machine and then judge the result on a wall clock, and four of those timesharing one 4-vCPU runner blow bounds that hold with room to spare when each has the box

The membership rule it states is structural, not "whatever failed last time" — which is exactly why this belongs in the set rather than being re-attributed once per PR. It also has form: #1812 was a different defect in this same test's satellite read.

Locally, uncontended, the class is 10/10 in 9 s.

Two ways to fix it — either, not both

  1. [Collection(ConcurrencyStressCollection.Name)] on the class. One line, consistent with the five already there, and honest about what the test is: a timing assertion that needs the box. Costs some wall clock on the shard.
  2. Make the wait event-driven instead of wall-clocked. The satellite write is an observable event; a bound that exists only to stop a hang can then be generous without weakening anything, because the assertion no longer is the bound.

Not raising 10 000 to 20 000. That is the band-aid AGENTS.md forbids: it does not make the test judge the right thing, it just moves the threshold until the next busy runner finds it.

Why this is worth its own issue

Relates to #1384, which is the right complaint at population level — six unrelated single-test failures in ten runs — and which notes that the recurring cost is attribution: each one buys a fresh investigation that ends in the same answer. This one is already attributed, with the commit that created the gap and a one-line fix, so it can be closed rather than re-diagnosed the next time it reddens somebody's PR.

🤖 Generated with Claude Code

Activity

  1. added a commit that references this issue on Aug 21, 2026
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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions