source_monitor 0.2.0 → 0.3.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.
Files changed (196) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/agents/rails-concern.md +464 -0
  3. data/.claude/agents/rails-controller.md +424 -0
  4. data/.claude/agents/rails-hotwire.md +446 -0
  5. data/.claude/agents/rails-implement.md +374 -0
  6. data/.claude/agents/rails-job.md +334 -0
  7. data/.claude/agents/rails-lint.md +294 -0
  8. data/.claude/agents/rails-mailer.md +371 -0
  9. data/.claude/agents/rails-migration.md +449 -0
  10. data/.claude/agents/rails-model.md +420 -0
  11. data/.claude/agents/rails-policy.md +443 -0
  12. data/.claude/agents/rails-presenter.md +427 -0
  13. data/.claude/agents/rails-query.md +412 -0
  14. data/.claude/agents/rails-review.md +490 -0
  15. data/.claude/agents/rails-service.md +458 -0
  16. data/.claude/agents/rails-state-records.md +465 -0
  17. data/.claude/agents/rails-tdd.md +314 -0
  18. data/.claude/agents/rails-test.md +441 -0
  19. data/.claude/agents/rails-view-component.md +418 -0
  20. data/.claude/hooks/block-secrets.sh +52 -0
  21. data/.claude/settings.json +85 -0
  22. data/.claude/skills/action-cable-patterns/SKILL.md +296 -0
  23. data/.claude/skills/action-mailer-patterns/SKILL.md +295 -0
  24. data/.claude/skills/active-storage-setup/SKILL.md +311 -0
  25. data/.claude/skills/api-versioning/SKILL.md +294 -0
  26. data/.claude/skills/authentication-flow/SKILL.md +335 -0
  27. data/.claude/skills/authentication-flow/reference/current.md +248 -0
  28. data/.claude/skills/authentication-flow/reference/passwordless.md +253 -0
  29. data/.claude/skills/authentication-flow/reference/sessions.md +201 -0
  30. data/.claude/skills/authorization-pundit/SKILL.md +462 -0
  31. data/.claude/skills/caching-strategies/SKILL.md +350 -0
  32. data/.claude/skills/database-migrations/SKILL.md +354 -0
  33. data/.claude/skills/form-object-patterns/SKILL.md +399 -0
  34. data/.claude/skills/hotwire-patterns/SKILL.md +247 -0
  35. data/.claude/skills/hotwire-patterns/reference/stimulus.md +307 -0
  36. data/.claude/skills/hotwire-patterns/reference/tailwind-integration.md +112 -0
  37. data/.claude/skills/hotwire-patterns/reference/turbo-frames.md +158 -0
  38. data/.claude/skills/hotwire-patterns/reference/turbo-streams.md +218 -0
  39. data/.claude/skills/i18n-patterns/SKILL.md +320 -0
  40. data/.claude/skills/install/SKILL.md +367 -0
  41. data/.claude/skills/performance-optimization/SKILL.md +311 -0
  42. data/.claude/skills/rails-architecture/SKILL.md +259 -0
  43. data/.claude/skills/rails-architecture/reference/error-handling.md +333 -0
  44. data/.claude/skills/rails-architecture/reference/event-tracking.md +142 -0
  45. data/.claude/skills/rails-architecture/reference/layer-interactions.md +417 -0
  46. data/.claude/skills/rails-architecture/reference/multi-tenancy.md +152 -0
  47. data/.claude/skills/rails-architecture/reference/query-patterns.md +342 -0
  48. data/.claude/skills/rails-architecture/reference/service-patterns.md +286 -0
  49. data/.claude/skills/rails-architecture/reference/state-records.md +250 -0
  50. data/.claude/skills/rails-architecture/reference/testing-strategy.md +326 -0
  51. data/.claude/skills/rails-concern/SKILL.md +399 -0
  52. data/.claude/skills/rails-controller/SKILL.md +336 -0
  53. data/.claude/skills/rails-model-generator/SKILL.md +321 -0
  54. data/.claude/skills/rails-model-generator/reference/validations.md +298 -0
  55. data/.claude/skills/rails-presenter/SKILL.md +274 -0
  56. data/.claude/skills/rails-query-object/SKILL.md +289 -0
  57. data/.claude/skills/rails-service-object/SKILL.md +349 -0
  58. data/.claude/skills/solid-queue-setup/SKILL.md +307 -0
  59. data/.claude/skills/tdd-cycle/SKILL.md +359 -0
  60. data/.claude/skills/viewcomponent-patterns/SKILL.md +333 -0
  61. data/.gitignore +1 -0
  62. data/.rubocop.yml +2 -0
  63. data/.ruby-version +1 -1
  64. data/.vbw-planning/.notification-log.jsonl +192 -0
  65. data/.vbw-planning/.session-log.jsonl +871 -0
  66. data/.vbw-planning/PROJECT.md +51 -0
  67. data/.vbw-planning/REQUIREMENTS.md +50 -0
  68. data/.vbw-planning/SHIPPED.md +28 -0
  69. data/.vbw-planning/codebase/ARCHITECTURE.md +147 -0
  70. data/.vbw-planning/codebase/CONCERNS.md +99 -0
  71. data/.vbw-planning/codebase/CONVENTIONS.md +97 -0
  72. data/.vbw-planning/codebase/DEPENDENCIES.md +100 -0
  73. data/.vbw-planning/codebase/INDEX.md +86 -0
  74. data/.vbw-planning/codebase/META.md +42 -0
  75. data/.vbw-planning/codebase/PATTERNS.md +262 -0
  76. data/.vbw-planning/codebase/STACK.md +101 -0
  77. data/.vbw-planning/codebase/STRUCTURE.md +324 -0
  78. data/.vbw-planning/codebase/TESTING.md +154 -0
  79. data/.vbw-planning/config.json +12 -0
  80. data/.vbw-planning/discovery.json +24 -0
  81. data/.vbw-planning/milestones/default/ROADMAP.md +115 -0
  82. data/.vbw-planning/milestones/default/STATE.md +83 -0
  83. data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-01-SUMMARY.md +56 -0
  84. data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-01.md +187 -0
  85. data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-02-SUMMARY.md +64 -0
  86. data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-02.md +137 -0
  87. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-01-SUMMARY.md +67 -0
  88. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-01.md +142 -0
  89. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-02-SUMMARY.md +64 -0
  90. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-02.md +138 -0
  91. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-03-SUMMARY.md +85 -0
  92. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-03.md +147 -0
  93. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-04-SUMMARY.md +63 -0
  94. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-04.md +129 -0
  95. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-05-SUMMARY.md +74 -0
  96. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-05.md +154 -0
  97. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/03-VERIFICATION-wave1.md +303 -0
  98. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/03-VERIFICATION.md +510 -0
  99. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-01-SUMMARY.md +61 -0
  100. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-01.md +161 -0
  101. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-02-SUMMARY.md +66 -0
  102. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-02.md +132 -0
  103. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-03-SUMMARY.md +59 -0
  104. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-03.md +171 -0
  105. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-04-SUMMARY.md +56 -0
  106. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-04.md +152 -0
  107. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/04-CONTEXT.md +33 -0
  108. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-01-SUMMARY.md +42 -0
  109. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-01.md +119 -0
  110. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-02-SUMMARY.md +52 -0
  111. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-02.md +195 -0
  112. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-03-SUMMARY.md +79 -0
  113. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-03.md +130 -0
  114. data/CHANGELOG.md +28 -0
  115. data/CLAUDE.md +179 -0
  116. data/Gemfile +8 -0
  117. data/Gemfile.lock +114 -101
  118. data/Rakefile +2 -0
  119. data/app/assets/builds/source_monitor/application.css +2076 -0
  120. data/app/assets/builds/source_monitor/application.js +2758 -0
  121. data/app/assets/builds/source_monitor/application.js.map +7 -0
  122. data/app/controllers/source_monitor/application_controller.rb +2 -0
  123. data/app/controllers/source_monitor/health_controller.rb +2 -0
  124. data/app/controllers/source_monitor/import_sessions/bulk_configuration.rb +106 -0
  125. data/app/controllers/source_monitor/import_sessions/entry_annotation.rb +187 -0
  126. data/app/controllers/source_monitor/import_sessions/health_check_management.rb +112 -0
  127. data/app/controllers/source_monitor/import_sessions/opml_parser.rb +130 -0
  128. data/app/controllers/source_monitor/import_sessions_controller.rb +6 -507
  129. data/app/controllers/source_monitor/items_controller.rb +2 -0
  130. data/app/controllers/source_monitor/sources_controller.rb +0 -14
  131. data/app/helpers/source_monitor/application_helper.rb +4 -112
  132. data/app/helpers/source_monitor/health_badge_helper.rb +69 -0
  133. data/app/helpers/source_monitor/table_sort_helper.rb +53 -0
  134. data/app/jobs/source_monitor/application_job.rb +2 -0
  135. data/app/models/source_monitor/application_record.rb +2 -0
  136. data/app/models/source_monitor/log_entry.rb +0 -2
  137. data/config/coverage_baseline.json +217 -1862
  138. data/config/routes.rb +2 -0
  139. data/db/migrate/20251009103000_add_feed_content_readability_to_sources.rb +2 -0
  140. data/db/migrate/20251014171659_add_performance_indexes.rb +2 -0
  141. data/db/migrate/20251014172525_add_fetch_status_check_constraint.rb +2 -0
  142. data/db/migrate/20251108120116_refresh_fetch_status_constraint.rb +2 -0
  143. data/db/migrate/20260210204022_add_composite_index_to_log_entries.rb +17 -0
  144. data/lib/source_monitor/assets/bundler.rb +2 -0
  145. data/lib/source_monitor/assets.rb +2 -0
  146. data/lib/source_monitor/configuration/authentication_settings.rb +62 -0
  147. data/lib/source_monitor/configuration/events.rb +60 -0
  148. data/lib/source_monitor/configuration/fetching_settings.rb +27 -0
  149. data/lib/source_monitor/configuration/health_settings.rb +27 -0
  150. data/lib/source_monitor/configuration/http_settings.rb +43 -0
  151. data/lib/source_monitor/configuration/model_definition.rb +108 -0
  152. data/lib/source_monitor/configuration/models.rb +36 -0
  153. data/lib/source_monitor/configuration/realtime_settings.rb +95 -0
  154. data/lib/source_monitor/configuration/retention_settings.rb +45 -0
  155. data/lib/source_monitor/configuration/scraper_registry.rb +67 -0
  156. data/lib/source_monitor/configuration/scraping_settings.rb +39 -0
  157. data/lib/source_monitor/configuration/validation_definition.rb +32 -0
  158. data/lib/source_monitor/configuration.rb +12 -579
  159. data/lib/source_monitor/dashboard/queries/recent_activity_query.rb +138 -0
  160. data/lib/source_monitor/dashboard/queries/stats_query.rb +71 -0
  161. data/lib/source_monitor/dashboard/queries.rb +2 -195
  162. data/lib/source_monitor/engine.rb +2 -0
  163. data/lib/source_monitor/fetching/feed_fetcher/adaptive_interval.rb +141 -0
  164. data/lib/source_monitor/fetching/feed_fetcher/entry_processor.rb +89 -0
  165. data/lib/source_monitor/fetching/feed_fetcher/source_updater.rb +200 -0
  166. data/lib/source_monitor/fetching/feed_fetcher.rb +37 -379
  167. data/lib/source_monitor/items/item_creator/content_extractor.rb +113 -0
  168. data/lib/source_monitor/items/item_creator/entry_parser/media_extraction.rb +96 -0
  169. data/lib/source_monitor/items/item_creator/entry_parser.rb +294 -0
  170. data/lib/source_monitor/items/item_creator.rb +28 -455
  171. data/lib/source_monitor/setup/bundle_installer.rb +2 -0
  172. data/lib/source_monitor/setup/cli.rb +2 -0
  173. data/lib/source_monitor/setup/dependency_checker.rb +2 -0
  174. data/lib/source_monitor/setup/detectors.rb +2 -0
  175. data/lib/source_monitor/setup/gemfile_editor.rb +2 -0
  176. data/lib/source_monitor/setup/initializer_patcher.rb +2 -0
  177. data/lib/source_monitor/setup/install_generator.rb +2 -0
  178. data/lib/source_monitor/setup/migration_installer.rb +2 -0
  179. data/lib/source_monitor/setup/node_installer.rb +2 -0
  180. data/lib/source_monitor/setup/prompter.rb +2 -0
  181. data/lib/source_monitor/setup/requirements.rb +2 -0
  182. data/lib/source_monitor/setup/shell_runner.rb +2 -0
  183. data/lib/source_monitor/setup/verification/action_cable_verifier.rb +2 -0
  184. data/lib/source_monitor/setup/verification/printer.rb +2 -0
  185. data/lib/source_monitor/setup/verification/result.rb +2 -0
  186. data/lib/source_monitor/setup/verification/runner.rb +2 -0
  187. data/lib/source_monitor/setup/verification/solid_queue_verifier.rb +2 -0
  188. data/lib/source_monitor/setup/verification/telemetry_logger.rb +2 -0
  189. data/lib/source_monitor/setup/workflow.rb +2 -0
  190. data/lib/source_monitor/version.rb +3 -1
  191. data/lib/source_monitor.rb +140 -58
  192. data/lib/tasks/source_monitor_assets.rake +2 -0
  193. data/lib/tasks/source_monitor_setup.rake +2 -0
  194. data/lib/tasks/source_monitor_tasks.rake +2 -0
  195. data/source_monitor.gemspec +3 -1
  196. metadata +144 -4
