1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
|
# frozen_string_literal: true
require "tmpdir"
require "fileutils"
# ---------------------------------------------------------------------------
# SAFETY: sandbox the developer's real home directory.
#
# Several constants in this gem (notably TokenStore::DEFAULT_PATH and the
# rate-limiter state file derived from it) freeze a path under Dir.home at
# class-load time. If we don't redirect HOME before the `require` below,
# every spec that constructs an adapter without an explicit token_store:
# would read/write ~/.config/dispatch/claude_oauth.json AND
# ~/.config/dispatch/claude_rate_limit on the developer's real machine —
# leaking state across runs and (because the default min_request_interval
# is 1.0s) silently injecting up to a full second of sleep before every
# stubbed HTTP call. That is what makes a fast unit suite take 10 minutes.
#
# We point HOME at a fresh tmp dir for the entire rspec process and clean
# it up on exit. This MUST happen before `require "dispatch/adapter/claude"`.
# ---------------------------------------------------------------------------
SPEC_SANDBOX_HOME = Dir.mktmpdir("dispatch-claude-spec-home-")
ENV["HOME"] = SPEC_SANDBOX_HOME
at_exit { FileUtils.rm_rf(SPEC_SANDBOX_HOME) }
require "dispatch/adapter/claude"
# ---------------------------------------------------------------------------
# SAFETY: block ALL real network traffic from every spec, unconditionally.
#
# This gem talks to the live Anthropic API, which costs real money and can
# hit production rate limits / billing. Tests must NEVER make a real
# outbound HTTP request, even if a spec author forgets to
# `require "webmock/rspec"` or to stub a request explicitly.
#
# Loading webmock here ensures Net::HTTP is monkey-patched process-wide for
# every rspec invocation — including running a single spec file in isolation.
# `disable_net_connect!` then makes any unstubbed request raise
# WebMock::NetConnectNotAllowedError instead of silently going to the wire.
#
# `allow_localhost: false` is explicit: even loopback traffic must be stubbed
# (the OAuth callback-server specs need to stub their own 127.0.0.1 calls).
# ---------------------------------------------------------------------------
require "webmock/rspec"
WebMock.disable_net_connect!(allow_localhost: false)
# Disable interactive OAuth auto-recovery during specs — otherwise an
# unstubbed 401 would attempt to spawn a browser and run the full OAuth
# login flow, which would hang the suite.
ENV["AUTH_RECOVERY"] = "0"
RSpec.configure do |config|
# Enable flags like --only-failures and --next-failure
config.example_status_persistence_file_path = ".rspec_status"
# Disable RSpec exposing methods globally on `Module` and `main`
config.disable_monkey_patching!
config.expect_with :rspec do |c|
c.syntax = :expect
end
# Belt-and-braces: re-assert net-connect lockdown before every example,
# so a spec that calls WebMock.allow_net_connect! cannot leak that state
# to subsequent specs.
config.before(:each) do
WebMock.disable_net_connect!(allow_localhost: false)
end
# ── Speed: neutralise the 1-second-per-request cooldown ────────────────
#
# Dispatch::Adapter::Claude defaults `min_request_interval` to 1.0, which
# makes RateLimiter#wait! flock a state file and sleep up to a full
# second before every API call. With WebMock stubs that cost is pure
# waste — it added many minutes to the suite. For every spec EXCEPT
# rate_limiter_spec.rb (which explicitly verifies real cooldown timing),
# we stub the constant down to 0 so wait! short-circuits as a no-op.
config.before(:each) do |example|
stub_const("Dispatch::Adapter::Claude::DEFAULT_MIN_REQUEST_INTERVAL", 0) unless example.metadata[:file_path].to_s.include?("rate_limiter_spec")
# Also wipe the sandboxed rate-limit / token files between examples so
# state from one example can never bleed into the next.
config_dir = File.join(SPEC_SANDBOX_HOME, ".config", "dispatch")
if Dir.exist?(config_dir)
Dir.glob(File.join(config_dir, "*")).each do |f|
File.delete(f)
rescue StandardError
nil
end
end
end
end
|