anima-core 0.3.0 → 1.0.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 (269) hide show
  1. checksums.yaml +4 -4
  2. data/.reek.yml +27 -1
  3. data/CHANGELOG.md +4 -0
  4. data/README.md +219 -25
  5. data/agents/codebase-analyzer.md +88 -0
  6. data/agents/codebase-pattern-finder.md +83 -0
  7. data/agents/documentation-researcher.md +59 -0
  8. data/agents/thoughts-analyzer.md +102 -0
  9. data/agents/web-search-researcher.md +71 -0
  10. data/anima-core.gemspec +3 -0
  11. data/app/channels/session_channel.rb +76 -28
  12. data/app/jobs/agent_request_job.rb +24 -0
  13. data/app/jobs/analytical_brain_job.rb +33 -0
  14. data/app/jobs/count_event_tokens_job.rb +1 -1
  15. data/app/models/concerns/event/broadcasting.rb +20 -2
  16. data/app/models/event.rb +1 -1
  17. data/app/models/goal.rb +91 -0
  18. data/app/models/session.rb +347 -22
  19. data/config/application.rb +2 -0
  20. data/db/migrate/20260314075248_add_subagent_support_to_sessions.rb +6 -0
  21. data/db/migrate/20260314112417_add_granted_tools_to_sessions.rb +5 -0
  22. data/db/migrate/20260314140000_add_name_to_sessions.rb +7 -0
  23. data/db/migrate/20260314150000_add_viewport_event_ids_to_sessions.rb +7 -0
  24. data/db/migrate/20260315100000_add_active_skills_to_sessions.rb +7 -0
  25. data/db/migrate/20260315140843_create_goals.rb +16 -0
  26. data/db/migrate/20260315144837_add_completed_at_to_goals.rb +5 -0
  27. data/db/migrate/20260315191105_add_active_workflow_to_sessions.rb +5 -0
  28. data/lib/agent_loop.rb +65 -9
  29. data/lib/agents/definition.rb +116 -0
  30. data/lib/agents/registry.rb +106 -0
  31. data/lib/analytical_brain/runner.rb +276 -0
  32. data/lib/analytical_brain/tools/activate_skill.rb +52 -0
  33. data/lib/analytical_brain/tools/deactivate_skill.rb +43 -0
  34. data/lib/analytical_brain/tools/deactivate_workflow.rb +34 -0
  35. data/lib/analytical_brain/tools/everything_is_ready.rb +28 -0
  36. data/lib/analytical_brain/tools/finish_goal.rb +62 -0
  37. data/lib/analytical_brain/tools/read_workflow.rb +58 -0
  38. data/lib/analytical_brain/tools/rename_session.rb +63 -0
  39. data/lib/analytical_brain/tools/set_goal.rb +60 -0
  40. data/lib/analytical_brain/tools/update_goal.rb +60 -0
  41. data/lib/analytical_brain.rb +23 -0
  42. data/lib/anima/cli/mcp/secrets.rb +76 -0
  43. data/lib/anima/cli/mcp.rb +197 -0
  44. data/lib/anima/cli.rb +4 -0
  45. data/lib/anima/installer.rb +168 -0
  46. data/lib/anima/settings.rb +226 -0
  47. data/lib/anima/version.rb +1 -1
  48. data/lib/anima.rb +9 -0
  49. data/lib/credential_store.rb +103 -0
  50. data/lib/environment_probe.rb +232 -0
  51. data/lib/llm/client.rb +29 -10
  52. data/lib/mcp/client_manager.rb +86 -0
  53. data/lib/mcp/config.rb +213 -0
  54. data/lib/mcp/health_check.rb +77 -0
  55. data/lib/mcp/secrets.rb +73 -0
  56. data/lib/mcp/stdio_transport.rb +206 -0
  57. data/lib/providers/anthropic.rb +8 -7
  58. data/lib/shell_session.rb +11 -10
  59. data/lib/skills/definition.rb +97 -0
  60. data/lib/skills/registry.rb +105 -0
  61. data/lib/tools/edit.rb +3 -4
  62. data/lib/tools/mcp_tool.rb +114 -0
  63. data/lib/tools/read.rb +15 -16
  64. data/lib/tools/registry.rb +14 -12
  65. data/lib/tools/request_feature.rb +121 -0
  66. data/lib/tools/return_result.rb +81 -0
  67. data/lib/tools/spawn_specialist.rb +109 -0
  68. data/lib/tools/spawn_subagent.rb +111 -0
  69. data/lib/tools/subagent_prompts.rb +12 -0
  70. data/lib/tools/web_get.rb +8 -9
  71. data/lib/tui/app.rb +332 -43
  72. data/lib/tui/message_store.rb +20 -0
  73. data/lib/tui/screens/chat.rb +207 -20
  74. data/lib/workflows/definition.rb +97 -0
  75. data/lib/workflows/registry.rb +89 -0
  76. data/skills/activerecord/SKILL.md +255 -0
  77. data/skills/activerecord/examples/associations/association_extensions.rb +298 -0
  78. data/skills/activerecord/examples/associations/basic_associations.rb +118 -0
  79. data/skills/activerecord/examples/associations/counter_caches.rb +215 -0
  80. data/skills/activerecord/examples/associations/polymorphic_associations.rb +217 -0
  81. data/skills/activerecord/examples/associations/self_referential.rb +302 -0
  82. data/skills/activerecord/examples/associations/through_associations.rb +203 -0
  83. data/skills/activerecord/examples/basics/crud_operations.rb +209 -0
  84. data/skills/activerecord/examples/basics/dirty_tracking.rb +218 -0
  85. data/skills/activerecord/examples/basics/inheritance.rb +377 -0
  86. data/skills/activerecord/examples/basics/type_casting.rb +317 -0
  87. data/skills/activerecord/examples/callbacks/alternatives_to_callbacks.rb +447 -0
  88. data/skills/activerecord/examples/callbacks/conditional_callbacks.rb +353 -0
  89. data/skills/activerecord/examples/callbacks/lifecycle_callbacks.rb +280 -0
  90. data/skills/activerecord/examples/callbacks/transaction_callbacks.rb +340 -0
  91. data/skills/activerecord/examples/migrations/indexes_and_constraints.rb +337 -0
  92. data/skills/activerecord/examples/migrations/reversible_patterns.rb +403 -0
  93. data/skills/activerecord/examples/migrations/safe_patterns.rb +420 -0
  94. data/skills/activerecord/examples/migrations/schema_changes.rb +277 -0
  95. data/skills/activerecord/examples/querying/batch_processing.rb +226 -0
  96. data/skills/activerecord/examples/querying/eager_loading.rb +259 -0
  97. data/skills/activerecord/examples/querying/finder_methods.rb +170 -0
  98. data/skills/activerecord/examples/querying/optimization.rb +275 -0
  99. data/skills/activerecord/examples/querying/scopes.rb +260 -0
  100. data/skills/activerecord/examples/validations/built_in_validators.rb +277 -0
  101. data/skills/activerecord/examples/validations/conditional_validations.rb +288 -0
  102. data/skills/activerecord/examples/validations/custom_validators.rb +381 -0
  103. data/skills/activerecord/examples/validations/database_constraints.rb +432 -0
  104. data/skills/activerecord/examples/validations/validation_contexts.rb +367 -0
  105. data/skills/activerecord/references/associations.md +709 -0
  106. data/skills/activerecord/references/basics.md +622 -0
  107. data/skills/activerecord/references/callbacks.md +738 -0
  108. data/skills/activerecord/references/migrations.md +657 -0
  109. data/skills/activerecord/references/querying.md +655 -0
  110. data/skills/activerecord/references/validations.md +596 -0
  111. data/skills/dragonruby/SKILL.md +250 -0
  112. data/skills/dragonruby/examples/audio/audio_events.rb +55 -0
  113. data/skills/dragonruby/examples/audio/background_music.rb +29 -0
  114. data/skills/dragonruby/examples/audio/crossfade.rb +51 -0
  115. data/skills/dragonruby/examples/audio/music_controls.rb +51 -0
  116. data/skills/dragonruby/examples/audio/sound_effects.rb +30 -0
  117. data/skills/dragonruby/examples/core/coordinate_system.rb +27 -0
  118. data/skills/dragonruby/examples/core/hello_world.rb +24 -0
  119. data/skills/dragonruby/examples/core/labels.rb +22 -0
  120. data/skills/dragonruby/examples/core/sprites.rb +35 -0
  121. data/skills/dragonruby/examples/core/state_management.rb +29 -0
  122. data/skills/dragonruby/examples/distribution/background_pause.rb +42 -0
  123. data/skills/dragonruby/examples/distribution/build_workflow.sh +26 -0
  124. data/skills/dragonruby/examples/distribution/cvars_production.txt +16 -0
  125. data/skills/dragonruby/examples/distribution/game_metadata_hd.txt +23 -0
  126. data/skills/dragonruby/examples/distribution/game_metadata_minimal.txt +9 -0
  127. data/skills/dragonruby/examples/distribution/game_metadata_mobile.txt +31 -0
  128. data/skills/dragonruby/examples/distribution/platform_detection.rb +36 -0
  129. data/skills/dragonruby/examples/distribution/steam_metadata.txt +19 -0
  130. data/skills/dragonruby/examples/entities/collision_detection.rb +43 -0
  131. data/skills/dragonruby/examples/entities/entity_lifecycle.rb +68 -0
  132. data/skills/dragonruby/examples/entities/entity_storage.rb +38 -0
  133. data/skills/dragonruby/examples/entities/factory_methods.rb +45 -0
  134. data/skills/dragonruby/examples/entities/random_spawning.rb +50 -0
  135. data/skills/dragonruby/examples/game-logic/reset_patterns.rb +98 -0
  136. data/skills/dragonruby/examples/game-logic/save_load.rb +101 -0
  137. data/skills/dragonruby/examples/game-logic/scoring.rb +104 -0
  138. data/skills/dragonruby/examples/game-logic/state_transitions.rb +103 -0
  139. data/skills/dragonruby/examples/game-logic/timers.rb +87 -0
  140. data/skills/dragonruby/examples/input/action_triggers.rb +36 -0
  141. data/skills/dragonruby/examples/input/analog_movement.rb +28 -0
  142. data/skills/dragonruby/examples/input/controller_input.rb +28 -0
  143. data/skills/dragonruby/examples/input/directional_input.rb +24 -0
  144. data/skills/dragonruby/examples/input/keyboard_input.rb +28 -0
  145. data/skills/dragonruby/examples/input/mouse_click.rb +26 -0
  146. data/skills/dragonruby/examples/input/movement_with_bounds.rb +22 -0
  147. data/skills/dragonruby/examples/input/normalized_movement.rb +32 -0
  148. data/skills/dragonruby/examples/rendering/frame_animation.rb +32 -0
  149. data/skills/dragonruby/examples/rendering/labels.rb +32 -0
  150. data/skills/dragonruby/examples/rendering/layering.rb +51 -0
  151. data/skills/dragonruby/examples/rendering/solids.rb +61 -0
  152. data/skills/dragonruby/examples/rendering/sprites.rb +33 -0
  153. data/skills/dragonruby/examples/rendering/spritesheet_animation.rb +39 -0
  154. data/skills/dragonruby/examples/scenes/case_dispatch.rb +60 -0
  155. data/skills/dragonruby/examples/scenes/class_based.rb +150 -0
  156. data/skills/dragonruby/examples/scenes/pause_overlay.rb +100 -0
  157. data/skills/dragonruby/examples/scenes/safe_transitions.rb +68 -0
  158. data/skills/dragonruby/examples/scenes/scene_transitions.rb +98 -0
  159. data/skills/dragonruby/examples/scenes/send_dispatch.rb +88 -0
  160. data/skills/dragonruby/references/audio.md +396 -0
  161. data/skills/dragonruby/references/core.md +385 -0
  162. data/skills/dragonruby/references/distribution.md +434 -0
  163. data/skills/dragonruby/references/entities.md +516 -0
  164. data/skills/dragonruby/references/game-logic/persistence.md +386 -0
  165. data/skills/dragonruby/references/game-logic/state.md +389 -0
  166. data/skills/dragonruby/references/input.md +414 -0
  167. data/skills/dragonruby/references/rendering/animation.md +467 -0
  168. data/skills/dragonruby/references/rendering/primitives.md +403 -0
  169. data/skills/dragonruby/references/scenes.md +443 -0
  170. data/skills/draper-decorators/SKILL.md +344 -0
  171. data/skills/draper-decorators/examples/application_decorator.rb +61 -0
  172. data/skills/draper-decorators/examples/decorator_spec.rb +253 -0
  173. data/skills/draper-decorators/examples/model_decorator.rb +152 -0
  174. data/skills/draper-decorators/references/anti-patterns.md +640 -0
  175. data/skills/draper-decorators/references/patterns.md +507 -0
  176. data/skills/draper-decorators/references/testing.md +559 -0
  177. data/skills/gh-issue.md +182 -0
  178. data/skills/mcp-server/SKILL.md +177 -0
  179. data/skills/mcp-server/examples/dynamic_tools.rb +36 -0
  180. data/skills/mcp-server/examples/file_manager_tool.rb +85 -0
  181. data/skills/mcp-server/examples/http_client.rb +48 -0
  182. data/skills/mcp-server/examples/http_server.rb +97 -0
  183. data/skills/mcp-server/examples/rails_integration.rb +88 -0
  184. data/skills/mcp-server/examples/stdio_server.rb +108 -0
  185. data/skills/mcp-server/examples/streaming_client.rb +95 -0
  186. data/skills/mcp-server/references/gotchas.md +183 -0
  187. data/skills/mcp-server/references/prompts.md +98 -0
  188. data/skills/mcp-server/references/resources.md +53 -0
  189. data/skills/mcp-server/references/server.md +140 -0
  190. data/skills/mcp-server/references/tools.md +146 -0
  191. data/skills/mcp-server/references/transport.md +104 -0
  192. data/skills/ratatui-ruby/SKILL.md +315 -0
  193. data/skills/ratatui-ruby/references/core-concepts.md +340 -0
  194. data/skills/ratatui-ruby/references/events.md +387 -0
  195. data/skills/ratatui-ruby/references/frameworks.md +522 -0
  196. data/skills/ratatui-ruby/references/layout.md +423 -0
  197. data/skills/ratatui-ruby/references/styling.md +268 -0
  198. data/skills/ratatui-ruby/references/testing.md +433 -0
  199. data/skills/ratatui-ruby/references/widgets.md +532 -0
  200. data/skills/rspec/SKILL.md +340 -0
  201. data/skills/rspec/examples/core/basic_structure.rb +69 -0
  202. data/skills/rspec/examples/core/configuration.rb +126 -0
  203. data/skills/rspec/examples/core/hooks.rb +126 -0
  204. data/skills/rspec/examples/core/memoized_helpers.rb +139 -0
  205. data/skills/rspec/examples/core/metadata_filtering.rb +144 -0
  206. data/skills/rspec/examples/core/shared_examples.rb +145 -0
  207. data/skills/rspec/examples/factory_bot/associations.rb +314 -0
  208. data/skills/rspec/examples/factory_bot/build_strategies.rb +272 -0
  209. data/skills/rspec/examples/factory_bot/callbacks.rb +320 -0
  210. data/skills/rspec/examples/factory_bot/custom_construction.rb +328 -0
  211. data/skills/rspec/examples/factory_bot/factory_definition.rb +191 -0
  212. data/skills/rspec/examples/factory_bot/inheritance.rb +314 -0
  213. data/skills/rspec/examples/factory_bot/traits.rb +293 -0
  214. data/skills/rspec/examples/factory_bot/transients.rb +229 -0
  215. data/skills/rspec/examples/matchers/change.rb +115 -0
  216. data/skills/rspec/examples/matchers/collections.rb +154 -0
  217. data/skills/rspec/examples/matchers/comparisons.rb +79 -0
  218. data/skills/rspec/examples/matchers/composing.rb +155 -0
  219. data/skills/rspec/examples/matchers/custom_matchers.rb +197 -0
  220. data/skills/rspec/examples/matchers/equality.rb +58 -0
  221. data/skills/rspec/examples/matchers/errors.rb +136 -0
  222. data/skills/rspec/examples/matchers/output.rb +103 -0
  223. data/skills/rspec/examples/matchers/predicates.rb +87 -0
  224. data/skills/rspec/examples/matchers/truthiness.rb +101 -0
  225. data/skills/rspec/examples/matchers/types.rb +82 -0
  226. data/skills/rspec/examples/matchers/yield.rb +147 -0
  227. data/skills/rspec/examples/mocks/any_instance.rb +172 -0
  228. data/skills/rspec/examples/mocks/argument_matchers.rb +206 -0
  229. data/skills/rspec/examples/mocks/constants.rb +177 -0
  230. data/skills/rspec/examples/mocks/doubles.rb +139 -0
  231. data/skills/rspec/examples/mocks/expectations.rb +137 -0
  232. data/skills/rspec/examples/mocks/message_chains.rb +173 -0
  233. data/skills/rspec/examples/mocks/ordering.rb +144 -0
  234. data/skills/rspec/examples/mocks/receive_counts.rb +181 -0
  235. data/skills/rspec/examples/mocks/responses.rb +223 -0
  236. data/skills/rspec/examples/mocks/spies.rb +149 -0
  237. data/skills/rspec/examples/mocks/stubbing.rb +133 -0
  238. data/skills/rspec/examples/rails/channels.rb +250 -0
  239. data/skills/rspec/examples/rails/controller_specs.rb +302 -0
  240. data/skills/rspec/examples/rails/helper_specs.rb +245 -0
  241. data/skills/rspec/examples/rails/job_specs.rb +256 -0
  242. data/skills/rspec/examples/rails/mailer_specs.rb +228 -0
  243. data/skills/rspec/examples/rails/matchers.rb +374 -0
  244. data/skills/rspec/examples/rails/model_specs.rb +193 -0
  245. data/skills/rspec/examples/rails/request_specs.rb +275 -0
  246. data/skills/rspec/examples/rails/routing_specs.rb +276 -0
  247. data/skills/rspec/examples/rails/system_specs.rb +294 -0
  248. data/skills/rspec/examples/rails/transactions.rb +254 -0
  249. data/skills/rspec/examples/rails/view_specs.rb +252 -0
  250. data/skills/rspec/references/core.md +816 -0
  251. data/skills/rspec/references/factory_bot.md +641 -0
  252. data/skills/rspec/references/matchers.md +516 -0
  253. data/skills/rspec/references/mocks.md +381 -0
  254. data/skills/rspec/references/rails.md +528 -0
  255. data/templates/soul.md +40 -0
  256. data/workflows/commit.md +45 -0
  257. data/workflows/create_handoff.md +98 -0
  258. data/workflows/create_note.md +82 -0
  259. data/workflows/create_plan.md +457 -0
  260. data/workflows/decompose_ticket.md +109 -0
  261. data/workflows/feature.md +91 -0
  262. data/workflows/implement_plan.md +87 -0
  263. data/workflows/iterate_plan.md +247 -0
  264. data/workflows/research_codebase.md +210 -0
  265. data/workflows/resume_handoff.md +217 -0
  266. data/workflows/review_pr.md +320 -0
  267. data/workflows/thoughts_init.md +71 -0
  268. data/workflows/validate_plan.md +166 -0
  269. metadata +284 -1
