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,172 @@
1
+ # RSpec Mocks: any_instance Examples
2
+ # Source: rspec-mocks gem features/working_with_legacy_code/any_instance.feature
3
+
4
+ # WARNING: any_instance is discouraged. It often indicates:
5
+ # - Missing dependency injection
6
+ # - Tight coupling in design
7
+ # - Need for refactoring
8
+ #
9
+ # Prefer instance_double with explicit injection.
10
+ # Use any_instance only for legacy code you can't easily refactor.
11
+
12
+ # allow_any_instance_of - stub method on all instances
13
+ RSpec.describe "allow_any_instance_of" do
14
+ describe "basic usage" do
15
+ it "stubs method on any instance" do
16
+ allow_any_instance_of(Widget).to receive(:name).and_return("Stubbed")
17
+
18
+ widget1 = Widget.new
19
+ widget2 = Widget.new
20
+
21
+ expect(widget1.name).to eq("Stubbed")
22
+ expect(widget2.name).to eq("Stubbed")
23
+ end
24
+ end
25
+
26
+ describe "with receive_messages" do
27
+ it "stubs multiple methods" do
28
+ allow_any_instance_of(Widget).to receive_messages(
29
+ name: "Stubbed",
30
+ price: 100
31
+ )
32
+
33
+ widget = Widget.new
34
+ expect(widget.name).to eq("Stubbed")
35
+ expect(widget.price).to eq(100)
36
+ end
37
+ end
38
+
39
+ describe "with arguments" do
40
+ it "matches specific arguments" do
41
+ allow_any_instance_of(Calculator).to receive(:add).with(1, 2).and_return(100)
42
+
43
+ calc = Calculator.new
44
+ expect(calc.add(1, 2)).to eq(100)
45
+ end
46
+ end
47
+
48
+ describe "block receives instance" do
49
+ it "passes instance as first argument to block" do
50
+ allow_any_instance_of(String).to receive(:slice) do |instance, start, length|
51
+ "Instance: #{instance[start, length]}"
52
+ end
53
+
54
+ expect("hello world".slice(0, 5)).to eq("Instance: hello")
55
+ end
56
+ end
57
+
58
+ describe "consecutive return values" do
59
+ it "applies per instance, not globally" do
60
+ allow_any_instance_of(Counter).to receive(:value).and_return(1, 2, 3)
61
+
62
+ first = Counter.new
63
+ second = Counter.new
64
+
65
+ # Each instance gets its own sequence
66
+ expect(first.value).to eq(1)
67
+ expect(first.value).to eq(2)
68
+ expect(second.value).to eq(1) # New instance, new sequence
69
+ expect(first.value).to eq(3)
70
+ end
71
+ end
72
+ end
73
+
74
+ # expect_any_instance_of - expect at least one instance receives message
75
+ RSpec.describe "expect_any_instance_of" do
76
+ describe "basic expectation" do
77
+ it "passes if any instance receives the message" do
78
+ expect_any_instance_of(Widget).to receive(:save)
79
+
80
+ widget = Widget.new
81
+ widget.save
82
+ end
83
+ end
84
+
85
+ describe "with return value" do
86
+ it "returns specified value and verifies call" do
87
+ expect_any_instance_of(Widget).to receive(:save).and_return(true)
88
+
89
+ widget = Widget.new
90
+ expect(widget.save).to be(true)
91
+ end
92
+ end
93
+ end
94
+
95
+ # Legacy code example - why any_instance exists
96
+ RSpec.describe "legacy code scenario" do
97
+ # Imagine this service creates its own dependencies internally
98
+ # and we can't easily inject them
99
+ class LegacyOrderService
100
+ def process(order_id)
101
+ order = Order.find(order_id) # Creates Order internally
102
+ order.process
103
+ order.save
104
+ end
105
+ end
106
+
107
+ describe LegacyOrderService do
108
+ subject(:service) { LegacyOrderService.new }
109
+
110
+ # Using any_instance because we can't inject Order
111
+ it "processes and saves the order" do
112
+ allow(Order).to receive(:find).and_return(build(:order))
113
+ expect_any_instance_of(Order).to receive(:process)
114
+ expect_any_instance_of(Order).to receive(:save)
115
+
116
+ service.process(1)
117
+ end
118
+ end
119
+
120
+ # BETTER: Refactor to use dependency injection
121
+ class RefactoredOrderService
122
+ def initialize(order_repository: Order)
123
+ @order_repository = order_repository
124
+ end
125
+
126
+ def process(order_id)
127
+ order = @order_repository.find(order_id)
128
+ order.process
129
+ order.save
130
+ end
131
+
132
+ private
133
+
134
+ attr_reader :order_repository
135
+ end
136
+
137
+ describe RefactoredOrderService do
138
+ subject(:service) { RefactoredOrderService.new(order_repository:) }
139
+
140
+ let(:order_repository) { class_double("Order") }
141
+ let(:order) { instance_double("Order") }
142
+
143
+ it "processes and saves the order" do
144
+ allow(order_repository).to receive(:find).and_return(order)
145
+ expect(order).to receive(:process)
146
+ expect(order).to receive(:save)
147
+
148
+ service.process(1)
149
+ end
150
+ end
151
+ end
152
+
153
+ # When any_instance might be acceptable
154
+ RSpec.describe "acceptable any_instance usage" do
155
+ describe "testing framework extensions" do
156
+ # Testing behavior added to core classes
157
+ it "stubs String extension method" do
158
+ allow_any_instance_of(String).to receive(:custom_method).and_return("extended")
159
+
160
+ expect("test".custom_method).to eq("extended")
161
+ end
162
+ end
163
+
164
+ describe "testing monkey patches in legacy code" do
165
+ it "verifies behavior without refactoring" do
166
+ expect_any_instance_of(LegacyModel).to receive(:legacy_callback)
167
+
168
+ LegacyModel.create(name: "test")
169
+ end
170
+ end
171
+ end
172
+
@@ -0,0 +1,206 @@
1
+ # RSpec Mocks: Argument Matchers Examples
2
+ # Source: rspec-mocks gem features/setting_constraints/matching_arguments.feature
3
+
4
+ # Built-in argument matchers
5
+ RSpec.describe "argument matchers" do
6
+ describe "anything" do
7
+ it "matches any single argument" do
8
+ dbl = double("collaborator")
9
+ expect(dbl).to receive(:foo).with(anything)
10
+ dbl.foo("whatever")
11
+ end
12
+
13
+ it "matches at specific positions" do
14
+ dbl = double("collaborator")
15
+ expect(dbl).to receive(:foo).with(1, anything, 3)
16
+ dbl.foo(1, "anything goes here", 3)
17
+ end
18
+ end
19
+
20
+ describe "any_args" do
21
+ it "matches any number of arguments" do
22
+ dbl = double("collaborator")
23
+ expect(dbl).to receive(:foo).with(any_args)
24
+ dbl.foo(1, 2, 3, 4, 5)
25
+ end
26
+ end
27
+
28
+ describe "no_args" do
29
+ it "matches zero arguments" do
30
+ dbl = double("collaborator")
31
+ expect(dbl).to receive(:foo).with(no_args)
32
+ dbl.foo
33
+ end
34
+ end
35
+
36
+ describe "type matchers" do
37
+ it "matches by kind_of" do
38
+ dbl = double("collaborator")
39
+ expect(dbl).to receive(:foo).with(kind_of(Numeric))
40
+ dbl.foo(42)
41
+ # Also matches floats, BigDecimal, etc.
42
+ end
43
+
44
+ it "matches by instance_of (exact class)" do
45
+ dbl = double("collaborator")
46
+ expect(dbl).to receive(:foo).with(instance_of(Integer))
47
+ dbl.foo(42)
48
+ end
49
+ end
50
+
51
+ describe "duck_type" do
52
+ it "matches by method presence" do
53
+ dbl = double("collaborator")
54
+ expect(dbl).to receive(:foo).with(duck_type(:to_s, :length))
55
+
56
+ dbl.foo("a string") # Has both methods
57
+ end
58
+ end
59
+
60
+ describe "boolean" do
61
+ it "matches true or false" do
62
+ dbl = double("collaborator")
63
+ allow(dbl).to receive(:foo).with(boolean)
64
+
65
+ dbl.foo(true)
66
+ dbl.foo(false)
67
+ end
68
+ end
69
+
70
+ describe "hash_including" do
71
+ it "matches partial hash" do
72
+ dbl = double("collaborator")
73
+ expect(dbl).to receive(:foo).with(hash_including(a: 1))
74
+ dbl.foo(a: 1, b: 2, c: 3)
75
+ end
76
+
77
+ it "matches nested structure" do
78
+ dbl = double("collaborator")
79
+ expect(dbl).to receive(:foo).with(
80
+ hash_including(user: hash_including(name: "Alice"))
81
+ )
82
+ dbl.foo(user: { name: "Alice", email: "alice@example.com" })
83
+ end
84
+ end
85
+
86
+ describe "hash_excluding" do
87
+ it "matches hash without specified keys" do
88
+ dbl = double("collaborator")
89
+ expect(dbl).to receive(:foo).with(hash_excluding(:admin))
90
+ dbl.foo(name: "Alice", role: "user")
91
+ end
92
+ end
93
+
94
+ describe "array_including" do
95
+ it "matches array containing items" do
96
+ dbl = double("collaborator")
97
+ expect(dbl).to receive(:foo).with(array_including(1, 2))
98
+ dbl.foo([1, 2, 3, 4])
99
+ end
100
+ end
101
+
102
+ describe "array_excluding" do
103
+ it "matches array without specified items" do
104
+ dbl = double("collaborator")
105
+ expect(dbl).to receive(:foo).with(array_excluding(:admin))
106
+ dbl.foo([:user, :guest])
107
+ end
108
+ end
109
+
110
+ describe "regex matching" do
111
+ it "matches strings with regex" do
112
+ dbl = double("collaborator")
113
+ expect(dbl).to receive(:foo).with(/bar/)
114
+ dbl.foo("foobar")
115
+ end
116
+ end
117
+
118
+ describe "RSpec matchers" do
119
+ it "uses collection matchers" do
120
+ dbl = double("collaborator")
121
+ expect(dbl).to receive(:foo).with(a_collection_containing_exactly(1, 2, 3))
122
+ dbl.foo([3, 1, 2])
123
+ end
124
+
125
+ it "uses string matchers" do
126
+ dbl = double("collaborator")
127
+ expect(dbl).to receive(:foo).with(a_string_starting_with("Hello"))
128
+ dbl.foo("Hello, World!")
129
+ end
130
+
131
+ it "uses comparison matchers" do
132
+ dbl = double("collaborator")
133
+ expect(dbl).to receive(:foo).with(a_value > 10)
134
+ dbl.foo(15)
135
+ end
136
+ end
137
+
138
+ describe "satisfy" do
139
+ it "matches with custom predicate" do
140
+ dbl = double("collaborator")
141
+ expect(dbl).to receive(:foo).with(
142
+ satisfy { |x| x[:a][:b][:c] == 5 }
143
+ )
144
+ dbl.foo(a: { b: { c: 5 } })
145
+ end
146
+ end
147
+
148
+ describe "having_attributes" do
149
+ it "matches object with attributes" do
150
+ dbl = double("collaborator")
151
+ user = build(:user, name: "Alice", email: "alice@example.com")
152
+
153
+ expect(dbl).to receive(:process).with(
154
+ having_attributes(name: "Alice")
155
+ )
156
+ dbl.process(user)
157
+ end
158
+ end
159
+ end
160
+
161
+ # Argument-dependent responses
162
+ RSpec.describe "conditional stubs" do
163
+ describe "different responses by argument" do
164
+ it "returns specific values for specific arguments" do
165
+ dbl = double("collaborator")
166
+ allow(dbl).to receive(:foo).and_return(:default)
167
+ allow(dbl).to receive(:foo).with(1).and_return(:one)
168
+ allow(dbl).to receive(:foo).with(2).and_return(:two)
169
+
170
+ expect(dbl.foo(0)).to eq(:default)
171
+ expect(dbl.foo(1)).to eq(:one)
172
+ expect(dbl.foo(2)).to eq(:two)
173
+ expect(dbl.foo(99)).to eq(:default)
174
+ end
175
+ end
176
+ end
177
+
178
+ # Practical example
179
+ RSpec.describe UserRepository do
180
+ subject(:repository) { build(:user_repository, database:) }
181
+
182
+ let(:database) { instance_double("Database") }
183
+
184
+ describe "#find_by_attributes" do
185
+ before do
186
+ allow(database).to receive(:query)
187
+ .with(hash_including(active: true))
188
+ .and_return([build(:user, :active)])
189
+
190
+ allow(database).to receive(:query)
191
+ .with(hash_including(admin: true))
192
+ .and_return([build(:user, :admin)])
193
+ end
194
+
195
+ it "queries active users" do
196
+ result = repository.find_by_attributes(active: true, name: "Alice")
197
+ expect(result.first).to be_active
198
+ end
199
+
200
+ it "queries admin users" do
201
+ result = repository.find_by_attributes(admin: true, department: "IT")
202
+ expect(result.first).to be_admin
203
+ end
204
+ end
205
+ end
206
+
@@ -0,0 +1,177 @@
1
+ # RSpec Mocks: Constant Stubbing Examples
2
+ # Source: rspec-mocks gem features/mutating_constants/*.feature
3
+
4
+ # stub_const - temporarily replace constant values
5
+ RSpec.describe "stub_const" do
6
+ describe "top-level constants" do
7
+ it "stubs constant for duration of example" do
8
+ stub_const("FOO", 5)
9
+ expect(FOO).to eq(5)
10
+ end
11
+
12
+ it "restores original value after example" do
13
+ original = FOO rescue nil
14
+ stub_const("FOO", 999)
15
+ # After this example, FOO returns to original
16
+ end
17
+ end
18
+
19
+ describe "nested constants" do
20
+ it "stubs nested class constants" do
21
+ stub_const("MyGem::SomeClass::PER_PAGE", 100)
22
+ expect(MyGem::SomeClass::PER_PAGE).to eq(100)
23
+ end
24
+
25
+ it "stubs module constants" do
26
+ stub_const("Rails::VERSION", "7.0.0")
27
+ expect(Rails::VERSION).to eq("7.0.0")
28
+ end
29
+ end
30
+
31
+ describe "replacing classes" do
32
+ it "stubs with fake class" do
33
+ fake_class = Class.new do
34
+ def self.perform
35
+ :fake_result
36
+ end
37
+ end
38
+
39
+ stub_const("HeavyWorker", fake_class)
40
+ expect(HeavyWorker.perform).to eq(:fake_result)
41
+ end
42
+ end
43
+
44
+ describe "transfer_nested_constants" do
45
+ it "transfers all nested constants" do
46
+ fake_deck = Class.new
47
+
48
+ stub_const("CardDeck", fake_deck, transfer_nested_constants: true)
49
+
50
+ # Original CardDeck::SUITS is now available on fake_deck
51
+ expect(CardDeck::SUITS).to eq(%w[hearts diamonds clubs spades])
52
+ end
53
+
54
+ it "transfers selected constants" do
55
+ fake_deck = Class.new
56
+
57
+ stub_const("CardDeck", fake_deck, transfer_nested_constants: [:SUITS])
58
+
59
+ expect(CardDeck::SUITS).to eq(%w[hearts diamonds clubs spades])
60
+ # Other constants like RANKS are not transferred
61
+ end
62
+ end
63
+
64
+ describe "undefined constants" do
65
+ it "can stub constants that don't exist yet" do
66
+ stub_const("FUTURE_FEATURE_FLAG", true)
67
+ expect(FUTURE_FEATURE_FLAG).to be(true)
68
+ # Constant is removed after example
69
+ end
70
+ end
71
+ end
72
+
73
+ # hide_const - temporarily make constant undefined
74
+ RSpec.describe "hide_const" do
75
+ describe "hiding defined constants" do
76
+ it "makes constant undefined" do
77
+ hide_const("SomeClass")
78
+ expect { SomeClass }.to raise_error(NameError)
79
+ end
80
+
81
+ it "restores constant after example" do
82
+ hide_const("SomeClass")
83
+ # After this example, SomeClass is available again
84
+ end
85
+ end
86
+
87
+ describe "hiding nested constants" do
88
+ it "hides nested constant" do
89
+ hide_const("MyGem::SomeClass::TIMEOUT")
90
+ expect { MyGem::SomeClass::TIMEOUT }.to raise_error(NameError)
91
+ end
92
+ end
93
+
94
+ describe "hiding undefined constants" do
95
+ it "does nothing for undefined constants" do
96
+ # Safe to call - no error raised
97
+ hide_const("DEFINITELY_NOT_DEFINED")
98
+ end
99
+ end
100
+ end
101
+
102
+ # Practical examples
103
+ RSpec.describe "configuration testing" do
104
+ describe FeatureToggle do
105
+ describe ".enabled?" do
106
+ context "when feature is enabled" do
107
+ before { stub_const("FeatureToggle::FEATURES", { dark_mode: true }) }
108
+
109
+ it "returns true" do
110
+ expect(FeatureToggle.enabled?(:dark_mode)).to be(true)
111
+ end
112
+ end
113
+
114
+ context "when feature is disabled" do
115
+ before { stub_const("FeatureToggle::FEATURES", { dark_mode: false }) }
116
+
117
+ it "returns false" do
118
+ expect(FeatureToggle.enabled?(:dark_mode)).to be(false)
119
+ end
120
+ end
121
+ end
122
+ end
123
+ end
124
+
125
+ RSpec.describe "environment-dependent code" do
126
+ describe ApiClient do
127
+ context "in production" do
128
+ before { stub_const("Rails.env", ActiveSupport::StringInquirer.new("production")) }
129
+
130
+ it "uses production URL" do
131
+ expect(ApiClient.base_url).to eq("https://api.example.com")
132
+ end
133
+ end
134
+
135
+ context "in development" do
136
+ before { stub_const("Rails.env", ActiveSupport::StringInquirer.new("development")) }
137
+
138
+ it "uses localhost URL" do
139
+ expect(ApiClient.base_url).to eq("http://localhost:3000")
140
+ end
141
+ end
142
+ end
143
+ end
144
+
145
+ RSpec.describe "testing error handling for missing dependencies" do
146
+ describe ExternalServiceClient do
147
+ context "when gem is not loaded" do
148
+ before { hide_const("ExternalGem") }
149
+
150
+ it "raises descriptive error" do
151
+ expect { ExternalServiceClient.connect }
152
+ .to raise_error("ExternalGem is required. Add it to your Gemfile.")
153
+ end
154
+ end
155
+ end
156
+ end
157
+
158
+ RSpec.describe "pagination configuration" do
159
+ describe UserListController do
160
+ subject(:controller) { build(:user_list_controller) }
161
+
162
+ context "with custom page size" do
163
+ before { stub_const("UserListController::DEFAULT_PAGE_SIZE", 50) }
164
+
165
+ it "uses custom page size" do
166
+ expect(controller.default_page_size).to eq(50)
167
+ end
168
+ end
169
+
170
+ context "with default page size" do
171
+ it "uses standard page size" do
172
+ expect(controller.default_page_size).to eq(25)
173
+ end
174
+ end
175
+ end
176
+ end
177
+
@@ -0,0 +1,139 @@
1
+ # RSpec Mocks: Test Doubles Examples
2
+ # Source: rspec-mocks gem features/basics/test_doubles.feature,
3
+ # verifying_doubles/*.feature
4
+
5
+ # Basic double - strict, raises on unexpected messages
6
+ RSpec.describe "basic double" do
7
+ it "raises on unexpected messages" do
8
+ dbl = double("collaborator")
9
+ expect { dbl.foo }.to raise_error(RSpec::Mocks::MockExpectationError)
10
+ end
11
+
12
+ it "can be created with predefined stubs" do
13
+ dbl = double("collaborator", foo: 3, bar: 4)
14
+ expect(dbl.foo).to eq(3)
15
+ expect(dbl.bar).to eq(4)
16
+ end
17
+
18
+ it "can be anonymous with stubs" do
19
+ dbl = double(foo: "bar", baz: "qux")
20
+ expect(dbl.foo).to eq("bar")
21
+ expect(dbl.baz).to eq("qux")
22
+ end
23
+ end
24
+
25
+ # instance_double - verifies against instance methods
26
+ RSpec.describe "instance_double" do
27
+ describe "verification" do
28
+ it "allows stubbing existing methods" do
29
+ notifier = instance_double("ConsoleNotifier", notify: true)
30
+ expect(notifier.notify).to be(true)
31
+ end
32
+
33
+ it "raises if stubbing non-existent method" do
34
+ notifier = instance_double("ConsoleNotifier")
35
+ expect {
36
+ allow(notifier).to receive(:non_existent_method)
37
+ }.to raise_error(RSpec::Mocks::MockExpectationError, /does not implement/)
38
+ end
39
+
40
+ it "verifies argument arity" do
41
+ calculator = instance_double("Calculator")
42
+ expect {
43
+ allow(calculator).to receive(:add).with(1, 2, 3, 4, 5)
44
+ }.to raise_error(RSpec::Mocks::MockExpectationError, /wrong number of arguments/)
45
+ end
46
+ end
47
+ end
48
+
49
+ # Practical example with instance_double
50
+ RSpec.describe UserNotificationService do
51
+ subject(:service) { build(:user_notification_service, notifier:) }
52
+
53
+ let(:notifier) { instance_double("ConsoleNotifier") }
54
+ let(:user) { build(:user) }
55
+
56
+ describe "#notify" do
57
+ it "delegates to the notifier" do
58
+ expect(notifier).to receive(:notify).with(user.email, "Welcome!")
59
+ service.notify(user, "Welcome!")
60
+ end
61
+ end
62
+ end
63
+
64
+ # class_double - verifies against class methods
65
+ RSpec.describe "class_double" do
66
+ describe "replacing constants" do
67
+ it "can replace the class constant with as_stubbed_const" do
68
+ fake_mailer = class_double("UserMailer").as_stubbed_const
69
+ allow(fake_mailer).to receive(:send_welcome)
70
+
71
+ # Now UserMailer refers to fake_mailer within this example
72
+ UserMailer.send_welcome
73
+ expect(fake_mailer).to have_received(:send_welcome)
74
+ end
75
+
76
+ it "transfers nested constants when requested" do
77
+ fake_class = class_double("CardDeck").as_stubbed_const(
78
+ transfer_nested_constants: true
79
+ )
80
+ # CardDeck::SUITS and other constants are now available
81
+ expect(CardDeck::SUITS).to eq(%w[hearts diamonds clubs spades])
82
+ end
83
+
84
+ it "can selectively transfer constants" do
85
+ fake_class = class_double("CardDeck").as_stubbed_const(
86
+ transfer_nested_constants: [:SUITS]
87
+ )
88
+ # Only SUITS is transferred
89
+ expect(CardDeck::SUITS).to eq(%w[hearts diamonds clubs spades])
90
+ end
91
+ end
92
+ end
93
+
94
+ # Practical example with class_double
95
+ RSpec.describe OrderProcessor do
96
+ subject(:processor) { build(:order_processor) }
97
+
98
+ let(:payment_gateway) { class_double("PaymentGateway").as_stubbed_const }
99
+
100
+ describe "#process" do
101
+ let(:order) { build(:order, amount: 100) }
102
+
103
+ it "charges the payment gateway" do
104
+ expect(payment_gateway).to receive(:charge).with(100).and_return(true)
105
+ processor.process(order)
106
+ end
107
+ end
108
+ end
109
+
110
+ # object_double - doubles an existing object instance
111
+ RSpec.describe "object_double" do
112
+ describe "doubling real objects" do
113
+ it "avoids side effects while verifying methods" do
114
+ real_user = User.new
115
+ user_double = object_double(real_user, save: true)
116
+
117
+ expect(user_double.save).to be(true)
118
+ end
119
+
120
+ it "verifies methods exist on the real object" do
121
+ real_user = User.new
122
+ user_double = object_double(real_user)
123
+
124
+ expect {
125
+ allow(user_double).to receive(:non_existent)
126
+ }.to raise_error(RSpec::Mocks::MockExpectationError)
127
+ end
128
+ end
129
+
130
+ describe "doubling constant objects" do
131
+ it "can replace global logger" do
132
+ logger = object_double("MyApp::LOGGER", info: nil, error: nil).as_stubbed_const
133
+
134
+ MyApp::LOGGER.info("test message")
135
+ expect(logger).to have_received(:info).with("test message")
136
+ end
137
+ end
138
+ end
139
+