package report import ( "errors" "fmt" "strings" "testing" "unicode/utf8" ) // Part D — the consume-once seam: an ACK arms the pending set, the FIRST build drains // it, the FOLLOWING build gets nothing. Red-proof: removing the clear in // drainPendingLogTails makes the second drain return the apps again → this fails. func TestPendingLogTails_ConsumeOnce(t *testing.T) { SetPendingLogTails([]string{"gokapi", "cwa"}) first := drainPendingLogTails() if len(first) != 2 || first[0] != "gokapi" || first[1] != "cwa" { t.Fatalf("first drain = %v, want the two requested apps", first) } second := drainPendingLogTails() if len(second) != 0 { t.Fatalf("second drain = %v — tails would ship on EVERY report (consume-once broken)", second) } } // An ACK without requests clears any stale local set (hub is the source of truth). func TestPendingLogTails_EmptyAckClears(t *testing.T) { SetPendingLogTails([]string{"gokapi"}) SetPendingLogTails(nil) if got := drainPendingLogTails(); len(got) != 0 { t.Fatalf("stale pending set survived an empty ACK: %v", got) } } func TestBuildLogTailsSection_OrderedAndRedacted(t *testing.T) { fetch := func(app string, lines int) (string, error) { if lines != 200 { t.Fatalf("fetch asked for %d lines, want 200", lines) } return "line one\nline two with password=hunter2\nline three\n", nil } tails := buildLogTailsSection([]string{"gokapi"}, fetch, nil) if len(tails) != 1 || tails[0].App != "gokapi" { t.Fatalf("tails = %+v", tails) } got := tails[0].Lines if len(got) != 3 { t.Fatalf("lines = %d (trailing empty must be dropped): %#v", len(got), got) } if got[0] != "line one" || got[2] != "line three" { t.Fatalf("order broken: %#v", got) } if strings.Contains(got[1], "hunter2") || !strings.Contains(got[1], "[REDACTED]") { t.Fatalf("tail line not redacted: %q", got[1]) } if tails[0].CollectedAt.IsZero() { t.Fatalf("collected_at not stamped") } } func TestBuildLogTailsSection_LineLengthCap(t *testing.T) { fetch := func(string, int) (string, error) { return strings.Repeat("y", 900) + "\nshort", nil } tails := buildLogTailsSection([]string{"a"}, fetch, nil) first := tails[0].Lines[0] if utf8.RuneCountInString(first) != 401 || !strings.HasSuffix(first, "…") { t.Fatalf("line not capped at 400+ellipsis: runes=%d", utf8.RuneCountInString(first)) } } // 64KB per-app budget: HEAD-truncate — the newest lines must survive. func TestBuildLogTailsSection_ByteBudgetKeepsNewest(t *testing.T) { var sb strings.Builder for i := 0; i < 300; i++ { sb.WriteString(fmt.Sprintf("%04d ", i)) sb.WriteString(strings.Repeat("p", 295)) sb.WriteString("\n") } fetch := func(string, int) (string, error) { return sb.String(), nil } tails := buildLogTailsSection([]string{"a"}, fetch, nil) lines := tails[0].Lines if len(lines) >= 300 { t.Fatalf("budget not enforced: %d lines kept", len(lines)) } total := 0 for _, l := range lines { total += len(l) + 1 } if total > logTailMaxBytes { t.Fatalf("kept %d bytes > %d budget", total, logTailMaxBytes) } if !strings.HasPrefix(lines[len(lines)-1], "0299") { t.Fatalf("NEWEST line lost — head-truncation inverted; last kept = %q", lines[len(lines)-1][:5]) } if strings.HasPrefix(lines[0], "0000") { t.Fatalf("oldest line survived a budget overflow — nothing was dropped") } } // A failed fetch skips that app but the others still ship (hub retries the failed one // next cycle since its pending request was never fulfilled). func TestBuildLogTailsSection_FetchErrorSkips(t *testing.T) { fetch := func(app string, _ int) (string, error) { if app == "broken" { return "", errors.New("no such stack") } return "ok line\n", nil } tails := buildLogTailsSection([]string{"broken", "healthy"}, fetch, nil) if len(tails) != 1 || tails[0].App != "healthy" { t.Fatalf("tails = %+v, want only the healthy app", tails) } }