@@ -0,0 +1,433 @@
1
+ # Testing Infrastructure
2
+
3
+ RatatuiRuby provides comprehensive TUI testing through the `TestHelper` module: headless terminals, event injection, snapshots, and style assertions.
4
+
5
+ ## Setup
6
+
7
+ ```ruby
8
+ require "ratatui_ruby/test_helper"
9
+ require "minitest/autorun"
10
+
11
+ class MyAppTest < Minitest::Test
12
+ include RatatuiRuby::TestHelper
13
+
14
+ def test_basic
15
+ with_test_terminal do
16
+ widget = tui.paragraph(text: "Hello")
17
+ RatatuiRuby.draw { |frame| frame.render_widget(widget, frame.area) }
18
+ assert_includes buffer_content.first, "Hello"
19
+ end
20
+ end
21
+ end
22
+ ```
23
+
24
+ Including `TestHelper` automatically enables debug mode for Rust backtraces.
25
+
26
+ ## Headless Terminal
27
+
28
+ ### with_test_terminal
29
+
30
+ ```ruby
31
+ with_test_terminal(width = 80, height = 24, **opts) { || ... }
32
+ ```
33
+
34
+ **Options:**
35
+ - `width`, `height`: Terminal dimensions (default: 80x24)
36
+ - `timeout`: Max execution time in seconds (default: 2, `nil` to disable)
37
+ - `viewport`: Optional viewport configuration
38
+
39
+ ```ruby
40
+ def test_custom_dimensions
41
+ with_test_terminal(120, 40, timeout: 5) do
42
+ # Test code
43
+ end
44
+ end
45
+ ```
46
+
47
+ ### Buffer Inspection
48
+
49
+ #### buffer_content
50
+
51
+ Returns array of strings (one per row):
52
+
53
+ ```ruby
54
+ content = buffer_content
55
+ assert_equal "Header", content[0].strip
56
+ assert_includes content[1], "Body"
57
+ ```
58
+
59
+ #### get_cell(x, y)
60
+
61
+ Cell attributes at coordinates:
62
+
63
+ ```ruby
64
+ cell = get_cell(0, 0)
65
+ cell.symbol # or cell.char - Character
66
+ cell.fg # Foreground color
67
+ cell.bg # Background color
68
+ ```
69
+
70
+ #### cursor_position
71
+
72
+ ```ruby
73
+ pos = cursor_position
74
+ pos[:x] # Column
75
+ pos[:y] # Row
76
+ ```
77
+
78
+ #### print_buffer
79
+
80
+ Outputs buffer with ANSI colors for debugging:
81
+
82
+ ```ruby
83
+ print_buffer # Visual inspection during development
84
+ ```
85
+
86
+ ## Event Injection
87
+
88
+ ### inject_keys
89
+
90
+ **String format (characters):**
91
+ ```ruby
92
+ inject_keys("h", "e", "l", "l", "o")
93
+ ```
94
+
95
+ **Symbol format (named keys):**
96
+ ```ruby
97
+ inject_keys(:enter, :esc, :tab, :backspace)
98
+ inject_keys(:up, :down, :left, :right)
99
+ inject_keys(:ctrl_c, :ctrl_d)
100
+ ```
101
+
102
+ **Hash format (modifiers):**
103
+ ```ruby
104
+ inject_keys({code: "k", modifiers: ["ctrl"]})
105
+ inject_keys({code: "s", modifiers: ["ctrl", "shift"]})
106
+ ```
107
+
108
+ ### Mouse Events
109
+
110
+ ```ruby
111
+ inject_mouse(x: 10, y: 5, kind: :down, button: :left, modifiers: [])
112
+ inject_mouse(x: 10, y: 5, kind: :up, button: :left)
113
+ ```
114
+
115
+ **Convenience methods:**
116
+ ```ruby
117
+ inject_click(x: 15, y: 8) # Left click
118
+ inject_right_click(x: 20, y: 10, modifiers: ["ctrl"])
119
+ inject_drag(x: 10, y: 5) # During drag
120
+ ```
121
+
122
+ ### inject_sync
123
+
124
+ Wait for async operations to complete:
125
+
126
+ ```ruby
127
+ start_async_operation
128
+ inject_sync
129
+ assert_includes buffer_content.join, "Data loaded"
130
+ ```
131
+
132
+ ### inject_event
133
+
134
+ Generic method for any event type:
135
+
136
+ ```ruby
137
+ event = RatatuiRuby::Event::Resize.new(width: 100, height: 30)
138
+ inject_event(event)
139
+ ```
140
+
141
+ ## Snapshot Testing
142
+
143
+ ### assert_snapshots (Recommended)
144
+
145
+ Creates both plain text and ANSI snapshots:
146
+
147
+ ```ruby
148
+ def test_initial_screen
149
+ with_test_terminal do
150
+ MyApp.new.run_once
151
+ assert_snapshots("initial")
152
+ end
153
+ end
154
+ ```
155
+
156
+ Creates:
157
+ - `snapshots/initial.txt` - Plain text
158
+ - `snapshots/initial.ansi` - With ANSI codes
159
+
160
+ ### Snapshot Workflow
161
+
162
+ **Create/update snapshots:**
163
+ ```bash
164
+ UPDATE_SNAPSHOTS=1 bundle exec rake test
165
+ ```
166
+
167
+ **Compare against existing:**
168
+ ```bash
169
+ bundle exec rake test
170
+ ```
171
+
172
+ ### Content Normalization
173
+
174
+ Handle dynamic content:
175
+
176
+ ```ruby
177
+ assert_snapshots("dashboard") do |lines|
178
+ lines.map { |line| line.gsub(/\d{4}-\d{2}-\d{2}/, "YYYY-MM-DD") }
179
+ end
180
+
181
+ assert_snapshots("users") do |lines|
182
+ lines.map { |line| line.gsub(/ID:\s*\d+/, "ID: XXXXX") }
183
+ end
184
+ ```
185
+
186
+ ### Deterministic Data
187
+
188
+ Seed random generators for stable snapshots:
189
+
190
+ ```ruby
191
+ def setup
192
+ @rng = Random.new(42)
193
+ @data = (0..20).map { @rng.rand(0.0..10.0) }
194
+ end
195
+ ```
196
+
197
+ ### Specialized Methods
198
+
199
+ ```ruby
200
+ assert_plain_snapshot("layout") # Only .txt
201
+ assert_rich_snapshot("styles") # Only .ansi
202
+ ```
203
+
204
+ ## Style Assertions
205
+
206
+ ### assert_cell_style
207
+
208
+ ```ruby
209
+ assert_cell_style(0, 0, char: "H", fg: :red, bg: :black)
210
+ assert_cell_style(1, 0, char: "e", fg: :red)
211
+ ```
212
+
213
+ ### Color Assertions
214
+
215
+ ```ruby
216
+ assert_color(:red, x: 0, y: 0, layer: :fg)
217
+ assert_color(196, x: 1, y: 0, layer: :fg) # Indexed
218
+ assert_color("#FF0000", x: 2, y: 0, layer: :fg) # Hex
219
+
220
+ # Shortcuts
221
+ assert_fg_color(:red, 0, 0)
222
+ assert_bg_color(:black, 0, 0)
223
+ ```
224
+
225
+ ### Area Style
226
+
227
+ ```ruby
228
+ # Hash notation
229
+ assert_area_style({x: 0, y: 0, w: 80, h: 1}, fg: :white, bg: :blue)
230
+
231
+ # Rect object
232
+ sidebar = RatatuiRuby::Layout::Rect.new(x: 0, y: 1, width: 20, height: 23)
233
+ assert_area_style(sidebar, bg: :dark_gray)
234
+ ```
235
+
236
+ ### Modifier Assertions
237
+
238
+ ```ruby
239
+ assert_bold(0, 0)
240
+ assert_italic(5, 0)
241
+ assert_underlined(10, 0)
242
+ assert_crossed_out(15, 0)
243
+ assert_reversed(20, 0)
244
+ assert_dim(25, 0)
245
+ assert_hidden(30, 0)
246
+ assert_slow_blink(35, 0)
247
+ assert_rapid_blink(40, 0)
248
+ ```
249
+
250
+ ## Test Doubles
251
+
252
+ ### MockFrame
253
+
254
+ Captures rendered widgets for inspection:
255
+
256
+ ```ruby
257
+ frame = RatatuiRuby::TestHelper::TestDoubles::MockFrame.new
258
+ area = RatatuiRuby::TestHelper::TestDoubles::StubRect.new(width: 80, height: 24)
259
+
260
+ View::Log.new.call(state, tui, frame, area)
261
+
262
+ widget = frame.rendered_widgets.first[:widget]
263
+ assert_equal "Event Log", widget.block.title
264
+ ```
265
+
266
+ ### StubRect
267
+
268
+ Fixed dimensions for layout testing:
269
+
270
+ ```ruby
271
+ narrow = RatatuiRuby::TestHelper::TestDoubles::StubRect.new(
272
+ x: 0, y: 0, width: 40, height: 24
273
+ )
274
+
275
+ wide = RatatuiRuby::TestHelper::TestDoubles::StubRect.new(
276
+ width: 120, height: 24
277
+ )
278
+ ```
279
+
280
+ ## RSpec Integration
281
+
282
+ ```ruby
283
+ require "ratatui_ruby/test_helper"
284
+
285
+ RSpec.describe MyApp do
286
+ include RatatuiRuby::TestHelper
287
+
288
+ describe "rendering" do
289
+ it "displays welcome message" do
290
+ with_test_terminal do
291
+ subject.run_once
292
+ expect(buffer_content.join).to include("Welcome")
293
+ end
294
+ end
295
+ end
296
+
297
+ describe "navigation" do
298
+ it "responds to arrow keys" do
299
+ with_test_terminal do
300
+ subject.run_once
301
+ inject_keys(:down, :down, :enter)
302
+ subject.handle_events
303
+ expect(buffer_content[2]).to include("Selected")
304
+ end
305
+ end
306
+ end
307
+ end
308
+ ```
309
+
310
+ ## Testing Patterns
311
+
312
+ ### State Transitions
313
+
314
+ ```ruby
315
+ def test_menu_navigation
316
+ with_test_terminal do
317
+ app = MyApp.new
318
+ app.run_once
319
+ assert_snapshots("menu_initial")
320
+
321
+ inject_keys(:down)
322
+ app.handle_events
323
+ assert_snapshots("menu_item_1")
324
+
325
+ inject_keys(:enter)
326
+ app.handle_events
327
+ assert_snapshots("menu_selected")
328
+ end
329
+ end
330
+ ```
331
+
332
+ ### Full User Flow
333
+
334
+ ```ruby
335
+ def test_complete_flow
336
+ with_test_terminal(120, 40, timeout: 5) do
337
+ app = MyApp.new
338
+ app.run_once
339
+
340
+ # Initial render
341
+ assert_snapshots("startup")
342
+ assert_fg_color(:white, 0, 0)
343
+
344
+ # Navigate
345
+ inject_keys(:down, :down)
346
+ app.handle_events
347
+ assert_bold(0, 2)
348
+
349
+ # Click
350
+ inject_click(x: 5, y: 2)
351
+ app.handle_events
352
+ assert_includes buffer_content.join, "Selected"
353
+
354
+ assert_snapshots("after_selection")
355
+ end
356
+ end
357
+ ```
358
+
359
+ ### Isolated View Testing
360
+
361
+ ```ruby
362
+ def test_header_view_isolated
363
+ frame = RatatuiRuby::TestHelper::TestDoubles::MockFrame.new
364
+ area = RatatuiRuby::TestHelper::TestDoubles::StubRect.new(width: 80, height: 3)
365
+ state = {title: "App", version: "1.0"}
366
+
367
+ View::Header.new.render(state, frame, area)
368
+
369
+ widget = frame.rendered_widgets.first[:widget]
370
+ assert_equal "App", widget.block.title
371
+ end
372
+ ```
373
+
374
+ ### Progressive Assertions
375
+
376
+ ```ruby
377
+ def test_dashboard
378
+ with_test_terminal do
379
+ render_dashboard
380
+
381
+ # Level 1: Snapshot
382
+ assert_snapshots("dashboard")
383
+
384
+ # Level 2: Content
385
+ assert_includes buffer_content.join, "Dashboard"
386
+
387
+ # Level 3: Styles
388
+ assert_fg_color(:cyan, 0, 0)
389
+ assert_bold(0, 0)
390
+
391
+ # Level 4: Cell inspection
392
+ cell = get_cell(0, 0)
393
+ assert_equal "D", cell["symbol"]
394
+ end
395
+ end
396
+ ```
397
+
398
+ ## Debugging
399
+
400
+ ```ruby
401
+ def test_complex_layout
402
+ with_test_terminal do
403
+ MyApp.new.render_dashboard
404
+
405
+ print_buffer # Visual output
406
+ binding.irb # Interactive debugging
407
+
408
+ assert_snapshots("dashboard")
409
+ end
410
+ end
411
+ ```
412
+
413
+ ## Quick Reference
414
+
415
+ | Method | Purpose |
416
+ |--------|---------|
417
+ | `with_test_terminal` | Headless terminal context |
418
+ | `buffer_content` | Array of row strings |
419
+ | `get_cell(x, y)` | Cell attributes |
420
+ | `cursor_position` | Cursor coordinates |
421
+ | `print_buffer` | Debug output |
422
+ | `inject_keys(...)` | Keyboard events |
423
+ | `inject_mouse(...)` | Mouse events |
424
+ | `inject_click(x:, y:)` | Left click |
425
+ | `inject_sync` | Wait for async |
426
+ | `assert_snapshots(name)` | Visual comparison |
427
+ | `assert_cell_style(x, y, ...)` | Cell validation |
428
+ | `assert_fg_color(color, x, y)` | Foreground color |
429
+ | `assert_bg_color(color, x, y)` | Background color |
430
+ | `assert_area_style(area, ...)` | Region validation |
431
+ | `assert_bold(x, y)` | Bold modifier |
432
+ | `MockFrame` | Widget capture |
433
+ | `StubRect` | Fixed dimensions |