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,250 @@
|
|
|
1
|
+
# RSpec Rails: Channel Specs Examples
|
|
2
|
+
# Source: rspec-rails gem features/channel_specs/
|
|
3
|
+
|
|
4
|
+
# Channel specs test ActionCable channels.
|
|
5
|
+
# Location: spec/channels/
|
|
6
|
+
|
|
7
|
+
# Basic channel spec
|
|
8
|
+
RSpec.describe ChatChannel, type: :channel do
|
|
9
|
+
describe "#subscribed" do
|
|
10
|
+
it "successfully subscribes" do
|
|
11
|
+
subscribe
|
|
12
|
+
expect(subscription).to be_confirmed
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
it "rejects without authentication" do
|
|
16
|
+
stub_connection current_user: nil
|
|
17
|
+
subscribe
|
|
18
|
+
expect(subscription).to be_rejected
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Testing with connection stub
|
|
24
|
+
RSpec.describe NotificationsChannel, type: :channel do
|
|
25
|
+
let(:user) { create(:user) }
|
|
26
|
+
|
|
27
|
+
before { stub_connection current_user: user }
|
|
28
|
+
|
|
29
|
+
describe "#subscribed" do
|
|
30
|
+
it "subscribes to user-specific stream" do
|
|
31
|
+
subscribe
|
|
32
|
+
|
|
33
|
+
expect(subscription).to be_confirmed
|
|
34
|
+
expect(subscription).to have_stream_for(user)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Testing streams
|
|
40
|
+
RSpec.describe RoomChannel, type: :channel do
|
|
41
|
+
describe "#subscribed" do
|
|
42
|
+
it "streams from room channel" do
|
|
43
|
+
subscribe(room_id: 42)
|
|
44
|
+
|
|
45
|
+
expect(subscription).to have_stream_from("room_42")
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
describe "with model-based stream" do
|
|
50
|
+
let(:room) { create(:room) }
|
|
51
|
+
|
|
52
|
+
it "streams for the room" do
|
|
53
|
+
subscribe(room_id: room.id)
|
|
54
|
+
|
|
55
|
+
expect(subscription).to have_stream_for(room)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Testing channel actions
|
|
61
|
+
RSpec.describe ChatChannel, type: :channel do
|
|
62
|
+
let(:user) { create(:user) }
|
|
63
|
+
let(:room) { create(:room) }
|
|
64
|
+
|
|
65
|
+
before do
|
|
66
|
+
stub_connection current_user: user
|
|
67
|
+
subscribe(room_id: room.id)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
describe "#speak" do
|
|
71
|
+
it "broadcasts message to room" do
|
|
72
|
+
expect {
|
|
73
|
+
perform :speak, message: "Hello!"
|
|
74
|
+
}.to have_broadcasted_to(room).with(
|
|
75
|
+
user: user.name,
|
|
76
|
+
message: "Hello!"
|
|
77
|
+
)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
describe "#typing" do
|
|
82
|
+
it "broadcasts typing status" do
|
|
83
|
+
expect {
|
|
84
|
+
perform :typing
|
|
85
|
+
}.to have_broadcasted_to(room).with(
|
|
86
|
+
user: user.name,
|
|
87
|
+
typing: true
|
|
88
|
+
)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Testing subscription rejection
|
|
94
|
+
RSpec.describe PrivateChannel, type: :channel do
|
|
95
|
+
describe "#subscribed" do
|
|
96
|
+
context "when user lacks access" do
|
|
97
|
+
let(:user) { create(:user) }
|
|
98
|
+
let(:private_room) { create(:room, :private) }
|
|
99
|
+
|
|
100
|
+
before { stub_connection current_user: user }
|
|
101
|
+
|
|
102
|
+
it "rejects subscription" do
|
|
103
|
+
subscribe(room_id: private_room.id)
|
|
104
|
+
|
|
105
|
+
expect(subscription).to be_rejected
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
context "when user has access" do
|
|
110
|
+
let(:user) { create(:user, :admin) }
|
|
111
|
+
let(:private_room) { create(:room, :private) }
|
|
112
|
+
|
|
113
|
+
before { stub_connection current_user: user }
|
|
114
|
+
|
|
115
|
+
it "confirms subscription" do
|
|
116
|
+
subscribe(room_id: private_room.id)
|
|
117
|
+
|
|
118
|
+
expect(subscription).to be_confirmed
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Testing unsubscribe
|
|
125
|
+
RSpec.describe PresenceChannel, type: :channel do
|
|
126
|
+
let(:user) { create(:user) }
|
|
127
|
+
|
|
128
|
+
before { stub_connection current_user: user }
|
|
129
|
+
|
|
130
|
+
describe "#unsubscribed" do
|
|
131
|
+
it "broadcasts user left" do
|
|
132
|
+
subscribe
|
|
133
|
+
|
|
134
|
+
expect {
|
|
135
|
+
unsubscribe
|
|
136
|
+
}.to have_broadcasted_to("presence").with(
|
|
137
|
+
action: "left",
|
|
138
|
+
user: user.name
|
|
139
|
+
)
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Testing connection
|
|
145
|
+
RSpec.describe ApplicationCable::Connection, type: :channel do
|
|
146
|
+
describe "#connect" do
|
|
147
|
+
context "with valid session" do
|
|
148
|
+
let(:user) { create(:user) }
|
|
149
|
+
|
|
150
|
+
before do
|
|
151
|
+
cookies.signed[:user_id] = user.id
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
it "successfully connects" do
|
|
155
|
+
connect
|
|
156
|
+
|
|
157
|
+
expect(connection.current_user).to eq(user)
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
context "without session" do
|
|
162
|
+
it "rejects connection" do
|
|
163
|
+
expect { connect }.to have_rejected_connection
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Testing channel with parameters
|
|
170
|
+
RSpec.describe DocumentChannel, type: :channel do
|
|
171
|
+
let(:user) { create(:user) }
|
|
172
|
+
let(:document) { create(:document) }
|
|
173
|
+
|
|
174
|
+
before { stub_connection current_user: user }
|
|
175
|
+
|
|
176
|
+
describe "#subscribed with params" do
|
|
177
|
+
it "uses provided document_id" do
|
|
178
|
+
subscribe(document_id: document.id)
|
|
179
|
+
|
|
180
|
+
expect(subscription).to have_stream_for(document)
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
describe "#update" do
|
|
185
|
+
before { subscribe(document_id: document.id) }
|
|
186
|
+
|
|
187
|
+
it "broadcasts update to document stream" do
|
|
188
|
+
expect {
|
|
189
|
+
perform :update, content: "New content"
|
|
190
|
+
}.to have_broadcasted_to(document).with(
|
|
191
|
+
a_hash_including(content: "New content")
|
|
192
|
+
)
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Testing broadcast matching
|
|
198
|
+
RSpec.describe "Broadcast matching", type: :channel do
|
|
199
|
+
describe "matching broadcast content" do
|
|
200
|
+
it "matches exact content" do
|
|
201
|
+
expect {
|
|
202
|
+
ActionCable.server.broadcast("test", { action: "update", data: 123 })
|
|
203
|
+
}.to have_broadcasted_to("test").with(action: "update", data: 123)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
it "matches with hash including" do
|
|
207
|
+
expect {
|
|
208
|
+
ActionCable.server.broadcast("test", { action: "update", data: 123, timestamp: Time.current })
|
|
209
|
+
}.to have_broadcasted_to("test").with(a_hash_including(action: "update"))
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
it "matches with block" do
|
|
213
|
+
expect {
|
|
214
|
+
ActionCable.server.broadcast("test", { count: 5 })
|
|
215
|
+
}.to have_broadcasted_to("test").with { |data|
|
|
216
|
+
expect(data[:count]).to be > 0
|
|
217
|
+
}
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# Testing broadcast counts
|
|
223
|
+
RSpec.describe "Broadcast counts", type: :channel do
|
|
224
|
+
it "matches exact broadcast count" do
|
|
225
|
+
expect {
|
|
226
|
+
3.times { ActionCable.server.broadcast("updates", { ping: true }) }
|
|
227
|
+
}.to have_broadcasted_to("updates").exactly(3).times
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
it "matches at least count" do
|
|
231
|
+
expect {
|
|
232
|
+
5.times { ActionCable.server.broadcast("updates", { ping: true }) }
|
|
233
|
+
}.to have_broadcasted_to("updates").at_least(:twice)
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
it "matches at most count" do
|
|
237
|
+
expect {
|
|
238
|
+
2.times { ActionCable.server.broadcast("updates", { ping: true }) }
|
|
239
|
+
}.to have_broadcasted_to("updates").at_most(3).times
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# Testing no broadcast
|
|
244
|
+
RSpec.describe "No broadcast", type: :channel do
|
|
245
|
+
it "verifies nothing was broadcast" do
|
|
246
|
+
expect {
|
|
247
|
+
# Some action that shouldn't broadcast
|
|
248
|
+
}.not_to have_broadcasted_to("notifications")
|
|
249
|
+
end
|
|
250
|
+
end
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
# RSpec Rails: Controller Specs Examples
|
|
2
|
+
# Source: rspec-rails gem features/controller_specs/
|
|
3
|
+
|
|
4
|
+
# NOTE: Request specs are generally preferred for new code.
|
|
5
|
+
# Controller specs are useful for testing specific controller behavior in isolation.
|
|
6
|
+
# Location: spec/controllers/
|
|
7
|
+
|
|
8
|
+
# Basic controller spec
|
|
9
|
+
RSpec.describe WidgetsController, type: :controller do
|
|
10
|
+
describe "GET #index" do
|
|
11
|
+
let!(:widgets) { create_list(:widget, 3) }
|
|
12
|
+
|
|
13
|
+
it "returns a successful response" do
|
|
14
|
+
get :index
|
|
15
|
+
expect(response).to have_http_status(:ok)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
it "assigns @widgets" do
|
|
19
|
+
get :index
|
|
20
|
+
expect(assigns(:widgets)).to match_array(widgets)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it "renders the index template" do
|
|
24
|
+
get :index
|
|
25
|
+
expect(response).to render_template(:index)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
describe "GET #show" do
|
|
30
|
+
let(:widget) { create(:widget) }
|
|
31
|
+
|
|
32
|
+
it "assigns @widget" do
|
|
33
|
+
get :show, params: { id: widget.id }
|
|
34
|
+
expect(assigns(:widget)).to eq(widget)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
describe "GET #new" do
|
|
39
|
+
it "assigns a new widget" do
|
|
40
|
+
get :new
|
|
41
|
+
expect(assigns(:widget)).to be_a_new(Widget)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
describe "POST #create" do
|
|
46
|
+
context "with valid params" do
|
|
47
|
+
let(:valid_params) { { widget: attributes_for(:widget) } }
|
|
48
|
+
|
|
49
|
+
it "creates a new Widget" do
|
|
50
|
+
expect {
|
|
51
|
+
post :create, params: valid_params
|
|
52
|
+
}.to change(Widget, :count).by(1)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
it "redirects to the created widget" do
|
|
56
|
+
post :create, params: valid_params
|
|
57
|
+
expect(response).to redirect_to(Widget.last)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
context "with invalid params" do
|
|
62
|
+
let(:invalid_params) { { widget: { name: "" } } }
|
|
63
|
+
|
|
64
|
+
it "does not create a Widget" do
|
|
65
|
+
expect {
|
|
66
|
+
post :create, params: invalid_params
|
|
67
|
+
}.not_to change(Widget, :count)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
it "renders the new template" do
|
|
71
|
+
post :create, params: invalid_params
|
|
72
|
+
expect(response).to render_template(:new)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
describe "PATCH #update" do
|
|
78
|
+
let(:widget) { create(:widget, name: "Old Name") }
|
|
79
|
+
|
|
80
|
+
context "with valid params" do
|
|
81
|
+
it "updates the widget" do
|
|
82
|
+
patch :update, params: { id: widget.id, widget: { name: "New Name" } }
|
|
83
|
+
expect(widget.reload.name).to eq("New Name")
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
it "redirects to the widget" do
|
|
87
|
+
patch :update, params: { id: widget.id, widget: { name: "New Name" } }
|
|
88
|
+
expect(response).to redirect_to(widget)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
describe "DELETE #destroy" do
|
|
94
|
+
let!(:widget) { create(:widget) }
|
|
95
|
+
|
|
96
|
+
it "destroys the widget" do
|
|
97
|
+
expect {
|
|
98
|
+
delete :destroy, params: { id: widget.id }
|
|
99
|
+
}.to change(Widget, :count).by(-1)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
it "redirects to index" do
|
|
103
|
+
delete :destroy, params: { id: widget.id }
|
|
104
|
+
expect(response).to redirect_to(widgets_url)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# render_views - actually render templates (stubbed by default)
|
|
110
|
+
RSpec.describe WidgetsController, type: :controller do
|
|
111
|
+
render_views
|
|
112
|
+
|
|
113
|
+
describe "GET #index" do
|
|
114
|
+
let!(:widget) { create(:widget, name: "My Widget") }
|
|
115
|
+
|
|
116
|
+
it "renders widget name in body" do
|
|
117
|
+
get :index
|
|
118
|
+
expect(response.body).to include("My Widget")
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Testing different response formats
|
|
124
|
+
RSpec.describe WidgetsController, type: :controller do
|
|
125
|
+
describe "POST #create" do
|
|
126
|
+
let(:valid_params) { { widget: attributes_for(:widget) } }
|
|
127
|
+
|
|
128
|
+
it "responds to HTML by default" do
|
|
129
|
+
post :create, params: valid_params
|
|
130
|
+
expect(response.content_type).to include("text/html")
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
it "responds to JSON when requested" do
|
|
134
|
+
post :create, params: valid_params.merge(format: :json)
|
|
135
|
+
expect(response.content_type).to include("application/json")
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Anonymous controllers for testing base controller behavior
|
|
141
|
+
RSpec.describe ApplicationController, type: :controller do
|
|
142
|
+
controller do
|
|
143
|
+
def index
|
|
144
|
+
raise ApplicationController::AccessDenied
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
describe "handling AccessDenied exceptions" do
|
|
149
|
+
it "redirects to the 401 page" do
|
|
150
|
+
get :index
|
|
151
|
+
expect(response).to redirect_to("/401.html")
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Testing custom controller action
|
|
157
|
+
RSpec.describe ApplicationController, type: :controller do
|
|
158
|
+
controller do
|
|
159
|
+
def custom_action
|
|
160
|
+
render plain: "Custom response"
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
before do
|
|
165
|
+
routes.draw { get "custom_action" => "anonymous#custom_action" }
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
it "renders custom response" do
|
|
169
|
+
get :custom_action
|
|
170
|
+
expect(response.body).to eq("Custom response")
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Testing with request headers
|
|
175
|
+
RSpec.describe ApiController, type: :controller do
|
|
176
|
+
controller do
|
|
177
|
+
def index
|
|
178
|
+
if request.headers["Authorization"].present?
|
|
179
|
+
render plain: "Authenticated"
|
|
180
|
+
else
|
|
181
|
+
render plain: "Not authenticated", status: :unauthorized
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
describe "GET #index" do
|
|
187
|
+
context "with authorization header" do
|
|
188
|
+
before { request.headers["Authorization"] = "Bearer token123" }
|
|
189
|
+
|
|
190
|
+
it "returns success" do
|
|
191
|
+
get :index
|
|
192
|
+
expect(response).to have_http_status(:ok)
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
context "without authorization header" do
|
|
197
|
+
it "returns unauthorized" do
|
|
198
|
+
get :index
|
|
199
|
+
expect(response).to have_http_status(:unauthorized)
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# Inherited controller specs
|
|
206
|
+
RSpec.describe FoosController, type: :controller do
|
|
207
|
+
# When controller inherits from another controller
|
|
208
|
+
controller FoosController do
|
|
209
|
+
def index
|
|
210
|
+
@name = self.class.name
|
|
211
|
+
@controller_name = controller_name
|
|
212
|
+
render plain: "Hello"
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
describe "GET #index" do
|
|
217
|
+
before { get :index }
|
|
218
|
+
|
|
219
|
+
it "gets the class name as described" do
|
|
220
|
+
expect(assigns(:name)).to eq("FoosController")
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
it "gets the controller_name as described" do
|
|
224
|
+
expect(assigns(:controller_name)).to eq("foos")
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# Testing before_action filters
|
|
230
|
+
RSpec.describe ProtectedController, type: :controller do
|
|
231
|
+
controller do
|
|
232
|
+
before_action :require_login
|
|
233
|
+
|
|
234
|
+
def index
|
|
235
|
+
render plain: "Protected content"
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
private
|
|
239
|
+
|
|
240
|
+
def require_login
|
|
241
|
+
redirect_to login_path unless session[:user_id]
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
describe "before_action :require_login" do
|
|
246
|
+
context "when not logged in" do
|
|
247
|
+
it "redirects to login" do
|
|
248
|
+
get :index
|
|
249
|
+
expect(response).to redirect_to(login_path)
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
context "when logged in" do
|
|
254
|
+
before { session[:user_id] = 1 }
|
|
255
|
+
|
|
256
|
+
it "allows access" do
|
|
257
|
+
get :index
|
|
258
|
+
expect(response).to have_http_status(:ok)
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# Testing flash messages
|
|
265
|
+
RSpec.describe WidgetsController, type: :controller do
|
|
266
|
+
describe "POST #create" do
|
|
267
|
+
context "with valid params" do
|
|
268
|
+
it "sets a success flash message" do
|
|
269
|
+
post :create, params: { widget: attributes_for(:widget) }
|
|
270
|
+
expect(flash[:notice]).to eq("Widget was successfully created.")
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
context "with invalid params" do
|
|
275
|
+
it "sets an error flash message" do
|
|
276
|
+
post :create, params: { widget: { name: "" } }
|
|
277
|
+
expect(flash[:alert]).to be_present
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
# Testing session modifications
|
|
284
|
+
RSpec.describe SessionsController, type: :controller do
|
|
285
|
+
describe "POST #create" do
|
|
286
|
+
let(:user) { create(:user, password: "secret") }
|
|
287
|
+
|
|
288
|
+
it "sets user_id in session" do
|
|
289
|
+
post :create, params: { email: user.email, password: "secret" }
|
|
290
|
+
expect(session[:user_id]).to eq(user.id)
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
describe "DELETE #destroy" do
|
|
295
|
+
before { session[:user_id] = 1 }
|
|
296
|
+
|
|
297
|
+
it "clears user_id from session" do
|
|
298
|
+
delete :destroy
|
|
299
|
+
expect(session[:user_id]).to be_nil
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
end
|