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,244 @@
1
+ # RSpec: Shared Examples
2
+
3
+ ## Pattern
4
+
5
+ Use `shared_examples` to DRY up specs that verify the same behavior across multiple contexts. Use `shared_context` for shared setup. Keep shared examples focused on one behavior. Pass parameters via `let`, block, or arguments.
6
+
7
+ ```ruby
8
+ # spec/support/shared_examples/authenticated_endpoint.rb
9
+ RSpec.shared_examples "an authenticated endpoint" do
10
+ context "without API key" do
11
+ it "returns 401" do
12
+ make_request(api_key: nil)
13
+ expect(response).to have_http_status(:unauthorized)
14
+ end
15
+ end
16
+
17
+ context "with revoked API key" do
18
+ let(:api_key) { create(:api_key, :revoked) }
19
+
20
+ it "returns 401" do
21
+ make_request(api_key: api_key.raw_key)
22
+ expect(response).to have_http_status(:unauthorized)
23
+ end
24
+ end
25
+
26
+ context "with expired API key" do
27
+ let(:api_key) { create(:api_key, :expired) }
28
+
29
+ it "returns 401" do
30
+ make_request(api_key: api_key.raw_key)
31
+ expect(response).to have_http_status(:unauthorized)
32
+ end
33
+ end
34
+ end
35
+ ```
36
+
37
+ ```ruby
38
+ # spec/support/shared_examples/credit_deducting_endpoint.rb
39
+ RSpec.shared_examples "a credit-deducting endpoint" do
40
+ it "deducts credits on success" do
41
+ expect { make_request }.to change { user.credit_ledger_entries.count }.by(1)
42
+ end
43
+
44
+ it "records the interaction" do
45
+ expect { make_request }.to change(Interaction, :count).by(1)
46
+ end
47
+
48
+ context "with insufficient credits" do
49
+ before do
50
+ allow_any_instance_of(Credits::BalanceChecker).to receive(:sufficient?).and_return(false)
51
+ end
52
+
53
+ it "returns 402" do
54
+ make_request
55
+ expect(response).to have_http_status(:payment_required)
56
+ end
57
+
58
+ it "does not call Claude" do
59
+ make_request
60
+ expect(Ai::ClaudeClient).not_to have_received(:call)
61
+ end
62
+ end
63
+ end
64
+ ```
65
+
66
+ ```ruby
67
+ # spec/support/shared_examples/project_scoped_endpoint.rb
68
+ RSpec.shared_examples "a project-scoped endpoint" do
69
+ context "when user is not a member of the project" do
70
+ let(:other_project) { create(:project) }
71
+
72
+ it "returns 403" do
73
+ make_request(project_id: other_project.id)
74
+ expect(response).to have_http_status(:forbidden)
75
+ end
76
+ end
77
+
78
+ context "when project does not exist" do
79
+ it "returns 404" do
80
+ make_request(project_id: 999999)
81
+ expect(response).to have_http_status(:not_found)
82
+ end
83
+ end
84
+ end
85
+ ```
86
+
87
+ Using shared examples in specs:
88
+
89
+ ```ruby
90
+ # spec/requests/api/v1/ai/refactor_spec.rb
91
+ RSpec.describe "POST /api/v1/ai/refactor", type: :request do
92
+ let(:user) { create(:user, :pro) }
93
+ let(:project) { create(:project) }
94
+ let(:membership) { create(:project_membership, user: user, project: project) }
95
+
96
+ def make_request(api_key: user.api_keys.first.raw_key, project_id: project.id)
97
+ post "/api/v1/ai/refactor",
98
+ params: { file_path: "app/controllers/orders_controller.rb", file_content: "...", project_id: project_id },
99
+ headers: { "Authorization" => "Bearer #{api_key}" }
100
+ end
101
+
102
+ before { membership }
103
+
104
+ it_behaves_like "an authenticated endpoint"
105
+ it_behaves_like "a credit-deducting endpoint"
106
+ it_behaves_like "a project-scoped endpoint"
107
+
108
+ # Endpoint-specific tests
109
+ it "returns a streaming response" do
110
+ make_request
111
+ expect(response).to have_http_status(:ok)
112
+ expect(response.content_type).to include("text/event-stream")
113
+ end
114
+ end
115
+ ```
116
+
117
+ Shared context for common setup:
118
+
119
+ ```ruby
120
+ # spec/support/shared_contexts/with_stubbed_claude.rb
121
+ RSpec.shared_context "with stubbed Claude" do
122
+ let(:claude_response) { "Here is the refactored code..." }
123
+
124
+ before do
125
+ allow(Ai::ClaudeClient).to receive(:call).and_return(
126
+ OpenStruct.new(
127
+ content: claude_response,
128
+ input_tokens: 500,
129
+ output_tokens: 200,
130
+ cache_read_tokens: 400,
131
+ cache_write_tokens: 0
132
+ )
133
+ )
134
+ end
135
+ end
136
+
137
+ # Usage
138
+ RSpec.describe Orders::CreateService do
139
+ include_context "with stubbed Claude"
140
+
141
+ it "uses the stubbed response" do
142
+ # Claude is already stubbed
143
+ end
144
+ end
145
+ ```
146
+
147
+ ## Why This Is Good
148
+
149
+ - **DRY without obscuring.** Auth checks are the same for every endpoint. Writing them once in a shared example and including them with `it_behaves_like` is clearer than copy-pasting the same 20 lines into 30 spec files.
150
+ - **Consistent coverage.** When you add a new auth check (e.g., "suspended account returns 403"), you add it to the shared example once. Every endpoint that includes it gets the new test automatically.
151
+ - **Contract enforcement.** `it_behaves_like "a credit-deducting endpoint"` acts as a contract: every AI endpoint must deduct credits. If a new endpoint doesn't pass the shared example, it's missing credit deduction logic.
152
+ - **Readable spec files.** The endpoint spec reads like a checklist: it's authenticated, it deducts credits, it's project-scoped, and here are the endpoint-specific behaviors.
153
+ - **Shared contexts reduce boilerplate.** Stubbing Claude the same way in 20 spec files is noisy. A shared context does it once and every spec file includes it by name.
154
+
155
+ ## Anti-Pattern
156
+
157
+ Shared examples that are too broad, too abstract, or tightly coupled to implementation:
158
+
159
+ ```ruby
160
+ # BAD: Shared example that does everything
161
+ RSpec.shared_examples "a standard API endpoint" do |method, path|
162
+ it "requires auth" do
163
+ send(method, path)
164
+ expect(response).to have_http_status(:unauthorized)
165
+ end
166
+
167
+ it "requires project membership" do
168
+ send(method, path, headers: auth_headers)
169
+ expect(response).to have_http_status(:forbidden)
170
+ end
171
+
172
+ it "deducts credits" do
173
+ expect { send(method, path, params: valid_params, headers: auth_headers) }
174
+ .to change(CreditLedger, :count)
175
+ end
176
+
177
+ it "records the interaction" do
178
+ expect { send(method, path, params: valid_params, headers: auth_headers) }
179
+ .to change(Interaction, :count)
180
+ end
181
+
182
+ it "returns success" do
183
+ send(method, path, params: valid_params, headers: auth_headers)
184
+ expect(response).to have_http_status(:ok)
185
+ end
186
+ end
187
+
188
+ # Usage becomes cryptic
189
+ it_behaves_like "a standard API endpoint", :post, "/api/v1/ai/refactor"
190
+ ```
191
+
192
+ ```ruby
193
+ # BAD: Shared example in a deeply nested file nobody can find
194
+ # spec/support/shared_examples/concerns/models/trackable_behavior_for_auditable_records.rb
195
+ RSpec.shared_examples "trackable auditable behavior" do
196
+ # 50 lines of tests that are impossible to discover
197
+ end
198
+ ```
199
+
200
+ ## Why This Is Bad
201
+
202
+ - **God shared examples test too many things at once.** When `"a standard API endpoint"` fails, you don't know if it's an auth issue, a credit issue, or a response format issue. Smaller shared examples give focused feedback.
203
+ - **Parameterized shared examples are hard to read.** `it_behaves_like "a standard API endpoint", :post, "/api/v1/ai/refactor"` hides what's being tested. The reader has to open the shared example file to understand what 5 tests are running.
204
+ - **Over-abstracted names.** `"trackable auditable behavior"` doesn't communicate what it tests. `"an authenticated endpoint"` does. Name shared examples by the behavior they verify.
205
+ - **Hidden in deep directories.** If shared examples are buried in `spec/support/shared_examples/concerns/models/`, nobody will find or use them. Keep them in `spec/support/shared_examples/` at one level deep.
206
+
207
+ ## When To Apply
208
+
209
+ - **Identical behavior across multiple specs.** Authentication, authorization, credit deduction, pagination, error handling — if 10+ specs verify the same behavior, extract it.
210
+ - **Contract testing.** "Every AI endpoint must deduct credits" is a contract. A shared example enforces it.
211
+ - **Setup that's the same across a describe group.** `shared_context` for stubbing external services, setting up test data, or configuring the test environment.
212
+
213
+ ## When NOT To Apply
214
+
215
+ - **Behavior specific to one endpoint.** If only the refactor endpoint has a specific behavior, test it inline. Don't create a shared example for one consumer.
216
+ - **When the shared example needs more than 2 parameters.** If you're passing 4 arguments to configure the shared example, it's too abstract. Write the tests inline.
217
+ - **When the setup is simple.** A 2-line `before` block doesn't need a shared context. Just write the 2 lines.
218
+
219
+ ## Edge Cases
220
+
221
+ **Shared examples that need different setup per includer:**
222
+ Use `let` overrides. The shared example references `let(:user)` — each including spec defines its own `user`:
223
+
224
+ ```ruby
225
+ RSpec.shared_examples "a credit-deducting endpoint" do
226
+ it "deducts from the user's balance" do
227
+ expect { make_request }.to change { user.reload.credit_balance }
228
+ end
229
+ end
230
+
231
+ # Spec A
232
+ let(:user) { create(:user, :pro) }
233
+ it_behaves_like "a credit-deducting endpoint"
234
+
235
+ # Spec B — different user setup, same shared example
236
+ let(:user) { create(:user, :free) }
237
+ it_behaves_like "a credit-deducting endpoint"
238
+ ```
239
+
240
+ **`it_behaves_like` vs `include_examples`:**
241
+ `it_behaves_like` creates a nested context (its own describe block). `include_examples` runs the examples in the current context. Use `it_behaves_like` when you want isolation. Use `include_examples` when the examples need access to the current context's `let` variables.
242
+
243
+ **Shared examples across spec types:**
244
+ An authenticated endpoint shared example works for both request specs and API specs. Keep them generic enough to work across contexts, using `make_request` as the interface contract.
@@ -0,0 +1,286 @@
1
+ # RSpec: System Specs (Capybara)
2
+
3
+ ## Pattern
4
+
5
+ System specs drive a real browser to test complete user journeys — clicking, filling forms, seeing results. They're the most expensive specs but provide the highest confidence that the feature works end-to-end. Write them for critical paths, not every edge case.
6
+
7
+ ```ruby
8
+ # spec/system/order_checkout_spec.rb
9
+ require "rails_helper"
10
+
11
+ RSpec.describe "Order checkout", type: :system do
12
+ let(:user) { create(:user) }
13
+ let!(:product) { create(:product, name: "Widget", price: 25_00, stock: 10) }
14
+
15
+ before do
16
+ driven_by(:selenium_chrome_headless)
17
+ sign_in user
18
+ end
19
+
20
+ it "completes a full checkout flow" do
21
+ # Browse products
22
+ visit products_path
23
+ expect(page).to have_content("Widget")
24
+ expect(page).to have_content("$25.00")
25
+
26
+ # Add to cart
27
+ within "#product_#{product.id}" do
28
+ click_button "Add to Cart"
29
+ end
30
+ expect(page).to have_content("Added to cart")
31
+
32
+ # View cart and proceed
33
+ visit cart_path
34
+ expect(page).to have_content("Widget")
35
+ fill_in "Quantity", with: "2"
36
+ click_button "Update"
37
+ expect(page).to have_content("$50.00")
38
+
39
+ # Checkout
40
+ click_link "Checkout"
41
+ fill_in "Shipping address", with: "123 Main St, Austin, TX 78701"
42
+ click_button "Place Order"
43
+
44
+ # Confirmation
45
+ expect(page).to have_content("Order placed")
46
+ expect(page).to have_content("ORD-")
47
+ expect(page).to have_content("$50.00")
48
+ expect(page).to have_content("123 Main St")
49
+ end
50
+
51
+ it "shows validation errors for incomplete checkout" do
52
+ visit new_order_path
53
+ click_button "Place Order"
54
+
55
+ expect(page).to have_content("Shipping address can't be blank")
56
+ expect(page).to have_selector(".field_with_errors")
57
+ end
58
+
59
+ it "prevents checkout when product is out of stock" do
60
+ product.update!(stock: 0)
61
+ visit product_path(product)
62
+
63
+ expect(page).to have_content("Out of Stock")
64
+ expect(page).not_to have_button("Add to Cart")
65
+ end
66
+ end
67
+ ```
68
+
69
+ ### Testing JavaScript Interactions
70
+
71
+ ```ruby
72
+ RSpec.describe "Order management", type: :system do
73
+ before do
74
+ driven_by(:selenium_chrome_headless)
75
+ sign_in create(:user, :admin)
76
+ end
77
+
78
+ it "filters orders with live search" do
79
+ create(:order, reference: "ORD-001", status: :pending)
80
+ create(:order, reference: "ORD-002", status: :shipped)
81
+
82
+ visit admin_orders_path
83
+
84
+ # Turbo Frame search — updates without page reload
85
+ fill_in "Search", with: "ORD-001"
86
+
87
+ # Capybara auto-waits for the DOM to update
88
+ expect(page).to have_content("ORD-001")
89
+ expect(page).not_to have_content("ORD-002")
90
+ end
91
+
92
+ it "toggles order details inline" do
93
+ order = create(:order, :with_line_items)
94
+ visit admin_orders_path
95
+
96
+ # Click to expand details (Stimulus controller)
97
+ within "#order_#{order.id}" do
98
+ click_button "Details"
99
+ expect(page).to have_content(order.line_items.first.product.name)
100
+
101
+ # Click again to collapse
102
+ click_button "Details"
103
+ expect(page).not_to have_content(order.line_items.first.product.name)
104
+ end
105
+ end
106
+
107
+ it "handles confirmation dialogs" do
108
+ order = create(:order, :pending)
109
+ visit admin_order_path(order)
110
+
111
+ accept_confirm "Are you sure you want to cancel this order?" do
112
+ click_button "Cancel Order"
113
+ end
114
+
115
+ expect(page).to have_content("Order cancelled")
116
+ expect(page).to have_content("Cancelled")
117
+ end
118
+
119
+ it "handles dismiss of confirmation" do
120
+ order = create(:order, :pending)
121
+ visit admin_order_path(order)
122
+
123
+ dismiss_confirm do
124
+ click_button "Cancel Order"
125
+ end
126
+
127
+ expect(page).to have_content("Pending") # Status unchanged
128
+ end
129
+ end
130
+ ```
131
+
132
+ ### Testing Authentication Flows
133
+
134
+ ```ruby
135
+ RSpec.describe "Authentication", type: :system do
136
+ before { driven_by(:selenium_chrome_headless) }
137
+
138
+ it "signs in with valid credentials" do
139
+ user = create(:user, email: "alice@example.com", password: "securepassword")
140
+
141
+ visit new_user_session_path
142
+ fill_in "Email", with: "alice@example.com"
143
+ fill_in "Password", with: "securepassword"
144
+ click_button "Sign In"
145
+
146
+ expect(page).to have_content("Signed in successfully")
147
+ expect(page).to have_content("alice@example.com")
148
+ end
149
+
150
+ it "rejects invalid credentials" do
151
+ create(:user, email: "alice@example.com", password: "securepassword")
152
+
153
+ visit new_user_session_path
154
+ fill_in "Email", with: "alice@example.com"
155
+ fill_in "Password", with: "wrongpassword"
156
+ click_button "Sign In"
157
+
158
+ expect(page).to have_content("Invalid Email or password")
159
+ expect(page).to have_current_path(new_user_session_path)
160
+ end
161
+
162
+ it "redirects unauthenticated users to sign in" do
163
+ visit orders_path
164
+
165
+ expect(page).to have_current_path(new_user_session_path)
166
+ expect(page).to have_content("You need to sign in")
167
+ end
168
+ end
169
+ ```
170
+
171
+ ### Setup and Configuration
172
+
173
+ ```ruby
174
+ # spec/rails_helper.rb (relevant additions)
175
+ RSpec.configure do |config|
176
+ # System test configuration
177
+ config.before(:each, type: :system) do
178
+ driven_by :selenium_chrome_headless
179
+ end
180
+
181
+ # Use visible Chrome for debugging (override in specific tests)
182
+ # driven_by :selenium, using: :chrome, screen_size: [1400, 900]
183
+ end
184
+ ```
185
+
186
+ ```ruby
187
+ # spec/support/system_helpers.rb
188
+ module SystemHelpers
189
+ def sign_in(user)
190
+ visit new_user_session_path
191
+ fill_in "Email", with: user.email
192
+ fill_in "Password", with: "password" # Assumes factory default
193
+ click_button "Sign In"
194
+ expect(page).to have_content("Signed in")
195
+ end
196
+
197
+ def sign_out
198
+ click_link "Sign Out"
199
+ end
200
+ end
201
+
202
+ RSpec.configure do |config|
203
+ config.include SystemHelpers, type: :system
204
+ end
205
+ ```
206
+
207
+ ### Capybara Matchers Cheat Sheet
208
+
209
+ ```ruby
210
+ # Content assertions
211
+ expect(page).to have_content("text") # Anywhere on page
212
+ expect(page).not_to have_content("text")
213
+ expect(page).to have_selector("h1", text: "Orders") # CSS + text
214
+ expect(page).to have_selector(".badge", count: 3) # Exact count
215
+
216
+ # Form assertions
217
+ expect(page).to have_field("Email", with: "alice@example.com")
218
+ expect(page).to have_checked_field("Remember me")
219
+ expect(page).to have_select("Status", selected: "Pending")
220
+ expect(page).to have_button("Submit")
221
+ expect(page).to have_link("Edit")
222
+
223
+ # Scoping
224
+ within "#order-form" do
225
+ fill_in "Address", with: "123 Main"
226
+ click_button "Save"
227
+ end
228
+
229
+ within_table "orders" do
230
+ expect(page).to have_content("ORD-001")
231
+ end
232
+
233
+ # Waiting (Capybara auto-waits by default, up to Capybara.default_max_wait_time)
234
+ expect(page).to have_content("Loading complete") # Waits automatically
235
+ expect(page).to have_selector(".result", wait: 10) # Custom wait time
236
+ expect(page).to have_no_content("Loading...", wait: 5) # Wait for disappearance
237
+
238
+ # Navigation
239
+ expect(page).to have_current_path(orders_path)
240
+ expect(page).to have_current_path(/orders\/\d+/) # Regex match
241
+
242
+ # JavaScript
243
+ page.execute_script("window.scrollTo(0, document.body.scrollHeight)")
244
+ accept_alert { click_link "Dangerous" }
245
+ accept_confirm { click_button "Delete" }
246
+ dismiss_confirm { click_button "Delete" }
247
+ ```
248
+
249
+ ## Why This Is Good
250
+
251
+ - **Tests what users actually experience.** Click a button, fill a form, see a result. If this test passes, the feature works.
252
+ - **Catches integration bugs.** Broken JavaScript, missing CSRF tokens, Turbo Frame issues, CSS hiding elements — system specs catch what unit tests miss.
253
+ - **Capybara auto-waits.** `have_content` waits for text to appear (for async rendering, Turbo updates). No manual `sleep` calls for most cases.
254
+ - **`driven_by :selenium_chrome_headless`** runs fast without a visible browser window.
255
+
256
+ ## Anti-Pattern
257
+
258
+ ```ruby
259
+ # BAD: Testing model logic in a system spec
260
+ it "validates email format" do
261
+ visit signup_path
262
+ fill_in "Email", with: "invalid"
263
+ click_button "Sign Up"
264
+ expect(page).to have_content("Email is invalid")
265
+ end
266
+ # This takes 2-3 seconds. A model spec does it in 2ms:
267
+ # expect(User.new(email: "invalid")).not_to be_valid
268
+
269
+ # BAD: Testing every edge case in system specs
270
+ # 15 system specs for form validation ← TOO MANY
271
+ # 1 system spec for happy path + 14 model specs for validations ← RIGHT
272
+ ```
273
+
274
+ ## When To Apply
275
+
276
+ - **Critical user journeys.** Sign up, sign in, checkout, key CRUD flows — 1 system spec per journey.
277
+ - **JavaScript-dependent features.** Turbo Frames, Stimulus controllers, live search, modals, drag-and-drop.
278
+ - **Smoke tests.** One test per major page to verify it loads without errors.
279
+ - **Keep count low.** Target 10-30 system specs for a typical Rails app. Not 200.
280
+
281
+ ## When NOT To Apply
282
+
283
+ - **Validation logic.** Test in model specs (2ms vs 2s).
284
+ - **API endpoints.** Test with request specs — no browser needed.
285
+ - **Service object logic.** Test in service specs.
286
+ - **Every permutation.** System specs for happy paths. Unit tests for edge cases.