Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 19 additions & 17 deletions test/examples/functional/roast_examples_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -465,20 +465,22 @@ class RoastExamplesTest < FunctionalTest
end

test "simple_agent.rb workflow runs successfully" do
use_command_runner_fixture(
"agent_transcripts/simple_agent",
expected_args: [
"claude",
"-p",
"--verbose",
"--output-format",
"stream-json",
"--model",
"haiku",
"--append-system-prompt",
"Always respond in haiku form",
],
expected_stdin_content: "What is the world's largest lake?",
use_command_runner_fixtures(
{
fixture: "agent_transcripts/simple_agent",
expected_args: [
"claude",
"-p",
"--verbose",
"--output-format",
"stream-json",
"--model",
"haiku",
"--append-system-prompt",
"Always respond in haiku form",
],
expected_stdin_content: "What is the world's largest lake?",
},
)

stdout, stderr = in_sandbox :simple_agent do
Expand Down Expand Up @@ -531,8 +533,8 @@ class RoastExamplesTest < FunctionalTest
end

test "simple_pi_agent.rb workflow runs successfully" do
use_command_runner_fixture(
"agent_transcripts/simple_pi_agent",
use_command_runner_fixtures({
fixture: "agent_transcripts/simple_pi_agent",
expected_args: [
"pi",
"--mode",
Expand All @@ -545,7 +547,7 @@ class RoastExamplesTest < FunctionalTest
"--no-session",
],
expected_stdin_content: "What is the world's largest lake?",
)
})

stdout, stderr = in_sandbox :simple_pi_agent do
Roast::Workflow.from_file("examples/simple_pi_agent.rb", EMPTY_PARAMS)
Expand Down
100 changes: 61 additions & 39 deletions test/test_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -159,49 +159,71 @@ def original_streams_from_logger_output(logger_output: @logger_output.string)
[stdout_lines.join, stderr_lines.join]
end

# Sets up a mock for the CommandRunner's execute method that does not actually run a command,
# but instead provides the standard output and standard error lines from fixture files to the
# stdout and stderr handlers provided when execute is invoked. It will also return a Process::Status with the
# provided exit_code (defaulting to 0).
# Sets up a mock for the CommandRunner's execute method that serves fixture files sequentially
# across multiple invocations. Each hash specifies a fixture and optional expectations for one call.
#
# This method can optionally validate that args, working_directory, timeout, and stdin_content values match provided
# expectations.
# Each hash supports:
# fixture: (String, required) fixture name under test/fixtures/
# exit_code: (Integer, default 0)
# expected_args: (Array[String]?)
# expected_working_directory: (Pathname | String)?
# expected_timeout: (Integer | Float)?
# expected_stdin_content: (String?)
#
#: (
#| String,
#| ?exit_code: Integer,
#| ?expected_args: Array[String]?,
#| ?expected_working_directory: (Pathname | String)?,
#| ?expected_timeout: (Integer | Float)?,
#| ?expected_stdin_content: String?,
#| ) -> void
def use_command_runner_fixture(
fixture_name,
exit_code: 0,
expected_args: nil,
expected_working_directory: nil,
expected_timeout: nil,
expected_stdin_content: nil
)
stdout_fixture_file = "test/fixtures/#{fixture_name}.stdout.txt"
stderr_fixture_file = "test/fixtures/#{fixture_name}.stderr.txt"
stdout_fixture = File.exist?(stdout_fixture_file) ? File.read(stdout_fixture_file) : ""
stderr_fixture = File.exist?(stderr_fixture_file) ? File.read(stderr_fixture_file) : ""

mock_status = mock("process_status")
mock_status.stubs(exitstatus: exit_code, success?: exit_code == 0, signaled?: false)

Roast::CommandRunner.stubs(:execute).with do |args, **kwargs|
assert_equal(expected_args, args, "CommandRunner args mismatch") if expected_args
assert_equal(expected_working_directory, kwargs[:working_directory], "CommandRunner working_directory mismatch") if expected_working_directory
assert_equal(expected_timeout, kwargs[:timeout], "CommandRunner timeout mismatch") if expected_timeout
assert_equal(expected_stdin_content, kwargs[:stdin_content], "CommandRunner stdin_content mismatch") if expected_stdin_content

stdout_fixture.each_line { |line| kwargs[:stdout_handler]&.call(line) }
stderr_fixture.each_line { |line| kwargs[:stderr_handler]&.call(line) }
#: (*Hash[Symbol, untyped]) -> void
def use_command_runner_fixtures(*specs)
call_index = 0

# Pre-load all fixtures and mock statuses so they're ready at call time
loaded = specs.map do |spec|
fixture_name = spec.fetch(:fixture)
exit_code = spec.fetch(:exit_code, 0)

stdout_fixture = load_command_runner_fixture_file(fixture_name, :stdout)
stderr_fixture = load_command_runner_fixture_file(fixture_name, :stderr)

mock_status = mock("process_status_#{call_index}")
mock_status.stubs(exitstatus: exit_code, success?: exit_code == 0, signaled?: false)

{ spec:, stdout: stdout_fixture, stderr: stderr_fixture, status: mock_status }
end

expectation = Roast::CommandRunner.stubs(:execute).with do |args, **kwargs|
assert call_index < loaded.size,
"CommandRunner.execute called #{call_index + 1} times, but only #{loaded.size} fixture(s) were provided"

entry = loaded[call_index]
spec = entry[:spec]
call_index += 1

assert_equal(spec[:expected_args], args, "CommandRunner args mismatch (invocation #{call_index})") if spec[:expected_args]
assert_equal(spec[:expected_working_directory], kwargs[:working_directory], "CommandRunner working_directory mismatch (invocation #{call_index})") if spec[:expected_working_directory]
assert_equal(spec[:expected_timeout], kwargs[:timeout], "CommandRunner timeout mismatch (invocation #{call_index})") if spec[:expected_timeout]
assert_equal(spec[:expected_stdin_content], kwargs[:stdin_content], "CommandRunner stdin_content mismatch (invocation #{call_index})") if spec[:expected_stdin_content]

entry[:stdout].each_line { |line| kwargs[:stdout_handler]&.call(line) }
entry[:stderr].each_line { |line| kwargs[:stderr_handler]&.call(line) }

true
end.returns([stdout_fixture, stderr_fixture, mock_status])
end

# Chain sequential return values: .returns(first).then.returns(second).then.returns(third)...
loaded.each_with_index do |entry, i|
ret = [entry[:stdout], entry[:stderr], entry[:status]]
expectation = i == 0 ? expectation.returns(ret) : expectation.then.returns(ret)
end
end

# Load a CommandRunner fixture file, trying .stdout.txt then .stdout.log (and likewise for stderr).
#
#: (String, Symbol) -> String
def load_command_runner_fixture_file(fixture_name, stream)
extensions = stream == :stdout ? [".stdout.txt", ".stdout.log"] : [".stderr.txt", ".stderr.log"]
extensions.each do |ext|
path = "test/fixtures/#{fixture_name}#{ext}"
return File.read(path) if File.exist?(path)
end
""
end

VCR.configure do |config|
Expand Down
Loading