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,250 @@
1
+ # RSpec Rails: Channel Specs Examples
2
+ # Source: rspec-rails gem features/channel_specs/
3
+
4
+ # Channel specs test ActionCable channels.
5
+ # Location: spec/channels/
6
+
7
+ # Basic channel spec
8
+ RSpec.describe ChatChannel, type: :channel do
9
+ describe "#subscribed" do
10
+ it "successfully subscribes" do
11
+ subscribe
12
+ expect(subscription).to be_confirmed
13
+ end
14
+
15
+ it "rejects without authentication" do
16
+ stub_connection current_user: nil
17
+ subscribe
18
+ expect(subscription).to be_rejected
19
+ end
20
+ end
21
+ end
22
+
23
+ # Testing with connection stub
24
+ RSpec.describe NotificationsChannel, type: :channel do
25
+ let(:user) { create(:user) }
26
+
27
+ before { stub_connection current_user: user }
28
+
29
+ describe "#subscribed" do
30
+ it "subscribes to user-specific stream" do
31
+ subscribe
32
+
33
+ expect(subscription).to be_confirmed
34
+ expect(subscription).to have_stream_for(user)
35
+ end
36
+ end
37
+ end
38
+
39
+ # Testing streams
40
+ RSpec.describe RoomChannel, type: :channel do
41
+ describe "#subscribed" do
42
+ it "streams from room channel" do
43
+ subscribe(room_id: 42)
44
+
45
+ expect(subscription).to have_stream_from("room_42")
46
+ end
47
+ end
48
+
49
+ describe "with model-based stream" do
50
+ let(:room) { create(:room) }
51
+
52
+ it "streams for the room" do
53
+ subscribe(room_id: room.id)
54
+
55
+ expect(subscription).to have_stream_for(room)
56
+ end
57
+ end
58
+ end
59
+
60
+ # Testing channel actions
61
+ RSpec.describe ChatChannel, type: :channel do
62
+ let(:user) { create(:user) }
63
+ let(:room) { create(:room) }
64
+
65
+ before do
66
+ stub_connection current_user: user
67
+ subscribe(room_id: room.id)
68
+ end
69
+
70
+ describe "#speak" do
71
+ it "broadcasts message to room" do
72
+ expect {
73
+ perform :speak, message: "Hello!"
74
+ }.to have_broadcasted_to(room).with(
75
+ user: user.name,
76
+ message: "Hello!"
77
+ )
78
+ end
79
+ end
80
+
81
+ describe "#typing" do
82
+ it "broadcasts typing status" do
83
+ expect {
84
+ perform :typing
85
+ }.to have_broadcasted_to(room).with(
86
+ user: user.name,
87
+ typing: true
88
+ )
89
+ end
90
+ end
91
+ end
92
+
93
+ # Testing subscription rejection
94
+ RSpec.describe PrivateChannel, type: :channel do
95
+ describe "#subscribed" do
96
+ context "when user lacks access" do
97
+ let(:user) { create(:user) }
98
+ let(:private_room) { create(:room, :private) }
99
+
100
+ before { stub_connection current_user: user }
101
+
102
+ it "rejects subscription" do
103
+ subscribe(room_id: private_room.id)
104
+
105
+ expect(subscription).to be_rejected
106
+ end
107
+ end
108
+
109
+ context "when user has access" do
110
+ let(:user) { create(:user, :admin) }
111
+ let(:private_room) { create(:room, :private) }
112
+
113
+ before { stub_connection current_user: user }
114
+
115
+ it "confirms subscription" do
116
+ subscribe(room_id: private_room.id)
117
+
118
+ expect(subscription).to be_confirmed
119
+ end
120
+ end
121
+ end
122
+ end
123
+
124
+ # Testing unsubscribe
125
+ RSpec.describe PresenceChannel, type: :channel do
126
+ let(:user) { create(:user) }
127
+
128
+ before { stub_connection current_user: user }
129
+
130
+ describe "#unsubscribed" do
131
+ it "broadcasts user left" do
132
+ subscribe
133
+
134
+ expect {
135
+ unsubscribe
136
+ }.to have_broadcasted_to("presence").with(
137
+ action: "left",
138
+ user: user.name
139
+ )
140
+ end
141
+ end
142
+ end
143
+
144
+ # Testing connection
145
+ RSpec.describe ApplicationCable::Connection, type: :channel do
146
+ describe "#connect" do
147
+ context "with valid session" do
148
+ let(:user) { create(:user) }
149
+
150
+ before do
151
+ cookies.signed[:user_id] = user.id
152
+ end
153
+
154
+ it "successfully connects" do
155
+ connect
156
+
157
+ expect(connection.current_user).to eq(user)
158
+ end
159
+ end
160
+
161
+ context "without session" do
162
+ it "rejects connection" do
163
+ expect { connect }.to have_rejected_connection
164
+ end
165
+ end
166
+ end
167
+ end
168
+
169
+ # Testing channel with parameters
170
+ RSpec.describe DocumentChannel, type: :channel do
171
+ let(:user) { create(:user) }
172
+ let(:document) { create(:document) }
173
+
174
+ before { stub_connection current_user: user }
175
+
176
+ describe "#subscribed with params" do
177
+ it "uses provided document_id" do
178
+ subscribe(document_id: document.id)
179
+
180
+ expect(subscription).to have_stream_for(document)
181
+ end
182
+ end
183
+
184
+ describe "#update" do
185
+ before { subscribe(document_id: document.id) }
186
+
187
+ it "broadcasts update to document stream" do
188
+ expect {
189
+ perform :update, content: "New content"
190
+ }.to have_broadcasted_to(document).with(
191
+ a_hash_including(content: "New content")
192
+ )
193
+ end
194
+ end
195
+ end
196
+
197
+ # Testing broadcast matching
198
+ RSpec.describe "Broadcast matching", type: :channel do
199
+ describe "matching broadcast content" do
200
+ it "matches exact content" do
201
+ expect {
202
+ ActionCable.server.broadcast("test", { action: "update", data: 123 })
203
+ }.to have_broadcasted_to("test").with(action: "update", data: 123)
204
+ end
205
+
206
+ it "matches with hash including" do
207
+ expect {
208
+ ActionCable.server.broadcast("test", { action: "update", data: 123, timestamp: Time.current })
209
+ }.to have_broadcasted_to("test").with(a_hash_including(action: "update"))
210
+ end
211
+
212
+ it "matches with block" do
213
+ expect {
214
+ ActionCable.server.broadcast("test", { count: 5 })
215
+ }.to have_broadcasted_to("test").with { |data|
216
+ expect(data[:count]).to be > 0
217
+ }
218
+ end
219
+ end
220
+ end
221
+
222
+ # Testing broadcast counts
223
+ RSpec.describe "Broadcast counts", type: :channel do
224
+ it "matches exact broadcast count" do
225
+ expect {
226
+ 3.times { ActionCable.server.broadcast("updates", { ping: true }) }
227
+ }.to have_broadcasted_to("updates").exactly(3).times
228
+ end
229
+
230
+ it "matches at least count" do
231
+ expect {
232
+ 5.times { ActionCable.server.broadcast("updates", { ping: true }) }
233
+ }.to have_broadcasted_to("updates").at_least(:twice)
234
+ end
235
+
236
+ it "matches at most count" do
237
+ expect {
238
+ 2.times { ActionCable.server.broadcast("updates", { ping: true }) }
239
+ }.to have_broadcasted_to("updates").at_most(3).times
240
+ end
241
+ end
242
+
243
+ # Testing no broadcast
244
+ RSpec.describe "No broadcast", type: :channel do
245
+ it "verifies nothing was broadcast" do
246
+ expect {
247
+ # Some action that shouldn't broadcast
248
+ }.not_to have_broadcasted_to("notifications")
249
+ end
250
+ end
@@ -0,0 +1,302 @@
1
+ # RSpec Rails: Controller Specs Examples
2
+ # Source: rspec-rails gem features/controller_specs/
3
+
4
+ # NOTE: Request specs are generally preferred for new code.
5
+ # Controller specs are useful for testing specific controller behavior in isolation.
6
+ # Location: spec/controllers/
7
+
8
+ # Basic controller spec
9
+ RSpec.describe WidgetsController, type: :controller do
10
+ describe "GET #index" do
11
+ let!(:widgets) { create_list(:widget, 3) }
12
+
13
+ it "returns a successful response" do
14
+ get :index
15
+ expect(response).to have_http_status(:ok)
16
+ end
17
+
18
+ it "assigns @widgets" do
19
+ get :index
20
+ expect(assigns(:widgets)).to match_array(widgets)
21
+ end
22
+
23
+ it "renders the index template" do
24
+ get :index
25
+ expect(response).to render_template(:index)
26
+ end
27
+ end
28
+
29
+ describe "GET #show" do
30
+ let(:widget) { create(:widget) }
31
+
32
+ it "assigns @widget" do
33
+ get :show, params: { id: widget.id }
34
+ expect(assigns(:widget)).to eq(widget)
35
+ end
36
+ end
37
+
38
+ describe "GET #new" do
39
+ it "assigns a new widget" do
40
+ get :new
41
+ expect(assigns(:widget)).to be_a_new(Widget)
42
+ end
43
+ end
44
+
45
+ describe "POST #create" do
46
+ context "with valid params" do
47
+ let(:valid_params) { { widget: attributes_for(:widget) } }
48
+
49
+ it "creates a new Widget" do
50
+ expect {
51
+ post :create, params: valid_params
52
+ }.to change(Widget, :count).by(1)
53
+ end
54
+
55
+ it "redirects to the created widget" do
56
+ post :create, params: valid_params
57
+ expect(response).to redirect_to(Widget.last)
58
+ end
59
+ end
60
+
61
+ context "with invalid params" do
62
+ let(:invalid_params) { { widget: { name: "" } } }
63
+
64
+ it "does not create a Widget" do
65
+ expect {
66
+ post :create, params: invalid_params
67
+ }.not_to change(Widget, :count)
68
+ end
69
+
70
+ it "renders the new template" do
71
+ post :create, params: invalid_params
72
+ expect(response).to render_template(:new)
73
+ end
74
+ end
75
+ end
76
+
77
+ describe "PATCH #update" do
78
+ let(:widget) { create(:widget, name: "Old Name") }
79
+
80
+ context "with valid params" do
81
+ it "updates the widget" do
82
+ patch :update, params: { id: widget.id, widget: { name: "New Name" } }
83
+ expect(widget.reload.name).to eq("New Name")
84
+ end
85
+
86
+ it "redirects to the widget" do
87
+ patch :update, params: { id: widget.id, widget: { name: "New Name" } }
88
+ expect(response).to redirect_to(widget)
89
+ end
90
+ end
91
+ end
92
+
93
+ describe "DELETE #destroy" do
94
+ let!(:widget) { create(:widget) }
95
+
96
+ it "destroys the widget" do
97
+ expect {
98
+ delete :destroy, params: { id: widget.id }
99
+ }.to change(Widget, :count).by(-1)
100
+ end
101
+
102
+ it "redirects to index" do
103
+ delete :destroy, params: { id: widget.id }
104
+ expect(response).to redirect_to(widgets_url)
105
+ end
106
+ end
107
+ end
108
+
109
+ # render_views - actually render templates (stubbed by default)
110
+ RSpec.describe WidgetsController, type: :controller do
111
+ render_views
112
+
113
+ describe "GET #index" do
114
+ let!(:widget) { create(:widget, name: "My Widget") }
115
+
116
+ it "renders widget name in body" do
117
+ get :index
118
+ expect(response.body).to include("My Widget")
119
+ end
120
+ end
121
+ end
122
+
123
+ # Testing different response formats
124
+ RSpec.describe WidgetsController, type: :controller do
125
+ describe "POST #create" do
126
+ let(:valid_params) { { widget: attributes_for(:widget) } }
127
+
128
+ it "responds to HTML by default" do
129
+ post :create, params: valid_params
130
+ expect(response.content_type).to include("text/html")
131
+ end
132
+
133
+ it "responds to JSON when requested" do
134
+ post :create, params: valid_params.merge(format: :json)
135
+ expect(response.content_type).to include("application/json")
136
+ end
137
+ end
138
+ end
139
+
140
+ # Anonymous controllers for testing base controller behavior
141
+ RSpec.describe ApplicationController, type: :controller do
142
+ controller do
143
+ def index
144
+ raise ApplicationController::AccessDenied
145
+ end
146
+ end
147
+
148
+ describe "handling AccessDenied exceptions" do
149
+ it "redirects to the 401 page" do
150
+ get :index
151
+ expect(response).to redirect_to("/401.html")
152
+ end
153
+ end
154
+ end
155
+
156
+ # Testing custom controller action
157
+ RSpec.describe ApplicationController, type: :controller do
158
+ controller do
159
+ def custom_action
160
+ render plain: "Custom response"
161
+ end
162
+ end
163
+
164
+ before do
165
+ routes.draw { get "custom_action" => "anonymous#custom_action" }
166
+ end
167
+
168
+ it "renders custom response" do
169
+ get :custom_action
170
+ expect(response.body).to eq("Custom response")
171
+ end
172
+ end
173
+
174
+ # Testing with request headers
175
+ RSpec.describe ApiController, type: :controller do
176
+ controller do
177
+ def index
178
+ if request.headers["Authorization"].present?
179
+ render plain: "Authenticated"
180
+ else
181
+ render plain: "Not authenticated", status: :unauthorized
182
+ end
183
+ end
184
+ end
185
+
186
+ describe "GET #index" do
187
+ context "with authorization header" do
188
+ before { request.headers["Authorization"] = "Bearer token123" }
189
+
190
+ it "returns success" do
191
+ get :index
192
+ expect(response).to have_http_status(:ok)
193
+ end
194
+ end
195
+
196
+ context "without authorization header" do
197
+ it "returns unauthorized" do
198
+ get :index
199
+ expect(response).to have_http_status(:unauthorized)
200
+ end
201
+ end
202
+ end
203
+ end
204
+
205
+ # Inherited controller specs
206
+ RSpec.describe FoosController, type: :controller do
207
+ # When controller inherits from another controller
208
+ controller FoosController do
209
+ def index
210
+ @name = self.class.name
211
+ @controller_name = controller_name
212
+ render plain: "Hello"
213
+ end
214
+ end
215
+
216
+ describe "GET #index" do
217
+ before { get :index }
218
+
219
+ it "gets the class name as described" do
220
+ expect(assigns(:name)).to eq("FoosController")
221
+ end
222
+
223
+ it "gets the controller_name as described" do
224
+ expect(assigns(:controller_name)).to eq("foos")
225
+ end
226
+ end
227
+ end
228
+
229
+ # Testing before_action filters
230
+ RSpec.describe ProtectedController, type: :controller do
231
+ controller do
232
+ before_action :require_login
233
+
234
+ def index
235
+ render plain: "Protected content"
236
+ end
237
+
238
+ private
239
+
240
+ def require_login
241
+ redirect_to login_path unless session[:user_id]
242
+ end
243
+ end
244
+
245
+ describe "before_action :require_login" do
246
+ context "when not logged in" do
247
+ it "redirects to login" do
248
+ get :index
249
+ expect(response).to redirect_to(login_path)
250
+ end
251
+ end
252
+
253
+ context "when logged in" do
254
+ before { session[:user_id] = 1 }
255
+
256
+ it "allows access" do
257
+ get :index
258
+ expect(response).to have_http_status(:ok)
259
+ end
260
+ end
261
+ end
262
+ end
263
+
264
+ # Testing flash messages
265
+ RSpec.describe WidgetsController, type: :controller do
266
+ describe "POST #create" do
267
+ context "with valid params" do
268
+ it "sets a success flash message" do
269
+ post :create, params: { widget: attributes_for(:widget) }
270
+ expect(flash[:notice]).to eq("Widget was successfully created.")
271
+ end
272
+ end
273
+
274
+ context "with invalid params" do
275
+ it "sets an error flash message" do
276
+ post :create, params: { widget: { name: "" } }
277
+ expect(flash[:alert]).to be_present
278
+ end
279
+ end
280
+ end
281
+ end
282
+
283
+ # Testing session modifications
284
+ RSpec.describe SessionsController, type: :controller do
285
+ describe "POST #create" do
286
+ let(:user) { create(:user, password: "secret") }
287
+
288
+ it "sets user_id in session" do
289
+ post :create, params: { email: user.email, password: "secret" }
290
+ expect(session[:user_id]).to eq(user.id)
291
+ end
292
+ end
293
+
294
+ describe "DELETE #destroy" do
295
+ before { session[:user_id] = 1 }
296
+
297
+ it "clears user_id from session" do
298
+ delete :destroy
299
+ expect(session[:user_id]).to be_nil
300
+ end
301
+ end
302
+ end