source_monitor 0.3.0 → 0.3.2

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 (54) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/skills/sm-architecture/SKILL.md +233 -0
  3. data/.claude/skills/sm-architecture/reference/extraction-patterns.md +192 -0
  4. data/.claude/skills/sm-architecture/reference/module-map.md +194 -0
  5. data/.claude/skills/sm-configuration-setting/SKILL.md +264 -0
  6. data/.claude/skills/sm-configuration-setting/reference/settings-catalog.md +248 -0
  7. data/.claude/skills/sm-configuration-setting/reference/settings-pattern.md +297 -0
  8. data/.claude/skills/sm-configure/SKILL.md +153 -0
  9. data/.claude/skills/sm-configure/reference/configuration-reference.md +321 -0
  10. data/.claude/skills/sm-dashboard-widget/SKILL.md +344 -0
  11. data/.claude/skills/sm-dashboard-widget/reference/dashboard-patterns.md +304 -0
  12. data/.claude/skills/sm-domain-model/SKILL.md +188 -0
  13. data/.claude/skills/sm-domain-model/reference/model-graph.md +114 -0
  14. data/.claude/skills/sm-domain-model/reference/table-structure.md +348 -0
  15. data/.claude/skills/sm-engine-migration/SKILL.md +395 -0
  16. data/.claude/skills/sm-engine-migration/reference/migration-conventions.md +255 -0
  17. data/.claude/skills/sm-engine-test/SKILL.md +302 -0
  18. data/.claude/skills/sm-engine-test/reference/test-helpers.md +259 -0
  19. data/.claude/skills/sm-engine-test/reference/test-patterns.md +411 -0
  20. data/.claude/skills/sm-event-handler/SKILL.md +265 -0
  21. data/.claude/skills/sm-event-handler/reference/events-api.md +229 -0
  22. data/.claude/skills/sm-health-rule/SKILL.md +327 -0
  23. data/.claude/skills/sm-health-rule/reference/health-system.md +269 -0
  24. data/.claude/skills/sm-host-setup/SKILL.md +223 -0
  25. data/.claude/skills/sm-host-setup/reference/initializer-template.md +195 -0
  26. data/.claude/skills/sm-host-setup/reference/setup-checklist.md +134 -0
  27. data/.claude/skills/sm-job/SKILL.md +263 -0
  28. data/.claude/skills/sm-job/reference/job-conventions.md +245 -0
  29. data/.claude/skills/sm-model-extension/SKILL.md +287 -0
  30. data/.claude/skills/sm-model-extension/reference/extension-api.md +317 -0
  31. data/.claude/skills/sm-pipeline-stage/SKILL.md +254 -0
  32. data/.claude/skills/sm-pipeline-stage/reference/completion-handlers.md +152 -0
  33. data/.claude/skills/sm-pipeline-stage/reference/entry-processing.md +191 -0
  34. data/.claude/skills/sm-pipeline-stage/reference/feed-fetcher-architecture.md +198 -0
  35. data/.claude/skills/sm-scraper-adapter/SKILL.md +284 -0
  36. data/.claude/skills/sm-scraper-adapter/reference/adapter-contract.md +167 -0
  37. data/.claude/skills/sm-scraper-adapter/reference/example-adapter.md +274 -0
  38. data/.vbw-planning/.notification-log.jsonl +102 -0
  39. data/.vbw-planning/.session-log.jsonl +505 -0
  40. data/AGENTS.md +20 -57
  41. data/CHANGELOG.md +19 -0
  42. data/CLAUDE.md +44 -1
  43. data/CONTRIBUTING.md +5 -5
  44. data/Gemfile.lock +20 -21
  45. data/README.md +18 -5
  46. data/VERSION +1 -0
  47. data/docs/deployment.md +1 -1
  48. data/docs/setup.md +4 -4
  49. data/lib/source_monitor/setup/skills_installer.rb +94 -0
  50. data/lib/source_monitor/setup/workflow.rb +17 -2
  51. data/lib/source_monitor/version.rb +1 -1
  52. data/lib/tasks/source_monitor_setup.rake +58 -0
  53. data/source_monitor.gemspec +1 -0
  54. metadata +39 -1
