backspin 0.3.0 → 0.4.0

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: 9afcbf90256cbfd84f6b9694544282dc593fc715d9886a9e3a9ba54e064410fc
4
- data.tar.gz: c3d6e95547f27364b4108b5f75641ed88a9e153f0e4ccdbed3d276e82dd5f99b
3
+ metadata.gz: 59bc6e711951434fc84dd2c2694f69aa5b00ef3bf878b81c5b4e97beeb430ad6
4
+ data.tar.gz: f9784962ef86ad73e0eb1c8388217700e477bc60ce0528b5dd16f08bc5c2f637
5
5
  SHA512:
6
- metadata.gz: 2c7c36cee5518f0944896c04c96d36ae134d56928510ee01aef646a2eb0c036c723ab65aff64c6726751ce6b4c406de90f486e4da5bd3242344fd23d8e7792f1
7
- data.tar.gz: 116e28fadefdc8651f648f25f0da4c347d86127448a34e0ca20a244f6bb0e7f101e140052e0f0e9029ec1693b40b6363ab3d7db6a0b7c2a0ab9252976ae64c33
6
+ metadata.gz: e4151f4a28909e1a80706a30520b268f5114124230df9869a3603d5502194f4885ca8dff19e15a9a8ffe5f559b14695614f9f113c66e82ccc105de5b7ae17006
7
+ data.tar.gz: a66f71d1a4b2e9404ca39178920d8b88686f5e8cec2fe62f282b1cda9de031c1041cdfa1007ee9dc725c24cfb1c4dad5445213091a20c0faf23165705a8d264e
data/.circleci/config.yml CHANGED
@@ -7,6 +7,7 @@ orbs:
7
7
 
8
8
  jobs:
9
9
  build:
10
+ resource_class: medium
10
11
  parameters:
11
12
  ruby-version:
12
13
  type: string
@@ -15,7 +16,8 @@ jobs:
15
16
 
16
17
  steps:
17
18
  - checkout
18
- - ruby/install-deps
19
+ - ruby/install-deps:
20
+ key: gems-v1-ruby<< parameters.ruby-version >>
19
21
  - run:
20
22
  name: Run specs and lint
21
23
  command: bundle exec rake
data/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.4.0] - 2025-06-06
4
+
5
+ Simpler, unified API: `Backspin.run` and `Backspin run!` methods that automatically record on first use and verify on subsequent runs. `run!` will raise an error if results differ, whereas `run` will return the result for the caller to decide what to do with
6
+
3
7
  ## [0.3.0] - 2025-06-05
4
8
  - Scrub credentials from command arguments
5
9
 
data/CLAUDE.md CHANGED
@@ -17,14 +17,14 @@ bin/setup
17
17
  ### Testing
18
18
  ```bash
19
19
  bin/rake spec # Run all tests
20
- rspec spec/[file] # Run specific test file
21
- rspec spec/[file]:[line] # Run specific test
20
+ bin/rspec spec/[file] # Run specific test file
21
+ bin/rspec spec/[file]:[line] # Run specific test
22
22
  ```
23
23
 
24
24
  ### Building and Releasing
25
25
  ```bash
26
- bundle exec rake install # Install gem locally for testing
27
- bundle exec rake release # Release to RubyGems (updates version, tags, pushes)
26
+ bin/rake install # Install gem locally for testing
27
+ bin/rake release # Release to RubyGems (updates version, tags, pushes)
28
28
  ```
29
29
 
30
30
  ### Code Quality
data/CONTRIBUTING.md CHANGED
@@ -161,7 +161,6 @@ end
161
161
 
162
162
  - Keep changes focused and atomic
163
163
  - Include tests for new functionality
164
- - Maintain backward compatibility when possible
165
164
  - Update examples in README.md if changing public APIs
166
165
  - Ensure CI passes (tests against Ruby 3.2, 3.3, and 3.4)
167
166
 
@@ -203,8 +202,7 @@ We welcome feature requests! When proposing new features:
203
202
  1. Check existing issues to avoid duplicates
