package router import ( "context" "os/exec" "testing" "time" ) // TestPythonDateParser requires python3 + dateparser installed. Skips // gracefully in environments without them (e.g. bare go test, dev machines // without the Docker runtime image). func TestPythonDateParser(t *testing.T) { // Skip if python3 isn't on PATH. if _, err := exec.LookPath("python3"); err != nil { t.Skip("python3 not on PATH — skipping dateparser tests") } // Skip if dateparser isn't installed. cmd := exec.Command("python3", "-c", "import dateparser") if err := cmd.Run(); err != nil { t.Skip("python dateparser not installed — skipping dateparser tests") } p := NewPythonDateParser() now := time.Date(2026, 7, 6, 18, 0, 0, 0, time.Local) ctx := context.Background() tests := []struct { name string text string wantOK bool checkT func(t *testing.T, got, now time.Time) }{ { name: "ru relative — через час", text: "напомни через час выпить воды", wantOK: true, checkT: func(t *testing.T, got, now time.Time) { d := got.Sub(now) if d < 50*time.Minute || d > 70*time.Minute { t.Errorf("через час: got %v from now, want ~1h", d) } }, }, { name: "ru relative — через 30 минут", text: "напомни через 30 минут снять бельё", wantOK: true, checkT: func(t *testing.T, got, now time.Time) { d := got.Sub(now) if d < 25*time.Minute || d > 35*time.Minute { t.Errorf("через 30 минут: got %v from now, want ~30m", d) } }, }, { name: "ru absolute — завтра в 9 утра", text: "напомни завтра в 9 утра позвонить", wantOK: true, checkT: func(t *testing.T, got, now time.Time) { if got.Hour() != 9 { t.Errorf("завтра в 9 утра: hour=%d, want 9", got.Hour()) } if got.Sub(now) < 12*time.Hour { t.Errorf("завтра в 9 утра: only %v from now, want >12h (tomorrow)", got.Sub(now)) } }, }, { name: "ru complex — в следующую пятницу", text: "напомни в следующую пятницу оплатить счёт", wantOK: true, checkT: func(t *testing.T, got, now time.Time) { if got.Weekday() != time.Friday { t.Errorf("в следующую пятницу: weekday=%v, want Friday", got.Weekday()) } if got.Sub(now) < 24*time.Hour { t.Errorf("в следующую пятницу: only %v from now, want >24h (future)", got.Sub(now)) } }, }, { name: "en relative — in 30 minutes", text: "remind me in 30 minutes to drink water", wantOK: true, checkT: func(t *testing.T, got, now time.Time) { d := got.Sub(now) if d < 25*time.Minute || d > 35*time.Minute { t.Errorf("in 30 minutes: got %v from now, want ~30m", d) } }, }, { name: "en absolute — tomorrow at 8am", text: "remind me tomorrow at 8am to call the doctor", wantOK: true, checkT: func(t *testing.T, got, now time.Time) { if got.Hour() != 8 { t.Errorf("tomorrow at 8am: hour=%d, want 8", got.Hour()) } if got.Sub(now) < 12*time.Hour { t.Errorf("tomorrow at 8am: only %v from now, want >12h (tomorrow)", got.Sub(now)) } }, }, { name: "no date — напомни мне", text: "напомни мне", wantOK: false, }, { name: "no date — remind me", text: "remind me to do something", wantOK: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, ok, err := p.Parse(ctx, tt.text, now) if err != nil { t.Fatalf("Parse(%q) error: %v", tt.text, err) } if ok != tt.wantOK { t.Fatalf("Parse(%q) ok=%v, want %v (got time=%v)", tt.text, ok, tt.wantOK, got) } if ok && tt.checkT != nil { tt.checkT(t, got, now) } }) } } // TestPythonDateParser_FallbackToStub verifies that when python3 is not // available, the parser falls back to StubDateTimeParser instead of // returning an error. func TestPythonDateParser_FallbackToStub(t *testing.T) { if _, err := exec.LookPath("python3"); err == nil { t.Skip("python3 is available — fallback path not exercised") } p := NewPythonDateParser() ctx := context.Background() now := time.Date(2026, 7, 6, 18, 0, 0, 0, time.Local) // The stub handles "in 1 hour" — should still work via fallback. got, ok, err := p.Parse(ctx, "in 1 hour", now) if err != nil { t.Fatalf("fallback Parse error: %v", err) } if !ok { t.Fatal("fallback Parse: ok=false, want true (stub should handle 'in 1 hour')") } d := got.Sub(now) if d < 50*time.Minute || d > 70*time.Minute { t.Errorf("fallback 'in 1 hour': got %v from now, want ~1h", d) } }