backspin 0.10.0 → 0.12.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.
Files changed (89) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +3 -0
  3. data/CHANGELOG.md +11 -1
  4. data/Gemfile.lock +3 -1
  5. data/README.md +82 -0
  6. data/Rakefile +27 -0
  7. data/backspin.gemspec +2 -0
  8. data/docs/backspin-result-api-sketch.md +8 -6
  9. data/fixtures/backspin/.gitkeep +1 -0
  10. data/fixtures/projects/dummy_cli_gem/.rspec +3 -0
  11. data/fixtures/projects/dummy_cli_gem/Gemfile +12 -0
  12. data/fixtures/projects/dummy_cli_gem/Gemfile.lock +55 -0
  13. data/fixtures/projects/dummy_cli_gem/LICENSE.txt +1 -0
  14. data/fixtures/projects/dummy_cli_gem/README.md +7 -0
  15. data/fixtures/projects/dummy_cli_gem/Rakefile +8 -0
  16. data/fixtures/projects/dummy_cli_gem/dummy_cli_gem.gemspec +22 -0
  17. data/fixtures/projects/dummy_cli_gem/exe/dummy_cli_gem +8 -0
  18. data/fixtures/projects/dummy_cli_gem/fixtures/backspin/dummy_echo.yml +18 -0
  19. data/fixtures/projects/dummy_cli_gem/fixtures/backspin/dummy_ls.yml +18 -0
  20. data/fixtures/projects/dummy_cli_gem/lib/dummy_cli_gem/cli.rb +35 -0
  21. data/fixtures/projects/dummy_cli_gem/lib/dummy_cli_gem/version.rb +5 -0
  22. data/fixtures/projects/dummy_cli_gem/lib/dummy_cli_gem.rb +7 -0
  23. data/fixtures/projects/dummy_cli_gem/mise.toml +2 -0
  24. data/fixtures/projects/dummy_cli_gem/spec/dummy_cli_gem_backspin_spec.rb +46 -0
  25. data/fixtures/projects/dummy_cli_gem/spec/fixtures/backspin/dummy_echo.yml +18 -0
  26. data/fixtures/projects/dummy_cli_gem/spec/fixtures/backspin/dummy_ls.yml +18 -0
  27. data/fixtures/projects/dummy_cli_gem/spec/fixtures/listing_target/alpha.txt +1 -0
  28. data/fixtures/projects/dummy_cli_gem/spec/spec_helper.rb +22 -0
  29. data/lib/backspin/configuration.rb +13 -0
  30. data/lib/backspin/record.rb +42 -5
  31. data/lib/backspin/version.rb +1 -1
  32. data/lib/backspin.rb +24 -12
  33. data/mise.toml +2 -0
  34. metadata +37 -57
  35. data/fixtures/backspin/all_and_fields.yml +0 -15
  36. data/fixtures/backspin/all_bypass_equality.yml +0 -14
  37. data/fixtures/backspin/all_checks_equality.yml +0 -17
  38. data/fixtures/backspin/all_for_logging.yml +0 -14
  39. data/fixtures/backspin/all_matcher_basic.yml +0 -14
  40. data/fixtures/backspin/all_matcher_custom.yml +0 -17
  41. data/fixtures/backspin/all_matcher_demo.yml +0 -14
  42. data/fixtures/backspin/all_matcher_test.yml +0 -14
  43. data/fixtures/backspin/all_mode_filter.yml +0 -14
  44. data/fixtures/backspin/all_no_short_circuit.yml +0 -14
  45. data/fixtures/backspin/all_pass_field_fail.yml +0 -14
  46. data/fixtures/backspin/all_short_circuit.yml +0 -14
  47. data/fixtures/backspin/all_skips_equality.yml +0 -17
  48. data/fixtures/backspin/all_with_equality.yml +0 -17
  49. data/fixtures/backspin/all_with_fields.yml +0 -17
  50. data/fixtures/backspin/combined_fail_demo.yml +0 -14
  51. data/fixtures/backspin/combined_matcher_demo.yml +0 -14
  52. data/fixtures/backspin/credential_filter.yml +0 -18
  53. data/fixtures/backspin/echo_hello.yml +0 -14
  54. data/fixtures/backspin/echo_verify.yml +0 -14
  55. data/fixtures/backspin/episodes_filter.yml +0 -26
  56. data/fixtures/backspin/failure_test.yml +0 -14
  57. data/fixtures/backspin/field_matcher_demo.yml +0 -17
  58. data/fixtures/backspin/field_matcher_values.yml +0 -14
  59. data/fixtures/backspin/full_data_filter.yml +0 -17
  60. data/fixtures/backspin/key_confusion_test.yml +0 -14
  61. data/fixtures/backspin/match_on_any_fail.yml +0 -21
  62. data/fixtures/backspin/match_on_bad_format.yml +0 -14
  63. data/fixtures/backspin/match_on_fail.yml +0 -15
  64. data/fixtures/backspin/match_on_invalid.yml +0 -14
  65. data/fixtures/backspin/match_on_multiple.yml +0 -28
  66. data/fixtures/backspin/match_on_nil.yml +0 -14
  67. data/fixtures/backspin/match_on_other_fields.yml +0 -23
  68. data/fixtures/backspin/match_on_run_bang.yml +0 -16
  69. data/fixtures/backspin/match_on_run_bang_fail.yml +0 -15
  70. data/fixtures/backspin/match_on_single.yml +0 -17
  71. data/fixtures/backspin/mixed_calls.yml +0 -24
  72. data/fixtures/backspin/multi_command.yml +0 -34
  73. data/fixtures/backspin/multi_command_filter.yml +0 -26
  74. data/fixtures/backspin/multi_field_filter.yml +0 -13
  75. data/fixtures/backspin/multi_system.yml +0 -20
  76. data/fixtures/backspin/nil_filter.yml +0 -14
  77. data/fixtures/backspin/none_mode_test.yml +0 -14
  78. data/fixtures/backspin/path_test.yml +0 -17
  79. data/fixtures/backspin/playback_system.yml +0 -12
  80. data/fixtures/backspin/playback_test.yml +0 -14
  81. data/fixtures/backspin/stderr_test.yml +0 -19
  82. data/fixtures/backspin/strict_test.yml +0 -14
  83. data/fixtures/backspin/string_symbol_test.yml +0 -14
  84. data/fixtures/backspin/system_echo.yml +0 -12
  85. data/fixtures/backspin/system_false.yml +0 -18
  86. data/fixtures/backspin/timestamp_test.yml +0 -18
  87. data/fixtures/backspin/verify_system.yml +0 -12
  88. data/fixtures/backspin/verify_system_diff.yml +0 -11
  89. data/fixtures/backspin/version_test.yml +0 -14
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: edf2daa2123a270ca28d593b594188db57913b2ca5905a45b6a9e0d1221c0da4
4
- data.tar.gz: 3ec4407d3396e3e8390f62b3c0e7d2124bdf026e4b02e74c036ee37fab8430a4
3
+ metadata.gz: 6373427307003e8469435ce1c461e6f9bf6ff307c952bce87f07c393470f5fdc
4
+ data.tar.gz: 3e8487ba8345a4fef0b8419487de0e84b756c302ada8b313e2bf0e5cd6c1ec1a
5
5
  SHA512:
