Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- Service class unit tests — model specs for `CableStats`, `CableTimeline`, `CacheStats`, `CacheSizeStats`, `CacheTimeline`, and `QueueStats`; covers zero/empty states, correct aggregation, time-window filtering, and the `slow_job_threshold` conditional
- Pagination boundary tests — `describe "pagination"` blocks added to jobs, failed jobs, cache entries, history, and cable message list specs; covers pagination controls appearing at 26+ records, page 2 returning 200, and out-of-range pages returning 200 without error
- `spec/support/request_helpers.rb` — shared `engine_root` let for all request specs; `rails_helper.rb` auto-loads `spec/support/**/*.rb`

Expand Down
7 changes: 2 additions & 5 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,9 @@ The path to v1.0.0 is staged: first achieve feature parity with `solid_queue_das

---

## v0.8.0 — Test Coverage
## v0.8.0 — Test Coverage

> _Make the test suite match the surface area of the engine._

### Remaining
- Service class unit tests — `CableStats`, `CacheStats`, `QueueStats`, `CableTimeline`, `CacheSizeStats`, `CacheTimeline` have no specs; `AlertWebhook`, `QueueDepthSparkline`, `ThroughputSparkline` are already covered
> All items complete — 315 examples, 99.36% line coverage.

---

Expand Down
59 changes: 59 additions & 0 deletions spec/models/solid_stack_web/cable_stats_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
require "rails_helper"

RSpec.describe SolidStackWeb::CableStats do
def broadcast(channel, payload = "msg")
SolidCable::Message.broadcast(channel, payload)
end

describe "#to_h" do
it "returns zeros and nil when there are no messages" do
stats = described_class.new.to_h
expect(stats[:messages]).to eq(0)
expect(stats[:channels]).to eq(0)
expect(stats[:messages_per_hour]).to eq(0)
expect(stats[:oldest_message]).to be_nil
expect(stats[:top_channels]).to be_empty
end

it "counts total messages" do
broadcast("chat", "hello")
broadcast("chat", "world")
expect(described_class.new.to_h[:messages]).to eq(2)
end

it "counts distinct channels" do
broadcast("chat", "hi")
broadcast("sports", "goal")
broadcast("sports", "offside")
expect(described_class.new.to_h[:channels]).to eq(2)
end

it "counts messages sent in the last hour" do
broadcast("chat", "recent")
old = SolidCable::Message.last
old.update_columns(created_at: 2.hours.ago)
broadcast("chat", "new")
expect(described_class.new.to_h[:messages_per_hour]).to eq(1)
end

it "returns the oldest message timestamp" do
travel_to 10.minutes.ago do
broadcast("chat", "first")
end
broadcast("chat", "second")
oldest = SolidCable::Message.minimum(:created_at)
expect(described_class.new.to_h[:oldest_message]).to be_within(1.second).of(oldest)
end

it "returns the top 3 channels by message volume" do
3.times { broadcast("alpha", "m") }
2.times { broadcast("beta", "m") }
1.times { broadcast("gamma", "m") }
broadcast("delta", "m")

top = described_class.new.to_h[:top_channels]
expect(top.keys.first).to eq("alpha")
expect(top.size).to be <= 3
end
end
end
49 changes: 49 additions & 0 deletions spec/models/solid_stack_web/cable_timeline_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
require "rails_helper"

RSpec.describe SolidStackWeb::CableTimeline do
def broadcast(channel, payload = "msg", created_at: Time.current)
SolidCable::Message.broadcast(channel, payload)
SolidCable::Message.order(created_at: :desc).first.tap do |m|
m.update_columns(created_at: created_at)
end
end

describe "#message_buckets" do
it "returns 24 buckets" do
expect(described_class.new.message_buckets.size).to eq(24)
end

it "returns all zeros when there are no messages" do
expect(described_class.new.message_buckets).to all(eq(0))
end

it "counts messages in the correct hour bucket" do
broadcast("chat", "hi", created_at: 1.hour.ago)
expect(described_class.new.message_buckets.sum).to eq(1)
end

it "excludes messages older than 24 hours" do
broadcast("chat", "old", created_at: 25.hours.ago)
expect(described_class.new.message_buckets).to all(eq(0))
end

it "places messages in separate buckets based on hour" do
broadcast("chat", "early", created_at: 5.hours.ago)
broadcast("chat", "recent", created_at: 1.hour.ago)
buckets = described_class.new.message_buckets
expect(buckets.count { |b| b > 0 }).to eq(2)
end
end

describe "#message_max" do
it "returns 0 when there are no messages" do
expect(described_class.new.message_max).to eq(0)
end

it "returns the highest bucket count" do
2.times { broadcast("chat", "m", created_at: 1.hour.ago) }
broadcast("sports", "g", created_at: 5.hours.ago)
expect(described_class.new.message_max).to eq(2)
end
end
end
52 changes: 52 additions & 0 deletions spec/models/solid_stack_web/cache_size_stats_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
require "rails_helper"

RSpec.describe SolidStackWeb::CacheSizeStats do
def create_entry(key:, value:)
SolidCache::Entry.write(key, value)
end

describe "#total" do
it "returns 0 when the cache is empty" do
expect(described_class.new.total).to eq(0)
end

it "returns the count of all entries" do
create_entry(key: "a", value: "x")
create_entry(key: "b", value: "y")
expect(described_class.new.total).to eq(2)
end
end

describe "#top_entries" do
it "returns entries ordered by byte_size descending" do
create_entry(key: "small", value: "x")
create_entry(key: "large", value: "x" * 500)
keys = described_class.new.top_entries.map(&:key)
expect(keys.first).to eq("large")
end

it "returns at most 10 entries" do
15.times { |i| create_entry(key: "key#{i}", value: "v") }
expect(described_class.new.top_entries.size).to eq(10)
end
end

describe "#buckets" do
it "returns one bucket per size range" do
expect(described_class.new.buckets.size).to eq(SolidStackWeb::CacheSizeStats::BUCKETS.size)
end

it "counts entries in the correct size bucket" do
create_entry(key: "tiny", value: "x")
buckets = described_class.new.buckets
small_bucket = buckets.find { |b| b[:label] == "< 1 KB" }
expect(small_bucket[:count]).to eq(1)
end

it "returns zero counts when the cache is empty" do
described_class.new.buckets.each do |b|
expect(b[:count]).to eq(0)
end
end
end
end
38 changes: 38 additions & 0 deletions spec/models/solid_stack_web/cache_stats_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
require "rails_helper"

RSpec.describe SolidStackWeb::CacheStats do
def create_entry(key: "k", value: "v")
SolidCache::Entry.write(key, value)
end

describe "#to_h" do
it "returns zeros and nil when the cache is empty" do
stats = described_class.new.to_h
expect(stats[:entries]).to eq(0)
expect(stats[:byte_size]).to eq(0)
expect(stats[:oldest_entry]).to be_nil
end

it "counts entries" do
create_entry(key: "a", value: "x")
create_entry(key: "b", value: "y")
expect(described_class.new.to_h[:entries]).to eq(2)
end

it "sums byte_size across all entries" do
create_entry(key: "small", value: "x")
create_entry(key: "large", value: "x" * 100)
total = SolidCache::Entry.sum(:byte_size)
expect(described_class.new.to_h[:byte_size]).to eq(total)
end

it "returns the oldest entry timestamp" do
travel_to 10.minutes.ago do
create_entry(key: "old", value: "o")
end
create_entry(key: "new", value: "n")
oldest = SolidCache::Entry.minimum(:created_at)
expect(described_class.new.to_h[:oldest_entry]).to be_within(1.second).of(oldest)
end
end
end
68 changes: 68 additions & 0 deletions spec/models/solid_stack_web/cache_timeline_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
require "rails_helper"

RSpec.describe SolidStackWeb::CacheTimeline do
def create_entry(key:, value:, created_at: Time.current)
SolidCache::Entry.write(key, value)
SolidCache::Entry.order(created_at: :desc).first.tap do |e|
e.update_columns(created_at: created_at)
end
end

describe "#entry_buckets" do
it "returns 24 buckets" do
expect(described_class.new.entry_buckets.size).to eq(24)
end

it "returns all zeros when the cache is empty" do
expect(described_class.new.entry_buckets).to all(eq(0))
end

it "counts entries created within the 24-hour window" do
create_entry(key: "recent", value: "v", created_at: 1.hour.ago)
expect(described_class.new.entry_buckets.sum).to eq(1)
end

it "excludes entries older than 24 hours" do
create_entry(key: "old", value: "v", created_at: 25.hours.ago)
expect(described_class.new.entry_buckets).to all(eq(0))
end
end

describe "#byte_buckets" do
it "returns 24 buckets" do
expect(described_class.new.byte_buckets.size).to eq(24)
end

it "returns all zeros when the cache is empty" do
expect(described_class.new.byte_buckets).to all(eq(0))
end

it "sums byte_size for entries in the window" do
create_entry(key: "data", value: "hello", created_at: 1.hour.ago)
expect(described_class.new.byte_buckets.sum).to be > 0
end
end

describe "#entry_max" do
it "returns 0 when the cache is empty" do
expect(described_class.new.entry_max).to eq(0)
end

it "returns the highest bucket count" do
2.times { |i| create_entry(key: "k#{i}", value: "v", created_at: 1.hour.ago) }
create_entry(key: "k2", value: "v", created_at: 5.hours.ago)
expect(described_class.new.entry_max).to eq(2)
end
end

describe "#byte_max" do
it "returns 0 when the cache is empty" do
expect(described_class.new.byte_max).to eq(0)
end

it "returns the highest byte sum across buckets" do
create_entry(key: "big", value: "x" * 100, created_at: 1.hour.ago)
expect(described_class.new.byte_max).to be > 0
end
end
end
91 changes: 91 additions & 0 deletions spec/models/solid_stack_web/queue_stats_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
require "rails_helper"

RSpec.describe SolidStackWeb::QueueStats do
def create_ready(queue_name: "default")
SolidQueue::Job.create!(class_name: "MyJob", queue_name:, priority: 0)
end

def create_failed
SolidQueue::Job.skip_callback(:create, :after, :prepare_for_execution)
job = SolidQueue::Job.create!(class_name: "FailingJob", queue_name: "default", priority: 0)
SolidQueue::FailedExecution.create!(
job: job,
error: { exception_class: "RuntimeError", message: "boom", backtrace: [] }
)
SolidQueue::Job.set_callback(:create, :after, :prepare_for_execution)
end

def create_process(heartbeat_at: Time.current)
SolidQueue::Process.create!(
kind: "Worker", name: "worker-#{SecureRandom.hex(4)}", pid: rand(99_999),
hostname: "test", last_heartbeat_at: heartbeat_at
)
end

def create_finished(finished_at: Time.current, duration: 10)
SolidQueue::Job.create!(
class_name: "FinishedJob", queue_name: "default", priority: 0,
created_at: finished_at - duration.seconds,
finished_at: finished_at
)
end

after { SolidStackWeb.slow_job_threshold = nil }

describe "#to_h" do
it "returns zero counts when the queue is empty" do
stats = described_class.new.to_h
expect(stats[:ready]).to eq(0)
expect(stats[:scheduled]).to eq(0)
expect(stats[:claimed]).to eq(0)
expect(stats[:blocked]).to eq(0)
expect(stats[:failed]).to eq(0)
end

it "counts ready executions" do
2.times { create_ready }
expect(described_class.new.to_h[:ready]).to eq(2)
end

it "counts failed executions" do
create_failed
expect(described_class.new.to_h[:failed]).to eq(1)
end

it "counts jobs finished in the last hour" do
create_finished(finished_at: 30.minutes.ago)
create_finished(finished_at: 2.hours.ago)
expect(described_class.new.to_h[:done_1h]).to eq(1)
end

it "counts jobs finished in the last 24 hours" do
create_finished(finished_at: 1.hour.ago)
create_finished(finished_at: 23.hours.ago)
create_finished(finished_at: 25.hours.ago)
expect(described_class.new.to_h[:done_24h]).to eq(2)
end

it "counts healthy processes (heartbeat within last 5 minutes)" do
create_process(heartbeat_at: 2.minutes.ago)
create_process(heartbeat_at: 10.minutes.ago)
expect(described_class.new.to_h[:processes_healthy]).to eq(1)
end

it "counts stale processes (heartbeat older than 5 minutes)" do
create_process(heartbeat_at: 2.minutes.ago)
create_process(heartbeat_at: 10.minutes.ago)
expect(described_class.new.to_h[:processes_stale]).to eq(1)
end

it "omits slow_jobs key when no threshold is configured" do
expect(described_class.new.to_h).not_to have_key(:slow_jobs)
end

it "counts slow jobs when threshold is configured" do
SolidStackWeb.slow_job_threshold = 30
create_finished(duration: 60)
create_finished(duration: 10)
expect(described_class.new.to_h[:slow_jobs]).to eq(1)
end
end
end