rubyn-code 0.1.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 (235) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +620 -0
  4. data/db/migrations/000_create_schema_migrations.sql +4 -0
  5. data/db/migrations/001_create_sessions.sql +16 -0
  6. data/db/migrations/002_create_messages.sql +16 -0
  7. data/db/migrations/003_create_tasks.sql +17 -0
  8. data/db/migrations/004_create_task_dependencies.sql +8 -0
  9. data/db/migrations/005_create_memories.sql +44 -0
  10. data/db/migrations/006_create_cost_records.sql +16 -0
  11. data/db/migrations/007_create_hooks.sql +12 -0
  12. data/db/migrations/008_create_skills_cache.sql +8 -0
  13. data/db/migrations/009_create_teams.sql +27 -0
  14. data/db/migrations/010_create_instincts.sql +15 -0
  15. data/exe/rubyn-code +6 -0
  16. data/lib/rubyn_code/agent/conversation.rb +193 -0
  17. data/lib/rubyn_code/agent/loop.rb +517 -0
  18. data/lib/rubyn_code/agent/loop_detector.rb +78 -0
  19. data/lib/rubyn_code/auth/oauth.rb +174 -0
  20. data/lib/rubyn_code/auth/server.rb +126 -0
  21. data/lib/rubyn_code/auth/token_store.rb +153 -0
  22. data/lib/rubyn_code/autonomous/daemon.rb +233 -0
  23. data/lib/rubyn_code/autonomous/idle_poller.rb +111 -0
  24. data/lib/rubyn_code/autonomous/task_claimer.rb +100 -0
  25. data/lib/rubyn_code/background/job.rb +19 -0
  26. data/lib/rubyn_code/background/notifier.rb +44 -0
  27. data/lib/rubyn_code/background/worker.rb +146 -0
  28. data/lib/rubyn_code/cli/app.rb +118 -0
  29. data/lib/rubyn_code/cli/input_handler.rb +79 -0
  30. data/lib/rubyn_code/cli/renderer.rb +205 -0
  31. data/lib/rubyn_code/cli/repl.rb +519 -0
  32. data/lib/rubyn_code/cli/spinner.rb +100 -0
  33. data/lib/rubyn_code/cli/stream_formatter.rb +149 -0
  34. data/lib/rubyn_code/config/defaults.rb +43 -0
  35. data/lib/rubyn_code/config/project_config.rb +120 -0
  36. data/lib/rubyn_code/config/settings.rb +127 -0
  37. data/lib/rubyn_code/context/auto_compact.rb +81 -0
  38. data/lib/rubyn_code/context/compactor.rb +89 -0
  39. data/lib/rubyn_code/context/manager.rb +91 -0
  40. data/lib/rubyn_code/context/manual_compact.rb +87 -0
  41. data/lib/rubyn_code/context/micro_compact.rb +135 -0
  42. data/lib/rubyn_code/db/connection.rb +176 -0
  43. data/lib/rubyn_code/db/migrator.rb +146 -0
  44. data/lib/rubyn_code/db/schema.rb +106 -0
  45. data/lib/rubyn_code/hooks/built_in.rb +124 -0
  46. data/lib/rubyn_code/hooks/registry.rb +99 -0
  47. data/lib/rubyn_code/hooks/runner.rb +88 -0
  48. data/lib/rubyn_code/hooks/user_hooks.rb +90 -0
  49. data/lib/rubyn_code/learning/extractor.rb +191 -0
  50. data/lib/rubyn_code/learning/injector.rb +138 -0
  51. data/lib/rubyn_code/learning/instinct.rb +172 -0
  52. data/lib/rubyn_code/llm/client.rb +218 -0
  53. data/lib/rubyn_code/llm/message_builder.rb +116 -0
  54. data/lib/rubyn_code/llm/streaming.rb +203 -0
  55. data/lib/rubyn_code/mcp/client.rb +139 -0
  56. data/lib/rubyn_code/mcp/config.rb +83 -0
  57. data/lib/rubyn_code/mcp/sse_transport.rb +225 -0
  58. data/lib/rubyn_code/mcp/stdio_transport.rb +196 -0
  59. data/lib/rubyn_code/mcp/tool_bridge.rb +164 -0
  60. data/lib/rubyn_code/memory/models.rb +62 -0
  61. data/lib/rubyn_code/memory/search.rb +181 -0
  62. data/lib/rubyn_code/memory/session_persistence.rb +194 -0
  63. data/lib/rubyn_code/memory/store.rb +199 -0
  64. data/lib/rubyn_code/observability/budget_enforcer.rb +159 -0
  65. data/lib/rubyn_code/observability/cost_calculator.rb +61 -0
  66. data/lib/rubyn_code/observability/models.rb +29 -0
  67. data/lib/rubyn_code/observability/token_counter.rb +42 -0
  68. data/lib/rubyn_code/observability/usage_reporter.rb +140 -0
  69. data/lib/rubyn_code/output/diff_renderer.rb +212 -0
  70. data/lib/rubyn_code/output/formatter.rb +120 -0
  71. data/lib/rubyn_code/permissions/deny_list.rb +49 -0
  72. data/lib/rubyn_code/permissions/policy.rb +59 -0
  73. data/lib/rubyn_code/permissions/prompter.rb +80 -0
  74. data/lib/rubyn_code/permissions/tier.rb +22 -0
  75. data/lib/rubyn_code/protocols/interrupt_handler.rb +95 -0
  76. data/lib/rubyn_code/protocols/plan_approval.rb +67 -0
  77. data/lib/rubyn_code/protocols/shutdown_handshake.rb +109 -0
  78. data/lib/rubyn_code/skills/catalog.rb +70 -0
  79. data/lib/rubyn_code/skills/document.rb +80 -0
  80. data/lib/rubyn_code/skills/loader.rb +57 -0
  81. data/lib/rubyn_code/sub_agents/runner.rb +168 -0
  82. data/lib/rubyn_code/sub_agents/summarizer.rb +57 -0
  83. data/lib/rubyn_code/tasks/dag.rb +208 -0
  84. data/lib/rubyn_code/tasks/manager.rb +212 -0
  85. data/lib/rubyn_code/tasks/models.rb +31 -0
  86. data/lib/rubyn_code/teams/mailbox.rb +128 -0
  87. data/lib/rubyn_code/teams/manager.rb +175 -0
  88. data/lib/rubyn_code/teams/teammate.rb +38 -0
  89. data/lib/rubyn_code/tools/background_run.rb +41 -0
  90. data/lib/rubyn_code/tools/base.rb +84 -0
  91. data/lib/rubyn_code/tools/bash.rb +81 -0
  92. data/lib/rubyn_code/tools/bundle_add.rb +53 -0
  93. data/lib/rubyn_code/tools/bundle_install.rb +41 -0
  94. data/lib/rubyn_code/tools/compact.rb +57 -0
  95. data/lib/rubyn_code/tools/db_migrate.rb +52 -0
  96. data/lib/rubyn_code/tools/edit_file.rb +49 -0
  97. data/lib/rubyn_code/tools/executor.rb +62 -0
  98. data/lib/rubyn_code/tools/git_commit.rb +97 -0
  99. data/lib/rubyn_code/tools/git_diff.rb +61 -0
  100. data/lib/rubyn_code/tools/git_log.rb +59 -0
  101. data/lib/rubyn_code/tools/git_status.rb +59 -0
  102. data/lib/rubyn_code/tools/glob.rb +44 -0
  103. data/lib/rubyn_code/tools/grep.rb +81 -0
  104. data/lib/rubyn_code/tools/load_skill.rb +41 -0
  105. data/lib/rubyn_code/tools/memory_search.rb +77 -0
  106. data/lib/rubyn_code/tools/memory_write.rb +52 -0
  107. data/lib/rubyn_code/tools/rails_generate.rb +54 -0
  108. data/lib/rubyn_code/tools/read_file.rb +38 -0
  109. data/lib/rubyn_code/tools/read_inbox.rb +64 -0
  110. data/lib/rubyn_code/tools/registry.rb +48 -0
  111. data/lib/rubyn_code/tools/review_pr.rb +145 -0
  112. data/lib/rubyn_code/tools/run_specs.rb +75 -0
  113. data/lib/rubyn_code/tools/schema.rb +59 -0
  114. data/lib/rubyn_code/tools/send_message.rb +53 -0
  115. data/lib/rubyn_code/tools/spawn_agent.rb +154 -0
  116. data/lib/rubyn_code/tools/spawn_teammate.rb +168 -0
  117. data/lib/rubyn_code/tools/task.rb +148 -0
  118. data/lib/rubyn_code/tools/web_fetch.rb +108 -0
  119. data/lib/rubyn_code/tools/web_search.rb +196 -0
  120. data/lib/rubyn_code/tools/write_file.rb +30 -0
  121. data/lib/rubyn_code/version.rb +5 -0
  122. data/lib/rubyn_code.rb +203 -0
  123. data/skills/code_quality/fits_in_your_head.md +189 -0
  124. data/skills/code_quality/naming_conventions.md +213 -0
  125. data/skills/code_quality/null_object.md +205 -0
  126. data/skills/code_quality/technical_debt.md +135 -0
  127. data/skills/code_quality/value_objects.md +216 -0
  128. data/skills/code_quality/yagni.md +176 -0
  129. data/skills/design_patterns/adapter.md +191 -0
  130. data/skills/design_patterns/bridge_memento_visitor.md +254 -0
  131. data/skills/design_patterns/builder.md +158 -0
  132. data/skills/design_patterns/command.md +126 -0
  133. data/skills/design_patterns/composite.md +147 -0
  134. data/skills/design_patterns/decorator.md +204 -0
  135. data/skills/design_patterns/facade.md +133 -0
  136. data/skills/design_patterns/factory_method.md +169 -0
  137. data/skills/design_patterns/iterator.md +116 -0
  138. data/skills/design_patterns/mediator.md +133 -0
  139. data/skills/design_patterns/observer.md +177 -0
  140. data/skills/design_patterns/proxy.md +140 -0
  141. data/skills/design_patterns/singleton.md +124 -0
  142. data/skills/design_patterns/state.md +207 -0
  143. data/skills/design_patterns/strategy.md +127 -0
  144. data/skills/design_patterns/template_method.md +173 -0
  145. data/skills/gems/devise.md +365 -0
  146. data/skills/gems/dry_rb.md +186 -0
  147. data/skills/gems/factory_bot.md +268 -0
  148. data/skills/gems/faraday.md +263 -0
  149. data/skills/gems/graphql_ruby.md +514 -0
  150. data/skills/gems/pundit.md +446 -0
  151. data/skills/gems/redis.md +219 -0
  152. data/skills/gems/rubocop.md +257 -0
  153. data/skills/gems/sidekiq.md +360 -0
  154. data/skills/gems/stripe.md +224 -0
  155. data/skills/minitest/assertions.md +185 -0
  156. data/skills/minitest/fixtures.md +238 -0
  157. data/skills/minitest/integration_tests.md +210 -0
  158. data/skills/minitest/mailers_and_jobs.md +218 -0
  159. data/skills/minitest/mocking_stubbing.md +202 -0
  160. data/skills/minitest/service_tests_and_performance.md +246 -0
  161. data/skills/minitest/structure_and_conventions.md +169 -0
  162. data/skills/minitest/system_tests.md +237 -0
  163. data/skills/rails/action_cable.md +160 -0
  164. data/skills/rails/active_record_basics.md +174 -0
  165. data/skills/rails/active_storage.md +242 -0
  166. data/skills/rails/api_design.md +212 -0
  167. data/skills/rails/associations.md +182 -0
  168. data/skills/rails/background_jobs.md +212 -0
  169. data/skills/rails/caching.md +158 -0
  170. data/skills/rails/callbacks.md +135 -0
  171. data/skills/rails/concerns_controllers.md +218 -0
  172. data/skills/rails/concerns_models.md +280 -0
  173. data/skills/rails/controllers.md +190 -0
  174. data/skills/rails/engines.md +201 -0
  175. data/skills/rails/form_objects.md +168 -0
  176. data/skills/rails/hotwire.md +229 -0
  177. data/skills/rails/internationalization.md +192 -0
  178. data/skills/rails/logging.md +198 -0
  179. data/skills/rails/mailers.md +180 -0
  180. data/skills/rails/migrations.md +200 -0
  181. data/skills/rails/multitenancy.md +207 -0
  182. data/skills/rails/n_plus_one.md +151 -0
  183. data/skills/rails/presenters.md +244 -0
  184. data/skills/rails/query_objects.md +177 -0
  185. data/skills/rails/routing.md +194 -0
  186. data/skills/rails/scopes.md +187 -0
  187. data/skills/rails/security.md +233 -0
  188. data/skills/rails/serializers.md +243 -0
  189. data/skills/rails/service_objects.md +184 -0
  190. data/skills/rails/testing_strategy.md +258 -0
  191. data/skills/rails/validations.md +206 -0
  192. data/skills/refactoring/code_smells.md +251 -0
  193. data/skills/refactoring/command_query_separation.md +166 -0
  194. data/skills/refactoring/encapsulate_collection.md +125 -0
  195. data/skills/refactoring/extract_class.md +138 -0
  196. data/skills/refactoring/extract_method.md +185 -0
  197. data/skills/refactoring/replace_conditional.md +211 -0
  198. data/skills/refactoring/value_objects.md +246 -0
  199. data/skills/rspec/build_stubbed.md +199 -0
  200. data/skills/rspec/factory_design.md +206 -0
  201. data/skills/rspec/let_vs_let_bang.md +161 -0
  202. data/skills/rspec/mocking_stubbing.md +209 -0
  203. data/skills/rspec/request_specs.md +212 -0
  204. data/skills/rspec/service_specs.md +262 -0
  205. data/skills/rspec/shared_examples.md +244 -0
  206. data/skills/rspec/system_specs.md +286 -0
  207. data/skills/rspec/test_performance.md +215 -0
  208. data/skills/ruby/blocks_procs_lambdas.md +204 -0
  209. data/skills/ruby/classes.md +155 -0
  210. data/skills/ruby/concurrency.md +194 -0
  211. data/skills/ruby/data_struct_openstruct.md +158 -0
  212. data/skills/ruby/debugging_profiling.md +204 -0
  213. data/skills/ruby/enumerable_patterns.md +168 -0
  214. data/skills/ruby/exception_handling.md +199 -0
  215. data/skills/ruby/file_io.md +217 -0
  216. data/skills/ruby/hashes.md +195 -0
  217. data/skills/ruby/metaprogramming.md +170 -0
  218. data/skills/ruby/modules.md +210 -0
  219. data/skills/ruby/pattern_matching.md +177 -0
  220. data/skills/ruby/regular_expressions.md +166 -0
  221. data/skills/ruby/result_objects.md +200 -0
  222. data/skills/ruby/strings.md +177 -0
  223. data/skills/ruby_project/bundler_dependencies.md +181 -0
  224. data/skills/ruby_project/cli_tools.md +224 -0
  225. data/skills/ruby_project/rake_tasks.md +146 -0
  226. data/skills/ruby_project/structure.md +261 -0
  227. data/skills/sinatra/application_structure.md +241 -0
  228. data/skills/sinatra/middleware_and_deployment.md +221 -0
  229. data/skills/sinatra/testing.md +233 -0
  230. data/skills/solid/dependency_inversion.md +195 -0
  231. data/skills/solid/interface_segregation.md +237 -0
  232. data/skills/solid/liskov_substitution.md +263 -0
  233. data/skills/solid/open_closed.md +212 -0
  234. data/skills/solid/single_responsibility.md +183 -0
  235. metadata +397 -0
