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.
- checksums.yaml +4 -4
- data/.reek.yml +27 -1
- data/CHANGELOG.md +4 -0
- data/README.md +219 -25
- 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 +76 -28
- data/app/jobs/agent_request_job.rb +24 -0
- data/app/jobs/analytical_brain_job.rb +33 -0
- data/app/jobs/count_event_tokens_job.rb +1 -1
- data/app/models/concerns/event/broadcasting.rb +20 -2
- data/app/models/event.rb +1 -1
- data/app/models/goal.rb +91 -0
- data/app/models/session.rb +347 -22
- data/config/application.rb +2 -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 -9
- 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 +4 -0
- 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/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 +8 -7
- 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 +3 -4
- data/lib/tools/mcp_tool.rb +114 -0
- data/lib/tools/read.rb +15 -16
- 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/tui/app.rb +332 -43
- data/lib/tui/message_store.rb +20 -0
- data/lib/tui/screens/chat.rb +207 -20
- 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 +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
|