Skip to content
Open
9 changes: 9 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,12 @@ Style/StringLiterals:

Style/StringLiteralsInInterpolation:
EnforcedStyle: double_quotes

RSpec/ExampleLength:
Enabled: false

RSpec/MultipleExpectations:
Enabled: false

RSpec/VerifiedDoubleReference:
Enabled: false
39 changes: 39 additions & 0 deletions spec/mars/agent_spec.rb
Original file line number Diff line number Diff line change
@@ -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
50 changes: 50 additions & 0 deletions spec/mars/aggregator_spec.rb
Original file line number Diff line number Diff line change
@@ -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
125 changes: 125 additions & 0 deletions spec/mars/gate_spec.rb
Original file line number Diff line number Diff line change
@@ -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