# frozen_string_literal: true require "rails_helper" RSpec.describe Streams::NightCloudSweeper do def with_env(vars) previous = vars.keys.index_with { |k| ENV[k] } vars.each { |k, v| ENV[k] = v } yield ensure previous.each { |k, v| v.nil? ? ENV.delete(k) : ENV[k] = v } end def cloud_node!(slug: "ingest-cloud-01") StreamNode.create!( slug: slug, hostname: "#{slug}.mltv-stream.net", role: "cloud", status: "ready", provider: "hetzner", provider_instance_id: "42", rtmp_base_url: "rtmp://#{slug}:1935", hls_base_url: "https://#{slug}/hls", api_base_url: "http://10.0.0.9:9997", max_publishers: 6, max_relays: 6, metadata: { "public_ip" => "1.2.3.4" } ) end before do StreamSession.where.not(stream_node_id: nil).update_all(stream_node_id: nil, status: "ended", ended_at: Time.current) StreamNode.where(role: "cloud").delete_all end it "skips outside quiet hours" do with_env( "STREAM_NIGHT_SWEEP_ENABLED" => "1", "STREAM_AUTOSCALE_QUIET_HOURS" => "02:00-07:00", "STREAM_AUTOSCALE_QUIET_TZ" => "Europe/Rome" ) do allow(Streams::QuietHours).to receive(:active?).and_return(false) cloud_node! result = described_class.sweep! expect(result.skipped).to eq(true) expect(StreamNode.where(role: "cloud").count).to eq(1) end end it "decommissions idle cloud nodes during quiet hours" do with_env("STREAM_NIGHT_SWEEP_ENABLED" => "1") do allow(Streams::QuietHours).to receive(:active?).and_return(true) node = cloud_node! provisioner = instance_double(Streams::NodeProvisioner) allow(provisioner).to receive(:drain!) do |n| n.update!(status: "draining") n end allow(provisioner).to receive(:decommission!) do |n| n.destroy! true end result = described_class.sweep!(provisioner: provisioner) expect(result.skipped).to eq(false) expect(result.actions).to include(:"decommission_#{node.slug}") expect(StreamNode.find_by(id: node.id)).to be_nil end end it "alerts and keeps nodes with active sessions" do with_env("STREAM_NIGHT_SWEEP_ENABLED" => "1") do allow(Streams::QuietHours).to receive(:active?).and_return(true) node = cloud_node! user = User.create!(email: "night@example.com", name: "N", password: "Password123", role: "coach") club = Club.create!(name: "Night", sport: "volleyball") team = club.teams.create!(name: "T", sport: "volleyball", slug: "night-t") match = team.matches.create!(opponent_name: "X", scheduled_at: 1.hour.from_now) StreamSession.create!(match: match, user: user, platform: "matchlivetv", status: "live", stream_node: node) allow(Ops::IncidentRecorder).to receive(:record) provisioner = instance_double(Streams::NodeProvisioner) expect(provisioner).not_to receive(:decommission!) result = described_class.sweep!(provisioner: provisioner) expect(result.actions).to include(:"alert_active_#{node.slug}") expect(Ops::IncidentRecorder).to have_received(:record).with(hash_including( fingerprint: "stream_overflow:night_active:#{node.slug}" )) expect(node.reload.status).to eq("ready") end end it "does not touch lab nodes" do with_env("STREAM_NIGHT_SWEEP_ENABLED" => "1") do allow(Streams::QuietHours).to receive(:active?).and_return(true) StreamNode.create!( slug: "ingest-lab-night-01", hostname: "lab-night.local", role: "lab", status: "ready", provider: "local", rtmp_base_url: "rtmp://lab:1935", hls_base_url: "https://lab/hls", api_base_url: "http://lab:9997", max_publishers: 2, max_relays: 2 ) result = described_class.sweep! expect(result.actions).to eq([]) expect(StreamNode.find_by(slug: "ingest-lab-night-01")).to be_present end end end