package web import ( "bytes" "io" "log" "strings" "testing" "time" ) // S1 capture-at-info (controller half): with the v0.116.0 writer layout — // MultiWriter(LevelFilterWriter(stdout, "info"), LogBuffer) — a [DEBUG] line // reaches the RING and is ABSENT from stdout, while [INFO] reaches both. // Companion red-proof: gate the ring behind the same filter (the pre-fix shape, // where the ring only existed at logging.level=debug) → the ring assertion fails. func TestCaptureAtInfo_RingHoldsDebugStdoutDoesNot(t *testing.T) { var stdout bytes.Buffer lb := NewLogBuffer(50) logger := log.New(io.MultiWriter(NewLevelFilterWriter(&stdout, "info"), lb), "", log.LstdFlags) logger.Printf("[DEBUG] [web] netstorage add \"vids\" phase agent_add -> verifying") logger.Printf("[INFO] [web] network storage added + verified: vids") entries, total := lb.Entries("DEBUG", 0, time.Time{}) if total != 2 || len(entries) != 2 { t.Fatalf("ring holds %d entries (returned %d), want 2 — DEBUG must be captured at logging.level=info", total, len(entries)) } if entries[0].Level != "DEBUG" || !strings.Contains(entries[0].Message, "phase agent_add") { t.Errorf("ring entry 0 = %+v, want the DEBUG phase line", entries[0]) } if strings.Contains(stdout.String(), "phase agent_add") { t.Errorf("stdout carries the DEBUG line at level info:\n%s", stdout.String()) } if !strings.Contains(stdout.String(), "added + verified") { t.Errorf("stdout missing the INFO line:\n%s", stdout.String()) } } // The filter respects higher minimums too (warn drops INFO) and passes untagged lines at info. func TestLevelFilterWriter_Thresholds(t *testing.T) { cases := []struct { min string line string wants bool }{ {"info", "[DEBUG] x", false}, {"info", "[INFO] x", true}, {"info", "untagged line", true}, // parses as INFO {"warn", "[INFO] x", false}, {"warn", "[ERROR] x", true}, {"debug", "[DEBUG] x", true}, } for _, c := range cases { var out bytes.Buffer logger := log.New(NewLevelFilterWriter(&out, c.min), "", log.LstdFlags) logger.Printf("%s", c.line) got := strings.Contains(out.String(), c.line) if got != c.wants { t.Errorf("min=%s line=%q passed=%v want %v", c.min, c.line, got, c.wants) } } } // Lines renders chronological plain-text lines and honors the byte budget by // keeping the NEWEST lines (the controller_log_tail 128 KB cap). func TestLogBufferLines_ByteBudgetKeepsNewest(t *testing.T) { lb := NewLogBuffer(10) logger := log.New(lb, "", log.LstdFlags) for i := 0; i < 5; i++ { logger.Printf("[INFO] line-%d %s", i, strings.Repeat("x", 80)) } all := lb.Lines(0) if len(all) != 5 || !strings.Contains(all[4], "line-4") { t.Fatalf("uncapped lines wrong: %d %v", len(all), all) } budget := len(all[3]) + len(all[4]) + 2 capped := lb.Lines(budget) if len(capped) >= 5 || !strings.Contains(capped[len(capped)-1], "line-4") { t.Errorf("byte cap kept %d lines, newest=%q — must drop oldest first", len(capped), capped[len(capped)-1]) } }