Files
felhom-controller/controller/cmd/controller/upwatch_wiring_test.go
T
admin 046b808792
gates / gates (push) Successful in 1m22s
upwatch: hold a seeding torrent app for one line test, then put its limit back exactly (R-1250)
When the agent waits for quiet and qBittorrent or Transmission runs: record the limit first,
hold at 1 KiB/s, ask the agent for a test on request, put the old limit back (deferred; a
household change during the test stays). Start-up puts back a limit a killed controller left;
the 50 % rule skips while a hold record exists. Commit only.
2026-10-24 13:19:07 +02:00

103 lines
4.6 KiB
Go

package main
import (
"io"
"log"
"os"
"path/filepath"
"strings"
"testing"
"gitea.dooplex.hu/admin/felhom-controller/internal/notify"
"gitea.dooplex.hu/admin/felhom-controller/internal/settings"
"gitea.dooplex.hu/admin/felhom-controller/internal/upwatch"
)
// The upload watch is built, started, handed to the off-site push BEFORE the scheduler can start one, and its view
// handed to the pages — a seam built and never wired is this project's commonest defect.
// RED-PROOF: delete the SetOffboxPushObserver line → fails; move it below the scheduler's start → fails; pass nil
// instead of agentLineTest(...) → the qBittorrent limit never gets a tested line → fails.
func TestUpwatchIsWiredAtStartup(t *testing.T) {
lines, _, _ := slice4CallLines(t)
for _, fn := range []string{"newUpwatch", "agentLineTest", "SetOffboxPushObserver", "startUpwatch", "SetUpwatchView"} {
if len(lines[fn]) == 0 {
t.Fatalf("main.go never calls %s", fn)
}
}
if lines["SetOffboxPushObserver"][0] > lines["Daily"][0] {
t.Fatalf("SetOffboxPushObserver (line %d) comes after the first scheduled job (line %d)", lines["SetOffboxPushObserver"][0], lines["Daily"][0])
}
}
// newUpwatch asks the three apps the spike proved, reports through the notifier, and reads exact counters.
// RED-PROOF: drop the transmission entry → fails.
func TestNewUpwatch_ExplainersAndEmit(t *testing.T) {
lg := log.New(io.Discard, "", 0)
sett, err := settings.Load(filepath.Join(t.TempDir(), "settings.json"), lg)
if err != nil {
t.Fatal(err)
}
svc := newUpwatch(sett, nil, nil, nil, notify.New("", "", "", sett, lg, false), agentLine{}, lg)
for _, a := range []string{"qbittorrent", "transmission", "plex"} {
if _, ok := svc.Watcher.Explainers[a]; !ok {
t.Errorf("no explainer for %s", a)
}
}
if svc.Watcher.Emit == nil || svc.Watcher.Line == nil || svc.QB == nil || svc.QBShare != 0.5 {
t.Fatalf("service not wired: %+v", svc)
}
if _, ok := svc.Watcher.Source.(*upwatch.DockerAPI); !ok {
t.Fatal("the watch must read the Engine API's exact counters, not docker stats' text")
}
// R-1250: both torrent apps can be held, qBittorrent first; without a local API there is no hold at all.
if len(svc.Torrents) != 2 || svc.Torrents[0].Stack() != "qbittorrent" || svc.Torrents[1].Stack() != "transmission" ||
svc.TorrentRunning == nil || svc.LineWaiting != nil || svc.LineRun != nil {
t.Fatalf("line-test hold not wired: %d torrents, waiting=%v run=%v", len(svc.Torrents), svc.LineWaiting != nil, svc.LineRun != nil)
}
if svc.Watcher.Baseline != nil {
t.Fatal("no metrics store, yet a baseline: an app with no explainer would be judged without history")
}
}
// R-1250: the start-up check puts a held limit back BEFORE the first rule reads it, and every tick runs the hold
// before the 50 % rule. Asserted on startUpwatch's source order.
// RED-PROOF: move svc.RecoverHold below the ticker, or drop SeedLineTestNow → fails.
func TestStartUpwatchRecoversTheHoldFirst(t *testing.T) {
src, err := os.ReadFile("upwatch_wiring.go")
if err != nil {
t.Fatal(err)
}
body := string(src)
i := strings.Index(body, "func startUpwatch(")
if i < 0 {
t.Fatal("no startUpwatch")
}
body = body[i:]
rec, ticker := strings.Index(body, "svc.RecoverHold(ctx)"), strings.Index(body, "time.NewTicker(")
seed, rule := strings.Index(body, "svc.SeedLineTestNow(ctx)"), strings.Index(body, "svc.QBLimitNow(ctx)")
if rec < 0 || rec > ticker {
t.Fatalf("RecoverHold at %d, the ticker at %d: a held limit must go back before anything else runs", rec, ticker)
}
if seed < 0 || seed > rule {
t.Fatalf("SeedLineTestNow at %d, QBLimitNow at %d: the hold must run before the 50 %% rule", seed, rule)
}
}
// With a local API, agentLineTest yields all three halves, and newUpwatch hands the hold both agent calls.
// RED-PROOF: drop LineRun from newUpwatch's Service, or Run from agentLineTest → fails.
func TestUpwatchHoldGetsBothAgentCalls(t *testing.T) {
lg := log.New(io.Discard, "", 0)
line := agentLineTest("127.0.0.1:1", "token-canary", strings.Repeat("ab", 32), lg)
if line.Measured == nil || line.Waiting == nil || line.Run == nil {
t.Fatalf("agentLineTest with a local API: measured=%v waiting=%v run=%v", line.Measured != nil, line.Waiting != nil, line.Run != nil)
}
sett, err := settings.Load(filepath.Join(t.TempDir(), "settings.json"), lg)
if err != nil {
t.Fatal(err)
}
svc := newUpwatch(sett, nil, nil, nil, notify.New("", "", "", sett, lg, false), line, lg)
if svc.Measured == nil || svc.LineWaiting == nil || svc.LineRun == nil {
t.Fatalf("newUpwatch dropped an agent call: measured=%v waiting=%v run=%v", svc.Measured != nil, svc.LineWaiting != nil, svc.LineRun != nil)
}
}