@@ -0,0 +1,209 @@
1
+ # RSpec: Mocking and Stubbing
2
+
3
+ ## Pattern
4
+
5
+ Use `instance_double` for type-safe mocks. Stub external dependencies, not the object under test. Prefer dependency injection over global stubs. Use `allow` for setup, `expect` for assertions.
6
+
7
+ ```ruby
8
+ # GOOD: instance_double verifies the interface exists
9
+ RSpec.describe Orders::CreateService do
10
+ let(:user) { build_stubbed(:user) }
11
+ let(:mailer) { instance_double(OrderMailer) }
12
+ let(:message) { instance_double(ActionMailer::MessageDelivery) }
13
+
14
+ before do
15
+ allow(OrderMailer).to receive(:confirmation).and_return(message)
16
+ allow(message).to receive(:deliver_later)
17
+ end
18
+
19
+ it "sends a confirmation email" do
20
+ expect(OrderMailer).to receive(:confirmation).with(an_instance_of(Order))
21
+ described_class.call(valid_params, user)
22
+ end
23
+ end
24
+ ```
25
+
26
+ ```ruby
27
+ # GOOD: Stub external HTTP dependency
28
+ RSpec.describe Embeddings::EmbeddingClient do
29
+ let(:client) { described_class.new(base_url: "http://localhost:8000") }
30
+
31
+ before do
32
+ stub_request(:post, "http://localhost:8000/embed")
33
+ .with(body: hash_including("texts"))
34
+ .to_return(
35
+ status: 200,
36
+ body: { embeddings: [[0.1, 0.2, 0.3]], dimensions: 1024, count: 1 }.to_json,
37
+ headers: { "Content-Type" => "application/json" }
38
+ )
39
+ end
40
+
41
+ it "returns embeddings from the service" do
42
+ result = client.embed(["def hello; end"])
43
+ expect(result.first.length).to eq(3)
44
+ end
45
+ end
46
+ ```
47
+
48
+ ```ruby
49
+ # GOOD: Dependency injection makes stubbing natural
50
+ class Orders::CreateService
51
+ def initialize(mailer: OrderMailer, notifier: WarehouseNotifier)
52
+ @mailer = mailer
53
+ @notifier = notifier
54
+ end
55
+
56
+ def call(params, user)
57
+ order = user.orders.create!(params)
58
+ @mailer.confirmation(order).deliver_later
59
+ @notifier.notify(order)
60
+ order
61
+ end
62
+ end
63
+
64
+ # Test: inject doubles instead of patching globals
65
+ RSpec.describe Orders::CreateService do
66
+ let(:mailer) { instance_double(OrderMailer) }
67
+ let(:notifier) { instance_double(WarehouseNotifier) }
68
+ let(:service) { described_class.new(mailer: mailer, notifier: notifier) }
69
+
70
+ before do
71
+ allow(mailer).to receive_message_chain(:confirmation, :deliver_later)
72
+ allow(notifier).to receive(:notify)
73
+ end
74
+
75
+ it "notifies the warehouse" do
76
+ expect(notifier).to receive(:notify).with(an_instance_of(Order))
77
+ service.call(valid_params, user)
78
+ end
79
+ end
80
+ ```
81
+
82
+ `allow` vs `expect`:
83
+
84
+ ```ruby
85
+ # allow: Setup — "if this gets called, return this"
86
+ # No failure if it's never called
87
+ allow(service).to receive(:call).and_return(result)
88
+
89
+ # expect: Assertion — "this MUST be called"
90
+ # Fails if it's never called
91
+ expect(service).to receive(:call).with(expected_args)
92
+ ```
93
+
94
+ ## Why This Is Good
95
+
96
+ - **`instance_double` catches interface drift.** If you rename `OrderMailer#confirmation` to `OrderMailer#order_confirmation`, tests using `instance_double(OrderMailer)` that stub `:confirmation` immediately fail. A plain `double` wouldn't catch this — the test would pass while production breaks.
97
+ - **Stubbing externals isolates the unit.** The service test doesn't depend on a running email server, a warehouse API, or an embedding service. It tests the orchestration logic in isolation.
98
+ - **Dependency injection is better than global patching.** `allow(OrderMailer).to receive(...)` patches a global constant. Injecting a double via the constructor is explicit, doesn't affect other tests, and doesn't depend on load order.
99
+ - **`allow` for setup, `expect` for assertions** keeps intent clear. Setup stubs say "the world looks like this." Assertion mocks say "this thing must happen."
100
+ - **WebMock for HTTP.** `stub_request` prevents real HTTP calls in tests, returns controlled responses, and verifies the request was made correctly.
101
+
102
+ ## Anti-Pattern
103
+
104
+ Mocking the object under test, overuse of `any_instance`, and testing implementation details:
105
+
106
+ ```ruby
107
+ # BAD: Stubbing the object under test
108
+ RSpec.describe Order do
109
+ it "calculates total" do
110
+ order = build(:order)
111
+ allow(order).to receive(:line_items).and_return([
112
+ double(total: 10), double(total: 20)
113
+ ])
114
+ expect(order.calculate_total).to eq(30)
115
+ end
116
+ end
117
+
118
+ # BAD: any_instance_of — fragile, global, affects all instances
119
+ RSpec.describe OrdersController do
120
+ it "creates an order" do
121
+ allow_any_instance_of(Order).to receive(:save).and_return(true)
122
+ post :create, params: { order: valid_params }
123
+ expect(response).to redirect_to(orders_path)
124
+ end
125
+ end
126
+
127
+ # BAD: Testing method call sequence — implementation detail
128
+ RSpec.describe Orders::CreateService do
129
+ it "creates then sends then notifies" do
130
+ expect(Order).to receive(:create!).ordered
131
+ expect(OrderMailer).to receive(:confirmation).ordered
132
+ expect(WarehouseNotifier).to receive(:notify).ordered
133
+ described_class.call(params, user)
134
+ end
135
+ end
136
+
137
+ # BAD: Plain doubles with no type checking
138
+ let(:user) { double("User", name: "Alice", save: true, banana: "yellow") }
139
+ # "banana" isn't a User method — double won't catch this
140
+ ```
141
+
142
+ ## Why This Is Bad
143
+
144
+ - **Stubbing the object under test.** If you stub `order.line_items`, you're not testing `calculate_total` against real data — you're testing that it sums a stubbed array. The real method might have a bug in how it queries line items, and you'll never know.
145
+ - **`any_instance_of` is global.** It affects every instance of the class in the entire test, including instances created inside the code under test. It's unpredictable, hard to scope, and a sign of untestable design.
146
+ - **Testing call order is brittle.** If someone reorders the operations (notify before mail, or in parallel), the test breaks even though the behavior is correct. Test outcomes, not sequence.
147
+ - **Plain doubles don't verify interfaces.** `double("User", banana: "yellow")` creates a fake that responds to `:banana`. If `User` doesn't have a `banana` method, you'll never know until production. `instance_double(User)` would catch this immediately.
148
+
149
+ ## When To Apply
150
+
151
+ - **Stub external services.** HTTP APIs, email delivery, file storage, third-party SDKs — anything outside your application boundary. Use WebMock for HTTP, instance_double for Ruby dependencies.
152
+ - **Stub slow operations in unit tests.** Database queries in a service spec that's testing logic, not persistence. But prefer `build_stubbed` over mocking AR.
153
+ - **Use `expect(...).to receive` when verifying side effects.** "Did the mailer get called?" is a legitimate assertion. "Did the service call `create!` then `deliver_later` in that order?" is not.
154
+ - **Inject dependencies** when a class collaborates with external services. Constructor injection (`def initialize(mailer:)`) makes testing trivial.
155
+
156
+ ## When NOT To Apply
157
+
158
+ - **Don't mock what you can build.** `build_stubbed(:user)` is better than `instance_double(User)` when you need a realistic user object. Doubles are for collaborators you want to isolate from, not for the subject's own data.
159
+ - **Don't mock ActiveRecord queries in model specs.** If you're testing a scope, run the real query against the test database. Mocking `where` defeats the purpose.
160
+ - **Don't use mocks in integration/system tests.** These tests exist to verify the full stack. Mocking within them undermines their value.
161
+ - **Don't mock more than 2-3 dependencies.** If a test needs 5 mocks to set up, the class under test has too many dependencies. Refactor the class before adding more mocks.
162
+
163
+ ## Edge Cases
164
+
165
+ **`class_double` for class method stubbing:**
166
+
167
+ ```ruby
168
+ auth_service = class_double(AuthService, verify: true)
169
+ stub_const("AuthService", auth_service)
170
+ expect(auth_service).to receive(:verify).with("token").and_return(user)
171
+ ```
172
+
173
+ **`receive_message_chain` — use sparingly:**
174
+
175
+ ```ruby
176
+ # Acceptable for mailer chains
177
+ allow(OrderMailer).to receive_message_chain(:confirmation, :deliver_later)
178
+
179
+ # NOT acceptable for business logic chains — sign of Law of Demeter violation
180
+ allow(order).to receive_message_chain(:user, :company, :billing_address, :country)
181
+ # Fix the code: order.billing_country instead of 4-deep chain
182
+ ```
183
+
184
+ **Verifying arguments:**
185
+
186
+ ```ruby
187
+ expect(mailer).to receive(:confirmation).with(
188
+ having_attributes(id: order.id, total: 100)
189
+ )
190
+
191
+ expect(client).to receive(:post).with(
192
+ "/api/v1/orders",
193
+ hash_including(status: "pending")
194
+ )
195
+ ```
196
+
197
+ **Spy pattern (assert after the fact):**
198
+
199
+ ```ruby
200
+ notifier = instance_double(WarehouseNotifier)
201
+ allow(notifier).to receive(:notify)
202
+
203
+ service = described_class.new(notifier: notifier)
204
+ service.call(params, user)
205
+
206
+ expect(notifier).to have_received(:notify).with(an_instance_of(Order))
207
+ ```
208
+
209
+ This is useful when you want `allow` in setup and assertion at the end, rather than `expect` before the action.
@@ -0,0 +1,212 @@
1
+ # RSpec: Request Specs
2
+
3
+ ## Pattern
4
+
5
+ Test controllers through request specs, not controller specs. Request specs exercise the full middleware stack — routing, params parsing, authentication, the action, and the response — giving you confidence the endpoint works end to end.
6
+
7
+ ```ruby
8
+ # spec/requests/orders_spec.rb
9
+ RSpec.describe "Orders", type: :request do
10
+ let(:user) { create(:user) }
11
+
12
+ before { sign_in user }
13
+
14
+ describe "GET /orders" do
15
+ it "returns the user's orders" do
16
+ create_list(:order, 3, user: user)
17
+ create(:order) # belongs to another user
18
+
19
+ get orders_path
20
+
21
+ expect(response).to have_http_status(:ok)
22
+ expect(response.body).to include("3 orders")
23
+ end
24
+ end
25
+
26
+ describe "POST /orders" do
27
+ let(:product) { create(:product, stock: 10) }
28
+ let(:valid_params) do
29
+ {
30
+ order: {
31
+ shipping_address: "123 Main St",
32
+ line_items_attributes: [{ product_id: product.id, quantity: 2 }]
33
+ }
34
+ }
35
+ end
36
+
37
+ context "with valid params" do
38
+ it "creates an order" do
39
+ expect {
40
+ post orders_path, params: valid_params
41
+ }.to change(Order, :count).by(1)
42
+ end
43
+
44
+ it "redirects to the order" do
45
+ post orders_path, params: valid_params
46
+ expect(response).to redirect_to(Order.last)
47
+ end
48
+ end
49
+
50
+ context "with invalid params" do
51
+ it "returns unprocessable entity" do
52
+ post orders_path, params: { order: { shipping_address: "" } }
53
+ expect(response).to have_http_status(:unprocessable_entity)
54
+ end
55
+
56
+ it "does not create an order" do
57
+ expect {
58
+ post orders_path, params: { order: { shipping_address: "" } }
59
+ }.not_to change(Order, :count)
60
+ end
61
+ end
62
+ end
63
+
64
+ describe "GET /orders/:id" do
65
+ it "returns the order" do
66
+ order = create(:order, user: user)
67
+ get order_path(order)
68
+ expect(response).to have_http_status(:ok)
69
+ end
70
+
71
+ it "returns not found for another user's order" do
72
+ other_order = create(:order)
73
+ expect {
74
+ get order_path(other_order)
75
+ }.to raise_error(ActiveRecord::RecordNotFound)
76
+ end
77
+ end
78
+
79
+ describe "DELETE /orders/:id" do
80
+ it "destroys the order" do
81
+ order = create(:order, user: user)
82
+ expect {
83
+ delete order_path(order)
84
+ }.to change(Order, :count).by(-1)
85
+ end
86
+
87
+ it "redirects to the index" do
88
+ order = create(:order, user: user)
89
+ delete order_path(order)
90
+ expect(response).to redirect_to(orders_path)
91
+ end
92
+ end
93
+ end
94
+ ```
95
+
96
+ For JSON APIs:
97
+
98
+ ```ruby
99
+ RSpec.describe "API::V1::Orders", type: :request do
100
+ let(:user) { create(:user) }
101
+ let(:headers) { { "Authorization" => "Bearer #{user.api_token}" } }
102
+
103
+ describe "GET /api/v1/orders" do
104
+ it "returns orders as JSON" do
105
+ create_list(:order, 2, user: user)
106
+
107
+ get "/api/v1/orders", headers: headers
108
+
109
+ expect(response).to have_http_status(:ok)
110
+ json = JSON.parse(response.body)
111
+ expect(json["orders"].length).to eq(2)
112
+ end
113
+ end
114
+
115
+ describe "POST /api/v1/orders" do
116
+ it "creates and returns the order" do
117
+ post "/api/v1/orders", params: valid_params.to_json,
118
+ headers: headers.merge("Content-Type" => "application/json")
119
+
120
+ expect(response).to have_http_status(:created)
121
+ json = JSON.parse(response.body)
122
+ expect(json["order"]["id"]).to be_present
123
+ end
124
+ end
125
+ end
126
+ ```
127
+
128
+ ## Why This Is Good
129
+
130
+ - **Tests what the user experiences.** A request spec hits the same code path as a real browser or API client. Routing, middleware, authentication, params parsing, the action, and the response are all exercised.
131
+ - **Catches integration bugs.** A controller spec might pass with the correct params, but a request spec catches a broken route, a missing authentication check, or a middleware that strips a header.
132
+ - **Rails official recommendation.** Since Rails 5, the Rails team recommends request specs over controller specs. Controller specs are considered legacy.
133
+ - **Simpler setup.** No `assigns` or `controller` objects to reason about. Just HTTP verbs, paths, params, and response assertions.
134
+
135
+ ## Anti-Pattern
136
+
137
+ Using controller specs with `assigns` and internal assertions:
138
+
139
+ ```ruby
140
+ # LEGACY — do not write new tests this way
141
+ RSpec.describe OrdersController, type: :controller do
142
+ describe "GET #index" do
143
+ it "assigns @orders" do
144
+ order = create(:order)
145
+ get :index
146
+ expect(assigns(:orders)).to include(order)
147
+ end
148
+
149
+ it "renders the index template" do
150
+ get :index
151
+ expect(response).to render_template(:index)
152
+ end
153
+ end
154
+
155
+ describe "POST #create" do
156
+ it "assigns a new order" do
157
+ post :create, params: { order: valid_attributes }
158
+ expect(assigns(:order)).to be_a(Order)
159
+ expect(assigns(:order)).to be_persisted
160
+ end
161
+ end
162
+ end
163
+ ```
164
+
165
+ ## Why This Is Bad
166
+
167
+ - **Tests implementation, not behavior.** `assigns(:orders)` tests that the controller set an instance variable — an implementation detail. The user doesn't care about instance variables; they care about what the page contains.
168
+ - **Skips the middleware stack.** Controller specs bypass routing, Rack middleware, and Devise authentication. A test can pass even if the route is broken or auth is misconfigured.
169
+ - **`assigns` is deprecated.** Rails removed `assigns` from the default stack. You need the `rails-controller-testing` gem to use it, which is a sign you're going against the grain.
170
+ - **Brittle.** If you rename an instance variable from `@orders` to `@user_orders`, every controller spec breaks even though the behavior is unchanged.
171
+
172
+ ## When To Apply
173
+
174
+ - **Every controller endpoint gets a request spec.** This is not optional. If there's a route, there's a request spec.
175
+ - **Test the happy path and the primary failure path for each action.** Create → success + validation failure. Update → success + validation failure. Show → found + not found. Index → with data + empty.
176
+ - **Test authentication and authorization.** Unauthenticated access returns 401. Accessing another user's resource returns 404 (scoped query) or 403 (authorization check).
177
+
178
+ ## When NOT To Apply
179
+
180
+ - **Don't test framework behavior.** Don't test that `before_action :authenticate_user!` calls Devise. Test that an unauthenticated request returns 401. The mechanism doesn't matter — the outcome does.
181
+ - **Don't test rendering details in request specs.** Use view specs or system specs for "the page shows the order total." Request specs check status codes, redirects, and JSON structure.
182
+ - **Don't test service object logic in request specs.** If `Orders::CreateService` has complex business logic, test it in a service spec. The request spec just verifies the controller delegates correctly and handles the result.
183
+
184
+ ## Edge Cases
185
+
186
+ **Testing file uploads:**
187
+ Use `fixture_file_upload`:
188
+
189
+ ```ruby
190
+ it "accepts an attachment" do
191
+ file = fixture_file_upload("receipt.pdf", "application/pdf")
192
+ post orders_path, params: { order: { receipt: file, **valid_params } }
193
+ expect(response).to redirect_to(Order.last)
194
+ end
195
+ ```
196
+
197
+ **Testing streaming responses:**
198
+ Request specs receive the full response after streaming completes. If you need to test the streaming behavior itself, use a system spec with Capybara.
199
+
200
+ **Shared authentication setup:**
201
+ Use a shared context to DRY up auth:
202
+
203
+ ```ruby
204
+ RSpec.shared_context "authenticated user" do
205
+ let(:user) { create(:user) }
206
+ before { sign_in user }
207
+ end
208
+
209
+ RSpec.describe "Orders", type: :request do
210
+ include_context "authenticated user"
211
+ end
212
+ ```
@@ -0,0 +1,262 @@
1
+ # RSpec: Testing Service Objects
2
+
3
+ ## Pattern
4
+
5
+ Test service objects in isolation. Pass in dependencies as doubles. Assert on the result object, not on implementation details. Test the happy path, each failure mode, and edge cases.
6
+
7
+ ```ruby
8
+ # spec/services/orders/create_service_spec.rb
9
+ RSpec.describe Orders::CreateService do
10
+ let(:user) { create(:user) }
11
+ let(:valid_params) do
12
+ {
13
+ shipping_address: "123 Main St",
14
+ line_items_attributes: [
15
+ { product_id: product.id, quantity: 2 }
16
+ ]
17
+ }
18
+ end
19
+ let(:product) { create(:product, stock: 10, price: 25.00) }
20
+
21
+ describe ".call" do
22
+ context "with valid params and sufficient stock" do
23
+ it "returns a successful result" do
24
+ result = described_class.call(valid_params, user)
25
+ expect(result).to be_success
26
+ end
27
+
28
+ it "creates an order" do
29
+ expect { described_class.call(valid_params, user) }
30
+ .to change(Order, :count).by(1)
31
+ end
32
+
33
+ it "creates the order for the correct user" do
34
+ result = described_class.call(valid_params, user)
35
+ expect(result.order.user).to eq(user)
36
+ end
37
+
38
+ it "sends a confirmation email" do
39
+ expect { described_class.call(valid_params, user) }
40
+ .to have_enqueued_job(ActionMailer::MailDeliveryJob)
41
+ end
42
+ end
43
+
44
+ context "with insufficient stock" do
45
+ let(:product) { create(:product, stock: 0, price: 25.00) }
46
+
47
+ it "returns a failed result" do
48
+ result = described_class.call(valid_params, user)
49
+ expect(result).not_to be_success
50
+ end
51
+
52
+ it "includes an error message" do
53
+ result = described_class.call(valid_params, user)
54
+ expect(result.order.errors[:base]).to include("Insufficient inventory")
55
+ end
56
+
57
+ it "does not create an order" do
58
+ expect { described_class.call(valid_params, user) }
59
+ .not_to change(Order, :count)
60
+ end
61
+
62
+ it "does not send a confirmation email" do
63
+ expect { described_class.call(valid_params, user) }
64
+ .not_to have_enqueued_job(ActionMailer::MailDeliveryJob)
65
+ end
66
+ end
67
+
68
+ context "with invalid params" do
69
+ let(:invalid_params) { { shipping_address: "" } }
70
+
71
+ it "returns a failed result" do
72
+ result = described_class.call(invalid_params, user)
73
+ expect(result).not_to be_success
74
+ end
75
+
76
+ it "returns validation errors on the order" do
77
+ result = described_class.call(invalid_params, user)
78
+ expect(result.order.errors).to be_present
79
+ end
80
+ end
81
+ end
82
+ end
83
+ ```
84
+
85
+ Testing service objects that call external services — inject doubles:
86
+
87
+ ```ruby
88
+ # spec/services/embeddings/codebase_indexer_spec.rb
89
+ RSpec.describe Embeddings::CodebaseIndexer do
90
+ let(:project) { create(:project) }
91
+ let(:embedding_client) { instance_double(Embeddings::EmbeddingClient) }
92
+ let(:fake_embeddings) { [Array.new(1024) { rand(-1.0..1.0) }] }
93
+
94
+ subject(:indexer) { described_class.new(embedding_client: embedding_client) }
95
+
96
+ before do
97
+ allow(embedding_client).to receive(:embed).and_return(fake_embeddings)
98
+ end
99
+
100
+ describe "#index_file" do
101
+ let(:file_content) do
102
+ <<~RUBY
103
+ class Order < ApplicationRecord
104
+ belongs_to :user
105
+ has_many :line_items
106
+
107
+ def total
108
+ line_items.sum(&:subtotal)
109
+ end
110
+ end
111
+ RUBY
112
+ end
113
+
114
+ it "chunks the file into classes and methods" do
115
+ indexer.index_file(project, "app/models/order.rb", file_content)
116
+ chunks = project.code_embeddings
117
+
118
+ expect(chunks.pluck(:chunk_type)).to include("class", "method")
119
+ end
120
+
121
+ it "calls the embedding client with chunk content" do
122
+ expect(embedding_client).to receive(:embed).with(array_including(/class Order/))
123
+ indexer.index_file(project, "app/models/order.rb", file_content)
124
+ end
125
+
126
+ it "stores embeddings on the project" do
127
+ expect { indexer.index_file(project, "app/models/order.rb", file_content) }
128
+ .to change(project.code_embeddings, :count).by_at_least(1)
129
+ end
130
+
131
+ it "records the file hash for change detection" do
132
+ indexer.index_file(project, "app/models/order.rb", file_content)
133
+ embedding = project.code_embeddings.last
134
+
135
+ expect(embedding.file_hash).to eq(Digest::SHA256.hexdigest(file_content))
136
+ end
137
+ end
138
+ end
139
+ ```
140
+
141
+ ## Why This Is Good
142
+
143
+ - **Tests behavior, not implementation.** The test asserts `result.success?` and `result.order.user == user` — observable outcomes. It doesn't assert which internal methods were called or in what order.
144
+ - **Each context tests one scenario.** Happy path, insufficient stock, invalid params — each is a separate context with its own setup and assertions. A failure tells you exactly which scenario broke.
145
+ - **Injected dependencies are doubled.** `EmbeddingClient` is an `instance_double` — the test doesn't need a running Python service. It verifies the indexer calls the client correctly and processes the result.
146
+ - **`described_class.call`** uses the same interface as production code. The test is a client of the service, exercising it the way real code would.
147
+ - **Side effects are tested explicitly.** "sends a confirmation email" and "does not send a confirmation email" are separate assertions. The happy path verifies the side effect happens; the failure path verifies it doesn't.
148
+
149
+ ## Anti-Pattern
150
+
151
+ Testing internal method calls, mocking the service itself, and mixing unit and integration concerns:
152
+
153
+ ```ruby
154
+ # BAD: Testing implementation sequence
155
+ RSpec.describe Orders::CreateService do
156
+ it "calls methods in order" do
157
+ service = described_class.new(params, user)
158
+ expect(service).to receive(:validate_inventory).ordered
159
+ expect(service).to receive(:create_order).ordered
160
+ expect(service).to receive(:charge_payment).ordered
161
+ expect(service).to receive(:send_confirmation).ordered
162
+ service.call
163
+ end
164
+ end
165
+
166
+ # BAD: Mocking the service you're testing
167
+ RSpec.describe Orders::CreateService do
168
+ it "creates an order" do
169
+ service = described_class.new(params, user)
170
+ allow(service).to receive(:validate_inventory).and_return(true)
171
+ allow(service).to receive(:send_confirmation)
172
+ result = service.call
173
+ expect(result).to be_success
174
+ end
175
+ end
176
+
177
+ # BAD: Integration test disguised as a unit test
178
+ RSpec.describe Orders::CreateService do
179
+ it "processes the order completely" do
180
+ result = described_class.call(params, user)
181
+ expect(result).to be_success
182
+ expect(Order.count).to eq(1)
183
+ expect(ActionMailer::Base.deliveries.count).to eq(1)
184
+ expect(Product.first.stock).to eq(8)
185
+ expect(user.reload.loyalty_points).to eq(10)
186
+ expect(WarehouseApi).to have_received(:notify)
187
+ expect(Analytics).to have_received(:track)
188
+ end
189
+ end
190
+ ```
191
+
192
+ ## Why This Is Bad
193
+
194
+ - **Testing method order is fragile.** Reordering internal steps breaks the test even if the behavior is correct. The user doesn't care if validation happens before or after order creation — they care about the result.
195
+ - **Mocking the subject is circular.** If you stub `validate_inventory` to return true, you're not testing that validation works — you're testing that the service calls `create_order` after something returns true. The test proves nothing about real behavior.
196
+ - **God assertions test everything at once.** When this test fails, which part broke? The order? The email? The stock update? The loyalty points? You have to read the failure message carefully and run the test in isolation to figure it out. Split into focused examples.
197
+
198
+ ## When To Apply
199
+
200
+ - **Every service object gets its own spec file.** If you wrote a service, you write a spec. No exceptions.
201
+ - **Test the `.call` interface.** Don't test private methods directly. Test them through the public interface. If a private method has complex logic worth testing independently, it might belong in its own class.
202
+ - **Inject and double external dependencies.** HTTP clients, mailers, external APIs, other services — anything that crosses a boundary gets doubled.
203
+ - **Test each outcome in its own context.** Success, each type of failure, and edge cases each get their own `context` block with focused assertions.
204
+
205
+ ## When NOT To Apply
206
+
207
+ - **Don't unit test trivial services.** A service that wraps a single `Model.create!` call with no logic doesn't need its own spec. Test it through a request spec instead.
208
+ - **Don't test private methods.** If you feel the need, either the private method is complex enough to be its own class, or you can test it through the public interface.
209
+ - **Integration between services is tested in request specs.** The controller calls ServiceA which calls ServiceB — test this flow through a request spec, not by testing ServiceA's use of ServiceB.
210
+
211
+ ## Edge Cases
212
+
213
+ **Service returns different result types:**
214
+ Test each result type explicitly:
215
+
216
+ ```ruby
217
+ context "when payment fails" do
218
+ it "returns result with :payment_failed error" do
219
+ result = described_class.call(params, user)
220
+ expect(result.error_code).to eq(:payment_failed)
221
+ expect(result.error).to include("card was declined")
222
+ end
223
+ end
224
+
225
+ context "when validation fails" do
226
+ it "returns result with :invalid error" do
227
+ result = described_class.call(invalid_params, user)
228
+ expect(result.error_code).to eq(:invalid)
229
+ end
230
+ end
231
+ ```
232
+
233
+ **Service wraps a transaction:**
234
+ Test that the transaction rolls back on failure:
235
+
236
+ ```ruby
237
+ context "when notification fails after order creation" do
238
+ before do
239
+ allow(notifier).to receive(:notify).and_raise(StandardError, "API down")
240
+ end
241
+
242
+ it "rolls back the order" do
243
+ expect { described_class.call(params, user) }.to raise_error(StandardError)
244
+ expect(Order.count).to eq(0)
245
+ end
246
+ end
247
+ ```
248
+
249
+ **Testing the Result/Response object:**
250
+ If your services return a Result struct, test it as a value object:
251
+
252
+ ```ruby
253
+ it "returns a result with the order" do
254
+ result = described_class.call(valid_params, user)
255
+
256
+ aggregate_failures do
257
+ expect(result).to be_success
258
+ expect(result.order).to be_persisted
259
+ expect(result.order.total).to eq(50.00)
260
+ end
261
+ end
262
+ ```