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,246 @@
1
+ # Minitest: Testing Service Objects
2
+
3
+ ## Pattern
4
+
5
+ Test service objects by calling `.call` with real or fixture data. Assert on the result object, side effects (DB changes, jobs enqueued, emails sent), and error conditions. Inject doubles for external dependencies.
6
+
7
+ ```ruby
8
+ # test/services/orders/create_service_test.rb
9
+ require "test_helper"
10
+
11
+ class Orders::CreateServiceTest < ActiveSupport::TestCase
12
+ setup do
13
+ @user = users(:alice)
14
+ @product = products(:widget)
15
+ end
16
+
17
+ test "creates an order with valid params" do
18
+ result = Orders::CreateService.call(valid_params, @user)
19
+
20
+ assert result.success?
21
+ assert_instance_of Order, result.order
22
+ assert_equal "pending", result.order.status
23
+ assert_equal @user, result.order.user
24
+ end
25
+
26
+ test "creates a database record" do
27
+ assert_difference "Order.count", 1 do
28
+ Orders::CreateService.call(valid_params, @user)
29
+ end
30
+ end
31
+
32
+ test "sends confirmation email" do
33
+ assert_emails 1 do
34
+ Orders::CreateService.call(valid_params, @user)
35
+ end
36
+ end
37
+
38
+ test "enqueues warehouse notification" do
39
+ assert_enqueued_with(job: WarehouseNotificationJob) do
40
+ Orders::CreateService.call(valid_params, @user)
41
+ end
42
+ end
43
+
44
+ test "returns failure for invalid params" do
45
+ result = Orders::CreateService.call({ shipping_address: "" }, @user)
46
+
47
+ refute result.success?
48
+ assert result.order.errors[:shipping_address].any?
49
+ end
50
+
51
+ test "does not create record on failure" do
52
+ assert_no_difference "Order.count" do
53
+ Orders::CreateService.call({ shipping_address: "" }, @user)
54
+ end
55
+ end
56
+
57
+ test "does not send email on failure" do
58
+ assert_no_emails do
59
+ Orders::CreateService.call({ shipping_address: "" }, @user)
60
+ end
61
+ end
62
+
63
+ private
64
+
65
+ def valid_params
66
+ {
67
+ shipping_address: "123 Main St",
68
+ line_items_attributes: [
69
+ { product_id: @product.id, quantity: 2 }
70
+ ]
71
+ }
72
+ end
73
+ end
74
+ ```
75
+
76
+ Testing services with external dependencies:
77
+
78
+ ```ruby
79
+ # test/services/embeddings/codebase_indexer_test.rb
80
+ require "test_helper"
81
+
82
+ class Embeddings::CodebaseIndexerTest < ActiveSupport::TestCase
83
+ setup do
84
+ @project = projects(:rubyn_project)
85
+ @fake_embedder = FakeEmbedder.new
86
+ @indexer = Embeddings::CodebaseIndexer.new(embedding_client: @fake_embedder)
87
+ end
88
+
89
+ test "creates code embeddings for each chunk" do
90
+ file_content = <<~RUBY
91
+ class Order < ApplicationRecord
92
+ def total
93
+ line_items.sum(&:subtotal)
94
+ end
95
+ end
96
+ RUBY
97
+
98
+ assert_difference "@project.code_embeddings.count" do
99
+ @indexer.index_file(@project, "app/models/order.rb", file_content)
100
+ end
101
+ end
102
+
103
+ test "stores the file path on each embedding" do
104
+ @indexer.index_file(@project, "app/models/order.rb", "class Order; end")
105
+
106
+ embedding = @project.code_embeddings.last
107
+ assert_equal "app/models/order.rb", embedding.file_path
108
+ end
109
+
110
+ test "stores a file hash for change detection" do
111
+ content = "class Order; end"
112
+ @indexer.index_file(@project, "app/models/order.rb", content)
113
+
114
+ embedding = @project.code_embeddings.last
115
+ assert_equal Digest::SHA256.hexdigest(content), embedding.file_hash
116
+ end
117
+
118
+ test "skips unchanged files" do
119
+ content = "class Order; end"
120
+ @indexer.index_file(@project, "app/models/order.rb", content)
121
+
122
+ assert_no_difference "@project.code_embeddings.count" do
123
+ @indexer.index_file(@project, "app/models/order.rb", content)
124
+ end
125
+ end
126
+ end
127
+
128
+ # test/support/fake_embedder.rb
129
+ class FakeEmbedder
130
+ DIMENSIONS = 1024
131
+
132
+ def embed(texts)
133
+ texts.map { Array.new(DIMENSIONS) { rand(-1.0..1.0) } }
134
+ end
135
+
136
+ def embed_query(text)
137
+ Array.new(DIMENSIONS) { rand(-1.0..1.0) }
138
+ end
139
+ end
140
+ ```
141
+
142
+ # Minitest: Test Performance
143
+
144
+ ## Pattern
145
+
146
+ Keep the suite fast: use fixtures, parallelize, avoid unnecessary DB writes, and profile regularly.
147
+
148
+ ```ruby
149
+ # test/test_helper.rb
150
+ class ActiveSupport::TestCase
151
+ # Run tests in parallel across CPU cores
152
+ parallelize(workers: :number_of_processors)
153
+
154
+ # Use transactions (default) — fastest cleanup strategy
155
+ # Each test rolls back, no data persists between tests
156
+ self.use_transactional_tests = true
157
+
158
+ fixtures :all
159
+ end
160
+ ```
161
+
162
+ ### Profile slow tests
163
+
164
+ ```bash
165
+ # Find the 10 slowest tests
166
+ bundle exec rails test --verbose 2>&1 | sort -t= -k2 -rn | head -10
167
+
168
+ # Or use minitest-reporters for detailed timing
169
+ ```
170
+
171
+ ```ruby
172
+ # Gemfile
173
+ group :test do
174
+ gem "minitest-reporters"
175
+ end
176
+
177
+ # test/test_helper.rb
178
+ require "minitest/reporters"
179
+ Minitest::Reporters.use! [
180
+ Minitest::Reporters::DefaultReporter.new,
181
+ Minitest::Reporters::MeanTimeReporter.new # Tracks average test times
182
+ ]
183
+ ```
184
+
185
+ ### Speed tips
186
+
187
+ ```ruby
188
+ # FAST: Use fixtures (zero per-test cost)
189
+ test "something with alice" do
190
+ assert users(:alice).valid?
191
+ end
192
+
193
+ # SLOW: Creating records per test
194
+ test "something with a user" do
195
+ user = User.create!(email: "test@example.com", name: "Test", password: "password")
196
+ assert user.valid?
197
+ end
198
+
199
+ # FAST: Test pure logic without DB
200
+ test "money arithmetic" do
201
+ price = Money.new(10_00)
202
+ tax = price * 0.08
203
+
204
+ assert_equal Money.new(80), tax
205
+ end
206
+
207
+ # FAST: Build without saving when testing validations
208
+ test "requires email" do
209
+ user = User.new(email: nil)
210
+ refute user.valid?
211
+ end
212
+
213
+ # FAST: Stub slow external calls
214
+ test "handles API timeout" do
215
+ WarehouseApi.stub(:notify, ->(*) { raise Faraday::TimeoutError }) do
216
+ result = Orders::ShipService.call(orders(:pending_order))
217
+ refute result.success?
218
+ end
219
+ end
220
+ ```
221
+
222
+ ### Parallel test configuration
223
+
224
+ ```ruby
225
+ # For system tests or tests that need separate DB state
226
+ class ActiveSupport::TestCase
227
+ parallelize(workers: :number_of_processors)
228
+
229
+ # If parallel tests have DB issues, use:
230
+ parallelize_setup do |worker|
231
+ ActiveStorage::Blob.service.root = "#{ActiveStorage::Blob.service.root}-#{worker}"
232
+ end
233
+ end
234
+ ```
235
+
236
+ ## Speed Hierarchy
237
+
238
+ 1. **Pure Ruby assertions** (no DB) — microseconds
239
+ 2. **Fixture reads** — microseconds (data already loaded)
240
+ 3. **`User.new` + `.valid?`** — milliseconds (no DB write)
241
+ 4. **Single `create!`** — ~1-5ms
242
+ 5. **Factory with associations** — ~5-50ms (cascading creates)
243
+ 6. **Integration test** — ~10-100ms (full request cycle)
244
+ 7. **System test with browser** — ~500ms-5s
245
+
246
+ Optimize by pushing tests as high on this list as possible. Don't use an integration test when a unit test will do. Don't create records when fixtures exist.
@@ -0,0 +1,169 @@
1
+ # Minitest: Test Structure and Conventions
2
+
3
+ ## Pattern
4
+
5
+ Minitest ships with Ruby — no extra gems needed. It provides two styles: `Minitest::Test` (classic xUnit) and `Minitest::Spec` (describe/it blocks). Both are fast, simple, and explicit. Choose one style per project and stick with it.
6
+
7
+ ### Classic Style (Minitest::Test)
8
+
9
+ ```ruby
10
+ # test/models/order_test.rb
11
+ require "test_helper"
12
+
13
+ class OrderTest < ActiveSupport::TestCase
14
+ setup do
15
+ @user = users(:alice)
16
+ @order = orders(:pending_order)
17
+ end
18
+
19
+ test "calculates total from line items" do
20
+ line_item = LineItem.create!(order: @order, product: products(:widget), quantity: 2, unit_price: 10_00)
21
+
22
+ assert_equal 20_00, @order.reload.total
23
+ end
24
+
25
+ test "requires a shipping address" do
26
+ order = Order.new(user: @user, shipping_address: nil)
27
+
28
+ assert_not order.valid?
29
+ assert_includes order.errors[:shipping_address], "can't be blank"
30
+ end
31
+
32
+ test "pending? returns true for pending orders" do
33
+ assert_predicate @order, :pending?
34
+ end
35
+
36
+ test "pending? returns false for shipped orders" do
37
+ @order.update!(status: :shipped)
38
+
39
+ refute_predicate @order, :pending?
40
+ end
41
+
42
+ test ".recent returns orders from the last 30 days" do
43
+ old_order = Order.create!(user: @user, shipping_address: "123 Main", created_at: 60.days.ago)
44
+
45
+ recent = Order.recent
46
+
47
+ assert_includes recent, @order
48
+ refute_includes recent, old_order
49
+ end
50
+ end
51
+ ```
52
+
53
+ ### Spec Style (Minitest::Spec)
54
+
55
+ ```ruby
56
+ # test/models/order_spec.rb
57
+ require "test_helper"
58
+
59
+ describe Order do
60
+ let(:user) { users(:alice) }
61
+ let(:order) { orders(:pending_order) }
62
+
63
+ describe "#total" do
64
+ it "calculates from line items" do
65
+ LineItem.create!(order: order, product: products(:widget), quantity: 2, unit_price: 10_00)
66
+
67
+ _(order.reload.total).must_equal 20_00
68
+ end
69
+ end
70
+
71
+ describe "validations" do
72
+ it "requires shipping address" do
73
+ order = Order.new(user: user, shipping_address: nil)
74
+
75
+ _(order).wont_be :valid?
76
+ _(order.errors[:shipping_address]).must_include "can't be blank"
77
+ end
78
+ end
79
+
80
+ describe ".recent" do
81
+ it "excludes orders older than 30 days" do
82
+ old_order = Order.create!(user: user, shipping_address: "123 Main", created_at: 60.days.ago)
83
+
84
+ _(Order.recent).must_include order
85
+ _(Order.recent).wont_include old_order
86
+ end
87
+ end
88
+ end
89
+ ```
90
+
91
+ ### The test_helper.rb
92
+
93
+ ```ruby
94
+ # test/test_helper.rb
95
+ ENV["RAILS_ENV"] ||= "test"
96
+ require_relative "../config/environment"
97
+ require "rails/test_help"
98
+ require "minitest/autorun"
99
+ require "minitest/pride" # Colorful output
100
+ require "webmock/minitest" # Stub HTTP requests
101
+
102
+ class ActiveSupport::TestCase
103
+ # Run tests in parallel
104
+ parallelize(workers: :number_of_processors)
105
+
106
+ # Use fixtures
107
+ fixtures :all
108
+
109
+ # Shared helpers available in all tests
110
+ def sign_in(user)
111
+ post login_path, params: { email: user.email, password: "password" }
112
+ end
113
+
114
+ def json_response
115
+ JSON.parse(response.body, symbolize_names: true)
116
+ end
117
+ end
118
+
119
+ class ActionDispatch::IntegrationTest
120
+ # Helpers for integration tests
121
+ def auth_headers(user)
122
+ { "Authorization" => "Bearer #{user.api_keys.first.raw_key}" }
123
+ end
124
+ end
125
+ ```
126
+
127
+ ## Why This Is Good
128
+
129
+ - **Ships with Ruby.** No Gemfile additions, no version conflicts, no `bundle install` waiting. It's always there.
130
+ - **Fast by default.** Minitest is ~5x faster to boot than RSpec. On a 500-test suite, this can save 3-5 seconds per run.
131
+ - **Plain Ruby.** Tests are classes with methods. No DSL magic, no `let` memoization surprises, no hidden context. Everything is explicit.
132
+ - **Fixtures over factories.** Rails fixtures load once, wrap in transactions, and are instant. No N factory creates per test.
133
+ - **Parallel by default.** `parallelize(workers: :number_of_processors)` runs tests across CPU cores out of the box.
134
+
135
+ ## Anti-Pattern
136
+
137
+ Overly complex test setup that mimics RSpec patterns instead of using Minitest idioms:
138
+
139
+ ```ruby
140
+ # BAD: Fighting Minitest to write RSpec-style tests
141
+ class OrderTest < ActiveSupport::TestCase
142
+ # Don't try to replicate RSpec's let/subject/context nesting
143
+ def setup
144
+ @company = Company.create!(name: "Acme")
145
+ @user = User.create!(email: "test@example.com", company: @company)
146
+ @product = Product.create!(name: "Widget", price: 10_00, company: @company)
147
+ @order = Order.create!(user: @user, shipping_address: "123 Main")
148
+ @line_item = LineItem.create!(order: @order, product: @product, quantity: 2, unit_price: 10_00)
149
+ end
150
+
151
+ # Every test pays for all 5 creates even if it only needs @user
152
+ end
153
+ ```
154
+
155
+ ## Why This Is Bad
156
+
157
+ - **Setup creates everything for every test.** A validation test that only needs an Order still creates Company, User, Product, and LineItem.
158
+ - **Fixtures solve this.** Define the data once in YAML, load it once per suite, wrap in transactions. Zero per-test cost.
159
+
160
+ ## When To Apply
161
+
162
+ - **Every Ruby or Rails project.** Minitest is the Rails default. New projects should start with it unless the team has strong RSpec preferences.
163
+ - **When test speed matters.** Minitest boots faster and runs faster. For CI-heavy teams, this compounds.
164
+ - **When simplicity matters.** Junior developers and contributors learn Minitest in minutes. It's just Ruby.
165
+
166
+ ## When NOT To Apply
167
+
168
+ - **The team already uses RSpec.** Don't switch mid-project. The cost of rewriting tests exceeds any speed benefit.
169
+ - **You need advanced matchers.** RSpec's `have_attributes`, `change { }.by`, `contain_exactly` are more expressive for complex assertions. Minitest can do the same but with more verbose code.
@@ -0,0 +1,237 @@
1
+ # Minitest: System Tests (Capybara)
2
+
3
+ ## Pattern
4
+
5
+ System tests drive a real browser to test full user journeys — clicking links, filling forms, asserting visible content. They're the most expensive tests but provide the highest confidence that the app works end-to-end.
6
+
7
+ ```ruby
8
+ # test/system/orders_test.rb
9
+ require "application_system_test_case"
10
+
11
+ class OrdersTest < ApplicationSystemTestCase
12
+ setup do
13
+ @user = users(:alice)
14
+ sign_in_as @user
15
+ end
16
+
17
+ test "viewing the orders list" do
18
+ visit orders_path
19
+
20
+ assert_text "Your Orders"
21
+ assert_text orders(:pending_order).reference
22
+ end
23
+
24
+ test "creating a new order" do
25
+ visit new_order_path
26
+
27
+ fill_in "Shipping address", with: "789 Elm St, Austin, TX"
28
+ select "Widget", from: "Product"
29
+ fill_in "Quantity", with: "3"
30
+
31
+ click_button "Place Order"
32
+
33
+ assert_text "Order placed"
34
+ assert_text "789 Elm St"
35
+ assert_text "Widget"
36
+ end
37
+
38
+ test "editing an existing order" do
39
+ visit order_path(orders(:pending_order))
40
+
41
+ click_link "Edit"
42
+
43
+ fill_in "Shipping address", with: "Updated Address"
44
+ click_button "Save"
45
+
46
+ assert_text "Updated"
47
+ assert_text "Updated Address"
48
+ end
49
+
50
+ test "cancelling an order" do
51
+ visit order_path(orders(:pending_order))
52
+
53
+ accept_confirm "Are you sure?" do
54
+ click_button "Cancel Order"
55
+ end
56
+
57
+ assert_text "Order cancelled"
58
+ assert_text "Cancelled"
59
+ end
60
+
61
+ test "searching orders" do
62
+ visit orders_path
63
+
64
+ fill_in "Search", with: orders(:pending_order).reference
65
+ click_button "Search"
66
+
67
+ assert_text orders(:pending_order).reference
68
+ assert_no_text orders(:shipped_order).reference
69
+ end
70
+
71
+ test "pagination" do
72
+ # Create enough orders to trigger pagination
73
+ 30.times { |i| Order.create!(user: @user, reference: "ORD-#{i}", shipping_address: "Test", status: :pending, total: 10_00) }
74
+
75
+ visit orders_path
76
+
77
+ assert_selector ".order-row", count: 25 # First page
78
+ click_link "Next"
79
+ assert_selector ".order-row" # Second page has remaining orders
80
+ end
81
+
82
+ private
83
+
84
+ def sign_in_as(user)
85
+ visit login_path
86
+ fill_in "Email", with: user.email
87
+ fill_in "Password", with: "password"
88
+ click_button "Sign In"
89
+ assert_text "Signed in"
90
+ end
91
+ end
92
+ ```
93
+
94
+ ### Setup
95
+
96
+ ```ruby
97
+ # test/application_system_test_case.rb
98
+ require "test_helper"
99
+
100
+ class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
101
+ # Headless Chrome — fast, no browser window pops up
102
+ driven_by :selenium, using: :headless_chrome, screen_size: [1400, 900]
103
+
104
+ # Use visible Chrome for debugging
105
+ # driven_by :selenium, using: :chrome, screen_size: [1400, 900]
106
+
107
+ def take_debug_screenshot
108
+ take_screenshot # Saves to tmp/screenshots/
109
+ end
110
+ end
111
+ ```
112
+
113
+ ### Testing Turbo/Hotwire Interactions
114
+
115
+ ```ruby
116
+ class TurboOrdersTest < ApplicationSystemTestCase
117
+ setup do
118
+ sign_in_as users(:alice)
119
+ end
120
+
121
+ test "inline editing with Turbo Frames" do
122
+ visit orders_path
123
+
124
+ within "##{dom_id(orders(:pending_order))}" do
125
+ click_link "Edit"
126
+
127
+ # The form appears INSIDE the frame (no page navigation)
128
+ fill_in "Shipping address", with: "New Address"
129
+ click_button "Save"
130
+ end
131
+
132
+ # The frame updates in-place
133
+ assert_text "New Address"
134
+ assert_no_selector "form" # Form is gone, replaced with display
135
+ end
136
+
137
+ test "live search with debounce" do
138
+ visit orders_path
139
+
140
+ fill_in "Search", with: orders(:pending_order).reference
141
+
142
+ # Wait for Turbo Frame to update (debounced search)
143
+ assert_text orders(:pending_order).reference
144
+ assert_no_text orders(:shipped_order).reference
145
+ end
146
+
147
+ test "flash messages appear and dismiss" do
148
+ visit new_order_path
149
+ fill_in "Shipping address", with: "123 Main St"
150
+ click_button "Place Order"
151
+
152
+ assert_text "Order placed"
153
+
154
+ # Flash auto-dismisses after a few seconds (Stimulus controller)
155
+ sleep 4
156
+ assert_no_text "Order placed"
157
+ end
158
+ end
159
+ ```
160
+
161
+ ### Capybara Matchers Cheat Sheet
162
+
163
+ ```ruby
164
+ # Finding elements
165
+ assert_text "Expected text" # Anywhere on page
166
+ assert_no_text "Should not appear"
167
+ assert_selector "h1", text: "Orders" # CSS selector with text
168
+ assert_selector ".order-row", count: 5 # Exact count
169
+ assert_selector "#order_123" # By ID
170
+ assert_link "Edit" # Link text
171
+ assert_button "Submit" # Button text
172
+ assert_field "Email", with: "alice@example.com" # Input with value
173
+
174
+ # Scoping
175
+ within "#order-form" do
176
+ fill_in "Address", with: "123 Main"
177
+ click_button "Save"
178
+ end
179
+
180
+ within_table "orders" do
181
+ assert_text "ORD-001"
182
+ end
183
+
184
+ # Waiting (Capybara auto-waits by default)
185
+ assert_text "Loading complete" # Waits up to Capybara.default_max_wait_time
186
+
187
+ # Force wait for async operations
188
+ assert_selector ".result", wait: 10 # Wait up to 10 seconds
189
+
190
+ # JavaScript interactions
191
+ accept_confirm { click_button "Delete" }
192
+ dismiss_confirm { click_button "Delete" }
193
+ accept_alert { click_link "Dangerous action" }
194
+
195
+ page.execute_script("window.scrollTo(0, document.body.scrollHeight)")
196
+ ```
197
+
198
+ ## Why This Is Good
199
+
200
+ - **Tests what users see.** "Fill in email, click sign in, see dashboard" — this is the user's actual experience. If this test passes, the feature works.
201
+ - **Catches integration bugs.** JavaScript errors, broken Turbo Frames, missing CSRF tokens, CSS hiding elements — system tests catch what unit tests miss.
202
+ - **Capybara auto-waits.** `assert_text` waits for the text to appear (up to the max wait time). No manual `sleep` needed for most async operations.
203
+
204
+ ## Anti-Pattern
205
+
206
+ Too many system tests or testing logic that belongs in unit tests:
207
+
208
+ ```ruby
209
+ # BAD: Testing validation messages in a system test
210
+ test "shows error for blank email" do
211
+ visit registration_path
212
+ fill_in "Email", with: ""
213
+ click_button "Sign Up"
214
+ assert_text "Email can't be blank"
215
+ end
216
+ # This takes 2-3 seconds. A model test takes 2ms.
217
+ ```
218
+
219
+ ## When To Apply
220
+
221
+ - **Critical user journeys only.** Sign up, sign in, checkout, key CRUD flows. 10-20 system tests, not 200.
222
+ - **JavaScript-dependent features.** Turbo Frames, Stimulus controllers, live search, modals.
223
+ - **Smoke tests.** One test per major page to verify it loads without errors.
224
+
225
+ ## When NOT To Apply
226
+
227
+ - **Validation logic.** Test in model specs (milliseconds vs seconds).
228
+ - **API endpoints.** Test with integration tests — no browser needed.
229
+ - **Every edge case.** System tests for happy paths, unit tests for edge cases.
230
+ - **CI with limited resources.** System tests are 10-100x slower. Keep the count low.
231
+
232
+ ## Speed Tips
233
+
234
+ - Use `headless_chrome` (no GUI overhead)
235
+ - Minimize `sleep` calls — rely on Capybara's auto-waiting
236
+ - Share login state across tests in the same class (use `setup`)
237
+ - Keep system test count under 50 for fast CI