package main import ( "context" "errors" "io" "log/slog" "strings" "sync" "testing" "time" "github.com/stretchr/testify/require" ) func TestDaemonLogFormatsLevelsAndPrefixes(t *testing.T) { testCases := []struct { name string level slog.Level prefix string priority string }{ { name: "error names the failed part", level: slog.LevelError, prefix: "onechat: ", priority: "3", }, { name: "warning names the retried part", level: slog.LevelWarn, prefix: "telegram: ", priority: "4", }, { name: "info names the connected part", level: slog.LevelInfo, prefix: "whatsapp: ", priority: "6", }, { name: "debug has its own priority", level: slog.LevelDebug, prefix: "onechat: ", priority: "7", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { for _, journal := range []bool{false, true} { var out strings.Builder handler := &DaemonLogHandler{ out: &out, prefix: tc.prefix, journal: journal, mutex: &sync.Mutex{}, } record := slog.NewRecord(time.Date(2026, 1, 2, 3, 4, 5, 0, time.UTC), tc.level, "ready", 0) require.True(t, handler.Enabled(t.Context(), tc.level)) require.NoError(t, handler.Handle(t.Context(), record)) expected := "2026/01/02 03:04:05 " + tc.level.String() + " " + tc.prefix + "ready\n" if journal { expected = "<" + tc.priority + ">" + tc.prefix + "ready\n" } require.Equal(t, expected, out.String()) } }) } } func TestDaemonLogKeepsAttributesInOrderAndCauseLast(t *testing.T) { var out strings.Builder handler := &DaemonLogHandler{ out: &out, prefix: "telegram: ", journal: true, mutex: &sync.Mutex{}, } logger := slog.New(handler).With("first", "two words", slog.Attr{}) logger.Warn("receive failed", "err", errors.New("502 Bad Gateway"), "retry", 2040*time.Millisecond, "count", 3, "short", 1500*time.Microsecond) require.Equal(t, "<4>telegram: receive failed first=two words retry=2s count=3 short=2ms: 502 Bad Gateway\n", out.String()) } func TestDaemonLogOmitsEmptyAttributesAndNilCause(t *testing.T) { var out strings.Builder handler := &DaemonLogHandler{ out: &out, prefix: "onechat: ", journal: true, mutex: &sync.Mutex{}, } slog.New(handler).Info("ready", slog.Attr{}, slog.Group("empty"), "err", nil) require.Equal(t, "<6>onechat: ready\n", out.String()) } func TestDaemonLogGroupsDoNotChangeOtherLoggers(t *testing.T) { var out strings.Builder handler := &DaemonLogHandler{ out: &out, journal: true, mutex: &sync.Mutex{}, } logger := slog.New(handler) grouped := logger.With("before", 1).WithGroup("outer").With("saved", 2).WithGroup("inner") grouped.Info("done", slog.Group("", "count", 3)) logger.Info("ready") require.Equal(t, "<6>done before=1 outer.saved=2 outer.inner.count=3\n<6>ready\n", out.String()) } func TestDaemonLogReturnsWriteFailure(t *testing.T) { handler := &DaemonLogHandler{ out: DaemonLogBrokenWriter{}, mutex: &sync.Mutex{}, } err := handler.Handle(context.Background(), slog.NewRecord(time.Now(), slog.LevelInfo, "ready", 0)) require.ErrorIs(t, err, io.ErrClosedPipe) require.ErrorContains(t, err, "write daemon log") } type DaemonLogBrokenWriter struct{} func (w DaemonLogBrokenWriter) Write(_ []byte) (int, error) { return 0, io.ErrClosedPipe }