Files
felhom-controller/controller/internal/web/capture_layer_test.go
T
admin 26a43708b7 v0.116.0: observability pass — always-on debug ring + leveled sweep + agent tab + self-log pull — MinAgent: 0.81.0
Capture layer: LogBuffer always exists; logger = MultiWriter(LevelFilterWriter
(stdout, logging.level), ring) so DEBUG detail exists remotely without a config
flip while docker logs keep respecting the level. New internal/logx leveled
helpers. Report ACK gains controller_log_requested (additive); next report
ships controller_log_tail (128KB, consume-once, app-tail wire byte-compatible).
Debug page: Vezérlő|Ügynök tabs; agent tab proxies agent /debug/logs with the
pre-0.83 notice on typed 404. Sweep: netstorage_job phases, netprobe, handler
validation refusals + orphan WARN, SupportsWithSource gate line, agentapi
per-call DEBUG, migrate phase lines, tier2/offbox unswallowed persists.
Red-proofs: filter-disabled, drain-removed, dropped-phase-line all FAIL.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
2026-07-11 16:45:57 +02:00

83 lines
2.9 KiB
Go

package web
import (
"bytes"
"io"
"log"
"strings"
"testing"
"time"
)
// S1 capture-at-info (controller half): with the v0.116.0 writer layout —
// MultiWriter(LevelFilterWriter(stdout, "info"), LogBuffer) — a [DEBUG] line
// reaches the RING and is ABSENT from stdout, while [INFO] reaches both.
// Companion red-proof: gate the ring behind the same filter (the pre-fix shape,
// where the ring only existed at logging.level=debug) → the ring assertion fails.
func TestCaptureAtInfo_RingHoldsDebugStdoutDoesNot(t *testing.T) {
var stdout bytes.Buffer
lb := NewLogBuffer(50)
logger := log.New(io.MultiWriter(NewLevelFilterWriter(&stdout, "info"), lb), "", log.LstdFlags)
logger.Printf("[DEBUG] [web] netstorage add \"vids\" phase agent_add -> verifying")
logger.Printf("[INFO] [web] network storage added + verified: vids")
entries, total := lb.Entries("DEBUG", 0, time.Time{})
if total != 2 || len(entries) != 2 {
t.Fatalf("ring holds %d entries (returned %d), want 2 — DEBUG must be captured at logging.level=info", total, len(entries))
}
if entries[0].Level != "DEBUG" || !strings.Contains(entries[0].Message, "phase agent_add") {
t.Errorf("ring entry 0 = %+v, want the DEBUG phase line", entries[0])
}
if strings.Contains(stdout.String(), "phase agent_add") {
t.Errorf("stdout carries the DEBUG line at level info:\n%s", stdout.String())
}
if !strings.Contains(stdout.String(), "added + verified") {
t.Errorf("stdout missing the INFO line:\n%s", stdout.String())
}
}
// The filter respects higher minimums too (warn drops INFO) and passes untagged lines at info.
func TestLevelFilterWriter_Thresholds(t *testing.T) {
cases := []struct {
min string
line string
wants bool
}{
{"info", "[DEBUG] x", false},
{"info", "[INFO] x", true},
{"info", "untagged line", true}, // parses as INFO
{"warn", "[INFO] x", false},
{"warn", "[ERROR] x", true},
{"debug", "[DEBUG] x", true},
}
for _, c := range cases {
var out bytes.Buffer
logger := log.New(NewLevelFilterWriter(&out, c.min), "", log.LstdFlags)
logger.Printf("%s", c.line)
got := strings.Contains(out.String(), c.line)
if got != c.wants {
t.Errorf("min=%s line=%q passed=%v want %v", c.min, c.line, got, c.wants)
}
}
}
// Lines renders chronological plain-text lines and honors the byte budget by
// keeping the NEWEST lines (the controller_log_tail 128 KB cap).
func TestLogBufferLines_ByteBudgetKeepsNewest(t *testing.T) {
lb := NewLogBuffer(10)
logger := log.New(lb, "", log.LstdFlags)
for i := 0; i < 5; i++ {
logger.Printf("[INFO] line-%d %s", i, strings.Repeat("x", 80))
}
all := lb.Lines(0)
if len(all) != 5 || !strings.Contains(all[4], "line-4") {
t.Fatalf("uncapped lines wrong: %d %v", len(all), all)
}
budget := len(all[3]) + len(all[4]) + 2
capped := lb.Lines(budget)
if len(capped) >= 5 || !strings.Contains(capped[len(capped)-1], "line-4") {
t.Errorf("byte cap kept %d lines, newest=%q — must drop oldest first", len(capped), capped[len(capped)-1])
}
}