6
- metadata.gz: a851d194aa4f400c9104643b5521e7b5bd5e159708a0d6114c70cfd39907fb6174041a3bccffdc824edb3e224bab7db453b906c35adc7f0a2908d2b034a9b11a
7
- data.tar.gz: 01b1c9b4288f978d9946090afda49e17001732ec0b4f5e47d3831767975d731e5c940cc0fbdd7ccfca4a2bdf54bc8d281eba7d3dc4cbbd137e3f4edb576d9378
6
+ metadata.gz: d7f1915e4632d8fb4b08ae6a8ff0e6e9846ed3f1cd783c7dcc283ca2d3816afb518173a50cd519b5859eb6d991be7636a81a48e34fb63f0176d14c9a2bdd500a
7
+ data.tar.gz: 7307a43c18730ec430f39dbadecb301eaad177ecc3795906f9682ec3e4781e9d5d9ec2b8c913220f8c5985393ae90e73974e5f2855a40ea17d33f87914b5a527
data/.circleci/config.yml CHANGED
@@ -20,6 +20,9 @@ jobs:
20
20
  - run:
21
21
  name: Run specs
22
22
  command: bundle exec rake spec
23
+ - run:
24
+ name: Run fake gem specs
25
+ command: bundle exec rake spec:fake_gem
23
26
 
