# frozen_string_literal: true RSpec.describe Dispatch::Adapter::Claude::Cloaking do describe ".billing_header" do it "returns a string starting with 'x-anthropic-billing-header:'" do result = described_class.billing_header({}) expect(result).to start_with("x-anthropic-billing-header:") end it "ends with a semicolon" do result = described_class.billing_header({}) expect(result).to end_with(";") end it "contains cc_entrypoint=cli" do result = described_class.billing_header({}) expect(result).to include("cc_entrypoint=cli") end it "contains cc_version starting with the CLAUDE_CODE_VERSION" do result = described_class.billing_header({}) version = Dispatch::Adapter::Claude::Headers::CLAUDE_CODE_VERSION expect(result).to match(/cc_version=#{Regexp.escape(version)}\.[0-9a-f]{3}/) end it "cc_version suffix is exactly 3 hex characters" do result = described_class.billing_header({}) match = result.match(/cc_version=[^;]+\.([0-9a-f]+);/) expect(match).not_to be_nil expect(match[1].length).to eq(3) end it "cch= is exactly 5 hex characters" do result = described_class.billing_header({}) match = result.match(/cch=([0-9a-f]+);/) expect(match).not_to be_nil expect(match[1].length).to eq(5) end it "cch is stable for the same input" do payload = { "model" => "claude-3-5-sonnet-20241022", "messages" => [] } result1 = described_class.billing_header(payload) result2 = described_class.billing_header(payload) cch1 = result1.match(/cch=([0-9a-f]+)/)[1] cch2 = result2.match(/cch=([0-9a-f]+)/)[1] expect(cch1).to eq(cch2) end it "cch differs for different inputs" do result1 = described_class.billing_header({ "a" => 1 }) result2 = described_class.billing_header({ "b" => 2 }) cch1 = result1.match(/cch=([0-9a-f]+)/)[1] cch2 = result2.match(/cch=([0-9a-f]+)/)[1] expect(cch1).not_to eq(cch2) end it "cc_version suffix varies between calls (random build hash)" do results = 10.times.map { described_class.billing_header({}) } suffixes = results.map { |r| r.match(/cc_version=[^;]+\.([0-9a-f]{3});/)[1] } expect(suffixes.uniq.size).to be > 1 end it "handles nil payload gracefully" do expect { described_class.billing_header(nil) }.not_to raise_error result = described_class.billing_header(nil) expect(result).to start_with("x-anthropic-billing-header:") end end describe ".apply_prefix" do it "prefixes a regular tool name with 'proxy_'" do expect(described_class.apply_prefix("grep")).to eq("proxy_grep") end it "does not prefix builtin tool names" do Dispatch::Adapter::Claude::Cloaking::BUILTINS.each do |builtin| expect(described_class.apply_prefix(builtin)).to eq(builtin) end end it "specifically does not prefix web_search" do expect(described_class.apply_prefix("web_search")).to eq("web_search") end it "specifically does not prefix code_execution" do expect(described_class.apply_prefix("code_execution")).to eq("code_execution") end it "is idempotent — does not double-prefix 'proxy_x'" do expect(described_class.apply_prefix("proxy_x")).to eq("proxy_x") end it "is idempotent for already-prefixed names" do expect(described_class.apply_prefix("proxy_grep")).to eq("proxy_grep") end it "handles mixed case builtin check case-insensitively" do expect(described_class.apply_prefix("Web_Search")).to eq("Web_Search") end it "handles case-insensitive already-prefixed check" do expect(described_class.apply_prefix("PROXY_foo")).to eq("PROXY_foo") end end describe ".strip_prefix" do it "strips 'proxy_' prefix from a prefixed name" do expect(described_class.strip_prefix("proxy_grep")).to eq("grep") end it "returns the name unchanged when not prefixed" do expect(described_class.strip_prefix("grep")).to eq("grep") end it "returns builtin names unchanged" do Dispatch::Adapter::Claude::Cloaking::BUILTINS.each do |builtin| expect(described_class.strip_prefix(builtin)).to eq(builtin) end end it "is case-insensitive for the prefix check" do expect(described_class.strip_prefix("PROXY_foo")).to eq("foo") end it "only strips the leading prefix, not occurrences elsewhere in the name" do expect(described_class.strip_prefix("proxy_proxy_foo")).to eq("proxy_foo") end end describe ".cloaking_user_id?" do it "returns true for a freshly generated cloaking user id" do id = described_class.generate_cloaking_user_id expect(described_class.cloaking_user_id?(id)).to be(true) end it "returns false for a plain string" do expect(described_class.cloaking_user_id?("custom")).to be(false) end it "returns false for nil" do expect(described_class.cloaking_user_id?(nil)).to be(false) end it "returns false for an integer" do expect(described_class.cloaking_user_id?(42)).to be(false) end it "returns false for a partial match" do expect(described_class.cloaking_user_id?("user_abc")).to be(false) end end describe ".generate_cloaking_user_id" do it "generates a string matching the USER_ID_REGEX" do id = described_class.generate_cloaking_user_id expect(id).to match(Dispatch::Adapter::Claude::Cloaking::USER_ID_REGEX) end it "generates unique ids on consecutive calls" do id1 = described_class.generate_cloaking_user_id id2 = described_class.generate_cloaking_user_id expect(id1).not_to eq(id2) end end describe ".resolve_user_id" do it "returns a generated id when provided is nil and is_oauth is true" do result = described_class.resolve_user_id(nil, true) expect(result).not_to be_nil expect(described_class.cloaking_user_id?(result)).to be(true) end it "returns a generated id when provided is 'custom' and is_oauth is true" do result = described_class.resolve_user_id("custom", true) expect(result).not_to eq("custom") expect(described_class.cloaking_user_id?(result)).to be(true) end it "returns the provided id when it already matches cloaking format and is_oauth is true" do id = described_class.generate_cloaking_user_id expect(described_class.resolve_user_id(id, true)).to eq(id) end it "returns 'custom' when is_oauth is false (passthrough)" do expect(described_class.resolve_user_id("custom", false)).to eq("custom") end it "returns nil when provided is nil and is_oauth is false" do expect(described_class.resolve_user_id(nil, false)).to be_nil end end describe ".build_system_blocks" do let(:opus_model) { "claude-opus-4-5" } let(:haiku_model) { "claude-3-5-haiku-20241022" } let(:user_text) { "You are a helpful assistant." } context "OAuth + non-haiku model (e.g. opus)" do it "returns 3 blocks: billing, agent, user" do blocks = described_class.build_system_blocks( user_text, is_oauth: true, model_id: opus_model ) expect(blocks.size).to eq(3) expect(blocks[0]["text"]).to start_with("x-anthropic-billing-header:") expect(blocks[1]["text"]).to include("Claude agent") expect(blocks[2]["text"]).to eq(user_text) end it "all blocks have type: 'text'" do blocks = described_class.build_system_blocks( user_text, is_oauth: true, model_id: opus_model ) blocks.each { |b| expect(b["type"]).to eq("text") } end end context "OAuth + haiku-3-5 model" do it "returns 2 blocks: billing, user — skips agent instruction" do blocks = described_class.build_system_blocks( user_text, is_oauth: true, model_id: haiku_model ) expect(blocks.size).to eq(2) expect(blocks[0]["text"]).to start_with("x-anthropic-billing-header:") expect(blocks[1]["text"]).to eq(user_text) end end context "API-key (non-OAuth)" do it "returns 1 block: just the user system" do blocks = described_class.build_system_blocks( user_text, is_oauth: false, model_id: opus_model ) expect(blocks).to be_a(Array) expect(blocks.size).to eq(1) expect(blocks[0]["text"]).to eq(user_text) end it "returns nil when user_system is nil and not OAuth" do result = described_class.build_system_blocks( nil, is_oauth: false, model_id: opus_model ) expect(result).to be_nil end end context "OAuth with nil user_system" do it "returns 2 blocks (billing + agent) when model is not haiku" do blocks = described_class.build_system_blocks( nil, is_oauth: true, model_id: opus_model ) expect(blocks.size).to eq(2) expect(blocks[0]["text"]).to start_with("x-anthropic-billing-header:") expect(blocks[1]["text"]).to include("Claude agent") end it "returns 1 block (billing only) for haiku model with nil user_system" do blocks = described_class.build_system_blocks( nil, is_oauth: true, model_id: haiku_model ) expect(blocks.size).to eq(1) expect(blocks[0]["text"]).to start_with("x-anthropic-billing-header:") end end context "cache_control" do it "attaches cache_control to the last block only" do cc = { "type" => "ephemeral" } blocks = described_class.build_system_blocks( user_text, is_oauth: true, model_id: opus_model, cache_control: cc ) expect(blocks.last["cache_control"]).to eq(cc) blocks[0..-2].each { |b| expect(b).not_to have_key("cache_control") } end it "does not mutate the original blocks when attaching cache_control" do cc = { "type" => "ephemeral" } original_blocks = [{ "type" => "text", "text" => "original" }] described_class.build_system_blocks( original_blocks, is_oauth: false, model_id: opus_model, cache_control: cc ) expect(original_blocks.last).not_to have_key("cache_control") end end context "pre-existing billing header short-circuit" do it "forwards user_system as-is when it already contains billing header" do existing_billing = "x-anthropic-billing-header: cc_version=2.0.0.aaa; cc_entrypoint=cli; cch=abcde;" user_blocks = [ { "type" => "text", "text" => existing_billing }, { "type" => "text", "text" => "System prompt." } ] blocks = described_class.build_system_blocks( user_blocks, is_oauth: true, model_id: opus_model ) expect(blocks.size).to eq(2) expect(blocks[0]["text"]).to eq(existing_billing) expect(blocks[1]["text"]).to eq("System prompt.") end end context "Array as user_system" do it "converts TextBlock structs to Hash blocks" do text_block = Dispatch::Adapter::TextBlock.new(text: "Structured system prompt") blocks = described_class.build_system_blocks( [text_block], is_oauth: false, model_id: opus_model ) expect(blocks.last["text"]).to eq("Structured system prompt") expect(blocks.last["type"]).to eq("text") end end end end