cb692f8788
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
68 lines
2.4 KiB
Go
68 lines
2.4 KiB
Go
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
|
|
}
|