anima-core 0.2.1 → 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 (280) hide show
  1. checksums.yaml +4 -4
  2. data/.reek.yml +27 -1
  3. data/CHANGELOG.md +19 -0
  4. data/README.md +213 -43
  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 +195 -45
  12. data/app/decorators/user_message_decorator.rb +16 -5
  13. data/app/jobs/agent_request_job.rb +55 -2
  14. data/app/jobs/analytical_brain_job.rb +33 -0
  15. data/app/jobs/count_event_tokens_job.rb +15 -4
  16. data/app/models/concerns/event/broadcasting.rb +81 -0
  17. data/app/models/event.rb +20 -1
  18. data/app/models/goal.rb +91 -0
  19. data/app/models/session.rb +366 -21
  20. data/config/application.rb +2 -0
  21. data/config/initializers/event_subscribers.rb +0 -1
  22. data/config/routes.rb +0 -6
  23. data/db/migrate/20260313010000_add_status_to_events.rb +8 -0
  24. data/db/migrate/20260313020000_add_processing_to_sessions.rb +7 -0
  25. data/db/migrate/20260314075248_add_subagent_support_to_sessions.rb +6 -0
  26. data/db/migrate/20260314112417_add_granted_tools_to_sessions.rb +5 -0
  27. data/db/migrate/20260314140000_add_name_to_sessions.rb +7 -0
  28. data/db/migrate/20260314150000_add_viewport_event_ids_to_sessions.rb +7 -0
  29. data/db/migrate/20260315100000_add_active_skills_to_sessions.rb +7 -0
  30. data/db/migrate/20260315140843_create_goals.rb +16 -0
  31. data/db/migrate/20260315144837_add_completed_at_to_goals.rb +5 -0
  32. data/db/migrate/20260315191105_add_active_workflow_to_sessions.rb +5 -0
  33. data/lib/agent_loop.rb +65 -6
  34. data/lib/agents/definition.rb +116 -0
  35. data/lib/agents/registry.rb +106 -0
  36. data/lib/analytical_brain/runner.rb +276 -0
  37. data/lib/analytical_brain/tools/activate_skill.rb +52 -0
  38. data/lib/analytical_brain/tools/deactivate_skill.rb +43 -0
  39. data/lib/analytical_brain/tools/deactivate_workflow.rb +34 -0
  40. data/lib/analytical_brain/tools/everything_is_ready.rb +28 -0
  41. data/lib/analytical_brain/tools/finish_goal.rb +62 -0
  42. data/lib/analytical_brain/tools/read_workflow.rb +58 -0
  43. data/lib/analytical_brain/tools/rename_session.rb +63 -0
  44. data/lib/analytical_brain/tools/set_goal.rb +60 -0
  45. data/lib/analytical_brain/tools/update_goal.rb +60 -0
  46. data/lib/analytical_brain.rb +23 -0
  47. data/lib/anima/cli/mcp/secrets.rb +76 -0
  48. data/lib/anima/cli/mcp.rb +197 -0
  49. data/lib/anima/cli.rb +5 -40
  50. data/lib/anima/installer.rb +168 -0
  51. data/lib/anima/settings.rb +226 -0
  52. data/lib/anima/version.rb +1 -1
  53. data/lib/anima.rb +9 -0
  54. data/lib/credential_store.rb +103 -0
  55. data/lib/environment_probe.rb +232 -0
  56. data/lib/events/subscribers/persister.rb +1 -0
  57. data/lib/events/user_message.rb +17 -0
  58. data/lib/llm/client.rb +29 -10
  59. data/lib/mcp/client_manager.rb +86 -0
  60. data/lib/mcp/config.rb +213 -0
  61. data/lib/mcp/health_check.rb +77 -0
  62. data/lib/mcp/secrets.rb +73 -0
  63. data/lib/mcp/stdio_transport.rb +206 -0
  64. data/lib/providers/anthropic.rb +11 -20
  65. data/lib/shell_session.rb +11 -10
  66. data/lib/skills/definition.rb +97 -0
  67. data/lib/skills/registry.rb +105 -0
  68. data/lib/tools/edit.rb +226 -0
  69. data/lib/tools/mcp_tool.rb +114 -0
  70. data/lib/tools/read.rb +151 -0
  71. data/lib/tools/registry.rb +14 -12
  72. data/lib/tools/request_feature.rb +121 -0
  73. data/lib/tools/return_result.rb +81 -0
  74. data/lib/tools/spawn_specialist.rb +109 -0
  75. data/lib/tools/spawn_subagent.rb +111 -0
  76. data/lib/tools/subagent_prompts.rb +12 -0
  77. data/lib/tools/web_get.rb +8 -9
  78. data/lib/tools/write.rb +86 -0
  79. data/lib/tui/app.rb +985 -26
  80. data/lib/tui/cable_client.rb +69 -31
  81. data/lib/tui/message_store.rb +103 -8
  82. data/lib/tui/screens/chat.rb +293 -45
  83. data/lib/workflows/definition.rb +97 -0
  84. data/lib/workflows/registry.rb +89 -0
  85. data/skills/activerecord/SKILL.md +255 -0
  86. data/skills/activerecord/examples/associations/association_extensions.rb +298 -0
  87. data/skills/activerecord/examples/associations/basic_associations.rb +118 -0
  88. data/skills/activerecord/examples/associations/counter_caches.rb +215 -0
  89. data/skills/activerecord/examples/associations/polymorphic_associations.rb +217 -0
  90. data/skills/activerecord/examples/associations/self_referential.rb +302 -0
  91. data/skills/activerecord/examples/associations/through_associations.rb +203 -0
  92. data/skills/activerecord/examples/basics/crud_operations.rb +209 -0
  93. data/skills/activerecord/examples/basics/dirty_tracking.rb +218 -0
  94. data/skills/activerecord/examples/basics/inheritance.rb +377 -0
  95. data/skills/activerecord/examples/basics/type_casting.rb +317 -0
  96. data/skills/activerecord/examples/callbacks/alternatives_to_callbacks.rb +447 -0
  97. data/skills/activerecord/examples/callbacks/conditional_callbacks.rb +353 -0
  98. data/skills/activerecord/examples/callbacks/lifecycle_callbacks.rb +280 -0
  99. data/skills/activerecord/examples/callbacks/transaction_callbacks.rb +340 -0
  100. data/skills/activerecord/examples/migrations/indexes_and_constraints.rb +337 -0
  101. data/skills/activerecord/examples/migrations/reversible_patterns.rb +403 -0
  102. data/skills/activerecord/examples/migrations/safe_patterns.rb +420 -0
  103. data/skills/activerecord/examples/migrations/schema_changes.rb +277 -0
  104. data/skills/activerecord/examples/querying/batch_processing.rb +226 -0
  105. data/skills/activerecord/examples/querying/eager_loading.rb +259 -0
  106. data/skills/activerecord/examples/querying/finder_methods.rb +170 -0
  107. data/skills/activerecord/examples/querying/optimization.rb +275 -0
  108. data/skills/activerecord/examples/querying/scopes.rb +260 -0
  109. data/skills/activerecord/examples/validations/built_in_validators.rb +277 -0
  110. data/skills/activerecord/examples/validations/conditional_validations.rb +288 -0
  111. data/skills/activerecord/examples/validations/custom_validators.rb +381 -0
  112. data/skills/activerecord/examples/validations/database_constraints.rb +432 -0
  113. data/skills/activerecord/examples/validations/validation_contexts.rb +367 -0
  114. data/skills/activerecord/references/associations.md +709 -0
  115. data/skills/activerecord/references/basics.md +622 -0
  116. data/skills/activerecord/references/callbacks.md +738 -0
  117. data/skills/activerecord/references/migrations.md +657 -0
  118. data/skills/activerecord/references/querying.md +655 -0
  119. data/skills/activerecord/references/validations.md +596 -0
  120. data/skills/dragonruby/SKILL.md +250 -0
  121. data/skills/dragonruby/examples/audio/audio_events.rb +55 -0
  122. data/skills/dragonruby/examples/audio/background_music.rb +29 -0
  123. data/skills/dragonruby/examples/audio/crossfade.rb +51 -0
  124. data/skills/dragonruby/examples/audio/music_controls.rb +51 -0
  125. data/skills/dragonruby/examples/audio/sound_effects.rb +30 -0
  126. data/skills/dragonruby/examples/core/coordinate_system.rb +27 -0
  127. data/skills/dragonruby/examples/core/hello_world.rb +24 -0
  128. data/skills/dragonruby/examples/core/labels.rb +22 -0
  129. data/skills/dragonruby/examples/core/sprites.rb +35 -0
  130. data/skills/dragonruby/examples/core/state_management.rb +29 -0
  131. data/skills/dragonruby/examples/distribution/background_pause.rb +42 -0
  132. data/skills/dragonruby/examples/distribution/build_workflow.sh +26 -0
  133. data/skills/dragonruby/examples/distribution/cvars_production.txt +16 -0
  134. data/skills/dragonruby/examples/distribution/game_metadata_hd.txt +23 -0
  135. data/skills/dragonruby/examples/distribution/game_metadata_minimal.txt +9 -0
  136. data/skills/dragonruby/examples/distribution/game_metadata_mobile.txt +31 -0
  137. data/skills/dragonruby/examples/distribution/platform_detection.rb +36 -0
  138. data/skills/dragonruby/examples/distribution/steam_metadata.txt +19 -0
  139. data/skills/dragonruby/examples/entities/collision_detection.rb +43 -0
  140. data/skills/dragonruby/examples/entities/entity_lifecycle.rb +68 -0
  141. data/skills/dragonruby/examples/entities/entity_storage.rb +38 -0
  142. data/skills/dragonruby/examples/entities/factory_methods.rb +45 -0
  143. data/skills/dragonruby/examples/entities/random_spawning.rb +50 -0
  144. data/skills/dragonruby/examples/game-logic/reset_patterns.rb +98 -0
  145. data/skills/dragonruby/examples/game-logic/save_load.rb +101 -0
  146. data/skills/dragonruby/examples/game-logic/scoring.rb +104 -0
  147. data/skills/dragonruby/examples/game-logic/state_transitions.rb +103 -0
  148. data/skills/dragonruby/examples/game-logic/timers.rb +87 -0
  149. data/skills/dragonruby/examples/input/action_triggers.rb +36 -0
  150. data/skills/dragonruby/examples/input/analog_movement.rb +28 -0
  151. data/skills/dragonruby/examples/input/controller_input.rb +28 -0
  152. data/skills/dragonruby/examples/input/directional_input.rb +24 -0
  153. data/skills/dragonruby/examples/input/keyboard_input.rb +28 -0
  154. data/skills/dragonruby/examples/input/mouse_click.rb +26 -0
  155. data/skills/dragonruby/examples/input/movement_with_bounds.rb +22 -0
  156. data/skills/dragonruby/examples/input/normalized_movement.rb +32 -0
  157. data/skills/dragonruby/examples/rendering/frame_animation.rb +32 -0
  158. data/skills/dragonruby/examples/rendering/labels.rb +32 -0
  159. data/skills/dragonruby/examples/rendering/layering.rb +51 -0
  160. data/skills/dragonruby/examples/rendering/solids.rb +61 -0
  161. data/skills/dragonruby/examples/rendering/sprites.rb +33 -0
  162. data/skills/dragonruby/examples/rendering/spritesheet_animation.rb +39 -0
  163. data/skills/dragonruby/examples/scenes/case_dispatch.rb +60 -0
  164. data/skills/dragonruby/examples/scenes/class_based.rb +150 -0
  165. data/skills/dragonruby/examples/scenes/pause_overlay.rb +100 -0
  166. data/skills/dragonruby/examples/scenes/safe_transitions.rb +68 -0
  167. data/skills/dragonruby/examples/scenes/scene_transitions.rb +98 -0
  168. data/skills/dragonruby/examples/scenes/send_dispatch.rb +88 -0
  169. data/skills/dragonruby/references/audio.md +396 -0
  170. data/skills/dragonruby/references/core.md +385 -0
  171. data/skills/dragonruby/references/distribution.md +434 -0
  172. data/skills/dragonruby/references/entities.md +516 -0
  173. data/skills/dragonruby/references/game-logic/persistence.md +386 -0
  174. data/skills/dragonruby/references/game-logic/state.md +389 -0
  175. data/skills/dragonruby/references/input.md +414 -0
  176. data/skills/dragonruby/references/rendering/animation.md +467 -0
  177. data/skills/dragonruby/references/rendering/primitives.md +403 -0
  178. data/skills/dragonruby/references/scenes.md +443 -0
  179. data/skills/draper-decorators/SKILL.md +344 -0
  180. data/skills/draper-decorators/examples/application_decorator.rb +61 -0
  181. data/skills/draper-decorators/examples/decorator_spec.rb +253 -0
  182. data/skills/draper-decorators/examples/model_decorator.rb +152 -0
  183. data/skills/draper-decorators/references/anti-patterns.md +640 -0
  184. data/skills/draper-decorators/references/patterns.md +507 -0
  185. data/skills/draper-decorators/references/testing.md +559 -0
  186. data/skills/gh-issue.md +182 -0
  187. data/skills/mcp-server/SKILL.md +177 -0
  188. data/skills/mcp-server/examples/dynamic_tools.rb +36 -0
  189. data/skills/mcp-server/examples/file_manager_tool.rb +85 -0
  190. data/skills/mcp-server/examples/http_client.rb +48 -0
  191. data/skills/mcp-server/examples/http_server.rb +97 -0
  192. data/skills/mcp-server/examples/rails_integration.rb +88 -0
  193. data/skills/mcp-server/examples/stdio_server.rb +108 -0
  194. data/skills/mcp-server/examples/streaming_client.rb +95 -0
  195. data/skills/mcp-server/references/gotchas.md +183 -0
  196. data/skills/mcp-server/references/prompts.md +98 -0
  197. data/skills/mcp-server/references/resources.md +53 -0
  198. data/skills/mcp-server/references/server.md +140 -0
  199. data/skills/mcp-server/references/tools.md +146 -0
  200. data/skills/mcp-server/references/transport.md +104 -0
  201. data/skills/ratatui-ruby/SKILL.md +315 -0
  202. data/skills/ratatui-ruby/references/core-concepts.md +340 -0
  203. data/skills/ratatui-ruby/references/events.md +387 -0
  204. data/skills/ratatui-ruby/references/frameworks.md +522 -0
  205. data/skills/ratatui-ruby/references/layout.md +423 -0
  206. data/skills/ratatui-ruby/references/styling.md +268 -0
  207. data/skills/ratatui-ruby/references/testing.md +433 -0
  208. data/skills/ratatui-ruby/references/widgets.md +532 -0
  209. data/skills/rspec/SKILL.md +340 -0
  210. data/skills/rspec/examples/core/basic_structure.rb +69 -0
  211. data/skills/rspec/examples/core/configuration.rb +126 -0
  212. data/skills/rspec/examples/core/hooks.rb +126 -0
  213. data/skills/rspec/examples/core/memoized_helpers.rb +139 -0
  214. data/skills/rspec/examples/core/metadata_filtering.rb +144 -0
  215. data/skills/rspec/examples/core/shared_examples.rb +145 -0
  216. data/skills/rspec/examples/factory_bot/associations.rb +314 -0
  217. data/skills/rspec/examples/factory_bot/build_strategies.rb +272 -0
  218. data/skills/rspec/examples/factory_bot/callbacks.rb +320 -0
  219. data/skills/rspec/examples/factory_bot/custom_construction.rb +328 -0
  220. data/skills/rspec/examples/factory_bot/factory_definition.rb +191 -0
  221. data/skills/rspec/examples/factory_bot/inheritance.rb +314 -0
  222. data/skills/rspec/examples/factory_bot/traits.rb +293 -0
  223. data/skills/rspec/examples/factory_bot/transients.rb +229 -0
  224. data/skills/rspec/examples/matchers/change.rb +115 -0
  225. data/skills/rspec/examples/matchers/collections.rb +154 -0
  226. data/skills/rspec/examples/matchers/comparisons.rb +79 -0
  227. data/skills/rspec/examples/matchers/composing.rb +155 -0
  228. data/skills/rspec/examples/matchers/custom_matchers.rb +197 -0
  229. data/skills/rspec/examples/matchers/equality.rb +58 -0
  230. data/skills/rspec/examples/matchers/errors.rb +136 -0
  231. data/skills/rspec/examples/matchers/output.rb +103 -0
  232. data/skills/rspec/examples/matchers/predicates.rb +87 -0
  233. data/skills/rspec/examples/matchers/truthiness.rb +101 -0
  234. data/skills/rspec/examples/matchers/types.rb +82 -0
  235. data/skills/rspec/examples/matchers/yield.rb +147 -0
  236. data/skills/rspec/examples/mocks/any_instance.rb +172 -0
  237. data/skills/rspec/examples/mocks/argument_matchers.rb +206 -0
  238. data/skills/rspec/examples/mocks/constants.rb +177 -0
  239. data/skills/rspec/examples/mocks/doubles.rb +139 -0
  240. data/skills/rspec/examples/mocks/expectations.rb +137 -0
  241. data/skills/rspec/examples/mocks/message_chains.rb +173 -0
  242. data/skills/rspec/examples/mocks/ordering.rb +144 -0
  243. data/skills/rspec/examples/mocks/receive_counts.rb +181 -0
  244. data/skills/rspec/examples/mocks/responses.rb +223 -0
  245. data/skills/rspec/examples/mocks/spies.rb +149 -0
  246. data/skills/rspec/examples/mocks/stubbing.rb +133 -0
  247. data/skills/rspec/examples/rails/channels.rb +250 -0
  248. data/skills/rspec/examples/rails/controller_specs.rb +302 -0
  249. data/skills/rspec/examples/rails/helper_specs.rb +245 -0
  250. data/skills/rspec/examples/rails/job_specs.rb +256 -0
  251. data/skills/rspec/examples/rails/mailer_specs.rb +228 -0
  252. data/skills/rspec/examples/rails/matchers.rb +374 -0
  253. data/skills/rspec/examples/rails/model_specs.rb +193 -0
  254. data/skills/rspec/examples/rails/request_specs.rb +275 -0
  255. data/skills/rspec/examples/rails/routing_specs.rb +276 -0
  256. data/skills/rspec/examples/rails/system_specs.rb +294 -0
  257. data/skills/rspec/examples/rails/transactions.rb +254 -0
  258. data/skills/rspec/examples/rails/view_specs.rb +252 -0
  259. data/skills/rspec/references/core.md +816 -0
  260. data/skills/rspec/references/factory_bot.md +641 -0
  261. data/skills/rspec/references/matchers.md +516 -0
  262. data/skills/rspec/references/mocks.md +381 -0
  263. data/skills/rspec/references/rails.md +528 -0
  264. data/templates/soul.md +40 -0
  265. data/workflows/commit.md +45 -0
  266. data/workflows/create_handoff.md +98 -0
  267. data/workflows/create_note.md +82 -0
  268. data/workflows/create_plan.md +457 -0
  269. data/workflows/decompose_ticket.md +109 -0
  270. data/workflows/feature.md +91 -0
  271. data/workflows/implement_plan.md +87 -0
  272. data/workflows/iterate_plan.md +247 -0
  273. data/workflows/research_codebase.md +210 -0
  274. data/workflows/resume_handoff.md +217 -0
  275. data/workflows/review_pr.md +320 -0
  276. data/workflows/thoughts_init.md +71 -0
  277. data/workflows/validate_plan.md +166 -0
  278. metadata +290 -3
  279. data/app/controllers/api/sessions_controller.rb +0 -25
  280. data/lib/events/subscribers/action_cable_bridge.rb +0 -59
