summaryrefslogtreecommitdiffhomepage
path: root/spec
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-04-28 14:11:16 +0900
committerAdam Malczewski <[email protected]>2026-04-28 14:11:16 +0900
commit07277435c0688ad9f5fa682633b86b99ef5bb854 (patch)
tree3e650e97bcbd229f942330542a333dcad1844542 /spec
downloaddispatch-adapter-interface-07277435c0688ad9f5fa682633b86b99ef5bb854.tar.gz
dispatch-adapter-interface-07277435c0688ad9f5fa682633b86b99ef5bb854.zip
update
Diffstat (limited to 'spec')
-rw-r--r--spec/dispatch/adapter/interface/base_spec.rb41
-rw-r--r--spec/dispatch/adapter/interface/errors_spec.rb69
-rw-r--r--spec/dispatch/adapter/interface/structs_spec.rb211
-rw-r--r--spec/dispatch/adapter/interface_spec.rb33
-rw-r--r--spec/spec_helper.rb12
5 files changed, 366 insertions, 0 deletions
diff --git a/spec/dispatch/adapter/interface/base_spec.rb b/spec/dispatch/adapter/interface/base_spec.rb
new file mode 100644
index 0000000..8c3a279
--- /dev/null
+++ b/spec/dispatch/adapter/interface/base_spec.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+RSpec.describe Dispatch::Adapter::Base do
+ subject(:base) { described_class.new }
+
+ describe "#chat" do
+ it "raises NotImplementedError" do
+ expect { base.chat([]) }.to raise_error(NotImplementedError, /chat must be implemented/)
+ end
+ end
+
+ describe "#model_name" do
+ it "raises NotImplementedError" do
+ expect { base.model_name }.to raise_error(NotImplementedError, /model_name must be implemented/)
+ end
+ end
+
+ describe "#count_tokens" do
+ it "returns -1" do
+ expect(base.count_tokens([])).to eq(-1)
+ end
+ end
+
+ describe "#list_models" do
+ it "raises NotImplementedError" do
+ expect { base.list_models }.to raise_error(NotImplementedError, /list_models must be implemented/)
+ end
+ end
+
+ describe "#provider_name" do
+ it "returns the class name" do
+ expect(base.provider_name).to eq("Dispatch::Adapter::Base")
+ end
+ end
+
+ describe "#max_context_tokens" do
+ it "returns nil" do
+ expect(base.max_context_tokens).to be_nil
+ end
+ end
+end
diff --git a/spec/dispatch/adapter/interface/errors_spec.rb b/spec/dispatch/adapter/interface/errors_spec.rb
new file mode 100644
index 0000000..906a4c2
--- /dev/null
+++ b/spec/dispatch/adapter/interface/errors_spec.rb
@@ -0,0 +1,69 @@
+# frozen_string_literal: true
+
+RSpec.describe Dispatch::Adapter::Error do
+ it "carries message, status_code, and provider" do
+ error = described_class.new("test error", status_code: 500, provider: "TestProvider")
+ expect(error.message).to eq("test error")
+ expect(error.status_code).to eq(500)
+ expect(error.provider).to eq("TestProvider")
+ end
+
+ it "defaults status_code and provider to nil" do
+ error = described_class.new("simple error")
+ expect(error.status_code).to be_nil
+ expect(error.provider).to be_nil
+ end
+
+ it "inherits from StandardError" do
+ expect(described_class.ancestors).to include(StandardError)
+ end
+
+ it "can be rescued as StandardError" do
+ expect do
+ raise described_class, "test"
+ end.to raise_error(StandardError)
+ end
+end
+
+RSpec.describe Dispatch::Adapter::AuthenticationError do
+ it "inherits from Error" do
+ expect(described_class.ancestors).to include(Dispatch::Adapter::Error)
+ end
+end
+
+RSpec.describe Dispatch::Adapter::RateLimitError do
+ it "carries retry_after" do
+ error = described_class.new("rate limited", status_code: 429, provider: "Test", retry_after: 30)
+ expect(error.retry_after).to eq(30)
+ expect(error.status_code).to eq(429)
+ end
+
+ it "defaults retry_after to nil" do
+ error = described_class.new("rate limited")
+ expect(error.retry_after).to be_nil
+ end
+
+ it "is rescuable as Dispatch::Adapter::Error" do
+ expect do
+ raise described_class, "rate limited"
+ end.to raise_error(Dispatch::Adapter::Error)
+ end
+end
+
+RSpec.describe Dispatch::Adapter::ServerError do
+ it "inherits from Error" do
+ expect(described_class.ancestors).to include(Dispatch::Adapter::Error)
+ end
+end
+
+RSpec.describe Dispatch::Adapter::RequestError do
+ it "inherits from Error" do
+ expect(described_class.ancestors).to include(Dispatch::Adapter::Error)
+ end
+end
+
+RSpec.describe Dispatch::Adapter::ConnectionError do
+ it "inherits from Error" do
+ expect(described_class.ancestors).to include(Dispatch::Adapter::Error)
+ end
+end
diff --git a/spec/dispatch/adapter/interface/structs_spec.rb b/spec/dispatch/adapter/interface/structs_spec.rb
new file mode 100644
index 0000000..ef8ec73
--- /dev/null
+++ b/spec/dispatch/adapter/interface/structs_spec.rb
@@ -0,0 +1,211 @@
+# frozen_string_literal: true
+
+RSpec.describe Dispatch::Adapter do
+ describe "Message" do
+ it "creates with keyword args" do
+ msg = Dispatch::Adapter::Message.new(role: "user", content: "Hello")
+ expect(msg.role).to eq("user")
+ expect(msg.content).to eq("Hello")
+ end
+
+ it "accepts array content" do
+ blocks = [Dispatch::Adapter::TextBlock.new(text: "hi")]
+ msg = Dispatch::Adapter::Message.new(role: "user", content: blocks)
+ expect(msg.content).to be_an(Array)
+ expect(msg.content.first.text).to eq("hi")
+ end
+ end
+
+ describe "TextBlock" do
+ it "defaults type to 'text'" do
+ block = Dispatch::Adapter::TextBlock.new(text: "hello")
+ expect(block.type).to eq("text")
+ expect(block.text).to eq("hello")
+ end
+ end
+
+ describe "ImageBlock" do
+ it "defaults type to 'image'" do
+ block = Dispatch::Adapter::ImageBlock.new(source: "data:image/png;base64,abc", media_type: "image/png")
+ expect(block.type).to eq("image")
+ expect(block.source).to eq("data:image/png;base64,abc")
+ expect(block.media_type).to eq("image/png")
+ end
+ end
+
+ describe "ToolUseBlock" do
+ it "defaults type to 'tool_use'" do
+ block = Dispatch::Adapter::ToolUseBlock.new(id: "call_1", name: "get_weather", arguments: { "city" => "NYC" })
+ expect(block.type).to eq("tool_use")
+ expect(block.id).to eq("call_1")
+ expect(block.name).to eq("get_weather")
+ expect(block.arguments).to eq({ "city" => "NYC" })
+ end
+ end
+
+ describe "ToolResultBlock" do
+ it "defaults type to 'tool_result' and is_error to false" do
+ block = Dispatch::Adapter::ToolResultBlock.new(tool_use_id: "call_1", content: "72F")
+ expect(block.type).to eq("tool_result")
+ expect(block.tool_use_id).to eq("call_1")
+ expect(block.content).to eq("72F")
+ expect(block.is_error).to be(false)
+ end
+
+ it "accepts is_error flag" do
+ block = Dispatch::Adapter::ToolResultBlock.new(tool_use_id: "call_1", content: "Error", is_error: true)
+ expect(block.is_error).to be(true)
+ end
+ end
+
+ describe "ToolDefinition" do
+ it "creates with keyword args" do
+ td = Dispatch::Adapter::ToolDefinition.new(
+ name: "search",
+ description: "Search the web",
+ parameters: { "type" => "object", "properties" => {} }
+ )
+ expect(td.name).to eq("search")
+ expect(td.description).to eq("Search the web")
+ expect(td.parameters).to be_a(Hash)
+ end
+ end
+
+ describe "Response" do
+ it "creates with defaults" do
+ usage = Dispatch::Adapter::Usage.new(input_tokens: 10, output_tokens: 20)
+ resp = Dispatch::Adapter::Response.new(model: "gpt-4", stop_reason: :end_turn, usage: usage)
+ expect(resp.content).to be_nil
+ expect(resp.tool_calls).to eq([])
+ expect(resp.model).to eq("gpt-4")
+ expect(resp.stop_reason).to eq(:end_turn)
+ expect(resp.usage).to eq(usage)
+ end
+
+ it "creates with all fields" do
+ usage = Dispatch::Adapter::Usage.new(input_tokens: 10, output_tokens: 20)
+ tool_call = Dispatch::Adapter::ToolUseBlock.new(id: "1", name: "test", arguments: {})
+ resp = Dispatch::Adapter::Response.new(
+ content: "Hello",
+ tool_calls: [tool_call],
+ model: "gpt-4",
+ stop_reason: :tool_use,
+ usage: usage
+ )
+ expect(resp.content).to eq("Hello")
+ expect(resp.tool_calls.size).to eq(1)
+ end
+ end
+
+ describe "Usage" do
+ it "defaults cache tokens to 0" do
+ usage = Dispatch::Adapter::Usage.new(input_tokens: 100, output_tokens: 50)
+ expect(usage.cache_read_tokens).to eq(0)
+ expect(usage.cache_creation_tokens).to eq(0)
+ end
+
+ it "accepts cache tokens" do
+ usage = Dispatch::Adapter::Usage.new(
+ input_tokens: 100,
+ output_tokens: 50,
+ cache_read_tokens: 10,
+ cache_creation_tokens: 5
+ )
+ expect(usage.cache_read_tokens).to eq(10)
+ expect(usage.cache_creation_tokens).to eq(5)
+ end
+ end
+
+ describe "StreamDelta" do
+ it "creates a text_delta" do
+ delta = Dispatch::Adapter::StreamDelta.new(type: :text_delta, text: "Hello")
+ expect(delta.type).to eq(:text_delta)
+ expect(delta.text).to eq("Hello")
+ expect(delta.tool_call_id).to be_nil
+ end
+
+ it "creates a tool_use_start" do
+ delta = Dispatch::Adapter::StreamDelta.new(type: :tool_use_start, tool_call_id: "1", tool_name: "search")
+ expect(delta.type).to eq(:tool_use_start)
+ expect(delta.tool_call_id).to eq("1")
+ expect(delta.tool_name).to eq("search")
+ end
+
+ it "creates a tool_use_delta" do
+ delta = Dispatch::Adapter::StreamDelta.new(type: :tool_use_delta, tool_call_id: "1", argument_delta: '{"q":')
+ expect(delta.type).to eq(:tool_use_delta)
+ expect(delta.argument_delta).to eq('{"q":')
+ end
+ end
+
+ describe "ModelInfo" do
+ it "creates with all fields" do
+ info = Dispatch::Adapter::ModelInfo.new(
+ id: "gpt-4",
+ name: "GPT-4",
+ max_context_tokens: 8192,
+ supports_vision: false,
+ supports_tool_use: true,
+ supports_streaming: true
+ )
+ expect(info.id).to eq("gpt-4")
+ expect(info.name).to eq("GPT-4")
+ expect(info.max_context_tokens).to eq(8192)
+ expect(info.supports_vision).to be(false)
+ expect(info.supports_tool_use).to be(true)
+ expect(info.supports_streaming).to be(true)
+ expect(info.premium_request_multiplier).to be_nil
+ end
+
+ it "accepts premium_request_multiplier" do
+ info = Dispatch::Adapter::ModelInfo.new(
+ id: "o3",
+ name: "o3",
+ max_context_tokens: 200_000,
+ supports_vision: false,
+ supports_tool_use: true,
+ supports_streaming: true,
+ premium_request_multiplier: 30.0
+ )
+ expect(info.premium_request_multiplier).to eq(30.0)
+ end
+
+ it "defaults premium_request_multiplier to nil" do
+ info = Dispatch::Adapter::ModelInfo.new(
+ id: "gpt-4.1-nano",
+ name: "GPT 4.1 Nano",
+ max_context_tokens: 1_047_576,
+ supports_vision: false,
+ supports_tool_use: true,
+ supports_streaming: true
+ )
+ expect(info.premium_request_multiplier).to be_nil
+ end
+ end
+
+ describe "Struct equality" do
+ it "considers structs with same values equal" do
+ a = Dispatch::Adapter::Message.new(role: "user", content: "hello")
+ b = Dispatch::Adapter::Message.new(role: "user", content: "hello")
+ expect(a).to eq(b)
+ end
+
+ it "considers structs with different values not equal" do
+ a = Dispatch::Adapter::Message.new(role: "user", content: "hello")
+ b = Dispatch::Adapter::Message.new(role: "user", content: "goodbye")
+ expect(a).not_to eq(b)
+ end
+
+ it "Usage structs are equal with same tokens" do
+ a = Dispatch::Adapter::Usage.new(input_tokens: 10, output_tokens: 20)
+ b = Dispatch::Adapter::Usage.new(input_tokens: 10, output_tokens: 20)
+ expect(a).to eq(b)
+ end
+
+ it "ToolUseBlock structs are equal with same fields" do
+ a = Dispatch::Adapter::ToolUseBlock.new(id: "1", name: "test", arguments: { "k" => "v" })
+ b = Dispatch::Adapter::ToolUseBlock.new(id: "1", name: "test", arguments: { "k" => "v" })
+ expect(a).to eq(b)
+ end
+ end
+end
diff --git a/spec/dispatch/adapter/interface_spec.rb b/spec/dispatch/adapter/interface_spec.rb
new file mode 100644
index 0000000..a62ed81
--- /dev/null
+++ b/spec/dispatch/adapter/interface_spec.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+RSpec.describe Dispatch::Adapter::Interface do
+ it "has a version number" do
+ expect(Dispatch::Adapter::Interface::VERSION).not_to be_nil
+ end
+
+ it "exposes the Base class" do
+ expect(Dispatch::Adapter::Base).to be_a(Class)
+ end
+
+ it "exposes error classes" do
+ expect(Dispatch::Adapter::Error).to be < StandardError
+ expect(Dispatch::Adapter::AuthenticationError).to be < Dispatch::Adapter::Error
+ expect(Dispatch::Adapter::RateLimitError).to be < Dispatch::Adapter::Error
+ expect(Dispatch::Adapter::ServerError).to be < Dispatch::Adapter::Error
+ expect(Dispatch::Adapter::RequestError).to be < Dispatch::Adapter::Error
+ expect(Dispatch::Adapter::ConnectionError).to be < Dispatch::Adapter::Error
+ end
+
+ it "exposes data structs" do
+ expect(Dispatch::Adapter::Message).to be_a(Class)
+ expect(Dispatch::Adapter::TextBlock).to be_a(Class)
+ expect(Dispatch::Adapter::ImageBlock).to be_a(Class)
+ expect(Dispatch::Adapter::ToolUseBlock).to be_a(Class)
+ expect(Dispatch::Adapter::ToolResultBlock).to be_a(Class)
+ expect(Dispatch::Adapter::ToolDefinition).to be_a(Class)
+ expect(Dispatch::Adapter::Response).to be_a(Class)
+ expect(Dispatch::Adapter::Usage).to be_a(Class)
+ expect(Dispatch::Adapter::StreamDelta).to be_a(Class)
+ expect(Dispatch::Adapter::ModelInfo).to be_a(Class)
+ end
+end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
new file mode 100644
index 0000000..8aa8377
--- /dev/null
+++ b/spec/spec_helper.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+require "dispatch/adapter/interface"
+
+RSpec.configure do |config|
+ config.example_status_persistence_file_path = ".rspec_status"
+ config.disable_monkey_patching!
+
+ config.expect_with :rspec do |c|
+ c.syntax = :expect
+ end
+end