# frozen_string_literal: true require "webmock/rspec" USAGE_FIXTURES_DIR = File.expand_path("../../../fixtures/responses", __dir__) RSpec.describe Dispatch::Adapter::Claude, "#usage_report (fixture-based)" do let(:oauth_key) { "sk-ant-oat-test-token" } let(:base_url) { "https://api.anthropic.com" } let(:usage_url) { "#{base_url}/api/oauth/usage" } let(:profile_url) { "#{base_url}/api/oauth/profile" } subject(:adapter) do described_class.new( model: "claude-sonnet-4-6", api_key: oauth_key, base_url: base_url, is_oauth: true ) end before { WebMock.disable_net_connect! } after { WebMock.reset! } def load_fixture(filename) JSON.parse(File.read(File.join(USAGE_FIXTURES_DIR, filename))) end def stub_usage(body) stub_request(:get, usage_url) .to_return( status: 200, body: JSON.generate(body), headers: { "Content-Type" => "application/json" } ) end def stub_profile(body) stub_request(:get, profile_url) .to_return( status: 200, body: JSON.generate(body), headers: { "Content-Type" => "application/json" } ) end # ── Scenario 1: full payload (4 buckets) via fixture ───────────────────── describe "full payload via oauth-usage-full.json" do before do stub_usage(load_fixture("oauth-usage-full.json")) stub_profile(load_fixture("oauth-profile.json")) end it "returns a UsageReport" do expect(adapter.usage_report).to be_a(Dispatch::Adapter::UsageReport) end it "has 4 UsageLimitEntry rows" do expect(adapter.usage_report.limits.size).to eq(4) end it "all rows are UsageLimitEntry objects" do expect(adapter.usage_report.limits).to all(be_a(Dispatch::Adapter::UsageLimitEntry)) end it "seven_day_sonnet is :exhausted (100%)" do entry = adapter.usage_report.limits.find { |e| e.id == "anthropic:7d:sonnet" } expect(entry.status).to eq(:exhausted) end it "each window carries the correct duration_ms" do report = adapter.usage_report five_h = report.limits.find { |e| e.id == "anthropic:5h" } seven_d = report.limits.find { |e| e.id == "anthropic:7d" } expect(five_h.window.duration_ms).to eq(18_000_000) expect(seven_d.window.duration_ms).to eq(604_800_000) end end # ── Scenario 2: partial payload (only 2 buckets) via fixture ───────────── describe "partial payload via oauth-usage-partial.json" do before do stub_usage(load_fixture("oauth-usage-partial.json")) stub_profile(load_fixture("oauth-profile.json")) end it "returns a UsageReport (not nil)" do expect(adapter.usage_report).to be_a(Dispatch::Adapter::UsageReport) end it "has exactly 2 UsageLimitEntry rows" do expect(adapter.usage_report.limits.size).to eq(2) end it "includes five_hour entry" do entry_ids = adapter.usage_report.limits.map(&:id) expect(entry_ids).to include("anthropic:5h") end it "includes seven_day entry" do entry_ids = adapter.usage_report.limits.map(&:id) expect(entry_ids).to include("anthropic:7d") end it "does NOT include seven_day_opus or seven_day_sonnet entries" do entry_ids = adapter.usage_report.limits.map(&:id) expect(entry_ids).not_to include("anthropic:7d:opus", "anthropic:7d:sonnet") end it "five_hour status is :ok (33%)" do entry = adapter.usage_report.limits.find { |e| e.id == "anthropic:5h" } expect(entry.status).to eq(:ok) end it "seven_day status is :ok (71%)" do entry = adapter.usage_report.limits.find { |e| e.id == "anthropic:7d" } expect(entry.status).to eq(:ok) end it "five_hour window.duration_ms is 18_000_000" do entry = adapter.usage_report.limits.find { |e| e.id == "anthropic:5h" } expect(entry.window.duration_ms).to eq(18_000_000) end it "seven_day window.duration_ms is 604_800_000" do entry = adapter.usage_report.limits.find { |e| e.id == "anthropic:7d" } expect(entry.window.duration_ms).to eq(604_800_000) end it "metadata.email is populated from profile fixture" do expect(adapter.usage_report.metadata[:email]).to eq("user@example.com") end end # ── Scenario 3: empty payload → nil (via fixture reference) ────────────── describe "empty payload fixture scenario" do before { stub_usage({}) } it "returns nil when no recognised buckets are present" do expect(adapter.usage_report).to be_nil end end # ── Scenario 6: profile fallback via oauth-profile.json ────────────────── describe "profile fallback via oauth-profile.json" do before do stub_usage(load_fixture("oauth-usage-partial.json")) stub_profile(load_fixture("oauth-profile.json")) end it "populates metadata[:email] from oauth-profile.json" do expect(adapter.usage_report.metadata[:email]).to eq("user@example.com") end it "populates metadata[:account_id] from oauth-profile.json" do expect(adapter.usage_report.metadata[:account_id]).to eq("acct_abc123") end end # ── Scenario 7: status thresholds (95% and 100%) ───────────────────────── describe "status thresholds" do before do stub_profile("email" => "u@example.com", "account_id" => "acct_1") end [ [95.0, :warning], [100.0, :exhausted] ].each do |utilization, expected_status| it "#{utilization}% → #{expected_status}" do stub_usage({ "five_hour" => { "utilization" => utilization, "resets_at" => nil } }) entry = adapter.usage_report.limits.first expect(entry.status).to eq(expected_status) end end end end