diff --git a/.rubocop.yml b/.rubocop.yml index 5c614cd..af86687 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -17,3 +17,12 @@ Style/StringLiterals: Style/StringLiteralsInInterpolation: EnforcedStyle: double_quotes + +RSpec/ExampleLength: + Enabled: false + +RSpec/MultipleExpectations: + Enabled: false + +RSpec/VerifiedDoubleReference: + Enabled: false diff --git a/spec/mars/agent_spec.rb b/spec/mars/agent_spec.rb new file mode 100644 index 0000000..89ce677 --- /dev/null +++ b/spec/mars/agent_spec.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +RSpec.describe Mars::Agent do + describe "#run" do + let(:agent) { described_class.new(name: "TestAgent", options: { model: "test-model" }) } + let(:mock_chat_instance) do + instance_double("RubyLLM::Chat").tap do |mock| + allow(mock).to receive_messages(with_tools: mock, with_schema: mock, ask: nil) + end + end + let(:mock_chat_class) { class_double("RubyLLM::Chat", new: mock_chat_instance) } + + before do + stub_const("RubyLLM::Chat", mock_chat_class) + end + + it "initializes RubyLLM::Chat with provided options" do + agent.run("test input") + + expect(mock_chat_class).to have_received(:new).with(model: "test-model") + end + + it "configures chat with tools if provided" do + tools = [proc { "tool" }] + agent_with_tools = described_class.new(name: "TestAgent", tools: tools) + agent_with_tools.run("test input") + + expect(mock_chat_instance).to have_received(:with_tools).with(tools) + end + + it "configures chat with schema if provided" do + schema = { type: "object" } + agent_with_schema = described_class.new(name: "TestAgent", schema: schema) + + agent_with_schema.run("test input") + expect(mock_chat_instance).to have_received(:with_schema).with(schema) + end + end +end diff --git a/spec/mars/aggregator_spec.rb b/spec/mars/aggregator_spec.rb new file mode 100644 index 0000000..1c2b6c1 --- /dev/null +++ b/spec/mars/aggregator_spec.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +RSpec.describe Mars::Aggregator do + describe "#run" do + let(:aggregator) { described_class.new } + + context "when called without a block" do + it "joins inputs with newlines" do + inputs = %w[first second third] + result = aggregator.run(inputs) + expect(result).to eq("first\nsecond\nthird") + end + + it "handles empty array" do + result = aggregator.run([]) + expect(result).to eq("") + end + + it "handles single input" do + result = aggregator.run(["single"]) + expect(result).to eq("single") + end + + it "handles numeric inputs" do + inputs = [1, 2, 3] + result = aggregator.run(inputs) + expect(result).to eq("1\n2\n3") + end + end + + context "when called with a block" do + it "executes the block and returns its value" do + result = aggregator.run(["ignored"]) { "block result" } + expect(result).to eq("block result") + end + + it "ignores the inputs when block is given" do + inputs = %w[first second] + result = aggregator.run(inputs) { "custom aggregation" } + expect(result).to eq("custom aggregation") + end + + it "can perform custom aggregation logic" do + inputs = [1, 2, 3, 4, 5] + result = aggregator.run(inputs) { inputs.sum } + expect(result).to eq(15) + end + end + end +end diff --git a/spec/mars/gate_spec.rb b/spec/mars/gate_spec.rb new file mode 100644 index 0000000..aac613e --- /dev/null +++ b/spec/mars/gate_spec.rb @@ -0,0 +1,125 @@ +# frozen_string_literal: true + +RSpec.describe Mars::Gate do + describe "#run" do + context "with simple boolean condition" do + let(:condition) { ->(input) { input > 5 } } + let(:true_branch) { instance_spy(Mars::Runnable) } + let(:false_branch) { instance_spy(Mars::Runnable) } + let(:branches) { { true => true_branch, false => false_branch } } + let(:gate) { described_class.new(name: "TestGate", condition: condition, branches: branches) } + + it "executes true branch when condition is true" do + allow(true_branch).to receive(:run).with(10).and_return("true result") + + result = gate.run(10) + + expect(result).to eq("true result") + expect(true_branch).to have_received(:run).with(10) + end + + it "executes false branch when condition is false" do + allow(false_branch).to receive(:run).with(3).and_return("false result") + + result = gate.run(3) + + expect(result).to eq("false result") + expect(false_branch).to have_received(:run).with(3) + end + end + + context "with string-based condition" do + let(:condition) { ->(input) { input.length > 5 ? "long" : "short" } } + let(:long_branch) { instance_spy(Mars::Runnable) } + let(:short_branch) { instance_spy(Mars::Runnable) } + let(:branches) { { "long" => long_branch, "short" => short_branch } } + let(:gate) { described_class.new(name: "LengthGate", condition: condition, branches: branches) } + + it "routes to correct branch based on string result" do + allow(long_branch).to receive(:run).with("longstring").and_return("long result") + + result = gate.run("longstring") + + expect(result).to eq("long result") + expect(long_branch).to have_received(:run).with("longstring") + end + + it "routes to short branch for short strings" do + allow(short_branch).to receive(:run).with("hi").and_return("short result") + + result = gate.run("hi") + + expect(result).to eq("short result") + expect(short_branch).to have_received(:run).with("hi") + end + end + + context "with missing branch" do + let(:condition) { ->(input) { input > 5 ? "high" : "low" } } + let(:high_branch) { instance_spy(Mars::Runnable) } + let(:branches) { { "high" => high_branch } } + let(:gate) { described_class.new(name: "TestGate", condition: condition, branches: branches) } + + it "executes defined branch when condition matches" do + allow(high_branch).to receive(:run).with(10).and_return("high result") + + result = gate.run(10) + + expect(result).to eq("high result") + expect(high_branch).to have_received(:run).with(10) + end + + it "raises an error when branch is not defined" do + # For input 3, condition returns "low" which is not in branches + expect { gate.run(3) }.to raise_error(NoMethodError) + end + end + + context "with complex condition logic" do + let(:condition) do + lambda do |input| + case input + when 0..10 then "low" + when 11..50 then "medium" + else "high" + end + end + end + + let(:low_branch) { instance_spy(Mars::Runnable) } + let(:medium_branch) { instance_spy(Mars::Runnable) } + let(:high_branch) { instance_spy(Mars::Runnable) } + let(:branches) { { "low" => low_branch, "medium" => medium_branch, "high" => high_branch } } + + it "routes to low branch" do + gate = described_class.new(name: "RangeGate", condition: condition, branches: branches) + allow(low_branch).to receive(:run).with(5).and_return("low result") + + result = gate.run(5) + + expect(result).to eq("low result") + expect(low_branch).to have_received(:run).with(5) + end + + it "routes to medium branch" do + gate = described_class.new(name: "RangeGate", condition: condition, branches: branches) + allow(medium_branch).to receive(:run).with(25).and_return("medium result") + + result = gate.run(25) + + expect(result).to eq("medium result") + expect(medium_branch).to have_received(:run).with(25) + end + + it "routes to high branch" do + gate = described_class.new(name: "RangeGate", condition: condition, branches: branches) + allow(high_branch).to receive(:run).with(100).and_return("high result") + + result = gate.run(100) + + expect(result).to eq("high result") + expect(high_branch).to have_received(:run).with(100) + end + end + end +end