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,199 @@
1
+ # RSpec: build_stubbed vs build vs create
2
+
3
+ ## Pattern
4
+
5
+ Choose the cheapest factory strategy that satisfies the test. Default to `build_stubbed`, fall back to `build`, use `create` only when the test genuinely needs a persisted database record.
6
+
7
+ ```ruby
8
+ RSpec.describe Order do
9
+ # FASTEST: No database, no save, generates a fake id
10
+ # Use for: testing methods that don't touch the DB
11
+ let(:order) { build_stubbed(:order, total: 100.00) }
12
+
13
+ describe "#discounted_total" do
14
+ it "applies 10% discount" do
15
+ expect(order.discounted_total).to eq(90.00)
16
+ end
17
+ end
18
+
19
+ describe "#high_value?" do
20
+ let(:cheap_order) { build_stubbed(:order, total: 50) }
21
+ let(:expensive_order) { build_stubbed(:order, total: 500) }
22
+
23
+ it "returns true for orders over 200" do
24
+ expect(expensive_order.high_value?).to be true
25
+ expect(cheap_order.high_value?).to be false
26
+ end
27
+ end
28
+ end
29
+ ```
30
+
31
+ ```ruby
32
+ RSpec.describe Order do
33
+ # MEDIUM: In-memory object, not saved. Has valid attributes.
34
+ # Use for: testing validations, or when you need to call .save yourself
35
+ let(:order) { build(:order) }
36
+
37
+ describe "validations" do
38
+ it "requires a shipping address" do
39
+ order.shipping_address = nil
40
+ expect(order).not_to be_valid
41
+ expect(order.errors[:shipping_address]).to include("can't be blank")
42
+ end
43
+
44
+ it "is valid with all required attributes" do
45
+ expect(order).to be_valid
46
+ end
47
+ end
48
+ end
49
+ ```
50
+
51
+ ```ruby
52
+ RSpec.describe Order do
53
+ # SLOWEST: Writes to database. Generates real id, timestamps, etc.
54
+ # Use for: testing scopes, queries, uniqueness, DB constraints, and associations that query
55
+ let(:user) { create(:user) }
56
+
57
+ describe ".recent" do
58
+ let!(:new_order) { create(:order, user: user, created_at: 1.day.ago) }
59
+ let!(:old_order) { create(:order, user: user, created_at: 1.year.ago) }
60
+
61
+ it "returns orders from the last 30 days" do
62
+ expect(user.orders.recent).to eq([new_order])
63
+ end
64
+ end
65
+
66
+ describe ".total_revenue" do
67
+ before do
68
+ create(:order, user: user, total: 100)
69
+ create(:order, user: user, total: 250)
70
+ end
71
+
72
+ it "sums all order totals" do
73
+ expect(user.orders.total_revenue).to eq(350)
74
+ end
75
+ end
76
+ end
77
+ ```
78
+
79
+ The decision tree:
80
+
81
+ ```
82
+ Does the test need the record in the database?
83
+ ├── YES (scopes, queries, uniqueness, associations that query) → create
84
+ └── NO
85
+ ├── Does the test call .save, .valid?, or .errors? → build
86
+ └── NO (testing return values, calculations, formatting) → build_stubbed
87
+ ```
88
+
89
+ ## Why This Is Good
90
+
91
+ - **Speed.** `build_stubbed` is 10-50x faster than `create`. It skips database writes, transactions, callbacks, and index updates. In a 2,000-spec suite, choosing the right strategy can save 5-10 minutes of run time.
92
+ - **Isolation.** `build_stubbed` tests logic in complete isolation from the database. If the test passes, the method works regardless of database state.
93
+ - **Clearer intent.** When a test uses `create`, it signals "this test depends on the database." When it uses `build_stubbed`, it signals "this test is about pure logic." Readers immediately understand the scope.
94
+ - **Less factory overhead.** `build_stubbed` doesn't trigger `after_create` callbacks or cascade through association chains. A stubbed order doesn't create a real user, real line items, and real products.
95
+
96
+ ## Anti-Pattern
97
+
98
+ Using `create` for everything because "it's easier" or "just in case":
99
+
100
+ ```ruby
101
+ RSpec.describe Order do
102
+ # SLOW: Every test creates 3+ database records
103
+ let(:user) { create(:user) }
104
+ let(:product) { create(:product) }
105
+ let(:order) { create(:order, user: user) }
106
+ let(:line_item) { create(:line_item, order: order, product: product) }
107
+
108
+ describe "#high_value?" do
109
+ it "returns true over 200" do
110
+ # This test only checks a comparison: total > 200
111
+ # It does NOT need any database records
112
+ order.total = 500
113
+ expect(order.high_value?).to be true
114
+ end
115
+ end
116
+
117
+ describe "#formatted_total" do
118
+ it "formats as currency" do
119
+ # This test only checks string formatting
120
+ # 4 database records created for zero reason
121
+ expect(order.formatted_total).to eq("$100.00")
122
+ end
123
+ end
124
+ end
125
+ ```
126
+
127
+ ## Why This Is Bad
128
+
129
+ - **4 INSERT statements for a string formatting test.** The `formatted_total` test checks `sprintf` behavior. It needs zero database interaction, yet it creates a user, product, order, and line item.
130
+ - **Factory chain cascades.** `create(:order)` triggers `create(:user)` via the association. `create(:line_item)` triggers `create(:product)` and `create(:order)`. One `create` can cascade into 5+ INSERTs.
131
+ - **Slower test suite.** Across hundreds of tests, unnecessary `create` calls add up to minutes. A test suite that should run in 30 seconds takes 3 minutes.
132
+ - **Fragile.** Database-backed tests can fail for reasons unrelated to the behavior being tested — unique constraint violations from other test data, unexpected callbacks, or association validation errors.
133
+
134
+ ## When To Apply
135
+
136
+ **Use `build_stubbed` when:**
137
+ - Testing instance methods that compute, format, or return values (`#total`, `#display_name`, `#high_value?`)
138
+ - Testing methods that check object state without querying (`#pending?`, `#can_cancel?`)
139
+ - Building objects to pass into service objects or other units under test
140
+ - You need an object with an `id` but don't need it in the database
141
+
142
+ **Use `build` when:**
143
+ - Testing model validations (`.valid?`, `.errors`)
144
+ - Testing `before_validation` or `before_save` callbacks
145
+ - You need to call `.save` in the test and check the result
146
+ - Building an object that will be passed to `create` or `save` explicitly
147
+
148
+ **Use `create` when:**
149
+ - Testing database scopes and queries (`.where`, `.recent`, `.active`)
150
+ - Testing uniqueness validations (need a real record to conflict with)
151
+ - Testing `has_many` / `belongs_to` associations that are loaded via query
152
+ - Testing `after_create` or `after_commit` callbacks
153
+ - Testing code that calls `.reload`
154
+ - Testing counter caches or database-computed columns
155
+
156
+ ## When NOT To Apply
157
+
158
+ - Don't overthink it for one-off tests. If a test file has 3 examples and they all need `create`, just use `create`. The optimization matters at scale — hundreds of tests, not three.
159
+ - Don't `build_stubbed` when the method under test calls `.reload`, `.save`, or queries the database. It will raise or return stale data.
160
+
161
+ ## Edge Cases
162
+
163
+ **`build_stubbed` and associations:**
164
+ Stubbed associations work for `belongs_to` (the foreign key is set). They don't work for `has_many` queries (no database to query against).
165
+
166
+ ```ruby
167
+ order = build_stubbed(:order)
168
+ order.user # Works — returns a stubbed user
169
+ order.line_items # Returns empty collection — no DB to query
170
+ ```
171
+
172
+ If you need associations, stub them manually or use `build_stubbed` with inline assignment:
173
+
174
+ ```ruby
175
+ items = build_stubbed_list(:line_item, 3)
176
+ order = build_stubbed(:order)
177
+ allow(order).to receive(:line_items).and_return(items)
178
+ ```
179
+
180
+ **`build_stubbed` and `.persisted?`:**
181
+ Stubbed objects return `true` for `.persisted?` and have a fake `id`. This makes them behave like saved records in most contexts — useful for testing path helpers, serializers, and view rendering.
182
+
183
+ **Testing both validation and persistence:**
184
+ Split into two tests. Validation test uses `build`. Persistence test uses `create`.
185
+
186
+ ```ruby
187
+ describe "email" do
188
+ it "validates format" do
189
+ user = build(:user, email: "invalid")
190
+ expect(user).not_to be_valid
191
+ end
192
+
193
+ it "enforces uniqueness in database" do
194
+ create(:user, email: "taken@example.com")
195
+ duplicate = build(:user, email: "taken@example.com")
196
+ expect(duplicate).not_to be_valid
197
+ end
198
+ end
199
+ ```
@@ -0,0 +1,206 @@
1
+ # RSpec: Factory Design
2
+
3
+ ## Pattern
4
+
5
+ Design factories to produce valid records with the minimum possible attributes. Use traits for variations. Use sequences for unique fields. Avoid deeply nested association chains and never put business logic in factories.
6
+
7
+ ```ruby
8
+ # spec/factories/users.rb
9
+ FactoryBot.define do
10
+ factory :user do
11
+ sequence(:email) { |n| "user#{n}@example.com" }
12
+ password { "password123" }
13
+ name { "Jane Doe" }
14
+ role { :user }
15
+ plan { :free }
16
+
17
+ trait :admin do
18
+ role { :admin }
19
+ sequence(:email) { |n| "admin#{n}@example.com" }
20
+ end
21
+
22
+ trait :pro do
23
+ plan { :pro }
24
+ end
25
+
26
+ trait :with_api_key do
27
+ after(:create) do |user|
28
+ create(:api_key, user: user)
29
+ end
30
+ end
31
+ end
32
+ end
33
+ ```
34
+
35
+ ```ruby
36
+ # spec/factories/orders.rb
37
+ FactoryBot.define do
38
+ factory :order do
39
+ user
40
+ sequence(:reference) { |n| "ORD-#{n.to_s.rjust(6, '0')}" }
41
+ shipping_address { "123 Main St" }
42
+ status { :pending }
43
+
44
+ trait :with_line_items do
45
+ transient do
46
+ item_count { 2 }
47
+ end
48
+
49
+ after(:create) do |order, evaluator|
50
+ create_list(:line_item, evaluator.item_count, order: order)
51
+ order.reload
52
+ end
53
+ end
54
+
55
+ trait :shipped do
56
+ status { :shipped }
57
+ shipped_at { 1.day.ago }
58
+ end
59
+
60
+ trait :cancelled do
61
+ status { :cancelled }
62
+ cancelled_at { 1.hour.ago }
63
+ end
64
+
65
+ trait :high_value do
66
+ total { 500.00 }
67
+ end
68
+ end
69
+ end
70
+ ```
71
+
72
+ ```ruby
73
+ # spec/factories/line_items.rb
74
+ FactoryBot.define do
75
+ factory :line_item do
76
+ order
77
+ product
78
+ quantity { 1 }
79
+ unit_price { 10.00 }
80
+ end
81
+ end
82
+ ```
83
+
84
+ Usage in tests:
85
+
86
+ ```ruby
87
+ # Minimal — just what the test needs
88
+ user = build_stubbed(:user)
89
+ admin = build_stubbed(:user, :admin)
90
+ pro_user = create(:user, :pro)
91
+
92
+ # Compose traits
93
+ order = create(:order, :shipped, :high_value)
94
+
95
+ # Override specific attributes
96
+ order = create(:order, total: 99.99, user: user)
97
+
98
+ # Use transient attributes
99
+ order = create(:order, :with_line_items, item_count: 5)
100
+ ```
101
+
102
+ ## Why This Is Good
103
+
104
+ - **Minimal by default.** The base factory creates a valid record with nothing extra. Tests that need specific attributes override them explicitly, making dependencies visible.
105
+ - **Traits are composable.** `:shipped`, `:cancelled`, `:high_value` can be mixed and matched. No need for separate factories like `shipped_order`, `cancelled_order`, `shipped_high_value_order`.
106
+ - **Sequences prevent collisions.** Unique fields use sequences, so tests never fail due to duplicate emails or reference numbers regardless of run order.
107
+ - **Transient attributes control association creation.** `item_count: 5` is clearer than creating 5 line items manually. The complexity is in the factory, not in every test.
108
+ - **Readable test code.** `create(:order, :shipped, :high_value)` reads like a description of what you need. No setup noise.
109
+
110
+ ## Anti-Pattern
111
+
112
+ Factories with heavy defaults, deep association chains, and business logic:
113
+
114
+ ```ruby
115
+ FactoryBot.define do
116
+ factory :order do
117
+ user
118
+ shipping_address { Faker::Address.full_address }
119
+ billing_address { Faker::Address.full_address }
120
+ status { :pending }
121
+ notes { Faker::Lorem.paragraph }
122
+ reference { "ORD-#{SecureRandom.hex(6)}" }
123
+ currency { "USD" }
124
+ tax_rate { 0.08 }
125
+ discount_code { nil }
126
+ ip_address { Faker::Internet.ip_v4_address }
127
+ user_agent { Faker::Internet.user_agent }
128
+
129
+ after(:create) do |order|
130
+ create_list(:line_item, 3, order: order)
131
+ order.update!(
132
+ subtotal: order.line_items.sum(&:total),
133
+ tax: order.line_items.sum(&:total) * 0.08,
134
+ total: order.line_items.sum(&:total) * 1.08
135
+ )
136
+ create(:shipment, order: order)
137
+ create(:payment, order: order, amount: order.total)
138
+ OrderMailer.confirmation(order).deliver_now
139
+ end
140
+ end
141
+ end
142
+ ```
143
+
144
+ ## Why This Is Bad
145
+
146
+ - **Every `create(:order)` creates 8+ records.** The order, a user, 3 line items, 3 products (via line items), a shipment, and a payment. A test that just needs an order object now waits for 8+ INSERTs.
147
+ - **Side effects in factories.** `OrderMailer.confirmation(order).deliver_now` runs in tests. Every test that creates an order sends an email. Tests become slow and flaky.
148
+ - **Unnecessary data.** `Faker::Lorem.paragraph` for notes, `Faker::Internet.ip_v4_address` for IP — these slow down factory execution with random generation, and the test almost certainly doesn't care about these values.
149
+ - **Hidden coupling.** The factory calculates subtotal, tax, and total. If the calculation logic changes, the factory breaks — or worse, silently produces wrong data that makes tests pass incorrectly.
150
+ - **Can't use `build_stubbed`.** Heavy `after(:create)` callbacks mean this factory only works with `create`. You're forced into database hits even for tests that don't need them.
151
+
152
+ ## When To Apply
153
+
154
+ Always. Every project using FactoryBot should follow these principles from the start:
155
+
156
+ - **Base factory has required fields only.** If the model validates presence of `email` and `name`, the factory sets `email` and `name`. Nothing else gets a default unless it's required for validity.
157
+ - **Use traits for every variation.** Don't add optional fields to the base factory. A shipped order is `create(:order, :shipped)`, not a factory that always sets `shipped_at`.
158
+ - **Sequences for every unique field.** Email, reference numbers, slugs, usernames — anything with a uniqueness validation.
159
+ - **Transient attributes for controlled association creation.** Don't create associations in the base factory. Use traits like `:with_line_items` that are opt-in.
160
+ - **No business logic in factories.** Don't calculate totals, send emails, or trigger service objects. Factories create data — that's it.
161
+
162
+ ## When NOT To Apply
163
+
164
+ - **Seed data is different from factories.** `db/seeds.rb` can and should create rich, interconnected data for development. That's not a factory — different purpose, different rules.
165
+ - **Complex setup for integration/system tests.** System tests may need a fully populated order with line items, shipments, and payments. Use a dedicated factory trait or a setup helper — don't bloat the base factory.
166
+
167
+ ## Edge Cases
168
+
169
+ **Circular associations:**
170
+ If Order belongs_to User and User has_many Orders, the factory chain can loop. Break the cycle by not auto-creating associations in both directions:
171
+
172
+ ```ruby
173
+ factory :user do
174
+ # Don't create orders here
175
+ end
176
+
177
+ factory :order do
178
+ user # Creates user, but user factory doesn't create orders
179
+ end
180
+ ```
181
+
182
+ **Factories for STI (Single Table Inheritance):**
183
+ Use inheritance in factories too:
184
+
185
+ ```ruby
186
+ factory :notification do
187
+ user
188
+ message { "Something happened" }
189
+
190
+ factory :email_notification, class: "EmailNotification" do
191
+ trait :sent do
192
+ sent_at { 1.hour.ago }
193
+ end
194
+ end
195
+
196
+ factory :sms_notification, class: "SmsNotification" do
197
+ phone_number { "+15551234567" }
198
+ end
199
+ end
200
+ ```
201
+
202
+ **Faker vs static values:**
203
+ Use static values in factories (`name { "Jane Doe" }`), not Faker (`name { Faker::Name.name }`). Faker adds execution time, produces random data that makes test output inconsistent, and occasionally generates values that fail validations (too long, invalid characters). Save Faker for seed data.
204
+
205
+ **Association strategy mismatch:**
206
+ When using `build_stubbed(:order)`, FactoryBot also stubs the `user` association. But if you `create(:line_item)`, it will `create` (not stub) the associated order. Be deliberate about which strategy you use at each level.
@@ -0,0 +1,161 @@
1
+ # RSpec: let vs let!
2
+
3
+ ## Pattern
4
+
5
+ Use `let` (lazy) by default. Use `let!` (eager) only when the record must exist in the database before the example runs, and no example in the group references it directly.
6
+
7
+ ```ruby
8
+ RSpec.describe Order do
9
+ # CORRECT: Lazy — only created when an example calls `user`
10
+ let(:user) { create(:user) }
11
+
12
+ # CORRECT: Lazy — only created when an example calls `order`
13
+ let(:order) { create(:order, user: user) }
14
+
15
+ describe "#total" do
16
+ # These line items are only created for examples that call `line_items`
17
+ let(:line_items) do
18
+ [
19
+ create(:line_item, order: order, quantity: 2, unit_price: 10.00),
20
+ create(:line_item, order: order, quantity: 1, unit_price: 25.00)
21
+ ]
22
+ end
23
+
24
+ it "calculates from line items" do
25
+ line_items # Trigger creation
26
+ expect(order.reload.total).to eq(45.00)
27
+ end
28
+ end
29
+
30
+ describe ".recent" do
31
+ # CORRECT use of let! — these must exist BEFORE the scope query runs.
32
+ # No example references `old_order` directly, but it must be in the DB
33
+ # for the scope to correctly exclude it.
34
+ let!(:recent_order) { create(:order, created_at: 1.day.ago) }
35
+ let!(:old_order) { create(:order, created_at: 1.year.ago) }
36
+
37
+ it "returns orders from the last 30 days" do
38
+ expect(Order.recent).to include(recent_order)
39
+ expect(Order.recent).not_to include(old_order)
40
+ end
41
+ end
42
+ end
43
+ ```
44
+
45
+ ## Why This Is Good
46
+
47
+ - **Lazy `let` avoids unnecessary DB hits.** If an example doesn't reference a `let` variable, the record is never created. In a describe block with 10 examples where only 3 need a specific record, you save 7 unnecessary INSERT statements.
48
+ - **Each example is self-documenting.** When you see `let(:order)` used in an example, you know that example needs an order. With `let!`, you have to mentally track "which records exist before every example runs?" even in examples that don't use them.
49
+ - **Faster test suite.** Lazy evaluation means the minimum number of records are created per example. In a large test suite, this compounds into minutes saved.
50
+ - **Memoized per example.** `let` evaluates once per example and caches. Calling `user` three times in one example hits the database once. No need for instance variables.
51
+
52
+ ## Anti-Pattern
53
+
54
+ Using `let!` everywhere "just to be safe":
55
+
56
+ ```ruby
57
+ RSpec.describe Order do
58
+ let!(:user) { create(:user) }
59
+ let!(:admin) { create(:user, role: :admin) }
60
+ let!(:product) { create(:product) }
61
+ let!(:category) { create(:category) }
62
+ let!(:order) { create(:order, user: user) }
63
+ let!(:line_item) { create(:line_item, order: order, product: product) }
64
+ let!(:shipping_rate) { create(:shipping_rate) }
65
+
66
+ describe "#total" do
67
+ it "calculates correctly" do
68
+ # Only needs order and line_item, but ALL 7 records are created
69
+ expect(order.total).to eq(line_item.quantity * line_item.unit_price)
70
+ end
71
+ end
72
+
73
+ describe "#shipped?" do
74
+ it "returns false when pending" do
75
+ # Only needs order, but ALL 7 records are created
76
+ expect(order.shipped?).to be false
77
+ end
78
+ end
79
+ end
80
+ ```
81
+
82
+ ## Why This Is Bad
83
+
84
+ - **Every example pays for every record.** 7 INSERT statements run before every single example, even if the example only needs 1 record. With 20 examples in this describe block, that's 140 INSERTs instead of ~40.
85
+ - **Hides dependencies.** When everything is `let!`, you can't tell which records an example actually needs by reading it. The implicit "everything exists" makes the test harder to understand and maintain.
86
+ - **Masks missing associations.** If an example works only because `let!(:product)` happens to exist, removing it later breaks the test in a confusing way. With `let`, the dependency is explicit — the example calls `product` or it doesn't.
87
+ - **Factory chain explosion.** If `create(:order)` creates a user, and `create(:line_item)` creates a product and a category, `let!` on all of them creates duplicate records you never asked for.
88
+
89
+ ## When To Apply
90
+
91
+ Use `let!` ONLY when ALL of these are true:
92
+
93
+ 1. The record must exist in the database BEFORE the example runs
94
+ 2. The example doesn't reference the variable directly — it's testing a query/scope that should find (or exclude) the record
95
+ 3. There's no other way to trigger the creation
96
+
97
+ The classic case is **testing scopes and queries**:
98
+
99
+ ```ruby
100
+ describe ".active" do
101
+ let!(:active_user) { create(:user, active: true) }
102
+ let!(:inactive_user) { create(:user, active: false) }
103
+
104
+ it "returns only active users" do
105
+ # Both must exist in DB before User.active runs
106
+ # The example references active_user for the assertion but
107
+ # inactive_user must exist to prove exclusion
108
+ expect(User.active).to eq([active_user])
109
+ end
110
+ end
111
+ ```
112
+
113
+ ## When NOT To Apply
114
+
115
+ - **The example references the variable directly.** If the example calls `order`, use `let` — it will be created on first reference.
116
+ - **You're "not sure if it needs to exist first."** Default to `let`. If the test fails because the record doesn't exist, then switch to `let!` for that specific variable. Don't preemptively use `let!`.
117
+ - **You're setting up context for a single example.** Use inline `create` inside the example instead:
118
+
119
+ ```ruby
120
+ it "rejects duplicate emails" do
121
+ create(:user, email: "taken@example.com")
122
+ duplicate = build(:user, email: "taken@example.com")
123
+ expect(duplicate).not_to be_valid
124
+ end
125
+ ```
126
+
127
+ ## Edge Cases
128
+
129
+ **`let` inside a `before` block:**
130
+ Don't. If you need something to exist before all examples, use `let!` or a `before` block with `create` directly. Calling `let` variables inside `before` works but obscures intent.
131
+
132
+ ```ruby
133
+ # Clear intent
134
+ let!(:admin) { create(:user, role: :admin) }
135
+
136
+ # Also clear
137
+ before { create(:user, role: :admin) }
138
+
139
+ # Confusing — looks lazy but is eager because before forces evaluation
140
+ let(:admin) { create(:user, role: :admin) }
141
+ before { admin }
142
+ ```
143
+
144
+ **`let` with `build_stubbed`:**
145
+ Always prefer `let(:user) { build_stubbed(:user) }` when the test doesn't need a database record. This is even faster than lazy `let` with `create` because no DB hit ever occurs.
146
+
147
+ **Nested `describe` blocks with `let`:**
148
+ Inner `let` overrides outer `let` with the same name. This is useful for testing variations:
149
+
150
+ ```ruby
151
+ let(:user) { create(:user, plan: :free) }
152
+
153
+ context "with pro plan" do
154
+ let(:user) { create(:user, plan: :pro) }
155
+ it { expect(user.can_export?).to be true }
156
+ end
157
+
158
+ context "with free plan" do
159
+ it { expect(user.can_export?).to be false }
160
+ end
161
+ ```