backspin 0.6.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e49507f2d196ebd4d7b4d39a6013d840224a357272fb5ba4880702e26d28c8d9
4
- data.tar.gz: cfee20f6ff8b65c622bfc9482d85e49c78f78e1e9a2c91b5f09f71e2b1d61663
3
+ metadata.gz: 803ecdad9628acd396e0d70b1e9e4081c612562f6872af8fa05108a820c61590
4
+ data.tar.gz: 563d3b7e8c413742453971859cf4bb31aeff11dbeee38d5804922ffa150615bb
5
5
  SHA512:
6
- metadata.gz: 660d3af239a57512fbd74eecf2f7fc81c91a6fc42c0a95957952f01931bcd2b5f5387af2004557af985c8c3ef6f37c6b284e41b757af2919a241d5408060b25e
7
- data.tar.gz: 6a0b7f7e2bc4533e80e8011fb17c1f851c31ddd6e831300efec0589423f541640e1ace953c75f066e8a9efc26c6e3fef886cc87941d204e09d1a2302d58adcbc
6
+ metadata.gz: 17aff3cf0ab930e13da63fa427135cde5e41816341d75ca7712662d4feefb4df1d753db983668a57562c9922d2dda4ae5f54d8cc579ce720d0ac9009015740ff
7
+ data.tar.gz: 203e8c9f1158712ae20e64864a20645cf5fad7e3bd79627bf6dfc7d7efa40cd51d5cbd2e5ac037ebe9a9f8c0408c33921f948400435aa023db32da9df9dfa8fc
data/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.7.1 - 2025-12-02
4
+ * Include result object on VerificationError to make it easier for callers to debug verification errors
5
+
6
+ ## 0.7.0 - 2025-08-13
7
+ * Breaking change: `Backspin.run` and `Backspin.capture` now raise an error if verification fails by default. Use `Backspin.configure` to opt-out. https://github.com/rsanheim/backspin/pull/18
8
+
9
+ ## 0.6.0 - 2025-08-13
10
+ * Introduce `Backspin.capture` for rspec-less, simpler stdout/stderr testing https://github.com/rsanheim/backspin/pull/17
11
+
3
12
  ## 0.5.0 - 2025-06-11
