# 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