# frozen_string_literal: true RSpec.describe Dispatch::Adapter::Claude::Headers do let(:oauth_token) { "sk-ant-oat01-abc123" } let(:api_key) { "sk-ant-api03-xyz789" } describe ".build" do context "with OAuth token" do subject(:headers) { described_class.build(api_key: oauth_token) } it "emits Authorization: Bearer ..." do expect(headers["Authorization"]).to eq("Bearer #{oauth_token}") end it "does not emit X-Api-Key" do expect(headers).not_to have_key("X-Api-Key") end it "sets User-Agent to the Claude CLI user agent" do expect(headers["User-Agent"]).to eq(Dispatch::Adapter::Claude::Headers::USER_AGENT) end it "sets Content-Type to application/json" do expect(headers["Content-Type"]).to eq("application/json") end it "sets Accept to application/json for non-streaming" do expect(headers["Accept"]).to eq("application/json") end it "sets Accept to text/event-stream for streaming" do h = described_class.build(api_key: oauth_token, stream: true) expect(h["Accept"]).to eq("text/event-stream") end it "includes anthropic-version header" do expect(headers["anthropic-version"]).to eq("2023-06-01") end end context "with raw API key" do subject(:headers) { described_class.build(api_key: api_key) } it "emits X-Api-Key" do expect(headers["X-Api-Key"]).to eq(api_key) end it "does not emit Authorization" do expect(headers).not_to have_key("Authorization") end it "does not set User-Agent" do expect(headers).not_to have_key("User-Agent") end end context "with explicit is_oauth: true override" do it "treats the key as OAuth even when it doesn't start with sk-ant-oat" do h = described_class.build(api_key: "some-other-token", is_oauth: true) expect(h["Authorization"]).to eq("Bearer some-other-token") expect(h).not_to have_key("X-Api-Key") end end context "with explicit is_oauth: false override" do it "treats an oat token as API key" do h = described_class.build(api_key: oauth_token, is_oauth: false) expect(h["X-Api-Key"]).to eq(oauth_token) expect(h).not_to have_key("Authorization") end end context "Anthropic-Beta header" do it "includes all DEFAULT_BETAS" do h = described_class.build(api_key: api_key) beta_values = h["anthropic-beta"].split(",") Dispatch::Adapter::Claude::Headers::DEFAULT_BETAS.each do |b| expect(beta_values).to include(b) end end it "includes interleaved-thinking beta by default" do h = described_class.build(api_key: api_key) expect(h["anthropic-beta"]).to include( Dispatch::Adapter::Claude::Headers::INTERLEAVED_THINKING_BETA ) end it "omits interleaved-thinking beta when interleaved_thinking: false" do h = described_class.build(api_key: api_key, interleaved_thinking: false) expect(h["anthropic-beta"]).not_to include( Dispatch::Adapter::Claude::Headers::INTERLEAVED_THINKING_BETA ) end it "includes extra_betas in the beta header" do h = described_class.build(api_key: api_key, extra_betas: ["my-beta-2025-01-01"]) expect(h["anthropic-beta"]).to include("my-beta-2025-01-01") end it "deduplicates beta values" do dup_beta = Dispatch::Adapter::Claude::Headers::DEFAULT_BETAS.first h = described_class.build(api_key: api_key, extra_betas: [dup_beta]) values = h["anthropic-beta"].split(",") expect(values.count(dup_beta)).to eq(1) end end context "with caller extra headers" do it "includes extra headers" do h = described_class.build(api_key: api_key, extra: { "X-Custom" => "value" }) expect(h["X-Custom"]).to eq("value") end it "caller extra does NOT clobber Authorization for OAuth tokens" do h = described_class.build( api_key: oauth_token, extra: { "Authorization" => "Bearer malicious" } ) expect(h["Authorization"]).to eq("Bearer #{oauth_token}") end it "caller extra does NOT clobber X-Api-Key for API keys" do h = described_class.build( api_key: api_key, extra: { "X-Api-Key" => "stolen-key" } ) expect(h["X-Api-Key"]).to eq(api_key) end it "accepts extra keys as symbols and converts them to strings" do h = described_class.build(api_key: api_key, extra: { "X-Custom": "value" }) expect(h["X-Custom"]).to eq("value") end end context "Stainless metadata headers" do subject(:headers) { described_class.build(api_key: api_key) } it "includes x-stainless-lang: ruby" do expect(headers["x-stainless-lang"]).to eq("ruby") end it "includes x-stainless-package-version" do expect(headers["x-stainless-package-version"]).to eq( Dispatch::Adapter::Claude::Headers::STAINLESS_PACKAGE_VERSION ) end it "includes x-stainless-runtime: ruby" do expect(headers["x-stainless-runtime"]).to eq("ruby") end it "includes x-stainless-runtime-version matching RUBY_VERSION" do expect(headers["x-stainless-runtime-version"]).to eq(RUBY_VERSION) end end end end