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,245 @@
1
+ # RSpec Rails: Helper Specs Examples
2
+ # Source: rspec-rails gem features/helper_specs/
3
+
4
+ # Helper specs test view helper methods in isolation.
5
+ # Access the helper module via `helper`.
6
+ # Location: spec/helpers/
7
+
8
+ # Basic helper spec
9
+ RSpec.describe ApplicationHelper, type: :helper do
10
+ describe "#page_title" do
11
+ context "when @title is not set" do
12
+ it "returns the default title" do
13
+ expect(helper.page_title).to eq("RSpec is your friend")
14
+ end
15
+ end
16
+
17
+ context "when @title is set" do
18
+ before { assign(:title, "Custom Title") }
19
+
20
+ it "returns the custom title" do
21
+ expect(helper.page_title).to eq("Custom Title")
22
+ end
23
+ end
24
+ end
25
+ end
26
+
27
+ # Testing formatting helpers
28
+ RSpec.describe FormattingHelper, type: :helper do
29
+ describe "#format_date" do
30
+ it "formats date in user-friendly format" do
31
+ date = Date.new(2024, 1, 15)
32
+ expect(helper.format_date(date)).to eq("January 15, 2024")
33
+ end
34
+
35
+ it "returns empty string for nil" do
36
+ expect(helper.format_date(nil)).to eq("")
37
+ end
38
+ end
39
+
40
+ describe "#format_currency" do
41
+ it "formats amount as currency" do
42
+ expect(helper.format_currency(1234.56)).to eq("$1,234.56")
43
+ end
44
+
45
+ it "handles zero" do
46
+ expect(helper.format_currency(0)).to eq("$0.00")
47
+ end
48
+
49
+ it "handles negative amounts" do
50
+ expect(helper.format_currency(-50)).to eq("-$50.00")
51
+ end
52
+ end
53
+ end
54
+
55
+ # Using URL helpers in helper specs
56
+ RSpec.describe WidgetsHelper, type: :helper do
57
+ describe "#link_to_widget" do
58
+ let(:widget) { create(:widget, name: "Super Widget") }
59
+
60
+ it "creates link to widget" do
61
+ result = helper.link_to_widget(widget)
62
+
63
+ expect(result).to include("Super Widget")
64
+ expect(result).to include(widget_path(widget))
65
+ end
66
+ end
67
+
68
+ describe "#widget_status_badge" do
69
+ let(:widget) { build(:widget) }
70
+
71
+ context "when widget is active" do
72
+ let(:widget) { build(:widget, :active) }
73
+
74
+ it "returns success badge" do
75
+ result = helper.widget_status_badge(widget)
76
+ expect(result).to include("badge-success")
77
+ expect(result).to include("Active")
78
+ end
79
+ end
80
+
81
+ context "when widget is inactive" do
82
+ let(:widget) { build(:widget, :inactive) }
83
+
84
+ it "returns warning badge" do
85
+ result = helper.widget_status_badge(widget)
86
+ expect(result).to include("badge-warning")
87
+ expect(result).to include("Inactive")
88
+ end
89
+ end
90
+ end
91
+ end
92
+
93
+ # Testing helpers that use Rails helpers
94
+ RSpec.describe TextHelper, type: :helper do
95
+ describe "#truncated_summary" do
96
+ it "truncates long text" do
97
+ text = "This is a very long text that should be truncated"
98
+ result = helper.truncated_summary(text, length: 20)
99
+
100
+ expect(result.length).to be <= 23 # 20 + "..."
101
+ expect(result).to end_with("...")
102
+ end
103
+
104
+ it "leaves short text unchanged" do
105
+ text = "Short text"
106
+ result = helper.truncated_summary(text, length: 50)
107
+
108
+ expect(result).to eq("Short text")
109
+ end
110
+ end
111
+
112
+ describe "#pluralized_count" do
113
+ it "pluralizes with count" do
114
+ expect(helper.pluralized_count(1, "item")).to eq("1 item")
115
+ expect(helper.pluralized_count(5, "item")).to eq("5 items")
116
+ expect(helper.pluralized_count(0, "item")).to eq("0 items")
117
+ end
118
+ end
119
+ end
120
+
121
+ # Testing helpers with HTML output
122
+ RSpec.describe IconHelper, type: :helper do
123
+ describe "#icon" do
124
+ it "generates icon element" do
125
+ result = helper.icon("check")
126
+ expect(result).to be_html_safe
127
+ expect(result).to include('<i class="icon icon-check">')
128
+ end
129
+
130
+ it "accepts additional classes" do
131
+ result = helper.icon("warning", class: "text-danger")
132
+ expect(result).to include("icon-warning")
133
+ expect(result).to include("text-danger")
134
+ end
135
+ end
136
+ end
137
+
138
+ # Testing helpers that use controller context
139
+ RSpec.describe NavigationHelper, type: :helper do
140
+ describe "#active_nav_class" do
141
+ before { allow(helper).to receive(:controller_name).and_return("widgets") }
142
+
143
+ context "when on matching controller" do
144
+ it "returns active class" do
145
+ expect(helper.active_nav_class("widgets")).to eq("active")
146
+ end
147
+ end
148
+
149
+ context "when on different controller" do
150
+ it "returns empty string" do
151
+ expect(helper.active_nav_class("users")).to eq("")
152
+ end
153
+ end
154
+ end
155
+ end
156
+
157
+ # Testing helpers that depend on current_user
158
+ RSpec.describe AuthorizationHelper, type: :helper do
159
+ let(:admin_user) { build(:user, :admin) }
160
+ let(:regular_user) { build(:user) }
161
+
162
+ describe "#can_edit?" do
163
+ let(:widget) { build(:widget, user: regular_user) }
164
+
165
+ context "when user is admin" do
166
+ before { allow(helper).to receive(:current_user).and_return(admin_user) }
167
+
168
+ it "returns true" do
169
+ expect(helper.can_edit?(widget)).to be(true)
170
+ end
171
+ end
172
+
173
+ context "when user is the owner" do
174
+ before { allow(helper).to receive(:current_user).and_return(regular_user) }
175
+
176
+ it "returns true" do
177
+ expect(helper.can_edit?(widget)).to be(true)
178
+ end
179
+ end
180
+
181
+ context "when user is neither admin nor owner" do
182
+ let(:other_user) { build(:user) }
183
+
184
+ before { allow(helper).to receive(:current_user).and_return(other_user) }
185
+
186
+ it "returns false" do
187
+ expect(helper.can_edit?(widget)).to be(false)
188
+ end
189
+ end
190
+ end
191
+ end
192
+
193
+ # Testing helpers that build complex structures
194
+ RSpec.describe BreadcrumbHelper, type: :helper do
195
+ describe "#breadcrumbs" do
196
+ it "builds breadcrumb trail" do
197
+ result = helper.breadcrumbs do |b|
198
+ b.add "Home", root_path
199
+ b.add "Widgets", widgets_path
200
+ b.add "Edit"
201
+ end
202
+
203
+ expect(result).to include(root_path)
204
+ expect(result).to include(widgets_path)
205
+ expect(result).to include("Edit")
206
+ end
207
+ end
208
+ end
209
+
210
+ # Testing helpers with flash messages
211
+ RSpec.describe FlashHelper, type: :helper do
212
+ describe "#flash_class" do
213
+ it "maps notice to success" do
214
+ expect(helper.flash_class(:notice)).to eq("alert-success")
215
+ end
216
+
217
+ it "maps alert to danger" do
218
+ expect(helper.flash_class(:alert)).to eq("alert-danger")
219
+ end
220
+
221
+ it "maps unknown to info" do
222
+ expect(helper.flash_class(:custom)).to eq("alert-info")
223
+ end
224
+ end
225
+ end
226
+
227
+ # Testing time-dependent helpers
228
+ RSpec.describe TimeHelper, type: :helper do
229
+ describe "#time_ago_in_words_short" do
230
+ it "returns minutes ago" do
231
+ time = 5.minutes.ago
232
+ expect(helper.time_ago_in_words_short(time)).to eq("5m ago")
233
+ end
234
+
235
+ it "returns hours ago" do
236
+ time = 3.hours.ago
237
+ expect(helper.time_ago_in_words_short(time)).to eq("3h ago")
238
+ end
239
+
240
+ it "returns days ago" do
241
+ time = 2.days.ago
242
+ expect(helper.time_ago_in_words_short(time)).to eq("2d ago")
243
+ end
244
+ end
245
+ end
@@ -0,0 +1,256 @@
1
+ # RSpec Rails: Job Specs Examples
2
+ # Source: rspec-rails gem features/job_specs/
3
+
4
+ # Job specs test ActiveJob classes.
5
+ # Requires: ActiveJob::Base.queue_adapter = :test
6
+ # Location: spec/jobs/
7
+
8
+ # Basic job spec
9
+ RSpec.describe UploadBackupsJob, type: :job do
10
+ describe "#perform_later" do
11
+ it "enqueues the job" do
12
+ expect {
13
+ UploadBackupsJob.perform_later("backup.sql")
14
+ }.to have_enqueued_job
15
+ end
16
+
17
+ it "enqueues with correct arguments" do
18
+ expect {
19
+ UploadBackupsJob.perform_later("backup.sql")
20
+ }.to have_enqueued_job.with("backup.sql")
21
+ end
22
+ end
23
+ end
24
+
25
+ # Testing on specific queue
26
+ RSpec.describe EmailDeliveryJob, type: :job do
27
+ describe "#perform_later" do
28
+ it "enqueues on the mailers queue" do
29
+ expect {
30
+ EmailDeliveryJob.perform_later
31
+ }.to have_enqueued_job.on_queue("mailers")
32
+ end
33
+ end
34
+ end
35
+
36
+ # Testing scheduled jobs
37
+ RSpec.describe ScheduledReportJob, type: :job do
38
+ describe "#perform_later" do
39
+ it "enqueues at specific time" do
40
+ scheduled_time = 1.day.from_now.noon
41
+
42
+ expect {
43
+ ScheduledReportJob.set(wait_until: scheduled_time).perform_later
44
+ }.to have_enqueued_job.at(scheduled_time)
45
+ end
46
+
47
+ it "enqueues with wait duration" do
48
+ expect {
49
+ ScheduledReportJob.set(wait: 1.hour).perform_later
50
+ }.to have_enqueued_job.at(a_value_within(1.second).of(1.hour.from_now))
51
+ end
52
+ end
53
+ end
54
+
55
+ # Testing job priority
56
+ RSpec.describe HighPriorityJob, type: :job do
57
+ describe "#perform_later" do
58
+ it "enqueues with priority" do
59
+ expect {
60
+ HighPriorityJob.set(priority: 1).perform_later
61
+ }.to have_enqueued_job.at_priority(1)
62
+ end
63
+ end
64
+ end
65
+
66
+ # Imperative (post-hoc) syntax
67
+ RSpec.describe ProcessOrderJob, type: :job do
68
+ describe "#perform_later" do
69
+ let(:order) { create(:order) }
70
+
71
+ before { ProcessOrderJob.perform_later(order) }
72
+
73
+ it "has been enqueued" do
74
+ expect(ProcessOrderJob).to have_been_enqueued
75
+ end
76
+
77
+ it "was enqueued exactly once" do
78
+ expect(ProcessOrderJob).to have_been_enqueued.exactly(:once)
79
+ end
80
+ end
81
+ end
82
+
83
+ # Testing job execution count
84
+ RSpec.describe BatchProcessingJob, type: :job do
85
+ describe "multiple enqueues" do
86
+ it "enqueues exactly 3 times" do
87
+ expect {
88
+ 3.times { BatchProcessingJob.perform_later }
89
+ }.to have_enqueued_job.exactly(3).times
90
+ end
91
+
92
+ it "enqueues at least twice" do
93
+ expect {
94
+ 5.times { BatchProcessingJob.perform_later }
95
+ }.to have_enqueued_job.at_least(:twice)
96
+ end
97
+ end
98
+ end
99
+
100
+ # Testing job perform behavior
101
+ RSpec.describe SendNotificationJob, type: :job do
102
+ describe "#perform" do
103
+ let(:user) { create(:user) }
104
+ let(:message) { "Hello!" }
105
+
106
+ it "sends notification to user" do
107
+ notifier = instance_double(NotificationService)
108
+ allow(NotificationService).to receive(:new).and_return(notifier)
109
+ expect(notifier).to receive(:notify).with(user, message)
110
+
111
+ SendNotificationJob.perform_now(user, message)
112
+ end
113
+ end
114
+ end
115
+
116
+ # Testing with perform_enqueued_jobs
117
+ RSpec.describe "Job execution", type: :job do
118
+ describe "running enqueued jobs" do
119
+ let(:user) { create(:user) }
120
+
121
+ it "performs jobs inline" do
122
+ perform_enqueued_jobs do
123
+ WelcomeEmailJob.perform_later(user)
124
+ end
125
+
126
+ expect(ActionMailer::Base.deliveries.count).to eq(1)
127
+ end
128
+
129
+ it "performs only specific jobs" do
130
+ perform_enqueued_jobs(only: WelcomeEmailJob) do
131
+ WelcomeEmailJob.perform_later(user)
132
+ CleanupJob.perform_later
133
+ end
134
+
135
+ expect(WelcomeEmailJob).not_to have_been_enqueued
136
+ expect(CleanupJob).to have_been_enqueued
137
+ end
138
+ end
139
+ end
140
+
141
+ # Testing job with retries
142
+ RSpec.describe RetryableJob, type: :job do
143
+ describe "retry behavior" do
144
+ let(:api_client) { instance_double(ExternalApi) }
145
+
146
+ before do
147
+ allow(ExternalApi).to receive(:new).and_return(api_client)
148
+ end
149
+
150
+ context "when API fails temporarily" do
151
+ before do
152
+ call_count = 0
153
+ allow(api_client).to receive(:call) do
154
+ call_count += 1
155
+ raise StandardError if call_count < 3
156
+ :success
157
+ end
158
+ end
159
+
160
+ it "retries and eventually succeeds" do
161
+ expect {
162
+ perform_enqueued_jobs { RetryableJob.perform_later }
163
+ }.not_to raise_error
164
+ end
165
+ end
166
+ end
167
+ end
168
+
169
+ # Testing job that spawns other jobs
170
+ RSpec.describe ParentJob, type: :job do
171
+ describe "#perform" do
172
+ let(:items) { create_list(:item, 3) }
173
+
174
+ it "enqueues child job for each item" do
175
+ expect {
176
+ ParentJob.perform_now(items.map(&:id))
177
+ }.to have_enqueued_job(ChildJob).exactly(3).times
178
+ end
179
+ end
180
+ end
181
+
182
+ # Testing with specific job class
183
+ RSpec.describe "Multiple job types", type: :job do
184
+ describe "selective matching" do
185
+ it "matches specific job class" do
186
+ expect {
187
+ SendEmailJob.perform_later
188
+ ProcessPaymentJob.perform_later
189
+ }.to have_enqueued_job(SendEmailJob)
190
+ .and have_enqueued_job(ProcessPaymentJob)
191
+ end
192
+ end
193
+ end
194
+
195
+ # Testing job with complex arguments using block
196
+ RSpec.describe DataExportJob, type: :job do
197
+ describe "#perform_later" do
198
+ it "enqueues with expected structure" do
199
+ expect {
200
+ DataExportJob.perform_later(
201
+ user_id: 1,
202
+ format: :csv,
203
+ filters: { status: :active }
204
+ )
205
+ }.to have_enqueued_job.with { |args|
206
+ expect(args[:user_id]).to eq(1)
207
+ expect(args[:format]).to eq(:csv)
208
+ expect(args[:filters]).to include(status: :active)
209
+ }
210
+ end
211
+ end
212
+ end
213
+
214
+ # Testing job queue adapter configuration
215
+ RSpec.describe "Queue adapter", type: :job do
216
+ it "uses test adapter" do
217
+ expect(ActiveJob::Base.queue_adapter).to be_a(ActiveJob::QueueAdapters::TestAdapter)
218
+ end
219
+ end
220
+
221
+ # Clearing enqueued jobs between examples
222
+ RSpec.describe "Job queue state", type: :job do
223
+ before { clear_enqueued_jobs }
224
+
225
+ it "starts with empty queue" do
226
+ expect(ActiveJob::Base.queue_adapter.enqueued_jobs).to be_empty
227
+ end
228
+ end
229
+
230
+ # Testing that no jobs were enqueued
231
+ RSpec.describe ConditionalJob, type: :job do
232
+ describe "#perform" do
233
+ context "when condition is not met" do
234
+ let(:order) { create(:order, :complete) }
235
+
236
+ it "does not enqueue follow-up job" do
237
+ expect {
238
+ ConditionalJob.perform_now(order)
239
+ }.not_to have_enqueued_job(FollowUpJob)
240
+ end
241
+ end
242
+ end
243
+ end
244
+
245
+ # Testing job with GlobalID arguments
246
+ RSpec.describe ProcessRecordJob, type: :job do
247
+ describe "#perform" do
248
+ let(:record) { create(:widget) }
249
+
250
+ it "accepts ActiveRecord objects" do
251
+ expect {
252
+ ProcessRecordJob.perform_later(record)
253
+ }.to have_enqueued_job.with(record)
254
+ end
255
+ end
256
+ end