ruby_raider 3.8.0 → 3.10.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 (81) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/release.yml +18 -29
  3. data/.github/workflows/security.yml +68 -0
  4. data/.rubocop.yml +28 -0
  5. data/README.md +22 -4
  6. data/docs/superpowers/plans/2026-10-25-failure-video-replay.md +400 -0
  7. data/docs/superpowers/plans/2026-10-26-parity-cleanup.md +233 -0
  8. data/docs/superpowers/plans/2026-10-28-failure-card.md +295 -0
  9. data/docs/superpowers/specs/2026-10-25-failure-video-replay-design.md +214 -0
  10. data/docs/superpowers/specs/2026-10-28-failure-card-design.md +210 -0
  11. data/lib/commands/scaffolding_commands.rb +3 -3
  12. data/lib/commands/utility_commands.rb +36 -6
  13. data/lib/generators/cucumber/templates/cucumber.tt +1 -1
  14. data/lib/generators/cucumber/templates/partials/appium_env.tt +6 -0
  15. data/lib/generators/cucumber/templates/partials/selenium_env.tt +6 -0
  16. data/lib/generators/cucumber/templates/partials/watir_env.tt +6 -0
  17. data/lib/generators/helper_generator.rb +5 -1
  18. data/lib/generators/infrastructure/azure_generator.rb +1 -0
  19. data/lib/generators/infrastructure/github_generator.rb +1 -0
  20. data/lib/generators/infrastructure/templates/azure_pipelines.tt +26 -2
  21. data/lib/generators/infrastructure/templates/github.tt +31 -1
  22. data/lib/generators/infrastructure/templates/github_appium.tt +50 -1
  23. data/lib/generators/invoke_generators.rb +20 -2
  24. data/lib/generators/rspec/rspec_generator.rb +0 -1
  25. data/lib/generators/templates/common/gemfile.tt +1 -1
  26. data/lib/generators/templates/common/git_ignore.tt +3 -0
  27. data/lib/generators/templates/common/partials/mobile_config.tt +5 -0
  28. data/lib/generators/templates/common/partials/web_config.tt +6 -1
  29. data/lib/generators/templates/common/read_me.tt +80 -2
  30. data/lib/generators/templates/helpers/browser_helper.tt +5 -1
  31. data/lib/generators/templates/helpers/driver_helper.tt +1 -0
  32. data/lib/generators/templates/helpers/partials/selenium_driver.tt +4 -1
  33. data/lib/generators/templates/helpers/spec_helper.tt +8 -0
  34. data/lib/generators/templates/helpers/video_helper.tt +338 -171
  35. data/lib/ruby_raider/ffmpeg.rb +280 -0
  36. data/lib/ruby_raider.rb +2 -3
  37. data/lib/scaffolding/project_detector.rb +1 -3
  38. data/lib/scaffolding/scaffolding.rb +0 -1
  39. data/lib/utilities/utilities.rb +15 -2
  40. data/lib/version +1 -1
  41. data/ruby_raider.gemspec +3 -1
  42. data/sig/commands/utility_commands.rbs +4 -1
  43. data/sig/generators/invoke_generators.rbs +2 -3
  44. data/sig/ruby_raider/ffmpeg.rbs +23 -0
  45. data/sig/utilities/utilities.rbs +1 -0
  46. data/spec/commands/raider_commands_spec.rb +56 -1
  47. data/spec/generators/fixtures/video_helper_stubs/selenium-webdriver.rb +28 -0
  48. data/spec/generators/video_helper_spec.rb +608 -0
  49. data/spec/integration/commands/browser_update_after_creation_spec.rb +7 -6
  50. data/spec/integration/commands/utility_commands_spec.rb +2 -2
  51. data/spec/integration/content/azure_content_spec.rb +48 -3
  52. data/spec/integration/content/ci_content_spec.rb +115 -5
  53. data/spec/integration/content/common_content_spec.rb +8 -0
  54. data/spec/integration/content/config_content_spec.rb +14 -0
  55. data/spec/integration/content/cucumber_reporter_content_spec.rb +22 -1
  56. data/spec/integration/content/gemfile_content_spec.rb +2 -3
  57. data/spec/integration/content/helper_content_spec.rb +146 -0
  58. data/spec/integration/content/page_content_spec.rb +0 -1
  59. data/spec/integration/content/readme_content_spec.rb +65 -0
  60. data/spec/integration/content/rspec_reporter_content_spec.rb +2 -2
  61. data/spec/integration/content/test_content_spec.rb +2 -3
  62. data/spec/integration/end_to_end_features_spec.rb +0 -2
  63. data/spec/integration/end_to_end_spec.rb +252 -26
  64. data/spec/integration/generators/azure_generator_spec.rb +7 -6
  65. data/spec/integration/generators/common_generator_spec.rb +1 -1
  66. data/spec/integration/generators/invoke_generators_spec.rb +52 -0
  67. data/spec/integration/scaffolding_e2e_spec.rb +13 -24
  68. data/spec/scaffolding/scaffold_project_detector_spec.rb +0 -1
  69. data/spec/scaffolding/scaffolding_features_spec.rb +0 -1
  70. data/spec/system/selenium_spec.rb +0 -1
  71. data/spec/system/watir_spec.rb +0 -1
  72. data/spec/utilities/ffmpeg_spec.rb +424 -0
  73. data/spec/utilities/utilities_spec.rb +19 -2
  74. data/spec/utilities/video_command_spec.rb +120 -0
  75. metadata +49 -11
  76. data/lib/commands/adopt_commands.rb +0 -42
  77. data/lib/commands/loaded_commands.rb +0 -9
  78. data/lib/commands/plugin_commands.rb +0 -32
  79. data/sig/commands/adopt_commands.rbs +0 -8
  80. data/sig/commands/loaded_commands.rbs +0 -5
  81. data/sig/commands/plugin_commands.rbs +0 -9
