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,559 @@
1
+ # Testing Draper Decorators with RSpec
2
+
3
+ ## Test Setup
4
+
5
+ Draper automatically integrates with RSpec. Specs in `spec/decorators/` are auto-tagged with `type: :decorator`.
6
+
7
+ ### Rails Helper Inclusion
8
+
9
+ ```ruby
10
+ # spec/decorators/user_decorator_spec.rb
11
+ require 'rails_helper'
12
+
13
+ RSpec.describe UserDecorator do
14
+ # Tests go here
15
+ end
16
+ ```
17
+
18
+ ### View Context
19
+
20
+ Draper clears view context before each decorator spec automatically. Access helpers via `helpers` method.
21
+
22
+ ## Basic Test Patterns
23
+
24
+ ### Subject and Let
25
+
26
+ ```ruby
27
+ RSpec.describe UserDecorator do
28
+ subject(:decorator) { described_class.new(user) }
29
+
30
+ let(:user) { build_stubbed(:user, first_name: "John", last_name: "Doe") }
31
+
32
+ describe "#full_name" do
33
+ subject(:full_name) { decorator.full_name }
34
+
35
+ it "combines first and last name" do
36
+ expect(full_name).to eq("John Doe")
37
+ end
38
+ end
39
+ end
40
+ ```
41
+
42
+ ### Testing Formatted Output
43
+
44
+ ```ruby
45
+ RSpec.describe OrderDecorator do
46
+ subject(:decorator) { described_class.new(order) }
47
+
48
+ describe "#formatted_total" do
49
+ subject(:formatted_total) { decorator.formatted_total }
50
+
51
+ let(:order) { build_stubbed(:order, total: 99.99) }
52
+
53
+ it "formats as currency" do
54
+ expect(formatted_total).to eq("$99.99")
55
+ end
56
+ end
57
+
58
+ describe "#formatted_created_at" do
59
+ subject(:formatted_date) { decorator.formatted_created_at }
60
+
61
+ let(:order) { build_stubbed(:order, created_at: Time.zone.parse("2024-03-15 10:30")) }
62
+
63
+ it "formats date in long format" do
64
+ expect(formatted_date).to eq("March 15, 2024 10:30")
65
+ end
66
+ end
67
+ end
68
+ ```
69
+
70
+ ### Testing Conditional Logic
71
+
72
+ ```ruby
73
+ RSpec.describe PostDecorator do
74
+ subject(:decorator) { described_class.new(post) }
75
+
76
+ describe "#publication_status" do
77
+ subject(:status) { decorator.publication_status }
78
+
79
+ context "when published" do
80
+ let(:post) { build_stubbed(:post, :published) }
81
+
82
+ it "returns Published" do
83
+ expect(status).to eq("Published")
84
+ end
85
+ end
86
+
87
+ context "when draft" do
88
+ let(:post) { build_stubbed(:post, :draft) }
89
+
90
+ it "returns Draft" do
91
+ expect(status).to eq("Draft")
92
+ end
93
+ end
94
+ end
95
+ end
96
+ ```
97
+
98
+ ## Testing with Context
99
+
100
+ ```ruby
101
+ RSpec.describe ProductDecorator do
102
+ subject(:decorator) { described_class.new(product, context:) }
103
+
104
+ let(:product) { build_stubbed(:product, price: 100, cost: 60) }
105
+
106
+ describe "#price_display" do
107
+ subject(:price_display) { decorator.price_display }
108
+
109
+ context "without user context" do
110
+ let(:context) { {} }
111
+
112
+ it "shows standard price" do
113
+ expect(price_display).to eq("$100.00")
114
+ end
115
+ end
116
+
117
+ context "with admin user" do
118
+ let(:context) { { current_user: build_stubbed(:user, :admin) } }
119
+
120
+ it "shows price with margin" do
121
+ expect(price_display).to include("$100.00")
122
+ expect(price_display).to include("Margin")
123
+ end
124
+ end
125
+
126
+ context "with premium user" do
127
+ let(:context) { { current_user: build_stubbed(:user, :premium) } }
128
+
129
+ it "shows discounted price" do
130
+ expect(price_display).to include("discount")
131
+ end
132
+ end
133
+ end
134
+ end
135
+ ```
136
+
137
+ ## Testing HTML Output
138
+
139
+ ### With Capybara Matchers
140
+
141
+ ```ruby
142
+ RSpec.describe StatusDecorator do
143
+ subject(:decorator) { described_class.new(order) }
144
+
145
+ describe "#status_badge" do
146
+ subject(:badge) { decorator.status_badge }
147
+
148
+ context "when pending" do
149
+ let(:order) { build_stubbed(:order, status: "pending") }
150
+
151
+ it "renders warning badge" do
152
+ markup = Capybara.string(badge)
153
+
154
+ expect(markup).to have_css("span.badge.badge-warning", text: "Pending")
155
+ end
156
+ end
157
+
158
+ context "when completed" do
159
+ let(:order) { build_stubbed(:order, status: "completed") }
160
+
161
+ it "renders success badge" do
162
+ markup = Capybara.string(badge)
163
+
164
+ expect(markup).to have_css("span.badge.badge-success", text: "Completed")
165
+ end
166
+ end
167
+ end
168
+ end
169
+ ```
170
+
171
+ ### Testing Links
172
+
173
+ ```ruby
174
+ RSpec.describe PostDecorator do
175
+ subject(:decorator) { described_class.new(post) }
176
+
177
+ let(:post) { create(:post) }
178
+
179
+ describe "#edit_link" do
180
+ subject(:link) { decorator.edit_link }
181
+
182
+ it "generates edit link with correct path" do
183
+ markup = Capybara.string(link)
184
+
185
+ expect(markup).to have_link("Edit", href: "/posts/#{post.id}/edit")
186
+ end
187
+
188
+ it "includes button class" do
189
+ markup = Capybara.string(link)
190
+
191
+ expect(markup).to have_css("a.btn")
192
+ end
193
+ end
194
+ end
195
+ ```
196
+
197
+ ### Testing Complex HTML Structures
198
+
199
+ ```ruby
200
+ RSpec.describe UserDecorator do
201
+ subject(:decorator) { described_class.new(user) }
202
+
203
+ describe "#profile_card" do
204
+ subject(:card) { decorator.profile_card }
205
+
206
+ let(:user) { build_stubbed(:user, first_name: "Jane", email: "jane@example.com") }
207
+
208
+ it "renders card with user info" do
209
+ markup = Capybara.string(card)
210
+
211
+ expect(markup).to have_css(".profile-card") do |card|
212
+ expect(card).to have_css(".name", text: "Jane")
213
+ expect(card).to have_css(".email", text: "jane@example.com")
214
+ end
215
+ end
216
+ end
217
+ end
218
+ ```
219
+
220
+ ## Testing Associations
221
+
222
+ ```ruby
223
+ RSpec.describe PostDecorator do
224
+ subject(:decorator) { described_class.new(post) }
225
+
226
+ let(:post) { create(:post) }
227
+ let!(:comments) { create_list(:comment, 3, post:) }
228
+
229
+ describe "#comments" do
230
+ subject(:decorated_comments) { decorator.comments }
231
+
232
+ it "returns decorated comments" do
233
+ expect(decorated_comments).to all(be_decorated_with(CommentDecorator))
234
+ end
235
+
236
+ it "returns all comments" do
237
+ expect(decorated_comments.count).to eq(3)
238
+ end
239
+ end
240
+
241
+ describe "#author" do
242
+ subject(:decorated_author) { decorator.author }
243
+
244
+ let(:author) { create(:user) }
245
+ let(:post) { create(:post, author:) }
246
+
247
+ it "returns decorated author" do
248
+ expect(decorated_author).to be_decorated_with(UserDecorator)
249
+ end
250
+ end
251
+ end
252
+ ```
253
+
254
+ ## Testing Collection Decorators
255
+
256
+ ```ruby
257
+ RSpec.describe PaginatingDecorator do
258
+ subject(:collection) { described_class.new(products) }
259
+
260
+ let(:products) { Product.page(1).per(10) }
261
+
262
+ before { create_list(:product, 25) }
263
+
264
+ describe "pagination delegation" do
265
+ it "delegates current_page" do
266
+ expect(collection.current_page).to eq(1)
267
+ end
268
+
269
+ it "delegates total_pages" do
270
+ expect(collection.total_pages).to eq(3)
271
+ end
272
+
273
+ it "delegates total_count" do
274
+ expect(collection.total_count).to eq(25)
275
+ end
276
+ end
277
+
278
+ describe "items" do
279
+ it "decorates each item" do
280
+ expect(collection.first).to be_decorated
281
+ end
282
+ end
283
+ end
284
+ ```
285
+
286
+ ## Testing Helpers Access
287
+
288
+ ```ruby
289
+ RSpec.describe ProductDecorator do
290
+ subject(:decorator) { described_class.new(product) }
291
+
292
+ let(:product) { create(:product) }
293
+
294
+ describe "#show_path" do
295
+ it "uses path helper correctly" do
296
+ expect(decorator.show_path).to eq(helpers.product_path(product))
297
+ end
298
+ end
299
+
300
+ describe "#formatted_price" do
301
+ let(:product) { build_stubbed(:product, price: 1234.56) }
302
+
303
+ it "uses number helper" do
304
+ expect(decorator.formatted_price).to eq(helpers.number_to_currency(1234.56))
305
+ end
306
+ end
307
+ end
308
+ ```
309
+
310
+ ## Fast Test Strategy
311
+
312
+ For unit tests without Rails overhead:
313
+
314
+ ```ruby
315
+ # spec/fast_spec_helper.rb
316
+ require 'draper'
317
+ require 'active_model'
318
+
319
+ Draper::ViewContext.test_strategy :fast
320
+
321
+ # Or with specific helpers
322
+ Draper::ViewContext.test_strategy :fast do
323
+ include ActionView::Helpers::NumberHelper
324
+ include ActionView::Helpers::TextHelper
325
+ end
326
+ ```
327
+
328
+ ```ruby
329
+ # spec/decorators/fast/product_decorator_spec.rb
330
+ require 'fast_spec_helper'
331
+ require_relative '../../../app/decorators/product_decorator'
332
+
333
+ Product = Struct.new(:name, :price, keyword_init: true) do
334
+ extend ActiveModel::Naming
335
+ end
336
+
337
+ RSpec.describe ProductDecorator do
338
+ subject(:decorator) { described_class.new(product) }
339
+
340
+ let(:product) { Product.new(name: "Widget", price: 10.0) }
341
+
342
+ describe "#display_name" do
343
+ it "formats name" do
344
+ expect(decorator.display_name).to eq("WIDGET")
345
+ end
346
+ end
347
+
348
+ # Note: Path/URL helpers won't work in fast mode
349
+ end
350
+ ```
351
+
352
+ ## Shared Examples
353
+
354
+ ### Common Decorator Behaviors
355
+
356
+ ```ruby
357
+ # spec/support/shared_examples/decorators.rb
358
+ RSpec.shared_examples "a timestamped decorator" do
359
+ describe "#formatted_created_at" do
360
+ let(:model) { build_stubbed(factory, created_at: Time.zone.parse("2024-01-15")) }
361
+
362
+ it "formats created_at" do
363
+ expect(decorator.formatted_created_at).to eq("January 15, 2024")
364
+ end
365
+ end
366
+
367
+ describe "#time_ago" do
368
+ let(:model) { build_stubbed(factory, created_at: 2.hours.ago) }
369
+
370
+ it "shows relative time" do
371
+ expect(decorator.time_ago).to include("hours ago")
372
+ end
373
+ end
374
+ end
375
+
376
+ # Usage
377
+ RSpec.describe PostDecorator do
378
+ subject(:decorator) { described_class.new(model) }
379
+
380
+ let(:factory) { :post }
381
+ let(:model) { build_stubbed(factory) }
382
+
383
+ it_behaves_like "a timestamped decorator"
384
+ end
385
+ ```
386
+
387
+ ### Testing Decorated State
388
+
389
+ ```ruby
390
+ RSpec.shared_examples "a decorated object" do
391
+ it "is decorated" do
392
+ expect(decorated).to be_decorated
393
+ end
394
+
395
+ it "wraps the original object" do
396
+ expect(decorated.object).to eq(object)
397
+ end
398
+
399
+ it "delegates to original object" do
400
+ expect(decorated.id).to eq(object.id)
401
+ end
402
+ end
403
+ ```
404
+
405
+ ## Mocking and Stubbing
406
+
407
+ ### Stubbing Model Methods
408
+
409
+ ```ruby
410
+ RSpec.describe OrderDecorator do
411
+ subject(:decorator) { described_class.new(order) }
412
+
413
+ let(:order) { build_stubbed(:order) }
414
+
415
+ describe "#shipping_estimate" do
416
+ before do
417
+ allow(order).to receive(:calculate_shipping).and_return(15.0)
418
+ end
419
+
420
+ it "formats shipping cost" do
421
+ expect(decorator.shipping_estimate).to eq("$15.00")
422
+ end
423
+ end
424
+ end
425
+ ```
426
+
427
+ ### Stubbing External Services
428
+
429
+ ```ruby
430
+ RSpec.describe ProductDecorator do
431
+ subject(:decorator) { described_class.new(product) }
432
+
433
+ let(:product) { build_stubbed(:product) }
434
+
435
+ describe "#stock_status" do
436
+ context "when in stock" do
437
+ before do
438
+ allow(product).to receive(:check_inventory).and_return(available: true, count: 10)
439
+ end
440
+
441
+ it "shows available" do
442
+ expect(decorator.stock_status).to include("In Stock")
443
+ expect(decorator.stock_status).to include("10")
444
+ end
445
+ end
446
+ end
447
+ end
448
+ ```
449
+
450
+ ## Testing Draper Matchers
451
+
452
+ Draper provides RSpec matchers:
453
+
454
+ ```ruby
455
+ RSpec.describe "Decorator matchers" do
456
+ let(:post) { create(:post) }
457
+ let(:decorated) { post.decorate }
458
+
459
+ it "checks if decorated" do
460
+ expect(decorated).to be_decorated
461
+ expect(post).not_to be_decorated
462
+ end
463
+
464
+ it "checks decorator class" do
465
+ expect(decorated).to be_decorated_with(PostDecorator)
466
+ end
467
+ end
468
+ ```
469
+
470
+ ## Controller Specs with Decorators
471
+
472
+ ```ruby
473
+ RSpec.describe PostsController, type: :controller do
474
+ describe "GET #show" do
475
+ let(:post) { create(:post) }
476
+
477
+ before { get :show, params: { id: post.id } }
478
+
479
+ it "assigns decorated post" do
480
+ expect(assigns(:post)).to be_decorated
481
+ expect(assigns(:post)).to be_decorated_with(PostDecorator)
482
+ end
483
+ end
484
+
485
+ describe "GET #index" do
486
+ before do
487
+ create_list(:post, 3)
488
+ get :index
489
+ end
490
+
491
+ it "assigns decorated collection" do
492
+ assigns(:posts).each do |post|
493
+ expect(post).to be_decorated
494
+ end
495
+ end
496
+ end
497
+ end
498
+ ```
499
+
500
+ ## View Specs with Decorated Objects
501
+
502
+ ```ruby
503
+ RSpec.describe "posts/show", type: :view do
504
+ let(:post) { create(:post, title: "Test Post").decorate }
505
+
506
+ before do
507
+ assign(:post, post)
508
+ render
509
+ end
510
+
511
+ it "displays formatted title" do
512
+ expect(rendered).to include(post.formatted_title)
513
+ end
514
+
515
+ it "displays publication status" do
516
+ expect(rendered).to have_css(".status", text: post.publication_status)
517
+ end
518
+ end
519
+ ```
520
+
521
+ ## Debugging Tips
522
+
523
+ ### Inspecting Decorator State
524
+
525
+ ```ruby
526
+ RSpec.describe UserDecorator do
527
+ subject(:decorator) { described_class.new(user, context: { admin: true }) }
528
+
529
+ let(:user) { build_stubbed(:user) }
530
+
531
+ it "has correct context" do
532
+ expect(decorator.context).to eq(admin: true)
533
+ end
534
+
535
+ it "wraps correct object" do
536
+ expect(decorator.object).to eq(user)
537
+ expect(decorator.model).to eq(user) # alias
538
+ end
539
+
540
+ it "tracks applied decorators" do
541
+ expect(decorator.applied_decorators).to eq([UserDecorator])
542
+ end
543
+ end
544
+ ```
545
+
546
+ ### Checking View Context
547
+
548
+ ```ruby
549
+ RSpec.describe "view context in tests" do
550
+ it "provides helpers" do
551
+ expect(helpers).to respond_to(:link_to)
552
+ expect(helpers).to respond_to(:number_to_currency)
553
+ end
554
+
555
+ it "uses test controller" do
556
+ expect(Draper::ViewContext.current.controller).to be_a(ActionController::Base)
557
+ end
558
+ end
559
+ ```