package progress import ( "bytes" "errors" "log/slog" "strings" "testing" "time" ) func TestTrackerLogsCIStablePercentLines(t *testing.T) { var out bytes.Buffer now := time.Date(2026, 5, 2, 12, 0, 0, 0, time.UTC) logger := testLogger(&out) tracker := New(logger, Options{ Name: "sync", Unit: "channels", Total: 4, MinDelta: 2, Attrs: []any{"source", "api"}, Now: func() time.Time { return now }, }) tracker.Set(1) tracker.Set(2, "phase", "messages") tracker.Finish(nil) logs := out.String() if strings.Count(logs, `msg="sync progress"`) != 3 { t.Fatalf("expected start, threshold, and finish lines:\n%s", logs) } for _, want := range []string{ `state=started`, `state=progress`, `state=finished`, `unit=channels`, `done=2`, `total=4`, `percent=50.0`, `completion=50.0%`, `source=api`, `phase=messages`, } { if !strings.Contains(logs, want) { t.Fatalf("missing %q in logs:\n%s", want, logs) } } } func TestTrackerFinishReportsFailure(t *testing.T) { var out bytes.Buffer logger := testLogger(&out) tracker := New(logger, Options{Name: "import", Total: 10}) tracker.Set(3) tracker.Finish(errors.New("boom")) logs := out.String() for _, want := range []string{`state=failed`, `done=3`, `percent=30.0`, `err=boom`} { if !strings.Contains(logs, want) { t.Fatalf("missing %q in logs:\n%s", want, logs) } } } func TestCompletionClampsToRange(t *testing.T) { if got := Completion(-1, 4); got != "0.0%" { t.Fatalf("low clamp = %s", got) } if got := Completion(5, 4); got != "100.0%" { t.Fatalf("high clamp = %s", got) } if got := Completion(1, 0); got != "" { t.Fatalf("unknown total = %q", got) } } func testLogger(out *bytes.Buffer) *slog.Logger { return slog.New(slog.NewTextHandler(out, &slog.HandlerOptions{ ReplaceAttr: func(_ []string, attr slog.Attr) slog.Attr { if attr.Key == slog.TimeKey { return slog.Attr{} } return attr }, })) }