anima-core 0.3.0 → 1.0.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 (269) hide show
  1. checksums.yaml +4 -4
  2. data/.reek.yml +27 -1
  3. data/CHANGELOG.md +4 -0
  4. data/README.md +219 -25
  5. data/agents/codebase-analyzer.md +88 -0
  6. data/agents/codebase-pattern-finder.md +83 -0
  7. data/agents/documentation-researcher.md +59 -0
  8. data/agents/thoughts-analyzer.md +102 -0
  9. data/agents/web-search-researcher.md +71 -0
  10. data/anima-core.gemspec +3 -0
  11. data/app/channels/session_channel.rb +76 -28
  12. data/app/jobs/agent_request_job.rb +24 -0
  13. data/app/jobs/analytical_brain_job.rb +33 -0
  14. data/app/jobs/count_event_tokens_job.rb +1 -1
  15. data/app/models/concerns/event/broadcasting.rb +20 -2
  16. data/app/models/event.rb +1 -1
  17. data/app/models/goal.rb +91 -0
  18. data/app/models/session.rb +347 -22
  19. data/config/application.rb +2 -0
  20. data/db/migrate/20260314075248_add_subagent_support_to_sessions.rb +6 -0
  21. data/db/migrate/20260314112417_add_granted_tools_to_sessions.rb +5 -0
  22. data/db/migrate/20260314140000_add_name_to_sessions.rb +7 -0
  23. data/db/migrate/20260314150000_add_viewport_event_ids_to_sessions.rb +7 -0
  24. data/db/migrate/20260315100000_add_active_skills_to_sessions.rb +7 -0
  25. data/db/migrate/20260315140843_create_goals.rb +16 -0
  26. data/db/migrate/20260315144837_add_completed_at_to_goals.rb +5 -0
  27. data/db/migrate/20260315191105_add_active_workflow_to_sessions.rb +5 -0
  28. data/lib/agent_loop.rb +65 -9
  29. data/lib/agents/definition.rb +116 -0
  30. data/lib/agents/registry.rb +106 -0
  31. data/lib/analytical_brain/runner.rb +276 -0
  32. data/lib/analytical_brain/tools/activate_skill.rb +52 -0
  33. data/lib/analytical_brain/tools/deactivate_skill.rb +43 -0
  34. data/lib/analytical_brain/tools/deactivate_workflow.rb +34 -0
  35. data/lib/analytical_brain/tools/everything_is_ready.rb +28 -0
  36. data/lib/analytical_brain/tools/finish_goal.rb +62 -0
  37. data/lib/analytical_brain/tools/read_workflow.rb +58 -0
  38. data/lib/analytical_brain/tools/rename_session.rb +63 -0
  39. data/lib/analytical_brain/tools/set_goal.rb +60 -0
  40. data/lib/analytical_brain/tools/update_goal.rb +60 -0
  41. data/lib/analytical_brain.rb +23 -0
  42. data/lib/anima/cli/mcp/secrets.rb +76 -0
  43. data/lib/anima/cli/mcp.rb +197 -0
  44. data/lib/anima/cli.rb +4 -0
  45. data/lib/anima/installer.rb +168 -0
  46. data/lib/anima/settings.rb +226 -0
  47. data/lib/anima/version.rb +1 -1
  48. data/lib/anima.rb +9 -0
  49. data/lib/credential_store.rb +103 -0
  50. data/lib/environment_probe.rb +232 -0
  51. data/lib/llm/client.rb +29 -10
  52. data/lib/mcp/client_manager.rb +86 -0
  53. data/lib/mcp/config.rb +213 -0
  54. data/lib/mcp/health_check.rb +77 -0
  55. data/lib/mcp/secrets.rb +73 -0
  56. data/lib/mcp/stdio_transport.rb +206 -0
  57. data/lib/providers/anthropic.rb +8 -7
  58. data/lib/shell_session.rb +11 -10
  59. data/lib/skills/definition.rb +97 -0
  60. data/lib/skills/registry.rb +105 -0
  61. data/lib/tools/edit.rb +3 -4
  62. data/lib/tools/mcp_tool.rb +114 -0
  63. data/lib/tools/read.rb +15 -16
  64. data/lib/tools/registry.rb +14 -12
  65. data/lib/tools/request_feature.rb +121 -0
  66. data/lib/tools/return_result.rb +81 -0
  67. data/lib/tools/spawn_specialist.rb +109 -0
  68. data/lib/tools/spawn_subagent.rb +111 -0
  69. data/lib/tools/subagent_prompts.rb +12 -0
  70. data/lib/tools/web_get.rb +8 -9
  71. data/lib/tui/app.rb +332 -43
  72. data/lib/tui/message_store.rb +20 -0
  73. data/lib/tui/screens/chat.rb +207 -20
  74. data/lib/workflows/definition.rb +97 -0
  75. data/lib/workflows/registry.rb +89 -0
  76. data/skills/activerecord/SKILL.md +255 -0
  77. data/skills/activerecord/examples/associations/association_extensions.rb +298 -0
  78. data/skills/activerecord/examples/associations/basic_associations.rb +118 -0
  79. data/skills/activerecord/examples/associations/counter_caches.rb +215 -0
  80. data/skills/activerecord/examples/associations/polymorphic_associations.rb +217 -0
  81. data/skills/activerecord/examples/associations/self_referential.rb +302 -0
  82. data/skills/activerecord/examples/associations/through_associations.rb +203 -0
  83. data/skills/activerecord/examples/basics/crud_operations.rb +209 -0
  84. data/skills/activerecord/examples/basics/dirty_tracking.rb +218 -0
  85. data/skills/activerecord/examples/basics/inheritance.rb +377 -0
  86. data/skills/activerecord/examples/basics/type_casting.rb +317 -0
  87. data/skills/activerecord/examples/callbacks/alternatives_to_callbacks.rb +447 -0
  88. data/skills/activerecord/examples/callbacks/conditional_callbacks.rb +353 -0
  89. data/skills/activerecord/examples/callbacks/lifecycle_callbacks.rb +280 -0
  90. data/skills/activerecord/examples/callbacks/transaction_callbacks.rb +340 -0
  91. data/skills/activerecord/examples/migrations/indexes_and_constraints.rb +337 -0
  92. data/skills/activerecord/examples/migrations/reversible_patterns.rb +403 -0
  93. data/skills/activerecord/examples/migrations/safe_patterns.rb +420 -0
  94. data/skills/activerecord/examples/migrations/schema_changes.rb +277 -0
  95. data/skills/activerecord/examples/querying/batch_processing.rb +226 -0
  96. data/skills/activerecord/examples/querying/eager_loading.rb +259 -0
  97. data/skills/activerecord/examples/querying/finder_methods.rb +170 -0
  98. data/skills/activerecord/examples/querying/optimization.rb +275 -0
  99. data/skills/activerecord/examples/querying/scopes.rb +260 -0
  100. data/skills/activerecord/examples/validations/built_in_validators.rb +277 -0
  101. data/skills/activerecord/examples/validations/conditional_validations.rb +288 -0
  102. data/skills/activerecord/examples/validations/custom_validators.rb +381 -0
  103. data/skills/activerecord/examples/validations/database_constraints.rb +432 -0
  104. data/skills/activerecord/examples/validations/validation_contexts.rb +367 -0
  105. data/skills/activerecord/references/associations.md +709 -0
  106. data/skills/activerecord/references/basics.md +622 -0
  107. data/skills/activerecord/references/callbacks.md +738 -0
  108. data/skills/activerecord/references/migrations.md +657 -0
  109. data/skills/activerecord/references/querying.md +655 -0
  110. data/skills/activerecord/references/validations.md +596 -0
  111. data/skills/dragonruby/SKILL.md +250 -0
  112. data/skills/dragonruby/examples/audio/audio_events.rb +55 -0
  113. data/skills/dragonruby/examples/audio/background_music.rb +29 -0
  114. data/skills/dragonruby/examples/audio/crossfade.rb +51 -0
  115. data/skills/dragonruby/examples/audio/music_controls.rb +51 -0
  116. data/skills/dragonruby/examples/audio/sound_effects.rb +30 -0
  117. data/skills/dragonruby/examples/core/coordinate_system.rb +27 -0
  118. data/skills/dragonruby/examples/core/hello_world.rb +24 -0
  119. data/skills/dragonruby/examples/core/labels.rb +22 -0
  120. data/skills/dragonruby/examples/core/sprites.rb +35 -0
  121. data/skills/dragonruby/examples/core/state_management.rb +29 -0
  122. data/skills/dragonruby/examples/distribution/background_pause.rb +42 -0
  123. data/skills/dragonruby/examples/distribution/build_workflow.sh +26 -0
  124. data/skills/dragonruby/examples/distribution/cvars_production.txt +16 -0
  125. data/skills/dragonruby/examples/distribution/game_metadata_hd.txt +23 -0
  126. data/skills/dragonruby/examples/distribution/game_metadata_minimal.txt +9 -0
  127. data/skills/dragonruby/examples/distribution/game_metadata_mobile.txt +31 -0
  128. data/skills/dragonruby/examples/distribution/platform_detection.rb +36 -0
  129. data/skills/dragonruby/examples/distribution/steam_metadata.txt +19 -0
  130. data/skills/dragonruby/examples/entities/collision_detection.rb +43 -0
  131. data/skills/dragonruby/examples/entities/entity_lifecycle.rb +68 -0
  132. data/skills/dragonruby/examples/entities/entity_storage.rb +38 -0
  133. data/skills/dragonruby/examples/entities/factory_methods.rb +45 -0
  134. data/skills/dragonruby/examples/entities/random_spawning.rb +50 -0
  135. data/skills/dragonruby/examples/game-logic/reset_patterns.rb +98 -0
  136. data/skills/dragonruby/examples/game-logic/save_load.rb +101 -0
  137. data/skills/dragonruby/examples/game-logic/scoring.rb +104 -0
  138. data/skills/dragonruby/examples/game-logic/state_transitions.rb +103 -0
  139. data/skills/dragonruby/examples/game-logic/timers.rb +87 -0
  140. data/skills/dragonruby/examples/input/action_triggers.rb +36 -0
  141. data/skills/dragonruby/examples/input/analog_movement.rb +28 -0
  142. data/skills/dragonruby/examples/input/controller_input.rb +28 -0
  143. data/skills/dragonruby/examples/input/directional_input.rb +24 -0
  144. data/skills/dragonruby/examples/input/keyboard_input.rb +28 -0
  145. data/skills/dragonruby/examples/input/mouse_click.rb +26 -0
  146. data/skills/dragonruby/examples/input/movement_with_bounds.rb +22 -0
  147. data/skills/dragonruby/examples/input/normalized_movement.rb +32 -0
  148. data/skills/dragonruby/examples/rendering/frame_animation.rb +32 -0
  149. data/skills/dragonruby/examples/rendering/labels.rb +32 -0
  150. data/skills/dragonruby/examples/rendering/layering.rb +51 -0
  151. data/skills/dragonruby/examples/rendering/solids.rb +61 -0
  152. data/skills/dragonruby/examples/rendering/sprites.rb +33 -0
  153. data/skills/dragonruby/examples/rendering/spritesheet_animation.rb +39 -0
  154. data/skills/dragonruby/examples/scenes/case_dispatch.rb +60 -0
  155. data/skills/dragonruby/examples/scenes/class_based.rb +150 -0
  156. data/skills/dragonruby/examples/scenes/pause_overlay.rb +100 -0
  157. data/skills/dragonruby/examples/scenes/safe_transitions.rb +68 -0
  158. data/skills/dragonruby/examples/scenes/scene_transitions.rb +98 -0
  159. data/skills/dragonruby/examples/scenes/send_dispatch.rb +88 -0
  160. data/skills/dragonruby/references/audio.md +396 -0
  161. data/skills/dragonruby/references/core.md +385 -0
  162. data/skills/dragonruby/references/distribution.md +434 -0
  163. data/skills/dragonruby/references/entities.md +516 -0
  164. data/skills/dragonruby/references/game-logic/persistence.md +386 -0
  165. data/skills/dragonruby/references/game-logic/state.md +389 -0
  166. data/skills/dragonruby/references/input.md +414 -0
  167. data/skills/dragonruby/references/rendering/animation.md +467 -0
  168. data/skills/dragonruby/references/rendering/primitives.md +403 -0
  169. data/skills/dragonruby/references/scenes.md +443 -0
  170. data/skills/draper-decorators/SKILL.md +344 -0
  171. data/skills/draper-decorators/examples/application_decorator.rb +61 -0
  172. data/skills/draper-decorators/examples/decorator_spec.rb +253 -0
  173. data/skills/draper-decorators/examples/model_decorator.rb +152 -0
  174. data/skills/draper-decorators/references/anti-patterns.md +640 -0
  175. data/skills/draper-decorators/references/patterns.md +507 -0
  176. data/skills/draper-decorators/references/testing.md +559 -0
  177. data/skills/gh-issue.md +182 -0
  178. data/skills/mcp-server/SKILL.md +177 -0
  179. data/skills/mcp-server/examples/dynamic_tools.rb +36 -0
  180. data/skills/mcp-server/examples/file_manager_tool.rb +85 -0
  181. data/skills/mcp-server/examples/http_client.rb +48 -0
  182. data/skills/mcp-server/examples/http_server.rb +97 -0
  183. data/skills/mcp-server/examples/rails_integration.rb +88 -0
  184. data/skills/mcp-server/examples/stdio_server.rb +108 -0
  185. data/skills/mcp-server/examples/streaming_client.rb +95 -0
  186. data/skills/mcp-server/references/gotchas.md +183 -0
  187. data/skills/mcp-server/references/prompts.md +98 -0
  188. data/skills/mcp-server/references/resources.md +53 -0
  189. data/skills/mcp-server/references/server.md +140 -0
  190. data/skills/mcp-server/references/tools.md +146 -0
  191. data/skills/mcp-server/references/transport.md +104 -0
  192. data/skills/ratatui-ruby/SKILL.md +315 -0
  193. data/skills/ratatui-ruby/references/core-concepts.md +340 -0
  194. data/skills/ratatui-ruby/references/events.md +387 -0
  195. data/skills/ratatui-ruby/references/frameworks.md +522 -0
  196. data/skills/ratatui-ruby/references/layout.md +423 -0
  197. data/skills/ratatui-ruby/references/styling.md +268 -0
  198. data/skills/ratatui-ruby/references/testing.md +433 -0
  199. data/skills/ratatui-ruby/references/widgets.md +532 -0
  200. data/skills/rspec/SKILL.md +340 -0
  201. data/skills/rspec/examples/core/basic_structure.rb +69 -0
  202. data/skills/rspec/examples/core/configuration.rb +126 -0
  203. data/skills/rspec/examples/core/hooks.rb +126 -0
  204. data/skills/rspec/examples/core/memoized_helpers.rb +139 -0
  205. data/skills/rspec/examples/core/metadata_filtering.rb +144 -0
  206. data/skills/rspec/examples/core/shared_examples.rb +145 -0
  207. data/skills/rspec/examples/factory_bot/associations.rb +314 -0
  208. data/skills/rspec/examples/factory_bot/build_strategies.rb +272 -0
  209. data/skills/rspec/examples/factory_bot/callbacks.rb +320 -0
  210. data/skills/rspec/examples/factory_bot/custom_construction.rb +328 -0
  211. data/skills/rspec/examples/factory_bot/factory_definition.rb +191 -0
  212. data/skills/rspec/examples/factory_bot/inheritance.rb +314 -0
  213. data/skills/rspec/examples/factory_bot/traits.rb +293 -0
  214. data/skills/rspec/examples/factory_bot/transients.rb +229 -0
  215. data/skills/rspec/examples/matchers/change.rb +115 -0
  216. data/skills/rspec/examples/matchers/collections.rb +154 -0
  217. data/skills/rspec/examples/matchers/comparisons.rb +79 -0
  218. data/skills/rspec/examples/matchers/composing.rb +155 -0
  219. data/skills/rspec/examples/matchers/custom_matchers.rb +197 -0
  220. data/skills/rspec/examples/matchers/equality.rb +58 -0
  221. data/skills/rspec/examples/matchers/errors.rb +136 -0
  222. data/skills/rspec/examples/matchers/output.rb +103 -0
  223. data/skills/rspec/examples/matchers/predicates.rb +87 -0
  224. data/skills/rspec/examples/matchers/truthiness.rb +101 -0
  225. data/skills/rspec/examples/matchers/types.rb +82 -0
  226. data/skills/rspec/examples/matchers/yield.rb +147 -0
  227. data/skills/rspec/examples/mocks/any_instance.rb +172 -0
  228. data/skills/rspec/examples/mocks/argument_matchers.rb +206 -0
  229. data/skills/rspec/examples/mocks/constants.rb +177 -0
  230. data/skills/rspec/examples/mocks/doubles.rb +139 -0
  231. data/skills/rspec/examples/mocks/expectations.rb +137 -0
  232. data/skills/rspec/examples/mocks/message_chains.rb +173 -0
  233. data/skills/rspec/examples/mocks/ordering.rb +144 -0
  234. data/skills/rspec/examples/mocks/receive_counts.rb +181 -0
  235. data/skills/rspec/examples/mocks/responses.rb +223 -0
  236. data/skills/rspec/examples/mocks/spies.rb +149 -0
  237. data/skills/rspec/examples/mocks/stubbing.rb +133 -0
  238. data/skills/rspec/examples/rails/channels.rb +250 -0
  239. data/skills/rspec/examples/rails/controller_specs.rb +302 -0
  240. data/skills/rspec/examples/rails/helper_specs.rb +245 -0
  241. data/skills/rspec/examples/rails/job_specs.rb +256 -0
  242. data/skills/rspec/examples/rails/mailer_specs.rb +228 -0
  243. data/skills/rspec/examples/rails/matchers.rb +374 -0
  244. data/skills/rspec/examples/rails/model_specs.rb +193 -0
  245. data/skills/rspec/examples/rails/request_specs.rb +275 -0
  246. data/skills/rspec/examples/rails/routing_specs.rb +276 -0
  247. data/skills/rspec/examples/rails/system_specs.rb +294 -0
  248. data/skills/rspec/examples/rails/transactions.rb +254 -0
  249. data/skills/rspec/examples/rails/view_specs.rb +252 -0
  250. data/skills/rspec/references/core.md +816 -0
  251. data/skills/rspec/references/factory_bot.md +641 -0
  252. data/skills/rspec/references/matchers.md +516 -0
  253. data/skills/rspec/references/mocks.md +381 -0
  254. data/skills/rspec/references/rails.md +528 -0
  255. data/templates/soul.md +40 -0
  256. data/workflows/commit.md +45 -0
  257. data/workflows/create_handoff.md +98 -0
  258. data/workflows/create_note.md +82 -0
  259. data/workflows/create_plan.md +457 -0
  260. data/workflows/decompose_ticket.md +109 -0
  261. data/workflows/feature.md +91 -0
  262. data/workflows/implement_plan.md +87 -0
  263. data/workflows/iterate_plan.md +247 -0
  264. data/workflows/research_codebase.md +210 -0
  265. data/workflows/resume_handoff.md +217 -0
  266. data/workflows/review_pr.md +320 -0
  267. data/workflows/thoughts_init.md +71 -0
  268. data/workflows/validate_plan.md +166 -0
  269. metadata +284 -1
