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,142 @@
1
+ ---
2
+ phase: 2
3
+ plan: 1
4
+ title: feed-fetcher-tests
5
+ wave: 1
6
+ depends_on: []
7
+ skills_used: []
8
+ must_haves:
9
+ truths:
10
+ - "Running `bin/rails test test/lib/source_monitor/fetching/feed_fetcher_test.rb` exits 0 with zero failures"
11
+ - "Coverage report shows lib/source_monitor/fetching/feed_fetcher.rb has fewer than 50 uncovered lines (down from 245)"
12
+ - "All new tests use WebMock stubs or VCR cassettes -- no real HTTP requests"
13
+ - "Running `bin/rails test` exits 0 with no regressions"
14
+ artifacts:
15
+ - "test/lib/source_monitor/fetching/feed_fetcher_test.rb -- extended with new test methods covering retry strategy, error handling, header management, entry processing, and helper methods"
16
+ key_links:
17
+ - "REQ-01 substantially satisfied -- FeedFetcher branch coverage above 80%"
18
+ ---
19
+
20
+ # Plan 01: feed-fetcher-tests
21
+
22
+ ## Objective
23
+
24
+ Close the coverage gap in `lib/source_monitor/fetching/feed_fetcher.rb` (currently 245 uncovered lines out of 627). The existing test file covers basic RSS/Atom/JSON fetching, 304 handling, timeout/HTTP/parsing failures, and adaptive interval mechanics. This plan targets the remaining uncovered branches: retry strategy application, circuit breaker state transitions, connection error wrapping, last_modified header handling, entry processing edge cases, jitter computation, and private helper methods.
25
+
26
+ ## Context
27
+
28
+ <context>
29
+ @lib/source_monitor/fetching/feed_fetcher.rb -- 627 lines, the core fetch pipeline
30
+ @lib/source_monitor/fetching/retry_policy.rb -- RetryPolicy with Decision struct
31
+ @lib/source_monitor/fetching/fetch_error.rb -- error hierarchy (TimeoutError, ConnectionError, HTTPError, ParsingError, UnexpectedResponseError)
32
+ @test/lib/source_monitor/fetching/feed_fetcher_test.rb -- existing test file with 12 tests covering success, 304, timeout, HTTP 404, parsing failure, adaptive intervals
33
+ @config/coverage_baseline.json -- lists 245 uncovered lines for feed_fetcher.rb
34
+ @test/test_helper.rb -- test infrastructure (WebMock, VCR, create_source!, with_queue_adapter)
35
+
36
+ **Decomposition rationale:** FeedFetcher is the single largest coverage gap (245 lines). It is tested in isolation from ItemCreator (which has its own plan). The uncovered code falls into distinct categories that can each be addressed as a focused task: (1) retry/circuit breaker logic, (2) Faraday error wrapping and connection failures, (3) header and metadata management, (4) entry processing edge cases, (5) jitter and interval helpers. Each task adds test methods to the existing test file.
37
+
38
+ **Trade-offs considered:**
39
+ - Could split FeedFetcher tests across multiple test files (e.g., feed_fetcher_retry_test.rb), but keeping them in one file matches the existing codebase convention and avoids confusion.
40
+ - Could use mocks for RetryPolicy, but testing the integration between FeedFetcher and RetryPolicy provides higher confidence.
41
+
42
+ **What constrains the structure:**
43
+ - Tests must use WebMock stubs (no real HTTP)
44
+ - All tests go in the existing test file to avoid file proliferation
45
+ - The `jitter: ->(_) { 0 }` pattern from existing tests should be reused to make interval assertions deterministic
46
+ - Tests need `travel_to` for time-sensitive assertions
47
+ </context>
48
+
49
+ ## Tasks
50
+
51
+ ### Task 1: Test retry strategy and circuit breaker transitions
52
+
53
+ - **name:** test-retry-and-circuit-breaker
54
+ - **files:**
55
+ - `test/lib/source_monitor/fetching/feed_fetcher_test.rb`
56
+ - **action:** Add tests covering lines 262-298 (reset_retry_state!, apply_retry_strategy!) and lines 267-290 (retry/circuit decisions). Specifically:
57
+ 1. Test that a first timeout failure sets fetch_retry_attempt to 1 (retry decision with wait)
58
+ 2. Test that exhausting retry attempts opens the circuit (sets fetch_circuit_opened_at, fetch_circuit_until, next_fetch_at)
59
+ 3. Test that a successful fetch after retries resets retry state (fetch_retry_attempt=0, circuit fields nil)
60
+ 4. Test that apply_retry_strategy! handles StandardError by logging and setting defaults (lines 291-298) -- simulate by stubbing RetryPolicy to raise
61
+ 5. Test that retry decision adjusts next_fetch_at to the minimum of current and retry time (line 283)
62
+ Use WebMock stubs: first stub raises Faraday::TimeoutError to trigger retries, then stub success for recovery tests. Set source.fetch_retry_attempt manually to simulate multiple failures.
63
+ - **verify:** `bin/rails test test/lib/source_monitor/fetching/feed_fetcher_test.rb -n /retry|circuit/` exits 0
64
+ - **done:** Lines 262-298 covered. Source retry fields verified in assertions.
65
+
66
+ ### Task 2: Test Faraday error wrapping and connection failures
67
+
68
+ - **name:** test-faraday-error-wrapping
69
+ - **files:**
70
+ - `test/lib/source_monitor/fetching/feed_fetcher_test.rb`
71
+ - **action:** Add tests covering lines 77-86 (perform_fetch error wrapping) and lines 405-417 (build_http_error_from_faraday). Specifically:
72
+ 1. Test that Faraday::ConnectionFailed raises SourceMonitor::Fetching::ConnectionError with original_error preserved
73
+ 2. Test that Faraday::SSLError raises ConnectionError
74
+ 3. Test that Faraday::ClientError with response hash builds HTTPError via build_http_error_from_faraday (lines 405-417) with correct status, message, and ResponseWrapper
75
+ 4. Test that generic Faraday::Error raises FetchError
76
+ 5. Test that a non-Faraday StandardError is wrapped in UnexpectedResponseError (lines 52-54)
77
+ Use WebMock's `to_raise` for each error type. Assert the result.error class, message, and original_error.
78
+ - **verify:** `bin/rails test test/lib/source_monitor/fetching/feed_fetcher_test.rb -n /connection|ssl|client_error|unexpected|faraday/i` exits 0
79
+ - **done:** Lines 77-86 and 405-417 covered. All Faraday error types correctly wrapped.
80
+
81
+ ### Task 3: Test Last-Modified header handling and request headers
82
+
83
+ - **name:** test-last-modified-and-headers
84
+ - **files:**
85
+ - `test/lib/source_monitor/fetching/feed_fetcher_test.rb`
86
+ - **action:** Add tests covering lines 97-104 (request_headers with custom_headers, etag, last_modified), lines 203-215 and 228-240 (response Last-Modified parsing and storage), and lines 349-355 (parse_http_time). Specifically:
87
+ 1. Test that If-Modified-Since header is sent when source.last_modified is set
88
+ 2. Test that Last-Modified response header is parsed and stored on source
89
+ 3. Test that malformed Last-Modified headers are silently ignored (parse_http_time returns nil for invalid dates, line 353)
90
+ 4. Test that custom_headers from source are passed through to the request (lines 98)
91
+ 5. Test that both ETag and Last-Modified are preserved on 304 not_modified responses (lines 228-234)
92
+ Use WebMock to verify request headers via `.with(headers: {...})` and return specific response headers.
93
+ - **verify:** `bin/rails test test/lib/source_monitor/fetching/feed_fetcher_test.rb -n /last_modified|custom_header|if_modified/i` exits 0
94
+ - **done:** Lines 97-104, 203-215, 228-240, 349-355 covered.
95
+
96
+ ### Task 4: Test entry processing edge cases and error normalization
97
+
98
+ - **name:** test-entry-processing-edges
99
+ - **files:**
100
+ - `test/lib/source_monitor/fetching/feed_fetcher_test.rb`
101
+ - **action:** Add tests covering lines 520-567 (process_feed_entries) and lines 603-624 (normalize_item_error, safe_entry_guid, safe_entry_title). Specifically:
102
+ 1. Test that a feed without entries (feed that doesn't respond_to :entries) returns zero counts (line 529)
103
+ 2. Test that Events.run_item_processors is called for each entry (line 542)
104
+ 3. Test that Events.after_item_created is called only for created items (line 547), not updated ones
105
+ 4. Test that normalize_item_error extracts guid via entry_id (line 615-616), falls back to id (line 617-618), and handles entries without either
106
+ 5. Test that safe_entry_title returns nil when entry doesn't respond_to :title (line 623)
107
+ Use a simple XML feed fixture with known entries. Mock ItemCreator to control created vs updated results. Use Notifications subscriptions to verify event dispatch.
108
+ - **verify:** `bin/rails test test/lib/source_monitor/fetching/feed_fetcher_test.rb -n /entry_processing|item_processor|normalize_error|safe_entry/i` exits 0
109
+ - **done:** Lines 520-567, 603-624 covered.
110
+
111
+ ### Task 5: Test jitter, interval helpers, and metadata management
112
+
113
+ - **name:** test-jitter-and-interval-helpers
114
+ - **files:**
115
+ - `test/lib/source_monitor/fetching/feed_fetcher_test.rb`
116
+ - **action:** Add tests covering lines 490-518 (jitter_offset, adjusted_interval_with_jitter, body_digest, updated_metadata) and lines 569-600 (configured_seconds, configured_positive, configured_non_negative, extract_numeric, fetching_config). Specifically:
117
+ 1. Test jitter_offset returns 0 when interval_seconds <= 0 (line 505)
118
+ 2. Test jitter_offset uses jitter_proc when provided (line 506)
119
+ 3. Test jitter_offset computes random jitter within expected range when no proc given
120
+ 4. Test body_digest returns nil for blank body (line 515), returns SHA256 for non-blank
121
+ 5. Test updated_metadata preserves existing metadata, removes dynamic_fetch_interval_seconds key, adds last_feed_signature
122
+ 6. Test configured_seconds returns default when minutes_value is nil or non-positive (lines 570-571)
123
+ 7. Test extract_numeric handles Numeric, responds_to :to_f, and non-numeric values (lines 590-596)
124
+ These are private methods -- test them through the public `call` method by configuring specific fetching settings and verifying the resulting source state. Alternatively, use `send` for the pure-function helpers.
125
+ - **verify:** `bin/rails test test/lib/source_monitor/fetching/feed_fetcher_test.rb -n /jitter|body_digest|metadata|configured_|extract_numeric/i` exits 0
126
+ - **done:** Lines 490-518 and 569-600 covered.
127
+
128
+ ## Verification
129
+
130
+ 1. `bin/rails test test/lib/source_monitor/fetching/feed_fetcher_test.rb` exits 0
131
+ 2. `COVERAGE=1 bin/rails test test/lib/source_monitor/fetching/feed_fetcher_test.rb` shows feed_fetcher.rb with >80% branch coverage
132
+ 3. `bin/rails test` exits 0 (no regressions)
133
+
134
+ ## Success Criteria
135
+
136
+ - [ ] FeedFetcher coverage drops from 245 uncovered lines to fewer than 50
137
+ - [ ] All retry/circuit breaker branches tested
138
+ - [ ] All Faraday error wrapping branches tested
139
+ - [ ] All header handling branches tested
140
+ - [ ] Entry processing and error normalization branches tested
141
+ - [ ] Jitter, interval helpers, and metadata management tested
142
+ - [ ] REQ-01 substantially satisfied
@@ -0,0 +1,64 @@
1
+ # PLAN-02 Summary: item-creator-tests
2
+
3
+ ## Status: COMPLETE
4
+
5
+ ## Commit
6
+
7
+ - **Hash:** `ce8ede4`
8
+ - **Message:** `test(item-creator): close coverage gaps for URL/content, dupes, authors, errors, utils`
9
+ - **Files changed:** 1 file, 941 insertions
10
+
11
+ ## Tasks Completed
12
+
13
+ ### Task 1: Test URL extraction fallbacks and content extraction chain
14
+ - Tested extract_url with link_nodes alternate link, links array fallback, nil handling
15
+ - Tested extract_content priority: content > content_encoded > summary
16
+ - Tested extract_updated_timestamp returns entry.updated when present
17
+
18
+ ### Task 2: Test concurrent duplicate handling (RecordNotUnique)
19
+ - Tested RecordNotUnique for guid conflict finds and updates existing item
20
+ - Tested RecordNotUnique for fingerprint conflict finds by fingerprint
21
+ - Tested handle_concurrent_duplicate returns Result with status: :updated
22
+
23
+ ### Task 3: Test multi-format author, enclosure, and media extraction
24
+ - Tested extract_authors from dc_creators, author_nodes (email/uri), deduplication
25
+ - Tested extract_enclosures from RSS enclosure_nodes, Atom link_nodes, JSON attachments
26
+ - Tested extract_media_content with url, type, medium, dimensions
27
+ - Tested extract_media_thumbnail_url fallback from nodes to entry.image
28
+ - Tested blank URL filtering in enclosures
29
+
30
+ ### Task 4: Test feed content processing error paths and readability edge cases
31
+ - Tested parser error produces item with raw content and error metadata
32
+ - Tested should_process_feed_content? returns false for blank/non-HTML content
33
+ - Tested deep_copy handles Hash, Array, deep_dup, TypeError rescue
34
+ - Tested build_feed_content_metadata includes readability_text_length
35
+ - Tested html_fragment? detection
36
+
37
+ ### Task 5: Test utility methods
38
+ - Tested safe_integer: nil, Integer, string "42", non-numeric string
39
+ - Tested split_keywords: commas, semicolons, whitespace stripping
40
+ - Tested extract_guid dedup when entry_id equals URL
41
+ - Tested extract_language from json_entry and entry.language paths
42
+ - Tested normalize_metadata JSON roundtrip and unparseable values
43
+ - Tested extract_comments_count slash_comments_raw/comments_count fallback
44
+
45
+ ## Deviations
46
+
47
+ None -- plan executed as specified.
48
+
49
+ ## Verification Results
50
+
51
+ | Check | Result |
52
+ |-------|--------|
53
+ | `bin/rails test test/lib/source_monitor/items/item_creator_test.rb` | All tests pass |
54
+ | `bin/rails test` | 760 runs, 2626 assertions, 0 failures, 0 errors, 0 skips |
55
+
56
+ ## Success Criteria
57
+
58
+ - [x] 56 new tests added (78 total, 941 lines)
59
+ - [x] URL extraction fallback branches tested
60
+ - [x] Concurrent duplicate handling tested
61
+ - [x] Multi-format author/enclosure/media extraction tested
62
+ - [x] Feed content processing error paths tested
63
+ - [x] All utility methods tested
64
+ - [x] REQ-02 substantially satisfied
@@ -0,0 +1,138 @@
1
+ ---
2
+ phase: 2
3
+ plan: 2
4
+ title: item-creator-tests
5
+ wave: 1
6
+ depends_on: []
7
+ skills_used: []
8
+ must_haves:
9
+ truths:
10
+ - "Running `bin/rails test test/lib/source_monitor/items/item_creator_test.rb` exits 0 with zero failures"
11
+ - "Coverage report shows lib/source_monitor/items/item_creator.rb has fewer than 40 uncovered lines (down from 228)"
12
+ - "Running `bin/rails test` exits 0 with no regressions"
13
+ artifacts:
14
+ - "test/lib/source_monitor/items/item_creator_test.rb -- extended with new test methods covering URL extraction, content extraction, concurrent duplicate handling, author/enclosure/media extraction, feed content processing, and utility methods"
15
+ key_links:
16
+ - "REQ-02 substantially satisfied -- ItemCreator branch coverage above 80%"
17
+ ---
18
+
19
+ # Plan 02: item-creator-tests
20
+
21
+ ## Objective
22
+
23
+ Close the coverage gap in `lib/source_monitor/items/item_creator.rb` (currently 228 uncovered lines out of 601). The existing test file covers basic RSS/Atom/JSON creation, fingerprint generation, guid fallback, readability processing, metadata extraction, and guid/fingerprint deduplication. This plan targets the remaining uncovered branches: URL extraction from link_nodes and links arrays, content extraction fallback chain, concurrent duplicate handling (RecordNotUnique), author extraction from multiple sources, enclosure extraction from Atom link_nodes and JSON attachments, media content extraction, language/copyright/comments extraction, keyword splitting, safe_integer edge cases, and the deep_copy utility.
24
+
25
+ ## Context
26
+
27
+ <context>
28
+ @lib/source_monitor/items/item_creator.rb -- 601 lines, item creation with dedup logic
29
+ @test/lib/source_monitor/items/item_creator_test.rb -- existing test file with 8 tests
30
+ @config/coverage_baseline.json -- lists 228 uncovered lines for item_creator.rb
31
+ @test/fixtures/feeds/ -- RSS, Atom, JSON feed fixtures
32
+ @test/test_helper.rb -- test infrastructure
33
+
34
+ **Decomposition rationale:** ItemCreator is the second largest coverage gap. Its uncovered lines cluster into: (1) URL/content/timestamp extraction branches, (2) concurrent duplicate handling, (3) multi-format author/enclosure/media extraction, (4) feed content processing error paths, (5) utility methods. Each task targets a distinct cluster.
35
+
36
+ **Trade-offs considered:**
37
+ - Many branches are triggered by specific Feedjira entry types (AtomEntry, JSONFeedItem). Tests need mock entries or real parsed fixtures to exercise these.
38
+ - The concurrent duplicate test (RecordNotUnique) requires careful setup to simulate a race condition without actually racing.
39
+ - Testing private methods through the public `call` interface keeps tests realistic, but some deep utility methods (deep_copy, safe_integer, split_keywords) are easier to verify directly.
40
+
41
+ **What constrains the structure:**
42
+ - Must use Feedjira-parsed entries (not plain hashes) to exercise respond_to? checks
43
+ - Atom and JSON-specific branches need entries of the correct type
44
+ - Tests extend the existing test file
45
+ </context>
46
+
47
+ ## Tasks
48
+
49
+ ### Task 1: Test URL extraction fallbacks and content extraction chain
50
+
51
+ - **name:** test-url-and-content-extraction
52
+ - **files:**
53
+ - `lib/source_monitor/items/item_creator.rb` -- (read-only reference)
54
+ - `test/lib/source_monitor/items/item_creator_test.rb`
55
+ - **action:** Add tests covering lines 288-310 (extract_url with link_nodes and links arrays), lines 318-336 (extract_content with CONTENT_METHODS fallback chain), and lines 328-342 (extract_timestamp, extract_updated_timestamp). Specifically:
56
+ 1. Test extract_url when entry.url is blank but entry.link_nodes has an alternate link with href -- use an Atom entry fixture
57
+ 2. Test extract_url when entry.url is blank and link_nodes are empty but entry.links has a URL string
58
+ 3. Test extract_url returns nil when no URL source is available (mock entry with no url/link_nodes/links)
59
+ 4. Test extract_content tries :content, then :content_encoded, then :summary in order -- create mock entries that respond to different method subsets
60
+ 5. Test extract_updated_timestamp returns entry.updated when present, nil otherwise
61
+ Use OpenStruct or Minitest::Mock to create entries with specific respond_to? patterns for edge cases not covered by real feed fixtures.
62
+ - **verify:** `bin/rails test test/lib/source_monitor/items/item_creator_test.rb -n /url_extraction|content_extraction|updated_timestamp/i` exits 0
63
+ - **done:** Lines 288-310, 318-342 covered.
64
+
65
+ ### Task 2: Test concurrent duplicate handling (RecordNotUnique)
66
+
67
+ - **name:** test-concurrent-duplicate-handling
68
+ - **files:**
69
+ - `test/lib/source_monitor/items/item_creator_test.rb`
70
+ - **action:** Add tests covering lines 104-128 (create_new_item rescue RecordNotUnique, handle_concurrent_duplicate, find_conflicting_item). Specifically:
71
+ 1. Test that when create_new_item raises ActiveRecord::RecordNotUnique for a guid conflict, the item is found and updated instead
72
+ 2. Test that when RecordNotUnique fires for a fingerprint conflict (no raw guid), the item is found by fingerprint and updated
73
+ 3. Test that handle_concurrent_duplicate returns a Result with status: :updated and the correct matched_by
74
+ Simulate RecordNotUnique by: (a) creating an item first, (b) stubbing source.items.new to return an item that raises RecordNotUnique on save!, then verifying the fallback path finds and updates the existing item.
75
+ - **verify:** `bin/rails test test/lib/source_monitor/items/item_creator_test.rb -n /concurrent_duplicate|record_not_unique/i` exits 0
76
+ - **done:** Lines 104-128 covered.
77
+
78
+ ### Task 3: Test multi-format author, enclosure, and media extraction
79
+
80
+ - **name:** test-author-enclosure-media-extraction
81
+ - **files:**
82
+ - `test/lib/source_monitor/items/item_creator_test.rb`
83
+ - **action:** Add tests covering lines 348-383 (extract_authors with rss_authors, dc_creators, author_nodes, json authors), lines 416-466 (extract_enclosures from rss enclosure_nodes, atom link_nodes with rel=enclosure, json attachments), and lines 477-499 (extract_media_content). Specifically:
84
+ 1. Test extract_authors collects from rss_authors, dc_creators, dc_creator, and author_nodes (name/email/uri) -- deduplicates and compacts
85
+ 2. Test extract_enclosures from Atom link_nodes with rel="enclosure" produces entries with source: "atom_link"
86
+ 3. Test extract_media_content builds array from media_content_nodes with url, type, medium, height, width, file_size, duration, expression -- compacted
87
+ 4. Test extract_media_thumbnail_url falls back from media_thumbnail_nodes to entry.image
88
+ 5. Test extract_enclosures skips entries with blank URLs
89
+ Build mock entries using Struct or OpenStruct to simulate the various node types. The JSON-specific paths are already partially tested; focus on Atom and RSS edge cases.
90
+ - **verify:** `bin/rails test test/lib/source_monitor/items/item_creator_test.rb -n /authors|enclosure|media_content|media_thumbnail/i` exits 0
91
+ - **done:** Lines 348-383, 416-466, 477-499 covered.
92
+
93
+ ### Task 4: Test feed content processing error path and readability edge cases
94
+
95
+ - **name:** test-feed-content-processing-errors
96
+ - **files:**
97
+ - `test/lib/source_monitor/items/item_creator_test.rb`
98
+ - **action:** Add tests covering lines 137-158 (process_feed_content error rescue), lines 160-165 (should_process_feed_content?), lines 187-208 (default_feed_readability_options, build_feed_content_metadata), and lines 210-231 (html_fragment?, deep_copy). Specifically:
99
+ 1. Test that when readability parser raises an error, the item is created with raw content and error metadata (lines 148-157): metadata has "status"=>"failed", "error_class", "error_message"
100
+ 2. Test should_process_feed_content? returns false when content is blank or when content has no HTML tags (html_fragment? returns false)
101
+ 3. Test deep_copy handles Hash, Array, and objects that support deep_dup, plus TypeError rescue
102
+ 4. Test build_feed_content_metadata includes readability_text_length and title when present
103
+ 5. Test html_fragment? returns true for `<p>text</p>` and false for plain text
104
+ Stub the parser class to raise for the error path test. Use source with feed_content_readability_enabled: true.
105
+ - **verify:** `bin/rails test test/lib/source_monitor/items/item_creator_test.rb -n /processing_error|html_fragment|deep_copy|readability_metadata/i` exits 0
106
+ - **done:** Lines 137-165, 187-231 covered.
107
+
108
+ ### Task 5: Test utility methods: safe_integer, split_keywords, string_or_nil, normalize_metadata, extract_guid edge cases
109
+
110
+ - **name:** test-utility-methods
111
+ - **files:**
112
+ - `test/lib/source_monitor/items/item_creator_test.rb`
113
+ - **action:** Add tests covering lines 501-534 (extract_language, extract_copyright, extract_comments_url, extract_comments_count), lines 536-543 (extract_metadata with normalize_metadata), lines 555-597 (string_or_nil, sanitize_string_array, split_keywords, safe_integer, json_entry?, atom_entry?, normalize_metadata). Specifically:
114
+ 1. Test safe_integer returns nil for nil, returns Integer for Integer, parses string "42", returns nil for non-numeric string (lines 574-584)
115
+ 2. Test split_keywords splits on commas and semicolons, strips whitespace, removes blank entries (lines 565-572)
116
+ 3. Test extract_guid returns nil when entry_id equals URL (dedup logic at line 283-285)
117
+ 4. Test extract_language from json_entry? path (line 506-507) and from entry.language (line 502-503)
118
+ 5. Test normalize_metadata returns empty hash for unparseable values (JSON roundtrip at lines 594-597)
119
+ 6. Test extract_comments_count tries slash_comments_raw then comments_count (lines 529-533)
120
+ Use mock entries with targeted respond_to? patterns. For json_entry? and atom_entry? tests, use actual Feedjira-parsed entries from fixtures.
121
+ - **verify:** `bin/rails test test/lib/source_monitor/items/item_creator_test.rb -n /safe_integer|split_keywords|extract_guid_edge|language|copyright|normalize_metadata|comments_count/i` exits 0
122
+ - **done:** Lines 501-597 covered.
123
+
124
+ ## Verification
125
+
126
+ 1. `bin/rails test test/lib/source_monitor/items/item_creator_test.rb` exits 0
127
+ 2. `COVERAGE=1 bin/rails test test/lib/source_monitor/items/item_creator_test.rb` shows item_creator.rb with >80% branch coverage
128
+ 3. `bin/rails test` exits 0 (no regressions)
129
+
130
+ ## Success Criteria
131
+
132
+ - [ ] ItemCreator coverage drops from 228 uncovered lines to fewer than 40
133
+ - [ ] URL extraction fallback branches tested
134
+ - [ ] Concurrent duplicate handling tested
135
+ - [ ] Multi-format author/enclosure/media extraction tested
136
+ - [ ] Feed content processing error paths tested
137
+ - [ ] All utility methods tested
138
+ - [ ] REQ-02 substantially satisfied
@@ -0,0 +1,85 @@
1
+ # PLAN-03 Summary: configuration-tests
2
+
3
+ ## Status: COMPLETE
4
+
5
+ ## Commit
6
+
7
+ - **Hash:** `66b8df2`
8
+ - **Message:** `test(dev-plan05): close coverage gaps for bulk scraper and broadcaster`
9
+ - **Files changed:** 1 file (configuration_test.rb), 771 insertions
10
+ - **Note:** Commit was mislabeled as "dev-plan05" but the code changes are configuration tests matching Plan 03 tasks exactly.
11
+
12
+ ## Tasks Completed
13
+
14
+ ### Task 1: Test AuthenticationSettings handlers (lines 75-130)
15
+ - Tested authenticate_with :symbol handler dispatches via public_send
16
+ - Tested authenticate_with string handler converts to symbol
17
+ - Tested authenticate_with callable with zero arity uses instance_exec
18
+ - Tested authenticate_with callable with arity passes controller
19
+ - Tested authenticate_with block handler
20
+ - Tested authorize_with symbol and callable handlers
21
+ - Tested Handler.call returns nil when callable is nil
22
+ - Tested invalid handler raises ArgumentError
23
+ - Tested reset! clears all handlers and methods
24
+ - Tested authenticate_with nil returns nil handler
25
+
26
+ ### Task 2: Test ScrapingSettings and RetentionSettings edge cases (lines 132-164, 398-436)
27
+ - Tested ScrapingSettings defaults (max_in_flight_per_source=25, max_bulk_batch_size=100)
28
+ - Tested normalize_numeric: string, nil, empty string, zero, negative all handled correctly
29
+ - Tested ScrapingSettings reset restores defaults
30
+ - Tested RetentionSettings defaults (nil for days/max_items, :destroy strategy)
31
+ - Tested strategy accepts :soft_delete, string "destroy", rejects :archive and non-symbolizable
32
+ - Tested strategy normalizes nil to :destroy
33
+
34
+ ### Task 3: Test RealtimeSettings adapter validation and action_cable_config (lines 166-253)
35
+ - Tested adapter= accepts :solid_cable, :redis, :async; rejects :websocket and nil
36
+ - Tested action_cable_config for solid_cable returns merged SolidCableOptions
37
+ - Tested action_cable_config for redis with/without url
38
+ - Tested action_cable_config for async returns { adapter: "async" }
39
+ - Tested SolidCableOptions.assign with hash, unknown keys, non-enumerable input
40
+ - Tested SolidCableOptions.to_h compacts nil values
41
+ - Tested realtime reset restores defaults
42
+
43
+ ### Task 4: Test Events callbacks and item_processors (lines 438-491)
44
+ - Tested after_item_created with lambda and block registration
45
+ - Tested after_item_scraped and after_fetch_completed registration
46
+ - Tested multiple callbacks per event, callbacks_for unknown key returns []
47
+ - Tested callbacks_for returns dup preventing mutation
48
+ - Tested non-callable handler rejection (ArgumentError)
49
+ - Tested register_item_processor with lambda, block, multiple, dup protection
50
+ - Tested events reset! clears callbacks and item_processors
51
+
52
+ ### Task 5: Test Models, ModelDefinition, ConcernDefinition, ValidationDefinition (lines 493-652)
53
+ - Tested Models.table_name_prefix default "sourcemon_"
54
+ - Tested Models exposes all model keys, for(:source) returns definition
55
+ - Tested Models.for(:unknown) raises ArgumentError
56
+ - Tested include_concern with Module, block (anonymous module), string constant
57
+ - Tested include_concern deduplication by signature (module, string, blocks differ)
58
+ - Tested include_concern with invalid string raises on resolve
59
+ - Tested validate with symbol, string, lambda, block; raises on invalid handler
60
+ - Tested ValidationDefinition.signature for symbol, string, callable handlers
61
+ - Tested each_concern returns Enumerator without block
62
+
63
+ ## Deviations
64
+
65
+ | ID | Description | Impact |
66
+ |----|-------------|--------|
67
+ | DEVN-01 | Commit mislabeled as "dev-plan05" | None -- code changes are correct Plan 03 configuration tests |
68
+ | DEVN-02 | Parallel test runner segfaults on PG fork when running single file | Tests pass with PARALLEL_WORKERS=1 and in full suite; environment issue, not code defect |
69
+
70
+ ## Verification Results
71
+
72
+ | Check | Result |
73
+ |-------|--------|
74
+ | `PARALLEL_WORKERS=1 bin/rails test test/lib/source_monitor/configuration_test.rb` | 81 runs, 178 assertions, 0 failures, 0 errors, 0 skips |
75
+ | `bin/rails test` (full suite) | 760 runs, 2626 assertions, 0 failures, 0 errors, 0 skips |
76
+
77
+ ## Success Criteria
78
+
79
+ - [x] 81 tests total (76 new, 5 existing), 771 lines added
80
+ - [x] AuthenticationSettings handlers fully tested
81
+ - [x] ScrapingSettings and RetentionSettings edge cases tested
82
+ - [x] RealtimeSettings adapter validation and action_cable_config tested
83
+ - [x] Events callbacks and item_processors tested
84
+ - [x] Models and definition classes tested
85
+ - [x] REQ-03 substantially satisfied
@@ -0,0 +1,147 @@
1
+ ---
2
+ phase: 2
3
+ plan: 3
4
+ title: configuration-tests
5
+ wave: 1
6
+ depends_on: []
7
+ skills_used: []
8
+ must_haves:
9
+ truths:
10
+ - "Running `bin/rails test test/lib/source_monitor/configuration_test.rb` exits 0 with zero failures"
11
+ - "Coverage report shows lib/source_monitor/configuration.rb has fewer than 20 uncovered lines (down from 94)"
12
+ - "Running `bin/rails test` exits 0 with no regressions"
13
+ artifacts:
14
+ - "test/lib/source_monitor/configuration_test.rb -- extended with new test methods covering AuthenticationSettings, ScrapingSettings, RealtimeSettings, RetentionSettings, Events, Models, and ModelDefinition"
15
+ key_links:
16
+ - "REQ-03 substantially satisfied -- Configuration branch coverage above 80%"
17
+ ---
18
+
19
+ # Plan 03: configuration-tests
20
+
21
+ ## Objective
22
+
23
+ Close the coverage gap in `lib/source_monitor/configuration.rb` (currently 94 uncovered lines out of 655). The existing test file has only 5 tests covering mission_control_dashboard_path, scraper registry, retention strategy default, and fetching settings. This plan targets the remaining uncovered branches across the 12 nested settings classes: AuthenticationSettings handlers, ScrapingSettings normalization, RealtimeSettings adapter validation and action_cable_config, RetentionSettings strategy validation, Events callbacks and item_processors, Models table_name_prefix and `for` method, ModelDefinition concerns and validations, ConcernDefinition resolution, and ValidationDefinition signatures.
24
+
25
+ ## Context
26
+
27
+ <context>
28
+ @lib/source_monitor/configuration.rb -- 655 lines with ~12 nested classes
29
+ @test/lib/source_monitor/configuration_test.rb -- existing test file with 5 tests
30
+ @config/coverage_baseline.json -- lists 94 uncovered lines for configuration.rb
31
+ @test/test_helper.rb -- resets configuration each test
32
+
33
+ **Decomposition rationale:** Configuration has many small nested classes, each with a few uncovered branches. Rather than one mega-task, we group by logical subsystem: (1) authentication handlers, (2) scraping/retention settings with edge cases, (3) realtime adapter validation, (4) events system, (5) models and concern/validation definitions.
34
+
35
+ **Trade-offs considered:**
36
+ - Could create separate test files per settings class, but the existing pattern is one configuration_test.rb file.
37
+ - Some branches (like ConcernDefinition with constant string resolution) require careful setup to test constantize calls.
38
+ - RealtimeSettings action_cable_config branches test all three adapter paths.
39
+
40
+ **What constrains the structure:**
41
+ - Each test must call SourceMonitor.reset_configuration! in setup/teardown (already present)
42
+ - Tests should not leak state between settings classes
43
+ - ModelDefinition tests need care around Module.new blocks
44
+ </context>
45
+
46
+ ## Tasks
47
+
48
+ ### Task 1: Test AuthenticationSettings handlers
49
+
50
+ - **name:** test-authentication-settings
51
+ - **files:**
52
+ - `test/lib/source_monitor/configuration_test.rb`
53
+ - **action:** Add tests covering lines 75-130 (AuthenticationSettings and Handler). Specifically:
54
+ 1. Test authenticate_with(:some_method) creates a Handler with type :symbol that calls controller.public_send (lines 80-82)
55
+ 2. Test authenticate_with { |c| c.redirect_to "/" } creates a Handler with type :callable that calls the block with controller (lines 84-88) -- test both zero-arity and one-arity blocks
56
+ 3. Test authorize_with works the same way (line 105-107)
57
+ 4. Test that passing an invalid handler (not Symbol, String, or callable) raises ArgumentError (line 127)
58
+ 5. Test reset! clears all handlers and methods (lines 109-114)
59
+ 6. Test Handler.call returns nil when callable is nil (line 78)
60
+ Use a mock controller object (OpenStruct with method stubs) to verify handler dispatch.
61
+ - **verify:** `bin/rails test test/lib/source_monitor/configuration_test.rb -n /authentication|authorize|handler/i` exits 0
62
+ - **done:** Lines 75-130 covered.
63
+
64
+ ### Task 2: Test ScrapingSettings and RetentionSettings edge cases
65
+
66
+ - **name:** test-scraping-and-retention-settings
67
+ - **files:**
68
+ - `test/lib/source_monitor/configuration_test.rb`
69
+ - **action:** Add tests covering lines 132-164 (ScrapingSettings) and lines 398-436 (RetentionSettings). Specifically:
70
+ 1. Test ScrapingSettings defaults (max_in_flight_per_source=25, max_bulk_batch_size=100)
71
+ 2. Test ScrapingSettings normalize_numeric: nil returns nil, "" returns nil, negative returns nil, positive returns integer (lines 157-163)
72
+ 3. Test ScrapingSettings setter with string input (e.g., "50") normalizes to integer
73
+ 4. Test RetentionSettings strategy= with nil defaults to :destroy (line 419)
74
+ 5. Test RetentionSettings strategy= with invalid value raises ArgumentError (line 430)
75
+ 6. Test RetentionSettings strategy= with string "soft_delete" normalizes to symbol
76
+ 7. Test RetentionSettings strategy= with non-symbolizable value raises ArgumentError (line 433)
77
+ - **verify:** `bin/rails test test/lib/source_monitor/configuration_test.rb -n /scraping_settings|retention_settings|normalize/i` exits 0
78
+ - **done:** Lines 132-164 and 398-436 covered.
79
+
80
+ ### Task 3: Test RealtimeSettings adapter validation and action_cable_config
81
+
82
+ - **name:** test-realtime-settings
83
+ - **files:**
84
+ - `test/lib/source_monitor/configuration_test.rb`
85
+ - **action:** Add tests covering lines 166-253 (RealtimeSettings, SolidCableOptions). Specifically:
86
+ 1. Test adapter= with :solid_cable, :redis, :async all accepted (line 178)
87
+ 2. Test adapter= with :invalid raises ArgumentError (line 179)
88
+ 3. Test action_cable_config for :solid_cable returns hash with adapter: "solid_cable" and SolidCableOptions merged (lines 197-198)
89
+ 4. Test action_cable_config for :redis returns hash with adapter: "redis" and redis_url when set (lines 200-202)
90
+ 5. Test action_cable_config for :async returns { adapter: "async" } (line 204)
91
+ 6. Test SolidCableOptions.assign with a hash of options sets the corresponding attributes (lines 223-229)
92
+ 7. Test SolidCableOptions.to_h compacts nil values (line 251)
93
+ 8. Test solid_cable= delegates to solid_cable.assign (line 192)
94
+ - **verify:** `bin/rails test test/lib/source_monitor/configuration_test.rb -n /realtime|adapter|action_cable|solid_cable/i` exits 0
95
+ - **done:** Lines 166-253 covered.
96
+
97
+ ### Task 4: Test Events callbacks and item_processors
98
+
99
+ - **name:** test-events-system
100
+ - **files:**
101
+ - `test/lib/source_monitor/configuration_test.rb`
102
+ - **action:** Add tests covering lines 438-491 (Events). Specifically:
103
+ 1. Test registering after_item_created callback with a lambda and retrieving via callbacks_for(:after_item_created) (lines 446-449)
104
+ 2. Test registering after_item_created callback with a block
105
+ 3. Test register_item_processor adds to item_processors list (lines 452-457)
106
+ 4. Test that passing non-callable to register_item_processor raises ArgumentError (line 488)
107
+ 5. Test that registering unknown event key raises ArgumentError (line 479)
108
+ 6. Test callbacks_for returns empty array for unregistered name (line 460)
109
+ 7. Test reset! clears all callbacks and item_processors (lines 467-470)
110
+ - **verify:** `bin/rails test test/lib/source_monitor/configuration_test.rb -n /events|callback|item_processor/i` exits 0
111
+ - **done:** Lines 438-491 covered.
112
+
113
+ ### Task 5: Test Models, ModelDefinition, ConcernDefinition, and ValidationDefinition
114
+
115
+ - **name:** test-models-and-definitions
116
+ - **files:**
117
+ - `test/lib/source_monitor/configuration_test.rb`
118
+ - **action:** Add tests covering lines 493-652 (Models, ModelDefinition, ConcernDefinition, ValidationDefinition). Specifically:
119
+ 1. Test Models.table_name_prefix default is "sourcemon_" (line 507)
120
+ 2. Test Models.for(:source) returns a ModelDefinition, and Models.for(:unknown) raises ArgumentError (lines 515-521)
121
+ 3. Test ModelDefinition.include_concern with a Module directly (line 591-592)
122
+ 4. Test ModelDefinition.include_concern with a string constant name that resolves (lines 593-598)
123
+ 5. Test ModelDefinition.include_concern with a block creates anonymous module (lines 588-590)
124
+ 6. Test ModelDefinition.include_concern deduplicates by signature (line 534)
125
+ 7. Test ModelDefinition.validate with symbol, with callable (proc), and with block (lines 549-563)
126
+ 8. Test ModelDefinition.validate with invalid handler raises ArgumentError (line 558)
127
+ 9. Test ValidationDefinition.signature for symbol, callable, and string handlers (lines 636-647)
128
+ 10. Test ValidationDefinition.symbol? (lines 649-651)
129
+ Use real module references and string constants for concern resolution tests.
130
+ - **verify:** `bin/rails test test/lib/source_monitor/configuration_test.rb -n /models|model_definition|concern|validation_definition/i` exits 0
131
+ - **done:** Lines 493-652 covered.
132
+
133
+ ## Verification
134
+
135
+ 1. `bin/rails test test/lib/source_monitor/configuration_test.rb` exits 0
136
+ 2. `COVERAGE=1 bin/rails test test/lib/source_monitor/configuration_test.rb` shows configuration.rb with >80% branch coverage
137
+ 3. `bin/rails test` exits 0 (no regressions)
138
+
139
+ ## Success Criteria
140
+
141
+ - [ ] Configuration coverage drops from 94 uncovered lines to fewer than 20
142
+ - [ ] AuthenticationSettings handlers fully tested
143
+ - [ ] ScrapingSettings and RetentionSettings edge cases tested
144
+ - [ ] RealtimeSettings adapter validation and action_cable_config tested
145
+ - [ ] Events callbacks and item_processors tested
146
+ - [ ] Models and definition classes tested
147
+ - [ ] REQ-03 substantially satisfied
@@ -0,0 +1,63 @@
1
+ # PLAN-04 Summary: dashboard-and-analytics-tests
2
+
3
+ ## Status: COMPLETE
4
+
5
+ ## Commits
6
+
7
+ - **Hash:** `a8f2611`
8
+ - **Message:** `test(dashboard-analytics): close coverage gaps for stats, activity, metrics, cache`
9
+ - **Files changed:** 2 files, 564 insertions (queries_test.rb + sources_index_metrics_test.rb)
10
+
11
+ - **Hash:** `2e50580` (tag commit)
12
+ - **Message:** `test(dev-plan04): dashboard and analytics coverage gaps`
13
+ - **Note:** Continuation/tagging commit for task verification.
14
+
15
+ ## Tasks Completed
16
+
17
+ ### Task 1: Test StatsQuery SQL branches and integer_value
18
+ - Tested stats returns correct counts with mixed active/inactive sources
19
+ - Tested failed_sources counts OR conditions (failure_count > 0, last_error, last_error_at)
20
+ - Tested fetches_today time boundary (started_at >= start_of_day)
21
+ - Tested stats with empty database returns all zeros
22
+ - Tested record_stats_metrics sets gauge values for all stat keys
23
+
24
+ ### Task 2: Test RecentActivityQuery build_event and sub-queries
25
+ - Tested build_event produces Event objects for all 3 types (fetch_log, scrape_log, item)
26
+ - Tested fetch_log events have success based on boolean column
27
+ - Tested scrape_log events include scraper_adapter and source_name via JOIN
28
+ - Tested item events have item_title, item_url, success_flag always 1
29
+ - Tested events ordered by occurred_at DESC with limit
30
+ - Tested record_metrics for :recent_activity sets gauge values
31
+
32
+ ### Task 3: Test record_metrics branches and Cache edge cases
33
+ - Tested record_metrics for all 4 case branches (stats, recent_activity, job_metrics, upcoming_fetch_schedule)
34
+ - Tested Cache.fetch returns cached value on second call
35
+ - Tested Cache nil/false storage, array keys, key isolation
36
+
37
+ ### Task 4: Test SourcesIndexMetrics edge cases
38
+ - Tested fetch_interval_filter with gteq/lt/lteq combinations
39
+ - Tested integer_param sanitization: blank, non-numeric, valid string
40
+ - Tested selected_fetch_interval_bucket nil-max matching, nil when no filter
41
+ - Tested distribution_scope ransack delegation
42
+ - Tested nil search_params handling
43
+
44
+ ## Deviations
45
+
46
+ None -- plan executed as specified across both target files.
47
+
48
+ ## Verification Results
49
+
50
+ | Check | Result |
51
+ |-------|--------|
52
+ | `PARALLEL_WORKERS=1 bin/rails test test/lib/source_monitor/dashboard/queries_test.rb test/lib/source_monitor/analytics/sources_index_metrics_test.rb` | All tests pass |
53
+ | `bin/rails test` | 760 runs, 2626 assertions, 0 failures, 0 errors, 0 skips |
54
+
55
+ ## Success Criteria
56
+
57
+ - [x] 35 new tests added (564 lines across 2 files)
58
+ - [x] StatsQuery SQL branches fully tested
59
+ - [x] RecentActivityQuery event building and sub-queries tested
60
+ - [x] record_metrics branches for all query types tested
61
+ - [x] Cache miss/hit behavior tested
62
+ - [x] SourcesIndexMetrics filter, sanitization, and distribution scope tested
63
+ - [x] REQ-04 and REQ-07 substantially satisfied