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,228 @@
1
+ # RSpec Rails: Mailer Specs Examples
2
+ # Source: rspec-rails gem features/mailer_specs/
3
+
4
+ # Mailer specs test ActionMailer classes.
5
+ # Location: spec/mailers/
6
+
7
+ # Basic mailer spec
8
+ RSpec.describe NotificationsMailer, type: :mailer do
9
+ describe "#signup" do
10
+ let(:mail) { NotificationsMailer.signup }
11
+
12
+ it "renders the headers" do
13
+ expect(mail.subject).to eq("Signup")
14
+ expect(mail.to).to eq(["to@example.org"])
15
+ expect(mail.from).to eq(["from@example.com"])
16
+ end
17
+
18
+ it "renders the body" do
19
+ expect(mail.body.encoded).to include("Hi")
20
+ end
21
+ end
22
+ end
23
+
24
+ # Testing with dynamic recipient
25
+ RSpec.describe WelcomeMailer, type: :mailer do
26
+ describe "#welcome" do
27
+ let(:user) { create(:user, email: "john@example.com", name: "John") }
28
+ let(:mail) { WelcomeMailer.welcome(user) }
29
+
30
+ it "sends to the user email" do
31
+ expect(mail.to).to eq(["john@example.com"])
32
+ end
33
+
34
+ it "personalizes the greeting" do
35
+ expect(mail.body.encoded).to include("Hello John")
36
+ end
37
+
38
+ it "includes welcome link" do
39
+ expect(mail.body.encoded).to include(dashboard_url)
40
+ end
41
+ end
42
+ end
43
+
44
+ # Testing multipart emails (HTML and text)
45
+ RSpec.describe NewsletterMailer, type: :mailer do
46
+ describe "#weekly_digest" do
47
+ let(:user) { create(:user) }
48
+ let(:mail) { NewsletterMailer.weekly_digest(user) }
49
+
50
+ it "has both HTML and text parts" do
51
+ expect(mail.parts.length).to eq(2)
52
+ expect(mail.parts.map(&:content_type)).to include(
53
+ a_string_matching(/text\/plain/),
54
+ a_string_matching(/text\/html/)
55
+ )
56
+ end
57
+
58
+ describe "HTML part" do
59
+ subject(:html_body) { mail.html_part.body.encoded }
60
+
61
+ it "includes styled content" do
62
+ expect(html_body).to include("<h1>")
63
+ expect(html_body).to include("Weekly Digest")
64
+ end
65
+ end
66
+
67
+ describe "text part" do
68
+ subject(:text_body) { mail.text_part.body.encoded }
69
+
70
+ it "includes plain text content" do
71
+ expect(text_body).to include("Weekly Digest")
72
+ expect(text_body).not_to include("<h1>")
73
+ end
74
+ end
75
+ end
76
+ end
77
+
78
+ # Testing email with attachments
79
+ RSpec.describe ReportMailer, type: :mailer do
80
+ describe "#monthly_report" do
81
+ let(:mail) { ReportMailer.monthly_report }
82
+
83
+ it "includes PDF attachment" do
84
+ expect(mail.attachments.length).to eq(1)
85
+ expect(mail.attachments.first.filename).to eq("report.pdf")
86
+ expect(mail.attachments.first.content_type).to start_with("application/pdf")
87
+ end
88
+ end
89
+ end
90
+
91
+ # Testing delivery
92
+ RSpec.describe OrderMailer, type: :mailer do
93
+ describe "#confirmation" do
94
+ let(:order) { create(:order) }
95
+ let(:mail) { OrderMailer.confirmation(order) }
96
+
97
+ it "delivers email" do
98
+ expect { mail.deliver_now }.to change { ActionMailer::Base.deliveries.count }.by(1)
99
+ end
100
+
101
+ it "queues email for later delivery" do
102
+ expect { mail.deliver_later }.to have_enqueued_job(ActionMailer::MailDeliveryJob)
103
+ end
104
+ end
105
+ end
106
+
107
+ # send_email matcher (rspec-rails 7.0+)
108
+ RSpec.describe NotificationsMailer, type: :mailer do
109
+ describe "#alert" do
110
+ let(:user) { create(:user, email: "user@example.com") }
111
+
112
+ it "sends email with correct attributes" do
113
+ expect {
114
+ NotificationsMailer.alert(user).deliver_now
115
+ }.to send_email(
116
+ from: "alerts@example.com",
117
+ to: "user@example.com",
118
+ subject: "Alert Notification"
119
+ )
120
+ end
121
+ end
122
+ end
123
+
124
+ # Testing email previews exist (meta-test)
125
+ RSpec.describe "Mailer Previews" do
126
+ it "has preview for WelcomeMailer" do
127
+ expect(defined?(WelcomeMailerPreview)).to be_truthy
128
+ end
129
+ end
130
+
131
+ # Testing with parameterized mailers (Rails 5.1+)
132
+ RSpec.describe NotificationsMailer, type: :mailer do
133
+ describe "#with parameters" do
134
+ let(:user) { create(:user) }
135
+ let(:mail) do
136
+ NotificationsMailer.with(user:, urgency: :high).important_update
137
+ end
138
+
139
+ it "uses parameterized values" do
140
+ expect(mail.to).to eq([user.email])
141
+ expect(mail.subject).to include("[URGENT]")
142
+ end
143
+ end
144
+ end
145
+
146
+ # Testing email headers
147
+ RSpec.describe TransactionalMailer, type: :mailer do
148
+ describe "#receipt" do
149
+ let(:order) { create(:order) }
150
+ let(:mail) { TransactionalMailer.receipt(order) }
151
+
152
+ it "sets reply-to header" do
153
+ expect(mail.reply_to).to eq(["support@example.com"])
154
+ end
155
+
156
+ it "sets custom headers" do
157
+ expect(mail.headers["X-Transaction-ID"]).to eq(order.id.to_s)
158
+ end
159
+
160
+ it "sets high priority" do
161
+ expect(mail.headers["X-Priority"]).to eq("1")
162
+ end
163
+ end
164
+ end
165
+
166
+ # Testing mailer with conditional content
167
+ RSpec.describe UserMailer, type: :mailer do
168
+ describe "#password_reset" do
169
+ let(:user) { create(:user) }
170
+ let(:mail) { UserMailer.password_reset(user) }
171
+
172
+ context "when user has two-factor enabled" do
173
+ let(:user) { create(:user, :with_two_factor) }
174
+
175
+ it "includes 2FA reminder" do
176
+ expect(mail.body.encoded).to include("two-factor authentication")
177
+ end
178
+ end
179
+
180
+ context "when user does not have two-factor" do
181
+ it "does not mention 2FA" do
182
+ expect(mail.body.encoded).not_to include("two-factor authentication")
183
+ end
184
+ end
185
+ end
186
+ end
187
+
188
+ # Testing mailer callbacks
189
+ RSpec.describe AuditableMailer, type: :mailer do
190
+ describe "after_action callback" do
191
+ let(:mail) { AuditableMailer.system_notification }
192
+
193
+ it "logs email delivery" do
194
+ expect(EmailLog).to receive(:create!).with(
195
+ mailer: "AuditableMailer",
196
+ action: "system_notification"
197
+ )
198
+
199
+ mail.deliver_now
200
+ end
201
+ end
202
+ end
203
+
204
+ # Clearing deliveries between examples
205
+ RSpec.describe "Email delivery", type: :mailer do
206
+ before { ActionMailer::Base.deliveries.clear }
207
+
208
+ it "starts with empty deliveries" do
209
+ expect(ActionMailer::Base.deliveries).to be_empty
210
+ end
211
+
212
+ it "tracks delivered emails" do
213
+ NotificationsMailer.signup.deliver_now
214
+ expect(ActionMailer::Base.deliveries.count).to eq(1)
215
+ end
216
+ end
217
+
218
+ # Testing I18n in emails
219
+ RSpec.describe LocalizedMailer, type: :mailer do
220
+ describe "#welcome" do
221
+ let(:user) { create(:user, locale: "es") }
222
+ let(:mail) { LocalizedMailer.welcome(user) }
223
+
224
+ it "uses user locale" do
225
+ expect(mail.subject).to eq("Bienvenido")
226
+ end
227
+ end
228
+ end
@@ -0,0 +1,374 @@
1
+ # RSpec Rails: Rails-Specific Matchers Examples
2
+ # Source: rspec-rails gem features/matchers/
3
+
4
+ # Rails-specific matchers for testing HTTP responses,
5
+ # redirects, templates, and more.
6
+
7
+ # have_http_status - testing response codes
8
+ RSpec.describe "have_http_status", type: :request do
9
+ describe "numeric status codes" do
10
+ it "matches exact code" do
11
+ get "/widgets"
12
+ expect(response).to have_http_status(200)
13
+ end
14
+
15
+ it "matches created status" do
16
+ post "/widgets", params: { widget: { name: "New" } }
17
+ expect(response).to have_http_status(201)
18
+ end
19
+ end
20
+
21
+ describe "symbolic status names" do
22
+ it "matches :ok" do
23
+ get "/widgets"
24
+ expect(response).to have_http_status(:ok)
25
+ end
26
+
27
+ it "matches :created" do
28
+ post "/api/widgets", params: { widget: { name: "New" } },
29
+ headers: { "ACCEPT" => "application/json" }
30
+ expect(response).to have_http_status(:created)
31
+ end
32
+
33
+ it "matches :not_found" do
34
+ get "/widgets/nonexistent"
35
+ expect(response).to have_http_status(:not_found)
36
+ end
37
+
38
+ it "matches :unprocessable_entity" do
39
+ post "/widgets", params: { widget: { name: "" } }
40
+ expect(response).to have_http_status(:unprocessable_entity)
41
+ end
42
+
43
+ it "matches :unauthorized" do
44
+ get "/admin/dashboard"
45
+ expect(response).to have_http_status(:unauthorized)
46
+ end
47
+ end
48
+
49
+ describe "status type matchers" do
50
+ it "matches :success (any 2xx)" do
51
+ get "/widgets"
52
+ expect(response).to have_http_status(:success)
53
+ end
54
+
55
+ it "matches :redirect (any 3xx)" do
56
+ post "/widgets", params: { widget: { name: "New" } }
57
+ expect(response).to have_http_status(:redirect)
58
+ end
59
+
60
+ it "matches :error (any 5xx)" do
61
+ allow_any_instance_of(WidgetsController).to receive(:index).and_raise(StandardError)
62
+ get "/widgets"
63
+ expect(response).to have_http_status(:error)
64
+ end
65
+
66
+ it "matches :missing (404)" do
67
+ get "/nonexistent"
68
+ expect(response).to have_http_status(:missing)
69
+ end
70
+ end
71
+ end
72
+
73
+ # redirect_to - testing redirects
74
+ RSpec.describe "redirect_to", type: :controller do
75
+ controller WidgetsController do
76
+ def create
77
+ @widget = Widget.create!(params.require(:widget).permit(:name))
78
+ redirect_to @widget
79
+ end
80
+ end
81
+
82
+ describe "POST #create" do
83
+ let(:valid_params) { { widget: { name: "Test" } } }
84
+
85
+ it "redirects to URL" do
86
+ post :create, params: valid_params
87
+ expect(response).to redirect_to(widget_url(assigns(:widget)))
88
+ end
89
+
90
+ it "redirects to path" do
91
+ post :create, params: valid_params
92
+ expect(response).to redirect_to("/widgets/#{assigns(:widget).id}")
93
+ end
94
+
95
+ it "redirects to hash" do
96
+ post :create, params: valid_params
97
+ expect(response).to redirect_to(action: :show, id: assigns(:widget).id)
98
+ end
99
+
100
+ it "redirects to record" do
101
+ post :create, params: valid_params
102
+ expect(response).to redirect_to(assigns(:widget))
103
+ end
104
+ end
105
+ end
106
+
107
+ # render_template - testing template rendering
108
+ RSpec.describe "render_template", type: :controller do
109
+ controller WidgetsController do
110
+ end
111
+
112
+ describe "GET #index" do
113
+ it "renders index template" do
114
+ get :index
115
+ expect(response).to render_template(:index)
116
+ end
117
+
118
+ it "renders with full path" do
119
+ get :index
120
+ expect(response).to render_template("widgets/index")
121
+ end
122
+
123
+ it "does not render other templates" do
124
+ get :index
125
+ expect(response).not_to render_template("widgets/show")
126
+ end
127
+ end
128
+
129
+ describe "layout rendering" do
130
+ it "renders with application layout" do
131
+ get :index
132
+ expect(response).to render_template("layouts/application")
133
+ end
134
+
135
+ it "does not render admin layout" do
136
+ get :index
137
+ expect(response).not_to render_template("layouts/admin")
138
+ end
139
+ end
140
+
141
+ describe "partial rendering" do
142
+ render_views
143
+
144
+ it "renders partial" do
145
+ create_list(:widget, 2)
146
+ get :index
147
+ expect(response).to render_template(partial: "_widget")
148
+ end
149
+ end
150
+ end
151
+
152
+ # route_to - testing routes
153
+ RSpec.describe "route_to", type: :routing do
154
+ describe "shortcut syntax" do
155
+ it "routes with controller#action" do
156
+ expect(get: "/widgets").to route_to("widgets#index")
157
+ end
158
+
159
+ it "routes with id" do
160
+ expect(get: "/widgets/1").to route_to("widgets#show", id: "1")
161
+ end
162
+ end
163
+
164
+ describe "hash syntax" do
165
+ it "routes with full hash" do
166
+ expect(get: "/widgets").to route_to(
167
+ controller: "widgets",
168
+ action: "index"
169
+ )
170
+ end
171
+
172
+ it "routes with params" do
173
+ expect(get: "/widgets/1").to route_to(
174
+ controller: "widgets",
175
+ action: "show",
176
+ id: "1"
177
+ )
178
+ end
179
+ end
180
+
181
+ describe "negative matching" do
182
+ it "does not route nonexistent paths" do
183
+ expect(get: "/nonexistent").not_to route_to("widgets#index")
184
+ end
185
+ end
186
+ end
187
+
188
+ # be_routable - testing route existence
189
+ RSpec.describe "be_routable", type: :routing do
190
+ describe "routable paths" do
191
+ it "matches existing routes" do
192
+ expect(get: "/widgets").to be_routable
193
+ expect(post: "/widgets").to be_routable
194
+ expect(get: "/widgets/1").to be_routable
195
+ end
196
+ end
197
+
198
+ describe "non-routable paths" do
199
+ it "does not match nonexistent routes" do
200
+ expect(put: "/widgets").not_to be_routable
201
+ expect(delete: "/admin/destroy_all").not_to be_routable
202
+ end
203
+ end
204
+ end
205
+
206
+ # be_a_new - testing new records
207
+ RSpec.describe "be_a_new", type: :controller do
208
+ controller WidgetsController do
209
+ def new
210
+ @widget = Widget.new
211
+ end
212
+ end
213
+
214
+ describe "GET #new" do
215
+ it "assigns a new widget" do
216
+ get :new
217
+ expect(assigns(:widget)).to be_a_new(Widget)
218
+ end
219
+ end
220
+
221
+ describe "after save" do
222
+ let(:widget) { Widget.create!(name: "Saved") }
223
+
224
+ it "is not a new widget" do
225
+ expect(widget).not_to be_a_new(Widget)
226
+ end
227
+ end
228
+
229
+ describe "with attributes" do
230
+ it "matches with expected attributes" do
231
+ widget = Widget.new(name: "Test")
232
+ expect(widget).to be_a_new(Widget).with(name: "Test")
233
+ end
234
+ end
235
+ end
236
+
237
+ # be_valid - testing model validity
238
+ RSpec.describe "be_valid", type: :model do
239
+ describe Widget do
240
+ it "is valid with valid attributes" do
241
+ widget = Widget.new(name: "Valid Widget")
242
+ expect(widget).to be_valid
243
+ end
244
+
245
+ it "is not valid without required attributes" do
246
+ widget = Widget.new(name: nil)
247
+ expect(widget).not_to be_valid
248
+ end
249
+
250
+ it "validates with context" do
251
+ widget = Widget.new(name: "Test")
252
+ expect(widget).to be_valid(:create)
253
+ end
254
+ end
255
+ end
256
+
257
+ # have_enqueued_job - testing job enqueuing
258
+ RSpec.describe "have_enqueued_job", type: :job do
259
+ describe "block form" do
260
+ it "matches enqueued job" do
261
+ expect {
262
+ ProcessJob.perform_later("data")
263
+ }.to have_enqueued_job
264
+ end
265
+
266
+ it "matches specific job class" do
267
+ expect {
268
+ ProcessJob.perform_later("data")
269
+ }.to have_enqueued_job(ProcessJob)
270
+ end
271
+
272
+ it "matches with arguments" do
273
+ expect {
274
+ ProcessJob.perform_later("data", 123)
275
+ }.to have_enqueued_job.with("data", 123)
276
+ end
277
+
278
+ it "matches on queue" do
279
+ expect {
280
+ ProcessJob.perform_later
281
+ }.to have_enqueued_job.on_queue("default")
282
+ end
283
+
284
+ it "matches at time" do
285
+ scheduled_time = 1.hour.from_now
286
+ expect {
287
+ ProcessJob.set(wait_until: scheduled_time).perform_later
288
+ }.to have_enqueued_job.at(scheduled_time)
289
+ end
290
+ end
291
+
292
+ describe "imperative form" do
293
+ before { ProcessJob.perform_later }
294
+
295
+ it "verifies job was enqueued" do
296
+ expect(ProcessJob).to have_been_enqueued
297
+ end
298
+
299
+ it "verifies enqueue count" do
300
+ ProcessJob.perform_later
301
+ expect(ProcessJob).to have_been_enqueued.exactly(:twice)
302
+ end
303
+ end
304
+ end
305
+
306
+ # have_broadcasted_to - testing ActionCable broadcasts
307
+ RSpec.describe "have_broadcasted_to", type: :channel do
308
+ describe "broadcasting messages" do
309
+ it "matches broadcast to channel" do
310
+ expect {
311
+ ActionCable.server.broadcast("notifications", text: "Hello!")
312
+ }.to have_broadcasted_to("notifications")
313
+ end
314
+
315
+ it "matches with message content" do
316
+ expect {
317
+ ActionCable.server.broadcast("notifications", text: "Hello!")
318
+ }.to have_broadcasted_to("notifications").with(text: "Hello!")
319
+ end
320
+
321
+ it "matches broadcast count" do
322
+ expect {
323
+ 2.times { ActionCable.server.broadcast("notifications", text: "Hi") }
324
+ }.to have_broadcasted_to("notifications").exactly(:twice)
325
+ end
326
+ end
327
+
328
+ describe "broadcasting to record" do
329
+ let(:user) { create(:user) }
330
+
331
+ it "matches broadcast to model" do
332
+ expect {
333
+ ChatChannel.broadcast_to(user, text: "Message")
334
+ }.to have_broadcasted_to(user)
335
+ end
336
+ end
337
+ end
338
+
339
+ # send_email - testing email sending (rspec-rails 7.0+)
340
+ RSpec.describe "send_email", type: :mailer do
341
+ describe "email sending" do
342
+ it "matches sent email" do
343
+ expect {
344
+ NotificationsMailer.welcome(user).deliver_now
345
+ }.to send_email
346
+ end
347
+
348
+ it "matches email attributes" do
349
+ user = create(:user, email: "test@example.com")
350
+
351
+ expect {
352
+ NotificationsMailer.welcome(user).deliver_now
353
+ }.to send_email(
354
+ from: "noreply@example.com",
355
+ to: "test@example.com",
356
+ subject: "Welcome!"
357
+ )
358
+ end
359
+ end
360
+ end
361
+
362
+ # match_array for ActiveRecord relations
363
+ RSpec.describe "match_array with relations", type: :model do
364
+ let!(:widgets) { create_list(:widget, 3) }
365
+
366
+ it "matches relation regardless of order" do
367
+ expect(Widget.all).to match_array(widgets)
368
+ end
369
+
370
+ it "matches scope results" do
371
+ published = create_list(:widget, 2, :published)
372
+ expect(Widget.published).to match_array(published)
373
+ end
374
+ end