204
203
  2. Describe the use case and motivation
205
204
  3. Provide code examples of how the feature would work
206
- 4. Consider backward compatibility
207
- 5. Be open to discussion and alternative approaches
205
+ 4. Be open to discussion and alternative approaches
208
206
 
209
207
  ## Additional Resources
210
208
 
data/Gemfile CHANGED
@@ -7,5 +7,5 @@ group :development do
7
7
  gem "rake", "~> 13.0"
8
8
  gem "rspec", "~> 3.0"
9
9
  gem "timecop", "~> 0.9"
10
- gem "standard", "~> 1.0"
10
+ gem "standard"
11
11
  end
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- backspin (0.3.0)
4
+ backspin (0.4.0)
5
5
  ostruct (~> 0.5.0)
6
6
  rspec-mocks (~> 3.0)
7
7
 
@@ -80,7 +80,7 @@ DEPENDENCIES
80
80
  backspin!
81
81
  rake (~> 13.0)
82
82
  rspec (~> 3.0)
83
- standard (~> 1.0)
83
+ standard
84
84
  timecop (~> 0.9)
85
85
 
86
86
  BUNDLED WITH
data/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # Backspin   [![Gem Version](https://badge.fury.io/rb/backspin.svg)](https://badge.fury.io/rb/backspin) [![CircleCI](https://dl.circleci.com/status-badge/img/gh/rsanheim/backspin/tree/main.svg?style=svg)](https://dl.circleci.com/status-badge/redirect/gh/rsanheim/backspin/tree/main)
2
2
 
3
- Backspin records and replays CLI interactions in Ruby for easy snapshot testing of command-line interfaces. Currently supports `Open3.capture3` and `system` and requires `rspec-mocks`. More system calls and flexible test integration are welcome - PRs welcome!
3
+ Backspin records and replays CLI interactions in Ruby for easy snapshot testing of command-line interfaces. Currently supports `Open3.capture3` and `system` and requires `rspec-mocks`. More system calls and test integrations are welcome - send a PR!
4
4
 
5
- **NOTE:** Backspin is in early development (version 0.2.x), and you can expect the API to change. It is being developed along-side in-production CLI apps, so the API will be refined and improved as we get to 1.0.
5
+ **NOTE:** Backspin should be considered alpha quality software while pre v1.0. It is in heavy development, and you can expect the API to change. It is being developed in conjunction with production CLI apps, so the API will be refined and improved as we get to 1.0.
6
6
 
