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
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
package report
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"log"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func resetSelfTail() {
|
||||
SetPendingControllerLog(false)
|
||||
SetControllerLogSource(nil)
|
||||
}
|
||||
|
||||
// S2 (controller half): the ACK's controller_log_requested arms the pull; the next
|
||||
// build attaches the ring tail; the build after that attaches nothing (consume-once).
|
||||
// Companion red-proof: remove the `selfTailPending = false` drain → the second build
|
||||
// ships again → the nil assertion fails.
|
||||
func TestControllerLogTail_ConsumeOnce(t *testing.T) {
|
||||
t.Cleanup(resetSelfTail)
|
||||
SetControllerLogSource(func(maxBytes int) []string { return []string{"l1", "l2", "l3"} })
|
||||
SetPendingControllerLog(true)
|
||||
|
||||
var logbuf bytes.Buffer
|
||||
logger := log.New(&logbuf, "", 0)
|
||||
first := buildControllerLogTail(logger)
|
||||
if first == nil || len(first.Lines) != 3 || first.CollectedAt.IsZero() {
|
||||
t.Fatalf("first build tail = %+v, want the 3 ring lines + collected_at", first)
|
||||
}
|
||||
// Customer transparency: the pull is announced in the box's own log.
|
||||
if !strings.Contains(logbuf.String(), "operator log pull served") {
|
||||
t.Errorf("transparency INFO line missing: %q", logbuf.String())
|
||||
}
|
||||
if second := buildControllerLogTail(logger); second != nil {
|
||||
t.Errorf("second build shipped again — consume-once broken: %+v", second)
|
||||
}
|
||||
}
|
||||
|
||||
// The hub is the source of truth: an ACK without the flag clears a stale pending.
|
||||
func TestControllerLogTail_AckClears(t *testing.T) {
|
||||
t.Cleanup(resetSelfTail)
|
||||
SetControllerLogSource(func(int) []string { return []string{"x"} })
|
||||
SetPendingControllerLog(true)
|
||||
SetPendingControllerLog(false)
|
||||
if tail := buildControllerLogTail(nil); tail != nil {
|
||||
t.Errorf("cleared pending still shipped: %+v", tail)
|
||||
}
|
||||
}
|
||||
|
||||
// No source wired (defensive) → pending is drained, nothing shipped, no panic.
|
||||
func TestControllerLogTail_NoSource(t *testing.T) {
|
||||
t.Cleanup(resetSelfTail)
|
||||
SetPendingControllerLog(true)
|
||||
if tail := buildControllerLogTail(nil); tail != nil {
|
||||
t.Errorf("tail shipped with no source: %+v", tail)
|
||||
}
|
||||
}
|
||||
|
||||
// S3 app-tail byte-compatibility: the v0.111.0 log_tails wire shape is unchanged by
|
||||
// the additive controller_log_tail — absent in the steady state (omitempty), and the
|
||||
// existing keys marshal identically. Companion red-proof: drop `omitempty` from
|
||||
// ControllerLogTail → the steady-state JSON gains a null key → FAIL.
|
||||
func TestReportSchema_AppTailUnchangedAndSelfTailAdditive(t *testing.T) {
|
||||
r := &Report{
|
||||
Version: 1,
|
||||
CustomerID: "c1",
|
||||
LogTails: []LogTail{{
|
||||
App: "gokapi", CollectedAt: time.Date(2026, 7, 11, 10, 0, 0, 0, time.UTC),
|
||||
Lines: []string{"a", "b"},
|
||||
}},
|
||||
}
|
||||
raw, err := json.Marshal(r)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var m map[string]json.RawMessage
|
||||
if err := json.Unmarshal(raw, &m); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, present := m["controller_log_tail"]; present {
|
||||
t.Errorf("controller_log_tail present in the steady state — must be omitted (additive)")
|
||||
}
|
||||
var tails []map[string]json.RawMessage
|
||||
if err := json.Unmarshal(m["log_tails"], &tails); err != nil || len(tails) != 1 {
|
||||
t.Fatalf("log_tails shape changed: %s", m["log_tails"])
|
||||
}
|
||||
for _, key := range []string{"app", "collected_at", "lines"} {
|
||||
if _, ok := tails[0][key]; !ok {
|
||||
t.Errorf("log_tails entry lost key %q — app-tail flow must stay byte-compatible", key)
|
||||
}
|
||||
}
|
||||
|
||||
// The ACK parse: old fields + the new flag coexist; absent flag = false.
|
||||
var pr PushResponse
|
||||
if err := json.Unmarshal([]byte(`{"status":"ok","log_tail_requests":["gokapi"],"controller_log_requested":true}`), &pr); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(pr.LogTailRequests) != 1 || !pr.ControllerLogRequested {
|
||||
t.Errorf("ACK parse = %+v", pr)
|
||||
}
|
||||
var old PushResponse
|
||||
if err := json.Unmarshal([]byte(`{"status":"ok"}`), &old); err != nil || old.ControllerLogRequested {
|
||||
t.Errorf("old ACK must parse with the flag false: %+v err=%v", old, err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user