anima-core 0.2.1 → 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 (280) hide show
  1. checksums.yaml +4 -4
  2. data/.reek.yml +27 -1
  3. data/CHANGELOG.md +19 -0
  4. data/README.md +213 -43
  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 +195 -45
  12. data/app/decorators/user_message_decorator.rb +16 -5
  13. data/app/jobs/agent_request_job.rb +55 -2
  14. data/app/jobs/analytical_brain_job.rb +33 -0
  15. data/app/jobs/count_event_tokens_job.rb +15 -4
  16. data/app/models/concerns/event/broadcasting.rb +81 -0
  17. data/app/models/event.rb +20 -1
  18. data/app/models/goal.rb +91 -0
  19. data/app/models/session.rb +366 -21
  20. data/config/application.rb +2 -0
  21. data/config/initializers/event_subscribers.rb +0 -1
  22. data/config/routes.rb +0 -6
  23. data/db/migrate/20260313010000_add_status_to_events.rb +8 -0
  24. data/db/migrate/20260313020000_add_processing_to_sessions.rb +7 -0
  25. data/db/migrate/20260314075248_add_subagent_support_to_sessions.rb +6 -0
  26. data/db/migrate/20260314112417_add_granted_tools_to_sessions.rb +5 -0
  27. data/db/migrate/20260314140000_add_name_to_sessions.rb +7 -0
  28. data/db/migrate/20260314150000_add_viewport_event_ids_to_sessions.rb +7 -0
  29. data/db/migrate/20260315100000_add_active_skills_to_sessions.rb +7 -0
  30. data/db/migrate/20260315140843_create_goals.rb +16 -0
  31. data/db/migrate/20260315144837_add_completed_at_to_goals.rb +5 -0
  32. data/db/migrate/20260315191105_add_active_workflow_to_sessions.rb +5 -0
  33. data/lib/agent_loop.rb +65 -6
  34. data/lib/agents/definition.rb +116 -0
  35. data/lib/agents/registry.rb +106 -0
  36. data/lib/analytical_brain/runner.rb +276 -0
  37. data/lib/analytical_brain/tools/activate_skill.rb +52 -0
  38. data/lib/analytical_brain/tools/deactivate_skill.rb +43 -0
  39. data/lib/analytical_brain/tools/deactivate_workflow.rb +34 -0
  40. data/lib/analytical_brain/tools/everything_is_ready.rb +28 -0
  41. data/lib/analytical_brain/tools/finish_goal.rb +62 -0
  42. data/lib/analytical_brain/tools/read_workflow.rb +58 -0
  43. data/lib/analytical_brain/tools/rename_session.rb +63 -0
  44. data/lib/analytical_brain/tools/set_goal.rb +60 -0
  45. data/lib/analytical_brain/tools/update_goal.rb +60 -0
  46. data/lib/analytical_brain.rb +23 -0
  47. data/lib/anima/cli/mcp/secrets.rb +76 -0
  48. data/lib/anima/cli/mcp.rb +197 -0
  49. data/lib/anima/cli.rb +5 -40
  50. data/lib/anima/installer.rb +168 -0
  51. data/lib/anima/settings.rb +226 -0
  52. data/lib/anima/version.rb +1 -1
  53. data/lib/anima.rb +9 -0
  54. data/lib/credential_store.rb +103 -0
  55. data/lib/environment_probe.rb +232 -0
  56. data/lib/events/subscribers/persister.rb +1 -0
  57. data/lib/events/user_message.rb +17 -0
  58. data/lib/llm/client.rb +29 -10
  59. data/lib/mcp/client_manager.rb +86 -0
  60. data/lib/mcp/config.rb +213 -0
  61. data/lib/mcp/health_check.rb +77 -0
  62. data/lib/mcp/secrets.rb +73 -0
  63. data/lib/mcp/stdio_transport.rb +206 -0
  64. data/lib/providers/anthropic.rb +11 -20
  65. data/lib/shell_session.rb +11 -10
  66. data/lib/skills/definition.rb +97 -0
  67. data/lib/skills/registry.rb +105 -0
  68. data/lib/tools/edit.rb +226 -0
  69. data/lib/tools/mcp_tool.rb +114 -0
  70. data/lib/tools/read.rb +151 -0
  71. data/lib/tools/registry.rb +14 -12
  72. data/lib/tools/request_feature.rb +121 -0
  73. data/lib/tools/return_result.rb +81 -0
  74. data/lib/tools/spawn_specialist.rb +109 -0
  75. data/lib/tools/spawn_subagent.rb +111 -0
  76. data/lib/tools/subagent_prompts.rb +12 -0
  77. data/lib/tools/web_get.rb +8 -9
  78. data/lib/tools/write.rb +86 -0
  79. data/lib/tui/app.rb +985 -26
  80. data/lib/tui/cable_client.rb +69 -31
  81. data/lib/tui/message_store.rb +103 -8
  82. data/lib/tui/screens/chat.rb +293 -45
  83. data/lib/workflows/definition.rb +97 -0
  84. data/lib/workflows/registry.rb +89 -0
  85. data/skills/activerecord/SKILL.md +255 -0
  86. data/skills/activerecord/examples/associations/association_extensions.rb +298 -0
  87. data/skills/activerecord/examples/associations/basic_associations.rb +118 -0
  88. data/skills/activerecord/examples/associations/counter_caches.rb +215 -0
  89. data/skills/activerecord/examples/associations/polymorphic_associations.rb +217 -0
  90. data/skills/activerecord/examples/associations/self_referential.rb +302 -0
  91. data/skills/activerecord/examples/associations/through_associations.rb +203 -0
  92. data/skills/activerecord/examples/basics/crud_operations.rb +209 -0
  93. data/skills/activerecord/examples/basics/dirty_tracking.rb +218 -0
  94. data/skills/activerecord/examples/basics/inheritance.rb +377 -0
  95. data/skills/activerecord/examples/basics/type_casting.rb +317 -0
  96. data/skills/activerecord/examples/callbacks/alternatives_to_callbacks.rb +447 -0
  97. data/skills/activerecord/examples/callbacks/conditional_callbacks.rb +353 -0
  98. data/skills/activerecord/examples/callbacks/lifecycle_callbacks.rb +280 -0
  99. data/skills/activerecord/examples/callbacks/transaction_callbacks.rb +340 -0
  100. data/skills/activerecord/examples/migrations/indexes_and_constraints.rb +337 -0
  101. data/skills/activerecord/examples/migrations/reversible_patterns.rb +403 -0
  102. data/skills/activerecord/examples/migrations/safe_patterns.rb +420 -0
  103. data/skills/activerecord/examples/migrations/schema_changes.rb +277 -0
  104. data/skills/activerecord/examples/querying/batch_processing.rb +226 -0
  105. data/skills/activerecord/examples/querying/eager_loading.rb +259 -0
  106. data/skills/activerecord/examples/querying/finder_methods.rb +170 -0
  107. data/skills/activerecord/examples/querying/optimization.rb +275 -0
  108. data/skills/activerecord/examples/querying/scopes.rb +260 -0
  109. data/skills/activerecord/examples/validations/built_in_validators.rb +277 -0
  110. data/skills/activerecord/examples/validations/conditional_validations.rb +288 -0
  111. data/skills/activerecord/examples/validations/custom_validators.rb +381 -0
  112. data/skills/activerecord/examples/validations/database_constraints.rb +432 -0
  113. data/skills/activerecord/examples/validations/validation_contexts.rb +367 -0
  114. data/skills/activerecord/references/associations.md +709 -0
  115. data/skills/activerecord/references/basics.md +622 -0
  116. data/skills/activerecord/references/callbacks.md +738 -0
  117. data/skills/activerecord/references/migrations.md +657 -0
  118. data/skills/activerecord/references/querying.md +655 -0
  119. data/skills/activerecord/references/validations.md +596 -0
  120. data/skills/dragonruby/SKILL.md +250 -0
  121. data/skills/dragonruby/examples/audio/audio_events.rb +55 -0
  122. data/skills/dragonruby/examples/audio/background_music.rb +29 -0
  123. data/skills/dragonruby/examples/audio/crossfade.rb +51 -0
  124. data/skills/dragonruby/examples/audio/music_controls.rb +51 -0
  125. data/skills/dragonruby/examples/audio/sound_effects.rb +30 -0
  126. data/skills/dragonruby/examples/core/coordinate_system.rb +27 -0
  127. data/skills/dragonruby/examples/core/hello_world.rb +24 -0
  128. data/skills/dragonruby/examples/core/labels.rb +22 -0
  129. data/skills/dragonruby/examples/core/sprites.rb +35 -0
  130. data/skills/dragonruby/examples/core/state_management.rb +29 -0
  131. data/skills/dragonruby/examples/distribution/background_pause.rb +42 -0
  132. data/skills/dragonruby/examples/distribution/build_workflow.sh +26 -0
  133. data/skills/dragonruby/examples/distribution/cvars_production.txt +16 -0
  134. data/skills/dragonruby/examples/distribution/game_metadata_hd.txt +23 -0
  135. data/skills/dragonruby/examples/distribution/game_metadata_minimal.txt +9 -0
  136. data/skills/dragonruby/examples/distribution/game_metadata_mobile.txt +31 -0
  137. data/skills/dragonruby/examples/distribution/platform_detection.rb +36 -0
  138. data/skills/dragonruby/examples/distribution/steam_metadata.txt +19 -0
  139. data/skills/dragonruby/examples/entities/collision_detection.rb +43 -0
  140. data/skills/dragonruby/examples/entities/entity_lifecycle.rb +68 -0
  141. data/skills/dragonruby/examples/entities/entity_storage.rb +38 -0
  142. data/skills/dragonruby/examples/entities/factory_methods.rb +45 -0
  143. data/skills/dragonruby/examples/entities/random_spawning.rb +50 -0
  144. data/skills/dragonruby/examples/game-logic/reset_patterns.rb +98 -0
  145. data/skills/dragonruby/examples/game-logic/save_load.rb +101 -0
  146. data/skills/dragonruby/examples/game-logic/scoring.rb +104 -0
  147. data/skills/dragonruby/examples/game-logic/state_transitions.rb +103 -0
  148. data/skills/dragonruby/examples/game-logic/timers.rb +87 -0
  149. data/skills/dragonruby/examples/input/action_triggers.rb +36 -0
  150. data/skills/dragonruby/examples/input/analog_movement.rb +28 -0
  151. data/skills/dragonruby/examples/input/controller_input.rb +28 -0
  152. data/skills/dragonruby/examples/input/directional_input.rb +24 -0
  153. data/skills/dragonruby/examples/input/keyboard_input.rb +28 -0
  154. data/skills/dragonruby/examples/input/mouse_click.rb +26 -0
  155. data/skills/dragonruby/examples/input/movement_with_bounds.rb +22 -0
  156. data/skills/dragonruby/examples/input/normalized_movement.rb +32 -0
  157. data/skills/dragonruby/examples/rendering/frame_animation.rb +32 -0
  158. data/skills/dragonruby/examples/rendering/labels.rb +32 -0
  159. data/skills/dragonruby/examples/rendering/layering.rb +51 -0
  160. data/skills/dragonruby/examples/rendering/solids.rb +61 -0
  161. data/skills/dragonruby/examples/rendering/sprites.rb +33 -0
  162. data/skills/dragonruby/examples/rendering/spritesheet_animation.rb +39 -0
  163. data/skills/dragonruby/examples/scenes/case_dispatch.rb +60 -0
  164. data/skills/dragonruby/examples/scenes/class_based.rb +150 -0
  165. data/skills/dragonruby/examples/scenes/pause_overlay.rb +100 -0
  166. data/skills/dragonruby/examples/scenes/safe_transitions.rb +68 -0
  167. data/skills/dragonruby/examples/scenes/scene_transitions.rb +98 -0
  168. data/skills/dragonruby/examples/scenes/send_dispatch.rb +88 -0
  169. data/skills/dragonruby/references/audio.md +396 -0
  170. data/skills/dragonruby/references/core.md +385 -0
  171. data/skills/dragonruby/references/distribution.md +434 -0
  172. data/skills/dragonruby/references/entities.md +516 -0
  173. data/skills/dragonruby/references/game-logic/persistence.md +386 -0
  174. data/skills/dragonruby/references/game-logic/state.md +389 -0
  175. data/skills/dragonruby/references/input.md +414 -0
  176. data/skills/dragonruby/references/rendering/animation.md +467 -0
  177. data/skills/dragonruby/references/rendering/primitives.md +403 -0
  178. data/skills/dragonruby/references/scenes.md +443 -0
  179. data/skills/draper-decorators/SKILL.md +344 -0
  180. data/skills/draper-decorators/examples/application_decorator.rb +61 -0
  181. data/skills/draper-decorators/examples/decorator_spec.rb +253 -0
  182. data/skills/draper-decorators/examples/model_decorator.rb +152 -0
  183. data/skills/draper-decorators/references/anti-patterns.md +640 -0
  184. data/skills/draper-decorators/references/patterns.md +507 -0
  185. data/skills/draper-decorators/references/testing.md +559 -0
  186. data/skills/gh-issue.md +182 -0
  187. data/skills/mcp-server/SKILL.md +177 -0
  188. data/skills/mcp-server/examples/dynamic_tools.rb +36 -0
  189. data/skills/mcp-server/examples/file_manager_tool.rb +85 -0
  190. data/skills/mcp-server/examples/http_client.rb +48 -0
  191. data/skills/mcp-server/examples/http_server.rb +97 -0
  192. data/skills/mcp-server/examples/rails_integration.rb +88 -0
  193. data/skills/mcp-server/examples/stdio_server.rb +108 -0
  194. data/skills/mcp-server/examples/streaming_client.rb +95 -0
  195. data/skills/mcp-server/references/gotchas.md +183 -0
  196. data/skills/mcp-server/references/prompts.md +98 -0
  197. data/skills/mcp-server/references/resources.md +53 -0
  198. data/skills/mcp-server/references/server.md +140 -0
  199. data/skills/mcp-server/references/tools.md +146 -0
  200. data/skills/mcp-server/references/transport.md +104 -0
  201. data/skills/ratatui-ruby/SKILL.md +315 -0
  202. data/skills/ratatui-ruby/references/core-concepts.md +340 -0
  203. data/skills/ratatui-ruby/references/events.md +387 -0
  204. data/skills/ratatui-ruby/references/frameworks.md +522 -0
  205. data/skills/ratatui-ruby/references/layout.md +423 -0
  206. data/skills/ratatui-ruby/references/styling.md +268 -0
  207. data/skills/ratatui-ruby/references/testing.md +433 -0
  208. data/skills/ratatui-ruby/references/widgets.md +532 -0
  209. data/skills/rspec/SKILL.md +340 -0
  210. data/skills/rspec/examples/core/basic_structure.rb +69 -0
  211. data/skills/rspec/examples/core/configuration.rb +126 -0
  212. data/skills/rspec/examples/core/hooks.rb +126 -0
  213. data/skills/rspec/examples/core/memoized_helpers.rb +139 -0
  214. data/skills/rspec/examples/core/metadata_filtering.rb +144 -0
  215. data/skills/rspec/examples/core/shared_examples.rb +145 -0
  216. data/skills/rspec/examples/factory_bot/associations.rb +314 -0
  217. data/skills/rspec/examples/factory_bot/build_strategies.rb +272 -0
  218. data/skills/rspec/examples/factory_bot/callbacks.rb +320 -0
  219. data/skills/rspec/examples/factory_bot/custom_construction.rb +328 -0
  220. data/skills/rspec/examples/factory_bot/factory_definition.rb +191 -0
  221. data/skills/rspec/examples/factory_bot/inheritance.rb +314 -0
  222. data/skills/rspec/examples/factory_bot/traits.rb +293 -0
  223. data/skills/rspec/examples/factory_bot/transients.rb +229 -0
  224. data/skills/rspec/examples/matchers/change.rb +115 -0
  225. data/skills/rspec/examples/matchers/collections.rb +154 -0
  226. data/skills/rspec/examples/matchers/comparisons.rb +79 -0
  227. data/skills/rspec/examples/matchers/composing.rb +155 -0
  228. data/skills/rspec/examples/matchers/custom_matchers.rb +197 -0
  229. data/skills/rspec/examples/matchers/equality.rb +58 -0
  230. data/skills/rspec/examples/matchers/errors.rb +136 -0
  231. data/skills/rspec/examples/matchers/output.rb +103 -0
  232. data/skills/rspec/examples/matchers/predicates.rb +87 -0
  233. data/skills/rspec/examples/matchers/truthiness.rb +101 -0
  234. data/skills/rspec/examples/matchers/types.rb +82 -0
  235. data/skills/rspec/examples/matchers/yield.rb +147 -0
  236. data/skills/rspec/examples/mocks/any_instance.rb +172 -0
  237. data/skills/rspec/examples/mocks/argument_matchers.rb +206 -0
  238. data/skills/rspec/examples/mocks/constants.rb +177 -0
  239. data/skills/rspec/examples/mocks/doubles.rb +139 -0
  240. data/skills/rspec/examples/mocks/expectations.rb +137 -0
  241. data/skills/rspec/examples/mocks/message_chains.rb +173 -0
  242. data/skills/rspec/examples/mocks/ordering.rb +144 -0
  243. data/skills/rspec/examples/mocks/receive_counts.rb +181 -0
  244. data/skills/rspec/examples/mocks/responses.rb +223 -0
  245. data/skills/rspec/examples/mocks/spies.rb +149 -0
  246. data/skills/rspec/examples/mocks/stubbing.rb +133 -0
  247. data/skills/rspec/examples/rails/channels.rb +250 -0
  248. data/skills/rspec/examples/rails/controller_specs.rb +302 -0
  249. data/skills/rspec/examples/rails/helper_specs.rb +245 -0
  250. data/skills/rspec/examples/rails/job_specs.rb +256 -0
  251. data/skills/rspec/examples/rails/mailer_specs.rb +228 -0
  252. data/skills/rspec/examples/rails/matchers.rb +374 -0
  253. data/skills/rspec/examples/rails/model_specs.rb +193 -0
  254. data/skills/rspec/examples/rails/request_specs.rb +275 -0
  255. data/skills/rspec/examples/rails/routing_specs.rb +276 -0
  256. data/skills/rspec/examples/rails/system_specs.rb +294 -0
  257. data/skills/rspec/examples/rails/transactions.rb +254 -0
  258. data/skills/rspec/examples/rails/view_specs.rb +252 -0
  259. data/skills/rspec/references/core.md +816 -0
  260. data/skills/rspec/references/factory_bot.md +641 -0
  261. data/skills/rspec/references/matchers.md +516 -0
  262. data/skills/rspec/references/mocks.md +381 -0
  263. data/skills/rspec/references/rails.md +528 -0
  264. data/templates/soul.md +40 -0
  265. data/workflows/commit.md +45 -0
  266. data/workflows/create_handoff.md +98 -0
  267. data/workflows/create_note.md +82 -0
  268. data/workflows/create_plan.md +457 -0
  269. data/workflows/decompose_ticket.md +109 -0
  270. data/workflows/feature.md +91 -0
  271. data/workflows/implement_plan.md +87 -0
  272. data/workflows/iterate_plan.md +247 -0
  273. data/workflows/research_codebase.md +210 -0
  274. data/workflows/resume_handoff.md +217 -0
  275. data/workflows/review_pr.md +320 -0
  276. data/workflows/thoughts_init.md +71 -0
  277. data/workflows/validate_plan.md +166 -0
  278. metadata +290 -3
  279. data/app/controllers/api/sessions_controller.rb +0 -25
  280. data/lib/events/subscribers/action_cable_bridge.rb +0 -59