@@ -0,0 +1,250 @@
1
+ # State-as-Records Patterns
2
+
3
+ ## Philosophy
4
+
5
+ Instead of boolean columns (`closed: true`), create separate state record models that capture who, when, and why.
6
+
7
+ ## When to Use State Records vs Booleans
8
+
9
+ ### Use State Records When:
10
+ - You need to track WHO changed the state
11
+ - You need to track WHEN the state changed
12
+ - You need to track WHY (reason, notes)
13
+ - State changes are business-significant events
14
+ - You need an audit trail
15
+
16
+ ### Booleans Are OK When:
17
+ - It's a technical flag (`email_verified`, `terms_accepted`)
18
+ - No audit trail needed
19
+ - Simple on/off with no metadata
20
+ - Performance-critical hot paths
21
+
22
+ ## Pattern 1: Simple Toggle (Closure)
23
+
24
+ ```ruby
25
+ # Migration
26
+ class CreateClosures < ActiveRecord::Migration[8.0]
27
+ def change
28
+ create_table :closures do |t|
29
+ t.references :card, null: false, foreign_key: true
30
+ t.references :user, foreign_key: true
31
+ t.timestamps
32
+ end
33
+ add_index :closures, :card_id, unique: true
34
+ end
35
+ end
36
+
37
+ # app/models/closure.rb
38
+ class Closure < ApplicationRecord
39
+ belongs_to :card, touch: true
40
+ belongs_to :user, optional: true
41
+ validates :card, uniqueness: true
42
+ end
43
+
44
+ # app/models/concerns/closeable.rb
45
+ module Closeable
46
+ extend ActiveSupport::Concern
47
+
48
+ included do
49
+ has_one :closure, dependent: :destroy
50
+
51
+ scope :open, -> { where.missing(:closure) }
52
+ scope :closed, -> { joins(:closure) }
53
+ end
54
+
55
+ def close(user: Current.user)
56
+ create_closure!(user: user)
57
+ end
58
+
59
+ def reopen
60
+ closure&.destroy!
61
+ end
62
+
63
+ def closed?
64
+ closure.present?
65
+ end
66
+
67
+ def open?
68
+ !closed?
69
+ end
70
+
71
+ def closed_at
72
+ closure&.created_at
73
+ end
74
+
75
+ def closed_by
76
+ closure&.user
77
+ end
78
+ end
79
+
80
+ # app/models/card.rb
81
+ class Card < ApplicationRecord
82
+ include Closeable
83
+ end
84
+ ```
85
+
86
+ ## Pattern 2: State with Reason (Approval)
87
+
88
+ ```ruby
89
+ class CreateApprovals < ActiveRecord::Migration[8.0]
90
+ def change
91
+ create_table :approvals do |t|
92
+ t.references :approvable, polymorphic: true, null: false
93
+ t.references :user, null: false, foreign_key: true
94
+ t.text :notes
95
+ t.timestamps
96
+ end
97
+ add_index :approvals, [:approvable_type, :approvable_id], unique: true
98
+ end
99
+ end
100
+
101
+ class Approval < ApplicationRecord
102
+ belongs_to :approvable, polymorphic: true, touch: true
103
+ belongs_to :user
104
+ validates :approvable, uniqueness: { scope: :approvable_type }
105
+ end
106
+
107
+ module Approvable
108
+ extend ActiveSupport::Concern
109
+
110
+ included do
111
+ has_one :approval, as: :approvable, dependent: :destroy
112
+
113
+ scope :approved, -> { joins(:approval) }
114
+ scope :pending_approval, -> { where.missing(:approval) }
115
+ end
116
+
117
+ def approve!(user:, notes: nil)
118
+ create_approval!(user: user, notes: notes)
119
+ end
120
+
121
+ def unapprove!
122
+ approval&.destroy!
123
+ end
124
+
125
+ def approved?
126
+ approval.present?
127
+ end
128
+
129
+ def approved_by
130
+ approval&.user
131
+ end
132
+
133
+ def approved_at
134
+ approval&.created_at
135
+ end
136
+ end
137
+ ```
138
+
139
+ ## Pattern 3: State with History (Publication)
140
+
141
+ When you need to track multiple state transitions over time:
142
+
143
+ ```ruby
144
+ class CreatePublications < ActiveRecord::Migration[8.0]
145
+ def change
146
+ create_table :publications do |t|
147
+ t.references :post, null: false, foreign_key: true
148
+ t.references :user, null: false, foreign_key: true
149
+ t.string :key, null: false
150
+ t.text :description
151
+ t.timestamps
152
+ end
153
+ add_index :publications, :post_id, unique: true
154
+ add_index :publications, :key, unique: true
155
+ end
156
+ end
157
+
158
+ class Publication < ApplicationRecord
159
+ belongs_to :post, touch: true
160
+ belongs_to :user
161
+
162
+ before_validation :generate_key, on: :create
163
+
164
+ private
165
+
166
+ def generate_key
167
+ self.key ||= SecureRandom.alphanumeric(12)
168
+ end
169
+ end
170
+ ```
171
+
172
+ ## CRUD Routing for State Records
173
+
174
+ ```ruby
175
+ # config/routes.rb
176
+ resources :cards do
177
+ resource :closure, only: [:create, :destroy]
178
+ end
179
+
180
+ resources :posts do
181
+ resource :publication, only: [:create, :destroy]
182
+ end
183
+
184
+ resources :documents do
185
+ resource :approval, only: [:create, :destroy]
186
+ end
187
+ ```
188
+
189
+ ```ruby
190
+ # app/controllers/closures_controller.rb
191
+ class ClosuresController < ApplicationController
192
+ before_action :set_card
193
+
194
+ def create
195
+ authorize @card, :close?
196
+ @card.close(user: Current.user)
197
+ redirect_to @card, notice: "Closed."
198
+ end
199
+
200
+ def destroy
201
+ authorize @card, :reopen?
202
+ @card.reopen
203
+ redirect_to @card, notice: "Reopened."
204
+ end
205
+
206
+ private
207
+
208
+ def set_card
209
+ @card = Card.find(params[:card_id])
210
+ end
211
+ end
212
+ ```
213
+
214
+ ## Testing State Records
215
+
216
+ ```ruby
217
+ # test/models/concerns/closeable_test.rb
218
+ require "test_helper"
219
+
220
+ class CloseableTest < ActiveSupport::TestCase
221
+ setup do
222
+ @card = cards(:open_card)
223
+ @user = users(:one)
224
+ end
225
+
226
+ test "#close creates a closure" do
227
+ assert_difference "Closure.count", 1 do
228
+ @card.close(user: @user)
229
+ end
230
+ assert @card.closed?
231
+ assert_equal @user, @card.closed_by
232
+ end
233
+
234
+ test "#reopen destroys the closure" do
235
+ @card.close(user: @user)
236
+ @card.reopen
237
+ assert @card.open?
238
+ end
239
+
240
+ test ".open scope excludes closed cards" do
241
+ @card.close(user: @user)
242
+ assert_not_includes Card.open, @card
243
+ end
244
+
245
+ test ".closed scope includes closed cards" do
246
+ @card.close(user: @user)
247
+ assert_includes Card.closed, @card
248
+ end
249
+ end
250
+ ```
@@ -0,0 +1,326 @@
1
+ # Testing Strategy by Layer
2
+
3
+ ## Test Pyramid
4
+
5
+ ```
6
+ /\
7
+ / \ System Tests (few)
8
+ /----\
9
+ / \ Controller/Integration Tests (moderate)
10
+ /--------\
11
+ / \ Unit Tests (many)
12
+ --------------
13
+ Models, Services, Queries, Presenters, Components
14
+ ```
15
+
16
+ ## Unit Tests
17
+
18
+ ### Model Tests
19
+
20
+ ```ruby
21
+ # test/models/event_test.rb
22
+ require "test_helper"
23
+
24
+ class EventTest < ActiveSupport::TestCase
25
+ test "requires name" do
26
+ event = Event.new(name: nil)
27
+ assert_not event.valid?
28
+ assert_includes event.errors[:name], "can't be blank"
29
+ end
30
+
31
+ test "requires event_date" do
32
+ event = Event.new(event_date: nil)
33
+ assert_not event.valid?
34
+ assert_includes event.errors[:event_date], "can't be blank"
35
+ end
36
+
37
+ test ".upcoming returns only future events" do
38
+ past_event = events(:past)
39
+ future_event = events(:upcoming)
40
+
41
+ results = Event.upcoming
42
+ assert_includes results, future_event
43
+ assert_not_includes results, past_event
44
+ end
45
+
46
+ test "#days_until returns days until event" do
47
+ event = Event.new(event_date: 5.days.from_now.to_date)
48
+ assert_equal 5, event.days_until
49
+ end
50
+ end
51
+ ```
52
+
53
+ ### Service Tests
54
+
55
+ ```ruby
56
+ # test/services/orders/create_service_test.rb
57
+ require "test_helper"
58
+
59
+ class Orders::CreateServiceTest < ActiveSupport::TestCase
60
+ setup do
61
+ @user = users(:one)
62
+ @product = products(:widget)
63
+ @service = Orders::CreateService.new
64
+ end
65
+
66
+ test "returns success with valid params" do
67
+ result = @service.call(user: @user, items: [{ product_id: @product.id, quantity: 2 }])
68
+ assert result.success?
69
+ assert_kind_of Order, result.data
70
+ end
71
+
72
+ test "creates an order" do
73
+ assert_difference "Order.count", 1 do
74
+ @service.call(user: @user, items: [{ product_id: @product.id, quantity: 2 }])
75
+ end
76
+ end
77
+
78
+ test "returns failure with empty items" do
79
+ result = @service.call(user: @user, items: [])
80
+ assert result.failure?
81
+ assert_equal :empty_cart, result.code
82
+ end
83
+
84
+ test "does not create order on failure" do
85
+ assert_no_difference "Order.count" do
86
+ @service.call(user: @user, items: [])
87
+ end
88
+ end
89
+ end
90
+ ```
91
+
92
+ ### Query Tests
93
+
94
+ ```ruby
95
+ # test/queries/active_events_query_test.rb
96
+ require "test_helper"
97
+
98
+ class ActiveEventsQueryTest < ActiveSupport::TestCase
99
+ setup do
100
+ @account = accounts(:one)
101
+ @other_account = accounts(:two)
102
+ @query = ActiveEventsQuery.new(account: @account)
103
+ end
104
+
105
+ test "returns active events for account" do
106
+ active = events(:active)
107
+ result = @query.call
108
+ assert_includes result, active
109
+ end
110
+
111
+ test "excludes inactive events" do
112
+ cancelled = events(:cancelled)
113
+ result = @query.call
114
+ assert_not_includes result, cancelled
115
+ end
116
+
117
+ test "excludes other account events (tenant isolation)" do
118
+ other_event = events(:other_account_event)
119
+ result = @query.call
120
+ assert_not_includes result, other_event
121
+ end
122
+ end
123
+ ```
124
+
125
+ ### Presenter Tests
126
+
127
+ ```ruby
128
+ # test/presenters/event_presenter_test.rb
129
+ require "test_helper"
130
+
131
+ class EventPresenterTest < ActiveSupport::TestCase
132
+ include ActionView::Helpers::TagHelper
133
+
134
+ test "delegates to model" do
135
+ event = events(:confirmed)
136
+ presenter = EventPresenter.new(event)
137
+ assert_equal event.name, presenter.name
138
+ end
139
+
140
+ test "#status_badge returns HTML-safe string" do
141
+ presenter = EventPresenter.new(events(:confirmed))
142
+ assert_predicate presenter.status_badge, :html_safe?
143
+ end
144
+
145
+ test "#status_badge includes status text" do
146
+ presenter = EventPresenter.new(events(:confirmed))
147
+ assert_match "Confirmed", presenter.status_badge
148
+ end
149
+
150
+ test "#formatted_date with date present" do
151
+ event = events(:confirmed)
152
+ presenter = EventPresenter.new(event)
153
+ assert_match event.event_date.year.to_s, presenter.formatted_date
154
+ end
155
+
156
+ test "#formatted_date with nil date" do
157
+ event = events(:no_date)
158
+ presenter = EventPresenter.new(event)
159
+ assert_match "TBD", presenter.formatted_date
160
+ end
161
+ end
162
+ ```
163
+
164
+ ## Integration Tests
165
+
166
+ ### Controller Tests
167
+
168
+ ```ruby
169
+ # test/controllers/events_controller_test.rb
170
+ require "test_helper"
171
+
172
+ class EventsControllerTest < ActionDispatch::IntegrationTest
173
+ setup do
174
+ @user = users(:one)
175
+ @event = events(:one)
176
+ sign_in_as @user
177
+ end
178
+
179
+ test "should get index" do
180
+ get events_url
181
+ assert_response :success
182
+ end
183
+
184
+ test "shows only own account events" do
185
+ get events_url
186
+ assert_response :success
187
+ other_event = events(:other_account_event)
188
+ assert_no_match other_event.name, response.body
189
+ end
190
+
191
+ test "should create event" do
192
+ assert_difference("Event.count") do
193
+ post events_url, params: { event: { name: "New Event", event_date: 1.week.from_now } }
194
+ end
195
+ assert_redirected_to event_url(Event.last)
196
+ end
197
+
198
+ test "renders form with errors for invalid params" do
199
+ post events_url, params: { event: { name: "" } }
200
+ assert_response :unprocessable_entity
201
+ end
202
+ end
203
+ ```
204
+
205
+ ### Policy Tests
206
+
207
+ ```ruby
208
+ # test/policies/event_policy_test.rb
209
+ require "test_helper"
210
+
211
+ class EventPolicyTest < ActiveSupport::TestCase
212
+ test "owner can show" do
213
+ user = users(:one)
214
+ event = events(:one) # belongs to user's account
215
+ assert EventPolicy.new(user, event).show?
216
+ end
217
+
218
+ test "non-owner cannot show" do
219
+ user = users(:two) # different account
220
+ event = events(:one)
221
+ assert_not EventPolicy.new(user, event).show?
222
+ end
223
+
224
+ test "scope returns only own events" do
225
+ user = users(:one)
226
+ scope = EventPolicy::Scope.new(user, Event).resolve
227
+ assert_includes scope, events(:one)
228
+ assert_not_includes scope, events(:other_account_event)
229
+ end
230
+ end
231
+ ```
232
+
233
+ ## System Tests
234
+
235
+ ```ruby
236
+ # test/system/create_event_test.rb
237
+ require "application_system_test_case"
238
+
239
+ class CreateEventTest < ApplicationSystemTestCase
240
+ setup do
241
+ sign_in_as users(:one)
242
+ end
243
+
244
+ test "creates event successfully" do
245
+ visit new_event_url
246
+
247
+ fill_in "Name", with: "Company Party"
248
+ fill_in "Event date", with: 1.month.from_now.to_date
249
+ click_button "Create Event"
250
+
251
+ assert_text "Event was successfully created"
252
+ assert_text "Company Party"
253
+ end
254
+
255
+ test "shows validation errors" do
256
+ visit new_event_url
257
+ click_button "Create Event"
258
+ assert_text "can't be blank"
259
+ end
260
+ end
261
+ ```
262
+
263
+ ## Component Tests
264
+
265
+ ```ruby
266
+ # test/components/event_card_component_test.rb
267
+ require "test_helper"
268
+
269
+ class EventCardComponentTest < ViewComponent::TestCase
270
+ test "renders event name" do
271
+ event = events(:one)
272
+ render_inline(EventCardComponent.new(event: event))
273
+ assert_text event.name
274
+ end
275
+
276
+ test "renders status badge" do
277
+ render_inline(EventCardComponent.new(event: events(:confirmed)))
278
+ assert_selector ".badge"
279
+ end
280
+
281
+ test "shows days until for upcoming events" do
282
+ event = events(:upcoming)
283
+ render_inline(EventCardComponent.new(event: event))
284
+ assert_selector "[data-days-until]"
285
+ end
286
+ end
287
+ ```
288
+
289
+ ## Test Helpers
290
+
291
+ ```ruby
292
+ # test/test_helper.rb
293
+ class ActiveSupport::TestCase
294
+ fixtures :all
295
+
296
+ def sign_in_as(user)
297
+ post session_url, params: { email: user.email_address, password: "password" }
298
+ end
299
+
300
+ def sign_out
301
+ delete session_url
302
+ end
303
+ end
304
+ ```
305
+
306
+ ## Coverage Requirements
307
+
308
+ | Layer | Minimum Coverage |
309
+ |-------|-----------------|
310
+ | Models | 90% |
311
+ | Services | 95% |
312
+ | Queries | 90% |
313
+ | Controllers | 80% |
314
+ | Overall | 85% |
315
+
316
+ ## Checklist
317
+
318
+ - [ ] Unit tests for all models (validations, scopes, methods)
319
+ - [ ] Service tests cover success/failure paths
320
+ - [ ] Query tests verify correctness and tenant isolation
321
+ - [ ] Controller tests for all endpoints
322
+ - [ ] Policy tests for authorization rules
323
+ - [ ] System tests for critical user flows
324
+ - [ ] Component tests for ViewComponents
325
+ - [ ] Fixtures with meaningful names
326
+ - [ ] Test helper with authentication methods