24
27
  lint:
25
28
  resource_class: medium
data/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.12.0
4
+ * Added `BACKSPIN_MODE` environment variable to globally override recording mode (`auto`, `record`, `verify`).
5
+ * Explicit `mode:` kwarg still takes highest precedence, followed by the env var, then auto-detection.
6
+ * Added configurable logger to `Backspin::Configuration` (defaults to WARN level, logfmt-lite format, and can be disabled with `config.logger = nil`).
7
+
8
+ ## 0.11.0 - 2026-02-11
9
+ * Added immutable top-level `first_recorded_at` metadata for record files.
10
+ * Added mutable top-level `recorded_at` metadata that updates on each successful re-record.
11
+ * Added top-level `record_count`, incremented on each successful record write.
12
+ * Bumped record format to 4.1; loading remains backward-compatible with 4.0 record files.
13
+
3
14
  ## 0.10.0 - 2026-02-11
4
15
  * Added `filter_on` to `Backspin.run` and `Backspin.capture` (`:both` default, `:record` opt-out).
5
16
  * Changed default filter behavior: `filter` now applies during verify comparisons/diffs when `filter_on: :both`.
@@ -11,7 +22,6 @@
11
22
  * Breaking: result convenience accessors (`result.stdout`, `result.stderr`, `result.status`) were removed in favor of snapshot access.
12
23
  * Breaking: record format bumped to 4.0 and now persists a single `snapshot` object (v3 records are rejected).
13
24
  * Simplification: removed legacy `Command`, `CommandResult`, and `RecordResult` layers; matcher/diff now operate directly on snapshots.
14
- * Added focused coverage for the new result contract and capture stream restoration behavior.
15
25
  * Updated project docs to reflect the BackspinResult + Snapshot API surface.
16
26
 
17
27
  ## 0.8.0 - 2026-02-05
data/Gemfile.lock CHANGED
@@ -1,7 +1,8 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- backspin (0.10.0)
4
+ backspin (0.12.0)
5
+ logger
5
6
 
6
7
  GEM
7
8
  remote: https://rubygems.org/
@@ -11,6 +12,7 @@ GEM
11
12
  json (2.16.0)
12
13
  language_server-protocol (3.17.0.5)
13
14
  lint_roller (1.1.0)
15
+ logger (1.7.0)
14
16
  parallel (1.27.0)
15
17
  parser (3.3.10.0)
16
18
  ast (~> 2.4.1)
data/README.md CHANGED
@@ -91,6 +91,48 @@ result = Backspin.run(["echo", "hello"], name: "echo_test", mode: :verify)
91
91
  expect(result.verified?).to be true