@@ -0,0 +1,223 @@
1
+ # RSpec Mocks: Configuring Responses Examples
2
+ # Source: rspec-mocks gem features/configuring_responses/*.feature
3
+
4
+ # and_return - return specified values
5
+ RSpec.describe "and_return" do
6
+ describe "single value" do
7
+ it "returns the value on every call" do
8
+ dbl = double("collaborator")
9
+ allow(dbl).to receive(:foo).and_return(14)
10
+
11
+ expect(dbl.foo).to eq(14)
12
+ expect(dbl.foo).to eq(14)
13
+ end
14
+ end
15
+
16
+ describe "consecutive values" do
17
+ it "returns values in sequence" do
18
+ die = double("die")
19
+ allow(die).to receive(:roll).and_return(1, 2, 3)
20
+
21
+ expect(die.roll).to eq(1)
22
+ expect(die.roll).to eq(2)
23
+ expect(die.roll).to eq(3)
24
+ expect(die.roll).to eq(3) # Repeats last value
25
+ end
26
+ end
27
+ end
28
+
29
+ # and_raise - raise exceptions
30
+ RSpec.describe "and_raise" do
31
+ describe "various forms" do
32
+ it "raises with string message" do
33
+ dbl = double("collaborator")
34
+ allow(dbl).to receive(:foo).and_raise("boom")
35
+
36
+ expect { dbl.foo }.to raise_error("boom")
37
+ end
38
+
39
+ it "raises specified error class" do
40
+ dbl = double("collaborator")
41
+ allow(dbl).to receive(:foo).and_raise(ArgumentError)
42
+
43
+ expect { dbl.foo }.to raise_error(ArgumentError)
44
+ end
45
+
46
+ it "raises error class with message" do
47
+ dbl = double("collaborator")
48
+ allow(dbl).to receive(:foo).and_raise(ArgumentError, "invalid")
49
+
50
+ expect { dbl.foo }.to raise_error(ArgumentError, "invalid")
51
+ end
52
+
53
+ it "raises error instance" do
54
+ dbl = double("collaborator")
55
+ error = StandardError.new("specific error")
56
+ allow(dbl).to receive(:foo).and_raise(error)
57
+
58
+ expect { dbl.foo }.to raise_error(error)
59
+ end
60
+ end
61
+ end
62
+
63
+ # and_yield - yield to blocks
64
+ RSpec.describe "and_yield" do
65
+ describe "yielding arguments" do
66
+ it "yields specified values to block" do
67
+ dbl = double("collaborator")
68
+ allow(dbl).to receive(:foo).and_yield(2, 3)
69
+
70
+ x = y = nil
71
+ dbl.foo { |a, b| x, y = a, b }
72
+
73
+ expect(x).to eq(2)
74
+ expect(y).to eq(3)
75
+ end
76
+ end
77
+
78
+ describe "multiple yields" do
79
+ it "yields multiple times" do
80
+ dbl = double("collaborator")
81
+ allow(dbl).to receive(:each)
82
+ .and_yield(1)
83
+ .and_yield(2)
84
+ .and_yield(3)
85
+
86
+ yielded = []
87
+ dbl.each { |x| yielded << x }
88
+
89
+ expect(yielded).to eq([1, 2, 3])
90
+ end
91
+ end
92
+ end
93
+
94
+ # and_call_original - call real implementation
95
+ RSpec.describe "and_call_original" do
96
+ describe "partial double with original" do
97
+ it "calls through to real method" do
98
+ expect(Calculator).to receive(:add).and_call_original
99
+ expect(Calculator.add(2, 3)).to eq(5)
100
+ end
101
+
102
+ it "combines with specific stubs" do
103
+ allow(Calculator).to receive(:add).and_call_original
104
+ allow(Calculator).to receive(:add).with(2, 3).and_return(-5)
105
+
106
+ expect(Calculator.add(2, 2)).to eq(4) # Calls original
107
+ expect(Calculator.add(2, 3)).to eq(-5) # Uses stub
108
+ end
109
+ end
110
+ end
111
+
112
+ # Block implementation - dynamic behavior
113
+ RSpec.describe "block implementation" do
114
+ describe "simple block" do
115
+ it "returns block result" do
116
+ dbl = double("collaborator")
117
+ allow(dbl).to receive(:foo) { 14 }
118
+
119
+ expect(dbl.foo).to eq(14)
120
+ end
121
+ end
122
+
123
+ describe "block with arguments" do
124
+ it "receives call arguments" do
125
+ dbl = double("collaborator")
126
+ allow(dbl).to receive(:greet) { |name| "Hello, #{name}!" }
127
+
128
+ expect(dbl.greet("Alice")).to eq("Hello, Alice!")
129
+ end
130
+ end
131
+
132
+ describe "calculations" do
133
+ it "performs dynamic calculations" do
134
+ loan = double("loan", amount: 1000)
135
+ allow(loan).to receive(:payment_for_rate) { |rate| loan.amount * rate }
136
+
137
+ expect(loan.payment_for_rate(0.05)).to eq(50)
138
+ expect(loan.payment_for_rate(0.10)).to eq(100)
139
+ end
140
+ end
141
+
142
+ describe "yielding to caller's block" do
143
+ it "invokes caller's block from implementation" do
144
+ dbl = double("collaborator")
145
+ allow(dbl).to receive(:process) { |&block| block.call(14) }
146
+
147
+ result = nil
148
+ dbl.process { |x| result = x * 2 }
149
+
150
+ expect(result).to eq(28)
151
+ end
152
+ end
153
+
154
+ describe "simulating failures" do
155
+ it "simulates transient errors" do
156
+ client = double("client")
157
+ call_count = 0
158
+
159
+ allow(client).to receive(:fetch_data) do
160
+ call_count += 1
161
+ call_count.odd? ? raise("timeout") : { count: 15 }
162
+ end
163
+
164
+ expect { client.fetch_data }.to raise_error("timeout")
165
+ expect(client.fetch_data).to eq(count: 15)
166
+ expect { client.fetch_data }.to raise_error("timeout")
167
+ end
168
+ end
169
+ end
170
+
171
+ # and_throw - throw symbols
172
+ RSpec.describe "and_throw" do
173
+ it "throws specified symbol" do
174
+ dbl = double("collaborator")
175
+ allow(dbl).to receive(:halt).and_throw(:abort)
176
+
177
+ expect { dbl.halt }.to throw_symbol(:abort)
178
+ end
179
+
180
+ it "throws symbol with value" do
181
+ dbl = double("collaborator")
182
+ allow(dbl).to receive(:halt).and_throw(:result, 42)
183
+
184
+ expect { dbl.halt }.to throw_symbol(:result, 42)
185
+ end
186
+ end
187
+
188
+ # and_invoke - mixed responses per call
189
+ RSpec.describe "and_invoke" do
190
+ it "executes different lambdas per call" do
191
+ dbl = double("collaborator")
192
+ allow(dbl).to receive(:foo).and_invoke(
193
+ -> { raise "first call fails" },
194
+ -> { "second call succeeds" }
195
+ )
196
+
197
+ expect { dbl.foo }.to raise_error("first call fails")
198
+ expect(dbl.foo).to eq("second call succeeds")
199
+ end
200
+ end
201
+
202
+ # Practical example: API client with retry
203
+ RSpec.describe ApiClient do
204
+ subject(:client) { build(:api_client, http:) }
205
+
206
+ let(:http) { instance_double("Net::HTTP") }
207
+
208
+ describe "#fetch_with_retry" do
209
+ context "when first request times out" do
210
+ before do
211
+ allow(http).to receive(:get).and_invoke(
212
+ -> { raise Timeout::Error },
213
+ -> { { data: "success" } }
214
+ )
215
+ end
216
+
217
+ it "retries and succeeds" do
218
+ expect(client.fetch_with_retry("/api")).to eq(data: "success")
219
+ end
220
+ end
221
+ end
222
+ end
223
+
@@ -0,0 +1,149 @@
1
+ # RSpec Mocks: Spies Examples
2
+ # Source: rspec-mocks gem features/basics/spies.feature,
3
+ # null_object_doubles.feature
4
+
5
+ # spy - null object double for arrange-act-assert pattern
6
+ RSpec.describe "spy" do
7
+ describe "basic usage" do
8
+ it "allows any message without raising" do
9
+ invitation = spy("invitation")
10
+ invitation.deliver # doesn't raise
11
+ invitation.foo.bar.baz # also doesn't raise
12
+ end
13
+
14
+ it "verifies messages after the fact" do
15
+ invitation = spy("invitation")
16
+ invitation.deliver("user@example.com")
17
+
18
+ expect(invitation).to have_received(:deliver).with("user@example.com")
19
+ end
20
+ end
21
+
22
+ describe "verifying doubles as spies" do
23
+ it "can create instance spy" do
24
+ user = instance_spy("User")
25
+ user.save
26
+ expect(user).to have_received(:save)
27
+ end
28
+
29
+ it "can create class spy" do
30
+ notifier = class_spy("UserMailer")
31
+ notifier.send_welcome("email@example.com")
32
+ expect(notifier).to have_received(:send_welcome)
33
+ end
34
+
35
+ it "can create object spy" do
36
+ real_user = User.new
37
+ user = object_spy(real_user)
38
+ user.save
39
+ expect(user).to have_received(:save)
40
+ end
41
+ end
42
+
43
+ describe "spy constraints" do
44
+ it "verifies call count" do
45
+ invitation = spy("invitation")
46
+ invitation.deliver
47
+ invitation.deliver
48
+
49
+ expect(invitation).to have_received(:deliver).twice
50
+ end
51
+
52
+ it "verifies message order" do
53
+ invitation = spy("invitation")
54
+ invitation.prepare
55
+ invitation.deliver
56
+
57
+ expect(invitation).to have_received(:prepare).ordered
58
+ expect(invitation).to have_received(:deliver).ordered
59
+ end
60
+
61
+ it "verifies arguments" do
62
+ invitation = spy("invitation")
63
+ invitation.deliver("foo@example.com")
64
+ invitation.deliver("bar@example.com")
65
+
66
+ expect(invitation).to have_received(:deliver).with("foo@example.com")
67
+ expect(invitation).to have_received(:deliver).with("bar@example.com")
68
+ end
69
+ end
70
+ end
71
+
72
+ # Practical example: arrange-act-assert with spies
73
+ RSpec.describe InvitationService do
74
+ subject(:service) { build(:invitation_service, mailer:) }
75
+
76
+ let(:mailer) { spy("mailer") }
77
+ let(:user) { build(:user, email: "test@example.com") }
78
+
79
+ describe "#send_invitation" do
80
+ it "sends email to the user" do
81
+ # Arrange (done in let blocks)
82
+
83
+ # Act
84
+ service.send_invitation(user)
85
+
86
+ # Assert
87
+ expect(mailer).to have_received(:deliver).with(user.email)
88
+ end
89
+
90
+ it "sends invitation with correct template" do
91
+ service.send_invitation(user)
92
+
93
+ expect(mailer).to have_received(:deliver)
94
+ .with(user.email)
95
+ .once
96
+ end
97
+ end
98
+ end
99
+
100
+ # null object double
101
+ RSpec.describe "null object double" do
102
+ describe "as_null_object" do
103
+ it "returns itself for any message" do
104
+ dbl = double("collaborator").as_null_object
105
+ expect(dbl.foo.bar.baz).to be(dbl)
106
+ end
107
+
108
+ it "can have specific stubs" do
109
+ dbl = double("collaborator", foo: 3).as_null_object
110
+ allow(dbl).to receive(:bar).and_return(4)
111
+
112
+ expect(dbl.foo).to eq(3)
113
+ expect(dbl.bar).to eq(4)
114
+ expect(dbl.undefined_method).to be(dbl)
115
+ end
116
+ end
117
+ end
118
+
119
+ # Practical use: chained methods without full stubbing
120
+ RSpec.describe QueryBuilder do
121
+ subject(:builder) { build(:query_builder) }
122
+
123
+ let(:query) { instance_spy("ActiveRecord::Relation").as_null_object }
124
+
125
+ describe "#build" do
126
+ before { allow(User).to receive(:where).and_return(query) }
127
+
128
+ it "chains query methods" do
129
+ builder.build(status: :active, role: :admin)
130
+
131
+ expect(query).to have_received(:where).with(status: :active)
132
+ expect(query).to have_received(:where).with(role: :admin)
133
+ end
134
+ end
135
+ end
136
+
137
+ # Partial doubles with spies
138
+ RSpec.describe "partial double spy pattern" do
139
+ describe "spying on real objects" do
140
+ it "allows spying on class methods" do
141
+ allow(Invitation).to receive(:deliver)
142
+
143
+ Invitation.deliver("test@example.com")
144
+
145
+ expect(Invitation).to have_received(:deliver).with("test@example.com")
146
+ end
147
+ end
148
+ end
149
+
@@ -0,0 +1,133 @@
1
+ # RSpec Mocks: Stubbing (allow) Examples
2
+ # Source: rspec-mocks gem features/basics/allowing_messages.feature,
3
+ # partial_test_doubles.feature
4
+
5
+ # Basic allow syntax
6
+ RSpec.describe "allow (stubbing)" do
7
+ describe "basic stubs" do
8
+ it "returns nil by default" do
9
+ dbl = double("collaborator")
10
+ allow(dbl).to receive(:foo)
11
+
12
+ expect(dbl.foo).to be_nil
13
+ end
14
+
15
+ it "returns specified value with and_return" do
16
+ dbl = double("collaborator")
17
+ allow(dbl).to receive(:foo).and_return("bar")
18
+
19
+ expect(dbl.foo).to eq("bar")
20
+ end
21
+
22
+ it "returns value with block syntax" do
23
+ dbl = double("collaborator")
24
+ allow(dbl).to receive(:foo) { "bar" }
25
+
26
+ expect(dbl.foo).to eq("bar")
27
+ end
28
+ end
29
+
30
+ describe "multiple stubs" do
31
+ it "stubs multiple methods with receive_messages" do
32
+ dbl = double("collaborator")
33
+ allow(dbl).to receive_messages(
34
+ title: "The RSpec Book",
35
+ subtitle: "BDD with RSpec"
36
+ )
37
+
38
+ expect(dbl.title).to eq("The RSpec Book")
39
+ expect(dbl.subtitle).to eq("BDD with RSpec")
40
+ end
41
+ end
42
+ end
43
+
44
+ # Partial doubles - stubbing methods on real objects
45
+ RSpec.describe "partial doubles" do
46
+ describe "stubbing real objects" do
47
+ it "stubs specific methods while preserving others" do
48
+ string = "a string"
49
+ allow(string).to receive(:length).and_return(500)
50
+
51
+ expect(string.length).to eq(500)
52
+ expect(string.reverse).to eq("gnirts a") # Still works
53
+ end
54
+
55
+ it "stubs class methods" do
56
+ allow(User).to receive(:find).and_return(build(:user, name: "Stubbed"))
57
+
58
+ expect(User.find(1).name).to eq("Stubbed")
59
+ end
60
+ end
61
+
62
+ describe "stub restoration" do
63
+ # Stubs are automatically restored after each example
64
+ it "first example stubs User.count" do
65
+ allow(User).to receive(:count).and_return(100)
66
+ expect(User.count).to eq(100)
67
+ end
68
+
69
+ it "next example has original behavior" do
70
+ # User.count works normally again
71
+ expect(User.count).to be_a(Integer)
72
+ end
73
+ end
74
+ end
75
+
76
+ # Practical example: stubbing external services
77
+ RSpec.describe WeatherService do
78
+ subject(:service) { build(:weather_service, client:) }
79
+
80
+ let(:client) { instance_double("HTTPClient") }
81
+
82
+ describe "#temperature" do
83
+ context "when API returns data" do
84
+ before do
85
+ allow(client).to receive(:get)
86
+ .with("/weather", hash_including(zip: "12345"))
87
+ .and_return(temp: 72, humidity: 45)
88
+ end
89
+
90
+ it "extracts temperature from response" do
91
+ expect(service.temperature("12345")).to eq(72)
92
+ end
93
+ end
94
+
95
+ context "when API returns nil" do
96
+ before do
97
+ allow(client).to receive(:get).and_return(nil)
98
+ end
99
+
100
+ it "returns default value" do
101
+ expect(service.temperature("12345")).to eq(0)
102
+ end
103
+ end
104
+ end
105
+ end
106
+
107
+ # Practical example: isolating database calls
108
+ RSpec.describe ReportGenerator do
109
+ subject(:generator) { build(:report_generator) }
110
+
111
+ describe "#monthly_summary" do
112
+ let(:orders) { build_list(:order, 3, total: 100) }
113
+
114
+ before do
115
+ allow(Order).to receive(:where).and_return(orders)
116
+ allow(orders).to receive(:sum).with(:total).and_return(300)
117
+ end
118
+
119
+ it "calculates total from orders" do
120
+ summary = generator.monthly_summary(Date.current)
121
+ expect(summary[:total]).to eq(300)
122
+ end
123
+
124
+ it "queries orders for the month" do
125
+ generator.monthly_summary(Date.new(2024, 1, 15))
126
+
127
+ expect(Order).to have_received(:where).with(
128
+ created_at: Date.new(2024, 1, 1)..Date.new(2024, 1, 31)
129
+ )
130
+ end
131
+ end
132
+ end
133
+