@@ -0,0 +1,320 @@
1
+ # FactoryBot: Callbacks Examples
2
+ # Source: factory_bot gem spec/acceptance/callbacks_spec.rb
3
+
4
+ # Callbacks execute at specific points during object creation.
5
+ # Available: before(:build), after(:build), before(:create),
6
+ # after(:create), after(:stub)
7
+
8
+ # Basic callbacks
9
+ FactoryBot.define do
10
+ factory :user do
11
+ name { "John" }
12
+
13
+ after(:build) do |user|
14
+ user.setup_defaults
15
+ end
16
+
17
+ after(:create) do |user|
18
+ user.send_welcome_email
19
+ end
20
+
21
+ after(:stub) do |user|
22
+ user.define_singleton_method(:fake?) { true }
23
+ end
24
+ end
25
+ end
26
+
27
+ RSpec.describe "Basic callbacks" do
28
+ describe "after(:build)" do
29
+ let(:user) { build(:user) }
30
+
31
+ it "runs after object is built" do
32
+ # setup_defaults was called
33
+ expect(user).to be_valid
34
+ end
35
+ end
36
+
37
+ describe "after(:create)" do
38
+ let(:user) { create(:user) }
39
+
40
+ it "runs after object is persisted" do
41
+ # send_welcome_email was called
42
+ expect(user).to be_persisted
43
+ end
44
+ end
45
+
46
+ describe "after(:stub)" do
47
+ let(:user) { build_stubbed(:user) }
48
+
49
+ it "runs after stubbing" do
50
+ expect(user.fake?).to be true
51
+ end
52
+ end
53
+ end
54
+
55
+ # Callback with evaluator
56
+ FactoryBot.define do
57
+ factory :user do
58
+ transient do
59
+ skip_confirmation { false }
60
+ posts_count { 0 }
61
+ end
62
+
63
+ after(:create) do |user, evaluator|
64
+ user.confirm! unless evaluator.skip_confirmation
65
+ create_list(:post, evaluator.posts_count, author: user)
66
+ end
67
+ end
68
+ end
69
+
70
+ RSpec.describe "Callback with evaluator" do
71
+ describe "accessing transient attributes" do
72
+ let(:unconfirmed_user) { create(:user, skip_confirmation: true) }
73
+ let(:user_with_posts) { create(:user, posts_count: 3) }
74
+
75
+ it "uses transient in callback logic" do
76
+ expect(unconfirmed_user).not_to be_confirmed
77
+ expect(user_with_posts.posts.count).to eq(3)
78
+ end
79
+ end
80
+ end
81
+
82
+ # Callback execution order
83
+ FactoryBot.define do
84
+ factory :item do
85
+ sequence(:log) { |n| [] }
86
+
87
+ before(:build) { |item| item.log << "before_build" }
88
+ after(:build) { |item| item.log << "after_build" }
89
+ before(:create) { |item| item.log << "before_create" }
90
+ after(:create) { |item| item.log << "after_create" }
91
+ end
92
+ end
93
+
94
+ RSpec.describe "Callback order" do
95
+ describe "build strategy" do
96
+ let(:item) { build(:item) }
97
+
98
+ it "runs build callbacks only" do
99
+ expect(item.log).to eq(%w[before_build after_build])
100
+ end
101
+ end
102
+
103
+ describe "create strategy" do
104
+ let(:item) { create(:item) }
105
+
106
+ it "runs all callbacks in order" do
107
+ expect(item.log).to eq(%w[
108
+ before_build after_build before_create after_create
109
+ ])
110
+ end
111
+ end
112
+ end
113
+
114
+ # Inherited callback order
115
+ FactoryBot.define do
116
+ factory :parent_item, class: "Item" do
117
+ after(:create) { |item| item.log << "parent_callback" }
118
+
119
+ factory :child_item do
120
+ after(:create) { |item| item.log << "child_callback" }
121
+ end
122
+ end
123
+ end
124
+
125
+ RSpec.describe "Inherited callbacks" do
126
+ describe "parent then child" do
127
+ let(:item) { create(:child_item) }
128
+
129
+ it "runs parent callbacks first" do
130
+ # parent_callback before child_callback
131
+ parent_idx = item.log.index("parent_callback")
132
+ child_idx = item.log.index("child_callback")
133
+ expect(parent_idx).to be < child_idx
134
+ end
135
+ end
136
+ end
137
+
138
+ # Trait callbacks
139
+ FactoryBot.define do
140
+ factory :user do
141
+ name { "User" }
142
+
143
+ trait :with_avatar do
144
+ after(:create) { |user| user.avatar.attach(io: File.open("avatar.png"), filename: "avatar.png") }
145
+ end
146
+
147
+ trait :activated do
148
+ after(:build) { |user| user.activate! }
149
+ end
150
+ end
151
+ end
152
+
153
+ RSpec.describe "Trait callbacks" do
154
+ describe "callback in trait" do
155
+ let(:user) { create(:user, :with_avatar) }
156
+
157
+ it "runs trait callback when trait applied" do
158
+ expect(user.avatar).to be_attached
159
+ end
160
+ end
161
+
162
+ describe "multiple trait callbacks" do
163
+ let(:user) { create(:user, :activated, :with_avatar) }
164
+
165
+ it "runs callbacks in trait order" do
166
+ expect(user).to be_activated
167
+ expect(user.avatar).to be_attached
168
+ end
169
+ end
170
+ end
171
+
172
+ # Global callbacks
173
+ FactoryBot.define do
174
+ # Global callback - applies to ALL factories
175
+ after(:build) do |object|
176
+ object.metadata = {created_by: "factory"} if object.respond_to?(:metadata=)
177
+ end
178
+
179
+ factory :record do
180
+ name { "Record" }
181
+ end
182
+
183
+ factory :document do
184
+ title { "Document" }
185
+ end
186
+ end
187
+
188
+ RSpec.describe "Global callbacks" do
189
+ describe "applies to all factories" do
190
+ let(:record) { build(:record) }
191
+ let(:document) { build(:document) }
192
+
193
+ it "runs for every factory" do
194
+ expect(record.metadata[:created_by]).to eq("factory")
195
+ expect(document.metadata[:created_by]).to eq("factory")
196
+ end
197
+ end
198
+ end
199
+
200
+ # Symbol#to_proc in callbacks
201
+ FactoryBot.define do
202
+ factory :user do
203
+ name { "user" }
204
+
205
+ after(:build, &:normalize_name!)
206
+ after(:create, &:index_for_search!)
207
+ end
208
+ end
209
+
210
+ RSpec.describe "Symbol#to_proc callbacks" do
211
+ let(:user) { create(:user) }
212
+
213
+ it "calls method on instance" do
214
+ # normalize_name! and index_for_search! were called
215
+ expect(user).to be_valid
216
+ end
217
+ end
218
+
219
+ # Multiple callbacks for same event
220
+ FactoryBot.define do
221
+ factory :order do
222
+ after(:create) { |order| order.calculate_totals }
223
+ after(:create) { |order| order.update_inventory }
224
+ after(:create) { |order| order.notify_warehouse }
225
+ end
226
+ end
227
+
228
+ RSpec.describe "Multiple callbacks" do
229
+ let(:order) { create(:order) }
230
+
231
+ it "runs all callbacks in definition order" do
232
+ # All three after(:create) callbacks ran
233
+ expect(order.totals_calculated?).to be true
234
+ expect(order.inventory_updated?).to be true
235
+ expect(order.warehouse_notified?).to be true
236
+ end
237
+ end
238
+
239
+ # Skipping create
240
+ FactoryBot.define do
241
+ factory :api_resource do
242
+ skip_create # Don't call save
243
+
244
+ name { "Resource" }
245
+ end
246
+ end
247
+
248
+ RSpec.describe "skip_create" do
249
+ describe "bypasses persistence" do
250
+ let(:resource) { create(:api_resource) }
251
+
252
+ it "doesn't persist to database" do
253
+ expect(resource).to be_new_record
254
+ end
255
+ end
256
+ end
257
+
258
+ # Custom to_create
259
+ FactoryBot.define do
260
+ factory :external_record do
261
+ name { "External" }
262
+
263
+ to_create { |instance| instance.remote_save! }
264
+ end
265
+
266
+ factory :validated_record do
267
+ name { "Validated" }
268
+
269
+ to_create { |instance| instance.save(validate: false) }
270
+ end
271
+ end
272
+
273
+ RSpec.describe "Custom to_create" do
274
+ describe "custom persistence" do
275
+ let(:external) { create(:external_record) }
276
+
277
+ it "uses custom create method" do
278
+ # remote_save! was called instead of save!
279
+ expect(external).to be_persisted
280
+ end
281
+ end
282
+
283
+ describe "skip validation" do
284
+ let(:record) { create(:validated_record) }
285
+
286
+ it "saves without validation" do
287
+ # save(validate: false) was called
288
+ expect(record).to be_persisted
289
+ end
290
+ end
291
+ end
292
+
293
+ # to_create with evaluator
294
+ FactoryBot.define do
295
+ factory :configurable_record do
296
+ transient do
297
+ persist_immediately { true }
298
+ end
299
+
300
+ to_create do |instance, evaluator|
301
+ if evaluator.persist_immediately
302
+ instance.save!
303
+ else
304
+ instance.queue_for_later_save
305
+ end
306
+ end
307
+ end
308
+ end
309
+
310
+ RSpec.describe "to_create with evaluator" do
311
+ describe "conditional persistence" do
312
+ let(:immediate) { create(:configurable_record, persist_immediately: true) }
313
+ let(:deferred) { create(:configurable_record, persist_immediately: false) }
314
+
315
+ it "uses transient to control behavior" do
316
+ expect(immediate).to be_persisted
317
+ expect(deferred).to be_queued
318
+ end
319
+ end
320
+ end
@@ -0,0 +1,328 @@
1
+ # FactoryBot: Custom Construction Examples
2
+ # Source: factory_bot gem spec/acceptance/initialize_with_spec.rb
3
+
4
+ # initialize_with customizes how objects are constructed.
5
+ # Use for non-standard constructors or value objects.
6
+
7
+ # Basic initialize_with
8
+ FactoryBot.define do
9
+ factory :point do
10
+ x { 0 }
11
+ y { 0 }
12
+
13
+ initialize_with { Point.new(x, y) }
14
+ end
15
+ end
16
+
17
+ RSpec.describe "Basic initialize_with" do
18
+ let(:point) { build(:point, x: 5, y: 10) }
19
+
20
+ it "uses custom constructor" do
21
+ expect(point.x).to eq(5)
22
+ expect(point.y).to eq(10)
23
+ end
24
+ end
25
+
26
+ # Using new helper
27
+ FactoryBot.define do
28
+ factory :user do
29
+ name { "John" }
30
+ email { "john@example.com" }
31
+
32
+ # 'new' automatically calls User.new
33
+ initialize_with { new(name:, email:) }
34
+ end
35
+ end
36
+
37
+ RSpec.describe "new helper" do
38
+ let(:user) { build(:user) }
39
+
40
+ it "calls class constructor" do
41
+ expect(user.name).to eq("John")
42
+ end
43
+ end
44
+
45
+ # Using attributes hash
46
+ FactoryBot.define do
47
+ factory :config do
48
+ setting_a { "value_a" }
49
+ setting_b { "value_b" }
50
+
51
+ initialize_with { new(**attributes) }
52
+ end
53
+ end
54
+
55
+ RSpec.describe "attributes hash" do
56
+ let(:config) { build(:config) }
57
+
58
+ it "passes all attributes to constructor" do
59
+ expect(config.setting_a).to eq("value_a")
60
+ expect(config.setting_b).to eq("value_b")
61
+ end
62
+ end
63
+
64
+ # Class methods as constructors
65
+ FactoryBot.define do
66
+ factory :user_from_api, class: "User" do
67
+ external_id { "ext_123" }
68
+ name { "API User" }
69
+
70
+ initialize_with { User.from_api(external_id, name) }
71
+ end
72
+
73
+ factory :user_with_defaults, class: "User" do
74
+ role { "member" }
75
+
76
+ initialize_with { User.create_with_defaults(role) }
77
+ end
78
+ end
79
+
80
+ RSpec.describe "Class method constructors" do
81
+ describe "factory method" do
82
+ let(:user) { build(:user_from_api) }
83
+
84
+ it "uses class method" do
85
+ expect(user.external_id).to eq("ext_123")
86
+ end
87
+ end
88
+ end
89
+
90
+ # Transient attributes in initialize_with
91
+ FactoryBot.define do
92
+ factory :document do
93
+ transient do
94
+ template { :blank }
95
+ end
96
+
97
+ title { "Document" }
98
+
99
+ initialize_with { Document.from_template(template, title:) }
100
+ end
101
+ end
102
+
103
+ RSpec.describe "Transient in initialize_with" do
104
+ describe "accessing transient" do
105
+ let(:invoice) { build(:document, template: :invoice) }
106
+
107
+ it "uses transient in constructor" do
108
+ expect(invoice.template).to eq(:invoice)
109
+ end
110
+ end
111
+ end
112
+
113
+ # Non-ActiveRecord objects
114
+ FactoryBot.define do
115
+ factory :report_generator do
116
+ transient do
117
+ report_name { "Monthly Report" }
118
+ data_source { :database }
119
+ end
120
+
121
+ initialize_with { ReportGenerator.new(report_name, data_source) }
122
+
123
+ skip_create # Not an ActiveRecord model
124
+ end
125
+ end
126
+
127
+ RSpec.describe "Plain Ruby objects" do
128
+ let(:generator) { create(:report_generator, report_name: "Sales") }
129
+
130
+ it "works without ActiveRecord" do
131
+ expect(generator.report_name).to eq("Sales")
132
+ end
133
+ end
134
+
135
+ # Value objects
136
+ FactoryBot.define do
137
+ factory :money do
138
+ amount { 100 }
139
+ currency { "USD" }
140
+
141
+ initialize_with { Money.new(amount, currency) }
142
+ skip_create
143
+ end
144
+ end
145
+
146
+ RSpec.describe "Value objects" do
147
+ let(:money) { build(:money, amount: 50, currency: "EUR") }
148
+
149
+ it "creates immutable value object" do
150
+ expect(money.amount).to eq(50)
151
+ expect(money.currency).to eq("EUR")
152
+ end
153
+ end
154
+
155
+ # Singleton-like construction
156
+ FactoryBot.define do
157
+ factory :app_config, class: "AppConfig" do
158
+ environment { "test" }
159
+
160
+ initialize_with { AppConfig.instance_for(environment) }
161
+ skip_create
162
+ end
163
+ end
164
+
165
+ # Block in constructor
166
+ FactoryBot.define do
167
+ factory :lazy_loader do
168
+ transient do
169
+ load_proc { -> { "loaded" } }
170
+ end
171
+
172
+ initialize_with { LazyLoader.new(&load_proc) }
173
+ skip_create
174
+ end
175
+ end
176
+
177
+ RSpec.describe "Constructor with block" do
178
+ let(:loader) { build(:lazy_loader) }
179
+
180
+ it "passes block to constructor" do
181
+ expect(loader.load).to eq("loaded")
182
+ end
183
+ end
184
+
185
+ # Inheritance with initialize_with
186
+ FactoryBot.define do
187
+ factory :base_model do
188
+ name { "Base" }
189
+ initialize_with { new(name:) }
190
+
191
+ factory :extended_model do
192
+ description { "Extended" }
193
+ # Inherits initialize_with from parent
194
+ end
195
+
196
+ factory :custom_model do
197
+ # Override parent's initialize_with
198
+ initialize_with { new(name: "Custom Override") }
199
+ end
200
+ end
201
+ end
202
+
203
+ RSpec.describe "initialize_with inheritance" do
204
+ describe "child inherits" do
205
+ let(:extended) { build(:extended_model) }
206
+
207
+ it "uses parent constructor" do
208
+ expect(extended.name).to eq("Base")
209
+ end
210
+ end
211
+
212
+ describe "child overrides" do
213
+ let(:custom) { build(:custom_model) }
214
+
215
+ it "uses own constructor" do
216
+ expect(custom.name).to eq("Custom Override")
217
+ end
218
+ end
219
+ end
220
+
221
+ # to_create customization
222
+ FactoryBot.define do
223
+ factory :external_resource do
224
+ name { "Resource" }
225
+
226
+ to_create { |instance| instance.sync_to_remote! }
227
+ end
228
+
229
+ factory :bulk_insertable do
230
+ name { "Bulk" }
231
+
232
+ to_create { |instance| instance.save(validate: false) }
233
+ end
234
+ end
235
+
236
+ RSpec.describe "Custom to_create" do
237
+ describe "remote sync" do
238
+ let(:resource) { create(:external_resource) }
239
+
240
+ it "uses custom persistence" do
241
+ expect(resource).to be_synced
242
+ end
243
+ end
244
+
245
+ describe "skip validation" do
246
+ let(:record) { create(:bulk_insertable) }
247
+
248
+ it "saves without validation" do
249
+ expect(record).to be_persisted
250
+ end
251
+ end
252
+ end
253
+
254
+ # to_create with evaluator
255
+ FactoryBot.define do
256
+ factory :conditional_save do
257
+ transient do
258
+ force { false }
259
+ end
260
+
261
+ name { "Record" }
262
+
263
+ to_create do |instance, evaluator|
264
+ if evaluator.force
265
+ instance.save!(validate: false)
266
+ else
267
+ instance.save!
268
+ end
269
+ end
270
+ end
271
+ end
272
+
273
+ RSpec.describe "to_create with evaluator" do
274
+ let(:forced) { create(:conditional_save, force: true) }
275
+ let(:normal) { create(:conditional_save, force: false) }
276
+
277
+ it "uses transient to control behavior" do
278
+ expect(forced).to be_persisted
279
+ expect(normal).to be_persisted
280
+ end
281
+ end
282
+
283
+ # skip_create for read-only objects
284
+ FactoryBot.define do
285
+ factory :read_only_view, class: "DatabaseView" do
286
+ name { "View" }
287
+ skip_create
288
+ end
289
+ end
290
+
291
+ RSpec.describe "skip_create" do
292
+ let(:view) { create(:read_only_view) }
293
+
294
+ it "doesn't persist" do
295
+ expect(view).to be_new_record
296
+ end
297
+ end
298
+
299
+ # Complex initialization
300
+ FactoryBot.define do
301
+ factory :complex_object do
302
+ transient do
303
+ config { {} }
304
+ dependencies { [] }
305
+ end
306
+
307
+ name { "Complex" }
308
+
309
+ initialize_with do
310
+ obj = ComplexObject.new(name)
311
+ obj.configure(config)
312
+ dependencies.each { |dep| obj.add_dependency(dep) }
313
+ obj
314
+ end
315
+
316
+ skip_create
317
+ end
318
+ end
319
+
320
+ RSpec.describe "Complex initialization" do
321
+ let(:deps) { [build(:dependency), build(:dependency)] }
322
+ let(:complex) { build(:complex_object, config: {key: "value"}, dependencies: deps) }
323
+
324
+ it "performs multi-step initialization" do
325
+ expect(complex.config[:key]).to eq("value")
326
+ expect(complex.dependencies.count).to eq(2)
327
+ end
328
+ end