92
92
  ```
93
93
 
94
+ ### Environment Variable Mode Override
95
+
96
+ Set `BACKSPIN_MODE` to globally force a recording mode without changing any test code:
97
+
98
+ ```bash
99
+ # Re-record all fixtures
100
+ BACKSPIN_MODE=record bundle exec rspec
101
+
102
+ # Verify-only (CI, no accidental re-records)
103
+ BACKSPIN_MODE=verify bundle exec rspec
104
+ ```
105
+
106
+ Precedence (highest to lowest):
107
+
108
+ 1. Explicit `mode:` kwarg (`:record` or `:verify`)
109
+ 2. `BACKSPIN_MODE` environment variable
110
+ 3. Auto-detection (record if no file exists, verify if it does)
111
+
112
+ Allowed values: `auto`, `record`, `verify` (case-insensitive). Invalid values raise `ArgumentError`.
113
+
114
+ ### Record Metadata
115
+
116
+ Backspin writes records using `format_version: "4.1"` with top-level metadata:
117
+
118
+ ```yaml
119
+ ---
120
+ format_version: "4.1"
121
+ first_recorded_at: "2026-01-01T10:00:00Z" # immutable
122
+ recorded_at: "2026-02-01T10:00:00Z" # updates on each write
123
+ record_count: 3 # increments on each write
124
+ snapshot:
125
+ command_type: "Open3::Capture3"
126
+ args: ["echo", "hello"]
127
+ stdout: "hello\n"
128
+ stderr: ""
129
+ status: 0
130
+ recorded_at: "2026-02-01T10:00:00Z"
131
+ ```
132
+
133
+ When re-recording with `mode: :record`, Backspin preserves `first_recorded_at`, updates `recorded_at`, and increments `record_count`.
134
+ Existing `4.0` records still load and are upgraded to `4.1` metadata on the next write.
135
+
94
136
  ### Environment Variables
95
137
 
96
138
  ```ruby
@@ -221,6 +263,34 @@ result = Backspin.run(["echo", "different"], name: "my_test")
221
263
  Backspin.reset_configuration!
222
264
  ```
223
265
 
266
+ ### Logging
267
+
268
+ Backspin includes a configurable logger for diagnostics. By default it is set to WARN level; but most messages are logged at DEBUG level.
269
+ So if you are looking for more detailed logs, you can set the logger to DEBUG level:
270
+
271
+ ```ruby
272
+ Backspin.configure do |config|
273
+ config.logger = Logger.new($stdout)
274
+ config.logger.level = Logger::DEBUG
275
+ end
276
+ ```
277
+
278
+ To replace the logger entirely:
279
+
280
+ ```ruby
281
+ Backspin.configure do |config|
282
+ config.logger = Logger.new("log/backspin.log")
283
+ end
284
+ ```
285
+
286
+ To disable Backspin logging entirely (for example in tests):
287
+
288
+ ```ruby
289
+ Backspin.configure do |config|
290
+ config.logger = nil
291
+ end
292
+ ```
293
+
224
294
  ### Credential Scrubbing
225
295
 
226
296
  If the CLI interaction you are recording contains sensitive data in stdout/stderr, you should be careful to make sure it is not recorded to YAML.
@@ -252,6 +322,18 @@ Automatic scrubbing includes:
252
322
 
253
323
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests.
254
324
 
325
+ This repo also includes a decoupled full-stack fixture gem at `fixtures/projects/dummy_cli_gem` that uses Backspin the way downstream projects do. Run it with:
326
+
327
+ ```bash
328
+ bundle exec rake full_stack:dummy_app
329
+ ```
330
+
331
+ To re-record that fixture's committed YAML snapshots:
332
+
333
+ ```bash
334
+ bundle exec rake full_stack:record_dummy_app
335
+ ```
336
+
255
337
  To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
256
338
 
257
339
  ## Contributing
data/Rakefile CHANGED
@@ -6,6 +6,33 @@ require "standard/rake"
6
6
 
7
7
  RSpec::Core::RakeTask.new(:spec)
8
8
 
9
+ def run_in_fake_gem(command)
10
+ dummy_app_dir = File.expand_path("fixtures/projects/dummy_cli_gem", __dir__)
11
+
12
+ Bundler.with_unbundled_env do
13
+ Dir.chdir(dummy_app_dir) do
14
+ sh "bundle check || bundle install"
15
+ sh command
16
+ end
17
+ end
18
+ end
19
+
20
+ namespace :spec do
21
+ desc "Run the dummy fixture gem specs with Backspin (decoupled from main suite)"
22
+ task :fake_gem do
23
+ run_in_fake_gem("bundle exec rspec")
24
+ end
25
+
26
+ desc "Re-record Backspin YAML fixtures for the dummy fixture gem"
27
+ task :fake_gem_record do
28
+ original_record_mode = ENV["RECORD_MODE"]
29
+ ENV["RECORD_MODE"] = "record"
30
+ run_in_fake_gem("bundle exec rspec")
31
+ ensure
32
+ ENV["RECORD_MODE"] = original_record_mode
33
+ end
34
+ end
35
+
9
36
  task default: %i[spec standard]
10
37
 
11
38
  load "release.rake" if File.exist?("release.rake")
data/backspin.gemspec CHANGED
@@ -24,4 +24,6 @@ Gem::Specification.new do |spec|
24
24
  spec.bindir = "exe"
25
25
  spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
26
26
  spec.require_paths = ["lib"]
27
+
28
+ spec.add_dependency "logger"
27
29
  end
@@ -141,14 +141,16 @@ result.expected.stdout
141
141
  - Error message is generated from `BackspinResult#error_message`.
