anima-core 0.3.0 → 1.0.1

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 (270) 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 +4 -1
  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 +182 -6
  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 -2
  270. data/.mise.toml +0 -2
@@ -0,0 +1,193 @@
1
+ # RSpec Rails: Model Specs Examples
2
+ # Source: rspec-rails gem features/model_specs/
3
+
4
+ # Model specs test ActiveRecord models, validations, associations,
5
+ # scopes, callbacks, and instance methods.
6
+ # Location: spec/models/
7
+
8
+ # Basic model testing
9
+ RSpec.describe Post, type: :model do
10
+ describe "validations" do
11
+ subject(:post) { build(:post) }
12
+
13
+ it "is valid with valid attributes" do
14
+ expect(post).to be_valid
15
+ end
16
+
17
+ it "is invalid without a title" do
18
+ post.title = nil
19
+ expect(post).not_to be_valid
20
+ expect(post.errors[:title]).to include("can't be blank")
21
+ end
22
+
23
+ # Validation context
24
+ it "validates uniqueness on create" do
25
+ expect(post).to be_valid(:create)
26
+ end
27
+ end
28
+
29
+ describe "associations" do
30
+ subject(:post) { create(:post) }
31
+
32
+ it "has many comments" do
33
+ comments = create_list(:comment, 3, post:)
34
+ expect(post.comments).to match_array(comments)
35
+ end
36
+
37
+ it "belongs to an author" do
38
+ expect(post.author).to be_a(User)
39
+ end
40
+
41
+ it "destroys dependent comments" do
42
+ create_list(:comment, 2, post:)
43
+ expect { post.destroy }.to change(Comment, :count).by(-2)
44
+ end
45
+ end
46
+
47
+ describe "scopes" do
48
+ describe ".published" do
49
+ let!(:published_post) { create(:post, :published) }
50
+ let!(:draft_post) { create(:post, :draft) }
51
+
52
+ it "returns only published posts" do
53
+ expect(Post.published).to contain_exactly(published_post)
54
+ end
55
+ end
56
+
57
+ describe ".recent" do
58
+ let!(:old_post) { create(:post, created_at: 1.month.ago) }
59
+ let!(:new_post) { create(:post, created_at: 1.day.ago) }
60
+
61
+ it "returns posts in reverse chronological order" do
62
+ expect(Post.recent).to eq([new_post, old_post])
63
+ end
64
+ end
65
+
66
+ # Testing scope combinations with match_array
67
+ describe ".featured" do
68
+ let!(:featured_posts) { create_list(:post, 3, :featured) }
69
+ let!(:regular_posts) { create_list(:post, 2) }
70
+
71
+ subject { Post.featured }
72
+
73
+ it "returns featured posts in any order" do
74
+ expect(subject).to match_array(featured_posts)
75
+ end
76
+ end
77
+ end
78
+
79
+ describe "callbacks" do
80
+ describe "before_save" do
81
+ subject(:post) { build(:post, title: " hello world ") }
82
+
83
+ it "strips whitespace from title" do
84
+ post.save
85
+ expect(post.title).to eq("hello world")
86
+ end
87
+ end
88
+
89
+ describe "after_create" do
90
+ subject(:post) { build(:post) }
91
+
92
+ it "schedules notification job" do
93
+ expect { post.save }
94
+ .to have_enqueued_job(NotifySubscribersJob)
95
+ .with(post)
96
+ end
97
+ end
98
+ end
99
+
100
+ describe "instance methods" do
101
+ describe "#published?" do
102
+ context "when published_at is set" do
103
+ subject(:post) { build(:post, published_at: Time.current) }
104
+
105
+ it "returns true" do
106
+ expect(post).to be_published
107
+ end
108
+ end
109
+
110
+ context "when published_at is nil" do
111
+ subject(:post) { build(:post, published_at: nil) }
112
+
113
+ it "returns false" do
114
+ expect(post).not_to be_published
115
+ end
116
+ end
117
+ end
118
+
119
+ describe "#reading_time" do
120
+ subject(:post) { build(:post, body: "word " * 500) }
121
+
122
+ it "calculates based on word count" do
123
+ expect(post.reading_time).to eq(2) # 500 words / 250 wpm
124
+ end
125
+ end
126
+ end
127
+ end
128
+
129
+ # be_a_new matcher - testing new record state
130
+ RSpec.describe Widget, type: :model do
131
+ describe "persistence state" do
132
+ context "when initialized" do
133
+ subject(:widget) { Widget.new }
134
+
135
+ it "is a new widget" do
136
+ expect(widget).to be_a_new(Widget)
137
+ end
138
+ end
139
+
140
+ context "when saved" do
141
+ subject(:widget) { create(:widget) }
142
+
143
+ it "is not a new widget" do
144
+ expect(widget).not_to be_a_new(Widget)
145
+ end
146
+ end
147
+ end
148
+ end
149
+
150
+ # Testing class methods
151
+ RSpec.describe Order, type: :model do
152
+ describe ".total_revenue" do
153
+ let!(:orders) { create_list(:order, 3, amount: 100) }
154
+
155
+ it "sums all order amounts" do
156
+ expect(Order.total_revenue).to eq(300)
157
+ end
158
+ end
159
+
160
+ describe ".find_by_reference" do
161
+ let!(:order) { create(:order, reference: "ORD-123") }
162
+
163
+ it "finds order by reference" do
164
+ expect(Order.find_by_reference("ORD-123")).to eq(order)
165
+ end
166
+
167
+ it "returns nil for unknown reference" do
168
+ expect(Order.find_by_reference("UNKNOWN")).to be_nil
169
+ end
170
+ end
171
+ end
172
+
173
+ # Testing concerns/modules
174
+ RSpec.describe Publishable, type: :model do
175
+ # Use a concrete class that includes the concern
176
+ let(:publishable_class) do
177
+ Class.new(ApplicationRecord) do
178
+ self.table_name = "articles"
179
+ include Publishable
180
+ end
181
+ end
182
+
183
+ subject(:record) { publishable_class.new }
184
+
185
+ describe "#publish!" do
186
+ it "sets published_at to current time" do
187
+ freeze_time do
188
+ record.publish!
189
+ expect(record.published_at).to eq(Time.current)
190
+ end
191
+ end
192
+ end
193
+ end
@@ -0,0 +1,275 @@
1
+ # RSpec Rails: Request Specs Examples
2
+ # Source: rspec-rails gem features/request_specs/
3
+
4
+ # Request specs are full-stack integration tests.
5
+ # They exercise the entire Rails stack from routing through the response.
6
+ # Preferred over controller specs for new code.
7
+ # Location: spec/requests/, spec/integration/, spec/api/
8
+
9
+ # Basic request spec
10
+ RSpec.describe "Widgets", type: :request do
11
+ describe "GET /widgets" do
12
+ let!(:widgets) { create_list(:widget, 3) }
13
+
14
+ it "returns a successful response" do
15
+ get "/widgets"
16
+ expect(response).to have_http_status(:ok)
17
+ end
18
+
19
+ it "displays all widgets" do
20
+ get "/widgets"
21
+ widgets.each do |widget|
22
+ expect(response.body).to include(widget.name)
23
+ end
24
+ end
25
+ end
26
+
27
+ describe "GET /widgets/:id" do
28
+ let(:widget) { create(:widget, name: "Test Widget") }
29
+
30
+ it "displays the widget" do
31
+ get "/widgets/#{widget.id}"
32
+
33
+ expect(response).to have_http_status(:ok)
34
+ expect(response.body).to include("Test Widget")
35
+ end
36
+ end
37
+
38
+ describe "POST /widgets" do
39
+ let(:valid_params) { { widget: { name: "New Widget" } } }
40
+ let(:invalid_params) { { widget: { name: "" } } }
41
+
42
+ context "with valid parameters" do
43
+ it "creates a widget" do
44
+ expect {
45
+ post "/widgets", params: valid_params
46
+ }.to change(Widget, :count).by(1)
47
+ end
48
+
49
+ it "redirects to the widget page" do
50
+ post "/widgets", params: valid_params
51
+
52
+ expect(response).to redirect_to(widget_path(Widget.last))
53
+ follow_redirect!
54
+ expect(response.body).to include("Widget was successfully created")
55
+ end
56
+ end
57
+
58
+ context "with invalid parameters" do
59
+ it "does not create a widget" do
60
+ expect {
61
+ post "/widgets", params: invalid_params
62
+ }.not_to change(Widget, :count)
63
+ end
64
+
65
+ it "renders the new template" do
66
+ post "/widgets", params: invalid_params
67
+ expect(response).to have_http_status(:unprocessable_entity)
68
+ end
69
+ end
70
+ end
71
+
72
+ describe "PATCH /widgets/:id" do
73
+ let(:widget) { create(:widget, name: "Old Name") }
74
+
75
+ it "updates the widget" do
76
+ patch "/widgets/#{widget.id}", params: { widget: { name: "New Name" } }
77
+
78
+ expect(response).to redirect_to(widget_path(widget))
79
+ expect(widget.reload.name).to eq("New Name")
80
+ end
81
+ end
82
+
83
+ describe "DELETE /widgets/:id" do
84
+ let!(:widget) { create(:widget) }
85
+
86
+ it "destroys the widget" do
87
+ expect {
88
+ delete "/widgets/#{widget.id}"
89
+ }.to change(Widget, :count).by(-1)
90
+ end
91
+
92
+ it "redirects to index" do
93
+ delete "/widgets/#{widget.id}"
94
+ expect(response).to redirect_to(widgets_path)
95
+ end
96
+ end
97
+ end
98
+
99
+ # JSON API request specs
100
+ RSpec.describe "API::Widgets", type: :request do
101
+ let(:json_headers) { { "ACCEPT" => "application/json" } }
102
+
103
+ describe "GET /api/widgets" do
104
+ let!(:widgets) { create_list(:widget, 3) }
105
+
106
+ it "returns JSON response" do
107
+ get "/api/widgets", headers: json_headers
108
+
109
+ expect(response).to have_http_status(:ok)
110
+ expect(response.content_type).to include("application/json")
111
+ end
112
+
113
+ it "returns all widgets" do
114
+ get "/api/widgets", headers: json_headers
115
+
116
+ json = JSON.parse(response.body, symbolize_names: true)
117
+ expect(json.length).to eq(3)
118
+ end
119
+ end
120
+
121
+ describe "POST /api/widgets" do
122
+ let(:valid_params) { { widget: { name: "New Widget" } } }
123
+
124
+ it "creates a widget and returns 201" do
125
+ post "/api/widgets", params: valid_params, headers: json_headers
126
+
127
+ expect(response).to have_http_status(:created)
128
+ expect(response.content_type).to include("application/json")
129
+ end
130
+
131
+ it "returns the created widget" do
132
+ post "/api/widgets", params: valid_params, headers: json_headers
133
+
134
+ json = JSON.parse(response.body, symbolize_names: true)
135
+ expect(json[:name]).to eq("New Widget")
136
+ end
137
+ end
138
+ end
139
+
140
+ # Authentication in request specs
141
+ RSpec.describe "Authenticated requests", type: :request do
142
+ describe "GET /dashboard" do
143
+ context "when not authenticated" do
144
+ it "redirects to login" do
145
+ get "/dashboard"
146
+ expect(response).to redirect_to(login_path)
147
+ end
148
+ end
149
+
150
+ context "when authenticated" do
151
+ let(:user) { create(:user) }
152
+
153
+ before { sign_in(user) }
154
+
155
+ it "shows the dashboard" do
156
+ get "/dashboard"
157
+ expect(response).to have_http_status(:ok)
158
+ end
159
+ end
160
+ end
161
+ end
162
+
163
+ # Token-based API authentication
164
+ RSpec.describe "API Authentication", type: :request do
165
+ let(:user) { create(:user) }
166
+ let(:token) { user.api_token }
167
+
168
+ describe "GET /api/profile" do
169
+ context "without token" do
170
+ it "returns unauthorized" do
171
+ get "/api/profile"
172
+ expect(response).to have_http_status(:unauthorized)
173
+ end
174
+ end
175
+
176
+ context "with valid token" do
177
+ it "returns the profile" do
178
+ get "/api/profile", headers: { "Authorization" => "Bearer #{token}" }
179
+ expect(response).to have_http_status(:ok)
180
+ end
181
+ end
182
+
183
+ context "with invalid token" do
184
+ it "returns unauthorized" do
185
+ get "/api/profile", headers: { "Authorization" => "Bearer invalid" }
186
+ expect(response).to have_http_status(:unauthorized)
187
+ end
188
+ end
189
+ end
190
+ end
191
+
192
+ # Subdomain testing
193
+ RSpec.describe "API subdomain", type: :request do
194
+ before { host! "api.example.com" }
195
+
196
+ describe "GET /widgets" do
197
+ let!(:widgets) { create_list(:widget, 2) }
198
+
199
+ it "serves JSON from api subdomain" do
200
+ get "/widgets", headers: { "ACCEPT" => "application/json" }
201
+
202
+ expect(response).to have_http_status(:ok)
203
+ expect(response.content_type).to start_with("application/json")
204
+ end
205
+ end
206
+ end
207
+
208
+ # Testing response headers
209
+ RSpec.describe "Response headers", type: :request do
210
+ describe "GET /api/widgets" do
211
+ it "includes pagination headers" do
212
+ create_list(:widget, 25)
213
+ get "/api/widgets"
214
+
215
+ expect(response.headers["X-Total-Count"]).to eq("25")
216
+ expect(response.headers["X-Page"]).to eq("1")
217
+ end
218
+ end
219
+
220
+ describe "caching headers" do
221
+ it "sets cache-control for public resources" do
222
+ widget = create(:widget, :published)
223
+ get "/widgets/#{widget.id}"
224
+
225
+ expect(response.headers["Cache-Control"]).to include("public")
226
+ end
227
+ end
228
+ end
229
+
230
+ # Multipart file upload
231
+ RSpec.describe "File uploads", type: :request do
232
+ describe "POST /documents" do
233
+ let(:file) { fixture_file_upload("spec/fixtures/document.pdf", "application/pdf") }
234
+
235
+ it "accepts file upload" do
236
+ post "/documents", params: { document: { file: } }
237
+
238
+ expect(response).to redirect_to(documents_path)
239
+ expect(Document.last.file).to be_attached
240
+ end
241
+ end
242
+ end
243
+
244
+ # Testing redirects with follow_redirect!
245
+ RSpec.describe "Redirect chains", type: :request do
246
+ describe "POST /login" do
247
+ let(:user) { create(:user, password: "secret") }
248
+
249
+ it "redirects to dashboard after login" do
250
+ post "/login", params: { email: user.email, password: "secret" }
251
+
252
+ expect(response).to redirect_to(dashboard_path)
253
+ follow_redirect!
254
+ expect(response.body).to include("Welcome back")
255
+ end
256
+ end
257
+ end
258
+
259
+ # Helper for JSON response parsing
260
+ RSpec.describe "Widgets API", type: :request do
261
+ def json_response
262
+ JSON.parse(response.body, symbolize_names: true)
263
+ end
264
+
265
+ describe "GET /api/widgets/:id" do
266
+ let(:widget) { create(:widget, name: "Test") }
267
+
268
+ it "returns widget attributes" do
269
+ get "/api/widgets/#{widget.id}", headers: { "ACCEPT" => "application/json" }
270
+
271
+ expect(json_response[:name]).to eq("Test")
272
+ expect(json_response[:id]).to eq(widget.id)
273
+ end
274
+ end
275
+ end