v0.83.0: observability pass — always-DEBUG capture ring + GET /debug/logs + heartbeat log-pull + gap-fill sweep
Capture layer: applog.New returns (logger, Ring) — slog fan-out, stderr at the configured level, ~1000-entry ring fixed at LevelDebug (remote diagnostics without a config flip). GET /debug/logs (token-authed, ?raw=1) + request-level DEBUG middleware. Heartbeat log-pull mirrors the report logtail pattern: envelope log_tail_requested -> next heartbeat carries log_tail (128KB cap, consume-once, failed-push retry proven). Gap-fill sweep over netverify/ netstorage/netmount/signedjobs/selfupdate/disks/controller-swap/desired/loop. Red-proofs: ring-at-emit-level FAILs capture test; drain removed FAILs consume-once; dropped phase line FAILs the S7 log-sequence smoke. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
package localapi
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
applog "gitea.dooplex.hu/admin/felhom-agent/internal/log"
|
||||
)
|
||||
|
||||
// S7 sweep smoke (agent half): a full fake NAS add at emit level INFO must leave the
|
||||
// EXPECTED LOG SEQUENCE in the debug ring — this is the test that encodes "an operator
|
||||
// can reconstruct the NAS flow from the debug view". Companion red-proof: remove any
|
||||
// one of the asserted phase lines (e.g. the /proc/mounts verdict Debug) → its marker
|
||||
// is absent → FAIL naming the missing phase.
|
||||
func TestNetAdd_LogSequenceReconstructsFlow(t *testing.T) {
|
||||
logger, ring := applog.NewWithWriter(&bytes.Buffer{}, "info", 200)
|
||||
n := &fakeNetOps{}
|
||||
srv := newVerifyServer(t, n, t.TempDir(), verifySeams{
|
||||
mounted: func(string) bool { return true }, // §8: in /proc/mounts ⇒ verified
|
||||
})
|
||||
srv.logger = logger // ring-backed capture layer under the whole flow
|
||||
|
||||
body := `{"name":"vids","protocol":"smb","server":"nas","export":"vids","mapped_uid":1000,"mapped_gid":1000,"username":"u","password":"p"}`
|
||||
if w := do(t, srv.Handler(), "POST", "/netstorage/add", "A", body); w.Code != http.StatusOK {
|
||||
t.Fatalf("add: got %d (%s)", w.Code, w.Body.String())
|
||||
}
|
||||
final := pollVerify(t, srv.Handler())
|
||||
if final["phase"] != netVerifyPhaseDone {
|
||||
t.Fatalf("phase = %v, want done", final["phase"])
|
||||
}
|
||||
|
||||
lines := ring.Lines(0)
|
||||
joined := strings.Join(lines, "\n")
|
||||
// The phase markers, in order (each must exist; each must come after the previous).
|
||||
sequence := []string{
|
||||
"NAS endpoint pre-probe passed",
|
||||
"SMB credentials staged",
|
||||
"network mount installed",
|
||||
"netverify: job started",
|
||||
"netverify: /proc/mounts verdict",
|
||||
"netverify: mount verified",
|
||||
}
|
||||
pos := -1
|
||||
for _, marker := range sequence {
|
||||
idx := indexOfLine(lines, marker, pos+1)
|
||||
if idx < 0 {
|
||||
t.Fatalf("phase line %q missing (or out of order) — flow not reconstructable.\nring:\n%s", marker, joined)
|
||||
}
|
||||
pos = idx
|
||||
}
|
||||
// The secret never appears in any line (creds are path-only).
|
||||
if strings.Contains(joined, "password") || strings.Contains(joined, `"p"`) {
|
||||
t.Errorf("a credential-looking token leaked into the log ring:\n%s", joined)
|
||||
}
|
||||
}
|
||||
|
||||
// indexOfLine finds the first line at or after `from` containing marker; -1 if none.
|
||||
func indexOfLine(lines []string, marker string, from int) int {
|
||||
for i := from; i < len(lines); i++ {
|
||||
if strings.Contains(lines[i], marker) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
Reference in New Issue
Block a user