26a43708b7
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
122 lines
4.3 KiB
Go
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
|
|
}
|
|
}
|