From a410ff50c54a5ce31d06d43aff2787e388d44114 Mon Sep 17 00:00:00 2001 From: Thomas Kosiewski Date: Tue, 13 Oct 2026 20:17:42 +0000 Subject: [PATCH] test(adapters/linear): fix TestRateLimitDeadlineReturnsTypedRateLimited flake under -race load The test used one 50ms real-clock context as both the transport context and the reference deadline for the no-sleep invariant. PostThought makes two localhost roundtrips inside that window (the OAuth client-credentials token fetch, then the GraphQL POST that gets the 429). Under -race CPU load either roundtrip can exceed 50ms, so the client returned a plain "context deadline exceeded" before the rate-limit path ran. The deadline branch in doJSONWithRetry fires whenever now+delay (one hour here) overshoots the caller deadline, so any deadline far below an hour exercises it identically. Widen the deadline to 10s and let a 5s completion bound separate "returned promptly" from "slept until the deadline or backoff". Same fix as the Slack flake in #39. No production change: returning the transport context error when the deadline expires before the 429 arrives is correct. Red check: disabling the deadline branch in retry.go makes the test fail with "slept past its caller deadline". Loaded repro (48 parallel -race -count=50 -cpu=1 instances plus 32 CPU spinners, 3 rounds): 316/7200 failures before, 0/7200 after. Fixes #109 --- adapters/linear/harden_capability_test.go | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/adapters/linear/harden_capability_test.go b/adapters/linear/harden_capability_test.go index fd1493a..ce09d50 100644 --- a/adapters/linear/harden_capability_test.go +++ b/adapters/linear/harden_capability_test.go @@ -193,11 +193,18 @@ func TestRateLimitDeadlineReturnsTypedRateLimited(t *testing.T) { }) threadID := agentSessionThread(t, bot, api, now()) - ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond) + // The deadline is the invariant's reference point, not a race the transport + // must win: the no-sleep branch triggers whenever now+delay (one hour here) + // overshoots the deadline, so any timeout far below an hour exercises it + // identically. The timeout only needs to leave the OAuth token fetch and the + // first GraphQL roundtrip comfortable headroom: 50ms lost that race under + // -race CPU load (issue #109, same cause as Slack's #39). 10s sits ~3 orders + // of magnitude above a loaded roundtrip and ~3 below the one-hour sleep it + // guards against. + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() done := make(chan error, 1) - start := time.Now() go func() { _, err := adapter.PostThought(ctx, threadID, "thinking") done <- err @@ -208,10 +215,10 @@ func TestRateLimitDeadlineReturnsTypedRateLimited(t *testing.T) { if _, ok := errors.AsType[*linear.RateLimited](err); !ok { t.Fatalf("err = %v, want *linear.RateLimited (deadline-bounded, not slept off)", err) } - if elapsed := time.Since(start); elapsed > time.Second { - t.Fatalf("PostThought took %s; it must not sleep past the context deadline", elapsed) - } - case <-time.After(2 * time.Second): + case <-time.After(5 * time.Second): + // A correct adapter returns after two roundtrips with no sleep. A broken + // one blocks at least until the 10s ctx deadline, or the full one-hour + // backoff, so 5s cleanly separates the two without racing the transport. t.Fatal("PostThought slept past its caller deadline: a long backoff was not deadline-bounded") } }