@@ -0,0 +1,293 @@
1
+ # FactoryBot: Traits Examples
2
+ # Source: factory_bot gem spec/acceptance/traits_spec.rb
3
+
4
+ # Traits define reusable attribute groups.
5
+ # Compose traits for flexible test data.
6
+
7
+ # Basic trait definition
8
+ FactoryBot.define do
9
+ factory :post do
10
+ title { "My Post" }
11
+ body { "Content here" }
12
+
13
+ trait :published do
14
+ published { true }
15
+ published_at { Time.current }
16
+ end
17
+
18
+ trait :draft do
19
+ published { false }
20
+ published_at { nil }
21
+ end
22
+
23
+ trait :featured do
24
+ featured { true }
25
+ featured_at { Time.current }
26
+ end
27
+
28
+ trait :with_long_content do
29
+ body { "A" * 1000 }
30
+ end
31
+ end
32
+ end
33
+
34
+ RSpec.describe "Traits" do
35
+ describe "basic usage" do
36
+ let(:published_post) { create(:post, :published) }
37
+
38
+ it "applies trait attributes" do
39
+ expect(published_post.published).to be true
40
+ expect(published_post.published_at).to be_present
41
+ end
42
+ end
43
+
44
+ describe "multiple traits" do
45
+ let(:featured_published) { create(:post, :published, :featured) }
46
+
47
+ it "applies all traits" do
48
+ expect(featured_published.published).to be true
49
+ expect(featured_published.featured).to be true
50
+ end
51
+ end
52
+
53
+ describe "traits with attribute overrides" do
54
+ let(:post) { create(:post, :published, title: "Custom Title") }
55
+
56
+ it "applies both trait and explicit override" do
57
+ expect(post.published).to be true
58
+ expect(post.title).to eq("Custom Title")
59
+ end
60
+ end
61
+ end
62
+
63
+ # Trait precedence
64
+ FactoryBot.define do
65
+ factory :user do
66
+ name { "Default" }
67
+
68
+ trait :john do
69
+ name { "John" }
70
+ end
71
+
72
+ trait :jane do
73
+ name { "Jane" }
74
+ end
75
+ end
76
+ end
77
+
78
+ RSpec.describe "Trait precedence" do
79
+ describe "last trait wins" do
80
+ it "applies traits in order" do
81
+ user_john_jane = build(:user, :john, :jane)
82
+ user_jane_john = build(:user, :jane, :john)
83
+
84
+ expect(user_john_jane.name).to eq("Jane")
85
+ expect(user_jane_john.name).to eq("John")
86
+ end
87
+ end
88
+
89
+ describe "explicit overrides trump traits" do
90
+ let(:user) { build(:user, :john, name: "Custom") }
91
+
92
+ it "uses explicit value" do
93
+ expect(user.name).to eq("Custom")
94
+ end
95
+ end
96
+ end
97
+
98
+ # Trait composition
99
+ FactoryBot.define do
100
+ factory :article do
101
+ title { "Article" }
102
+
103
+ trait :published do
104
+ status { "published" }
105
+ end
106
+
107
+ trait :featured do
108
+ featured { true }
109
+ end
110
+
111
+ trait :popular do
112
+ published
113
+ featured
114
+ views_count { 1000 }
115
+ end
116
+
117
+ # Child factory with traits
118
+ factory :popular_article, traits: [:popular]
119
+ end
120
+ end
121
+
122
+ RSpec.describe "Trait composition" do
123
+ describe "traits referencing other traits" do
124
+ let(:article) { build(:article, :popular) }
125
+
126
+ it "includes composed traits" do
127
+ expect(article.status).to eq("published")
128
+ expect(article.featured).to be true
129
+ expect(article.views_count).to eq(1000)
130
+ end
131
+ end
132
+
133
+ describe "child factory with traits" do
134
+ let(:popular) { build(:popular_article) }
135
+
136
+ it "inherits traits from factory definition" do
137
+ expect(popular.status).to eq("published")
138
+ expect(popular.featured).to be true
139
+ end
140
+ end
141
+ end
142
+
143
+ # Enum traits (Rails)
144
+ FactoryBot.define do
145
+ factory :order do
146
+ # Assuming: enum status: { pending: 0, processing: 1, shipped: 2, delivered: 3 }
147
+ # Auto-generated traits: :pending, :processing, :shipped, :delivered
148
+ customer_name { "Customer" }
149
+ end
150
+ end
151
+
152
+ RSpec.describe "Enum traits" do
153
+ describe "auto-generated enum traits" do
154
+ it "creates traits for each enum value" do
155
+ pending_order = build(:order, :pending)
156
+ shipped_order = build(:order, :shipped)
157
+
158
+ expect(pending_order.status).to eq("pending")
159
+ expect(shipped_order.status).to eq("shipped")
160
+ end
161
+ end
162
+ end
163
+
164
+ # Manual enum traits with traits_for_enum
165
+ FactoryBot.define do
166
+ factory :task do
167
+ # Define manually when auto-generation disabled
168
+ traits_for_enum :priority, {low: 0, medium: 1, high: 2}
169
+ end
170
+ end
171
+
172
+ # Global traits
173
+ FactoryBot.define do
174
+ # Traits defined outside factories are global
175
+ trait :timestamped do
176
+ created_at { 1.day.ago }
177
+ updated_at { Time.current }
178
+ end
179
+
180
+ trait :soft_deleted do
181
+ deleted_at { Time.current }
182
+ end
183
+ end
184
+
185
+ RSpec.describe "Global traits" do
186
+ FactoryBot.define do
187
+ factory :record do
188
+ name { "Record" }
189
+ end
190
+ end
191
+
192
+ describe "using global traits" do
193
+ let(:record) { build(:record, :timestamped, :soft_deleted) }
194
+
195
+ it "applies global traits to any factory" do
196
+ expect(record.created_at).to be_present
197
+ expect(record.deleted_at).to be_present
198
+ end
199
+ end
200
+ end
201
+
202
+ # Traits with callbacks
203
+ FactoryBot.define do
204
+ factory :user do
205
+ name { "User" }
206
+
207
+ trait :with_posts do
208
+ after(:create) do |user|
209
+ create_list(:post, 3, author: user)
210
+ end
211
+ end
212
+
213
+ trait :activated do
214
+ after(:build) { |user| user.activate! }
215
+ end
216
+
217
+ trait :confirmed do
218
+ after(:create) { |user| user.confirm! }
219
+ end
220
+ end
221
+ end
222
+
223
+ RSpec.describe "Traits with callbacks" do
224
+ describe "trait with after(:create)" do
225
+ let(:user) { create(:user, :with_posts) }
226
+
227
+ it "runs callback when trait applied" do
228
+ expect(user.posts.count).to eq(3)
229
+ end
230
+ end
231
+
232
+ describe "callback order with multiple traits" do
233
+ # Callbacks run in order traits are specified
234
+ let(:user) { create(:user, :activated, :confirmed) }
235
+
236
+ it "executes callbacks in trait order" do
237
+ expect(user).to be_activated
238
+ expect(user).to be_confirmed
239
+ end
240
+ end
241
+ end
242
+
243
+ # Traits with transient attributes
244
+ FactoryBot.define do
245
+ factory :user do
246
+ name { "User" }
247
+
248
+ trait :with_posts do
249
+ transient do
250
+ posts_count { 5 }
251
+ end
252
+
253
+ after(:create) do |user, evaluator|
254
+ create_list(:post, evaluator.posts_count, author: user)
255
+ end
256
+ end
257
+ end
258
+ end
259
+
260
+ RSpec.describe "Traits with transient attributes" do
261
+ describe "configurable trait behavior" do
262
+ let(:user) { create(:user, :with_posts, posts_count: 10) }
263
+
264
+ it "uses transient attribute in callback" do
265
+ expect(user.posts.count).to eq(10)
266
+ end
267
+ end
268
+ end
269
+
270
+ # Traits in associations
271
+ FactoryBot.define do
272
+ factory :comment do
273
+ body { "Comment" }
274
+ association :author, factory: [:user, :admin]
275
+ end
276
+
277
+ factory :post do
278
+ title { "Post" }
279
+ association :author, :verified, factory: :user
280
+ end
281
+ end
282
+
283
+ RSpec.describe "Traits in associations" do
284
+ describe "association with trait" do
285
+ let(:comment) { create(:comment) }
286
+ let(:post) { create(:post) }
287
+
288
+ it "applies traits to associated factory" do
289
+ expect(comment.author).to be_admin
290
+ expect(post.author).to be_verified
291
+ end
292
+ end
293
+ end
@@ -0,0 +1,229 @@
1
+ # FactoryBot: Transient Attributes Examples
2
+ # Source: factory_bot gem spec/acceptance/transient_attributes_spec.rb
3
+
4
+ # Transient attributes exist only during factory execution.
5
+ # They're available in attribute definitions and callbacks,
6
+ # but not set on the final object.
7
+
8
+ # Basic transient attributes
9
+ FactoryBot.define do
10
+ factory :user do
11
+ transient do
12
+ upcased { false }
13
+ prefix { "" }
14
+ end
15
+
16
+ name { "#{prefix}John Doe" }
17
+
18
+ after(:create) do |user, evaluator|
19
+ user.name.upcase! if evaluator.upcased
20
+ end
21
+ end
22
+ end
23
+
24
+ RSpec.describe "Transient attributes" do
25
+ describe "in attribute definitions" do
26
+ let(:user) { build(:user, prefix: "Mr. ") }
27
+
28
+ it "uses transient in attribute computation" do
29
+ expect(user.name).to eq("Mr. John Doe")
30
+ end
31
+ end
32
+
33
+ describe "in callbacks" do
34
+ let(:user) { create(:user, upcased: true) }
35
+
36
+ it "accesses transient via evaluator" do
37
+ expect(user.name).to eq("JOHN DOE")
38
+ end
39
+ end
40
+
41
+ describe "not on final object" do
42
+ let(:user) { build(:user, upcased: true) }
43
+
44
+ it "does not set transient as attribute" do
45
+ expect(user).not_to respond_to(:upcased)
46
+ expect(user).not_to respond_to(:prefix)
47
+ end
48
+ end
49
+ end
50
+
51
+ # Transient with attributes_for
52
+ RSpec.describe "Transient with attributes_for" do
53
+ FactoryBot.define do
54
+ factory :product do
55
+ transient do
56
+ discount { 0 }
57
+ end
58
+
59
+ name { "Product" }
60
+ price { 100 - discount }
61
+ end
62
+ end
63
+
64
+ describe "excludes transient from hash" do
65
+ subject(:attrs) { attributes_for(:product, discount: 20) }
66
+
67
+ it "computes attributes using transient" do
68
+ expect(attrs[:price]).to eq(80)
69
+ end
70
+
71
+ it "excludes transient from result" do
72
+ expect(attrs).not_to have_key(:discount)
73
+ end
74
+ end
75
+ end
76
+
77
+ # Transient sequences
78
+ FactoryBot.define do
79
+ factory :numbered_item do
80
+ transient do
81
+ sequence(:counter)
82
+ end
83
+
84
+ name { "Item ##{counter}" }
85
+ end
86
+ end
87
+
88
+ RSpec.describe "Transient sequences" do
89
+ describe "sequence in transient block" do
90
+ let(:items) { build_list(:numbered_item, 3) }
91
+
92
+ it "increments across instances" do
93
+ names = items.map(&:name)
94
+ expect(names).to eq(["Item #1", "Item #2", "Item #3"])
95
+ end
96
+ end
97
+ end
98
+
99
+ # Transient for dynamic associations
100
+ FactoryBot.define do
101
+ factory :author do
102
+ name { "Author" }
103
+
104
+ transient do
105
+ books_count { 0 }
106
+ published_books_count { 0 }
107
+ end
108
+
109
+ after(:create) do |author, evaluator|
110
+ create_list(:book, evaluator.books_count, author:)
111
+ create_list(:book, evaluator.published_books_count, :published, author:)
112
+ end
113
+ end
114
+ end
115
+
116
+ RSpec.describe "Transient for associations" do
117
+ describe "controlling association count" do
118
+ let(:author) { create(:author, books_count: 3, published_books_count: 2) }
119
+
120
+ it "creates specified associations" do
121
+ expect(author.books.count).to eq(5)
122
+ expect(author.books.published.count).to eq(2)
123
+ end
124
+ end
125
+ end
126
+
127
+ # Transient objects
128
+ FactoryBot.define do
129
+ factory :invoice do
130
+ transient do
131
+ customer { build(:customer) }
132
+ end
133
+
134
+ customer_name { customer.name }
135
+ customer_email { customer.email }
136
+ customer_id { customer.id }
137
+ end
138
+ end
139
+
140
+ RSpec.describe "Transient objects" do
141
+ describe "using object properties" do
142
+ let(:vip_customer) { build(:customer, :vip, name: "VIP Corp") }
143
+ let(:invoice) { build(:invoice, customer: vip_customer) }
144
+
145
+ it "extracts properties from transient object" do
146
+ expect(invoice.customer_name).to eq("VIP Corp")
147
+ expect(invoice.customer_email).to eq(vip_customer.email)
148
+ end
149
+ end
150
+ end
151
+
152
+ # Transient with defaults
153
+ FactoryBot.define do
154
+ factory :notification do
155
+ transient do
156
+ recipient { nil }
157
+ send_email { true }
158
+ end
159
+
160
+ user { recipient || association(:user) }
161
+
162
+ after(:create) do |notification, evaluator|
163
+ NotificationMailer.send(notification) if evaluator.send_email
164
+ end
165
+ end
166
+ end
167
+
168
+ RSpec.describe "Transient with defaults" do
169
+ describe "nil default with fallback" do
170
+ let(:notification) { create(:notification) }
171
+ let(:specific_user) { create(:user) }
172
+ let(:targeted_notification) { create(:notification, recipient: specific_user) }
173
+
174
+ it "uses fallback when transient is nil" do
175
+ expect(notification.user).to be_a(User)
176
+ end
177
+
178
+ it "uses provided value when given" do
179
+ expect(targeted_notification.user).to eq(specific_user)
180
+ end
181
+ end
182
+
183
+ describe "boolean transient" do
184
+ let(:silent_notification) { create(:notification, send_email: false) }
185
+
186
+ it "respects boolean transient" do
187
+ # Email not sent when send_email: false
188
+ expect(silent_notification).to be_persisted
189
+ end
190
+ end
191
+ end
192
+
193
+ # Transient inheritance
194
+ FactoryBot.define do
195
+ factory :base_record do
196
+ transient do
197
+ metadata { {} }
198
+ end
199
+
200
+ name { "Record" }
201
+ end
202
+
203
+ factory :special_record, parent: :base_record do
204
+ transient do
205
+ priority { "normal" }
206
+ end
207
+
208
+ name { "Special #{priority} Record" }
209
+ end
210
+ end
211
+
212
+ RSpec.describe "Transient inheritance" do
213
+ describe "child inherits parent transient" do
214
+ let(:record) { build(:special_record, metadata: {key: "value"}) }
215
+
216
+ it "has access to parent transient" do
217
+ # metadata from parent is available
218
+ expect(record.name).to include("Special")
219
+ end
220
+ end
221
+
222
+ describe "child adds own transient" do
223
+ let(:urgent) { build(:special_record, priority: "urgent") }
224
+
225
+ it "uses child-specific transient" do
226
+ expect(urgent.name).to eq("Special urgent Record")
227
+ end
228
+ end
229
+ end
@@ -0,0 +1,115 @@
1
+ # RSpec Matchers: Change Observation Examples
2
+ # Source: rspec-expectations gem features/built_in_matchers/change.feature
3
+
4
+ # Basic change detection
5
+ RSpec.describe "change matcher" do
6
+ describe "block form" do
7
+ it "detects any change" do
8
+ counter = 0
9
+ expect { counter += 1 }.to change { counter }
10
+ end
11
+
12
+ it "detects no change" do
13
+ value = 5
14
+ expect { value * 2 }.not_to change { value }
15
+ end
16
+ end
17
+
18
+ describe "receiver/method form" do
19
+ it "uses object and method name" do
20
+ user = build(:user, name: "Alice")
21
+ expect { user.name = "Bob" }.to change(user, :name)
22
+ end
23
+ end
24
+ end
25
+
26
+ # Chaining methods for specific changes
27
+ RSpec.describe "change with chains" do
28
+ describe ".by(delta)" do
29
+ it "specifies exact change amount" do
30
+ counter = 0
31
+ expect { counter += 5 }.to change { counter }.by(5)
32
+ end
33
+ end
34
+
35
+ describe ".from(old).to(new)" do
36
+ it "specifies before and after values" do
37
+ value = "old"
38
+ expect { value = "new" }.to change { value }.from("old").to("new")
39
+ end
40
+ end
41
+
42
+ describe ".by_at_least(minimum)" do
43
+ it "specifies minimum change" do
44
+ counter = 0
45
+ expect { counter += 5 }.to change { counter }.by_at_least(3)
46
+ end
47
+ end
48
+
49
+ describe ".by_at_most(maximum)" do
50
+ it "specifies maximum change" do
51
+ counter = 0
52
+ expect { counter += 5 }.to change { counter }.by_at_most(10)
53
+ end
54
+ end
55
+ end
56
+
57
+ # Practical example: database record changes
58
+ RSpec.describe User do
59
+ describe "#save" do
60
+ subject(:user) { build(:user) }
61
+
62
+ it "increments total count" do
63
+ expect { user.save }.to change(User, :count).by(1)
64
+ end
65
+
66
+ it "changes persisted status" do
67
+ expect { user.save }.to change(user, :persisted?).from(false).to(true)
68
+ end
69
+ end
70
+ end
71
+
72
+ RSpec.describe Order do
73
+ subject(:order) { create(:order, :with_items) }
74
+
75
+ describe "#add_item" do
76
+ let(:item) { build(:item, price: 25) }
77
+
78
+ it "increments item count" do
79
+ expect { order.add_item(item) }.to change { order.items.count }.by(1)
80
+ end
81
+
82
+ it "increases total" do
83
+ expect { order.add_item(item) }.to change(order, :total).by(25)
84
+ end
85
+ end
86
+
87
+ describe "#apply_discount" do
88
+ let(:discount) { build(:discount, percentage: 10) }
89
+
90
+ it "decreases total by at least discount percentage" do
91
+ original = order.total
92
+ min_decrease = original * 0.10
93
+
94
+ expect { order.apply_discount(discount) }
95
+ .to change(order, :total)
96
+ .by_at_least(-min_decrease)
97
+ end
98
+ end
99
+ end
100
+
101
+ # Using composed matchers with change
102
+ RSpec.describe "change with composed matchers" do
103
+ it "combines with be_within for floating point" do
104
+ value = 0.0
105
+ expect { value += 1.05 }.to change { value }.by(a_value_within(0.1).of(1.0))
106
+ end
107
+
108
+ it "combines with string matchers" do
109
+ text = "foo bar"
110
+ expect { text = "baz qux" }
111
+ .to change { text }
112
+ .from(a_string_matching(/foo/))
113
+ .to(a_string_matching(/baz/))
114
+ end
115
+ end