@@ -0,0 +1,214 @@
1
+ # Failure video replay
2
+
3
+ Date: 2026-10-25
4
+ Status: approved 2026-10-25
5
+
6
+ Open items (approved as drafted; confirm before the step they block):
7
+ - RaiderHQ re-hosts LGPL ffmpeg builds, as drafted. **Still unassigned:** who cuts the `ffmpeg-<version>` release. That person blocks the real checksums in the manifest, so build against a placeholder manifest until then.
8
+ - The 15% slowdown limit stands as drafted. If the spike misses it, **stop and ask** which gives way (the limit, the frame rate, or default-on for some browsers) rather than choosing.
9
+ Repos touched: ruby_raider (3.9.0 → next minor), raider_reporter (0.9.0 → next minor), raider_desktop (3.6.0 → next minor)
10
+
11
+ ## Intent
12
+
13
+ When a test fails, the developer opens the report on their own machine and presses play to watch what the browser or app did. That is the first Playwright parity gap we are closing (see the parity audit: video and trace were the top gap once Playwright-as-automation and a Raider DSL were ruled out).
14
+
15
+ Failures are usually investigated **locally**, so the local experience comes first. CI must work too, but is secondary.
16
+
17
+ ## Goals
18
+
19
+ 1. Newly generated web and mobile projects record a video of every test **by default** and keep it **only when the test fails** (Playwright's `retain-on-failure`).
20
+ 2. Nothing has to be installed by hand. Like Playwright, Raider fetches its own ffmpeg on first use.
21
+ 3. The video plays inline in the raider_reporter HTML report and in the raider_desktop result card.
22
+ 4. The video is a normal file (WebM) that can be downloaded and shared.
23
+ 5. If recording is impossible (download failed, unsupported platform, recorder error), tests still run and pass or fail on their own merits. One warning, no video.
24
+
25
+ ## Non-goals
26
+
27
+ - Step-by-step trace (actions + screenshots + console). Later feature; the user chose video replay first.
28
+ - Playwright as an automation choice, or a Raider-owned locator/wait DSL (standing product decisions).
29
+ - Raider server changes. Videos upload under the existing 45 MB / 1000-file budget; anything past it stays local, as today.
30
+ - Videos for passing tests, or a "record everything" mode in v1.
31
+ - Upgrading existing generated projects automatically. Only new projects get this; an upgrade note in the README is enough.
32
+
33
+ ## Anti-goals (would make this a failure even if it works)
34
+
35
+ - A test that would pass fails, hangs, or becomes flaky because recording is on.
36
+ - Raider downloads and runs a binary it did not verify.
37
+ - The default suite gets noticeably slower. Target: under 15% added wall time on the generated sample suite. *(My number; adjust if you want a different budget.)*
38
+ - Report folders balloon because passing-test videos were kept.
39
+
40
+ ## Decisions made
41
+
42
+ | Decision | Made by |
43
+ |---|---|
44
+ | Video replay before trace | user |
45
+ | Local machine is the primary place failures are investigated | user |
46
+ | ffmpeg is downloaded automatically (option 1), not a zero-binary frame player | user |
47
+ | Video is on by default in new projects | user |
48
+ | Binaries are re-hosted by RaiderHQ as GitHub release assets, pinned, SHA-256 verified | Claude's call |
49
+ | LGPL ffmpeg build, WebM/VP8 output (avoids GPL H.264 obligations; Playwright made the same choice) | Claude's call |
50
+ | Web recording = periodic page screenshots (works for any Selenium/Watir browser, headless or headed, never captures the rest of the desktop); mobile = Appium's native `start_recording_screen` | Claude's call |
51
+ | Screen-capture recorder (`ScreenCaptureRecorder`, full desktop via avfoundation/x11grab/gdigrab) is dropped | Claude's call |
52
+ | Downloader lives in the ruby_raider gem, which every generated project already has in its Gemfile | Claude's call |
53
+
54
+ ## Design
55
+
56
+ ### 1. ffmpeg provisioning (ruby_raider)
57
+
58
+ New module `RubyRaider::Ffmpeg` in `lib/utilities/ffmpeg.rb`, requirable from generated projects (`require 'utilities/ffmpeg'` or a small `ruby_raider/ffmpeg` entry point; builder's choice, but it must not load Thor).
59
+
60
+ - **Pinned manifest** in the gem: ffmpeg version, and per platform (`darwin-arm64`, `darwin-x64`, `linux-x64`, `windows-x64`) the asset URL on a RaiderHQ GitHub release and its SHA-256.
61
+ - **Cache path:** `~/.cache/raider/ffmpeg/<version>/ffmpeg[.exe]` (`%LOCALAPPDATA%\raider\ffmpeg\…` on Windows). Override with `RAIDER_FFMPEG_PATH` (use this binary, no download) and `RAIDER_CACHE_DIR`.
62
+ - **Resolution order:** `RAIDER_FFMPEG_PATH` → cached pinned binary → download. A system `ffmpeg` on PATH is **not** used by default (unknown build/licence/codecs); allowed only via `RAIDER_FFMPEG_PATH`.
63
+ - **Download:** HTTPS only, follow GitHub redirects, timeouts like `DesktopDownloader` (10s open / 15s read, longer total for the ~20–40 MB file), write to a temp file, verify SHA-256, then atomic rename and `chmod +x`. Checksum mismatch → delete, warn, no video. Concurrent processes (parallel_tests) must not corrupt the cache: lock file or rename-wins.
64
+ - **Offline / CI:** `raider u video install` downloads and verifies ahead of time; `raider u video off|on` toggles `config.yml`; `raider u video status` prints the resolved binary and version.
65
+ - Unlike `DesktopDownloader`, never query "latest". The manifest is the source of truth, bumped deliberately in a gem release.
66
+
67
+ **Release chore (RaiderHQ):** publish the LGPL ffmpeg builds as assets on a dedicated release (e.g. `RaiderHQ/ruby_raider` tag `ffmpeg-<version>`), keep the LICENSE/source-offer text next to them, and record checksums in the manifest. Source of the upstream builds is the builder's call, but it must be an LGPL configuration with `libvpx` enabled for every platform listed.
68
+
69
+ ### 2. Recording in generated projects (ruby_raider templates)
70
+
71
+ Revive `lib/generators/templates/helpers/video_helper.tt` (orphaned since 366946e) and generate it as `helpers/video_helper.rb` for every project.
72
+
73
+ - **Config** (`common/partials/web_config.tt`, `mobile_config.tt`):
74
+ ```yaml
75
+ video:
76
+ enabled: true
77
+ fps: 5
78
+ max_width: 1280
79
+ ```
80
+ `HEADLESS`-style env override: `VIDEO=off` disables for one run.
81
+ - **Web recorder** (`FrameRecorder`, replaces `CdpRecorder`): captures page screenshots on an interval into a per-test temp dir, then encodes with the provisioned ffmpeg to `video.webm` (VP8). Works for Chrome, Firefox and Edge via Selenium, and for Watir (which wraps the Selenium driver).
82
+ - **Mobile recorder:** keep `AppiumRecorder`. Write the result as-is (mp4 from Appium; no re-encode needed; reporter already knows `video/mp4`).
83
+ - **Hooks:**
84
+ - RSpec `spec_helper.tt`: start in `before(:each)` after driver setup; in `after(:each)`, stop. If `example.exception`, encode and `RaiderReporter.attach('video', path, type: :webm)` next to the existing screenshot; otherwise discard frames without encoding.
85
+ - Cucumber `*_env.tt`: same in `Before` / `After`, keyed on `scenario.failed?`.
86
+ - Encoding happens **only for failures**, so passing tests pay capture cost but not encode cost.
87
+ - **Retries** *(Claude's call)*: each attempt records its own video. The template attaches a video only on the final attempt, for example when the attempt number equals the retry count or the example is no longer going to be retried. Earlier attempts' frames are thrown away without being encoded. This is enforced in the template, not left to raider_reporter. Cucumber already keeps only the final attempt.
88
+ - **Failure isolation:** every recorder call is wrapped; any error → warn once per run, disable recording for the rest of the process, never raise into the test.
89
+
90
+ **Known risk the builder must spike first:** the current `CdpRecorder` calls `save_screenshot` from a background thread while the test thread drives the same Selenium session. Selenium's Ruby client is not documented as thread-safe for concurrent commands on one bridge. The spike must show whether this is safe across Chrome and Firefox under the sample suite. If it is not, acceptable fallbacks in order: (a) capture after each driver command via Selenium's built-in event listener (`listener:` on driver creation; Watir passes it through) plus a final frame; (b) a second HTTP client bound to the same session id. Pick whichever passes the anti-goals. Record the result in this doc.
91
+
92
+ ### 3. Reporter (raider_reporter)
93
+
94
+ - **Size cap:** add `max_video_bytes` (default 50 MB) applied when the resolved content type is `video/*`; `max_attachment_bytes` (5 MB) keeps applying to everything else. Over the cap → existing warning, not attached.
95
+ - **`type: :webm`** already maps to `video/webm` (`content_types.rb`). No schema change: videos are ordinary attachments.
96
+ - **HTML report** (`writers/html.rb` `attachments`): for `video/*`, render `<video controls preload="metadata" src="…">` with a download link beneath, through the existing `safe_href` checks. Keep images as they are.
97
+ - **Upload ordering** *(Claude's call)*: today `Uploader#within_budget` keeps attachments in order until 45 MB, so one early video could push every later screenshot out of the upload. Change the order so all non-video attachments go first, then videos fill whatever room is left. Videos that don't fit stay local, with the existing warning.
98
+ - **Server:** no change needed. raider_saas already serves `video/mp4` and `video/webm` inline (`app/controllers/attachments_controller.rb:11`) and renders `<video controls>` for them (`app/views/test_results/_card.html.erb:37-38`).
99
+ - **RSpec retry pile-up (separate bug, noted, not fixed here):** the RSpec formatter calls `start_test` once per example, while rspec-retry reruns `after(:each)` on every attempt, so attachments from every failed attempt may pile up. The builder should confirm this with a test. This design avoids it for video by attaching only on the final attempt (see Hooks). The screenshot behaviour is a separate ticket.
100
+
101
+ ### 4. Desktop (raider_desktop)
102
+
103
+ - `foundation/raiderResults.ts`: alongside the first image, pick the first `video/*` attachment, through the same relative-path guard (`attachments(-N)?/…`).
104
+ - `TestResultCard`: when a video is present, show an inline player (HTML5 `<video>`; Electron's Chromium plays WebM and MP4) above the screenshot, collapsed by default for long lists.
105
+ - **Serving the file** *(Claude's call)*: screenshots go through the `readImage` IPC as base64 data URLs, which is unworkable for videos up to 50 MB. Register a custom Electron protocol (e.g. `raider-media://`) whose handler serves only files under `<project>/raider-results/attachments(-N)?/`. It resolves real paths, rejects symlinks and `..` (same rules as the existing open-report handler), and supports range requests so the player can seek. Do not enable general `file://` access.
106
+
107
+ ### 5. CI templates (ruby_raider)
108
+
109
+ - `github.tt`, `github_appium.tt`, `azure_pipelines.tt`: cache `~/.cache/raider` between runs (actions/cache, Azure `Cache@2`) keyed on the gem's ffmpeg version, and add a `raider u video install` step before tests so a download failure is visible as its own step.
110
+ - `raider-results/` artifact upload already includes `attachments/`, so videos ride along.
111
+
112
+ ## Acceptance
113
+
114
+ - `raider new demo -p framework:rspec automation:selenium`, then `bundle exec rspec` with one deliberately failing example, on a clean macOS machine with no ffmpeg installed: ffmpeg downloads once, the failing example has `video.webm` in `raider-results/attachments/`, passing examples have none, and `raider-results/index.html` plays it inline.
115
+ - Same for Cucumber, Watir, Firefox, and headless Chrome.
116
+ - Second run makes no network request for ffmpeg.
117
+ - Tampered cached binary (checksum mismatch) or no network: tests run, one `[video]` warning, no video, exit code unchanged.
118
+ - Appium iOS sample with a failing test: mp4 attached and playable in report and desktop.
119
+ - raider_desktop result card for the failing test plays the video.
120
+ - Generated sample suite wall time with video on is within the slowdown budget vs `VIDEO=off`.
121
+ - Generated GitHub Actions workflow passes with the cache step; second run hits the cache.
122
+
123
+ ## Left to the builder
124
+
125
+ - Exact require path / module layout for `RubyRaider::Ffmpeg`.
126
+ - Upstream source of the LGPL ffmpeg builds, and the release tag name.
127
+ - Frame naming, temp-dir layout, encoder flags (VP8 bitrate/quality), as long as a 30 s failing test produces a video under the 50 MB cap.
128
+ - Desktop player styling.
129
+ - Spec layout for content/integration tests (follow the existing `spec/integration/content/*_content_spec.rb` pattern).
130
+
131
+ ## Spike result (2026-10-25)
132
+
133
+ Gate for "Recording in generated projects". **Outcome: resolved 2026-10-26: `CAPTURE_MODE = :listener` with state-changing frames (see "Final decision" at the end of this section).**
134
+
135
+ Setup: project generated with `raider new spike -p framework:rspec automation:selenium` (selenium-webdriver 4.44.0, Ruby 3.2.8, Chrome 155, Firefox 149.0.2 via geckodriver 0.37.1, macOS arm64), sample suite = 2 examples (login page), site warmed up first. Each run alternates capture on / `VIDEO=off` equivalent (`CAPTURE=off`) to cancel drift. 20 runs per browser per mode (above the 5-run minimum), 40 `off` runs per browser. Median wall time of the whole `rspec` process. Headless Chrome and Firefox via `HEADLESS=true`; headed Chrome on the desktop.
136
+
137
+ ### Step 1: background thread (`save_screenshot` every 200 ms) is not safe
138
+
139
+ Not usable. Every one of the 60 runs (20 per browser) broke at least one test, against 0 failures in 120 `off` runs.
140
+
141
+ | Browser | Runs with test failures | Errors seen (tests and capture thread) |
142
+ |---|---|---|
143
+ | Chrome headless | 20/20 | `NoMethodError`, `RuntimeError`, `IOError`, `Net::HTTPBadResponse`, `NoSuchElementError` ("element id string is malformed") |
144
+ | Chrome headed | 20/20 | the same plus `Net::ReadTimeout` |
145
+ | Firefox headless | 20/20 | `Net::HTTPBadResponse`, `IOError`, `RuntimeError`, `WebDriverError` |
146
+
147
+ Cause: two threads share one `Net::HTTP` connection inside the Selenium bridge, so responses interleave ("garbled response", "can't modify string; temporarily locked"). It also corrupted the test thread's own commands. The option of a second HTTP client on the same session id (fallback b) was not tried. Thread-mode timings are meaningless (tests died early, so they look faster).
148
+
149
+ ### Step 3: listener capture (frame after `navigate_to`, `click`, `change_value_of`, `execute_script`, plus one final frame)
150
+
151
+ Safe. 0 test failures and 0 capture errors in 60/60 runs (Chrome headless, Chrome headed, Firefox headless). About 6 frames per example.
152
+
153
+ | Browser | Median off | Median listener | Median slowdown | Added per frame |
154
+ |---|---|---|---|---|
155
+ | Chrome headless | 4.09 s | 5.39 s | **31.8%** | about 110 ms |
156
+ | Chrome headed | 5.07 s | 6.30 s | **24.4%** | about 100 ms |
157
+ | Firefox headless | 6.97 s | 7.30 s | **4.7%** | about 28 ms |
158
+
159
+ ### Decision needed
160
+
161
+ Listener capture is the only safe mode and is under 15% on Firefox only. Chrome misses by 10 to 17 points on this suite. The suite is tiny (2 examples, about 4 s, mostly browser start-up), and the cost is a roughly fixed 100 ms per `save_screenshot` in Chrome, so the ratio improves as examples get longer. It is also a sparse recording (a frame per command, not 5 fps). Options for the user, per the open item above: (1) raise the limit for Chrome, or measure it on a longer suite; (2) lower the frame count (for example skip `execute_script` and `change_value_of`, or cap frames per second of wall time); (3) make video default-on for Firefox only and opt-in for Chrome. Not chosen here.
162
+
163
+ Notes for the builder: listener capture must go through `Selenium::WebDriver.for(..., listener:)`, which wraps the bridge in `EventFiringBridge` (present in selenium-webdriver 4.44.0). The spike replaced the generated Firefox `browser_arguments` (`acceptInsecureCerts`, `no-sandbox`, written without dashes, which Firefox does not treat as flags) with none; whether they matter was not tested.
164
+
165
+ ### Spike re-measure (longer suite)
166
+
167
+ Per the user's ruling (limit, fps and default unchanged), listener capture was re-measured on a longer scratch suite: 16 examples against a local static page (`file://`, no network), each doing navigate, find, type, click, an explicit wait on a JS-delayed element, `execute_script` and asserts. No sleeps. 10 runs per browser, alternating off and listener; median whole-`rspec` wall time. Firefox was not re-run (optional; it already passed at +4.7%).
168
+
169
+ | Browser | Median off | Median listener | Median slowdown | Frames per example | Added per frame | Errors |
170
+ |---|---|---|---|---|---|---|
171
+ | Chrome headless | 40.3 s | 47.7 s | **18.5%** | 9 | about 52 ms | 0 |
172
+ | Chrome headed | 47.1 s | 56.7 s | **20.3%** | 9 | about 66 ms | 0 |
173
+
174
+ Result: **BLOCKED.** Both required browsers are over 15%, so `CAPTURE_MODE` is not set. Listener capture stays safe (0 test failures and 0 capture errors in 40 runs), and the longer suite lowered the per-frame cost (about 100 ms on the 2-example suite, 52 to 66 ms here), but the overhead still scales with command count: this suite issues about 9 captured commands per 2.5 s of off-time. Headless off time (40.3 s) came in slightly under the 45 s target; headed met it (47.1 s). The ratio is per-example, so more examples would not change it. Which of the limit, the frame count (for example skip `execute_script` and `change_value_of`) or default-on for Chrome gives way is still the user's call.
175
+
176
+ ### Spike re-measure (state-changing frames)
177
+
178
+ Per the user's ruling (fewer frames), listener capture was limited to state-changing commands plus a final frame. Hook set measured: `after_navigate_to`, `after_click`, `after_change_value_of`, and the final frame in `after(:each)`. `after_change_value_of` is what `send_keys` and `clear` dispatch through in Selenium 4.44.0 (`EventFiringBridge#send_keys_to_element`, `#clear_element`). There is no `submit` hook in the bridge. `after_execute_script`, `after_find` and text/attribute reads are not captured. `after_navigate_back` and `after_navigate_forward` exist and were not included (not in the requested set). Same 16-example local suite, 10 alternating runs per browser, median whole-`rspec` wall time.
179
+
180
+ | Browser | Median off | Median listener | Median slowdown | Frames per example | Added per frame | Errors |
181
+ |---|---|---|---|---|---|---|
182
+ | Chrome headless | 44.4 s | 51.4 s | **15.8%** | 8 | about 55 ms | 0 |
183
+ | Chrome headed | 49.5 s | 55.4 s | **11.8%** | 8 | about 46 ms | 0 |
184
+
185
+ Result: **BLOCKED.** Headless Chrome is 15.8%, just over the limit, so `CAPTURE_MODE` is not set. Headed Chrome passes. Per-pair slowdown is noisy (headless 4.8% to 24.1%, median of pairs 17.0%; headed 5.8% to 14.4%, median 12.7%), and the off baseline itself moved from 40.3 s to 44.4 s between measurement sets, so the result is borderline rather than clearly failing. Dropping `execute_script` took the headless result from 18.5% to 15.8% and headed from 20.3% to 11.8%.
186
+
187
+ Frame check on one failing example (navigate, type, click Add, click Next, click Go, wait, failing assert): 6 frames. Each page change shows up (first page with typed text, then the second page). Weakness: frames are taken right after the command returns, so effects that arrive later in the page are missed. The toast shown 800 ms after "Add" appears in no frame, and the "done" text after "Go" appears only in the final frame. The sequence is usable as a replay of navigation and input, but not of asynchronous results between commands.
188
+
189
+ ### Final decision (2026-10-26)
190
+
191
+ The user accepted the state-changing-frames result as within measurement noise: headed Chrome +11.8%, headless Chrome +15.8% with a per-pair spread of 4.8–24.1%.
192
+
193
+ **`CAPTURE_MODE = :listener`**
194
+
195
+ Task 7's `FrameRecorder` must use exactly this capture set:
196
+ - `after_navigate_to`
197
+ - `after_click`
198
+ - `after_change_value_of`
199
+ - one final frame at stop
200
+
201
+ It must not capture after `execute_script`, finds, or reads.
202
+
203
+ Known limit: async page changes between commands only show in the next frame or the final frame.
204
+
205
+ ### Release slowdown check (2026-10-26, 3.10.0)
206
+
207
+ Measured on the shipped code, not the spike recorder: a project generated with `raider new` (rspec + selenium) using this branch's `video_helper.rb`, `driver_helper.rb` and `spec_helper.rb` (listener capture after `navigate_to`, `click` and `change_value_of`, with the alert probe before each frame), ruby_raider and raider_reporter 0.10.0 from local checkouts, selenium-webdriver 4.51.0, Ruby 3.4.1, headless Chrome (`HEADLESS=true`), macOS arm64. Runs alternate video on and `VIDEO=off`. The time is the wall time of the whole `rspec` process, and every example passed, so nothing was encoded: the cost is frame capture only. `RAIDER_FFMPEG_PATH` pointed at a local ffmpeg 8.0.1 with libvpx.
208
+
209
+ | Suite | Runs per mode | Median off | Median on | Median slowdown | Per-pair spread | Errors |
210
+ |---|---|---|---|---|---|---|
211
+ | Generated sample (`login_page_spec.rb`, 2 examples, remote site) | 5 | 3.51 s | 4.90 s | **39.7%** | 22.0–41.4% | 0 failures, 0 `[video]` lines |
212
+ | 16-example local suite (`long_suite_spec.rb` from the spike) | 10 | 36.44 s | 41.87 s | **14.9%** | 13.8–15.9% | 0 failures, 0 `[video]` lines |
213
+
214
+ Result: **within budget.** The 16-example suite is the binding number (same basis as the accepted +15.8%), and +14.9% sits under the 18% stop line. The spread is much tighter than in the spike (13.8–15.9% against 4.8–24.1%). The 2-example sample shows the fixed cost of a few frames against a 3.5 s run, mostly browser start-up, as Task 1 found; it is reported, not gated.
@@ -0,0 +1,210 @@
1
+ # Failed-test card: design
2
+
3
+ Parity item #2 (Raider ↔ Playwright). Date: 2026-10-28.
4
+
5
+ ## Intent
6
+
7
+ When a test goes red, the user wants to see what broke without digging through
8
+ terminal output or re-running. Opening a failed test should answer "what broke and
9
+ where" on one card: title, screenshot, a quick description and a stack trace they
10
+ can open. This is Playwright's error view, adapted to Raider. The user named the
11
+ order (title, screenshot, quick description, openable stack trace), chose layout
12
+ B (side by side), and wants it in **raider_desktop and raider_saas**.
13
+
14
+ ## Goals
15
+
16
+ 1. One failed-test card layout, shared in spirit by desktop and SaaS:
17
+ - **Header:** FAILED badge and the test's full name.
18
+ - **Left:** the failure screenshot (the first image attachment). If there is
19
+ none, the right column takes the full width.
20
+ - **Right, quick description:**
21
+ - the error message, with the exception class above it as today in SaaS
22
+ - "Failed at `<app_frame>`", the first line of the trace in the user's own
23
+ project, e.g. `spec/login_spec.rb:12`
24
+ - duration
25
+ - browser, when known
26
+ - **Stack trace:** collapsed by default. When opened, project frames are
27
+ highlighted and gem/framework frames dimmed. Runs of consecutive dimmed
28
+ frames fold into "… N more", which can be clicked to expand.
29
+ - **Failure video:** a collapsed section under the trace, as today.
30
+ 2. **Desktop:** clicking "Failed at …" opens that file at that line in the app's
31
+ editor.
32
+ 3. **Cucumber traces get the same project-frame treatment as RSpec.** Today
33
+ RSpec traces are pre-filtered by RSpec's backtrace formatter, but Cucumber
34
+ traces keep every gem line, so the user's line is buried.
35
+
36
+ ## Non-goals
37
+
38
+ - Test stdout/stderr capture and per-attempt retry details. These were in the
39
+ original #2 scope; the user's answers pointed at the stack trace and visuals,
40
+ and they didn't ask for them back. Possible later item.
41
+ - Inline source-code snippets around the failing line (Playwright shows these).
42
+ Not asked for; "Failed at" plus open-in-editor covers it on desktop.
43
+ - The raider_reporter HTML report. It already has a collapsible backtrace, so it
44
+ is left as is (my call).
45
+ - Steps, timeline, trace viewer.
46
+
47
+ ## Anti-goals
48
+
49
+ - The card must not hide the full trace. Dimming and folding are presentation
50
+ only; every frame is still reachable.
51
+ - No guessing at the project line in the UI when the reporter didn't record one.
52
+ In that case, omit "Failed at" rather than show a wrong line.
53
+ - Don't break existing callers of the desktop editor route.
54
+
55
+ ## What exists today (verified in code)
56
+
57
+ - **raider_reporter:**
58
+ - `failure` in run.json and the upload payload has `message`,
59
+ `exception_class`, `backtrace` (max 50 lines) and optional `app_frame`
60
+ (`lib/raider_reporter/app_frame.rb`: the first frame inside the project root,
61
+ excluding vendor/bundle/gem paths; at most 500 characters).
62
+ - RSpec backtraces are filtered by `RSpec.configuration.backtrace_formatter`.
63
+ Cucumber backtraces are raw, the first 50 lines.
64
+ - Results also carry `duration_ms` and `attempts`.
65
+ - There is no browser field. `run.environment` is a user-configurable hash
66
+ (`RaiderReporter.configure { |c| c.environment = {...} }`).
67
+ - **raider_desktop:**
68
+ - `foundation/raiderResults.ts` `toRow` joins message + backtrace into one
69
+ `statusDetails.message` string and ignores `app_frame`.
70
+ - `TestResultCard` renders a message and an optional backtrace.
71
+ - `ProjectDashboard` uses `loadRaiderResults`.
72
+ - Editor route is `file-editor` (`Router.tsx:23`). It reads
73
+ `location.state as { fileName, filePath }`, with no line.
74
+ - **raider_saas:**
75
+ - `app/views/test_results/_card.html.erb` is a `<details>` card: the summary
76
+ holds status, name, pills, attempts, duration and a History link. The body
77
+ holds the exception box, a collapsible raw backtrace `<pre>`, an attachments
78
+ row (images go to the lightbox, videos are inline) and an expired-attachments
79
+ note.
80
+ - `test_results` has no `app_frame` column. `app_frame` is only kept as
81
+ `error_groups.sample_app_frame`.
82
+ - `Api::V1::BuildPresenter` derives `app_frame` with
83
+ `Failures::Signature.fallback_frame(backtrace)`.
84
+
85
+ ## Decisions
86
+
87
+ - **Layout B (side by side)** in both apps. User decision.
88
+ - **The project line comes from the reporter's `app_frame`.** The reporter is
89
+ the only place that knows the project root, so the UI never re-derives it.
90
+ Exception: SaaS's existing `fallback_frame` stays as the API fallback for old
91
+ rows.
92
+ - **Classifying frames as project vs. dimmed (for highlighting).**
93
+ - A frame is "project" if its path matches the `app_frame` file, or if it is a
94
+ relative path not under `vendor/`, `.bundle/`, `gems/` or `<internal:`.
95
+ Everything else is dimmed.
96
+ - This is a small pure function per app (TS on desktop, Ruby helper on SaaS)
97
+ with unit tests over RSpec-style and Cucumber-style traces.
98
+ - My call: classify in the UI so old run.json files and old SaaS rows also
99
+ benefit, rather than adding a per-frame flag to the payload.
100
+ - **Cucumber filtering in the reporter.**
101
+ - Keep the raw trace, so nothing is lost and the anti-goal holds.
102
+ - Fix `app_frame` for Cucumber so it skips Cucumber's own step-definition
103
+ plumbing and lands on the user's step definition or page object.
104
+ - Verify this with a Cucumber fixture. If it already does, this becomes a test
105
+ only.
106
+ - **Browser.** See the builder-check rulings below.
107
+ - **Desktop open-at-line.**
108
+ - Add an optional `line?: number` to the `file-editor` location state. The
109
+ editor scrolls to and highlights that line when it is present. Existing
110
+ callers are unchanged.
111
+ - "Failed at" resolves `app_frame`'s relative path against the project path.
112
+ It is clickable only if the file exists; otherwise it is plain text.
113
+ - Per the desktop session: keep `app_frame` parsing and the open-at-line helper
114
+ generic (in `foundation/`, not inside the Dashboard component), because their
115
+ later plans merge dashboards and add `/tests/:id`.
116
+ - **SaaS storage.**
117
+ - Add a nullable `test_results.app_frame` column, written at ingest from
118
+ `failure.app_frame` and capped like `SAMPLE_APP_FRAME_LIMIT`.
119
+ - `BuildPresenter` prefers the stored value and falls back to
120
+ `fallback_frame`, so the API, MCP and the card agree. The OpenAPI shape is
121
+ unchanged (a nullable string).
122
+ - **SaaS card layout.**
123
+ - Keep the `<details>` summary row as is.
124
+ - Restructure the open body to layout B: screenshot left (still the lightbox),
125
+ right column with exception class, message, "Failed at …" and the
126
+ collapsible trace with highlighted, dimmed and folded frames.
127
+ - Videos go in a collapsed "Failure video" section. Other attachments stay
128
+ listed below.
129
+ - Handle the cases where attachments are expired or purged (show the existing
130
+ note; the right column takes the full width) and where the run has
131
+ `results_purged_at`.
132
+ - **Passing and other non-failing results** keep today's rendering in both apps.
133
+ Only failing statuses (`TestResult::FAILING_STATUSES` in SaaS, failed and
134
+ broken on desktop) get the new body.
135
+
136
+ ### Builder-check rulings (my calls, 2026-10-28)
137
+
138
+ - **Absolute paths in traces.**
139
+ - The reporter rewrites backtrace frames that lie inside the project root to
140
+ root-relative paths before writing run.json and the payload. It reuses
141
+ `AppFrame`'s root and symlink logic. Frames outside the root stay as they
142
+ are.
143
+ - The UI rule "relative path not under vendor/.bundle/gems/<internal:" then
144
+ holds for both RSpec and Cucumber.
145
+ - Older data with absolute paths degrades gracefully: only the `app_frame`
146
+ file is highlighted. This supersedes the "no per-frame flag" concern
147
+ because nothing new is added to the schema.
148
+ - **"Failed at" display.**
149
+ - Show `file:line` only, dropping the `:in '…'` suffix.
150
+ - The full `app_frame` string is the element's title/tooltip.
151
+ - One parse helper per app, `parseFrame(str) -> { file, line } | null`, tested
152
+ against RSpec frames, Cucumber frames, `:in` suffixes and Windows drive
153
+ paths.
154
+ - **Screenshot slot.**
155
+ - Use the attachment named `screenshot`. ruby_raider's `spec_helper.tt`
156
+ attaches it as `RaiderReporter.attach('screenshot', …)`.
157
+ - Fall back to the first `image/*` attachment.
158
+ - Other images stay in the attachments list.
159
+ - **RecordingDashboard.** Out of scope; it is recorder-adjacent and the desktop
160
+ session will merge dashboards later. The new failure body is opt-in on
161
+ `TestResultCard` (for example a prop, or a separate `FailureDetails`
162
+ component) and enabled only from `ProjectDashboard`. RecordingDashboard
163
+ renders exactly as today.
164
+ - **Browser.**
165
+ - Dropped from the ruby_raider deliverables. Generated projects never call
166
+ `RaiderReporter.configure`, and adding it would mean new generator surface.
167
+ - The cards show the browser only when `run.environment["browser"]` is
168
+ present, which users can set themselves. Otherwise the card shows duration
169
+ only.
170
+ - **Base and order.**
171
+ - Desktop builds on `feature/parity-cleanup` (PR #74) in a new worktree,
172
+ stacked as a new PR.
173
+ - The desktop session said it will rebase its later dashboard and `/tests/:id`
174
+ plans on top of this work, so this lands first.
175
+ - Helpers live in `src/renderer/src/foundation/` as planned.
176
+
177
+ ## Constraints
178
+
179
+ - **Ownership (other sessions).**
180
+ - Desktop: the recorder, codegen, `projectLayout.ts`, `rubyCommand.ts`,
181
+ `appState`, suites, the Overview tab bar and the Tests tab belong to the
182
+ desktop session. These files are clear: `raiderResults.ts`,
183
+ `TestResultCard`, `ProjectDashboard`, `FileEditor`, `Router.tsx` and
184
+ `components/Editor`.
185
+ - Desktop work goes in its own worktree, on top of `feature/parity-cleanup`
186
+ (PR #74).
187
+ - SaaS: work in a new worktree off `origin/main` (never `~/raider_saas`). The
188
+ SaaS session's analytics files, `config/routes.rb` and the projects/show
189
+ Analytics link are off-limits.
190
+ - **SaaS rules.**
191
+ - Run `source dev-env.sh` first (Ruby 4.0.2).
192
+ - CSP is enforced: no inline `style=` or `on*=`. JS goes in Stimulus via
193
+ importmap.
194
+ - RuboCop omakase.
195
+ - SQL must work on both SQLite and Postgres, with bound params.
196
+ - Run the full suite on both adapters (Postgres: only `raider_*` databases).
197
+ - Don't use ports 3000 or 3001.
198
+ - Attachments go through `attachment_path` (presigned 302), never blob URLs.
199
+ - Commits carry no Co-Authored-By or generated trailers.
200
+ - **General.** No pushes or PRs without the user's okay.
201
+
202
+ ## Left to the builder
203
+
204
+ - Exact Tailwind classes and spacing, matching each app's existing card style.
205
+ - How the editor scrolls and highlights a line (CodeMirror or Monaco, whichever
206
+ `components/Editor` uses).
207
+ - Which fixtures and tests are needed beyond the classification unit tests, the
208
+ open-at-line test, the ingest/presenter specs and one request spec per card
209
+ state (failing with screenshot, without screenshot, attachments expired, no
210
+ app_frame).
@@ -10,6 +10,8 @@ require_relative '../scaffolding/project_detector'
10
10
  require_relative '../commands/utility_commands'
11
11
 
12
12
  class ScaffoldingCommands < Thor
13
+ def self.exit_on_failure? = true
14
+
13
15
  desc 'page [PAGE_NAME]', 'Creates a new page object'
14
16
  option :path, type: :string, required: false,
15
17
  desc: 'The path where your page will be created', aliases: '-p'
@@ -82,9 +84,7 @@ class ScaffoldingCommands < Thor
82
84
  else
83
85
  automation = ScaffoldProjectDetector.detect_automation(gemfile)
84
86
  framework = ScaffoldProjectDetector.detect_framework(gemfile)
85
- if automation.nil?
86
- say 'Warning: Could not detect automation library from Gemfile, defaulting to selenium.', :yellow
87
- end
87
+ say 'Warning: Could not detect automation library from Gemfile, defaulting to selenium.', :yellow if automation.nil?
88
88
  say 'Warning: Could not detect test framework from Gemfile, defaulting to rspec.', :yellow if framework.nil?
89
89
  end
90
90
  end
@@ -4,6 +4,8 @@ require 'thor'
4
4
  require_relative '../utilities/utilities'
5
5
 
6
6
  class UtilityCommands < Thor
7
+ def self.exit_on_failure? = true
8
+
7
9
  desc 'path [PATH]', 'Sets the default path for scaffolding'
8
10
  option :feature,
9
11
  type: :boolean, required: false, desc: 'The default path for your features', aliases: '-f'
@@ -68,12 +70,6 @@ class UtilityCommands < Thor
68
70
  end
69
71
  end
70
72
 
71
- desc 'config', 'Creates a new config file'
72
- option :path,
73
- type: :string, required: false, desc: 'The path where your config file will be created', aliases: '-p'
74
- option :delete,
75
- type: :boolean, required: false, desc: 'This will delete the selected config file', aliases: '-d'
76
-
77
73
  desc 'timeout [SECONDS]', 'Sets the default test timeout in seconds'
78
74
 
79
75
  def timeout(seconds)
@@ -114,4 +110,38 @@ class UtilityCommands < Thor
114
110
  say 'Raider Desktop downloaded successfully!', :green
115
111
  end
116
112
 
113
+ desc 'video [install|status|on|off]', 'Manages failure video recording (ffmpeg install, status, on/off)'
114
+
115
+ def video(action)
116
+ case action
117
+ when 'install' then video_install
118
+ when 'status' then video_status
119
+ when 'on', 'off' then video_toggle(action == 'on')
120
+ else
121
+ say_error "Unknown video command '#{action}'. Use install|status|on|off", :red
122
+ exit 1
123
+ end
124
+ end
125
+
126
+ no_commands do
127
+ def video_install
128
+ require_relative '../ruby_raider/ffmpeg'
129
+ path = RubyRaider::Ffmpeg.ensure!
130
+ exit 1 unless path
131
+
132
+ say path
133
+ end
134
+
135
+ def video_status
136
+ require_relative '../ruby_raider/ffmpeg'
137
+ path = RubyRaider::Ffmpeg.resolve
138
+ source = path && path == ENV['RAIDER_FFMPEG_PATH'] ? '(RAIDER_FFMPEG_PATH)' : RubyRaider::Ffmpeg::MANIFEST[:version]
139
+ say "ffmpeg #{source}: #{path || 'not installed'}"
140
+ end
141
+
142
+ def video_toggle(enabled)
143
+ Utilities.video_enabled = enabled
144
+ say "Failure video #{enabled ? 'enabled' : 'disabled'}", :green
145
+ end
146
+ end
117
147
  end
@@ -1 +1 @@
1
- default: --format pretty --publish-quiet --retry 0
1
+ default: --format pretty --format rerun --out rerun.txt --publish-quiet --retry 0
@@ -2,12 +2,17 @@
2
2
 
3
3
  require 'raider_reporter/cucumber'
4
4
  require 'tmpdir'
5
+ require_relative '../../helpers/video_helper'
5
6
 
6
7
  Before do
7
8
  driver.start_driver
9
+ @video_recorder = VideoHelper.recorder_for(driver)
10
+ @video_recorder.start
8
11
  end
9
12
 
10
13
  After do |scenario|
14
+ video_path = @video_recorder&.stop(keep: scenario.failed?)
15
+ RaiderReporter.attach('video', video_path, type: :mp4) if video_path
11
16
  if scenario.failed?
12
17
  Dir.mktmpdir do |temp_folder|
13
18
  screenshot_path = File.join(temp_folder, 'screenshot.png')
@@ -16,5 +21,6 @@ After do |scenario|
16
21
  end
17
22
  end
18
23
  ensure
24
+ @video_recorder&.cleanup
19
25
  driver.quit_driver
20
26
  end
@@ -7,6 +7,7 @@ require 'rspec'
7
7
  require 'raider_reporter/cucumber'
8
8
  require 'tmpdir'
9
9
  require 'yaml'
10
+ require_relative '../../helpers/video_helper'
10
11
 
11
12
  Before do
12
13
  viewport = YAML.load_file('config/config.yml')['viewport']
@@ -15,9 +16,13 @@ Before do
15
16
  else
16
17
  driver.manage.window.maximize
17
18
  end
19
+ @video_recorder = VideoHelper.recorder_for(driver)
20
+ @video_recorder.start
18
21
  end
19
22
 
20
23
  After do |scenario|
24
+ video_path = @video_recorder&.stop(keep: scenario.failed?)
25
+ RaiderReporter.attach('video', video_path, type: :webm) if video_path
21
26
  if scenario.failed?
22
27
  Dir.mktmpdir do |temp_folder|
23
28
  screenshot_path = File.join(temp_folder, 'screenshot.png')
@@ -26,5 +31,6 @@ After do |scenario|
26
31
  end
27
32
  end
28
33
  ensure
34
+ @video_recorder&.cleanup
29
35
  driver.quit
30
36
  end
@@ -3,6 +3,7 @@
3
3
  require 'raider_reporter/cucumber'
4
4
  require 'tmpdir'
5
5
  require 'yaml'
6
+ require_relative '../../helpers/video_helper'
6
7
 
7
8
  Before do
8
9
  viewport = YAML.load_file('config/config.yml')['viewport']
@@ -11,9 +12,13 @@ Before do
11
12
  else
12
13
  browser.window.maximize
13
14
  end
15
+ @video_recorder = VideoHelper.recorder_for(browser)
16
+ @video_recorder.start
14
17
  end
15
18
 
16
19
  After do |scenario|
20
+ video_path = @video_recorder&.stop(keep: scenario.failed?)
21
+ RaiderReporter.attach('video', video_path, type: :webm) if video_path
17
22
  if scenario.failed?
18
23
  Dir.mktmpdir do |temp_folder|
19
24
  screenshot_path = File.join(temp_folder, 'screenshot.png')
@@ -22,5 +27,6 @@ After do |scenario|
22
27
  end
23
28
  end
24
29
  ensure
30
+ @video_recorder&.cleanup
25
31
  browser.quit
26
32
  end
@@ -8,6 +8,7 @@ class HelpersGenerator < Generator
8
8
  generate_driver_helper
9
9
  generate_appium_helper
10
10
  generate_spec_helper
11
+ generate_video_helper
11
12
  end
12
13
 
13
14
  private
@@ -30,10 +31,13 @@ class HelpersGenerator < Generator
30
31
  template('helpers/driver_helper.tt', "#{name}/helpers/driver_helper.rb")
31
32
  end
32
33
 
34
+ def generate_video_helper
35
+ template('helpers/video_helper.tt', "#{name}/helpers/video_helper.rb")
36
+ end
37
+
33
38
  def generate_appium_helper
34
39
  return unless cross_platform?
35
40
 
36
41
  template('helpers/appium_helper.tt', "#{name}/helpers/appium_helper.rb")
37
42
  end
38
-
39
43
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative '../generator'
4
+ require_relative '../../ruby_raider/ffmpeg'
4
5
 
5
6
  class AzureGenerator < Generator
6
7
  def generate_azure_pipeline
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative '../generator'
4
+ require_relative '../../ruby_raider/ffmpeg'
4
5
 
5
6
  class GithubGenerator < Generator
6
7
  def generate_actions_file