-
Notifications
You must be signed in to change notification settings - Fork 0
Add spec files for mars components #10
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
andresg4
merged 10 commits into
main
from
cursor/add-spec-files-for-mars-components-fee2
Dec 5, 2025
Merged
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
4e6dddb
feat: Add tests for Agent, Aggregator, Exit, and Gate
cursoragent bec384c
Refactor: Improve Mars::Agent spec and chat delegation
cursoragent 9606891
Refactor: Improve test clarity and remove redundant code
cursoragent 0f2df69
fix linters and add environment.json for cursor background agents
santiagodiaz bfd94bf
update rubocop yml
santiagodiaz d46aaf0
update gitignore
santiagodiaz 34126cb
update agent_spec
santiagodiaz 8ceb7cd
update specs to reduce examples
santiagodiaz ba81f0c
remove test file
santiagodiaz 25f4166
fix spec
santiagodiaz File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
RSpec/VerifiedDoubleReference: Enabled: falsewas added because of the instance_double line for skipping this rubocop offense:
I think it depends if we want to keep that test that is mocking the rubyllm chat in
agent_spec.rb