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.
- checksums.yaml +4 -4
- data/.reek.yml +27 -1
- data/CHANGELOG.md +19 -0
- data/README.md +213 -43
- data/agents/codebase-analyzer.md +88 -0
- data/agents/codebase-pattern-finder.md +83 -0
- data/agents/documentation-researcher.md +59 -0
- data/agents/thoughts-analyzer.md +102 -0
- data/agents/web-search-researcher.md +71 -0
- data/anima-core.gemspec +3 -0
- data/app/channels/session_channel.rb +195 -45
- data/app/decorators/user_message_decorator.rb +16 -5
- data/app/jobs/agent_request_job.rb +55 -2
- data/app/jobs/analytical_brain_job.rb +33 -0
- data/app/jobs/count_event_tokens_job.rb +15 -4
- data/app/models/concerns/event/broadcasting.rb +81 -0
- data/app/models/event.rb +20 -1
- data/app/models/goal.rb +91 -0
- data/app/models/session.rb +366 -21
- data/config/application.rb +2 -0
- data/config/initializers/event_subscribers.rb +0 -1
- data/config/routes.rb +0 -6
- data/db/migrate/20260313010000_add_status_to_events.rb +8 -0
- data/db/migrate/20260313020000_add_processing_to_sessions.rb +7 -0
- data/db/migrate/20260314075248_add_subagent_support_to_sessions.rb +6 -0
- data/db/migrate/20260314112417_add_granted_tools_to_sessions.rb +5 -0
- data/db/migrate/20260314140000_add_name_to_sessions.rb +7 -0
- data/db/migrate/20260314150000_add_viewport_event_ids_to_sessions.rb +7 -0
- data/db/migrate/20260315100000_add_active_skills_to_sessions.rb +7 -0
- data/db/migrate/20260315140843_create_goals.rb +16 -0
- data/db/migrate/20260315144837_add_completed_at_to_goals.rb +5 -0
- data/db/migrate/20260315191105_add_active_workflow_to_sessions.rb +5 -0
- data/lib/agent_loop.rb +65 -6
- data/lib/agents/definition.rb +116 -0
- data/lib/agents/registry.rb +106 -0
- data/lib/analytical_brain/runner.rb +276 -0
- data/lib/analytical_brain/tools/activate_skill.rb +52 -0
- data/lib/analytical_brain/tools/deactivate_skill.rb +43 -0
- data/lib/analytical_brain/tools/deactivate_workflow.rb +34 -0
- data/lib/analytical_brain/tools/everything_is_ready.rb +28 -0
- data/lib/analytical_brain/tools/finish_goal.rb +62 -0
- data/lib/analytical_brain/tools/read_workflow.rb +58 -0
- data/lib/analytical_brain/tools/rename_session.rb +63 -0
- data/lib/analytical_brain/tools/set_goal.rb +60 -0
- data/lib/analytical_brain/tools/update_goal.rb +60 -0
- data/lib/analytical_brain.rb +23 -0
- data/lib/anima/cli/mcp/secrets.rb +76 -0
- data/lib/anima/cli/mcp.rb +197 -0
- data/lib/anima/cli.rb +5 -40
- data/lib/anima/installer.rb +168 -0
- data/lib/anima/settings.rb +226 -0
- data/lib/anima/version.rb +1 -1
- data/lib/anima.rb +9 -0
- data/lib/credential_store.rb +103 -0
- data/lib/environment_probe.rb +232 -0
- data/lib/events/subscribers/persister.rb +1 -0
- data/lib/events/user_message.rb +17 -0
- data/lib/llm/client.rb +29 -10
- data/lib/mcp/client_manager.rb +86 -0
- data/lib/mcp/config.rb +213 -0
- data/lib/mcp/health_check.rb +77 -0
- data/lib/mcp/secrets.rb +73 -0
- data/lib/mcp/stdio_transport.rb +206 -0
- data/lib/providers/anthropic.rb +11 -20
- data/lib/shell_session.rb +11 -10
- data/lib/skills/definition.rb +97 -0
- data/lib/skills/registry.rb +105 -0
- data/lib/tools/edit.rb +226 -0
- data/lib/tools/mcp_tool.rb +114 -0
- data/lib/tools/read.rb +151 -0
- data/lib/tools/registry.rb +14 -12
- data/lib/tools/request_feature.rb +121 -0
- data/lib/tools/return_result.rb +81 -0
- data/lib/tools/spawn_specialist.rb +109 -0
- data/lib/tools/spawn_subagent.rb +111 -0
- data/lib/tools/subagent_prompts.rb +12 -0
- data/lib/tools/web_get.rb +8 -9
- data/lib/tools/write.rb +86 -0
- data/lib/tui/app.rb +985 -26
- data/lib/tui/cable_client.rb +69 -31
- data/lib/tui/message_store.rb +103 -8
- data/lib/tui/screens/chat.rb +293 -45
- data/lib/workflows/definition.rb +97 -0
- data/lib/workflows/registry.rb +89 -0
- data/skills/activerecord/SKILL.md +255 -0
- data/skills/activerecord/examples/associations/association_extensions.rb +298 -0
- data/skills/activerecord/examples/associations/basic_associations.rb +118 -0
- data/skills/activerecord/examples/associations/counter_caches.rb +215 -0
- data/skills/activerecord/examples/associations/polymorphic_associations.rb +217 -0
- data/skills/activerecord/examples/associations/self_referential.rb +302 -0
- data/skills/activerecord/examples/associations/through_associations.rb +203 -0
- data/skills/activerecord/examples/basics/crud_operations.rb +209 -0
- data/skills/activerecord/examples/basics/dirty_tracking.rb +218 -0
- data/skills/activerecord/examples/basics/inheritance.rb +377 -0
- data/skills/activerecord/examples/basics/type_casting.rb +317 -0
- data/skills/activerecord/examples/callbacks/alternatives_to_callbacks.rb +447 -0
- data/skills/activerecord/examples/callbacks/conditional_callbacks.rb +353 -0
- data/skills/activerecord/examples/callbacks/lifecycle_callbacks.rb +280 -0
- data/skills/activerecord/examples/callbacks/transaction_callbacks.rb +340 -0
- data/skills/activerecord/examples/migrations/indexes_and_constraints.rb +337 -0
- data/skills/activerecord/examples/migrations/reversible_patterns.rb +403 -0
- data/skills/activerecord/examples/migrations/safe_patterns.rb +420 -0
- data/skills/activerecord/examples/migrations/schema_changes.rb +277 -0
- data/skills/activerecord/examples/querying/batch_processing.rb +226 -0
- data/skills/activerecord/examples/querying/eager_loading.rb +259 -0
- data/skills/activerecord/examples/querying/finder_methods.rb +170 -0
- data/skills/activerecord/examples/querying/optimization.rb +275 -0
- data/skills/activerecord/examples/querying/scopes.rb +260 -0
- data/skills/activerecord/examples/validations/built_in_validators.rb +277 -0
- data/skills/activerecord/examples/validations/conditional_validations.rb +288 -0
- data/skills/activerecord/examples/validations/custom_validators.rb +381 -0
- data/skills/activerecord/examples/validations/database_constraints.rb +432 -0
- data/skills/activerecord/examples/validations/validation_contexts.rb +367 -0
- data/skills/activerecord/references/associations.md +709 -0
- data/skills/activerecord/references/basics.md +622 -0
- data/skills/activerecord/references/callbacks.md +738 -0
- data/skills/activerecord/references/migrations.md +657 -0
- data/skills/activerecord/references/querying.md +655 -0
- data/skills/activerecord/references/validations.md +596 -0
- data/skills/dragonruby/SKILL.md +250 -0
- data/skills/dragonruby/examples/audio/audio_events.rb +55 -0
- data/skills/dragonruby/examples/audio/background_music.rb +29 -0
- data/skills/dragonruby/examples/audio/crossfade.rb +51 -0
- data/skills/dragonruby/examples/audio/music_controls.rb +51 -0
- data/skills/dragonruby/examples/audio/sound_effects.rb +30 -0
- data/skills/dragonruby/examples/core/coordinate_system.rb +27 -0
- data/skills/dragonruby/examples/core/hello_world.rb +24 -0
- data/skills/dragonruby/examples/core/labels.rb +22 -0
- data/skills/dragonruby/examples/core/sprites.rb +35 -0
- data/skills/dragonruby/examples/core/state_management.rb +29 -0
- data/skills/dragonruby/examples/distribution/background_pause.rb +42 -0
- data/skills/dragonruby/examples/distribution/build_workflow.sh +26 -0
- data/skills/dragonruby/examples/distribution/cvars_production.txt +16 -0
- data/skills/dragonruby/examples/distribution/game_metadata_hd.txt +23 -0
- data/skills/dragonruby/examples/distribution/game_metadata_minimal.txt +9 -0
- data/skills/dragonruby/examples/distribution/game_metadata_mobile.txt +31 -0
- data/skills/dragonruby/examples/distribution/platform_detection.rb +36 -0
- data/skills/dragonruby/examples/distribution/steam_metadata.txt +19 -0
- data/skills/dragonruby/examples/entities/collision_detection.rb +43 -0
- data/skills/dragonruby/examples/entities/entity_lifecycle.rb +68 -0
- data/skills/dragonruby/examples/entities/entity_storage.rb +38 -0
- data/skills/dragonruby/examples/entities/factory_methods.rb +45 -0
- data/skills/dragonruby/examples/entities/random_spawning.rb +50 -0
- data/skills/dragonruby/examples/game-logic/reset_patterns.rb +98 -0
- data/skills/dragonruby/examples/game-logic/save_load.rb +101 -0
- data/skills/dragonruby/examples/game-logic/scoring.rb +104 -0
- data/skills/dragonruby/examples/game-logic/state_transitions.rb +103 -0
- data/skills/dragonruby/examples/game-logic/timers.rb +87 -0
- data/skills/dragonruby/examples/input/action_triggers.rb +36 -0
- data/skills/dragonruby/examples/input/analog_movement.rb +28 -0
- data/skills/dragonruby/examples/input/controller_input.rb +28 -0
- data/skills/dragonruby/examples/input/directional_input.rb +24 -0
- data/skills/dragonruby/examples/input/keyboard_input.rb +28 -0
- data/skills/dragonruby/examples/input/mouse_click.rb +26 -0
- data/skills/dragonruby/examples/input/movement_with_bounds.rb +22 -0
- data/skills/dragonruby/examples/input/normalized_movement.rb +32 -0
- data/skills/dragonruby/examples/rendering/frame_animation.rb +32 -0
- data/skills/dragonruby/examples/rendering/labels.rb +32 -0
- data/skills/dragonruby/examples/rendering/layering.rb +51 -0
- data/skills/dragonruby/examples/rendering/solids.rb +61 -0
- data/skills/dragonruby/examples/rendering/sprites.rb +33 -0
- data/skills/dragonruby/examples/rendering/spritesheet_animation.rb +39 -0
- data/skills/dragonruby/examples/scenes/case_dispatch.rb +60 -0
- data/skills/dragonruby/examples/scenes/class_based.rb +150 -0
- data/skills/dragonruby/examples/scenes/pause_overlay.rb +100 -0
- data/skills/dragonruby/examples/scenes/safe_transitions.rb +68 -0
- data/skills/dragonruby/examples/scenes/scene_transitions.rb +98 -0
- data/skills/dragonruby/examples/scenes/send_dispatch.rb +88 -0
- data/skills/dragonruby/references/audio.md +396 -0
- data/skills/dragonruby/references/core.md +385 -0
- data/skills/dragonruby/references/distribution.md +434 -0
- data/skills/dragonruby/references/entities.md +516 -0
- data/skills/dragonruby/references/game-logic/persistence.md +386 -0
- data/skills/dragonruby/references/game-logic/state.md +389 -0
- data/skills/dragonruby/references/input.md +414 -0
- data/skills/dragonruby/references/rendering/animation.md +467 -0
- data/skills/dragonruby/references/rendering/primitives.md +403 -0
- data/skills/dragonruby/references/scenes.md +443 -0
- data/skills/draper-decorators/SKILL.md +344 -0
- data/skills/draper-decorators/examples/application_decorator.rb +61 -0
- data/skills/draper-decorators/examples/decorator_spec.rb +253 -0
- data/skills/draper-decorators/examples/model_decorator.rb +152 -0
- data/skills/draper-decorators/references/anti-patterns.md +640 -0
- data/skills/draper-decorators/references/patterns.md +507 -0
- data/skills/draper-decorators/references/testing.md +559 -0
- data/skills/gh-issue.md +182 -0
- data/skills/mcp-server/SKILL.md +177 -0
- data/skills/mcp-server/examples/dynamic_tools.rb +36 -0
- data/skills/mcp-server/examples/file_manager_tool.rb +85 -0
- data/skills/mcp-server/examples/http_client.rb +48 -0
- data/skills/mcp-server/examples/http_server.rb +97 -0
- data/skills/mcp-server/examples/rails_integration.rb +88 -0
- data/skills/mcp-server/examples/stdio_server.rb +108 -0
- data/skills/mcp-server/examples/streaming_client.rb +95 -0
- data/skills/mcp-server/references/gotchas.md +183 -0
- data/skills/mcp-server/references/prompts.md +98 -0
- data/skills/mcp-server/references/resources.md +53 -0
- data/skills/mcp-server/references/server.md +140 -0
- data/skills/mcp-server/references/tools.md +146 -0
- data/skills/mcp-server/references/transport.md +104 -0
- data/skills/ratatui-ruby/SKILL.md +315 -0
- data/skills/ratatui-ruby/references/core-concepts.md +340 -0
- data/skills/ratatui-ruby/references/events.md +387 -0
- data/skills/ratatui-ruby/references/frameworks.md +522 -0
- data/skills/ratatui-ruby/references/layout.md +423 -0
- data/skills/ratatui-ruby/references/styling.md +268 -0
- data/skills/ratatui-ruby/references/testing.md +433 -0
- data/skills/ratatui-ruby/references/widgets.md +532 -0
- data/skills/rspec/SKILL.md +340 -0
- data/skills/rspec/examples/core/basic_structure.rb +69 -0
- data/skills/rspec/examples/core/configuration.rb +126 -0
- data/skills/rspec/examples/core/hooks.rb +126 -0
- data/skills/rspec/examples/core/memoized_helpers.rb +139 -0
- data/skills/rspec/examples/core/metadata_filtering.rb +144 -0
- data/skills/rspec/examples/core/shared_examples.rb +145 -0
- data/skills/rspec/examples/factory_bot/associations.rb +314 -0
- data/skills/rspec/examples/factory_bot/build_strategies.rb +272 -0
- data/skills/rspec/examples/factory_bot/callbacks.rb +320 -0
- data/skills/rspec/examples/factory_bot/custom_construction.rb +328 -0
- data/skills/rspec/examples/factory_bot/factory_definition.rb +191 -0
- data/skills/rspec/examples/factory_bot/inheritance.rb +314 -0
- data/skills/rspec/examples/factory_bot/traits.rb +293 -0
- data/skills/rspec/examples/factory_bot/transients.rb +229 -0
- data/skills/rspec/examples/matchers/change.rb +115 -0
- data/skills/rspec/examples/matchers/collections.rb +154 -0
- data/skills/rspec/examples/matchers/comparisons.rb +79 -0
- data/skills/rspec/examples/matchers/composing.rb +155 -0
- data/skills/rspec/examples/matchers/custom_matchers.rb +197 -0
- data/skills/rspec/examples/matchers/equality.rb +58 -0
- data/skills/rspec/examples/matchers/errors.rb +136 -0
- data/skills/rspec/examples/matchers/output.rb +103 -0
- data/skills/rspec/examples/matchers/predicates.rb +87 -0
- data/skills/rspec/examples/matchers/truthiness.rb +101 -0
- data/skills/rspec/examples/matchers/types.rb +82 -0
- data/skills/rspec/examples/matchers/yield.rb +147 -0
- data/skills/rspec/examples/mocks/any_instance.rb +172 -0
- data/skills/rspec/examples/mocks/argument_matchers.rb +206 -0
- data/skills/rspec/examples/mocks/constants.rb +177 -0
- data/skills/rspec/examples/mocks/doubles.rb +139 -0
- data/skills/rspec/examples/mocks/expectations.rb +137 -0
- data/skills/rspec/examples/mocks/message_chains.rb +173 -0
- data/skills/rspec/examples/mocks/ordering.rb +144 -0
- data/skills/rspec/examples/mocks/receive_counts.rb +181 -0
- data/skills/rspec/examples/mocks/responses.rb +223 -0
- data/skills/rspec/examples/mocks/spies.rb +149 -0
- data/skills/rspec/examples/mocks/stubbing.rb +133 -0
- data/skills/rspec/examples/rails/channels.rb +250 -0
- data/skills/rspec/examples/rails/controller_specs.rb +302 -0
- data/skills/rspec/examples/rails/helper_specs.rb +245 -0
- data/skills/rspec/examples/rails/job_specs.rb +256 -0
- data/skills/rspec/examples/rails/mailer_specs.rb +228 -0
- data/skills/rspec/examples/rails/matchers.rb +374 -0
- data/skills/rspec/examples/rails/model_specs.rb +193 -0
- data/skills/rspec/examples/rails/request_specs.rb +275 -0
- data/skills/rspec/examples/rails/routing_specs.rb +276 -0
- data/skills/rspec/examples/rails/system_specs.rb +294 -0
- data/skills/rspec/examples/rails/transactions.rb +254 -0
- data/skills/rspec/examples/rails/view_specs.rb +252 -0
- data/skills/rspec/references/core.md +816 -0
- data/skills/rspec/references/factory_bot.md +641 -0
- data/skills/rspec/references/matchers.md +516 -0
- data/skills/rspec/references/mocks.md +381 -0
- data/skills/rspec/references/rails.md +528 -0
- data/templates/soul.md +40 -0
- data/workflows/commit.md +45 -0
- data/workflows/create_handoff.md +98 -0
- data/workflows/create_note.md +82 -0
- data/workflows/create_plan.md +457 -0
- data/workflows/decompose_ticket.md +109 -0
- data/workflows/feature.md +91 -0
- data/workflows/implement_plan.md +87 -0
- data/workflows/iterate_plan.md +247 -0
- data/workflows/research_codebase.md +210 -0
- data/workflows/resume_handoff.md +217 -0
- data/workflows/review_pr.md +320 -0
- data/workflows/thoughts_init.md +71 -0
- data/workflows/validate_plan.md +166 -0
- metadata +290 -3
- data/app/controllers/api/sessions_controller.rb +0 -25
- 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
|