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,340 @@
1
+ ---
2
+ name: rspec
3
+ description: "RSpec testing with FactoryBot — matchers, test doubles, shared examples. Activate when writing specs, fixing failing tests, working with describe/it/expect blocks, editing *_spec.rb files, planning test strategy, or discussing unit/integration tests."
4
+ ---
5
+
6
+ # RSpec Testing
7
+
8
+ This skill provides comprehensive guidance for writing effective RSpec tests in Ruby and Rails applications. Use for writing new specs, fixing failing tests, understanding matchers, using test doubles, and following RSpec best practices.
9
+
10
+ ## Quick Reference
11
+
12
+ ### Basic Structure
13
+
14
+ ```ruby
15
+ RSpec.describe Order do
16
+ subject(:order) { described_class.new(items) }
17
+ let(:items) { [item1, item2] }
18
+ let(:item1) { double("item", price: 10) }
19
+ let(:item2) { double("item", price: 20) }
20
+
21
+ describe "#total" do
22
+ it "sums item prices" do
23
+ expect(order.total).to eq(30)
24
+ end
25
+ end
26
+
27
+ context "with discount" do
28
+ let(:order) { described_class.new(items, discount: 5) }
29
+
30
+ it "applies discount" do
31
+ expect(order.total).to eq(25)
32
+ end
33
+ end
34
+ end
35
+ ```
36
+
37
+ ### Key Concepts
38
+
39
+ | Concept | Purpose |
40
+ |---------|---------|
41
+ | `describe` / `context` | Group related examples |
42
+ | `it` / `specify` | Define individual test cases |
43
+ | `let` | Lazy-evaluated, memoized helper |
44
+ | `let!` | Eager-evaluated helper (runs before each example) |
45
+ | `subject` | Primary object under test |
46
+ | `before` / `after` | Setup and teardown hooks |
47
+ | `expect` | Make assertions |
48
+
49
+ ## Writing Good Specs
50
+
51
+ ### Use Named Subject for Method Tests
52
+
53
+ ```ruby
54
+ describe "#calculate_total" do
55
+ subject(:total) { order.calculate_total }
56
+
57
+ it "returns sum of items" do
58
+ expect(total).to eq(100)
59
+ end
60
+ end
61
+ ```
62
+
63
+ ### Context Blocks for Different States
64
+
65
+ ```ruby
66
+ describe "#withdraw" do
67
+ context "with sufficient funds" do
68
+ let(:account) { build(:account, balance: 100) }
69
+
70
+ it "reduces balance" do
71
+ expect { account.withdraw(50) }.to change(account, :balance).by(-50)
72
+ end
73
+ end
74
+
75
+ context "with insufficient funds" do
76
+ let(:account) { build(:account, balance: 10) }
77
+
78
+ it "raises error" do
79
+ expect { account.withdraw(50) }.to raise_error(InsufficientFunds)
80
+ end
81
+ end
82
+ end
83
+ ```
84
+
85
+ ## Common Matchers
86
+
87
+ ### Equality
88
+
89
+ ```ruby
90
+ expect(x).to eq(y) # ==
91
+ expect(x).to eql(y) # eql? (type-sensitive)
92
+ expect(x).to be(y) # equal? (identity)
93
+ ```
94
+
95
+ ### Truthiness
96
+
97
+ ```ruby
98
+ expect(x).to be_truthy # not nil or false
99
+ expect(x).to be_falsey # nil or false
100
+ expect(x).to be_nil
101
+ expect(x).to be true # exactly true
102
+ ```
103
+
104
+ ### Comparisons
105
+
106
+ ```ruby
107
+ expect(x).to be > 3
108
+ expect(x).to be_between(1, 10).inclusive
109
+ expect(x).to be_within(0.1).of(3.14)
110
+ ```
111
+
112
+ ### Collections
113
+
114
+ ```ruby
115
+ expect(arr).to include(1, 2)
116
+ expect(arr).to contain_exactly(3, 2, 1) # order-independent
117
+ expect(arr).to all(be_positive)
118
+ expect(str).to start_with("hello")
119
+ expect(hash).to have_key(:name)
120
+ ```
121
+
122
+ ### Changes
123
+
124
+ ```ruby
125
+ expect { x += 1 }.to change { x }.by(1)
126
+ expect { x += 1 }.to change { x }.from(0).to(1)
127
+ expect { user.save }.to change(User, :count).by(1)
128
+ ```
129
+
130
+ ### Errors
131
+
132
+ ```ruby
133
+ expect { raise "boom" }.to raise_error
134
+ expect { raise ArgumentError, "bad" }.to raise_error(ArgumentError, /bad/)
135
+ ```
136
+
137
+ ### Predicates (Dynamic)
138
+
139
+ ```ruby
140
+ expect([]).to be_empty # [].empty?
141
+ expect(user).to be_valid # user.valid?
142
+ expect(hash).to have_key(k) # hash.has_key?(k)
143
+ ```
144
+
145
+ ## Test Doubles
146
+
147
+ ### Types
148
+
149
+ ```ruby
150
+ # Basic double (strict)
151
+ user = double("user", name: "Bob")
152
+
153
+ # Verifying doubles (recommended)
154
+ user = instance_double("User", name: "Bob") # validates instance methods
155
+ api = class_double("Api", fetch: data) # validates class methods
156
+ logger = object_double(Rails.logger) # validates object methods
157
+
158
+ # Spy (null object for after-the-fact verification)
159
+ notifier = spy("notifier")
160
+ ```
161
+
162
+ ### Stubbing
163
+
164
+ ```ruby
165
+ allow(user).to receive(:name).and_return("Bob")
166
+ allow(Api).to receive(:fetch).and_return(data)
167
+ allow(obj).to receive(:method) { computed_value }
168
+ ```
169
+
170
+ ### Expectations
171
+
172
+ ```ruby
173
+ expect(user).to receive(:save).and_return(true)
174
+ expect(Api).to receive(:post).with(hash_including(id: 1))
175
+
176
+ # Spy pattern (verify after action)
177
+ notifier = spy("notifier")
178
+ service.call(notifier)
179
+ expect(notifier).to have_received(:notify).with("done")
180
+ ```
181
+
182
+ ### Argument Matchers
183
+
184
+ ```ruby
185
+ expect(obj).to receive(:call).with(anything)
186
+ expect(obj).to receive(:call).with(kind_of(Integer))
187
+ expect(obj).to receive(:call).with(hash_including(a: 1))
188
+ expect(obj).to receive(:call).with(array_including(1, 2))
189
+ ```
190
+
191
+ ## Rails Specs
192
+
193
+ | Spec Type | Use For | Key Helpers |
194
+ |-----------|---------|-------------|
195
+ | `type: :model` | Business logic, scopes, validations | `build`, `create`, associations |
196
+ | `type: :request` | Controller actions (preferred) | `get`, `post`, `response`, `have_http_status` |
197
+ | `type: :system` | Browser/UI testing | `visit`, `fill_in`, `click_button`, `have_text` |
198
+ | `type: :job` | Background jobs | `have_enqueued_job`, `perform_now` |
199
+ | `type: :mailer` | Email delivery | `have_enqueued_mail`, `deliver_now` |
200
+ | `type: :routing` | Route resolution | `route_to`, `be_routable` |
201
+
202
+ See `examples/rails/` for complete spec templates.
203
+
204
+ ## Best Practices
205
+
206
+ ### Do
207
+
208
+ - Use `described_class` instead of hardcoding class name
209
+ - Use `let` for test data, `let!` when database records must exist before test
210
+ - Use **named subject** when referencing in tests: `subject(:user) { ... }`
211
+ - Use context blocks to organize different scenarios
212
+ - Use verifying doubles (`instance_double`) over plain `double`
213
+ - Name examples with verbs: `it "creates user"` not `it "should create user"`
214
+ - Keep examples focused on one behavior
215
+ - Use factories over fixtures for flexible test data
216
+ - Prefer `build_stubbed` or `build` over `create` when database not needed
217
+
218
+ ### Don't
219
+
220
+ - Don't use instance variables (`@user`) - use `let` for type safety
221
+ - Don't use `before` just to trigger `let` evaluation - use `let!` instead:
222
+ ```ruby
223
+ # BAD - before just to initialize
224
+ let(:user) { create(:user) }
225
+ before { user }
226
+
227
+ # GOOD - let! for eager evaluation
228
+ let!(:user) { create(:user) }
229
+ ```
230
+ Use `before` for side-effects like `sign_in(user)` or `driven_by(:rack_test)`
231
+ - Don't use `let!` when `let` suffices (wastes resources)
232
+ - Don't test Rails framework (validations work, focus on business logic)
233
+ - Don't stub the object under test
234
+ - Avoid `any_instance_of` - prefer stubbing `ClassName.new` to return a double (see `references/mocks.md`)
235
+ - Don't use `receive_message_chain` (violates Law of Demeter)
236
+ - Don't write examples without descriptions
237
+ - Shared examples work best for testing concerns across including classes. For unique behaviors, prefer repetition over abstraction
238
+
239
+ ### Factory Bot
240
+
241
+ ```ruby
242
+ # Build strategies
243
+ user = build(:user) # In-memory, not persisted
244
+ user = create(:user) # Persisted to database
245
+ user = build_stubbed(:user) # Fake persisted (fastest)
246
+ attrs = attributes_for(:user) # Hash of attributes
247
+
248
+ # With traits and attributes
249
+ user = create(:user, :admin, :verified, name: "Bob")
250
+
251
+ # Lists
252
+ users = create_list(:user, 5, :admin)
253
+ ```
254
+
255
+ **Strategy Selection**:
256
+ - `build_stubbed` - Unit tests without database (fastest)
257
+ - `build` - Validation tests, method tests
258
+ - `create` - Database queries, scopes, associations
259
+ - `attributes_for` - Controller params
260
+
261
+ See `references/factory_bot.md` for traits, sequences, associations, and callbacks.
262
+
263
+ ## Configuration
264
+
265
+ Essential settings for `spec_helper.rb` and `rails_helper.rb`:
266
+
267
+ | Setting | Purpose |
268
+ |---------|---------|
269
+ | `verify_partial_doubles = true` | Validates stubbed methods exist |
270
+ | `filter_run_when_matching :focus` | Run only focused specs (`fit`, `fdescribe`) |
271
+ | `order = :random` | Randomize spec order to catch dependencies |
272
+ | `use_transactional_fixtures = true` | Rollback database after each spec |
273
+ | `infer_spec_type_from_file_location!` | Auto-detect spec type from path |
274
+
275
+ See `examples/core/configuration.rb` for complete setup.
276
+
277
+ ## Before You Write
278
+
279
+ **What are you about to do?**
280
+
281
+ ```
282
+ ├── Creating or modifying a factory?
283
+ │ └── Read `references/factory_bot.md`
284
+
285
+ ├── Writing a new spec file?
286
+ │ ├── Model/service/PORO → Read `references/core.md`
287
+ │ ├── Request/controller → Read `references/rails.md`
288
+ │ └── System/feature/job/mailer → Read `references/rails.md`
289
+
290
+ ├── Using test doubles, stubs, or mocks?
291
+ │ └── Read `references/mocks.md`
292
+
293
+ ├── Writing custom or complex matchers?
294
+ │ └── Read `references/matchers.md`
295
+
296
+ └── Fixing a failing spec?
297
+ ├── Factory-related error → Read `references/factory_bot.md`
298
+ ├── Mock/stub error → Read `references/mocks.md`
299
+ ├── Matcher error → Read `references/matchers.md`
300
+ └── Rails-specific error → Read `references/rails.md`
301
+ ```
302
+
303
+ **Need code examples?**
304
+
305
+ ```
306
+ ├── Basic spec structure, hooks, shared examples
307
+ │ └── See `examples/core/`
308
+
309
+ ├── Matcher usage patterns
310
+ │ └── See `examples/matchers/`
311
+
312
+ ├── Test doubles and stubbing
313
+ │ └── See `examples/mocks/`
314
+
315
+ ├── Rails spec templates (model, request, system, job, mailer)
316
+ │ └── See `examples/rails/`
317
+
318
+ └── Factory definitions with traits and associations
319
+ └── See `examples/factory_bot/`
320
+ ```
321
+
322
+ ### Running Specs
323
+
324
+ ```bash
325
+ rspec # all specs
326
+ rspec spec/models # directory
327
+ rspec spec/user_spec.rb # file
328
+ rspec spec/user_spec.rb:23 # line
329
+ rspec --format doc # documentation format
330
+ rspec --only-failures # re-run failures
331
+ rspec --profile 10 # show slowest
332
+ ```
333
+
334
+ ### Debugging
335
+
336
+ ```bash
337
+ rspec --seed 12345 # reproduce random order
338
+ rspec --fail-fast # stop on first failure
339
+ rspec --backtrace # full backtrace
340
+ ```
@@ -0,0 +1,69 @@
1
+ # RSpec Core: Basic Structure Examples
2
+ # Source: rspec-core gem features/example_groups/basic_structure.feature
3
+
4
+ # Basic describe/context/it structure
5
+ RSpec.describe Order do
6
+ context "with no items" do
7
+ let(:order) { build(:order) }
8
+
9
+ it "has zero total" do
10
+ expect(order.total).to eq(0)
11
+ end
12
+ end
13
+
14
+ context "with one item" do
15
+ let(:order) { build(:order) }
16
+ let(:item) { build(:item, price: 10) }
17
+
18
+ before { order.add_item(item) }
19
+
20
+ it "has item price as total" do
21
+ expect(order.total).to eq(10)
22
+ end
23
+ end
24
+ end
25
+
26
+ # Method-focused describe blocks
27
+ RSpec.describe Calculator do
28
+ subject(:calculator) { build(:calculator) }
29
+
30
+ describe "#add" do
31
+ subject(:result) { calculator.add(2, 3) }
32
+
33
+ it "sums two numbers" do
34
+ expect(result).to eq(5)
35
+ end
36
+ end
37
+
38
+ describe ".from_string" do
39
+ subject(:calc) { described_class.from_string("2+3") }
40
+
41
+ it "parses expression" do
42
+ expect(calc.result).to eq(5)
43
+ end
44
+ end
45
+ end
46
+
47
+ # Nested contexts for state variations
48
+ RSpec.describe BankAccount do
49
+ describe "#withdraw" do
50
+ subject(:account) { build(:bank_account, balance:) }
51
+
52
+ context "with sufficient funds" do
53
+ let(:balance) { 100 }
54
+
55
+ it "reduces balance" do
56
+ account.withdraw(50)
57
+ expect(account.balance).to eq(50)
58
+ end
59
+ end
60
+
61
+ context "with insufficient funds" do
62
+ let(:balance) { 10 }
63
+
64
+ it "raises InsufficientFundsError" do
65
+ expect { account.withdraw(50) }.to raise_error(InsufficientFundsError)
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,126 @@
1
+ # RSpec Core: Configuration Examples
2
+ # Source: rspec-core gem spec/spec_helper.rb, features/configuration/*.feature
3
+
4
+ # Full spec_helper.rb configuration
5
+ RSpec.configure do |config|
6
+ # Expectations configuration
7
+ config.expect_with :rspec do |expectations|
8
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
9
+ expectations.max_formatted_output_length = 1000
10
+ end
11
+
12
+ # Mocks configuration
13
+ config.mock_with :rspec do |mocks|
14
+ mocks.verify_partial_doubles = true
15
+ end
16
+
17
+ # Execution order
18
+ config.order = :random
19
+ Kernel.srand config.seed
20
+
21
+ # Failure handling
22
+ config.fail_fast = false # or number like 3
23
+
24
+ # Focus filtering - run only focused tests when any exist
25
+ config.filter_run_when_matching :focus
26
+
27
+ # Persist example status for --only-failures
28
+ config.example_status_persistence_file_path = "spec/examples.txt"
29
+
30
+ # Output formatting
31
+ config.default_formatter = "doc" if config.files_to_run.one?
32
+
33
+ # Profile slow examples
34
+ config.profile_examples = 10
35
+
36
+ # Disable monkey patching (no should syntax)
37
+ config.disable_monkey_patching!
38
+
39
+ # Warnings
40
+ config.raise_errors_for_deprecations!
41
+ end
42
+
43
+ # Including modules conditionally
44
+ RSpec.configure do |config|
45
+ # Include everywhere
46
+ config.include FactoryBot::Syntax::Methods
47
+
48
+ # Include only in specific types
49
+ config.include Devise::Test::ControllerHelpers, type: :controller
50
+ config.include Devise::Test::IntegrationHelpers, type: :request
51
+ config.include Capybara::DSL, type: :feature
52
+
53
+ # Include based on metadata
54
+ config.include ApiHelpers, :api
55
+ config.include AuthHelpers, :authorized
56
+ end
57
+
58
+ # Extending example groups
59
+ RSpec.configure do |config|
60
+ config.extend ControllerMacros, type: :controller
61
+ end
62
+
63
+ # Custom type inference
64
+ RSpec.configure do |config|
65
+ config.infer_spec_type_from_file_location!
66
+
67
+ config.define_derived_metadata(file_path: %r{/spec/api/}) do |metadata|
68
+ metadata[:type] = :request
69
+ end
70
+ end
71
+
72
+ # Shared context auto-inclusion
73
+ RSpec.configure do |config|
74
+ config.include_context "authenticated user", :authenticated
75
+ config.include_context "with admin", :admin
76
+ end
77
+
78
+ # Filter by Ruby version
79
+ RSpec.configure do |config|
80
+ config.filter_run_excluding ruby: ->(version) {
81
+ case version.to_s
82
+ when "!jruby"
83
+ RUBY_ENGINE == "jruby"
84
+ when /^> (.*)/
85
+ !(RUBY_VERSION.to_s > $1)
86
+ else
87
+ !(RUBY_VERSION.to_s =~ /^#{version}/)
88
+ end
89
+ }
90
+ end
91
+
92
+ # Example:
93
+ it "uses Ruby 3.2 feature", ruby: "> 3.2" do
94
+ end
95
+
96
+ # Alias it_behaves_like for readability
97
+ RSpec.configure do |config|
98
+ config.alias_it_behaves_like_to :it_has_behavior
99
+ config.alias_it_behaves_like_to :it_should_behave_like
100
+ end
101
+
102
+ # Around hooks for specific metadata
103
+ RSpec.configure do |config|
104
+ config.around(:example, :freeze_time) do |example|
105
+ travel_to(Time.zone.local(2024, 1, 1)) do
106
+ example.run
107
+ end
108
+ end
109
+
110
+ config.around(:example, :isolated_directory) do |example|
111
+ Dir.mktmpdir do |dir|
112
+ Dir.chdir(dir) { example.run }
113
+ end
114
+ end
115
+ end
116
+
117
+ # .rspec file example
118
+ # --format documentation
119
+ # --color
120
+ # --require spec_helper
121
+ # --order random
122
+ # --profile 10
123
+
124
+ # .rspec-local (gitignored, personal preferences)
125
+ # --fail-fast
126
+ # --format progress
@@ -0,0 +1,126 @@
1
+ # RSpec Core: Hooks Examples
2
+ # Source: rspec-core gem features/hooks/before_and_after_hooks.feature
3
+
4
+ # before(:example) - runs before each example
5
+ # NOTE: Use let/let! instead of before + instance variables
6
+ RSpec.describe Thing do
7
+ let(:thing) { build(:thing) }
8
+
9
+ it "has 0 widgets initially" do
10
+ expect(thing.widgets.count).to eq(0)
11
+ end
12
+
13
+ it "can accept widgets" do
14
+ thing.widgets << build(:widget)
15
+ expect(thing.widgets.count).to eq(1)
16
+ end
17
+
18
+ it "does not share state across examples" do
19
+ expect(thing.widgets.count).to eq(0) # Fresh thing via let
20
+ end
21
+ end
22
+
23
+ # Hook scopes - configuration level
24
+ # NOTE: Suite/context level hooks are configured globally
25
+ RSpec.configure do |config|
26
+ config.before(:suite) do
27
+ # Run once before all specs (database setup, etc.)
28
+ DatabaseCleaner.strategy = :transaction
29
+ end
30
+
31
+ config.after(:suite) do
32
+ # Run once after all specs (cleanup)
33
+ end
34
+ end
35
+
36
+ # Conditional hooks with metadata
37
+ RSpec.configure do |config|
38
+ config.before(:example, :authorized) do
39
+ sign_in_as(:authorized_user)
40
+ end
41
+
42
+ config.before(:example, db: :clean) do
43
+ DatabaseCleaner.clean
44
+ end
45
+ end
46
+
47
+ RSpec.describe AdminController, :authorized do
48
+ let(:admin) { create(:user, :admin) }
49
+
50
+ it "allows access" do # sign_in_as runs automatically
51
+ get :index
52
+ expect(response).to be_successful
53
+ end
54
+ end
55
+
56
+ # around hooks - wrap example execution
57
+ RSpec.describe "Database transaction" do
58
+ around(:example) do |example|
59
+ DatabaseCleaner.cleaning do
60
+ example.run
61
+ end
62
+ end
63
+
64
+ let(:user) { create(:user) }
65
+
66
+ it "runs inside transaction" do
67
+ expect(user).to be_persisted
68
+ # Transaction rolled back after example
69
+ end
70
+ end
71
+
72
+ # around with before/after - execution order
73
+ RSpec.describe "Hook order" do
74
+ # Output order:
75
+ # 1. around: before
76
+ # 2. before
77
+ # 3. example
78
+ # 4. after
79
+ # 5. around: after
80
+
81
+ around(:example) do |example|
82
+ puts "around: before"
83
+ example.run
84
+ puts "around: after"
85
+ end
86
+
87
+ before(:example) { puts "before" }
88
+ after(:example) { puts "after" }
89
+
90
+ it "runs in order" do
91
+ puts "example"
92
+ end
93
+ end
94
+
95
+ # before(:context) - shared expensive setup
96
+ # NOTE: let/subject/mocks NOT available in before(:context)
97
+ # Use instance variables ONLY when before(:context) is required
98
+ RSpec.describe "Expensive shared setup" do
99
+ before(:context) do
100
+ # This is the ONE exception where instance variables are acceptable
101
+ # because let is not supported in before(:context)
102
+ @shared_resource = ExpensiveResource.create
103
+ end
104
+
105
+ after(:context) do
106
+ @shared_resource.cleanup
107
+ end
108
+
109
+ it "uses shared resource" do
110
+ expect(@shared_resource).to be_ready
111
+ end
112
+
113
+ it "reuses same resource" do
114
+ expect(@shared_resource).to be_ready
115
+ end
116
+ end
117
+
118
+ # Preferred alternative: use let! with memoization at context level
119
+ RSpec.describe "Preferred shared setup" do
120
+ # If possible, restructure to avoid before(:context)
121
+ let!(:resource) { create(:expensive_resource) }
122
+
123
+ it "uses resource" do
124
+ expect(resource).to be_ready
125
+ end
126
+ end