What happens
ruby/test/basecamp/oauth_transport_test.rb:839 —
OAuthTransportTest#test_https_proxy_refused_without_p_use_ssl_support — was
observed failing during a parallel make run while the #669/#674 build-hygiene
wave was being proven (2026-08-12, macOS, several concurrent agent workspaces on
one machine). It passes on a quiet machine and it passed on every serial run
since. Filing it rather than fixing it, because it is out of scope for the PR
that hit it and it should not be lost.
I have not reproduced it deterministically. What follows is a mechanism read of
the source, which is the actionable part.
Why it is timing-dependent
The test asserts the fetcher refuses an https:// proxy when the bundled
net-http lacks Net::HTTP.new's eighth p_use_ssl parameter:
error = assert_raises(Basecamp::Oauth::OauthError) do
Basecamp::Oauth::Fetcher.stream_http(:get, "https://old-net-http.test/token", timeout: 1)
end
assert_equal "validation", error.type
That OauthError is raised at ruby/lib/basecamp/oauth/fetcher.rb:371. Reaching
it requires getting past line 363 first:
proxy_uri = Timeout.timeout(timeout, Net::OpenTimeout) { uri.find_proxy }
URI::Generic#find_proxy resolves the target hostname — IPSocket.getaddress —
to evaluate its loopback rule. The fetcher's own comment above that line says so
and is deliberate about it ("a blocking DNS call that must sit inside the
advertised bound like every other network step"). So the test performs a live
DNS lookup for old-net-http.test inside a 1-second bound, before the
assertion's subject is ever evaluated.
.test is RFC 6761 reserved, so this is normally a fast NXDOMAIN — but it is
still a real query to whatever resolver the machine has. Blow the second and the
Timeout fires instead, surfacing as Faraday::TimeoutError rather than
Basecamp::Oauth::OauthError, and assert_raises fails. Under make -j the
same machine is running npm, uv, bundler, Gradle and Go concurrently, which is
exactly when a resolver round-trip stops being free.
That predicts a specific failure signature. Worth confirming against the real
failure output before acting:
Expected Basecamp::Oauth::OauthError, got #<Faraday::TimeoutError>
If the observed failure was a different error or a different assertion, this
analysis is wrong and the finding still stands on the observation.
Notes for whoever picks it up
- The neighbouring test at
oauth_transport_test.rb:800 already stubs
IPSocket.getaddress (there, to simulate a stall). This test does not, so it
is the only one in the file whose pass/fail depends on the machine's resolver.
- The
timeout: 1 is doing double duty: the test's subject is a validation
refusal that needs no timeout at all, but the value also bounds a DNS call the
test does not care about.
- Directions, not a prescription — which is right depends on what the test is
meant to pin: stub the resolution the way its neighbour does; raise the bound;
or reorder so the capability check precedes proxy resolution (that last one
changes shipped behaviour and would need its own argument).
ruby/test is not parallelized (no parallelize_me!), so this is not
intra-suite interference. The concurrency is make -j across toolchains,
competing for the machine — not for Ruby's process.
Scope
Split off from the PR closing #669 and #674. That PR closes #674 against its
Gradle half only (serializing the Gradle-backed check-targets); this is the
other thing make -j surfaced, and it is unrelated to Gradle.
What happens
ruby/test/basecamp/oauth_transport_test.rb:839—OAuthTransportTest#test_https_proxy_refused_without_p_use_ssl_support— wasobserved failing during a parallel
makerun while the #669/#674 build-hygienewave was being proven (2026-08-12, macOS, several concurrent agent workspaces on
one machine). It passes on a quiet machine and it passed on every serial run
since. Filing it rather than fixing it, because it is out of scope for the PR
that hit it and it should not be lost.
I have not reproduced it deterministically. What follows is a mechanism read of
the source, which is the actionable part.
Why it is timing-dependent
The test asserts the fetcher refuses an
https://proxy when the bundlednet-http lacks
Net::HTTP.new's eighthp_use_sslparameter:That
OauthErroris raised atruby/lib/basecamp/oauth/fetcher.rb:371. Reachingit requires getting past line 363 first:
URI::Generic#find_proxyresolves the target hostname —IPSocket.getaddress—to evaluate its loopback rule. The fetcher's own comment above that line says so
and is deliberate about it ("a blocking DNS call that must sit inside the
advertised bound like every other network step"). So the test performs a live
DNS lookup for
old-net-http.testinside a 1-second bound, before theassertion's subject is ever evaluated.
.testis RFC 6761 reserved, so this is normally a fast NXDOMAIN — but it isstill a real query to whatever resolver the machine has. Blow the second and the
Timeoutfires instead, surfacing asFaraday::TimeoutErrorrather thanBasecamp::Oauth::OauthError, andassert_raisesfails. Undermake -jthesame machine is running npm, uv, bundler, Gradle and Go concurrently, which is
exactly when a resolver round-trip stops being free.
That predicts a specific failure signature. Worth confirming against the real
failure output before acting:
If the observed failure was a different error or a different assertion, this
analysis is wrong and the finding still stands on the observation.
Notes for whoever picks it up
oauth_transport_test.rb:800already stubsIPSocket.getaddress(there, to simulate a stall). This test does not, so itis the only one in the file whose pass/fail depends on the machine's resolver.
timeout: 1is doing double duty: the test's subject is a validationrefusal that needs no timeout at all, but the value also bounds a DNS call the
test does not care about.
meant to pin: stub the resolution the way its neighbour does; raise the bound;
or reorder so the capability check precedes proxy resolution (that last one
changes shipped behaviour and would need its own argument).
ruby/testis not parallelized (noparallelize_me!), so this is notintra-suite interference. The concurrency is
make -jacross toolchains,competing for the machine — not for Ruby's process.
Scope
Split off from the PR closing #669 and #674. That PR closes #674 against its
Gradle half only (serializing the Gradle-backed
check-targets); this is theother thing
make -jsurfaced, and it is unrelated to Gradle.