summaryrefslogtreecommitdiffhomepage
path: root/spec/dispatch/tools/definition_spec.rb
diff options
context:
space:
mode:
Diffstat (limited to 'spec/dispatch/tools/definition_spec.rb')
-rw-r--r--spec/dispatch/tools/definition_spec.rb231
1 files changed, 231 insertions, 0 deletions
diff --git a/spec/dispatch/tools/definition_spec.rb b/spec/dispatch/tools/definition_spec.rb
new file mode 100644
index 0000000..c9640e2
--- /dev/null
+++ b/spec/dispatch/tools/definition_spec.rb
@@ -0,0 +1,231 @@
+# frozen_string_literal: true
+
+RSpec.describe Dispatch::Tools::Definition do
+ let(:parameters_schema) do
+ {
+ type: "object",
+ properties: {
+ path: { type: "string", description: "File path" },
+ start_line: { type: "integer", description: "Start line (0-based)" },
+ end_line: { type: "integer", description: "End line (0-based, -1 for EOF)" }
+ },
+ required: ["path"]
+ }
+ end
+
+ let(:tool) do
+ described_class.new(
+ name: "read_file",
+ description: "Read the contents of a file",
+ parameters: parameters_schema
+ ) do |params, _context|
+ Dispatch::Tools::Result.success(output: "contents of #{params[:path]}")
+ end
+ end
+
+ describe "#initialize" do
+ it "creates a definition with all attributes" do
+ expect(tool.name).to eq("read_file")
+ expect(tool.description).to eq("Read the contents of a file")
+ expect(tool.parameters).to eq(parameters_schema)
+ end
+
+ it "has read-only attributes" do
+ expect(tool).to respond_to(:name)
+ expect(tool).to respond_to(:description)
+ expect(tool).to respond_to(:parameters)
+ expect(tool).not_to respond_to(:name=)
+ expect(tool).not_to respond_to(:description=)
+ expect(tool).not_to respond_to(:parameters=)
+ end
+ end
+
+ describe "#call" do
+ it "executes the block and returns a Result" do
+ result = tool.call({ path: "src/main.rb" })
+
+ expect(result).to be_a(Dispatch::Tools::Result)
+ expect(result.success?).to be true
+ expect(result.output).to eq("contents of src/main.rb")
+ end
+
+ it "passes context to the block" do
+ context_tool = described_class.new(
+ name: "context_test",
+ description: "Test context passing",
+ parameters: { type: "object", properties: {}, required: [] }
+ ) do |_params, context|
+ Dispatch::Tools::Result.success(output: "worktree: #{context[:worktree_path]}")
+ end
+
+ result = context_tool.call({}, context: { worktree_path: "/path/to/worktree" })
+
+ expect(result.output).to eq("worktree: /path/to/worktree")
+ end
+
+ it "defaults context to an empty hash" do
+ context_tool = described_class.new(
+ name: "context_default",
+ description: "Test default context",
+ parameters: { type: "object", properties: {}, required: [] }
+ ) do |_params, context|
+ Dispatch::Tools::Result.success(output: "context is #{context.class}")
+ end
+
+ result = context_tool.call({})
+
+ expect(result.output).to eq("context is Hash")
+ end
+
+ it "never raises when the block raises an exception" do
+ failing_tool = described_class.new(
+ name: "failing",
+ description: "A tool that fails",
+ parameters: { type: "object", properties: {}, required: [] }
+ ) do |_params, _context|
+ raise StandardError, "something broke"
+ end
+
+ result = nil
+ expect { result = failing_tool.call({}) }.not_to raise_error
+
+ expect(result.failure?).to be true
+ expect(result.error).to include("something broke")
+ end
+
+ it "catches non-StandardError exceptions from the block" do
+ failing_tool = described_class.new(
+ name: "type_error_tool",
+ description: "Raises TypeError",
+ parameters: { type: "object", properties: {}, required: [] }
+ ) do |_params, _context|
+ raise TypeError, "wrong type"
+ end
+
+ result = nil
+ expect { result = failing_tool.call({}) }.not_to raise_error
+
+ expect(result.failure?).to be true
+ expect(result.error).to include("wrong type")
+ end
+
+ it "returns Result.failure when params fail validation" do
+ result = tool.call({})
+
+ expect(result.failure?).to be true
+ expect(result.error).to be_a(String)
+ expect(result.error).not_to be_empty
+ end
+
+ it "symbolizes string-keyed params before passing to the block" do
+ received_params = nil
+
+ spy_tool = described_class.new(
+ name: "spy",
+ description: "Records params",
+ parameters: {
+ type: "object",
+ properties: {
+ name: { type: "string" }
+ },
+ required: []
+ }
+ ) do |params, _context|
+ received_params = params
+ Dispatch::Tools::Result.success(output: "ok")
+ end
+
+ spy_tool.call({ "name" => "Alice" })
+
+ expect(received_params).to have_key(:name)
+ expect(received_params).not_to have_key("name")
+ expect(received_params[:name]).to eq("Alice")
+ end
+
+ it "handles deeply nested string keys by symbolizing top-level keys" do
+ received_params = nil
+
+ spy_tool = described_class.new(
+ name: "deep_spy",
+ description: "Records params",
+ parameters: {
+ type: "object",
+ properties: {
+ options: { type: "object" }
+ },
+ required: []
+ }
+ ) do |params, _context|
+ received_params = params
+ Dispatch::Tools::Result.success(output: "ok")
+ end
+
+ spy_tool.call({ "options" => { "nested" => true } })
+
+ expect(received_params).to have_key(:options)
+ end
+ end
+
+ describe "#to_h" do
+ it "returns a hash with name, description, and parameters" do
+ hash = tool.to_h
+
+ expect(hash).to eq({
+ name: "read_file",
+ description: "Read the contents of a file",
+ parameters: parameters_schema
+ })
+ end
+
+ it "returns a plain hash, not a struct" do
+ expect(tool.to_h).to be_a(Hash)
+ end
+ end
+
+ describe "#to_tool_definition" do
+ it "returns the same shape as to_h" do
+ expect(tool.to_tool_definition).to eq(tool.to_h)
+ end
+
+ it "returns a plain hash" do
+ expect(tool.to_tool_definition).to be_a(Hash)
+ end
+ end
+
+ describe "#validate_params" do
+ it "returns [true, []] for valid params" do
+ valid, errors = tool.validate_params({ path: "src/main.rb" })
+
+ expect(valid).to be true
+ expect(errors).to eq([])
+ end
+
+ it "returns [true, []] for valid params with optional fields" do
+ valid, errors = tool.validate_params({ path: "src/main.rb", start_line: 0, end_line: 10 })
+
+ expect(valid).to be true
+ expect(errors).to eq([])
+ end
+
+ it "returns [false, errors] when required params are missing" do
+ valid, errors = tool.validate_params({})
+
+ expect(valid).to be false
+ expect(errors).not_to be_empty
+ end
+
+ it "returns [false, errors] when params have wrong types" do
+ valid, errors = tool.validate_params({ path: 123 })
+
+ expect(valid).to be false
+ expect(errors).not_to be_empty
+ end
+
+ it "handles string-keyed params for validation" do
+ valid, errors = tool.validate_params({ "path" => "src/main.rb" })
+
+ expect(valid).to be true
+ expect(errors).to eq([])
+ end
+ end
+end