package events import ( "strings" "testing" "time" ) func TestLogWriterLines(t *testing.T) { lbus := NewLogBus(16) ch, unsub := lbus.Subscribe() defer unsub() w := NewLogWriter(lbus) if _, err := w.Write([]byte("first line\nsecond line\n")); err != nil { t.Fatalf("Write: %v", err) } first := receiveOne(t, ch) ll, ok := first.(LogLine) if !ok { t.Fatalf("got %T, want LogLine", first) } // строка без маркеров ошибок/отладки — информационная if ll.Text != "first line" || ll.Level != LevelInfo { t.Fatalf("unexpected first line: %+v", ll) } second := receiveOne(t, ch) if ll, ok := second.(LogLine); !ok || ll.Text != "second line" { t.Fatalf("unexpected second line: %+v", second) } } func TestLogWriterPartialLine(t *testing.T) { lbus := NewLogBus(16) ch, unsub := lbus.Subscribe() defer unsub() w := NewLogWriter(lbus) if _, err := w.Write([]byte("partial")); err != nil { t.Fatalf("Write: %v", err) } select { case got := <-ch: t.Fatalf("partial line should not be published yet, got %#v", got) case <-time.After(150 * time.Millisecond): } if _, err := w.Write([]byte(" line\n")); err != nil { t.Fatalf("Write: %v", err) } got := receiveOne(t, ch) ll, ok := got.(LogLine) if !ok || ll.Text != "partial line" { t.Fatalf("unexpected: %#v", got) } } func TestLogWriterMultiSplit(t *testing.T) { lbus := NewLogBus(16) ch, unsub := lbus.Subscribe() defer unsub() w := NewLogWriter(lbus) if _, err := w.Write([]byte("line1\nline2\nline3\n")); err != nil { t.Fatalf("Write: %v", err) } var texts []string for i := 0; i < 3; i++ { ev := receiveOne(t, ch) texts = append(texts, ev.(LogLine).Text) } if strings.Join(texts, ",") != "line1,line2,line3" { t.Fatalf("got %v", texts) } } // ClassifyLevel определяет уровень строки по маркерам в тексте. func TestClassifyLevel(t *testing.T) { cases := []struct { line string want string }{ {"app: db opened /tmp/r.db", LevelInfo}, {"app: worker started", LevelInfo}, {"opencode: debug: poll request", LevelDebug}, {"opencode api debug: prompt -> POST http://127.0.0.1:4096/api/session", LevelDebug}, {"opencode api debug: messages response (512 bytes)", LevelDebug}, {"trace: session resumed", LevelDebug}, {"tg: warn: long poll timeout", LevelWarning}, {"ПРЕДУПРЕЖДЕНИЕ: конфиг не задан", LevelWarning}, {"app: process /start #5: run failed: exit status 1", LevelError}, {"ERROR: connection refused", LevelError}, {"worker: job failed after 3 retries", LevelError}, {"panic: runtime error", LevelError}, } for _, c := range cases { if got := ClassifyLevel(c.line); got != c.want { t.Errorf("ClassifyLevel(%q) = %q, want %q", c.line, got, c.want) } } } // LogWriter публикует уровень, определённый по тексту строки. func TestLogWriterClassifiesLevels(t *testing.T) { lbus := NewLogBus(16) ch, unsub := lbus.Subscribe() defer unsub() w := NewLogWriter(lbus) if _, err := w.Write([]byte("all good\nERROR: boom\n")); err != nil { t.Fatalf("Write: %v", err) } first := receiveOne(t, ch).(LogLine) if first.Level != LevelInfo || first.Text != "all good" { t.Fatalf("unexpected first: %+v", first) } second := receiveOne(t, ch).(LogLine) if second.Level != LevelError || second.Text != "ERROR: boom" { t.Fatalf("unexpected second: %+v", second) } }