Files
felhom-controller/controller/internal/web/observability_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

122 lines
4.3 KiB
Go

package web
import (
"context"
"encoding/json"
"log"
"net/http"
"net/http/httptest"
"strings"
"testing"
"gitea.dooplex.hu/admin/felhom-controller/internal/agentapi"
)
// S6 old-agent degradation: the Debug page agent tab against a pre-0.83 agent (typed
// 404 — the StatusError precedent) renders the Hungarian notice, ok:true, no error
// spam. Companion red-proof: match the error by string instead of errors.As →
// unsupported turns into an error payload → the notice assertion fails.
func TestDebugAgentLogs_Pre083AgentRendersNotice(t *testing.T) {
s := testServer(t)
s.agentLogsFn = func(context.Context) (agentapi.AgentLogsResponse, error) {
return agentapi.AgentLogsResponse{}, &agentapi.StatusError{Path: "/debug/logs", Code: http.StatusNotFound}
}
w := httptest.NewRecorder()
s.debugAgentLogs(w, httptest.NewRequest("GET", "/api/debug/agent-logs", nil))
var resp struct {
OK bool `json:"ok"`
Data struct {
Unsupported bool `json:"unsupported"`
Notice string `json:"notice"`
} `json:"data"`
}
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
t.Fatalf("decode: %v (%s)", err, w.Body.String())
}
if !resp.OK || !resp.Data.Unsupported {
t.Fatalf("resp = %+v, want ok+unsupported (not an error)", resp)
}
if !strings.Contains(resp.Data.Notice, "az ügynök következő frissítése után") {
t.Errorf("notice = %q, want the next-update text", resp.Data.Notice)
}
}
// A working ≥0.83 agent's entries are proxied through verbatim.
func TestDebugAgentLogs_ProxiesEntries(t *testing.T) {
s := testServer(t)
s.agentLogsFn = func(context.Context) (agentapi.AgentLogsResponse, error) {
return agentapi.AgentLogsResponse{
Entries: []agentapi.AgentLogEntry{{Level: "DEBUG", Message: "netverify: job started"}},
Total: 1,
}, nil
}
w := httptest.NewRecorder()
s.debugAgentLogs(w, httptest.NewRequest("GET", "/api/debug/agent-logs", nil))
body := w.Body.String()
if !strings.Contains(body, "netverify: job started") || !strings.Contains(body, `"total":1`) {
t.Errorf("proxy body missing agent entries: %s", body)
}
}
// A non-404 agent error (down, 5xx) is an honest error payload — never the notice.
func TestDebugAgentLogs_OtherErrorIsError(t *testing.T) {
s := testServer(t)
s.agentLogsFn = func(context.Context) (agentapi.AgentLogsResponse, error) {
return agentapi.AgentLogsResponse{}, &agentapi.StatusError{Path: "/debug/logs", Code: 502}
}
w := httptest.NewRecorder()
s.debugAgentLogs(w, httptest.NewRequest("GET", "/api/debug/agent-logs", nil))
if strings.Contains(w.Body.String(), "unsupported") || !strings.Contains(w.Body.String(), `"ok":false`) {
t.Errorf("non-404 must be an error, not the notice: %s", w.Body.String())
}
}
// S7 sweep smoke (controller half): a full fake NAS add at logging.level=info must
// leave the EXPECTED ORDERED phase lines in the debug ring — the test that encodes
// "an operator can reconstruct the NAS flow from the debug view". Companion
// red-proof: drop one asserted phase line (e.g. the probe verdict Debug) → FAIL
// naming the missing marker.
func TestNetAdd_LogSequenceReconstructsFlow(t *testing.T) {
s := testServer(t)
lb := NewLogBuffer(200)
// The production writer layout at logging.level=info: ring captures everything.
s.logger = log.New(lb, "", log.LstdFlags)
agent := &fakeNetAgent{addRes: okAddRes("media"), verify: agentapi.NetVerifyStatus{Phase: "done", JobID: "job-1"}}
s.netProbeFn = func(context.Context, string) probeOutcome { return probeOutcome{OK: true} }
if !s.startNetAdd(agent, netAddReq("media"), "NAS media") {
t.Fatal("startNetAdd refused")
}
job := waitNetAdd(t, s)
if job.Phase != netAddPhaseDone {
t.Fatalf("phase = %s, want done", job.Phase)
}
lines := lb.Lines(0)
sequence := []string{
`netstorage add "media" started`,
"agent add accepted",
"phase agent_add -> verifying",
"agent verify verdict: phase=done",
"phase verifying -> probing",
"probe verdict: ok=true",
"phase probing -> registering",
"network storage added + verified",
}
pos := -1
for _, marker := range sequence {
idx := -1
for i := pos + 1; i < len(lines); i++ {
if strings.Contains(lines[i], marker) {
idx = i
break
}
}
if idx < 0 {
t.Fatalf("phase line %q missing (or out of order) — flow not reconstructable.\nring:\n%s",
marker, strings.Join(lines, "\n"))
}
pos = idx
}
}