Conversation
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Red by design: 173 scenarios have no fixture yet. Plan 2.0b writes them in seven batches; this test's message is the list of what remains, and the suite cannot call the ERB lane complete while it is non-empty. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…-context shapes accordion, aspect_ratio, shortcut_key, toggle, toggle_group, toast, data_table, select, codeblock — 25 fixtures, each green against the frozen canonical and strict snapshots with the components still Phlex. These are the shapes that had to be proved first: a block parameter writing raw HTML, text beside an element, yield(self) with group.ToggleGroupItem, DataTableForm reaching for the view context, a block yielding nothing, a heredoc, a nested .new. 148 scenarios remain. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…randomness components tooltip, date_picker, sidebar, dialog, alert_dialog, sheet — 23 fixtures. The harness pins SecureRandom.hex and rand(Range) in the ERB lane exactly as in the Phlex lane, so the generated ids and the skeleton widths match the recorded snapshots. 125 scenarios remain. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
dropdown_menu, context_menu, popover, hover_card, command, combobox, collapsible — 22 fixtures, green against the frozen snapshots. 103 scenarios remain. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
checkbox, radio_button, switch, input, textarea, masked_input, native_select, input_otp, form, calendar — 21 fixtures, green against the frozen snapshots. 82 scenarios remain. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
alert, badge, avatar, breadcrumb, card, separator, skeleton, progress — 23 fixtures, green against the frozen snapshots; badge/all_variants is the catalog's one loop, 28 badges with nothing between them. 59 scenarios remain. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
empty, table, tabs, pagination, carousel, chart, clipboard, theme_toggle, bubble, message, message_scroller — 23 fixtures, green against the frozen snapshots. 36 scenarios remain: link and typography. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…d the instrumenter id A controller run after Task 7 caught tooltip/default's ERB lane failing its own determinism check, once in fourteen runs. The cause is in the harness pin, not in a fixture: ActiveSupport's per-thread Instrumenter mints its id with SecureRandom.hex on the first instrumented render, inside the pin window. Task 7b records the evidence and the ruling that amends the plan's "harness does not change" boundary for one line and one test; the end-state counts move to 410 golden runs and 776 total. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…rumenter id to the pin ActionView instruments every render, and the first instrumentation on a thread creates ActiveSupport's Instrumenter, whose id is SecureRandom.hex(10). That call landed inside the harness pin on the first ERB render of a process, took the counter's first value, and shifted every generated id in that one render by one — so tooltip/default, select/default or date_picker/generated_id failed their own determinism check whenever one of them was the first ERB-lane test to run (about 3 runs in 190). render_erb now creates the instrumenter before activating the pin; a test renders twice on a fresh thread, where the registry is empty, and asserts identical output. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…scenario has one The last 36: link's six variants, four sizes and icon; typography's headings, nine text sizes, four weights, four elements, inline code, inline link and blockquote. 188 scenarios, 188 fixtures, 188 canonical and 188 strict snapshots, every component still Phlex. From here a migration changes an implementation, never the ruler. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…measured Through Herb 0.10.4, `-%>` removes the newline after an opening tag and `<%-` the indentation before one, but the `end` closing a `<%= … do %>` block ignores its own trim markers and the indentation before an output tag is emitted — so an indented one-render-per-line template is not whitespace-tight, and decision 8's "or <%-/-%>" does not hold for it. Fixtures are one line; the two layouts that do emit nothing are the candidates for sidecars, decided in 2.1. Spec §6 Phase 2.0, §6.2 and §9.1 amended; the catalog header points at the fixtures. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… wording, plan counts The final review found two spec passages the Task 9 amendments left inconsistent (§7's testing table and §9.4 still named a text-only strict lane and trim mode), decision 10's first bullet crediting `<%-` for what Herb's line-start rule does with or without markers, a reversal condition that named one of two needed changes, an overstated cost, and two counts in the plan (Task 7b's assertion count, the PR body's test breakdown). All prose; no code, fixture or snapshot changes; the coverage test's comment moves to the past tense. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
There was a problem hiding this comment.
3 issues found across 180 files
Not reviewed (too large): design/plans/2026-09-20-phase-2-0b-fixtures-implementation.md (~1,788 lines) - if these are generated or fixture files, add them to ignored paths to exclude them from future reviews.
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="gem/test/golden/views/clipboard/popover_success.html.erb">
<violation number="1" location="gem/test/golden/views/clipboard/popover_success.html.erb:1">
P2: This fixture renders a ClipboardPopover whose panel is hidden at initial render (parent div has class="hidden"), but the recorded snapshot freezes the panel in data-state="open". ContextMenu, HoverCard, and Popover all rest hidden panels in data-state="closed" (see context_menu_content.rb, hover_card_content.rb, popover_content.rb), and starting "open" leaves the animate-in classes active at rest when the panel is not visible. Since this PR records the golden snapshot for this scenario, set ClipboardPopover#default_attrs state to :closed so hidden popovers start closed, then re-record the snapshot.</violation>
</file>
<file name="gem/test/golden/views/switch/checked_without_hidden_input.html.erb">
<violation number="1" location="gem/test/golden/views/switch/checked_without_hidden_input.html.erb:1">
P2: This new fixture's golden snapshot locks in an inaccessible switch: the label carries role="switch" but no aria-checked, and the actual control (checkbox class="hidden peer") is display:none, so it is not keyboard-focusable. The fixture faithfully records the component's current behavior, so the fix belongs in lib/ruby_ui/switch/switch.rb: mirror the checked state onto aria-checked on the label and make the checkbox visually hidden but focusable (e.g. replace the "hidden" class with "sr-only peer") instead of display:none. Re-record this snapshot (and the switch default snapshot) once the component is updated.</violation>
</file>
<file name="gem/test/golden/harness.rb">
<violation number="1" location="gem/test/golden/harness.rb:39">
P3: The fix makes the Instrumenter deterministic on a single thread, but the pin state it relies on is not thread-local: `@active`, `@hex_calls`, and `@rand_calls` are instance variables on the `Golden::Harness` module, shared by every thread and reset by each render. If two threads render concurrently (the fresh-thread test in `gem/test/golden/harness_test.rb` is the first code path that exercises threads), the second thread's `ActiveSupport::Notifications.instrumenter` call can run while another thread has `@active == true`; its `SecureRandom.hex(10)` then calls `next_hex` and consumes a shared counter slot, shifting the ids of the in-flight render and of the new render. Each render also rewrites the shared counters and toggles `@active`, so one thread's `ensure` turns the pin off while another thread is still rendering — letting real randomness leak into a pinned render. The claimed "identical output on a fresh thread" property therefore holds only when renders across threads never overlap. Make the counters and `@active` thread-local (Thread.current) or serialize renders with a Mutex, or document that the harness is single-threaded-only.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| @@ -0,0 +1 @@ | |||
| <%= render RubyUI::ClipboardPopover.new(type: :success) do %>success<% end %> | |||
There was a problem hiding this comment.
P2: This fixture renders a ClipboardPopover whose panel is hidden at initial render (parent div has class="hidden"), but the recorded snapshot freezes the panel in data-state="open". ContextMenu, HoverCard, and Popover all rest hidden panels in data-state="closed" (see context_menu_content.rb, hover_card_content.rb, popover_content.rb), and starting "open" leaves the animate-in classes active at rest when the panel is not visible. Since this PR records the golden snapshot for this scenario, set ClipboardPopover#default_attrs state to :closed so hidden popovers start closed, then re-record the snapshot.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At gem/test/golden/views/clipboard/popover_success.html.erb, line 1:
<comment>This fixture renders a ClipboardPopover whose panel is hidden at initial render (parent div has class="hidden"), but the recorded snapshot freezes the panel in data-state="open". ContextMenu, HoverCard, and Popover all rest hidden panels in data-state="closed" (see context_menu_content.rb, hover_card_content.rb, popover_content.rb), and starting "open" leaves the animate-in classes active at rest when the panel is not visible. Since this PR records the golden snapshot for this scenario, set ClipboardPopover#default_attrs state to :closed so hidden popovers start closed, then re-record the snapshot.</comment>
<file context>
@@ -0,0 +1 @@
+<%= render RubyUI::ClipboardPopover.new(type: :success) do %>success<% end %>
</file context>
| @@ -0,0 +1 @@ | |||
| <%= render RubyUI::Switch.new(name: "notifications", include_hidden: false, checked: true, checked_value: "yes", unchecked_value: "no") %> | |||
There was a problem hiding this comment.
P2: This new fixture's golden snapshot locks in an inaccessible switch: the label carries role="switch" but no aria-checked, and the actual control (checkbox class="hidden peer") is display:none, so it is not keyboard-focusable. The fixture faithfully records the component's current behavior, so the fix belongs in lib/ruby_ui/switch/switch.rb: mirror the checked state onto aria-checked on the label and make the checkbox visually hidden but focusable (e.g. replace the "hidden" class with "sr-only peer") instead of display:none. Re-record this snapshot (and the switch default snapshot) once the component is updated.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At gem/test/golden/views/switch/checked_without_hidden_input.html.erb, line 1:
<comment>This new fixture's golden snapshot locks in an inaccessible switch: the label carries role="switch" but no aria-checked, and the actual control (checkbox class="hidden peer") is display:none, so it is not keyboard-focusable. The fixture faithfully records the component's current behavior, so the fix belongs in lib/ruby_ui/switch/switch.rb: mirror the checked state onto aria-checked on the label and make the checkbox visually hidden but focusable (e.g. replace the "hidden" class with "sr-only peer") instead of display:none. Re-record this snapshot (and the switch default snapshot) once the component is updated.</comment>
<file context>
@@ -0,0 +1 @@
+<%= render RubyUI::Switch.new(name: "notifications", include_hidden: false, checked: true, checked_value: "yes", unchecked_value: "no") %>
</file context>
| # thread creates the Instrumenter, whose id is SecureRandom.hex(10). | ||
| # Inside the pin that call would take the counter's first value and | ||
| # shift every generated id in that one render by one. Create it first. | ||
| ActiveSupport::Notifications.instrumenter |
There was a problem hiding this comment.
P3: The fix makes the Instrumenter deterministic on a single thread, but the pin state it relies on is not thread-local: @active, @hex_calls, and @rand_calls are instance variables on the Golden::Harness module, shared by every thread and reset by each render. If two threads render concurrently (the fresh-thread test in gem/test/golden/harness_test.rb is the first code path that exercises threads), the second thread's ActiveSupport::Notifications.instrumenter call can run while another thread has @active == true; its SecureRandom.hex(10) then calls next_hex and consumes a shared counter slot, shifting the ids of the in-flight render and of the new render. Each render also rewrites the shared counters and toggles @active, so one thread's ensure turns the pin off while another thread is still rendering — letting real randomness leak into a pinned render. The claimed "identical output on a fresh thread" property therefore holds only when renders across threads never overlap. Make the counters and @active thread-local (Thread.current) or serialize renders with a Mutex, or document that the harness is single-threaded-only.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At gem/test/golden/harness.rb, line 39:
<comment>The fix makes the Instrumenter deterministic on a single thread, but the pin state it relies on is not thread-local: `@active`, `@hex_calls`, and `@rand_calls` are instance variables on the `Golden::Harness` module, shared by every thread and reset by each render. If two threads render concurrently (the fresh-thread test in `gem/test/golden/harness_test.rb` is the first code path that exercises threads), the second thread's `ActiveSupport::Notifications.instrumenter` call can run while another thread has `@active == true`; its `SecureRandom.hex(10)` then calls `next_hex` and consumes a shared counter slot, shifting the ids of the in-flight render and of the new render. Each render also rewrites the shared counters and toggles `@active`, so one thread's `ensure` turns the pin off while another thread is still rendering — letting real randomness leak into a pinned render. The claimed "identical output on a fresh thread" property therefore holds only when renders across threads never overlap. Make the counters and `@active` thread-local (Thread.current) or serialize renders with a Mutex, or document that the harness is single-threaded-only.</comment>
<file context>
@@ -32,6 +32,11 @@ def render(&block)
+ # thread creates the Instrumenter, whose id is SecureRandom.hex(10).
+ # Inside the pin that call would take the counter's first value and
+ # shift every generated id in that one render by one. Create it first.
+ ActiveSupport::Notifications.instrumenter
@active = true
@hex_calls = 0
</file context>
What
Stacked on #548. Every one of the 188 golden scenarios now has an ERB
fixture under
gem/test/golden/views/, rendered through the harness(ReActionView → Herb) and compared against the canonical and strict
snapshots Phase 2.0a froze from Phlex — with every component still Phlex,
through
phlex-rails. Button's 15 were in #548; the other 173 are here, inseven batches, each green before the next.
A coverage test,
every scenario has a fixture, red from the first commit(173 missing) to the last (0). Its message was the inventory.
Every fixture is one line, plus a trailing newline. Decision 10 records why:
measured through Herb 0.10.4, the
endclosing a<%= … do %>blockignores its own trim markers and the indentation before an output tag is
emitted, so an indented one-render-per-line template is not
whitespace-tight — and the strict lane would see every such character.
The plan's Appendix A has each probe's source and output.
The shapes the catalog uses — a block parameter writing raw HTML,
yield(self)withgroup.ToggleGroupItem,DataTableFormreaching forthe view context, a block yielding nothing, a Ruby loop, a heredoc — were
each measured strict-identical before the plan was written; the plan's
evidence table has the list.
One harness fix the lane exposed (Task 7b). ActionView instruments every
render, and the first instrumentation on a thread creates ActiveSupport's
Instrumenter, whose id isSecureRandom.hex(10)— inside the harness pin,on the first ERB render of a process, that call took the counter's first
value and shifted every generated id in that render by one, so
tooltip/default,select/defaultordate_picker/generated_idfailedtheir own determinism check when one of them ran first (about 3 runs in
190).
render_erbnow creates the instrumenter before the pin; a testrenders twice on a fresh thread and asserts identical output. This amended
the plan's "harness does not change" boundary by a ruling taken during
execution — please ratify or revert it explicitly in review.
No component changes, no snapshot changes. Nothing under
gem/lib/,docs/or
mcp/moves; the harness changes by that one line.Why
Decision 7: with every fixture written before any component migrates, a
migration from 2.1 on changes only an implementation, never the ruler — "did I
write the fixture right" and "did I port the component right" stop being one
failure. Plan:
design/plans/2026-09-20-phase-2-0b-fixtures-implementation.md.Test steps
To see a fixture fail, add a space before
<% end %>intest/golden/views/shortcut_key/default.html.erband re-runrake golden:the canonical form passes, the strict form fails — whitespace only, exactly
what the strict lane exists for.
🤖 Generated with Claude Code
Summary by cubic
Adds the remaining 173 ERB fixtures so all 188 golden scenarios render through the harness and compare against the snapshots Phase 2.0a froze from Phlex. Components stay on Phlex this phase, so a later migration changes only an implementation, never the test baseline.
A coverage test enforces that every scenario has a fixture; it was red at 173 missing and is green now.
Harness fix
InstrumenterwhoseSecureRandom.hex(10)id landed inside the harness pin.render_erbnow creates the instrumenter before the pin, with a regression test rendering twice on a fresh thread.All fixtures are one line because measured Herb 0.10.4 trim behavior keeps the newline after a block-closing
endand emits indentation before output tags; recorded as decision 10.Written for commit 8ac23ac. Summary will update on new commits.