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,245 @@
|
|
|
1
|
+
# RSpec Rails: Helper Specs Examples
|
|
2
|
+
# Source: rspec-rails gem features/helper_specs/
|
|
3
|
+
|
|
4
|
+
# Helper specs test view helper methods in isolation.
|
|
5
|
+
# Access the helper module via `helper`.
|
|
6
|
+
# Location: spec/helpers/
|
|
7
|
+
|
|
8
|
+
# Basic helper spec
|
|
9
|
+
RSpec.describe ApplicationHelper, type: :helper do
|
|
10
|
+
describe "#page_title" do
|
|
11
|
+
context "when @title is not set" do
|
|
12
|
+
it "returns the default title" do
|
|
13
|
+
expect(helper.page_title).to eq("RSpec is your friend")
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
context "when @title is set" do
|
|
18
|
+
before { assign(:title, "Custom Title") }
|
|
19
|
+
|
|
20
|
+
it "returns the custom title" do
|
|
21
|
+
expect(helper.page_title).to eq("Custom Title")
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Testing formatting helpers
|
|
28
|
+
RSpec.describe FormattingHelper, type: :helper do
|
|
29
|
+
describe "#format_date" do
|
|
30
|
+
it "formats date in user-friendly format" do
|
|
31
|
+
date = Date.new(2024, 1, 15)
|
|
32
|
+
expect(helper.format_date(date)).to eq("January 15, 2024")
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it "returns empty string for nil" do
|
|
36
|
+
expect(helper.format_date(nil)).to eq("")
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
describe "#format_currency" do
|
|
41
|
+
it "formats amount as currency" do
|
|
42
|
+
expect(helper.format_currency(1234.56)).to eq("$1,234.56")
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
it "handles zero" do
|
|
46
|
+
expect(helper.format_currency(0)).to eq("$0.00")
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
it "handles negative amounts" do
|
|
50
|
+
expect(helper.format_currency(-50)).to eq("-$50.00")
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Using URL helpers in helper specs
|
|
56
|
+
RSpec.describe WidgetsHelper, type: :helper do
|
|
57
|
+
describe "#link_to_widget" do
|
|
58
|
+
let(:widget) { create(:widget, name: "Super Widget") }
|
|
59
|
+
|
|
60
|
+
it "creates link to widget" do
|
|
61
|
+
result = helper.link_to_widget(widget)
|
|
62
|
+
|
|
63
|
+
expect(result).to include("Super Widget")
|
|
64
|
+
expect(result).to include(widget_path(widget))
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
describe "#widget_status_badge" do
|
|
69
|
+
let(:widget) { build(:widget) }
|
|
70
|
+
|
|
71
|
+
context "when widget is active" do
|
|
72
|
+
let(:widget) { build(:widget, :active) }
|
|
73
|
+
|
|
74
|
+
it "returns success badge" do
|
|
75
|
+
result = helper.widget_status_badge(widget)
|
|
76
|
+
expect(result).to include("badge-success")
|
|
77
|
+
expect(result).to include("Active")
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
context "when widget is inactive" do
|
|
82
|
+
let(:widget) { build(:widget, :inactive) }
|
|
83
|
+
|
|
84
|
+
it "returns warning badge" do
|
|
85
|
+
result = helper.widget_status_badge(widget)
|
|
86
|
+
expect(result).to include("badge-warning")
|
|
87
|
+
expect(result).to include("Inactive")
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Testing helpers that use Rails helpers
|
|
94
|
+
RSpec.describe TextHelper, type: :helper do
|
|
95
|
+
describe "#truncated_summary" do
|
|
96
|
+
it "truncates long text" do
|
|
97
|
+
text = "This is a very long text that should be truncated"
|
|
98
|
+
result = helper.truncated_summary(text, length: 20)
|
|
99
|
+
|
|
100
|
+
expect(result.length).to be <= 23 # 20 + "..."
|
|
101
|
+
expect(result).to end_with("...")
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
it "leaves short text unchanged" do
|
|
105
|
+
text = "Short text"
|
|
106
|
+
result = helper.truncated_summary(text, length: 50)
|
|
107
|
+
|
|
108
|
+
expect(result).to eq("Short text")
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
describe "#pluralized_count" do
|
|
113
|
+
it "pluralizes with count" do
|
|
114
|
+
expect(helper.pluralized_count(1, "item")).to eq("1 item")
|
|
115
|
+
expect(helper.pluralized_count(5, "item")).to eq("5 items")
|
|
116
|
+
expect(helper.pluralized_count(0, "item")).to eq("0 items")
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Testing helpers with HTML output
|
|
122
|
+
RSpec.describe IconHelper, type: :helper do
|
|
123
|
+
describe "#icon" do
|
|
124
|
+
it "generates icon element" do
|
|
125
|
+
result = helper.icon("check")
|
|
126
|
+
expect(result).to be_html_safe
|
|
127
|
+
expect(result).to include('<i class="icon icon-check">')
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
it "accepts additional classes" do
|
|
131
|
+
result = helper.icon("warning", class: "text-danger")
|
|
132
|
+
expect(result).to include("icon-warning")
|
|
133
|
+
expect(result).to include("text-danger")
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Testing helpers that use controller context
|
|
139
|
+
RSpec.describe NavigationHelper, type: :helper do
|
|
140
|
+
describe "#active_nav_class" do
|
|
141
|
+
before { allow(helper).to receive(:controller_name).and_return("widgets") }
|
|
142
|
+
|
|
143
|
+
context "when on matching controller" do
|
|
144
|
+
it "returns active class" do
|
|
145
|
+
expect(helper.active_nav_class("widgets")).to eq("active")
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
context "when on different controller" do
|
|
150
|
+
it "returns empty string" do
|
|
151
|
+
expect(helper.active_nav_class("users")).to eq("")
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Testing helpers that depend on current_user
|
|
158
|
+
RSpec.describe AuthorizationHelper, type: :helper do
|
|
159
|
+
let(:admin_user) { build(:user, :admin) }
|
|
160
|
+
let(:regular_user) { build(:user) }
|
|
161
|
+
|
|
162
|
+
describe "#can_edit?" do
|
|
163
|
+
let(:widget) { build(:widget, user: regular_user) }
|
|
164
|
+
|
|
165
|
+
context "when user is admin" do
|
|
166
|
+
before { allow(helper).to receive(:current_user).and_return(admin_user) }
|
|
167
|
+
|
|
168
|
+
it "returns true" do
|
|
169
|
+
expect(helper.can_edit?(widget)).to be(true)
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
context "when user is the owner" do
|
|
174
|
+
before { allow(helper).to receive(:current_user).and_return(regular_user) }
|
|
175
|
+
|
|
176
|
+
it "returns true" do
|
|
177
|
+
expect(helper.can_edit?(widget)).to be(true)
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
context "when user is neither admin nor owner" do
|
|
182
|
+
let(:other_user) { build(:user) }
|
|
183
|
+
|
|
184
|
+
before { allow(helper).to receive(:current_user).and_return(other_user) }
|
|
185
|
+
|
|
186
|
+
it "returns false" do
|
|
187
|
+
expect(helper.can_edit?(widget)).to be(false)
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Testing helpers that build complex structures
|
|
194
|
+
RSpec.describe BreadcrumbHelper, type: :helper do
|
|
195
|
+
describe "#breadcrumbs" do
|
|
196
|
+
it "builds breadcrumb trail" do
|
|
197
|
+
result = helper.breadcrumbs do |b|
|
|
198
|
+
b.add "Home", root_path
|
|
199
|
+
b.add "Widgets", widgets_path
|
|
200
|
+
b.add "Edit"
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
expect(result).to include(root_path)
|
|
204
|
+
expect(result).to include(widgets_path)
|
|
205
|
+
expect(result).to include("Edit")
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Testing helpers with flash messages
|
|
211
|
+
RSpec.describe FlashHelper, type: :helper do
|
|
212
|
+
describe "#flash_class" do
|
|
213
|
+
it "maps notice to success" do
|
|
214
|
+
expect(helper.flash_class(:notice)).to eq("alert-success")
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
it "maps alert to danger" do
|
|
218
|
+
expect(helper.flash_class(:alert)).to eq("alert-danger")
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
it "maps unknown to info" do
|
|
222
|
+
expect(helper.flash_class(:custom)).to eq("alert-info")
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# Testing time-dependent helpers
|
|
228
|
+
RSpec.describe TimeHelper, type: :helper do
|
|
229
|
+
describe "#time_ago_in_words_short" do
|
|
230
|
+
it "returns minutes ago" do
|
|
231
|
+
time = 5.minutes.ago
|
|
232
|
+
expect(helper.time_ago_in_words_short(time)).to eq("5m ago")
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
it "returns hours ago" do
|
|
236
|
+
time = 3.hours.ago
|
|
237
|
+
expect(helper.time_ago_in_words_short(time)).to eq("3h ago")
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
it "returns days ago" do
|
|
241
|
+
time = 2.days.ago
|
|
242
|
+
expect(helper.time_ago_in_words_short(time)).to eq("2d ago")
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
end
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
# RSpec Rails: Job Specs Examples
|
|
2
|
+
# Source: rspec-rails gem features/job_specs/
|
|
3
|
+
|
|
4
|
+
# Job specs test ActiveJob classes.
|
|
5
|
+
# Requires: ActiveJob::Base.queue_adapter = :test
|
|
6
|
+
# Location: spec/jobs/
|
|
7
|
+
|
|
8
|
+
# Basic job spec
|
|
9
|
+
RSpec.describe UploadBackupsJob, type: :job do
|
|
10
|
+
describe "#perform_later" do
|
|
11
|
+
it "enqueues the job" do
|
|
12
|
+
expect {
|
|
13
|
+
UploadBackupsJob.perform_later("backup.sql")
|
|
14
|
+
}.to have_enqueued_job
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
it "enqueues with correct arguments" do
|
|
18
|
+
expect {
|
|
19
|
+
UploadBackupsJob.perform_later("backup.sql")
|
|
20
|
+
}.to have_enqueued_job.with("backup.sql")
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Testing on specific queue
|
|
26
|
+
RSpec.describe EmailDeliveryJob, type: :job do
|
|
27
|
+
describe "#perform_later" do
|
|
28
|
+
it "enqueues on the mailers queue" do
|
|
29
|
+
expect {
|
|
30
|
+
EmailDeliveryJob.perform_later
|
|
31
|
+
}.to have_enqueued_job.on_queue("mailers")
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Testing scheduled jobs
|
|
37
|
+
RSpec.describe ScheduledReportJob, type: :job do
|
|
38
|
+
describe "#perform_later" do
|
|
39
|
+
it "enqueues at specific time" do
|
|
40
|
+
scheduled_time = 1.day.from_now.noon
|
|
41
|
+
|
|
42
|
+
expect {
|
|
43
|
+
ScheduledReportJob.set(wait_until: scheduled_time).perform_later
|
|
44
|
+
}.to have_enqueued_job.at(scheduled_time)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
it "enqueues with wait duration" do
|
|
48
|
+
expect {
|
|
49
|
+
ScheduledReportJob.set(wait: 1.hour).perform_later
|
|
50
|
+
}.to have_enqueued_job.at(a_value_within(1.second).of(1.hour.from_now))
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Testing job priority
|
|
56
|
+
RSpec.describe HighPriorityJob, type: :job do
|
|
57
|
+
describe "#perform_later" do
|
|
58
|
+
it "enqueues with priority" do
|
|
59
|
+
expect {
|
|
60
|
+
HighPriorityJob.set(priority: 1).perform_later
|
|
61
|
+
}.to have_enqueued_job.at_priority(1)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Imperative (post-hoc) syntax
|
|
67
|
+
RSpec.describe ProcessOrderJob, type: :job do
|
|
68
|
+
describe "#perform_later" do
|
|
69
|
+
let(:order) { create(:order) }
|
|
70
|
+
|
|
71
|
+
before { ProcessOrderJob.perform_later(order) }
|
|
72
|
+
|
|
73
|
+
it "has been enqueued" do
|
|
74
|
+
expect(ProcessOrderJob).to have_been_enqueued
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
it "was enqueued exactly once" do
|
|
78
|
+
expect(ProcessOrderJob).to have_been_enqueued.exactly(:once)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Testing job execution count
|
|
84
|
+
RSpec.describe BatchProcessingJob, type: :job do
|
|
85
|
+
describe "multiple enqueues" do
|
|
86
|
+
it "enqueues exactly 3 times" do
|
|
87
|
+
expect {
|
|
88
|
+
3.times { BatchProcessingJob.perform_later }
|
|
89
|
+
}.to have_enqueued_job.exactly(3).times
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
it "enqueues at least twice" do
|
|
93
|
+
expect {
|
|
94
|
+
5.times { BatchProcessingJob.perform_later }
|
|
95
|
+
}.to have_enqueued_job.at_least(:twice)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Testing job perform behavior
|
|
101
|
+
RSpec.describe SendNotificationJob, type: :job do
|
|
102
|
+
describe "#perform" do
|
|
103
|
+
let(:user) { create(:user) }
|
|
104
|
+
let(:message) { "Hello!" }
|
|
105
|
+
|
|
106
|
+
it "sends notification to user" do
|
|
107
|
+
notifier = instance_double(NotificationService)
|
|
108
|
+
allow(NotificationService).to receive(:new).and_return(notifier)
|
|
109
|
+
expect(notifier).to receive(:notify).with(user, message)
|
|
110
|
+
|
|
111
|
+
SendNotificationJob.perform_now(user, message)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Testing with perform_enqueued_jobs
|
|
117
|
+
RSpec.describe "Job execution", type: :job do
|
|
118
|
+
describe "running enqueued jobs" do
|
|
119
|
+
let(:user) { create(:user) }
|
|
120
|
+
|
|
121
|
+
it "performs jobs inline" do
|
|
122
|
+
perform_enqueued_jobs do
|
|
123
|
+
WelcomeEmailJob.perform_later(user)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
expect(ActionMailer::Base.deliveries.count).to eq(1)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
it "performs only specific jobs" do
|
|
130
|
+
perform_enqueued_jobs(only: WelcomeEmailJob) do
|
|
131
|
+
WelcomeEmailJob.perform_later(user)
|
|
132
|
+
CleanupJob.perform_later
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
expect(WelcomeEmailJob).not_to have_been_enqueued
|
|
136
|
+
expect(CleanupJob).to have_been_enqueued
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Testing job with retries
|
|
142
|
+
RSpec.describe RetryableJob, type: :job do
|
|
143
|
+
describe "retry behavior" do
|
|
144
|
+
let(:api_client) { instance_double(ExternalApi) }
|
|
145
|
+
|
|
146
|
+
before do
|
|
147
|
+
allow(ExternalApi).to receive(:new).and_return(api_client)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
context "when API fails temporarily" do
|
|
151
|
+
before do
|
|
152
|
+
call_count = 0
|
|
153
|
+
allow(api_client).to receive(:call) do
|
|
154
|
+
call_count += 1
|
|
155
|
+
raise StandardError if call_count < 3
|
|
156
|
+
:success
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
it "retries and eventually succeeds" do
|
|
161
|
+
expect {
|
|
162
|
+
perform_enqueued_jobs { RetryableJob.perform_later }
|
|
163
|
+
}.not_to raise_error
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Testing job that spawns other jobs
|
|
170
|
+
RSpec.describe ParentJob, type: :job do
|
|
171
|
+
describe "#perform" do
|
|
172
|
+
let(:items) { create_list(:item, 3) }
|
|
173
|
+
|
|
174
|
+
it "enqueues child job for each item" do
|
|
175
|
+
expect {
|
|
176
|
+
ParentJob.perform_now(items.map(&:id))
|
|
177
|
+
}.to have_enqueued_job(ChildJob).exactly(3).times
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Testing with specific job class
|
|
183
|
+
RSpec.describe "Multiple job types", type: :job do
|
|
184
|
+
describe "selective matching" do
|
|
185
|
+
it "matches specific job class" do
|
|
186
|
+
expect {
|
|
187
|
+
SendEmailJob.perform_later
|
|
188
|
+
ProcessPaymentJob.perform_later
|
|
189
|
+
}.to have_enqueued_job(SendEmailJob)
|
|
190
|
+
.and have_enqueued_job(ProcessPaymentJob)
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Testing job with complex arguments using block
|
|
196
|
+
RSpec.describe DataExportJob, type: :job do
|
|
197
|
+
describe "#perform_later" do
|
|
198
|
+
it "enqueues with expected structure" do
|
|
199
|
+
expect {
|
|
200
|
+
DataExportJob.perform_later(
|
|
201
|
+
user_id: 1,
|
|
202
|
+
format: :csv,
|
|
203
|
+
filters: { status: :active }
|
|
204
|
+
)
|
|
205
|
+
}.to have_enqueued_job.with { |args|
|
|
206
|
+
expect(args[:user_id]).to eq(1)
|
|
207
|
+
expect(args[:format]).to eq(:csv)
|
|
208
|
+
expect(args[:filters]).to include(status: :active)
|
|
209
|
+
}
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Testing job queue adapter configuration
|
|
215
|
+
RSpec.describe "Queue adapter", type: :job do
|
|
216
|
+
it "uses test adapter" do
|
|
217
|
+
expect(ActiveJob::Base.queue_adapter).to be_a(ActiveJob::QueueAdapters::TestAdapter)
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Clearing enqueued jobs between examples
|
|
222
|
+
RSpec.describe "Job queue state", type: :job do
|
|
223
|
+
before { clear_enqueued_jobs }
|
|
224
|
+
|
|
225
|
+
it "starts with empty queue" do
|
|
226
|
+
expect(ActiveJob::Base.queue_adapter.enqueued_jobs).to be_empty
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# Testing that no jobs were enqueued
|
|
231
|
+
RSpec.describe ConditionalJob, type: :job do
|
|
232
|
+
describe "#perform" do
|
|
233
|
+
context "when condition is not met" do
|
|
234
|
+
let(:order) { create(:order, :complete) }
|
|
235
|
+
|
|
236
|
+
it "does not enqueue follow-up job" do
|
|
237
|
+
expect {
|
|
238
|
+
ConditionalJob.perform_now(order)
|
|
239
|
+
}.not_to have_enqueued_job(FollowUpJob)
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# Testing job with GlobalID arguments
|
|
246
|
+
RSpec.describe ProcessRecordJob, type: :job do
|
|
247
|
+
describe "#perform" do
|
|
248
|
+
let(:record) { create(:widget) }
|
|
249
|
+
|
|
250
|
+
it "accepts ActiveRecord objects" do
|
|
251
|
+
expect {
|
|
252
|
+
ProcessRecordJob.perform_later(record)
|
|
253
|
+
}.to have_enqueued_job.with(record)
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
end
|