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,154 @@
|
|
|
1
|
+
# RSpec Matchers: Collection Examples
|
|
2
|
+
# Source: rspec-expectations gem features/built_in_matchers/include.feature,
|
|
3
|
+
# contain_exactly.feature, all.feature, have_attributes.feature
|
|
4
|
+
|
|
5
|
+
# include - partial matching
|
|
6
|
+
RSpec.describe "include matcher" do
|
|
7
|
+
describe "with arrays" do
|
|
8
|
+
it "checks single element" do
|
|
9
|
+
expect([1, 2, 3]).to include(1)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
it "checks multiple elements" do
|
|
13
|
+
expect([1, 2, 3]).to include(1, 2)
|
|
14
|
+
expect([1, 2, 3]).to include(1, 2, 3)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
it "works with composed matchers" do
|
|
18
|
+
expect([1, 3, 7]).to include(a_kind_of(Integer))
|
|
19
|
+
expect([1, 3, 7]).to include(be_odd.and be < 10)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
describe "with strings" do
|
|
24
|
+
it "checks substring" do
|
|
25
|
+
expect("hello world").to include("world")
|
|
26
|
+
expect("hello world").to include("hello", "world")
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it "works with regex" do
|
|
30
|
+
expect("hello world").to include(/wor.d/)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
describe "with hashes" do
|
|
35
|
+
let(:hash) { { a: 1, b: 2, c: 3 } }
|
|
36
|
+
|
|
37
|
+
it "checks key existence" do
|
|
38
|
+
expect(hash).to include(:a)
|
|
39
|
+
expect(hash).to include(:a, :b)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it "checks key-value pairs" do
|
|
43
|
+
expect(hash).to include(a: 1)
|
|
44
|
+
expect(hash).to include(a: 1, b: 2)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
describe "with counts" do
|
|
49
|
+
let(:items) { [{ type: :a }, { type: :b }, { type: :a }] }
|
|
50
|
+
|
|
51
|
+
it "specifies occurrence count" do
|
|
52
|
+
expect(items).to include(have_key(:type)).exactly(3).times
|
|
53
|
+
expect(items).to include(type: :a).twice
|
|
54
|
+
expect(items).to include(type: :b).once
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# contain_exactly / match_array - order-independent full match
|
|
60
|
+
RSpec.describe "contain_exactly matcher" do
|
|
61
|
+
it "matches regardless of order" do
|
|
62
|
+
expect([1, 2, 3]).to contain_exactly(3, 2, 1)
|
|
63
|
+
expect([1, 2, 3]).to contain_exactly(2, 3, 1)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
it "requires all elements present" do
|
|
67
|
+
expect([1, 2, 3]).not_to contain_exactly(1, 2) # Missing 3
|
|
68
|
+
expect([1, 2, 3]).not_to contain_exactly(1, 2, 3, 4) # Extra 4
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
it "works with composed matchers" do
|
|
72
|
+
expect(["barn", 2.45]).to contain_exactly(
|
|
73
|
+
a_value_within(0.1).of(2.5),
|
|
74
|
+
a_string_starting_with("bar")
|
|
75
|
+
)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
it "has match_array alias" do
|
|
79
|
+
expect([1, 2, 3]).to match_array([3, 2, 1])
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# start_with / end_with
|
|
84
|
+
RSpec.describe "start_with/end_with matchers" do
|
|
85
|
+
describe "with strings" do
|
|
86
|
+
it "checks prefix/suffix" do
|
|
87
|
+
expect("hello world").to start_with("hello")
|
|
88
|
+
expect("hello world").to end_with("world")
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
describe "with arrays" do
|
|
93
|
+
it "checks first/last elements" do
|
|
94
|
+
expect([0, 1, 2, 3]).to start_with(0)
|
|
95
|
+
expect([0, 1, 2, 3]).to start_with(0, 1)
|
|
96
|
+
expect([0, 1, 2, 3]).to end_with(3)
|
|
97
|
+
expect([0, 1, 2, 3]).to end_with(2, 3)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# all - every element matches
|
|
103
|
+
RSpec.describe "all matcher" do
|
|
104
|
+
it "requires all elements to match" do
|
|
105
|
+
expect([1, 3, 5]).to all(be_odd)
|
|
106
|
+
expect([1, 3, 5]).to all(be_an(Integer))
|
|
107
|
+
expect([1, 3, 5]).to all(be < 10)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
it "works with compound matchers" do
|
|
111
|
+
expect([1, 3, 5]).to all(be_odd.and be_an(Integer))
|
|
112
|
+
expect([1, 4, 21]).to all(be_odd.or be < 10)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
it "provides clear failure messages" do
|
|
116
|
+
# When one element fails, message shows which
|
|
117
|
+
expect(["foo", "bar", "baz"]).to all(be_a(String).and include("a"))
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# have_attributes - object attribute matching
|
|
122
|
+
RSpec.describe "have_attributes matcher" do
|
|
123
|
+
subject(:user) { build(:user, name: "Alice", age: 25) }
|
|
124
|
+
|
|
125
|
+
it "checks single attribute" do
|
|
126
|
+
expect(user).to have_attributes(name: "Alice")
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
it "checks multiple attributes" do
|
|
130
|
+
expect(user).to have_attributes(name: "Alice", age: 25)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
it "works with composed matchers" do
|
|
134
|
+
expect(user).to have_attributes(name: a_string_starting_with("A"))
|
|
135
|
+
expect(user).to have_attributes(age: a_value > 18)
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Practical example: testing query results
|
|
140
|
+
RSpec.describe User, ".active scope" do
|
|
141
|
+
let!(:active_users) { create_list(:user, 3, :active) }
|
|
142
|
+
let!(:inactive_user) { create(:user, :inactive) }
|
|
143
|
+
|
|
144
|
+
subject(:results) { described_class.active }
|
|
145
|
+
|
|
146
|
+
it "returns only active users" do
|
|
147
|
+
expect(results).to contain_exactly(*active_users)
|
|
148
|
+
expect(results).not_to include(inactive_user)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
it "returns users with expected attributes" do
|
|
152
|
+
expect(results).to all(have_attributes(status: "active"))
|
|
153
|
+
end
|
|
154
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# RSpec Matchers: Comparison Examples
|
|
2
|
+
# Source: rspec-expectations gem features/built_in_matchers/comparisons.feature, be_within.feature
|
|
3
|
+
|
|
4
|
+
# Operator comparisons
|
|
5
|
+
RSpec.describe "comparison operators" do
|
|
6
|
+
it "supports numeric comparisons" do
|
|
7
|
+
expect(18).to be > 15
|
|
8
|
+
expect(18).to be >= 17
|
|
9
|
+
expect(18).to be <= 19
|
|
10
|
+
expect(18).to be < 20
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
it "supports string comparisons (alphabetical)" do
|
|
14
|
+
expect("Strawberry").to be < "Tomato"
|
|
15
|
+
expect("Strawberry").to be > "Apple"
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# be_within - floating point tolerance
|
|
20
|
+
RSpec.describe "be_within matcher" do
|
|
21
|
+
it "handles floating point precision" do
|
|
22
|
+
radius = 3
|
|
23
|
+
area = radius * radius * Math::PI
|
|
24
|
+
|
|
25
|
+
expect(area).to be_within(0.1).of(28.3)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it "specifies acceptable delta" do
|
|
29
|
+
expect(27.5).to be_within(0.5).of(27.9)
|
|
30
|
+
expect(27.5).to be_within(0.5).of(28.0)
|
|
31
|
+
expect(27.5).to be_within(0.5).of(27.0)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Works with Time objects too
|
|
35
|
+
it "compares times with tolerance" do
|
|
36
|
+
now = Time.now
|
|
37
|
+
expect(now).to be_within(1.second).of(Time.now)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# be_between - range checking
|
|
42
|
+
RSpec.describe "be_between matcher" do
|
|
43
|
+
it "defaults to inclusive" do
|
|
44
|
+
expect(5).to be_between(1, 10)
|
|
45
|
+
expect(1).to be_between(1, 10) # Inclusive: includes 1
|
|
46
|
+
expect(10).to be_between(1, 10) # Inclusive: includes 10
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
it "supports explicit inclusive" do
|
|
50
|
+
expect(5).to be_between(1, 10).inclusive
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
it "supports exclusive ranges" do
|
|
54
|
+
expect(5).to be_between(1, 10).exclusive
|
|
55
|
+
expect(1).not_to be_between(1, 10).exclusive # Excludes boundaries
|
|
56
|
+
expect(10).not_to be_between(1, 10).exclusive
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Practical example
|
|
61
|
+
RSpec.describe Order do
|
|
62
|
+
subject(:order) { build(:order, total: 150) }
|
|
63
|
+
|
|
64
|
+
describe "#shipping_rate" do
|
|
65
|
+
context "with standard order" do
|
|
66
|
+
it "returns rate within expected range" do
|
|
67
|
+
expect(order.shipping_rate).to be_between(5.0, 25.0)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
context "with heavy order" do
|
|
72
|
+
subject(:order) { build(:order, :heavy, total: 500) }
|
|
73
|
+
|
|
74
|
+
it "returns higher rate" do
|
|
75
|
+
expect(order.shipping_rate).to be > 20.0
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# RSpec Matchers: Composing Matchers Examples
|
|
2
|
+
# Source: rspec-expectations gem features/composing_matchers.feature,
|
|
3
|
+
# compound_expectations.feature
|
|
4
|
+
|
|
5
|
+
# Compound expectations with .and / &
|
|
6
|
+
RSpec.describe "compound matchers with .and" do
|
|
7
|
+
it "combines matchers with .and" do
|
|
8
|
+
expect("hello world").to start_with("hello").and end_with("world")
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
it "uses & as alias" do
|
|
12
|
+
expect("hello world").to start_with("hello") & end_with("world")
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
it "chains multiple conditions" do
|
|
16
|
+
expect(5).to be_positive.and be < 10
|
|
17
|
+
expect([1, 2, 3]).to include(1).and include(3)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Compound expectations with .or / |
|
|
22
|
+
RSpec.describe "compound matchers with .or" do
|
|
23
|
+
it "passes if any matcher passes" do
|
|
24
|
+
color = %w[red green blue].sample
|
|
25
|
+
expect(color).to eq("red").or eq("green").or eq("blue")
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it "uses | as alias" do
|
|
29
|
+
value = [true, false].sample
|
|
30
|
+
expect(value).to be(true) | be(false)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Matcher aliases for composition
|
|
35
|
+
RSpec.describe "composable matcher aliases" do
|
|
36
|
+
# Noun-phrase aliases read better in composed contexts:
|
|
37
|
+
# be_within => a_value_within
|
|
38
|
+
# be_an_instance => an_instance_of
|
|
39
|
+
# start_with => a_string_starting_with
|
|
40
|
+
# include => a_collection_including
|
|
41
|
+
# be_a => a_kind_of
|
|
42
|
+
|
|
43
|
+
it "uses noun-phrase aliases in expect(...).to include" do
|
|
44
|
+
expect([1, 2.5, "foo"]).to include(
|
|
45
|
+
an_instance_of(Integer),
|
|
46
|
+
a_value_within(0.1).of(2.5),
|
|
47
|
+
a_string_starting_with("f")
|
|
48
|
+
)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
it "uses noun-phrase aliases with contain_exactly" do
|
|
52
|
+
expect(["barn", 2.45]).to contain_exactly(
|
|
53
|
+
a_value_within(0.1).of(2.5),
|
|
54
|
+
a_string_starting_with("bar")
|
|
55
|
+
)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Matchers as arguments
|
|
60
|
+
RSpec.describe "matchers accepting matcher arguments" do
|
|
61
|
+
describe "with change" do
|
|
62
|
+
it "uses composed matchers for from/to" do
|
|
63
|
+
text = "foo bar"
|
|
64
|
+
expect { text = "baz qux" }
|
|
65
|
+
.to change { text }
|
|
66
|
+
.from(a_string_matching(/foo/))
|
|
67
|
+
.to(a_string_matching(/baz/))
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
it "uses composed matchers for delta" do
|
|
71
|
+
value = 0.0
|
|
72
|
+
expect { value += 1.05 }
|
|
73
|
+
.to change { value }
|
|
74
|
+
.by(a_value_within(0.1).of(1.0))
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
describe "with include" do
|
|
79
|
+
it "matches hash values with matchers" do
|
|
80
|
+
expect(a: "food", b: "good").to include(a: a_string_matching(/foo/))
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
it "matches hash keys with matchers" do
|
|
84
|
+
expect("food" => 1, "drink" => 2).to include(a_string_matching(/foo/))
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
describe "with match (nested structures)" do
|
|
89
|
+
it "validates deeply nested data" do
|
|
90
|
+
response = {
|
|
91
|
+
user: {
|
|
92
|
+
name: "Alice",
|
|
93
|
+
roles: ["admin", "editor"],
|
|
94
|
+
settings: { theme: "dark" }
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
expect(response).to match(
|
|
99
|
+
user: {
|
|
100
|
+
name: a_string_starting_with("A"),
|
|
101
|
+
roles: a_collection_including("admin"),
|
|
102
|
+
settings: { theme: a_kind_of(String) }
|
|
103
|
+
}
|
|
104
|
+
)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Practical examples
|
|
110
|
+
RSpec.describe API::Response do
|
|
111
|
+
subject(:response) { build(:api_response, :success) }
|
|
112
|
+
|
|
113
|
+
describe "response structure validation" do
|
|
114
|
+
it "has expected structure" do
|
|
115
|
+
expect(response.body).to match(
|
|
116
|
+
status: "success",
|
|
117
|
+
data: {
|
|
118
|
+
id: an_instance_of(Integer),
|
|
119
|
+
created_at: a_string_matching(/\d{4}-\d{2}-\d{2}/),
|
|
120
|
+
items: a_collection_including(
|
|
121
|
+
have_attributes(name: a_kind_of(String))
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
)
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
RSpec.describe User do
|
|
130
|
+
describe "#update" do
|
|
131
|
+
subject(:user) { create(:user, name: "Alice", updated_at: 1.day.ago) }
|
|
132
|
+
|
|
133
|
+
it "changes name and updated_at" do
|
|
134
|
+
expect { user.update(name: "Bob") }
|
|
135
|
+
.to change(user, :name)
|
|
136
|
+
.from(a_string_starting_with("A"))
|
|
137
|
+
.to(a_string_starting_with("B"))
|
|
138
|
+
.and change(user, :updated_at)
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
RSpec.describe "result validation" do
|
|
144
|
+
subject(:result) { build(:calculation_result) }
|
|
145
|
+
|
|
146
|
+
it "returns numeric value in expected range" do
|
|
147
|
+
expect(result.value)
|
|
148
|
+
.to be_a(Numeric)
|
|
149
|
+
.and be_within(0.01).of(expected_value)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
it "has valid status" do
|
|
153
|
+
expect(result.status).to eq(:success).or eq(:partial)
|
|
154
|
+
end
|
|
155
|
+
end
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# RSpec Matchers: Custom Matcher Examples
|
|
2
|
+
# Source: rspec-expectations gem features/custom_matchers/define_matcher.feature
|
|
3
|
+
|
|
4
|
+
# Basic custom matcher using DSL
|
|
5
|
+
RSpec::Matchers.define :be_a_multiple_of do |expected|
|
|
6
|
+
match do |actual|
|
|
7
|
+
actual % expected == 0
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
RSpec.describe "be_a_multiple_of matcher" do
|
|
12
|
+
it "passes for multiples" do
|
|
13
|
+
expect(9).to be_a_multiple_of(3)
|
|
14
|
+
expect(12).to be_a_multiple_of(4)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
it "fails for non-multiples" do
|
|
18
|
+
expect(9).not_to be_a_multiple_of(4)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Custom matcher with messages and description
|
|
23
|
+
RSpec::Matchers.define :be_in_range do |min, max|
|
|
24
|
+
match do |actual|
|
|
25
|
+
actual >= min && actual <= max
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
failure_message do |actual|
|
|
29
|
+
"expected #{actual} to be between #{min} and #{max}"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
failure_message_when_negated do |actual|
|
|
33
|
+
"expected #{actual} not to be between #{min} and #{max}"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
description do
|
|
37
|
+
"be in range #{min}..#{max}"
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
RSpec.describe "be_in_range matcher" do
|
|
42
|
+
it "passes for values in range" do
|
|
43
|
+
expect(5).to be_in_range(1, 10)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
it "provides readable description" do
|
|
47
|
+
# In output: "should be in range 1..10"
|
|
48
|
+
expect(5).to be_in_range(1, 10)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Custom matcher with chaining
|
|
53
|
+
RSpec::Matchers.define :have_errors_on do |attribute|
|
|
54
|
+
chain :with_message do |message|
|
|
55
|
+
@expected_message = message
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
match do |model|
|
|
59
|
+
model.valid?
|
|
60
|
+
errors = model.errors[attribute]
|
|
61
|
+
|
|
62
|
+
if @expected_message
|
|
63
|
+
errors.include?(@expected_message)
|
|
64
|
+
else
|
|
65
|
+
errors.any?
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
failure_message do |model|
|
|
70
|
+
if @expected_message
|
|
71
|
+
"expected #{model.class} to have error '#{@expected_message}' on #{attribute}, " \
|
|
72
|
+
"but got: #{model.errors[attribute].inspect}"
|
|
73
|
+
else
|
|
74
|
+
"expected #{model.class} to have errors on #{attribute}"
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
RSpec.describe "have_errors_on matcher with chaining" do
|
|
80
|
+
subject(:user) { build(:user, email: nil) }
|
|
81
|
+
|
|
82
|
+
it "checks for any error on attribute" do
|
|
83
|
+
expect(user).to have_errors_on(:email)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
it "checks for specific error message" do
|
|
87
|
+
expect(user).to have_errors_on(:email).with_message("can't be blank")
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Custom matcher with diffable output
|
|
92
|
+
RSpec::Matchers.define :match_json_structure do |expected|
|
|
93
|
+
diffable # Enables diff output on failure
|
|
94
|
+
|
|
95
|
+
match do |actual|
|
|
96
|
+
@actual_parsed = JSON.parse(actual)
|
|
97
|
+
structure_matches?(expected, @actual_parsed)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def structure_matches?(expected, actual)
|
|
101
|
+
case expected
|
|
102
|
+
when Hash
|
|
103
|
+
expected.all? { |k, v| actual.key?(k.to_s) && structure_matches?(v, actual[k.to_s]) }
|
|
104
|
+
when Array
|
|
105
|
+
expected.all? { |v| actual.any? { |a| structure_matches?(v, a) } }
|
|
106
|
+
when Class
|
|
107
|
+
actual.is_a?(expected)
|
|
108
|
+
else
|
|
109
|
+
true
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Custom matcher supporting block expectations
|
|
115
|
+
RSpec::Matchers.define :complete_within do |timeout|
|
|
116
|
+
supports_block_expectations # Required for expect { }
|
|
117
|
+
|
|
118
|
+
match do |block|
|
|
119
|
+
started = Time.now
|
|
120
|
+
block.call
|
|
121
|
+
Time.now - started < timeout
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
failure_message do
|
|
125
|
+
"expected block to complete within #{timeout} seconds"
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
RSpec.describe "complete_within matcher" do
|
|
130
|
+
it "passes for fast operations" do
|
|
131
|
+
expect { 1 + 1 }.to complete_within(1.second)
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Custom class-based matcher (from scratch)
|
|
136
|
+
class BeValidJSON
|
|
137
|
+
include RSpec::Matchers::Composable
|
|
138
|
+
|
|
139
|
+
def matches?(actual)
|
|
140
|
+
@actual = actual
|
|
141
|
+
JSON.parse(actual)
|
|
142
|
+
true
|
|
143
|
+
rescue JSON::ParserError => e
|
|
144
|
+
@error = e
|
|
145
|
+
false
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def failure_message
|
|
149
|
+
"expected valid JSON, got parse error: #{@error.message}"
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def failure_message_when_negated
|
|
153
|
+
"expected invalid JSON, but it parsed successfully"
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def description
|
|
157
|
+
"be valid JSON"
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def be_valid_json
|
|
162
|
+
BeValidJSON.new
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
RSpec.describe "be_valid_json matcher" do
|
|
166
|
+
it "passes for valid JSON" do
|
|
167
|
+
expect('{"key": "value"}').to be_valid_json
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
it "fails for invalid JSON" do
|
|
171
|
+
expect("{invalid}").not_to be_valid_json
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Negated matcher using define_negated_matcher
|
|
176
|
+
RSpec::Matchers.define_negated_matcher :exclude, :include
|
|
177
|
+
|
|
178
|
+
RSpec.describe "negated matcher" do
|
|
179
|
+
it "reads more naturally" do
|
|
180
|
+
expect([1, 2, 3]).to exclude(4)
|
|
181
|
+
expect("hello").to exclude("xyz")
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Aliased matcher
|
|
186
|
+
RSpec::Matchers.alias_matcher :a_user_with, :have_attributes
|
|
187
|
+
|
|
188
|
+
RSpec.describe "aliased matcher" do
|
|
189
|
+
it "provides domain-specific language" do
|
|
190
|
+
users = [
|
|
191
|
+
build(:user, name: "Alice", role: :admin),
|
|
192
|
+
build(:user, name: "Bob", role: :member)
|
|
193
|
+
]
|
|
194
|
+
|
|
195
|
+
expect(users).to include(a_user_with(name: "Alice", role: :admin))
|
|
196
|
+
end
|
|
197
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# RSpec Matchers: Equality Examples
|
|
2
|
+
# Source: rspec-expectations gem features/built_in_matchers/equality.feature
|
|
3
|
+
|
|
4
|
+
# eq - value equality using ==
|
|
5
|
+
RSpec.describe "eq matcher" do
|
|
6
|
+
it "compares using ==" do
|
|
7
|
+
expect(5).to eq(5)
|
|
8
|
+
expect("string").to eq("string")
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
it "allows type coercion" do
|
|
12
|
+
expect(5).to eq(5.0) # Integer == Float
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
it "fails for different values" do
|
|
16
|
+
expect(5).not_to eq(6)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# eql - value equivalence using eql? (type-sensitive)
|
|
21
|
+
RSpec.describe "eql matcher" do
|
|
22
|
+
it "compares using eql?" do
|
|
23
|
+
expect(5).to eql(5)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
it "is type-sensitive" do
|
|
27
|
+
expect(5).not_to eql(5.0) # Integer.eql?(Float) => false
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# equal/be - object identity using equal?
|
|
32
|
+
RSpec.describe "equal/be matcher" do
|
|
33
|
+
let(:string) { "hello" }
|
|
34
|
+
|
|
35
|
+
it "passes for same object" do
|
|
36
|
+
expect(string).to equal(string)
|
|
37
|
+
expect(string).to be(string) # be is alias for equal
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
it "fails for different objects with same value" do
|
|
41
|
+
expect("hello").not_to equal("hello") # Different String objects
|
|
42
|
+
expect("hello").not_to be("hello")
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Practical example: memoization verification
|
|
47
|
+
RSpec.describe User do
|
|
48
|
+
subject(:user) { build(:user) }
|
|
49
|
+
|
|
50
|
+
describe "#full_name" do
|
|
51
|
+
it "memoizes result" do
|
|
52
|
+
first_call = user.full_name
|
|
53
|
+
second_call = user.full_name
|
|
54
|
+
|
|
55
|
+
expect(first_call).to be(second_call) # Same object returned
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|