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,254 @@
|
|
|
1
|
+
# RSpec Rails: Transactions and Database Examples
|
|
2
|
+
# Source: rspec-rails gem features/Transactions.md
|
|
3
|
+
|
|
4
|
+
# Database transactions in RSpec Rails.
|
|
5
|
+
# Each example runs in a transaction that's rolled back after.
|
|
6
|
+
# Location: spec/rails_helper.rb configuration
|
|
7
|
+
|
|
8
|
+
# Default transactional behavior
|
|
9
|
+
RSpec.describe Widget, type: :model do
|
|
10
|
+
describe "transaction isolation" do
|
|
11
|
+
it "has no widgets initially" do
|
|
12
|
+
expect(Widget.count).to eq(0)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
it "has one widget after creating" do
|
|
16
|
+
Widget.create!(name: "Test")
|
|
17
|
+
expect(Widget.count).to eq(1)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
it "has no widgets again (previous was rolled back)" do
|
|
21
|
+
expect(Widget.count).to eq(0)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Using let! for setup within transaction
|
|
27
|
+
RSpec.describe "let! with transactions" do
|
|
28
|
+
let!(:widget) { create(:widget) }
|
|
29
|
+
|
|
30
|
+
it "widget exists in first example" do
|
|
31
|
+
expect(Widget.count).to eq(1)
|
|
32
|
+
expect(Widget.first).to eq(widget)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it "widget exists in second example (recreated)" do
|
|
36
|
+
# let! runs before each example
|
|
37
|
+
expect(Widget.count).to eq(1)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# before(:example) vs before(:context)
|
|
42
|
+
RSpec.describe "before hooks and transactions" do
|
|
43
|
+
describe "before(:example)" do
|
|
44
|
+
before(:example) { create(:widget, name: "Example Widget") }
|
|
45
|
+
|
|
46
|
+
it "creates widget for each example" do
|
|
47
|
+
expect(Widget.count).to eq(1)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
it "has fresh widget in next example" do
|
|
51
|
+
expect(Widget.count).to eq(1)
|
|
52
|
+
expect(Widget.first.name).to eq("Example Widget")
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
describe "before(:context)" do
|
|
57
|
+
# WARNING: Data created in before(:context) persists across examples
|
|
58
|
+
# and is NOT rolled back. Must clean up manually.
|
|
59
|
+
|
|
60
|
+
before(:context) do
|
|
61
|
+
@context_widget = Widget.create!(name: "Context Widget")
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
after(:context) do
|
|
65
|
+
@context_widget.destroy
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
before(:example) do
|
|
69
|
+
# Must reload to avoid stale data within transaction
|
|
70
|
+
@context_widget.reload
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
it "uses shared context widget" do
|
|
74
|
+
expect(@context_widget.name).to eq("Context Widget")
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
it "also sees the context widget" do
|
|
78
|
+
expect(Widget.find(@context_widget.id)).to be_present
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Testing transactions explicitly
|
|
84
|
+
RSpec.describe "Explicit transaction testing" do
|
|
85
|
+
describe "rollback behavior" do
|
|
86
|
+
it "rolls back on error" do
|
|
87
|
+
expect {
|
|
88
|
+
Widget.transaction do
|
|
89
|
+
Widget.create!(name: "Will be rolled back")
|
|
90
|
+
raise ActiveRecord::Rollback
|
|
91
|
+
end
|
|
92
|
+
}.not_to change(Widget, :count)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
it "commits without error" do
|
|
96
|
+
expect {
|
|
97
|
+
Widget.transaction do
|
|
98
|
+
Widget.create!(name: "Will be committed")
|
|
99
|
+
end
|
|
100
|
+
}.to change(Widget, :count).by(1)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Configuration example (for rails_helper.rb)
|
|
106
|
+
RSpec.describe "Transaction configuration" do
|
|
107
|
+
# Enable transactional fixtures (default)
|
|
108
|
+
# RSpec.configure do |config|
|
|
109
|
+
# config.use_transactional_fixtures = true
|
|
110
|
+
# end
|
|
111
|
+
|
|
112
|
+
it "demonstrates transactional isolation" do
|
|
113
|
+
widget = create(:widget)
|
|
114
|
+
expect(Widget.count).to eq(1)
|
|
115
|
+
|
|
116
|
+
# After this test, widget won't exist
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Database Cleaner for non-transactional scenarios
|
|
121
|
+
# (e.g., JavaScript tests with Selenium)
|
|
122
|
+
RSpec.describe "Database Cleaner setup", skip: "documentation only" do
|
|
123
|
+
# Configuration example:
|
|
124
|
+
#
|
|
125
|
+
# RSpec.configure do |config|
|
|
126
|
+
# config.before(:suite) do
|
|
127
|
+
# DatabaseCleaner.strategy = :transaction
|
|
128
|
+
# DatabaseCleaner.clean_with(:truncation)
|
|
129
|
+
# end
|
|
130
|
+
#
|
|
131
|
+
# config.around(:each) do |example|
|
|
132
|
+
# DatabaseCleaner.cleaning do
|
|
133
|
+
# example.run
|
|
134
|
+
# end
|
|
135
|
+
# end
|
|
136
|
+
#
|
|
137
|
+
# # Use truncation for JS tests
|
|
138
|
+
# config.before(:each, js: true) do
|
|
139
|
+
# DatabaseCleaner.strategy = :truncation
|
|
140
|
+
# end
|
|
141
|
+
#
|
|
142
|
+
# config.after(:each, js: true) do
|
|
143
|
+
# DatabaseCleaner.strategy = :transaction
|
|
144
|
+
# end
|
|
145
|
+
# end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Testing database constraints
|
|
149
|
+
RSpec.describe "Database constraints" do
|
|
150
|
+
describe "unique constraint" do
|
|
151
|
+
let!(:widget) { create(:widget, name: "Unique") }
|
|
152
|
+
|
|
153
|
+
it "raises error on duplicate" do
|
|
154
|
+
expect {
|
|
155
|
+
create(:widget, name: "Unique")
|
|
156
|
+
}.to raise_error(ActiveRecord::RecordNotUnique)
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
describe "foreign key constraint" do
|
|
161
|
+
let(:widget) { create(:widget) }
|
|
162
|
+
let!(:component) { create(:component, widget:) }
|
|
163
|
+
|
|
164
|
+
it "prevents deletion of referenced record" do
|
|
165
|
+
expect {
|
|
166
|
+
widget.destroy!
|
|
167
|
+
}.to raise_error(ActiveRecord::InvalidForeignKey)
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Testing with specific database features
|
|
173
|
+
RSpec.describe "Database-specific testing" do
|
|
174
|
+
describe "locking" do
|
|
175
|
+
let!(:widget) { create(:widget, count: 0) }
|
|
176
|
+
|
|
177
|
+
it "uses pessimistic locking" do
|
|
178
|
+
Widget.transaction do
|
|
179
|
+
locked = Widget.lock.find(widget.id)
|
|
180
|
+
locked.update!(count: locked.count + 1)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
expect(widget.reload.count).to eq(1)
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
describe "advisory locks" do
|
|
188
|
+
it "obtains and releases lock", skip: "database-specific" do
|
|
189
|
+
Widget.with_advisory_lock("test_lock") do
|
|
190
|
+
# Critical section
|
|
191
|
+
expect(Widget.advisory_lock_exists?("test_lock")).to be true
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Testing seeds
|
|
198
|
+
RSpec.describe "Database seeds", type: :model do
|
|
199
|
+
# Usually seeds are loaded once in test setup
|
|
200
|
+
# Here's how to test seed data
|
|
201
|
+
|
|
202
|
+
describe "seed data" do
|
|
203
|
+
before(:context) do
|
|
204
|
+
# Load seeds if needed
|
|
205
|
+
# Rails.application.load_seed
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
it "creates default categories" do
|
|
209
|
+
expect(Category.count).to be > 0
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
it "creates admin user" do
|
|
213
|
+
expect(User.find_by(role: "admin")).to be_present
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Testing database migrations
|
|
219
|
+
RSpec.describe "Migration testing", skip: "run separately" do
|
|
220
|
+
describe "up migration" do
|
|
221
|
+
it "adds new column" do
|
|
222
|
+
# Assuming a pending migration adds 'status' column
|
|
223
|
+
ActiveRecord::Migrator.up(
|
|
224
|
+
ActiveRecord::Migrator.migrations_paths,
|
|
225
|
+
20240101000000
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
expect(Widget.column_names).to include("status")
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
describe "down migration" do
|
|
233
|
+
it "removes column" do
|
|
234
|
+
ActiveRecord::Migrator.down(
|
|
235
|
+
ActiveRecord::Migrator.migrations_paths,
|
|
236
|
+
20240101000000
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
expect(Widget.column_names).not_to include("status")
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# Parallel testing considerations
|
|
245
|
+
RSpec.describe "Parallel test isolation" do
|
|
246
|
+
# Each parallel worker has its own database
|
|
247
|
+
# Transactions work within each worker
|
|
248
|
+
|
|
249
|
+
it "works in parallel environment" do
|
|
250
|
+
# Create data - won't conflict with other workers
|
|
251
|
+
widget = create(:widget)
|
|
252
|
+
expect(widget).to be_persisted
|
|
253
|
+
end
|
|
254
|
+
end
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
# RSpec Rails: View Specs Examples
|
|
2
|
+
# Source: rspec-rails gem features/view_specs/
|
|
3
|
+
|
|
4
|
+
# View specs test templates in isolation.
|
|
5
|
+
# Three-step pattern: assign, render, assert.
|
|
6
|
+
# Location: spec/views/
|
|
7
|
+
|
|
8
|
+
# Basic view spec
|
|
9
|
+
RSpec.describe "widgets/index", type: :view do
|
|
10
|
+
let(:widgets) { create_list(:widget, 2, :with_names) }
|
|
11
|
+
|
|
12
|
+
before { assign(:widgets, widgets) }
|
|
13
|
+
|
|
14
|
+
it "displays all widgets" do
|
|
15
|
+
render
|
|
16
|
+
|
|
17
|
+
widgets.each do |widget|
|
|
18
|
+
expect(rendered).to include(widget.name)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Testing specific elements
|
|
24
|
+
RSpec.describe "widgets/show", type: :view do
|
|
25
|
+
let(:widget) { create(:widget, name: "Slicer", price: 99) }
|
|
26
|
+
|
|
27
|
+
before { assign(:widget, widget) }
|
|
28
|
+
|
|
29
|
+
it "displays the widget name" do
|
|
30
|
+
render
|
|
31
|
+
expect(rendered).to match(/Slicer/)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it "displays the widget price" do
|
|
35
|
+
render
|
|
36
|
+
expect(rendered).to include("$99")
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Explicit template rendering
|
|
41
|
+
RSpec.describe "rendering the widget template", type: :view do
|
|
42
|
+
let(:widget) { create(:widget, name: "Dicer") }
|
|
43
|
+
|
|
44
|
+
before { assign(:widget, widget) }
|
|
45
|
+
|
|
46
|
+
it "renders with explicit template path" do
|
|
47
|
+
render template: "widgets/widget"
|
|
48
|
+
expect(rendered).to include("Dicer")
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Rendering with layouts
|
|
53
|
+
RSpec.describe "widgets/widget", type: :view do
|
|
54
|
+
let(:widget) { create(:widget, name: "Blender") }
|
|
55
|
+
|
|
56
|
+
before { assign(:widget, widget) }
|
|
57
|
+
|
|
58
|
+
context "with application layout" do
|
|
59
|
+
it "includes header from layout" do
|
|
60
|
+
render template: "widgets/widget", layout: "layouts/application"
|
|
61
|
+
|
|
62
|
+
expect(rendered).to include("Application Header")
|
|
63
|
+
expect(rendered).to include("Blender")
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
context "with admin layout" do
|
|
68
|
+
it "includes admin navigation" do
|
|
69
|
+
render template: "widgets/widget", layout: "layouts/admin"
|
|
70
|
+
|
|
71
|
+
expect(rendered).to include("Admin Navigation")
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Testing partials
|
|
77
|
+
RSpec.describe "widgets/_widget", type: :view do
|
|
78
|
+
let(:widget) { create(:widget, name: "Grinder") }
|
|
79
|
+
|
|
80
|
+
it "renders the widget partial with locals" do
|
|
81
|
+
render partial: "widgets/widget", locals: { widget: }
|
|
82
|
+
|
|
83
|
+
expect(rendered).to include("Grinder")
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Testing collection rendering
|
|
88
|
+
RSpec.describe "widgets/_widget", type: :view do
|
|
89
|
+
let(:widgets) { build_list(:widget, 3) }
|
|
90
|
+
|
|
91
|
+
it "renders each widget in collection" do
|
|
92
|
+
render partial: "widgets/widget", collection: widgets
|
|
93
|
+
|
|
94
|
+
widgets.each do |widget|
|
|
95
|
+
expect(rendered).to include(widget.name)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Stubbing view helpers
|
|
101
|
+
RSpec.describe "secrets/index", type: :view do
|
|
102
|
+
context "when user is admin" do
|
|
103
|
+
before { allow(view).to receive(:admin?).and_return(true) }
|
|
104
|
+
|
|
105
|
+
it "displays admin section" do
|
|
106
|
+
render
|
|
107
|
+
expect(rendered).to include("Secret admin area")
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
context "when user is not admin" do
|
|
112
|
+
before { allow(view).to receive(:admin?).and_return(false) }
|
|
113
|
+
|
|
114
|
+
it "hides admin section" do
|
|
115
|
+
render
|
|
116
|
+
expect(rendered).not_to include("Secret admin area")
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Stubbing current_user
|
|
122
|
+
RSpec.describe "dashboard/index", type: :view do
|
|
123
|
+
let(:user) { build(:user, name: "John") }
|
|
124
|
+
|
|
125
|
+
before { allow(view).to receive(:current_user).and_return(user) }
|
|
126
|
+
|
|
127
|
+
it "greets the current user" do
|
|
128
|
+
render
|
|
129
|
+
expect(rendered).to include("Welcome, John")
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Testing forms
|
|
134
|
+
RSpec.describe "widgets/new", type: :view do
|
|
135
|
+
before { assign(:widget, Widget.new) }
|
|
136
|
+
|
|
137
|
+
it "renders the new widget form" do
|
|
138
|
+
render
|
|
139
|
+
|
|
140
|
+
expect(rendered).to have_selector("form[action='/widgets'][method='post']")
|
|
141
|
+
expect(rendered).to have_selector("input[name='widget[name]']")
|
|
142
|
+
expect(rendered).to have_selector("input[type='submit']")
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Testing edit forms with existing record
|
|
147
|
+
RSpec.describe "widgets/edit", type: :view do
|
|
148
|
+
let(:widget) { create(:widget, name: "Existing Widget") }
|
|
149
|
+
|
|
150
|
+
before { assign(:widget, widget) }
|
|
151
|
+
|
|
152
|
+
it "populates form with widget values" do
|
|
153
|
+
render
|
|
154
|
+
|
|
155
|
+
expect(rendered).to have_selector("form[action='/widgets/#{widget.id}']")
|
|
156
|
+
expect(rendered).to have_selector("input[name='widget[name]'][value='Existing Widget']")
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Testing conditional content
|
|
161
|
+
RSpec.describe "widgets/show", type: :view do
|
|
162
|
+
let(:widget) { create(:widget, :published) }
|
|
163
|
+
|
|
164
|
+
before { assign(:widget, widget) }
|
|
165
|
+
|
|
166
|
+
context "when widget is published" do
|
|
167
|
+
it "shows published badge" do
|
|
168
|
+
render
|
|
169
|
+
expect(rendered).to include("Published")
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
context "when widget is draft" do
|
|
174
|
+
let(:widget) { create(:widget, :draft) }
|
|
175
|
+
|
|
176
|
+
it "shows draft badge" do
|
|
177
|
+
render
|
|
178
|
+
expect(rendered).to include("Draft")
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Testing links
|
|
184
|
+
RSpec.describe "widgets/index", type: :view do
|
|
185
|
+
let(:widgets) { create_list(:widget, 2) }
|
|
186
|
+
|
|
187
|
+
before { assign(:widgets, widgets) }
|
|
188
|
+
|
|
189
|
+
it "links to each widget" do
|
|
190
|
+
render
|
|
191
|
+
|
|
192
|
+
widgets.each do |widget|
|
|
193
|
+
expect(rendered).to have_link(widget.name, href: widget_path(widget))
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
it "links to new widget" do
|
|
198
|
+
render
|
|
199
|
+
expect(rendered).to have_link("New Widget", href: new_widget_path)
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Using Capybara matchers in view specs
|
|
204
|
+
RSpec.describe "widgets/show", type: :view do
|
|
205
|
+
let(:widget) { create(:widget, name: "Test Widget", description: "A great widget") }
|
|
206
|
+
|
|
207
|
+
before { assign(:widget, widget) }
|
|
208
|
+
|
|
209
|
+
it "renders widget details" do
|
|
210
|
+
render
|
|
211
|
+
|
|
212
|
+
expect(rendered).to have_css("h1", text: "Test Widget")
|
|
213
|
+
expect(rendered).to have_css("p.description", text: "A great widget")
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Testing empty states
|
|
218
|
+
RSpec.describe "widgets/index", type: :view do
|
|
219
|
+
context "with no widgets" do
|
|
220
|
+
before { assign(:widgets, []) }
|
|
221
|
+
|
|
222
|
+
it "shows empty state message" do
|
|
223
|
+
render
|
|
224
|
+
expect(rendered).to include("No widgets found")
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# Testing content_for blocks
|
|
230
|
+
RSpec.describe "widgets/show", type: :view do
|
|
231
|
+
let(:widget) { create(:widget, name: "Widget") }
|
|
232
|
+
|
|
233
|
+
before { assign(:widget, widget) }
|
|
234
|
+
|
|
235
|
+
it "sets page title via content_for" do
|
|
236
|
+
render
|
|
237
|
+
|
|
238
|
+
expect(view.content_for(:title)).to eq("Widget")
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# Access view helpers defined in the application
|
|
243
|
+
RSpec.describe "widgets/show", type: :view do
|
|
244
|
+
let(:widget) { create(:widget, price: 1000) }
|
|
245
|
+
|
|
246
|
+
before { assign(:widget, widget) }
|
|
247
|
+
|
|
248
|
+
it "formats currency using helper" do
|
|
249
|
+
render
|
|
250
|
+
expect(rendered).to include("$1,000.00")
|
|
251
|
+
end
|
|
252
|
+
end
|