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,154 @@
1
+ # RSpec Matchers: Collection Examples
2
+ # Source: rspec-expectations gem features/built_in_matchers/include.feature,
3
+ # contain_exactly.feature, all.feature, have_attributes.feature
4
+
5
+ # include - partial matching
6
+ RSpec.describe "include matcher" do
7
+ describe "with arrays" do
8
+ it "checks single element" do
9
+ expect([1, 2, 3]).to include(1)
10
+ end
11
+
12
+ it "checks multiple elements" do
13
+ expect([1, 2, 3]).to include(1, 2)
14
+ expect([1, 2, 3]).to include(1, 2, 3)
15
+ end
16
+
17
+ it "works with composed matchers" do
18
+ expect([1, 3, 7]).to include(a_kind_of(Integer))
19
+ expect([1, 3, 7]).to include(be_odd.and be < 10)
20
+ end
21
+ end
22
+
23
+ describe "with strings" do
24
+ it "checks substring" do
25
+ expect("hello world").to include("world")
26
+ expect("hello world").to include("hello", "world")
27
+ end
28
+
29
+ it "works with regex" do
30
+ expect("hello world").to include(/wor.d/)
31
+ end
32
+ end
33
+
34
+ describe "with hashes" do
35
+ let(:hash) { { a: 1, b: 2, c: 3 } }
36
+
37
+ it "checks key existence" do
38
+ expect(hash).to include(:a)
39
+ expect(hash).to include(:a, :b)
40
+ end
41
+
42
+ it "checks key-value pairs" do
43
+ expect(hash).to include(a: 1)
44
+ expect(hash).to include(a: 1, b: 2)
45
+ end
46
+ end
47
+
48
+ describe "with counts" do
49
+ let(:items) { [{ type: :a }, { type: :b }, { type: :a }] }
50
+
51
+ it "specifies occurrence count" do
52
+ expect(items).to include(have_key(:type)).exactly(3).times
53
+ expect(items).to include(type: :a).twice
54
+ expect(items).to include(type: :b).once
55
+ end
56
+ end
57
+ end
58
+
59
+ # contain_exactly / match_array - order-independent full match
60
+ RSpec.describe "contain_exactly matcher" do
61
+ it "matches regardless of order" do
62
+ expect([1, 2, 3]).to contain_exactly(3, 2, 1)
63
+ expect([1, 2, 3]).to contain_exactly(2, 3, 1)
64
+ end
65
+
66
+ it "requires all elements present" do
67
+ expect([1, 2, 3]).not_to contain_exactly(1, 2) # Missing 3
68
+ expect([1, 2, 3]).not_to contain_exactly(1, 2, 3, 4) # Extra 4
69
+ end
70
+
71
+ it "works with composed matchers" do
72
+ expect(["barn", 2.45]).to contain_exactly(
73
+ a_value_within(0.1).of(2.5),
74
+ a_string_starting_with("bar")
75
+ )
76
+ end
77
+
78
+ it "has match_array alias" do
79
+ expect([1, 2, 3]).to match_array([3, 2, 1])
80
+ end
81
+ end
82
+
83
+ # start_with / end_with
84
+ RSpec.describe "start_with/end_with matchers" do
85
+ describe "with strings" do
86
+ it "checks prefix/suffix" do
87
+ expect("hello world").to start_with("hello")
88
+ expect("hello world").to end_with("world")
89
+ end
90
+ end
91
+
92
+ describe "with arrays" do
93
+ it "checks first/last elements" do
94
+ expect([0, 1, 2, 3]).to start_with(0)
95
+ expect([0, 1, 2, 3]).to start_with(0, 1)
96
+ expect([0, 1, 2, 3]).to end_with(3)
97
+ expect([0, 1, 2, 3]).to end_with(2, 3)
98
+ end
99
+ end
100
+ end
101
+
102
+ # all - every element matches
103
+ RSpec.describe "all matcher" do
104
+ it "requires all elements to match" do
105
+ expect([1, 3, 5]).to all(be_odd)
106
+ expect([1, 3, 5]).to all(be_an(Integer))
107
+ expect([1, 3, 5]).to all(be < 10)
108
+ end
109
+
110
+ it "works with compound matchers" do
111
+ expect([1, 3, 5]).to all(be_odd.and be_an(Integer))
112
+ expect([1, 4, 21]).to all(be_odd.or be < 10)
113
+ end
114
+
115
+ it "provides clear failure messages" do
116
+ # When one element fails, message shows which
117
+ expect(["foo", "bar", "baz"]).to all(be_a(String).and include("a"))
118
+ end
119
+ end
120
+
121
+ # have_attributes - object attribute matching
122
+ RSpec.describe "have_attributes matcher" do
123
+ subject(:user) { build(:user, name: "Alice", age: 25) }
124
+
125
+ it "checks single attribute" do
126
+ expect(user).to have_attributes(name: "Alice")
127
+ end
128
+
129
+ it "checks multiple attributes" do
130
+ expect(user).to have_attributes(name: "Alice", age: 25)
131
+ end
132
+
133
+ it "works with composed matchers" do
134
+ expect(user).to have_attributes(name: a_string_starting_with("A"))
135
+ expect(user).to have_attributes(age: a_value > 18)
136
+ end
137
+ end
138
+
139
+ # Practical example: testing query results
140
+ RSpec.describe User, ".active scope" do
141
+ let!(:active_users) { create_list(:user, 3, :active) }
142
+ let!(:inactive_user) { create(:user, :inactive) }
143
+
144
+ subject(:results) { described_class.active }
145
+
146
+ it "returns only active users" do
147
+ expect(results).to contain_exactly(*active_users)
148
+ expect(results).not_to include(inactive_user)
149
+ end
150
+
151
+ it "returns users with expected attributes" do
152
+ expect(results).to all(have_attributes(status: "active"))
153
+ end
154
+ end
@@ -0,0 +1,79 @@
1
+ # RSpec Matchers: Comparison Examples
2
+ # Source: rspec-expectations gem features/built_in_matchers/comparisons.feature, be_within.feature
3
+
4
+ # Operator comparisons
5
+ RSpec.describe "comparison operators" do
6
+ it "supports numeric comparisons" do
7
+ expect(18).to be > 15
8
+ expect(18).to be >= 17
9
+ expect(18).to be <= 19
10
+ expect(18).to be < 20
11
+ end
12
+
13
+ it "supports string comparisons (alphabetical)" do
14
+ expect("Strawberry").to be < "Tomato"
15
+ expect("Strawberry").to be > "Apple"
16
+ end
17
+ end
18
+
19
+ # be_within - floating point tolerance
20
+ RSpec.describe "be_within matcher" do
21
+ it "handles floating point precision" do
22
+ radius = 3
23
+ area = radius * radius * Math::PI
24
+
25
+ expect(area).to be_within(0.1).of(28.3)
26
+ end
27
+
28
+ it "specifies acceptable delta" do
29
+ expect(27.5).to be_within(0.5).of(27.9)
30
+ expect(27.5).to be_within(0.5).of(28.0)
31
+ expect(27.5).to be_within(0.5).of(27.0)
32
+ end
33
+
34
+ # Works with Time objects too
35
+ it "compares times with tolerance" do
36
+ now = Time.now
37
+ expect(now).to be_within(1.second).of(Time.now)
38
+ end
39
+ end
40
+
41
+ # be_between - range checking
42
+ RSpec.describe "be_between matcher" do
43
+ it "defaults to inclusive" do
44
+ expect(5).to be_between(1, 10)
45
+ expect(1).to be_between(1, 10) # Inclusive: includes 1
46
+ expect(10).to be_between(1, 10) # Inclusive: includes 10
47
+ end
48
+
49
+ it "supports explicit inclusive" do
50
+ expect(5).to be_between(1, 10).inclusive
51
+ end
52
+
53
+ it "supports exclusive ranges" do
54
+ expect(5).to be_between(1, 10).exclusive
55
+ expect(1).not_to be_between(1, 10).exclusive # Excludes boundaries
56
+ expect(10).not_to be_between(1, 10).exclusive
57
+ end
58
+ end
59
+
60
+ # Practical example
61
+ RSpec.describe Order do
62
+ subject(:order) { build(:order, total: 150) }
63
+
64
+ describe "#shipping_rate" do
65
+ context "with standard order" do
66
+ it "returns rate within expected range" do
67
+ expect(order.shipping_rate).to be_between(5.0, 25.0)
68
+ end
69
+ end
70
+
71
+ context "with heavy order" do
72
+ subject(:order) { build(:order, :heavy, total: 500) }
73
+
74
+ it "returns higher rate" do
75
+ expect(order.shipping_rate).to be > 20.0
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,155 @@
1
+ # RSpec Matchers: Composing Matchers Examples
2
+ # Source: rspec-expectations gem features/composing_matchers.feature,
3
+ # compound_expectations.feature
4
+
5
+ # Compound expectations with .and / &
6
+ RSpec.describe "compound matchers with .and" do
7
+ it "combines matchers with .and" do
8
+ expect("hello world").to start_with("hello").and end_with("world")
9
+ end
10
+
11
+ it "uses & as alias" do
12
+ expect("hello world").to start_with("hello") & end_with("world")
13
+ end
14
+
15
+ it "chains multiple conditions" do
16
+ expect(5).to be_positive.and be < 10
17
+ expect([1, 2, 3]).to include(1).and include(3)
18
+ end
19
+ end
20
+
21
+ # Compound expectations with .or / |
22
+ RSpec.describe "compound matchers with .or" do
23
+ it "passes if any matcher passes" do
24
+ color = %w[red green blue].sample
25
+ expect(color).to eq("red").or eq("green").or eq("blue")
26
+ end
27
+
28
+ it "uses | as alias" do
29
+ value = [true, false].sample
30
+ expect(value).to be(true) | be(false)
31
+ end
32
+ end
33
+
34
+ # Matcher aliases for composition
35
+ RSpec.describe "composable matcher aliases" do
36
+ # Noun-phrase aliases read better in composed contexts:
37
+ # be_within => a_value_within
38
+ # be_an_instance => an_instance_of
39
+ # start_with => a_string_starting_with
40
+ # include => a_collection_including
41
+ # be_a => a_kind_of
42
+
43
+ it "uses noun-phrase aliases in expect(...).to include" do
44
+ expect([1, 2.5, "foo"]).to include(
45
+ an_instance_of(Integer),
46
+ a_value_within(0.1).of(2.5),
47
+ a_string_starting_with("f")
48
+ )
49
+ end
50
+
51
+ it "uses noun-phrase aliases with contain_exactly" do
52
+ expect(["barn", 2.45]).to contain_exactly(
53
+ a_value_within(0.1).of(2.5),
54
+ a_string_starting_with("bar")
55
+ )
56
+ end
57
+ end
58
+
59
+ # Matchers as arguments
60
+ RSpec.describe "matchers accepting matcher arguments" do
61
+ describe "with change" do
62
+ it "uses composed matchers for from/to" do
63
+ text = "foo bar"
64
+ expect { text = "baz qux" }
65
+ .to change { text }
66
+ .from(a_string_matching(/foo/))
67
+ .to(a_string_matching(/baz/))
68
+ end
69
+
70
+ it "uses composed matchers for delta" do
71
+ value = 0.0
72
+ expect { value += 1.05 }
73
+ .to change { value }
74
+ .by(a_value_within(0.1).of(1.0))
75
+ end
76
+ end
77
+
78
+ describe "with include" do
79
+ it "matches hash values with matchers" do
80
+ expect(a: "food", b: "good").to include(a: a_string_matching(/foo/))
81
+ end
82
+
83
+ it "matches hash keys with matchers" do
84
+ expect("food" => 1, "drink" => 2).to include(a_string_matching(/foo/))
85
+ end
86
+ end
87
+
88
+ describe "with match (nested structures)" do
89
+ it "validates deeply nested data" do
90
+ response = {
91
+ user: {
92
+ name: "Alice",
93
+ roles: ["admin", "editor"],
94
+ settings: { theme: "dark" }
95
+ }
96
+ }
97
+
98
+ expect(response).to match(
99
+ user: {
100
+ name: a_string_starting_with("A"),
101
+ roles: a_collection_including("admin"),
102
+ settings: { theme: a_kind_of(String) }
103
+ }
104
+ )
105
+ end
106
+ end
107
+ end
108
+
109
+ # Practical examples
110
+ RSpec.describe API::Response do
111
+ subject(:response) { build(:api_response, :success) }
112
+
113
+ describe "response structure validation" do
114
+ it "has expected structure" do
115
+ expect(response.body).to match(
116
+ status: "success",
117
+ data: {
118
+ id: an_instance_of(Integer),
119
+ created_at: a_string_matching(/\d{4}-\d{2}-\d{2}/),
120
+ items: a_collection_including(
121
+ have_attributes(name: a_kind_of(String))
122
+ )
123
+ }
124
+ )
125
+ end
126
+ end
127
+ end
128
+
129
+ RSpec.describe User do
130
+ describe "#update" do
131
+ subject(:user) { create(:user, name: "Alice", updated_at: 1.day.ago) }
132
+
133
+ it "changes name and updated_at" do
134
+ expect { user.update(name: "Bob") }
135
+ .to change(user, :name)
136
+ .from(a_string_starting_with("A"))
137
+ .to(a_string_starting_with("B"))
138
+ .and change(user, :updated_at)
139
+ end
140
+ end
141
+ end
142
+
143
+ RSpec.describe "result validation" do
144
+ subject(:result) { build(:calculation_result) }
145
+
146
+ it "returns numeric value in expected range" do
147
+ expect(result.value)
148
+ .to be_a(Numeric)
149
+ .and be_within(0.01).of(expected_value)
150
+ end
151
+
152
+ it "has valid status" do
153
+ expect(result.status).to eq(:success).or eq(:partial)
154
+ end
155
+ end
@@ -0,0 +1,197 @@
1
+ # RSpec Matchers: Custom Matcher Examples
2
+ # Source: rspec-expectations gem features/custom_matchers/define_matcher.feature
3
+
4
+ # Basic custom matcher using DSL
5
+ RSpec::Matchers.define :be_a_multiple_of do |expected|
6
+ match do |actual|
7
+ actual % expected == 0
8
+ end
9
+ end
10
+
11
+ RSpec.describe "be_a_multiple_of matcher" do
12
+ it "passes for multiples" do
13
+ expect(9).to be_a_multiple_of(3)
14
+ expect(12).to be_a_multiple_of(4)
15
+ end
16
+
17
+ it "fails for non-multiples" do
18
+ expect(9).not_to be_a_multiple_of(4)
19
+ end
20
+ end
21
+
22
+ # Custom matcher with messages and description
23
+ RSpec::Matchers.define :be_in_range do |min, max|
24
+ match do |actual|
25
+ actual >= min && actual <= max
26
+ end
27
+
28
+ failure_message do |actual|
29
+ "expected #{actual} to be between #{min} and #{max}"
30
+ end
31
+
32
+ failure_message_when_negated do |actual|
33
+ "expected #{actual} not to be between #{min} and #{max}"
34
+ end
35
+
36
+ description do
37
+ "be in range #{min}..#{max}"
38
+ end
39
+ end
40
+
41
+ RSpec.describe "be_in_range matcher" do
42
+ it "passes for values in range" do
43
+ expect(5).to be_in_range(1, 10)
44
+ end
45
+
46
+ it "provides readable description" do
47
+ # In output: "should be in range 1..10"
48
+ expect(5).to be_in_range(1, 10)
49
+ end
50
+ end
51
+
52
+ # Custom matcher with chaining
53
+ RSpec::Matchers.define :have_errors_on do |attribute|
54
+ chain :with_message do |message|
55
+ @expected_message = message
56
+ end
57
+
58
+ match do |model|
59
+ model.valid?
60
+ errors = model.errors[attribute]
61
+
62
+ if @expected_message
63
+ errors.include?(@expected_message)
64
+ else
65
+ errors.any?
66
+ end
67
+ end
68
+
69
+ failure_message do |model|
70
+ if @expected_message
71
+ "expected #{model.class} to have error '#{@expected_message}' on #{attribute}, " \
72
+ "but got: #{model.errors[attribute].inspect}"
73
+ else
74
+ "expected #{model.class} to have errors on #{attribute}"
75
+ end
76
+ end
77
+ end
78
+
79
+ RSpec.describe "have_errors_on matcher with chaining" do
80
+ subject(:user) { build(:user, email: nil) }
81
+
82
+ it "checks for any error on attribute" do
83
+ expect(user).to have_errors_on(:email)
84
+ end
85
+
86
+ it "checks for specific error message" do
87
+ expect(user).to have_errors_on(:email).with_message("can't be blank")
88
+ end
89
+ end
90
+
91
+ # Custom matcher with diffable output
92
+ RSpec::Matchers.define :match_json_structure do |expected|
93
+ diffable # Enables diff output on failure
94
+
95
+ match do |actual|
96
+ @actual_parsed = JSON.parse(actual)
97
+ structure_matches?(expected, @actual_parsed)
98
+ end
99
+
100
+ def structure_matches?(expected, actual)
101
+ case expected
102
+ when Hash
103
+ expected.all? { |k, v| actual.key?(k.to_s) && structure_matches?(v, actual[k.to_s]) }
104
+ when Array
105
+ expected.all? { |v| actual.any? { |a| structure_matches?(v, a) } }
106
+ when Class
107
+ actual.is_a?(expected)
108
+ else
109
+ true
110
+ end
111
+ end
112
+ end
113
+
114
+ # Custom matcher supporting block expectations
115
+ RSpec::Matchers.define :complete_within do |timeout|
116
+ supports_block_expectations # Required for expect { }
117
+
118
+ match do |block|
119
+ started = Time.now
120
+ block.call
121
+ Time.now - started < timeout
122
+ end
123
+
124
+ failure_message do
125
+ "expected block to complete within #{timeout} seconds"
126
+ end
127
+ end
128
+
129
+ RSpec.describe "complete_within matcher" do
130
+ it "passes for fast operations" do
131
+ expect { 1 + 1 }.to complete_within(1.second)
132
+ end
133
+ end
134
+
135
+ # Custom class-based matcher (from scratch)
136
+ class BeValidJSON
137
+ include RSpec::Matchers::Composable
138
+
139
+ def matches?(actual)
140
+ @actual = actual
141
+ JSON.parse(actual)
142
+ true
143
+ rescue JSON::ParserError => e
144
+ @error = e
145
+ false
146
+ end
147
+
148
+ def failure_message
149
+ "expected valid JSON, got parse error: #{@error.message}"
150
+ end
151
+
152
+ def failure_message_when_negated
153
+ "expected invalid JSON, but it parsed successfully"
154
+ end
155
+
156
+ def description
157
+ "be valid JSON"
158
+ end
159
+ end
160
+
161
+ def be_valid_json
162
+ BeValidJSON.new
163
+ end
164
+
165
+ RSpec.describe "be_valid_json matcher" do
166
+ it "passes for valid JSON" do
167
+ expect('{"key": "value"}').to be_valid_json
168
+ end
169
+
170
+ it "fails for invalid JSON" do
171
+ expect("{invalid}").not_to be_valid_json
172
+ end
173
+ end
174
+
175
+ # Negated matcher using define_negated_matcher
176
+ RSpec::Matchers.define_negated_matcher :exclude, :include
177
+
178
+ RSpec.describe "negated matcher" do
179
+ it "reads more naturally" do
180
+ expect([1, 2, 3]).to exclude(4)
181
+ expect("hello").to exclude("xyz")
182
+ end
183
+ end
184
+
185
+ # Aliased matcher
186
+ RSpec::Matchers.alias_matcher :a_user_with, :have_attributes
187
+
188
+ RSpec.describe "aliased matcher" do
189
+ it "provides domain-specific language" do
190
+ users = [
191
+ build(:user, name: "Alice", role: :admin),
192
+ build(:user, name: "Bob", role: :member)
193
+ ]
194
+
195
+ expect(users).to include(a_user_with(name: "Alice", role: :admin))
196
+ end
197
+ end
@@ -0,0 +1,58 @@
1
+ # RSpec Matchers: Equality Examples
2
+ # Source: rspec-expectations gem features/built_in_matchers/equality.feature
3
+
4
+ # eq - value equality using ==
5
+ RSpec.describe "eq matcher" do
6
+ it "compares using ==" do
7
+ expect(5).to eq(5)
8
+ expect("string").to eq("string")
9
+ end
10
+
11
+ it "allows type coercion" do
12
+ expect(5).to eq(5.0) # Integer == Float
13
+ end
14
+
15
+ it "fails for different values" do
16
+ expect(5).not_to eq(6)
17
+ end
18
+ end
19
+
20
+ # eql - value equivalence using eql? (type-sensitive)
21
+ RSpec.describe "eql matcher" do
22
+ it "compares using eql?" do
23
+ expect(5).to eql(5)
24
+ end
25
+
26
+ it "is type-sensitive" do
27
+ expect(5).not_to eql(5.0) # Integer.eql?(Float) => false
28
+ end
29
+ end
30
+
31
+ # equal/be - object identity using equal?
32
+ RSpec.describe "equal/be matcher" do
33
+ let(:string) { "hello" }
34
+
35
+ it "passes for same object" do
36
+ expect(string).to equal(string)
37
+ expect(string).to be(string) # be is alias for equal
38
+ end
39
+
40
+ it "fails for different objects with same value" do
41
+ expect("hello").not_to equal("hello") # Different String objects
42
+ expect("hello").not_to be("hello")
43
+ end
44
+ end
45
+
46
+ # Practical example: memoization verification
47
+ RSpec.describe User do
48
+ subject(:user) { build(:user) }
49
+
50
+ describe "#full_name" do
51
+ it "memoizes result" do
52
+ first_call = user.full_name
53
+ second_call = user.full_name
54
+
55
+ expect(first_call).to be(second_call) # Same object returned
56
+ end
57
+ end
58
+ end