Newer
Older
navi-1 / tests / unit / tools / test_time_parser.py
@Eugene Sukhodolskiy Eugene Sukhodolskiy on 15 May 3 KB Add self-recall (scheduled callback) system
"""Unit tests for the lightweight natural-language time parser."""

from datetime import datetime, timedelta, timezone

import pytest

from navi.tools._internal.time_parser import parse_when


class TestParseIso:
    def test_iso_datetime(self):
        dt, err = parse_when("2026-05-20T14:30:00")
        assert err is None
        assert dt == datetime(2026, 5, 20, 14, 30, 0, tzinfo=timezone.utc)

    def test_iso_datetime_with_z(self):
        dt, err = parse_when("2026-05-20T14:30:00Z")
        assert err is None
        assert dt == datetime(2026, 5, 20, 14, 30, 0, tzinfo=timezone.utc)

    def test_iso_datetime_with_offset(self):
        dt, err = parse_when("2026-05-20T14:30:00+03:00")
        assert err is None
        assert dt is not None
        assert dt.utcoffset().total_seconds() == 10800


class TestParseRelative:
    def test_relative_days_hours(self):
        dt, err = parse_when("2d 6h")
        assert err is None
        expected = datetime.now(timezone.utc) + timedelta(days=2, hours=6)
        assert abs((dt - expected).total_seconds()) < 5

    def test_relative_in_hours(self):
        dt, err = parse_when("in 3 hours")
        assert err is None
        expected = datetime.now(timezone.utc) + timedelta(hours=3)
        assert abs((dt - expected).total_seconds()) < 5

    def test_relative_in_minutes(self):
        dt, err = parse_when("in 30 minutes")
        assert err is None
        expected = datetime.now(timezone.utc) + timedelta(minutes=30)
        assert abs((dt - expected).total_seconds()) < 5

    def test_relative_compact_minutes(self):
        dt, err = parse_when("30m")
        assert err is None
        expected = datetime.now(timezone.utc) + timedelta(minutes=30)
        assert abs((dt - expected).total_seconds()) < 5

    def test_relative_compact_seconds(self):
        dt, err = parse_when("90s")
        assert err is None
        expected = datetime.now(timezone.utc) + timedelta(seconds=90)
        assert abs((dt - expected).total_seconds()) < 5

    def test_relative_mixed_compact(self):
        dt, err = parse_when("1d 2h 30m")
        assert err is None
        expected = datetime.now(timezone.utc) + timedelta(days=1, hours=2, minutes=30)
        assert abs((dt - expected).total_seconds()) < 5


class TestParseTomorrow:
    def test_tomorrow_at_time(self):
        dt, err = parse_when("tomorrow at 09:00")
        assert err is None
        now = datetime.now(timezone.utc)
        assert dt.date() == (now + timedelta(days=1)).date()
        assert dt.hour == 9
        assert dt.minute == 0

    def test_tomorrow_without_at(self):
        dt, err = parse_when("tomorrow 14:30")
        assert err is None
        now = datetime.now(timezone.utc)
        assert dt.date() == (now + timedelta(days=1)).date()
        assert dt.hour == 14
        assert dt.minute == 30


class TestParseErrors:
    def test_empty_string(self):
        dt, err = parse_when("")
        assert dt is None
        assert "Empty" in err

    def test_whitespace_only(self):
        dt, err = parse_when("   ")
        assert dt is None
        assert "Empty" in err

    def test_gibberish(self):
        dt, err = parse_when("asdfghjkl")
        assert dt is None
        assert "Could not parse" in err

    def test_past_iso(self):
        dt, err = parse_when("2020-01-01T00:00:00")
        assert err is None
        assert dt == datetime(2020, 1, 1, 0, 0, 0, tzinfo=timezone.utc)