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,381 @@
1
+ # RSpec Mocks Reference
2
+
3
+ Comprehensive reference for test doubles, stubbing, and message expectations.
4
+
5
+ ## Test Doubles
6
+
7
+ ### double (Basic)
8
+
9
+ Strict double - raises on unexpected messages.
10
+
11
+ ```ruby
12
+ book = double("book")
13
+ book = double("book", title: "The RSpec Book") # with stubs
14
+ book = double(foo: "bar", baz: "qux") # anonymous with stubs
15
+ ```
16
+
17
+ ### instance_double (Verifying)
18
+
19
+ Verifies against instance methods of the class.
20
+
21
+ ```ruby
22
+ notifier = instance_double("ConsoleNotifier")
23
+ expect(notifier).to receive(:notify).with("message")
24
+
25
+ book = instance_double("Book", pages: 250)
26
+ ```
27
+
28
+ Verification (when class loaded):
29
+ - Method exists on instance
30
+ - Argument arity matches
31
+ - Keyword arguments valid
32
+
33
+ ### class_double (Verifying)
34
+
35
+ Verifies against class methods.
36
+
37
+ ```ruby
38
+ notifier = class_double("ConsoleNotifier")
39
+ .as_stubbed_const(transfer_nested_constants: true)
40
+
41
+ expect(notifier).to receive(:notify).with("message")
42
+ ```
43
+
44
+ `as_stubbed_const` options:
45
+ - `transfer_nested_constants: true` - copies all nested constants
46
+ - `transfer_nested_constants: [:CONSTANT]` - selective transfer
47
+
48
+ ### object_double (Verifying)
49
+
50
+ Doubles an existing object instance.
51
+
52
+ ```ruby
53
+ user = object_double(User.new, save: true)
54
+ logger = object_double("MyApp::LOGGER", info: nil).as_stubbed_const
55
+ ```
56
+
57
+ Use for:
58
+ - Objects with side effects
59
+ - Methods via `method_missing`
60
+ - Singleton objects
61
+
62
+ ### spy
63
+
64
+ Null object double for after-the-fact verification.
65
+
66
+ ```ruby
67
+ invitation = spy("invitation")
68
+ invitation.deliver # doesn't raise
69
+ expect(invitation).to have_received(:deliver)
70
+
71
+ user_spy = instance_spy("User")
72
+ model_spy = class_spy("Model")
73
+ obj_spy = object_spy(my_object)
74
+ ```
75
+
76
+ ### Null Object Double
77
+
78
+ Returns itself for any message.
79
+
80
+ ```ruby
81
+ dbl = double("Collaborator").as_null_object
82
+ dbl.foo.bar.bazz # returns dbl
83
+ ```
84
+
85
+ ## Stubbing (allow)
86
+
87
+ ### Basic Stubs
88
+
89
+ ```ruby
90
+ allow(dbl).to receive(:foo) # returns nil
91
+ allow(dbl).to receive(:title).and_return("X") # returns "X"
92
+ allow(dbl).to receive(:title) { "X" } # block syntax
93
+ ```
94
+
95
+ ### Multiple Messages
96
+
97
+ ```ruby
98
+ allow(dbl).to receive_messages(
99
+ title: "The RSpec Book",
100
+ subtitle: "BDD with RSpec"
101
+ )
102
+ ```
103
+
104
+ ### Partial Doubles (Real Objects)
105
+
106
+ ```ruby
107
+ allow(string).to receive(:length).and_return(500)
108
+ allow(Person).to receive(:find).and_return(person_double)
109
+ ```
110
+
111
+ Enable verification:
112
+ ```ruby
113
+ RSpec.configure do |config|
114
+ config.mock_with :rspec do |mocks|
115
+ mocks.verify_partial_doubles = true
116
+ end
117
+ end
118
+ ```
119
+
120
+ ## Message Expectations (expect)
121
+
122
+ ### Basic Expectations
123
+
124
+ ```ruby
125
+ expect(dbl).to receive(:foo) # must be called
126
+ expect(dbl).not_to receive(:foo) # must NOT be called
127
+ expect(dbl).to receive(:foo), "message" # custom failure message
128
+ ```
129
+
130
+ ### have_received (Spy Pattern)
131
+
132
+ ```ruby
133
+ invitation = spy("invitation")
134
+ user.accept_invitation(invitation)
135
+ expect(invitation).to have_received(:accept)
136
+ expect(invitation).to have_received(:accept).with(mailer)
137
+ expect(invitation).to have_received(:deliver).twice
138
+ ```
139
+
140
+ ## Configuring Responses
141
+
142
+ ### Return Values
143
+
144
+ ```ruby
145
+ allow(dbl).to receive(:foo).and_return(14)
146
+
147
+ # Consecutive values
148
+ allow(die).to receive(:roll).and_return(1, 2, 3)
149
+ die.roll # => 1
150
+ die.roll # => 2
151
+ die.roll # => 3
152
+ die.roll # => 3 (repeats last)
153
+ ```
154
+
155
+ ### Raising Errors
156
+
157
+ ```ruby
158
+ allow(dbl).to receive(:foo).and_raise("boom")
159
+ allow(dbl).to receive(:foo).and_raise(StandardError)
160
+ allow(dbl).to receive(:foo).and_raise(ArgumentError, "invalid")
161
+ allow(dbl).to receive(:foo).and_raise(StandardError.new("error"))
162
+ ```
163
+
164
+ ### Yielding
165
+
166
+ ```ruby
167
+ allow(dbl).to receive(:foo).and_yield(2, 3)
168
+
169
+ # Multiple yields
170
+ allow(dbl).to receive(:foo)
171
+ .and_yield(1)
172
+ .and_yield(2)
173
+ .and_yield(3)
174
+ ```
175
+
176
+ ### Calling Original
177
+
178
+ ```ruby
179
+ expect(Calculator).to receive(:add).and_call_original
180
+ Calculator.add(2, 3) # => 5 (real method)
181
+ ```
182
+
183
+ ### Block Implementation
184
+
185
+ ```ruby
186
+ allow(dbl).to receive(:foo) do |arg|
187
+ expect(arg).to eq("bar")
188
+ end
189
+
190
+ allow(loan).to receive(:payment) do |rate|
191
+ loan.amount * rate
192
+ end
193
+
194
+ # Yield to caller's block
195
+ allow(dbl).to receive(:foo) { |&block| block.call(14) }
196
+ ```
197
+
198
+ ### Throwing
199
+
200
+ ```ruby
201
+ allow(dbl).to receive(:foo).and_throw(:halt)
202
+ ```
203
+
204
+ ## Argument Matchers
205
+
206
+ ### Built-in Matchers
207
+
208
+ | Matcher | Description |
209
+ |---------|-------------|
210
+ | `anything` | Any single argument |
211
+ | `any_args` | Any number of arguments |
212
+ | `no_args` | No arguments |
213
+ | `kind_of(Class)` | `arg.kind_of?(Class)` |
214
+ | `instance_of(Class)` | `arg.instance_of?(Class)` |
215
+ | `duck_type(:method)` | Responds to method(s) |
216
+ | `boolean` | `true` or `false` |
217
+ | `hash_including(key: val)` | Partial hash match |
218
+ | `hash_excluding(key: val)` | Hash without keys |
219
+ | `array_including(items)` | Array contains items |
220
+ | `array_excluding(items)` | Array without items |
221
+
222
+ ### Usage
223
+
224
+ ```ruby
225
+ expect(dbl).to receive(:foo).with(1, any_args)
226
+ expect(dbl).to receive(:msg).with(/abc/)
227
+ expect(dbl).to receive(:msg).with(hash_including(a: 1))
228
+ expect(dbl).to receive(:foo).with(a_collection_containing_exactly(1, 2))
229
+
230
+ # Custom
231
+ expect(dbl).to receive(:foo).with(satisfy { |x| x > 3 })
232
+ ```
233
+
234
+ ### Argument-Dependent Responses
235
+
236
+ ```ruby
237
+ allow(dbl).to receive(:foo).and_return(:default)
238
+ allow(dbl).to receive(:foo).with(1).and_return(1)
239
+ allow(dbl).to receive(:foo).with(2).and_return(2)
240
+
241
+ dbl.foo(0) # => :default
242
+ dbl.foo(1) # => 1
243
+ ```
244
+
245
+ ## Receive Counts
246
+
247
+ ```ruby
248
+ expect(dbl).to receive(:msg).once
249
+ expect(dbl).to receive(:msg).twice
250
+ expect(dbl).to receive(:msg).exactly(3).times
251
+ expect(dbl).to receive(:msg).at_least(:once)
252
+ expect(dbl).to receive(:msg).at_least(3).times
253
+ expect(dbl).to receive(:msg).at_most(:twice)
254
+ expect(dbl).to receive(:msg).at_most(3).times
255
+ ```
256
+
257
+ ## Message Order
258
+
259
+ ```ruby
260
+ expect(collaborator_1).to receive(:step_1).ordered
261
+ expect(collaborator_2).to receive(:step_2).ordered
262
+ expect(collaborator_1).to receive(:step_3).ordered
263
+ ```
264
+
265
+ ## any_instance
266
+
267
+ **Warning**: Stubs the object under test, making tests unreliable, and has confusing count semantics. Acceptable for legacy code where DI refactoring isn't feasible, or for testing ActiveRecord callbacks. Prefer stubbing `ClassName.new` to return a double instead:
268
+
269
+ ```ruby
270
+ # Prefer this pattern
271
+ user_double = instance_double(User, save: true)
272
+ allow(User).to receive(:new).and_return(user_double)
273
+ ```
274
+
275
+ ### Basic Usage
276
+
277
+ ```ruby
278
+ allow_any_instance_of(Widget).to receive(:name).and_return("Wibble")
279
+ expect_any_instance_of(Widget).to receive(:name).and_return("Wobble")
280
+
281
+ allow_any_instance_of(Object).to receive_messages(
282
+ foo: "foo",
283
+ bar: "bar"
284
+ )
285
+ ```
286
+
287
+ ### Block with Instance
288
+
289
+ ```ruby
290
+ allow_any_instance_of(String).to receive(:slice) do |instance, start, length|
291
+ instance[start, length]
292
+ end
293
+ ```
294
+
295
+ ## Constant Stubbing
296
+
297
+ ### stub_const
298
+
299
+ ```ruby
300
+ stub_const("MyClass", Class.new)
301
+ stub_const("SomeModel::PER_PAGE", 5)
302
+
303
+ # Transfer nested constants
304
+ stub_const("CardDeck", Class.new, transfer_nested_constants: true)
305
+ stub_const("CardDeck", Class.new, transfer_nested_constants: [:SUITS])
306
+ ```
307
+
308
+ ### hide_const
309
+
310
+ ```ruby
311
+ hide_const("MyClass") # Makes constant undefined
312
+ ```
313
+
314
+ ## Configuration
315
+
316
+ ```ruby
317
+ RSpec.configure do |config|
318
+ config.mock_with :rspec do |mocks|
319
+ mocks.verify_doubled_constant_names = true
320
+ mocks.verify_partial_doubles = true
321
+ mocks.allow_message_expectations_on_nil = false
322
+ end
323
+ end
324
+ ```
325
+
326
+ ## Patterns
327
+
328
+ ### Unit Test with Collaborator
329
+
330
+ ```ruby
331
+ RSpec.describe Account do
332
+ let(:logger) { instance_double("Logger") }
333
+ let(:account) { Account.new(logger) }
334
+
335
+ it "logs when closing" do
336
+ expect(logger).to receive(:info).with("Account closed")
337
+ account.close
338
+ end
339
+ end
340
+ ```
341
+
342
+ ### Spy Pattern (Arrange-Act-Assert)
343
+
344
+ ```ruby
345
+ RSpec.describe User do
346
+ it "sends invitation" do
347
+ invitation = spy(Invitation)
348
+
349
+ user = User.new
350
+ user.invite(invitation)
351
+
352
+ expect(invitation).to have_received(:deliver)
353
+ .with(user.email)
354
+ .once
355
+ end
356
+ end
357
+ ```
358
+
359
+ ### Stubbing External Service
360
+
361
+ ```ruby
362
+ RSpec.describe WeatherService do
363
+ it "fetches weather" do
364
+ http_client = instance_double(HTTPClient)
365
+ allow(http_client).to receive(:get)
366
+ .with("/weather", hash_including(zip: "12345"))
367
+ .and_return({ temp: 72 })
368
+
369
+ service = WeatherService.new(http_client)
370
+ expect(service.temperature("12345")).to eq(72)
371
+ end
372
+ end
373
+ ```
374
+
375
+ ### Conditional Stubs
376
+
377
+ ```ruby
378
+ allow(calculator).to receive(:compute).and_call_original
379
+ allow(calculator).to receive(:compute).with(0).and_return(0)
380
+ allow(calculator).to receive(:compute).with(-1).and_raise("Invalid")
381
+ ```