142
142
  - Do not duplicate `diff` content in exception formatting.
143
143
 
144
- ## Record Format Sketch (v4)
144
+ ## Record Format Sketch (v4.1)
145
145
 
146
146
  Single-snapshot format to match single-snapshot runtime model:
147
147
 
148
148
  ```yaml
149
149
  ---
150
- format_version: "4.0"
150
+ format_version: "4.1"
151
+ first_recorded_at: "2026-01-01T00:00:00Z"
151
152
  recorded_at: "2026-02-11T00:00:00Z"
153
+ record_count: 3
152
154
  snapshot:
153
155
  command_type: "Open3::Capture3"
154
156
  args: ["echo", "hello"]
@@ -184,11 +186,11 @@ Status date: 2026-02-11
184
186
 
185
187
  1. `Snapshot` and `BackspinResult` classes are implemented and wired into runtime paths.
186
188
  2. `Backspin.run` and `Backspin.capture` now return `BackspinResult`.
187
- 3. `Record` persistence moved to v4 single-snapshot format (`snapshot` key, no `commands` array).
189
+ 3. `Record` persistence moved to v4 single-snapshot format (`snapshot` key, no `commands` array), with v4.1 top-level metadata.
188
190
  4. `Matcher` and `CommandDiff` now operate on expected/actual snapshots.
189
191
  5. Legacy result/command layering was removed from `lib/`.
190
- 6. Specs have been migrated to the new result contract and v4 format.
191
- 7. Validation is green: `66 examples, 0 failures` and Standard lint passes.
192
+ 6. Specs have been migrated to the new result contract and v4.1 format.
193
+ 7. Validation is green: `89 examples, 0 failures` and Standard lint passes.
192
194
  8. Public docs now use `result.actual` / `result.expected` terminology.
193
195
 
194
196
  ## Success Criteria
@@ -198,7 +200,7 @@ Status date: 2026-02-11
198
200
  3. In `:verify` mode, `result.expected` is present, `result.verified?` is boolean, and mismatch cases populate `result.diff` plus `result.error_message`.
199
201
  4. No multi-command result API remains in the public result contract.
200
202
  5. Snapshot object exposes a stable single-command shape: `stdout`, `stderr`, `status`, `args`, `env`, `command_type`.
201
- 6. Record format uses one snapshot (v4), not a commands array.
203
+ 6. Record format uses one snapshot (v4.x), not a commands array.
202
204
  7. Existing strict verification behavior remains: default raises `Backspin::VerificationError`, while `raise_on_verification_failure = false` returns a failed result without raising.
203
205
  8. End-to-end Unix command examples are covered in specs: `echo` record/verify, `ls -1` record/verify, `date` mismatch behavior (or matcher override), and captured `grep | wc` pipeline output via `Backspin.capture`.
204
206
  9. Matcher behavior is preserved: default matching remains stdout/stderr/status, and custom `matcher:` contract (Proc, hash fields, `:all`) continues to work for both run and capture verification.
@@ -0,0 +1 @@
1
+
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gemspec
6
+
7
+ gem "backspin", path: "../../.."
8
+
9
+ group :development, :test do
10
+ gem "rake", "~> 13"
11
+ gem "rspec", "~> 3"
12
+ end
@@ -0,0 +1,55 @@
1
+ PATH
2
+ remote: ../../..
3
+ specs:
4
+ backspin (0.11.0)
5
+ logger
6
+
7
+ PATH
8
+ remote: .
9
+ specs:
10
+ dummy_cli_gem (0.1.0)
11
+
12
+ GEM
13
+ remote: https://rubygems.org/
14
+ specs:
15
+ diff-lcs (1.6.2)
16
+ logger (1.7.0)
17
+ rake (13.3.1)
18
+ rspec (3.13.2)
19
+ rspec-core (~> 3.13.0)
20
+ rspec-expectations (~> 3.13.0)
21
+ rspec-mocks (~> 3.13.0)
22
+ rspec-core (3.13.6)
23
+ rspec-support (~> 3.13.0)
24
+ rspec-expectations (3.13.5)
25
+ diff-lcs (>= 1.2.0, < 2.0)
26
+ rspec-support (~> 3.13.0)
27
+ rspec-mocks (3.13.7)
28
+ diff-lcs (>= 1.2.0, < 2.0)
29
+ rspec-support (~> 3.13.0)
30
+ rspec-support (3.13.7)
31
+
32
+ PLATFORMS
33
+ aarch64-linux
34
+ ruby
35
+
36
+ DEPENDENCIES
37
+ backspin!
38
+ dummy_cli_gem!
39
+ rake (~> 13)
40
+ rspec (~> 3)
41
+
42
+ CHECKSUMS
43
+ backspin (0.11.0)
44
+ diff-lcs (1.6.2) sha256=9ae0d2cba7d4df3075fe8cd8602a8604993efc0dfa934cff568969efb1909962
45
+ dummy_cli_gem (0.1.0)
46
+ logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203
47
+ rake (13.3.1) sha256=8c9e89d09f66a26a01264e7e3480ec0607f0c497a861ef16063604b1b08eb19c
48
+ rspec (3.13.2) sha256=206284a08ad798e61f86d7ca3e376718d52c0bc944626b2349266f239f820587
49
+ rspec-core (3.13.6) sha256=a8823c6411667b60a8bca135364351dda34cd55e44ff94c4be4633b37d828b2d
50
+ rspec-expectations (3.13.5) sha256=33a4d3a1d95060aea4c94e9f237030a8f9eae5615e9bd85718fe3a09e4b58836
51
+ rspec-mocks (3.13.7) sha256=0979034e64b1d7a838aaaddf12bf065ea4dc40ef3d4c39f01f93ae2c66c62b1c
52
+ rspec-support (3.13.7) sha256=0640e5570872aafefd79867901deeeeb40b0c9875a36b983d85f54fb7381c47c
53
+
54
+ BUNDLED WITH
55
+ 4.0.5
@@ -0,0 +1 @@
1
+ MIT License
@@ -0,0 +1,7 @@
1
+ # DummyCliGem Fixture
2
+
3
+ This is a minimal fixture gem used by Backspin's full-stack integration checks.
4
+
5
+ - It shells out to standard unix utilities (`echo`, `ls`).
6
+ - Its RSpec suite verifies command output through `Backspin.run`.
7
+ - Snapshot YAML records are stored in-repo under `spec/fixtures/backspin`.
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/dummy_cli_gem/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "dummy_cli_gem"
7
+ spec.version = DummyCliGem::VERSION
8
+ spec.authors = ["Backspin"]
9
+ spec.email = ["noreply@example.com"]
10
+ spec.summary = "Dummy CLI gem fixture for Backspin full-stack verification"
11
+ spec.description = "Fixture gem that shells out to unix utilities and is tested via Backspin snapshots."
12
+ spec.homepage = "https://example.com/dummy_cli_gem"
13
+ spec.license = "MIT"
14
+ spec.required_ruby_version = Gem::Requirement.new(">= 3.1.0")
15
+
16
+ spec.files = Dir.chdir(__dir__) do
17
+ Dir["lib/**/*.rb", "exe/*", "spec/**/*", "script/**/*", "README.md", "LICENSE.txt"]
18
+ end
19
+ spec.bindir = "exe"
20
+ spec.executables = ["dummy_cli_gem"]
21
+ spec.require_paths = ["lib"]
22
+ end
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ $LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
5
+
6
+ require "dummy_cli_gem"
7
+
8
+ exit(DummyCliGem::CLI.run(ARGV))
@@ -0,0 +1,18 @@
1
+ ---
2
+ format_version: '4.1'
3
+ first_recorded_at: '2026-02-11T01:57:42-06:00'
4
+ recorded_at: '2026-02-11T01:57:42-06:00'
5
+ record_count: 1
6
+ snapshot:
7
+ command_type: Open3::Capture3
8
+ args:
9
+ - ruby
10
+ - exe/dummy_cli_gem
11
+ - echo
12
+ - hello from dummy gem
13
+ stdout: 'hello from dummy gem
14
+
15
+ '
16
+ stderr: ''
17
+ status: 0
18
+ recorded_at: '2026-02-11T01:57:42-06:00'
@@ -0,0 +1,18 @@
1
+ ---
2
+ format_version: '4.1'
3
+ first_recorded_at: '2026-02-11T01:57:42-06:00'
4
+ recorded_at: '2026-02-11T01:57:42-06:00'
5
+ record_count: 1
6
+ snapshot:
7
+ command_type: Open3::Capture3
8
+ args:
9
+ - ruby
10
+ - exe/dummy_cli_gem
11
+ - list
12
+ - spec/fixtures/listing_target
13
+ stdout: 'alpha.txt
14
+
15
+ '
16
+ stderr: ''
17
+ status: 0
18
+ recorded_at: '2026-02-11T01:57:42-06:00'
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+
5
+ module DummyCliGem
6
+ module CLI
7
+ module_function
8
+
9
+ def run(argv, out: $stdout, err: $stderr)
10
+ command = argv.first
11
+
12
+ case command
13
+ when "echo"
14
+ text = argv[1]
15
+ return usage(err) if text.nil? || text.empty?
16
+
17
+ stdout, stderr, status = Open3.capture3("echo", text)
18
+ when "list"
19
+ target = argv[1] || "."
20
+ stdout, stderr, status = Open3.capture3("ls", "-1", target)
21
+ else
22
+ return usage(err)
23
+ end
24
+
25
+ out.print(stdout)
26
+ err.print(stderr)
27
+ status.exitstatus
28
+ end
29
+
30
+ def usage(err)
31
+ err.puts("usage: dummy_cli_gem echo <text> | dummy_cli_gem list <path>")
32
+ 1
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DummyCliGem
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dummy_cli_gem/version"
4
+ require "dummy_cli_gem/cli"
5
+
6
+ module DummyCliGem
7
+ end
@@ -0,0 +1,2 @@
1
+ [tools]
2
+ ruby = "4.0.0"
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe "DummyCliGem Backspin full-stack fixture" do
6
+ let(:project_root) { Pathname(__dir__).join("..").expand_path }
7
+
8
+ it "verifies echo command output from committed YAML" do
9
+ expect(BACKSPIN_DIR.join("dummy_echo.yml")).to exist
10
+
11
+ result = Backspin.run(
12
+ ["ruby", "exe/dummy_cli_gem", "echo", "hello from dummy gem"],
13
+ name: "dummy_echo"
14
+ )
15
+
16
+ expect(result).to be_verified
17
+ expect(result.actual.stdout).to eq("hello from dummy gem\n")
18
+ expect(result.actual.stderr).to eq("")
19
+ expect(result.actual.status).to eq(0)
20
+ end
21
+
22
+ it "verifies list command output from committed YAML" do
23
+ expect(BACKSPIN_DIR.join("dummy_ls.yml")).to exist
24
+
25
+ result = Backspin.run(
26
+ ["ruby", "exe/dummy_cli_gem", "list", "spec/fixtures/listing_target"],
27
+ name: "dummy_ls"
28
+ )
29
+
30
+ expect(result).to be_verified
31
+ expect(result.actual.stdout).to eq("alpha.txt\n")
32
+ expect(result.actual.stderr).to eq("")
33
+ expect(result.actual.status).to eq(0)
34
+ end
35
+
36
+ it "uses current Backspin record format for fixture YAML files" do
37
+ %w[dummy_echo dummy_ls].each do |record_name|
38
+ record_path = BACKSPIN_DIR.join("#{record_name}.yml")
39
+ expect(record_path).to exist
40
+
41
+ record_data = YAML.load_file(record_path)
42
+ expect(record_data["format_version"]).to eq("4.1")
43
+ expect(record_data["snapshot"]["command_type"]).to eq("Open3::Capture3")
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,18 @@
1
+ ---
2
+ format_version: '4.1'
3
+ first_recorded_at: '2026-02-11T07:43:21+00:00'
4
+ recorded_at: '2026-02-11T07:43:21+00:00'
5
+ record_count: 1
6
+ snapshot:
7
+ command_type: Open3::Capture3
8
+ args:
9
+ - ruby
10
+ - exe/dummy_cli_gem
11
+ - echo
12
+ - hello from dummy gem
13
+ stdout: 'hello from dummy gem
14
+
15
+ '
16
+ stderr: ''
17
+ status: 0
18
+ recorded_at: '2026-02-11T07:43:21+00:00'
@@ -0,0 +1,18 @@
1
+ ---
2
+ format_version: '4.1'
3
+ first_recorded_at: '2026-02-11T07:43:21+00:00'
4
+ recorded_at: '2026-02-11T07:43:21+00:00'
5
+ record_count: 1
6
+ snapshot:
7
+ command_type: Open3::Capture3
8
+ args:
9
+ - ruby
10
+ - exe/dummy_cli_gem
11
+ - list
12
+ - spec/fixtures/listing_target
13
+ stdout: 'alpha.txt
14
+
15
+ '
16
+ stderr: ''
17
+ status: 0
18
+ recorded_at: '2026-02-11T07:43:21+00:00'
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/setup"
4
+ require "pathname"
5
+ require "yaml"
6
+ require "backspin"
7
+ require "dummy_cli_gem"
8
+
9
+ BACKSPIN_DIR = Pathname(__dir__).join("fixtures", "backspin")
10
+
11
+ Backspin.configure do |config|
12
+ config.backspin_dir = BACKSPIN_DIR
13
+ config.logger = nil
14
+ end
15
+
16
+ RSpec.configure do |config|
17
+ config.disable_monkey_patching!
18
+
19
+ config.expect_with :rspec do |expectations|
20
+ expectations.syntax = :expect
21
+ end
22
+ end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "logger"
3
4
  require "pathname"
4
5
 
5
6
  module Backspin
@@ -10,6 +11,8 @@ module Backspin
10
11
  attr_accessor :backspin_dir
11
12
  # Whether to raise an exception when verification fails in `run`/`capture` - defaults to true
12
13
  attr_accessor :raise_on_verification_failure
14
+ # Logger for Backspin diagnostics - defaults to WARN level, logfmt-lite format
15
+ attr_accessor :logger
13
16
  # Regex patterns to scrub from saved output
14
17
  attr_reader :credential_patterns
15
18
 
@@ -18,6 +21,7 @@ module Backspin
18
21
  @raise_on_verification_failure = true
19
22
  @credential_patterns = default_credential_patterns
20
23
  @backspin_dir = Pathname(Dir.pwd).join("fixtures", "backspin")
24
+ @logger = default_logger
21
25
  end
22
26
 
23
27
  def add_credential_pattern(pattern)
@@ -34,6 +38,15 @@ module Backspin
34
38
 
35
39
  private
36
40
 
41
+ def default_logger
42
+ logger = Logger.new($stdout)
43
+ logger.level = Logger::WARN
44
+ logger.formatter = proc { |severity, _time, _progname, msg|
45
+ "level=#{severity.downcase} lib=backspin #{msg}\n"
46
+ }
47
+ logger
48
+ end
49
+
37
50
  # Some default patterns for common credential types
38
51
  def default_credential_patterns
39
52
  [