7
7
  Inspired by [VCR](https://github.com/vcr/vcr) and other [golden master](https://en.wikipedia.org/wiki/Golden_master_(software_development)) libraries.
8
8
 
@@ -24,73 +24,147 @@ And then run `bundle install`.
24
24
 
25
25
  ## Usage
26
26
 
27
- ### Recording CLI interactions
27
+ ### Quick Start
28
+
29
+ The simplest way to use Backspin is with the `run` method, which automatically records on the first execution and verifies on subsequent runs:
28
30
 
29
31
  ```ruby
30
- require "backspin"
32
+ require "backspin"
33
+
34
+ # First run: records the output
35
+ result = Backspin.run("my_command") do
36
+ Open3.capture3("echo hello world")
37
+ end
31
38
 
32
- # Record a command's output
33
- result = Backspin.call("echo_hello") do
34
- stdout, stderr, status = Open3.capture3("echo hello")
35
- # This will save the output to `spec/backspin_data/echo_hello.yaml`.
39
+ # Subsequent runs: verifies the output matches
40
+ result = Backspin.run("my_command") do
41
+ Open3.capture3("echo hello world")
36
42
  end
37
43
 
44
+ # Use run! to automatically fail tests on mismatch
45
+ Backspin.run!("my_command") do
46
+ Open3.capture3("echo hello mars")
47
+ end
48
+ # Raises an error because stdout will not match the recorded output
38
49
  ```
39
50
 
40
- ### Verifying CLI output
51
+ ### Recording Modes
52
+
53
+ Backspin supports different modes for controlling how commands are recorded and verified:
41
54
 
42
55
  ```ruby
43
- # Verify that a command produces the expected output
44
- result = Backspin.verify("echo_hello") do
56
+ # Auto mode (default): Record on first run, verify on subsequent runs
57
+ result = Backspin.run("my_command") do
58
+ Open3.capture3("echo hello")
59
+ end
60
+
61
+ # Explicit record mode: Always record, overwriting existing recordings
62
+ result = Backspin.run("echo_test", mode: :record) do
45
63
  Open3.capture3("echo hello")
46
64
  end
65
+ # This will save the output to `spec/backspin_data/echo_test.yml`.
47
66
 
67
+ # Explicit verify mode: Always verify against existing recording
68
+ result = Backspin.run("echo_test", mode: :verify) do
69
+ Open3.capture3("echo hello")
70
+ end
48
71
  expect(result.verified?).to be true
72
+
73
+ # Playback mode: Return recorded output without running the command
74
+ result = Backspin.run("slow_command", mode: :playback) do
75
+ Open3.capture3("slow_command") # Not executed - returns recorded output
76
+ end
49
77
  ```
50
78
 
51
- ### Using verify! for automatic test failures
79
+ ### Using run! for automatic test failures
80
+
81
+ The `run!` method works exactly like `run` but automatically fails the test if verification fails:
52
82
 
53
83
  ```ruby
54
84
  # Automatically fail the test if output doesn't match
55
- Backspin.verify!("echo_hello") do
85
+ Backspin.run!("echo_test") do
56
86
  Open3.capture3("echo hello")
57
87
  end
88
+ # Raises an error with detailed diff if verification fails from recorded data in "echo_test.yml"
58
89
  ```
59
90
 
60
- ### Playback mode for fast tests
91
+ ### Custom matchers
92
+
93
+ For cases where exact matching isn't suitable, you can provide custom verification logic:
61
94
 
62
95
  ```ruby
63
- # Return recorded output without running the command
64
- result = Backspin.verify("slow_command", mode: :playback) do
65
- Open3.capture3("slow_command") # Not executed - will playback from the record yaml (assuming it exists)
96
+ # Use custom logic to verify output
97
+ result = Backspin.run("version_check",
98
+ matcher: ->(recorded, actual) {
99
+ # Just check that both start with "ruby"
100
+ recorded["stdout"].start_with?("ruby") &&
101
+ actual["stdout"].start_with?("ruby")
102
+ }) do
103
+ Open3.capture3("ruby --version")
66
104
  end
67
105
  ```
68
106
 
69
- ### Custom matchers
107
+ ### Working with the Result Object
108
+
109
+ The API returns a `RecordResult` object with helpful methods:
70
110
 
71
111
  ```ruby
72
- # Use custom logic to verify output
73
- Backspin.verify("version_check",
74
- matcher: ->(recorded, actual) {
75
- # Just check that both start with "ruby"
76
- recorded["stdout"].start_with?("ruby") &&
77
- actual["stdout"].start_with?("ruby")
78
- }) do
79
- Open3.capture3("ruby --version")
112
+ result = Backspin.run("my_test") do
113
+ Open3.capture3("echo out; echo err >&2; exit 42")
80
114
  end
115
+
116
+ # Check the mode
117
+ result.recorded? # true on first run
118
+ result.verified? # true/false on subsequent runs, nil when recording
119
+ result.playback? # true in playback mode
120
+
121
+ # Access output (first command for single commands)
122
+ result.stdout # "out\n"
123
+ result.stderr # "err\n"
124
+ result.status # 42
125
+ result.success? # false (non-zero exit)
126
+ result.output # The raw return value from the block
127
+
128
+ # Debug information
129
+ result.record_path # Path to the YAML file
130
+ result.error_message # Human-readable error if verification failed
131
+ result.diff # Diff between expected and actual output
81
132
  ```
82
133
 
83
- ### VCR-style use_record
134
+ ### Multiple Commands
84
135
 
85
- _The plan is to make something like this the main entry point API for ease of use_
136
+ Backspin automatically records and verifies all commands executed in a block:
86
137
 
87
138
  ```ruby
88
- # Record on first run, replay on subsequent runs
89
- Backspin.use_record("my_command", record: :once) do
90
- Open3.capture3("echo hello")
139
+ result = Backspin.run("multi_command_test") do
140
+ # All of these commands will be recorded
141
+ version, = Open3.capture3("ruby --version")
142
+ files, = Open3.capture3("ls -la")
143
+ system("echo 'Processing...'") # Note: system doesn't capture output
144
+ data, stderr, = Open3.capture3("curl https://api.example.com/data")
145
+
146
+ # Return whatever you need
147
+ { version: version.strip, file_count: files.lines.count, data: data }
91
148
  end
149
+
150
+ # Access individual command results
151
+ result.commands.size # 4
152
+ result.multiple_commands? # true
153
+
154
+ # For multiple commands, use these accessors
155
+ result.all_stdout # Array of stdout from each command
156
+ result.all_stderr # Array of stderr from each command
157
+ result.all_status # Array of exit statuses
158
+
159
+ # Or access specific commands
160
+ result.commands[0].stdout # Ruby version output
161
+ result.commands[1].stdout # ls output
162
+ result.commands[2].status # system call exit status (stdout is empty)
163
+ result.commands[3].stderr # curl errors if any
92
164
  ```
93
165
 
166
+ 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.
167
+
94
168
  ### Credential Scrubbing
95
169
 
96
170
  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!
@@ -102,7 +176,7 @@ A tool like [trufflehog](https://github.com/trufflesecurity/trufflehog) or [gitl
102
176
 
103
177
  ```ruby
104
178
  # This will automatically scrub AWS keys, API tokens, passwords, etc.
105
- Backspin.call("aws_command") do
179
+ Backspin.run("aws_command") do
106
180
  Open3.capture3("aws s3 ls")
107
181
  end
108
182
 
@@ -146,4 +220,4 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/rsanhe
146
220
 
147
221
  ## License
148
222
 
149
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
223
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/backspin.gemspec CHANGED
@@ -20,8 +20,8 @@ Gem::Specification.new do |spec|
20
20
  spec.files = Dir.chdir(File.expand_path("..", __FILE__)) do
21
21
  `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
22
22
  end
23
- spec.bindir = "bin"
24
- spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
23
+ spec.bindir = "exe"
24
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
25
25
  spec.require_paths = ["lib"]
26
26
 
27
27
  spec.add_dependency "rspec-mocks", "~> 3.0"
data/bin/rake ADDED
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'rake' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
12
+
13
+ bundle_binstub = File.expand_path("bundle", __dir__)
14
+
15
+ if File.file?(bundle_binstub)
16
+ if File.read(bundle_binstub, 300).include?("This file was generated by Bundler")
17
+ load(bundle_binstub)
18
+ else
19
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
20
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
21
+ end
22
+ end
23
+
24
+ require "rubygems"
25
+ require "bundler/setup"
26
+
27
+ load Gem.bin_path("rake", "rake")
data/bin/rspec ADDED
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'rspec' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
12
+
13
+ bundle_binstub = File.expand_path("bundle", __dir__)
14
+
15
+ if File.file?(bundle_binstub)
16
+ if File.read(bundle_binstub, 300).include?("This file was generated by Bundler")
17
+ load(bundle_binstub)
18
+ else
19
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
20
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
21
+ end
22
+ end
23
+
24
+ require "rubygems"
25
+ require "bundler/setup"
26
+
27
+ load Gem.bin_path("rspec-core", "rspec")
@@ -1,14 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "command_result"
4
+
1
5
  module Backspin
2
6
  class Command
3
- attr_reader :args, :stdout, :stderr, :status, :recorded_at, :method_class
7
+ attr_reader :args, :result, :recorded_at, :method_class
4
8
 
5
- def initialize(method_class:, args:, stdout: nil, stderr: nil, status: nil, recorded_at: nil)
9
+ def initialize(method_class:, args:, stdout: nil, stderr: nil, status: nil, result: nil, recorded_at: nil)
6
10
  @method_class = method_class
7
11
  @args = args
8
- @stdout = stdout
9
- @stderr = stderr
10
- @status = status
11
12
  @recorded_at = recorded_at
13
+
14
+ # Accept either a CommandResult or individual stdout/stderr/status
15
+ @result = result || CommandResult.new(
16
+ stdout: stdout || "",
17
+ stderr: stderr || "",
18
+ status: status || 0
19
+ )
20
+ end
21
+
22
+ def stdout
23
+ @result.stdout
24
+ end
25
+
26
+ def stderr
27
+ @result.stderr
28
+ end
29
+
30
+ def status
31
+ @result.status
12
32
  end
13
33
 
14
34
  # Convert to hash for YAML serialization
@@ -16,16 +36,14 @@ module Backspin
16
36
  data = {
17
37
  "command_type" => @method_class.name,
18
38
  "args" => scrub_args(@args),
19
- "stdout" => Backspin.scrub_text(@stdout),
20
- "stderr" => Backspin.scrub_text(@stderr),
21
- "status" => @status,
39
+ "stdout" => Backspin.scrub_text(@result.stdout),
40
+ "stderr" => Backspin.scrub_text(@result.stderr),
41
+ "status" => @result.status,
22
42
  "recorded_at" => @recorded_at
23
43
  }
24
44
 
25
45
  # Apply filter if provided
26
- if filter
27
- data = filter.call(data)
28
- end
46
+ data = filter.call(data) if filter
29
47
 
30
48
  data
31
49
  end
@@ -59,11 +77,12 @@ module Backspin
59
77
  return args unless Backspin.configuration.scrub_credentials && args
60
78
 
61
79
  args.map do |arg|
62
- if arg.is_a?(String)
80
+ case arg
81
+ when String
63
82
  Backspin.scrub_text(arg)
64
- elsif arg.is_a?(Array)
83
+ when Array
65
84
  scrub_args(arg)
66
- elsif arg.is_a?(Hash)
85
+ when Hash
67
86
  arg.transform_values { |v| v.is_a?(String) ? Backspin.scrub_text(v) : v }
68
87
  else
69
88
  arg
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Backspin
4
+ # Represents the difference between a recorded command and actual execution
5
+ # Handles verification and diff generation for a single command
6
+ class CommandDiff
7
+ attr_reader :recorded_command, :actual_result, :matcher
8
+
9
+ def initialize(recorded_command:, actual_result:, matcher: nil)
10
+ @recorded_command = recorded_command
11
+ @actual_result = actual_result
12
+ @matcher = matcher
13
+ end
14
+
15
+ # @return [Boolean] true if the command output matches
16
+ def verified?
17
+ if matcher
18
+ matcher.call(recorded_command.to_h, actual_result.to_h)
19
+ else
20
+ recorded_command.result == actual_result
21
+ end
22
+ end
23
+
24
+ # @return [String, nil] Human-readable diff if not verified
25
+ def diff
26
+ return nil if verified?
27
+
28
+ parts = []
29
+
30
+ parts << stdout_diff if recorded_command.stdout != actual_result.stdout
31
+
32
+ parts << stderr_diff if recorded_command.stderr != actual_result.stderr
33
+
34
+ if recorded_command.status != actual_result.status
35
+ parts << "Exit status: expected #{recorded_command.status}, got #{actual_result.status}"
36
+ end
37
+
38
+ parts.join("\n\n")
39
+ end
40
+
41
+ # @return [String] Single line summary for error messages
42
+ def summary
43
+ if verified?
44
+ "✓ Command verified"
45
+ else
46
+ "✗ Command failed: #{failure_reason}"
47
+ end
48
+ end
49
+
50
+ private
51
+
52
+ def failure_reason
53
+ reasons = []
54
+ reasons << "stdout differs" if recorded_command.stdout != actual_result.stdout
55
+ reasons << "stderr differs" if recorded_command.stderr != actual_result.stderr
56
+ reasons << "exit status differs" if recorded_command.status != actual_result.status
57
+ reasons.join(", ")
58
+ end
59
+
60
+ def stdout_diff
61
+ "stdout diff:\n#{generate_line_diff(recorded_command.stdout, actual_result.stdout)}"
62
+ end
63
+
64
+ def stderr_diff
65
+ "stderr diff:\n#{generate_line_diff(recorded_command.stderr, actual_result.stderr)}"
66
+ end
67
+
68
+ def generate_line_diff(expected, actual)
69
+ expected_lines = (expected || "").lines
70
+ actual_lines = (actual || "").lines
71
+
72
+ diff_lines = []
73
+ max_lines = [expected_lines.length, actual_lines.length].max
74
+
75
+ max_lines.times do |i|
76
+ expected_line = expected_lines[i]
77
+ actual_line = actual_lines[i]
78
+
79
+ if expected_line != actual_line
80
+ diff_lines << "-#{expected_line.chomp}" if expected_line
81
+ diff_lines << "+#{actual_line.chomp}" if actual_line
82
+ end
83
+ end
84
+
85
+ diff_lines.join("\n")
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Backspin
4
+ # Represents the result of executing a command
5
+ # Stores stdout, stderr, and exit status
6
+ class CommandResult
7
+ attr_reader :stdout, :stderr, :status
8
+
9
+ def initialize(stdout:, stderr:, status:)
10
+ @stdout = stdout
11
+ @stderr = stderr
12
+ @status = normalize_status(status)
13
+ end
14
+
15
+ # @return [Boolean] true if the command succeeded (exit status 0)
16
+ def success?
17
+ status.zero?
18
+ end
19
+
20
+ # @return [Boolean] true if the command failed (non-zero exit status)
21
+ def failure?
22
+ !success?
23
+ end
24
+
25
+ # @return [Hash] Hash representation of the result
26
+ def to_h
27
+ {
28
+ "stdout" => stdout,
29
+ "stderr" => stderr,
30
+ "status" => status
31
+ }
32
+ end
33
+
34
+ # Compare two results for equality
35
+ def ==(other)
36
+ return false unless other.is_a?(CommandResult)
37
+
38
+ stdout == other.stdout &&
39
+ stderr == other.stderr &&
40
+ status == other.status
41
+ end
42
+
43
+ def inspect
44
+ "#<Backspin::CommandResult status=#{status} stdout=#{stdout.inspect.truncate(50)} stderr=#{stderr.inspect.truncate(50)}>"
45
+ end
46
+
47
+ private
48
+
49
+ def normalize_status(status)
50
+ case status
51
+ when Integer
52
+ status
53
+ when Process::Status
54
+ status.exitstatus
55
+ else
56
+ status.respond_to?(:exitstatus) ? status.exitstatus : status.to_i
57
+ end
58
+ end
59
+ end
60
+ end
@@ -4,6 +4,7 @@ module Backspin
4
4
  class NoMoreRecordingsError < StandardError; end
5
5
 
6
6
  class Record
7
+ FORMAT_VERSION = "2.0"
7
8
  attr_reader :path, :commands, :first_recorded_at
8
9
 
9
10
  def initialize(path)
@@ -22,10 +23,9 @@ module Backspin
22
23
 
23
24
  def save(filter: nil)
24
25
  FileUtils.mkdir_p(File.dirname(@path))
25
- # New format: top-level metadata with commands array
26
26
  record_data = {
27
27
  "first_recorded_at" => @first_recorded_at,
28
- "format_version" => "2.0",
28
+ "format_version" => FORMAT_VERSION,
29
29
  "commands" => @commands.map { |cmd| cmd.to_h(filter: filter) }
30
30
  }
31
31
  File.write(@path, record_data.to_yaml)