@@ -0,0 +1,302 @@
1
+ ---
2
+ name: sm-engine-test
3
+ description: Source Monitor engine test patterns and helpers. Use when writing or modifying tests for the Source Monitor engine, debugging test failures, setting up test isolation, or working with VCR/WebMock in this project.
4
+ allowed-tools: Read, Write, Edit, Bash, Glob, Grep
5
+ ---
6
+
7
+ # Source Monitor Engine Tests
8
+
9
+ ## Quick Start
10
+
11
+ ```ruby
12
+ # Minimal test file
13
+ require "test_helper"
14
+
15
+ module SourceMonitor
16
+ class MyFeatureTest < ActiveSupport::TestCase
17
+ test "does something" do
18
+ source = create_source!(name: "Test", feed_url: "https://example.com/feed.xml")
19
+ assert source.persisted?
20
+ end
21
+ end
22
+ end
23
+ ```
24
+
25
+ ## Critical: Parallel Worker Caveat
26
+
27
+ **Single test files MUST use `PARALLEL_WORKERS=1`** due to a PG fork segfault bug (Ruby 3.4+ / pg 1.6+ / fork-based parallelism).
28
+
29
+ ```bash
30
+ # Single file — REQUIRED
31
+ PARALLEL_WORKERS=1 bin/rails test test/models/source_monitor/source_test.rb
32
+
33
+ # Full suite — works fine (handles PG connections properly)
34
+ bin/rails test
35
+
36
+ # Coverage mode — automatically uses threads, not forks
37
+ COVERAGE=1 PARALLEL_WORKERS=1 bin/rails test
38
+ ```
39
+
40
+ ## Test Helpers
41
+
42
+ All helpers are available in every test via `ActiveSupport::TestCase`.
43
+
44
+ | Helper | Purpose | Defined In |
45
+ |--------|---------|------------|
46
+ | `create_source!(attrs)` | Factory for Source records | `test/test_helper.rb` |
47
+ | `with_queue_adapter(adapter)` | Temporarily swap ActiveJob adapter | `test/test_helper.rb` |
48
+ | `with_inline_jobs { }` | Execute jobs inline in a block | `test/test_prof.rb` |
49
+ | `setup_once { }` | TestProf `before_all` for expensive setup | `test/test_prof.rb` |
50
+ | `clean_source_monitor_tables!` | Delete all engine records (FK-safe order) | `test/test_helper.rb` |
51
+ | `SourceMonitor.reset_configuration!` | Reset config to defaults (runs in setup) | automatic |
52
+
53
+ ## Configuration Reset
54
+
55
+ Every test automatically calls `SourceMonitor.reset_configuration!` in setup. If you modify config in a test, it will be reset before the next test.
56
+
57
+ ```ruby
58
+ test "custom config behavior" do
59
+ SourceMonitor.configure do |config|
60
+ config.fetching.min_interval_minutes = 10
61
+ end
62
+
63
+ # Test with custom config...
64
+ assert_equal 10, SourceMonitor.config.fetching.min_interval_minutes
65
+ end
66
+ # Config is automatically reset after this test
67
+ ```
68
+
69
+ For tests that explicitly need teardown:
70
+
71
+ ```ruby
72
+ setup do
73
+ SourceMonitor.reset_configuration!
74
+ end
75
+
76
+ teardown do
77
+ SourceMonitor.reset_configuration!
78
+ end
79
+ ```
80
+
81
+ ## Factory Helper: create_source!
82
+
83
+ Creates a `SourceMonitor::Source` record, bypassing validations for speed.
84
+
85
+ ```ruby
86
+ # Defaults
87
+ source = create_source!
88
+ # => name: "Test Source"
89
+ # => feed_url: "https://example.com/feed-<random>.xml"
90
+ # => website_url: "https://example.com"
91
+ # => fetch_interval_minutes: 60
92
+ # => scraper_adapter: "readability"
93
+
94
+ # Override any attribute
95
+ source = create_source!(
96
+ name: "Custom Source",
97
+ feed_url: "https://custom.example.com/feed.xml",
98
+ active: false,
99
+ adaptive_fetching_enabled: false,
100
+ fetch_interval_minutes: 120
101
+ )
102
+ ```
103
+
104
+ **Important:** `create_source!` uses `save!(validate: false)` so it skips model validations. This is intentional for test speed but means you can create records that would fail validation.
105
+
106
+ ## WebMock Setup
107
+
108
+ WebMock is configured globally to block all external HTTP requests except localhost.
109
+
110
+ ```ruby
111
+ # Stub a request
112
+ stub_request(:get, "https://example.com/feed.xml")
113
+ .to_return(
114
+ status: 200,
115
+ body: File.read(file_fixture("feeds/rss_sample.xml")),
116
+ headers: { "Content-Type" => "application/rss+xml" }
117
+ )
118
+
119
+ # Stub with specific headers
120
+ stub_request(:get, url)
121
+ .with(headers: { "If-None-Match" => '"etag123"' })
122
+ .to_return(status: 304, headers: { "ETag" => '"etag123"' })
123
+
124
+ # Stub a timeout
125
+ stub_request(:get, url).to_raise(Faraday::TimeoutError.new("execution expired"))
126
+
127
+ # Stub a connection failure
128
+ stub_request(:get, url).to_raise(Faraday::ConnectionFailed.new("connection refused"))
129
+ ```
130
+
131
+ ## VCR Cassettes
132
+
133
+ VCR is configured to hook into WebMock. Cassettes are stored in `test/vcr_cassettes/`.
134
+
135
+ ```ruby
136
+ test "fetches feed" do
137
+ source = create_source!(feed_url: "https://www.ruby-lang.org/en/feeds/news.rss")
138
+
139
+ VCR.use_cassette("source_monitor/fetching/rss_success") do
140
+ result = FeedFetcher.new(source: source, jitter: ->(_) { 0 }).call
141
+ assert_equal :fetched, result.status
142
+ end
143
+ end
144
+ ```
145
+
146
+ **Naming convention:** `source_monitor/<module>/<descriptor>` (e.g., `source_monitor/fetching/rss_success`).
147
+
148
+ ## Test Isolation
149
+
150
+ ### Scope Queries to Specific Records
151
+
152
+ Parallel tests share the database. Always scope assertions to records you created:
153
+
154
+ ```ruby
155
+ # GOOD - scoped to specific source
156
+ assert_equal 3, SourceMonitor::Item.where(source: source).count
157
+
158
+ # BAD - counts all items across parallel tests
159
+ assert_equal 3, SourceMonitor::Item.count
160
+ ```
161
+
162
+ ### Use Unique Feed URLs
163
+
164
+ `create_source!` generates random feed URLs by default. When you need a specific URL, make it unique:
165
+
166
+ ```ruby
167
+ source = create_source!(feed_url: "https://example.com/feed-#{SecureRandom.hex(4)}.xml")
168
+ ```
169
+
170
+ ### Clean Tables When Needed
171
+
172
+ For tests that need a blank-slate database:
173
+
174
+ ```ruby
175
+ setup do
176
+ clean_source_monitor_tables!
177
+ end
178
+ ```
179
+
180
+ The cleanup order respects foreign keys: LogEntry > ScrapeLog > FetchLog > HealthCheckLog > ItemContent > Item > Source.
181
+
182
+ ## Job Testing
183
+
184
+ ```ruby
185
+ # Default adapter is :test (jobs are enqueued but not performed)
186
+ test "enqueues fetch job" do
187
+ source = create_source!
188
+ assert_enqueued_with(job: SourceMonitor::FetchSourceJob, args: [source]) do
189
+ source.enqueue_fetch!
190
+ end
191
+ end
192
+
193
+ # Execute jobs inline for integration tests
194
+ test "performs fetch end-to-end" do
195
+ with_inline_jobs do
196
+ # Jobs execute immediately when enqueued
197
+ end
198
+ end
199
+
200
+ # Switch to a specific adapter temporarily
201
+ test "with async adapter" do
202
+ with_queue_adapter(:async) do
203
+ # ...
204
+ end
205
+ end
206
+ ```
207
+
208
+ ## Test Types
209
+
210
+ | Type | Base Class | Location |
211
+ |------|-----------|----------|
212
+ | Model | `ActiveSupport::TestCase` | `test/models/source_monitor/` |
213
+ | Controller | `ActionDispatch::IntegrationTest` | `test/controllers/source_monitor/` |
214
+ | Library | `ActiveSupport::TestCase` | `test/lib/source_monitor/` |
215
+ | Generator | `Rails::Generators::TestCase` | `test/lib/generators/` |
216
+
217
+ ## Common Patterns
218
+
219
+ ### Testing with Notifications
220
+
221
+ ```ruby
222
+ test "emits fetch finish event" do
223
+ payloads = []
224
+ ActiveSupport::Notifications.subscribed(
225
+ ->(_name, _start, _finish, _id, payload) { payloads << payload },
226
+ "source_monitor.fetch.finish"
227
+ ) do
228
+ FeedFetcher.new(source: source, jitter: ->(_) { 0 }).call
229
+ end
230
+
231
+ assert payloads.last[:success]
232
+ end
233
+ ```
234
+
235
+ ### Testing with Time Travel
236
+
237
+ ```ruby
238
+ test "schedules next fetch" do
239
+ travel_to Time.zone.parse("2024-01-01 10:00:00 UTC")
240
+
241
+ # ... test logic ...
242
+
243
+ assert_equal Time.current + 45.minutes, source.next_fetch_at
244
+ ensure
245
+ travel_back
246
+ end
247
+ ```
248
+
249
+ ### Private Method Testing (via send)
250
+
251
+ ```ruby
252
+ test "jitter_offset returns zero for zero interval" do
253
+ fetcher = FeedFetcher.new(source: source)
254
+ assert_equal 0, fetcher.send(:jitter_offset, 0)
255
+ end
256
+ ```
257
+
258
+ ## Running Tests
259
+
260
+ ```bash
261
+ # Full suite
262
+ bin/rails test
263
+
264
+ # Single file (MUST use PARALLEL_WORKERS=1)
265
+ PARALLEL_WORKERS=1 bin/rails test test/models/source_monitor/source_test.rb
266
+
267
+ # Verbose output
268
+ PARALLEL_WORKERS=1 bin/rails test test/models/source_monitor/source_test.rb --verbose
269
+
270
+ # Single test by name
271
+ PARALLEL_WORKERS=1 bin/rails test test/models/source_monitor/source_test.rb -n "test_is_valid_with_minimal_attributes"
272
+
273
+ # Coverage report
274
+ COVERAGE=1 PARALLEL_WORKERS=1 bin/rails test
275
+
276
+ # Random seed subset (via TestProf)
277
+ SAMPLE=10 bin/rails test
278
+ ```
279
+
280
+ ## File Fixtures
281
+
282
+ Test fixtures (feed XML files, etc.) are in `test/fixtures/`:
283
+
284
+ ```ruby
285
+ body = File.read(file_fixture("feeds/rss_sample.xml"))
286
+ ```
287
+
288
+ ## Testing Checklist
289
+
290
+ - [ ] Test file requires `"test_helper"`
291
+ - [ ] Tests wrapped in `module SourceMonitor` namespace
292
+ - [ ] Queries scoped to specific test records (not global counts)
293
+ - [ ] Feed URLs are unique per test
294
+ - [ ] `PARALLEL_WORKERS=1` used when running single files
295
+ - [ ] WebMock stubs or VCR cassettes for any HTTP calls
296
+ - [ ] `SourceMonitor.reset_configuration!` used if config is modified
297
+ - [ ] `travel_back` called in `ensure` when using `travel_to`
298
+
299
+ ## References
300
+
301
+ - [reference/test-helpers.md](reference/test-helpers.md) -- Detailed helper documentation
302
+ - [reference/test-patterns.md](reference/test-patterns.md) -- VCR, WebMock, and isolation patterns
@@ -0,0 +1,259 @@
1
+ # Test Helpers Reference
2
+
3
+ ## create_source!(attributes = {})
4
+
5
+ **File:** `test/test_helper.rb:97-109`
6
+
7
+ Creates a `SourceMonitor::Source` record with sensible defaults, bypassing model validations.
8
+
9
+ ### Default Values
10
+
11
+ | Attribute | Default |
12
+ |-----------|---------|
13
+ | `name` | `"Test Source"` |
14
+ | `feed_url` | `"https://example.com/feed-<random_hex>.xml"` |
15
+ | `website_url` | `"https://example.com"` |
16
+ | `fetch_interval_minutes` | `60` |
17
+ | `scraper_adapter` | `"readability"` |
18
+
19
+ ### Usage
20
+
21
+ ```ruby
22
+ # All defaults
23
+ source = create_source!
24
+
25
+ # Override specific attributes
26
+ source = create_source!(
27
+ name: "My Feed",
28
+ feed_url: "https://example.com/specific-feed.xml",
29
+ active: false,
30
+ adaptive_fetching_enabled: true,
31
+ fetch_interval_minutes: 120,
32
+ scraping_enabled: true,
33
+ auto_scrape: true,
34
+ custom_headers: { "X-Api-Key" => "secret123" },
35
+ metadata: { "category" => "tech" }
36
+ )
37
+ ```
38
+
39
+ ### Implementation Detail
40
+
41
+ Uses `save!(validate: false)` intentionally. This means:
42
+ - Records skip URL normalization that happens during validation
43
+ - Records with duplicate feed_urls can be created
44
+ - Invalid data can be inserted (useful for edge case testing)
45
+
46
+ ### Creating Related Records
47
+
48
+ ```ruby
49
+ source = create_source!
50
+
51
+ # Items
52
+ item = source.items.create!(
53
+ guid: "guid-1",
54
+ title: "Item Title",
55
+ url: "https://example.com/1",
56
+ published_at: Time.current
57
+ )
58
+
59
+ # Fetch logs
60
+ source.fetch_logs.create!(
61
+ success: true,
62
+ started_at: Time.current,
63
+ completed_at: Time.current,
64
+ items_created: 1
65
+ )
66
+
67
+ # Scrape logs
68
+ source.scrape_logs.create!(
69
+ item: item,
70
+ success: true,
71
+ started_at: Time.current,
72
+ completed_at: Time.current,
73
+ scraper_adapter: "readability"
74
+ )
75
+ ```
76
+
77
+ ---
78
+
79
+ ## with_queue_adapter(adapter)
80
+
81
+ **File:** `test/test_helper.rb:111-117`
82
+
83
+ Temporarily swaps the ActiveJob queue adapter for the duration of a block.
84
+
85
+ ### Usage
86
+
87
+ ```ruby
88
+ test "enqueues with inline adapter" do
89
+ with_queue_adapter(:inline) do
90
+ # Jobs execute immediately
91
+ source.enqueue_fetch!
92
+ end
93
+ end
94
+
95
+ test "with test adapter" do
96
+ with_queue_adapter(:test) do
97
+ assert_enqueued_with(job: FetchSourceJob) do
98
+ source.enqueue_fetch!
99
+ end
100
+ end
101
+ end
102
+ ```
103
+
104
+ ### Behavior
105
+
106
+ - Saves current adapter
107
+ - Sets new adapter
108
+ - Yields to block
109
+ - Restores previous adapter in `ensure` (always runs, even on exception)
110
+
111
+ ---
112
+
113
+ ## with_inline_jobs
114
+
115
+ **File:** `test/test_prof.rb:24-29`
116
+
117
+ Convenience wrapper around `with_queue_adapter(:inline)`.
118
+
119
+ ### Usage
120
+
121
+ ```ruby
122
+ test "performs complete fetch pipeline" do
123
+ with_inline_jobs do
124
+ # All enqueued jobs execute immediately
125
+ SourceMonitor::FetchSourceJob.perform_later(source)
126
+ source.reload
127
+ assert source.last_fetched_at.present?
128
+ end
129
+ end
130
+ ```
131
+
132
+ ---
133
+
134
+ ## setup_once(setup_fixtures: false, &block)
135
+
136
+ **File:** `test/test_prof.rb:18-20`
137
+
138
+ Wraps TestProf's `before_all` for expensive setup that should run once per test class, not per test method.
139
+
140
+ ### Usage
141
+
142
+ ```ruby
143
+ class ExpensiveSetupTest < ActiveSupport::TestCase
144
+ setup_once do
145
+ @shared_source = create_source!(name: "Shared")
146
+ 3.times do |i|
147
+ @shared_source.items.create!(
148
+ guid: "item-#{i}",
149
+ url: "https://example.com/#{i}"
150
+ )
151
+ end
152
+ end
153
+
154
+ test "source has items" do
155
+ assert_equal 3, @shared_source.items.count
156
+ end
157
+
158
+ test "another test reuses same data" do
159
+ assert @shared_source.persisted?
160
+ end
161
+ end
162
+ ```
163
+
164
+ ### Caveats
165
+
166
+ - Data created in `setup_once` is rolled back after all tests in the class
167
+ - Use `setup_fixtures: true` if you need fixtures loaded in the `before_all` block
168
+ - Do NOT modify `setup_once` data in individual tests (it is shared across tests)
169
+
170
+ ---
171
+
172
+ ## clean_source_monitor_tables!
173
+
174
+ **File:** `test/test_helper.rb:85-93`
175
+
176
+ Deletes all records from engine tables in FK-safe order.
177
+
178
+ ### Deletion Order
179
+
180
+ 1. `SourceMonitor::LogEntry`
181
+ 2. `SourceMonitor::ScrapeLog`
182
+ 3. `SourceMonitor::FetchLog`
183
+ 4. `SourceMonitor::HealthCheckLog`
184
+ 5. `SourceMonitor::ItemContent`
185
+ 6. `SourceMonitor::Item`
186
+ 7. `SourceMonitor::Source`
187
+
188
+ ### Usage
189
+
190
+ ```ruby
191
+ setup do
192
+ clean_source_monitor_tables!
193
+ end
194
+ ```
195
+
196
+ ### When to Use
197
+
198
+ - Tests that assert global counts (e.g., `Source.count`)
199
+ - Tests that need no pre-existing data
200
+ - Tests that are sensitive to data created by other tests in parallel
201
+
202
+ ---
203
+
204
+ ## SourceMonitor.reset_configuration!
205
+
206
+ **Automatically called** in setup for every `ActiveSupport::TestCase`.
207
+
208
+ Resets the `SourceMonitor::Configuration` instance to default values. This means:
209
+
210
+ - All queue names/concurrency reset to defaults
211
+ - HTTP settings (timeouts, user agent) reset
212
+ - Fetching adaptive interval settings reset
213
+ - Health thresholds reset
214
+ - Retention settings reset
215
+ - Realtime adapter reset to `:solid_cable`
216
+ - Authentication handlers cleared
217
+ - Events callbacks cleared
218
+ - Scraping settings reset
219
+ - Scraper registry cleared
220
+ - Model definitions reset
221
+
222
+ ### Manual Usage
223
+
224
+ ```ruby
225
+ setup do
226
+ SourceMonitor.reset_configuration!
227
+ end
228
+
229
+ teardown do
230
+ SourceMonitor.reset_configuration!
231
+ end
232
+ ```
233
+
234
+ ---
235
+
236
+ ## Parallelization Configuration
237
+
238
+ **File:** `test/test_helper.rb:68-77`
239
+
240
+ ```ruby
241
+ # With COVERAGE env var: single-threaded
242
+ parallelize(workers: 1, with: :threads)
243
+
244
+ # Without COVERAGE: uses system CPU count (fork-based)
245
+ parallelize(workers: :number_of_processors)
246
+
247
+ # Override with SOURCE_MONITOR_TEST_WORKERS env var
248
+ SOURCE_MONITOR_TEST_WORKERS=4 bin/rails test
249
+ ```
250
+
251
+ ### Environment Variables
252
+
253
+ | Variable | Effect |
254
+ |----------|--------|
255
+ | `COVERAGE` | Forces `workers: 1` with threads |
256
+ | `SOURCE_MONITOR_TEST_WORKERS` | Override worker count |
257
+ | `PARALLEL_WORKERS` | Rails built-in parallelism control |
258
+ | `SAMPLE` | TestProf: run random subset of tests |
259
+ | `SAMPLE_GROUPS` | TestProf: run random subset of test groups |