4
13
  * Simplify matcher API so user provided matchers override defaults - [#14](https://github.com/rsanheim/backspin/pull/14)
5
14
  * Also extract a proper `Matcher` object
data/CLAUDE.md CHANGED
@@ -41,9 +41,11 @@ bin/rake standard # Alternative: Run via Rake task
41
41
  ### Core Components
42
42
 
43
43
  **Backspin Module** (`lib/backspin.rb`)
44
- - Main API: `call`, `verify`, `verify!`, `use_record`
44
+ - Main API: `run`, `run!` (both raise on verification failure by default)
45
+ - Capture API: `capture` (raises `VerificationError` on verification failure by default)
46
+ - Legacy API: `call`, `verify`, `verify!`, `use_record`
45
47
  - Credential scrubbing logic
46
- - Configuration management
48
+ - Configuration management (including `raise_on_verification_failure` which defaults to `true` and affects both `run` and `capture`)
47
49
 
48
50
  **Command Class** (`lib/backspin.rb`)
49
51
  - Represents a single CLI execution
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- backspin (0.6.0)
4
+ backspin (0.7.1)
5
5
  ostruct
6
6
  rspec-mocks (~> 3)
7
7
 
@@ -10,19 +10,19 @@ GEM
10
10
  specs:
11
11
  ast (2.4.3)
12
12
  diff-lcs (1.6.2)
13
- json (2.12.2)
13
+ json (2.16.0)
14
14
  language_server-protocol (3.17.0.5)
15
15
  lint_roller (1.1.0)
16
16
  ostruct (0.6.1)
17
17
  parallel (1.27.0)
18
- parser (3.3.8.0)
18
+ parser (3.3.10.0)
19
19
  ast (~> 2.4.1)
20
20
  racc
21
- prism (1.4.0)
21
+ prism (1.6.0)
22
22
  racc (1.8.1)
23
23
  rainbow (3.1.1)
24
24
  rake (13.3.0)
25
- regexp_parser (2.10.0)
25
+ regexp_parser (2.11.3)
26
26
  rspec (3.13.1)
27
27
  rspec-core (~> 3.13.0)
28
28
  rspec-expectations (~> 3.13.0)
@@ -36,7 +36,7 @@ GEM
36
36
  diff-lcs (>= 1.2.0, < 2.0)
37
37
  rspec-support (~> 3.13.0)
38
38
  rspec-support (3.13.4)
39
- rubocop (1.75.8)
39
+ rubocop (1.81.7)
40
40
  json (~> 2.3)
41
41
  language_server-protocol (~> 3.17.0.2)
42
42
  lint_roller (~> 1.1.0)
@@ -44,10 +44,10 @@ GEM
44
44
  parser (>= 3.3.0.2)
45
45
  rainbow (>= 2.2.2, < 4.0)
46
46
  regexp_parser (>= 2.9.3, < 3.0)
47
- rubocop-ast (>= 1.44.0, < 2.0)
47
+ rubocop-ast (>= 1.47.1, < 2.0)
48
48
  ruby-progressbar (~> 1.7)
49
49
  unicode-display_width (>= 2.4.0, < 4.0)
50
- rubocop-ast (1.45.1)
50
+ rubocop-ast (1.48.0)
51
51
  parser (>= 3.3.7.2)
52
52
  prism (~> 1.4)
53
53
  rubocop-performance (1.25.0)
@@ -55,10 +55,10 @@ GEM
55
55
  rubocop (>= 1.75.0, < 2.0)
56
56
  rubocop-ast (>= 1.38.0, < 2.0)
57
57
  ruby-progressbar (1.13.0)
58
- standard (1.50.0)
58
+ standard (1.52.0)
59
59
  language_server-protocol (~> 3.17.0.2)
60
60
  lint_roller (~> 1.0)
61
- rubocop (~> 1.75.5)
61
+ rubocop (~> 1.81.7)
62
62
  standard-custom (~> 1.0.0)
63
63
  standard-performance (~> 1.8)
64
64
  standard-custom (1.0.2)
@@ -68,9 +68,9 @@ GEM
68
68
  lint_roller (~> 1.1)
69
69
  rubocop-performance (~> 1.25.0)
70
70
  timecop (0.9.10)
71
- unicode-display_width (3.1.4)
72
- unicode-emoji (~> 4.0, >= 4.0.4)
73
- unicode-emoji (4.0.4)
71
+ unicode-display_width (3.2.0)
72
+ unicode-emoji (~> 4.1)
73
+ unicode-emoji (4.1.0)
74
74
 
75
75
  PLATFORMS
76
76
  arm64-darwin-24
@@ -84,4 +84,4 @@ DEPENDENCIES
84
84
  timecop (~> 0.9)
85
85
 
86
86
  BUNDLED WITH
87
- 2.6.9
87
+ 4.0.0.beta2
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Backspin
1
+ # Backspin
2
2
 
3
3
  [![Ruby](https://img.shields.io/badge/ruby-%23CC342D.svg?style=flat&logo=ruby&logoColor=white)](https://www.ruby-lang.org/)
4
4
  [![Gem Version](https://img.shields.io/gem/v/backspin)](https://rubygems.org/gems/backspin)
@@ -33,7 +33,7 @@ And then run `bundle install`.
33
33
 
34
34
  ### Quick Start
35
35
 
36
- The simplest way to use Backspin is with the `run` method, which automatically records on the first execution and verifies on subsequent runs:
36
+ The simplest way to use Backspin is with the `run` method, which automatically records on the first execution and verifies on subsequent runs.
37
37
 
38
38
  ```ruby
39
39
  require "backspin"
@@ -43,18 +43,20 @@ result = Backspin.run("my_command") do
43
43
  Open3.capture3("echo hello world")
44
44
  end
45
45
 
46
- # Subsequent runs: verifies the output matches
47
- result = Backspin.run("my_command") do
48
- Open3.capture3("echo hello world")
46
+ # Subsequent runs: verifies the output matches and raises on mismatch
47
+ Backspin.run("my_command") do
48
+ Open3.capture3("echo hello world") # Passes - output matches
49
49
  end
50
50
 
51
- # Use run! to automatically fail tests on mismatch
52
- Backspin.run!("my_command") do
53
- Open3.capture3("echo hello mars")
51
+ # This will raise an error automatically
52
+ Backspin.run("my_command") do
53
+ Open3.capture3("echo hello mars")
54
54
  end
55
- # Raises an error because stdout will not match the recorded output
55
+ # Raises RSpec::Expectations::ExpectationNotMetError because output doesn't match
56
56
  ```
57
57
 
58
+ By default, `Backspin.run` will raise an exception when verification fails, making your tests fail automatically. This is the recommended approach for most scenarios.
59
+
58
60
  ### Recording Modes
59
61
 
60
62
  Backspin supports different modes for controlling how commands are recorded and verified:
@@ -83,18 +85,25 @@ result = Backspin.run("slow_command", mode: :playback) do
83
85
  end
84
86
  ```
85
87
 
86
- ### Using run! for automatic test failures
88
+ ### The run! method
89
+
90
+ **NOTE:** This method is deprecated and will be removed soon.
87
91
 
88
- The `run!` method works exactly like `run` but automatically fails the test if verification fails:
92
+ The `run!` method is maintained for backwards compatibility and works identically to `run`. Since `run` now raises on verification failure by default, both methods behave the same way:
89
93
 
90
94
  ```ruby
91
- # Automatically fail the test if output doesn't match
95
+ # Both of these are equivalent and will raise on verification failure
96
+ Backspin.run("echo_test") do
97
+ Open3.capture3("echo hello")
98
+ end
99
+
92
100
  Backspin.run!("echo_test") do
93
101
  Open3.capture3("echo hello")
94
102
  end
95
- # Raises an error with detailed diff if verification fails from recorded data in "echo_test.yml"
96
103
  ```
97
104
 
105
+ For new code, we recommend using `run` as it's the primary API method.
106
+
98
107
  ### Custom matchers
99
108
 
100
109
  For cases where full matching isn't suitable, you can override via `matcher:`. **NOTE**: If you provide
@@ -199,6 +208,42 @@ result.commands[3].stderr # curl errors if any
199
208
 
200
209
  When verifying multiple commands, Backspin ensures all commands match in the exact order they were recorded. If any command differs, you'll get a detailed error showing which commands failed.
201
210
 
211
+ ### Configuration
212
+
213
+ You can configure Backspin's behavior globally:
214
+
215
+ ```ruby
216
+ Backspin.configure do |config|
217
+ # Both run and capture methods will raise on verification failure by default
218
+ config.raise_on_verification_failure = false # default is true
219
+ config.backspin_dir = "spec/fixtures/cli_records" # default is "fixtures/backspin"
220
+ config.scrub_credentials = false # default is true
221
+ end
222
+ ```
223
+
224
+ The `raise_on_verification_failure` setting affects both `Backspin.run` and `Backspin.capture`:
225
+ - When `true` (default): Both methods raise exceptions on verification failure
226
+ - `run` raises `RSpec::Expectations::ExpectationNotMetError`
227
+ - `capture` raises `Backspin::VerificationError` (framework-agnostic)
228
+ - When `false`: Both methods return a result with `verified?` set to false
229
+
230
+ If you need to disable the raising behavior for a specific test, you can temporarily configure it:
231
+
232
+ ```ruby
233
+ # Temporarily disable raising for this block
234
+ Backspin.configure do |config|
235
+ config.raise_on_verification_failure = false
236
+ end
237
+
238
+ result = Backspin.run("my_test") do
239
+ Open3.capture3("echo different")
240
+ end
241
+ # result.verified? will be false but won't raise
242
+
243
+ # Reset configuration
244
+ Backspin.reset_configuration!
245
+ ```
246
+
202
247
  ### Credential Scrubbing
203
248
 
204
249
  If the CLI interaction you are recording contains sensitive data in stdout or stderr, you should be careful to make sure it is not recorded to yaml!
@@ -4,8 +4,9 @@ format_version: '2.0'
4
4
  commands:
5
5
  - command_type: Open3::Capture3
6
6
  args:
7
- - date
8
- stdout: 'Wed Jun 11 02:48:13 CDT 2025
7
+ - echo
8
+ - 'test output with : colons'
9
+ stdout: 'test output with : colons
9
10
 
10
11
  '
11
12
  stderr: ''
@@ -5,9 +5,8 @@ commands:
5
5
  - command_type: Open3::Capture3
6
6
  args:
7
7
  - echo
8
- - "'hello"
9
- - world'
10
- stdout: 'HELLO WORLD
8
+ - exact output
9
+ stdout: 'exact output
11
10
 
12
11
  '
13
12
  stderr: ''
@@ -4,9 +4,9 @@ format_version: '2.0'
4
4
  commands:
5
5
  - command_type: Open3::Capture3
6
6
  args:
7
- - ruby
8
- - "--version"
9
- stdout: 'ruby 3.4.4 (2025-05-14 revision a38531fd3f) +PRISM [arm64-darwin24]
7
+ - echo
8
+ - ruby version 3.4.5
9
+ stdout: 'ruby version 3.4.5
10
10
 
11
11
  '
12
12
  stderr: ''
@@ -68,11 +68,11 @@ module Backspin
68
68
  end
69
69
 
70
70
  def stdout_diff
71
- "stdout diff:\n#{generate_line_diff(recorded_command.stdout, actual_command.stdout)}"
71
+ "[stdout]\n#{generate_line_diff(recorded_command.stdout, actual_command.stdout)}"
72
72
  end
73
73
 
74
74
  def stderr_diff
75
- "stderr diff:\n#{generate_line_diff(recorded_command.stderr, actual_command.stderr)}"
75
+ "[stderr]\n#{generate_line_diff(recorded_command.stderr, actual_command.stderr)}"
76
76
  end
77
77
 
78
78
  def generate_line_diff(expected, actual)
@@ -8,11 +8,14 @@ module Backspin
8
8
  attr_accessor :scrub_credentials
9
9
  # The directory where backspin will store its files - defaults to fixtures/backspin
10
10
  attr_accessor :backspin_dir
11
+ # Whether to raise an exception when verification fails in `run` method - defaults to true
12
+ attr_accessor :raise_on_verification_failure
11
13
  # Regex patterns to scrub from saved output
12
14
  attr_reader :credential_patterns
13
15
 
14
16
  def initialize
15
17
  @scrub_credentials = true
18
+ @raise_on_verification_failure = true
16
19
  @credential_patterns = default_credential_patterns
17
20
  @backspin_dir = Pathname(Dir.pwd).join("fixtures", "backspin")
18
21
  end
@@ -10,6 +10,7 @@ module Backspin
10
10
  # Handles stubbing and recording of command executions
11
11
  class Recorder
12
12
  include RSpec::Mocks::ExampleMethods
13
+
13
14
  SUPPORTED_COMMAND_TYPES = %i[capture3 system].freeze
14
15
 
15
16
  attr_reader :commands, :mode, :record, :matcher, :filter
@@ -153,7 +154,6 @@ module Backspin
153
154
  $stdout.reopen(stdout_tempfile)
154
155
  $stderr.reopen(stderr_tempfile)
155
156
 
156
- # Execute the block
157
157
  result = yield
158
158
 
159
159
  # Flush and read captured output
@@ -168,7 +168,7 @@ module Backspin
168
168
  # Create a single command representing all captured output
169
169
  command = Command.new(
170
170
  method_class: Backspin::Capturer,
171
- args: ["<captured block>"],
171
+ args: [],
172
172
  stdout: captured_stdout,
173
173
  stderr: captured_stderr,
174
174
  status: 0,
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Backspin
4
- VERSION = "0.6.0"
4
+ VERSION = "0.7.1"
5
5
  end
data/lib/backspin.rb CHANGED
@@ -19,6 +19,27 @@ require "backspin/record_result"
19
19
  module Backspin
20
20
  class RecordNotFoundError < StandardError; end
21
21
 
22
+ class VerificationError < StandardError
23
+ attr_reader :result
24
+
25
+ def initialize(message, result: nil)
26
+ super(message)
27
+ @result = result
28
+ end
29
+
30
+ def diff
31
+ result.diff
32
+ end
33
+
34
+ def recorded_commands
35
+ result.command_diffs.map(&:recorded_command)
36
+ end
37
+
38
+ def actual_commands
39
+ result.command_diffs.map(&:actual_command)
40
+ end
41
+ end
42
+
22
43
  # Include RSpec mocks methods
23
44
  extend RSpec::Mocks::ExampleMethods
24
45
 
@@ -79,7 +100,7 @@ module Backspin
79
100
  recorder = Recorder.new(record: record, mode: mode, matcher: matcher, filter: filter)
80
101
 
81
102
  # Execute the appropriate mode
82
- case mode
103
+ result = case mode
83
104
  when :record
84
105
  recorder.setup_recording_stubs(:capture3, :system)
85
106
  recorder.perform_recording(&block)
@@ -90,6 +111,17 @@ module Backspin
90
111
  else
91
112
  raise ArgumentError, "Unknown mode: #{mode}"
92
113
  end
114
+
115
+ # Check if we should raise on verification failure
116
+ if configuration.raise_on_verification_failure && result.verified? == false
117
+ error_message = "Backspin verification failed!\n"
118
+ error_message += "Record: #{result.record.path}\n"
119
+ error_message += "\n#{result.error_message}" if result.error_message
120
+
121
+ raise RSpec::Expectations::ExpectationNotMetError, error_message
122
+ end
123
+
124
+ result
93
125
  end
94
126
 
95
127
  # Strict version of run that raises on verification failure
@@ -141,7 +173,7 @@ module Backspin
141
173
  recorder = Recorder.new(record: record, mode: mode, matcher: matcher, filter: filter)
142
174
 
143
175
  # Execute the appropriate mode
144
- case mode
176
+ result = case mode
145
177
  when :record
146
178
  recorder.perform_capture_recording(&block)
147
179
  when :verify
@@ -151,6 +183,22 @@ module Backspin
151
183
  else
152
184
  raise ArgumentError, "Unknown mode: #{mode}"
153
185
  end
186
+
187
+ # Check if we should raise on verification failure
188
+ if configuration.raise_on_verification_failure && result.verified? == false
189
+ error_message = "Backspin verification failed!\n"
190
+ error_message += "Record: #{result.record.path}\n"
191
+ error_message += result.error_message || "Output verification failed"
192
+
193
+ # Include diff if available
194
+ if result.diff
195
+ error_message += "\n\nDiff:\n#{result.diff}"
196
+ end
197
+
198
+ raise VerificationError.new(error_message, result: result)
199
+ end
200
+
201
+ result
154
202
  end
155
203
 
156
204
  private
data/script/lint CHANGED
@@ -1,6 +1,22 @@
1
1
  #!/usr/bin/env bash
2
2
  set -euo pipefail
3
3
 
4
- # Run Standard Ruby linter
5
- echo "Running Standard Ruby linter..."
6
- bundle exec standardrb "$@"
4
+ FIX_OPT="--no-fix"
5
+ while [[ $# -gt 0 ]]; do
6
+ case $1 in
7
+ -f|--fix)
8
+ FIX_OPT="--fix"
9
+ shift
10
+ ;;
11
+ --fix-unsafely)
12
+ FIX_OPT="--fix-unsafely"
13
+ shift
14
+ ;;
15
+ *)
16
+ echo "Error: Unknown option: $1"
17
+ exit 1
18
+ ;;
19
+ esac
20
+ done
21
+
22
+ bundle exec standardrb "$@" $FIX_OPT
@@ -0,0 +1,179 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "json"
5
+ require "pathname"
6
+ require "open3"
7
+
8
+ class AffectedTestRunner
9
+ EXIT_SUCCESS = 0
10
+ EXIT_FAILURE = 2
11
+
12
+ def initialize
13
+ @project_dir = ENV["CLAUDE_PROJECT_DIR"]
14
+ validate_environment!
15
+ end
16
+
17
+ def run
18
+ input_data = parse_input
19
+ file_path = extract_file_path(input_data)
20
+
21
+ return EXIT_SUCCESS unless ruby_file?(file_path)
22
+
23
+ validated_path = validate_and_normalize_path(file_path)
24
+ return EXIT_SUCCESS unless validated_path
25
+
26
+ test_file = determine_test_file(validated_path)
27
+ return EXIT_SUCCESS unless test_file
28
+
29
+ run_tests(test_file)
30
+ rescue => e
31
+ abort_to_claude("Unexpected error: #{e.message}")
32
+ end
33
+
34
+ private
35
+
36
+ def validate_environment!
37
+ unless @project_dir
38
+ abort_to_claude("CLAUDE_PROJECT_DIR environment variable not set")
39
+ end
40
+
41
+ unless Dir.exist?(@project_dir)
42
+ abort_to_claude("CLAUDE_PROJECT_DIR does not exist: #{@project_dir}")
43
+ end
44
+
45
+ @project_path = Pathname.new(@project_dir).realpath
46
+ rescue => e
47
+ abort_to_claude("Invalid CLAUDE_PROJECT_DIR: #{e.message}")
48
+ end
49
+
50
+ def parse_input
51
+ input = $stdin.read
52
+ JSON.parse(input)
53
+ rescue JSON::ParserError => e
54
+ abort_to_claude("Invalid JSON input: #{e.message}")
55
+ end
56
+
57
+ def extract_file_path(data)
58
+ file_path = data.dig("tool_input", "file_path")
59
+
60
+ unless file_path && !file_path.empty?
61
+ abort_to_claude("No file_path provided in input")
62
+ end
63
+
64
+ file_path
65
+ end
66
+
67
+ def ruby_file?(file_path)
68
+ file_path.end_with?(".rb")
69
+ end
70
+
71
+ def validate_and_normalize_path(file_path)
72
+ # Expand the path to get absolute path
73
+ expanded_path = File.expand_path(file_path, @project_dir)
74
+ normalized_path = Pathname.new(expanded_path).cleanpath
75
+
76
+ # Check if the path is within the project directory
77
+ unless normalized_path.to_s.start_with?(@project_path.to_s)
78
+ log_info("File path outside project directory: #{file_path}")
79
+ return nil
80
+ end
81
+
82
+ # Convert back to relative path from project root for consistency
83
+ normalized_path.relative_path_from(@project_path).to_s
84
+ rescue => e
85
+ log_info("Invalid file path: #{file_path} - #{e.message}")
86
+ nil
87
+ end
88
+
89
+ def determine_test_file(file_path)
90
+ log_info("Determining tests for: #{file_path}")
91
+
92
+ if spec_file?(file_path)
93
+ # If a spec file was modified, run just that spec
94
+ test_file = file_path
95
+ log_info("Running single spec: #{test_file}")
96
+ elsif lib_file?(file_path)
97
+ # If a lib file was modified, try to find corresponding spec
98
+ test_file = lib_to_spec_path(file_path)
99
+
100
+ if File.exist?(File.join(@project_dir, test_file))
101
+ log_info("Running corresponding spec: #{test_file}")
102
+ else
103
+ # No corresponding spec found, run all tests
104
+ log_info("No corresponding spec found, running all tests")
105
+ test_file = "spec"
106
+ end
107
+ else
108
+ # For other Ruby files, run all tests
109
+ log_info("Running all tests")
110
+ test_file = "spec"
111
+ end
112
+
113
+ # Validate test file/directory exists
114
+ full_test_path = File.join(@project_dir, test_file)
115
+ unless File.exist?(full_test_path)
116
+ abort_to_claude("Test file/directory does not exist: #{test_file}")
117
+ end
118
+
119
+ test_file
120
+ end
121
+
122
+ def spec_file?(file_path)
123
+ file_path.match?(%r{^.*spec/.*_spec\.rb$})
124
+ end
125
+
126
+ def lib_file?(file_path)
127
+ file_path.match?(%r{^.*lib/.*})
128
+ end
129
+
130
+ def lib_to_spec_path(lib_path)
131
+ # Convert lib/backspin/foo.rb to spec/backspin/foo_spec.rb
132
+ lib_path.sub(%r{^(.*/)?lib/}, '\1spec/')
133
+ .sub(/\.rb$/, "_spec.rb")
134
+ end
135
+
136
+ def run_tests(test_file)
137
+ rspec_path = File.join(@project_dir, "bin", "rspec")
138
+
139
+ unless File.executable?(rspec_path)
140
+ abort_to_claude("rspec binary not found or not executable: #{rspec_path}")
141
+ end
142
+
143
+ log_info("Running: bin/rspec #{test_file}")
144
+
145
+ # Change to project directory for command execution
146
+ Dir.chdir(@project_dir) do
147
+ stdout, stderr, status = Open3.capture3("bin/rspec", test_file)
148
+
149
+ # Output both stdout and stderr
150
+ $stdout.print stdout
151
+ $stderr.print stderr
152
+
153
+ if status.success?
154
+ log_info("✅ Tests passed")
155
+ EXIT_SUCCESS
156
+ else
157
+ warn("❌ Tests failed - fix before continuing")
158
+ EXIT_FAILURE
159
+ end
160
+ end
161
+ rescue => e
162
+ abort_to_claude("Failed to run tests: #{e.message}")
163
+ end
164
+
165
+ def log_info(message)
166
+ warn(message)
167
+ end
168
+
169
+ def abort_to_claude(message)
170
+ warn("❌ ERROR: #{message}")
171
+ exit EXIT_FAILURE
172
+ end
173
+ end
174
+
175
+ # Run the script
176
+ if __FILE__ == $0
177
+ runner = AffectedTestRunner.new
178
+ exit runner.run
179
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: backspin
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.7.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rob Sanheim
@@ -112,11 +112,11 @@ files:
112
112
  - fixtures/backspin/playback_system.yml
113
113
  - fixtures/backspin/playback_test.yml
114
114
  - fixtures/backspin/stderr_test.yml
115
+ - fixtures/backspin/strict_test.yml
115
116
  - fixtures/backspin/string_symbol_test.yml
116
117
  - fixtures/backspin/system_echo.yml
117
118
  - fixtures/backspin/system_false.yml
118
119
  - fixtures/backspin/timestamp_test.yml
119
- - fixtures/backspin/use_record_filter.yml
120
120
  - fixtures/backspin/verify_system.yml
121
121
  - fixtures/backspin/verify_system_diff.yml
122
122
  - fixtures/backspin/version_test.yml
@@ -132,6 +132,7 @@ files:
132
132
  - lib/backspin/version.rb
133
133
  - release.rake
134
134
  - script/lint
135
+ - script/run_affected_tests
135
136
  homepage: https://github.com/rsanheim/backspin
136
137
  licenses:
137
138
  - MIT
@@ -153,7 +154,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
153
154
  - !ruby/object:Gem::Version
154
155
  version: '0'
155
156
  requirements: []
156
- rubygems_version: 3.6.7
157
+ rubygems_version: 3.7.2
157
158
  specification_version: 4
158
159
  summary: Record and replay CLI interactions for testing
159
160
  test_files: []