From 2afa6dce286e4a54df65a97b3b88ee0806502ad1 Mon Sep 17 00:00:00 2001 From: Neil Carvalho Date: Fri, 12 Jun 2026 16:27:09 -0300 Subject: [PATCH 1/2] Remove FixtureRunner in favor of fake Open3 --- lib/commands.rb | 28 ++---------- test/commands_test.rb | 101 ----------------------------------------- test/executor_test.rb | 97 ++++++++++++++++++++++----------------- test/fixture_runner.rb | 53 --------------------- test/test_helper.rb | 1 - 5 files changed, 58 insertions(+), 222 deletions(-) delete mode 100644 test/fixture_runner.rb diff --git a/lib/commands.rb b/lib/commands.rb index 229cca2..c308dfc 100644 --- a/lib/commands.rb +++ b/lib/commands.rb @@ -4,19 +4,6 @@ require "json" module ImportmapUpdate - # Abstracts execution of external commands (bin/importmap) so the rest - # of the codebase doesn't shell out directly. This is the seam tests hook - # into — production code runs commands for real, tests inject a - # FixtureRunner that replays pre-recorded (argv → stdout, exit) tuples. - # - # The interface deliberately mirrors what Open3.capture3 returns: - # - # runner.run("bin/importmap", "outdated") - # # => Result(stdout: "...", stderr: "...", success: true, exit: 0) - # - # Commands are passed as an argv array, not a shell string. That's both - # safer (no shell-injection surprises from package names) and easier to - # match against fixture keys. module Commands Result = Data.define(:stdout, :stderr, :exit_code) do def success? @@ -34,29 +21,20 @@ def initialize(argv, result) end end - # Production runner: actually executes the command. class ShellRunner - # @param cwd [String, nil] working directory (defaults to current) - def initialize(cwd: nil) + def initialize(cwd: nil, open3: Open3) @cwd = cwd + @open3 = open3 end def run(*argv) opts = {} opts[:chdir] = @cwd if @cwd Bundler.with_unbundled_env do - stdout, stderr, status = Open3.capture3(*argv, opts) + stdout, stderr, status = @open3.capture3(*argv, **opts) Result.new(stdout:, stderr:, exit_code: status.exitstatus) end end - - # Raises on non-zero exit. Use when you have no recovery strategy - # and just want to surface the error to the caller. - def run!(*argv) - result = run(*argv) - raise CommandError.new(argv, result) unless result.success? - result - end end end end diff --git a/test/commands_test.rb b/test/commands_test.rb index 8039271..159d8cb 100644 --- a/test/commands_test.rb +++ b/test/commands_test.rb @@ -23,105 +23,4 @@ def test_shell_runner_captures_stderr_and_failure assert_equal 3, result.exit_code refute_predicate result, :success? end - - def test_shell_runner_bang_raises_on_non_zero_exit - err = assert_raises(Commands::CommandError) do - Commands::ShellRunner.new.run!("sh", "-c", "echo nope 1>&2; exit 1") - end - assert_equal 1, err.result.exit_code - assert_includes err.message, "exited 1" - assert_includes err.message, "nope" - end - - def test_shell_runner_argv_is_safe_from_shell_metacharacters - # argv-style invocation must not interpret `;` as a command separator. - # If it did, this test would attempt to delete a file. - result = Commands::ShellRunner.new.run("echo", "a; rm -rf /b") - assert_equal "a; rm -rf /b\n", result.stdout - end - - # ---- FixtureRunner: literal matching ---- - - def test_fixture_runner_returns_recorded_result_for_exact_argv_match - runner = Commands::FixtureRunner.new - runner.add( - pattern: ["bin/importmap", "outdated"], - stdout: "| Package | Current | Latest |\n" - ) - result = runner.run("bin/importmap", "outdated") - assert_equal "| Package | Current | Latest |\n", result.stdout - assert_predicate result, :success? - end - - def test_fixture_runner_records_calls_in_order - runner = Commands::FixtureRunner.new - runner.add(pattern: ["bin/importmap", "outdated"], stdout: "") - runner.add(pattern: ["bin/importmap", "audit"], stdout: "") - runner.run("bin/importmap", "outdated") - runner.run("bin/importmap", "audit") - assert_equal [ - ["bin/importmap", "outdated"], - ["bin/importmap", "audit"] - ], runner.calls - end - - def test_fixture_runner_first_matching_pattern_wins - # Order matters: if you register a fallback first, it'll swallow more - # specific patterns. This test pins the behavior so callers know to - # register specific patterns before general ones. - runner = Commands::FixtureRunner.new - runner.add(pattern: ["bin/importmap", "pin", "lodash@4.17.21"], stdout: "first\n") - runner.add(pattern: ["bin/importmap", "pin", "lodash@4.17.21"], stdout: "second\n") - assert_equal "first\n", runner.run("bin/importmap", "pin", "lodash@4.17.21").stdout - end - - # ---- FixtureRunner: regex matching ---- - - def test_fixture_runner_supports_regex_elements_in_patterns - # Package versions change; the pattern uses a regex to allow any semver. - runner = Commands::FixtureRunner.new - runner.add( - pattern: ["bin/importmap", "pin", /\Alodash@\d+\.\d+\.\d+\z/], - stdout: "Pinned lodash\n" - ) - assert_equal "Pinned lodash\n", runner.run("bin/importmap", "pin", "lodash@4.17.21").stdout - end - - def test_fixture_runner_regex_must_match_exactly_at_position - runner = Commands::FixtureRunner.new - runner.add(pattern: ["bin/importmap", "pin", /\Alodash@/], stdout: "ok\n") - err = assert_raises(RuntimeError) do - runner.run("bin/importmap", "pin", "axios@1.7.0") - end - assert_match(/No fixture matched/, err.message) - end - - # ---- FixtureRunner: misses and errors ---- - - def test_fixture_runner_raises_clearly_when_no_pattern_matches - runner = Commands::FixtureRunner.new - runner.add(pattern: ["bin/importmap", "outdated"], stdout: "") - err = assert_raises(RuntimeError) { runner.run("bin/importmap", "audit") } - assert_includes err.message, "No fixture matched" - assert_includes err.message, "audit" - end - - def test_fixture_runner_argv_size_mismatch_does_not_match - # A 2-element pattern must not match a 3-element call. - runner = Commands::FixtureRunner.new - runner.add(pattern: ["bin/importmap", "outdated"], stdout: "ok\n") - assert_raises(RuntimeError) { runner.run("bin/importmap", "outdated", "--verbose") } - end - - def test_fixture_runner_bang_raises_command_error_on_recorded_failure - runner = Commands::FixtureRunner.new - runner.add( - pattern: ["bin/importmap", "pin", "lodash@4.17.21"], - stderr: "network error", - exit_code: 1 - ) - err = assert_raises(Commands::CommandError) { runner.run!("bin/importmap", "pin", "lodash@4.17.21") } - assert_equal 1, err.result.exit_code - assert_includes err.message, "network error" - end end diff --git a/test/executor_test.rb b/test/executor_test.rb index 85c1162..990757f 100644 --- a/test/executor_test.rb +++ b/test/executor_test.rb @@ -11,10 +11,7 @@ class ExecutorTest < Minitest::Test Reconciler = ImportmapUpdate::Reconciler Commands = ImportmapUpdate::Commands - # ---- fakes ---- - # Spy GitHubClient that records calls and lets tests configure the PR number - # returned by create_pr. class FakeGh attr_reader :created, :updated, :closed attr_accessor :next_pr_number @@ -47,7 +44,6 @@ def close_pr(number:, comment: nil) end end - # Spy GitClient that records every git operation but does nothing. class FakeGit attr_reader :checkouts, :commits, :pushes attr_accessor :commit_returns @@ -61,6 +57,7 @@ def initialize def checkout_fresh_branch(branch:, base:) @checkouts << {branch:, base:} + nil end @@ -71,14 +68,55 @@ def commit_changes(message:) def push(branch:, force: false) @pushes << {branch:, force:} + nil end end - # ---- builders mirroring the planner's output ---- + class FakeOpen3 + Fixture = Data.define(:pattern, :stdout, :stderr, :exit_code) + ProcessStatus = Data.define(:exitstatus) + + attr_reader :calls + + def initialize(fixtures = []) + @fixtures = fixtures + @calls = [] + end + + def add(pattern:, stdout: "", stderr: "", exit_code: 0) + @fixtures << Fixture.new(pattern:, stdout:, stderr:, exit_code:) + end + + def capture3(*argv, **) + @calls << argv + match = @fixtures.find { pattern_matches?(_1.pattern, argv) } + + if match.nil? + raise "No fixture matched argv: #{argv.inspect}.\nRegistered patterns: #{@fixtures.map(&:pattern).inspect}" + end + + [match.stdout, match.stderr, ProcessStatus.new(exitstatus: match.exit_code)] + end + + private + + def pattern_matches?(pattern, argv) + return false unless pattern.is_a?(Array) + return false unless pattern.size == argv.size + + pattern.zip(argv).all? do |pat, arg| + case pat + when Regexp then pat.match?(arg) + else pat == arg + end + end + end + end def bump(name, from, to, kind: :patch, severity: nil) advisory = severity ? {severity:} : nil + Planner::PackageBump.new(name:, from:, to:, semver_kind: kind, advisory:) end @@ -99,7 +137,8 @@ def existing_pr(number:, branch:) def setup @gh = FakeGh.new @git = FakeGit.new - @runner = Commands::FixtureRunner.new + @open3 = FakeOpen3.new + @runner = Commands::ShellRunner.new(open3: @open3) end def make_executor(dry_run: false) @@ -109,13 +148,10 @@ def make_executor(dry_run: false) commit_message_prefix: "", labels: %w[dependencies], dry_run:, - # Override the body renderer so tests don't depend on the exact body string. body_renderer: ->(s) { "body for #{s.branch}" } ) end - # ---- :noop ---- - def test_noop_action_records_success_without_touching_git_or_gh s = spec(branch: "importmap-updates/patch", packages: [bump("lodash", "4.17.20", "4.17.21")]) e = existing_pr(number: 1, branch: "importmap-updates/patch") @@ -130,15 +166,13 @@ def test_noop_action_records_success_without_touching_git_or_gh assert_empty @gh.created end - # ---- :open ---- - def test_open_action_pins_pushes_and_creates_pr s = spec( branch: "importmap-updates/patch", packages: [bump("lodash", "4.17.20", "4.17.21")], title: "Bump lodash 4.17.20 → 4.17.21" ) - @runner.add(pattern: ["bin/importmap", "pin", "lodash@4.17.21"], stdout: "pinned\n") + @open3.add(pattern: ["bin/importmap", "pin", "lodash@4.17.21"], stdout: "pinned\n") action = Reconciler::Action.new(type: :open, pr_spec: s) @gh.next_pr_number = 555 @@ -147,12 +181,10 @@ def test_open_action_pins_pushes_and_creates_pr assert_predicate report.outcomes.first, :success? assert_equal 555, report.outcomes.first.pr_number - # Git operations: checkout fresh, commit, push. assert_equal [{branch: "importmap-updates/patch", base: "main"}], @git.checkouts assert_equal 1, @git.commits.size assert_equal [{branch: "importmap-updates/patch", force: true}], @git.pushes - # gh create_pr called with planner-provided title and rendered body. assert_equal 1, @gh.created.size assert_equal "Bump lodash 4.17.20 → 4.17.21", @gh.created.first[:title] assert_equal "body for importmap-updates/patch", @gh.created.first[:body] @@ -160,12 +192,8 @@ def test_open_action_pins_pushes_and_creates_pr end def test_open_action_skips_pr_creation_when_pinning_produced_no_changes - # Race condition: the importmap was updated between the planner's read - # and the executor's run, so `bin/importmap pin` is a no-op. The - # `git diff --cached --quiet` step returns true; commit_changes returns - # false; we abort the open and record a skip. s = spec(branch: "importmap-updates/patch", packages: [bump("lodash", "4.17.20", "4.17.21")]) - @runner.add(pattern: ["bin/importmap", "pin", "lodash@4.17.21"], stdout: "pinned\n") + @open3.add(pattern: ["bin/importmap", "pin", "lodash@4.17.21"], stdout: "pinned\n") @git.commit_returns = false report = make_executor.call([Reconciler::Action.new(type: :open, pr_spec: s)]) @@ -175,16 +203,14 @@ def test_open_action_skips_pr_creation_when_pinning_produced_no_changes assert_empty @git.pushes end - # ---- :force_push ---- - def test_force_push_updates_branch_then_edits_pr s = spec( branch: "importmap-updates/patch", packages: [bump("lodash", "4.17.20", "4.17.21"), bump("axios", "1.7.0", "1.7.1")] ) e = existing_pr(number: 42, branch: "importmap-updates/patch") - @runner.add(pattern: ["bin/importmap", "pin", "lodash@4.17.21"], stdout: "") - @runner.add(pattern: ["bin/importmap", "pin", "axios@1.7.1"], stdout: "") + @open3.add(pattern: ["bin/importmap", "pin", "lodash@4.17.21"], stdout: "") + @open3.add(pattern: ["bin/importmap", "pin", "axios@1.7.1"], stdout: "") action = Reconciler::Action.new(type: :force_push, pr_spec: s, existing_pr: e, reason: "axios added") report = make_executor.call([action]) @@ -195,8 +221,6 @@ def test_force_push_updates_branch_then_edits_pr assert_equal 42, @gh.updated.first[:number] end - # ---- :close ---- - def test_close_action_closes_pr_with_reason_as_comment e = existing_pr(number: 99, branch: "importmap-updates/old") action = Reconciler::Action.new(type: :close, existing_pr: e, reason: "no longer outdated") @@ -207,8 +231,6 @@ def test_close_action_closes_pr_with_reason_as_comment assert_equal [{number: 99, comment: "no longer outdated"}], @gh.closed end - # ---- dry run ---- - def test_dry_run_records_skipped_outcomes_and_invokes_nothing s_open = spec(branch: "importmap-updates/patch", packages: [bump("lodash", "4.17.20", "4.17.21")]) s_fp = spec(branch: "importmap-updates/minor", packages: [bump("stim", "3.2.1", "3.3.0", kind: :minor)]) @@ -237,19 +259,12 @@ def test_dry_run_records_skipped_outcomes_and_invokes_nothing assert_includes fp_outcome.detail, "stim" end - # ---- failure isolation ---- - def test_one_failing_action_does_not_block_subsequent_actions - # First action will fail (no fixture matching → CommandError); second - # should still run cleanly. failing = spec(branch: "importmap-updates/patch", packages: [bump("broken", "1.0.0", "2.0.0")]) succeeding = spec(branch: "importmap-updates/minor", packages: [bump("ok", "1.0.0", "1.1.0", kind: :minor)]) - # No fixture for "broken" → bin/importmap will raise via FixtureRunner. - @runner.add(pattern: ["bin/importmap", "pin", "ok@1.1.0"], stdout: "") - # Suppress the "no fixture matched" RuntimeError from the FixtureRunner - # by registering a failing fixture instead. - @runner.add(pattern: ["bin/importmap", "pin", "broken@2.0.0"], stderr: "boom", exit_code: 1) + @open3.add(pattern: ["bin/importmap", "pin", "ok@1.1.0"], stdout: "") + @open3.add(pattern: ["bin/importmap", "pin", "broken@2.0.0"], stderr: "boom", exit_code: 1) actions = [ Reconciler::Action.new(type: :open, pr_spec: failing), @@ -264,9 +279,6 @@ def test_one_failing_action_does_not_block_subsequent_actions end def test_partial_group_failure_keeps_what_was_pinned_successfully - # In a grouped PR with three packages, if the second fails but the - # first and third succeed, we still want a PR with two packages — - # not zero. The executor swallows mid-group failures and continues. s = spec( branch: "importmap-updates/patch", packages: [ @@ -275,9 +287,10 @@ def test_partial_group_failure_keeps_what_was_pinned_successfully bump("c", "1.0.0", "1.0.1") ] ) - @runner.add(pattern: ["bin/importmap", "pin", "a@1.0.1"], stdout: "") - @runner.add(pattern: ["bin/importmap", "pin", "b@1.0.1"], stderr: "boom", exit_code: 1) - @runner.add(pattern: ["bin/importmap", "pin", "c@1.0.1"], stdout: "") + + @open3.add(pattern: ["bin/importmap", "pin", "a@1.0.1"], stdout: "") + @open3.add(pattern: ["bin/importmap", "pin", "b@1.0.1"], stderr: "boom", exit_code: 1) + @open3.add(pattern: ["bin/importmap", "pin", "c@1.0.1"], stdout: "") report = make_executor.call([Reconciler::Action.new(type: :open, pr_spec: s)]) diff --git a/test/fixture_runner.rb b/test/fixture_runner.rb deleted file mode 100644 index 1006486..0000000 --- a/test/fixture_runner.rb +++ /dev/null @@ -1,53 +0,0 @@ -# frozen_string_literal: true - -require "commands" - -module ImportmapUpdate - module Commands - class FixtureRunner - Fixture = Data.define(:pattern, :result) - - def initialize(fixtures = []) - @fixtures = fixtures - @calls = [] - end - - attr_reader :calls - - def add(pattern:, stdout: "", stderr: "", exit_code: 0) - @fixtures << Fixture.new( - pattern:, - result: Result.new(stdout:, stderr:, exit_code:) - ) - end - - def run(*argv) - @calls << argv - match = @fixtures.find { |f| pattern_matches?(f.pattern, argv) } - if match.nil? - raise "No fixture matched argv: #{argv.inspect}.\nRegistered patterns: #{@fixtures.map(&:pattern).inspect}" - end - match.result - end - - def run!(*argv) - result = run(*argv) - raise CommandError.new(argv, result) unless result.success? - result - end - - private - - def pattern_matches?(pattern, argv) - return false unless pattern.is_a?(Array) - return false unless pattern.size == argv.size - pattern.zip(argv).all? do |pat, arg| - case pat - when Regexp then pat.match?(arg) - else pat == arg - end - end - end - end - end -end diff --git a/test/test_helper.rb b/test/test_helper.rb index 9bac678..371c13d 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -3,7 +3,6 @@ $LOAD_PATH.unshift File.expand_path("../lib", __dir__) require "minitest/autorun" -require_relative "fixture_runner" module TestHelpers FIXTURE_DIR = File.expand_path("fixtures", __dir__) From 0619976bb763f4607218b55805980a3fb77498b3 Mon Sep 17 00:00:00 2001 From: Neil Carvalho Date: Fri, 12 Jun 2026 17:08:46 -0300 Subject: [PATCH 2/2] Use Open3.capture2e instead of Open3.capture3 --- lib/commands.rb | 8 ++++---- test/commands_test.rb | 8 ++------ test/executor_test.rb | 29 ++++++++++++++--------------- 3 files changed, 20 insertions(+), 25 deletions(-) diff --git a/lib/commands.rb b/lib/commands.rb index c308dfc..e27dbb7 100644 --- a/lib/commands.rb +++ b/lib/commands.rb @@ -5,7 +5,7 @@ module ImportmapUpdate module Commands - Result = Data.define(:stdout, :stderr, :exit_code) do + Result = Data.define(:output, :exit_code) do def success? exit_code == 0 end @@ -17,7 +17,7 @@ class CommandError < StandardError def initialize(argv, result) @argv = argv @result = result - super("`#{argv.join(" ")}` exited #{result.exit_code}: #{result.stderr.strip}") + super("`#{argv.join(" ")}` exited #{result.exit_code}: #{result.output.strip}") end end @@ -31,8 +31,8 @@ def run(*argv) opts = {} opts[:chdir] = @cwd if @cwd Bundler.with_unbundled_env do - stdout, stderr, status = @open3.capture3(*argv, **opts) - Result.new(stdout:, stderr:, exit_code: status.exitstatus) + output, status = @open3.capture2e(*argv, **opts) + Result.new(output:, exit_code: status.exitstatus) end end end diff --git a/test/commands_test.rb b/test/commands_test.rb index 159d8cb..3fdeb65 100644 --- a/test/commands_test.rb +++ b/test/commands_test.rb @@ -6,20 +6,16 @@ class CommandsTest < Minitest::Test Commands = ImportmapUpdate::Commands - # ---- ShellRunner ---- - def test_shell_runner_captures_stdout result = Commands::ShellRunner.new.run("echo", "hello world") - assert_equal "hello world\n", result.stdout + assert_equal "hello world\n", result.output assert_predicate result, :success? assert_equal 0, result.exit_code end def test_shell_runner_captures_stderr_and_failure - # `sh -c` lets us write to stderr cleanly without depending on a - # specific binary's behavior. result = Commands::ShellRunner.new.run("sh", "-c", "echo oops 1>&2; exit 3") - assert_equal "oops\n", result.stderr + assert_equal "oops\n", result.output assert_equal 3, result.exit_code refute_predicate result, :success? end diff --git a/test/executor_test.rb b/test/executor_test.rb index 990757f..0c9b1b9 100644 --- a/test/executor_test.rb +++ b/test/executor_test.rb @@ -11,7 +11,6 @@ class ExecutorTest < Minitest::Test Reconciler = ImportmapUpdate::Reconciler Commands = ImportmapUpdate::Commands - class FakeGh attr_reader :created, :updated, :closed attr_accessor :next_pr_number @@ -74,7 +73,7 @@ def push(branch:, force: false) end class FakeOpen3 - Fixture = Data.define(:pattern, :stdout, :stderr, :exit_code) + Fixture = Data.define(:pattern, :output, :exit_code) ProcessStatus = Data.define(:exitstatus) attr_reader :calls @@ -84,11 +83,11 @@ def initialize(fixtures = []) @calls = [] end - def add(pattern:, stdout: "", stderr: "", exit_code: 0) - @fixtures << Fixture.new(pattern:, stdout:, stderr:, exit_code:) + def stub(pattern, output: "", exit_code: 0) + @fixtures << Fixture.new(pattern:, output:, exit_code:) end - def capture3(*argv, **) + def capture2e(*argv, **) @calls << argv match = @fixtures.find { pattern_matches?(_1.pattern, argv) } @@ -96,7 +95,7 @@ def capture3(*argv, **) raise "No fixture matched argv: #{argv.inspect}.\nRegistered patterns: #{@fixtures.map(&:pattern).inspect}" end - [match.stdout, match.stderr, ProcessStatus.new(exitstatus: match.exit_code)] + [match.output, ProcessStatus.new(exitstatus: match.exit_code)] end private @@ -172,7 +171,7 @@ def test_open_action_pins_pushes_and_creates_pr packages: [bump("lodash", "4.17.20", "4.17.21")], title: "Bump lodash 4.17.20 → 4.17.21" ) - @open3.add(pattern: ["bin/importmap", "pin", "lodash@4.17.21"], stdout: "pinned\n") + @open3.stub(["bin/importmap", "pin", "lodash@4.17.21"], output: "pinned\n") action = Reconciler::Action.new(type: :open, pr_spec: s) @gh.next_pr_number = 555 @@ -193,7 +192,7 @@ def test_open_action_pins_pushes_and_creates_pr def test_open_action_skips_pr_creation_when_pinning_produced_no_changes s = spec(branch: "importmap-updates/patch", packages: [bump("lodash", "4.17.20", "4.17.21")]) - @open3.add(pattern: ["bin/importmap", "pin", "lodash@4.17.21"], stdout: "pinned\n") + @open3.stub(["bin/importmap", "pin", "lodash@4.17.21"], output: "pinned\n") @git.commit_returns = false report = make_executor.call([Reconciler::Action.new(type: :open, pr_spec: s)]) @@ -209,8 +208,8 @@ def test_force_push_updates_branch_then_edits_pr packages: [bump("lodash", "4.17.20", "4.17.21"), bump("axios", "1.7.0", "1.7.1")] ) e = existing_pr(number: 42, branch: "importmap-updates/patch") - @open3.add(pattern: ["bin/importmap", "pin", "lodash@4.17.21"], stdout: "") - @open3.add(pattern: ["bin/importmap", "pin", "axios@1.7.1"], stdout: "") + @open3.stub(["bin/importmap", "pin", "lodash@4.17.21"], output: "") + @open3.stub(["bin/importmap", "pin", "axios@1.7.1"], output: "") action = Reconciler::Action.new(type: :force_push, pr_spec: s, existing_pr: e, reason: "axios added") report = make_executor.call([action]) @@ -263,8 +262,8 @@ def test_one_failing_action_does_not_block_subsequent_actions failing = spec(branch: "importmap-updates/patch", packages: [bump("broken", "1.0.0", "2.0.0")]) succeeding = spec(branch: "importmap-updates/minor", packages: [bump("ok", "1.0.0", "1.1.0", kind: :minor)]) - @open3.add(pattern: ["bin/importmap", "pin", "ok@1.1.0"], stdout: "") - @open3.add(pattern: ["bin/importmap", "pin", "broken@2.0.0"], stderr: "boom", exit_code: 1) + @open3.stub(["bin/importmap", "pin", "ok@1.1.0"], output: "") + @open3.stub(["bin/importmap", "pin", "broken@2.0.0"], output: "boom", exit_code: 1) actions = [ Reconciler::Action.new(type: :open, pr_spec: failing), @@ -288,9 +287,9 @@ def test_partial_group_failure_keeps_what_was_pinned_successfully ] ) - @open3.add(pattern: ["bin/importmap", "pin", "a@1.0.1"], stdout: "") - @open3.add(pattern: ["bin/importmap", "pin", "b@1.0.1"], stderr: "boom", exit_code: 1) - @open3.add(pattern: ["bin/importmap", "pin", "c@1.0.1"], stdout: "") + @open3.stub(["bin/importmap", "pin", "a@1.0.1"], output: "") + @open3.stub(["bin/importmap", "pin", "b@1.0.1"], output: "boom", exit_code: 1) + @open3.stub(["bin/importmap", "pin", "c@1.0.1"], output: "") report = make_executor.call([Reconciler::Action.new(type: :open, pr_spec: s)])