From ffa4676a34851d667acbf8a460321ff93310b7ba Mon Sep 17 00:00:00 2001 From: Lukasz Kasprzak Date: Thu, 23 Jul 2026 21:01:05 +0200 Subject: cli: flag-driven interface (short+long); -u maximal blip-proof harvest Replace the today/date/show/compare/update subcommands with a single flag-driven interface like the Python predecessor: lectio [DATE] -a/-b/-c/-r/-w/-R/-o/-l/-g/-u, short and long names bound to the same variable, DATE (YYYY-MM-DD) extracted from any position in the args. Fix Harvest's "-u" to walk to the true maximal horizon without a transient network error being mistaken for it: a fetch failure now retries a few times with backoff and, if it still fails, is reported as an error (after saving partial progress via writeSigla), while a Parse failure (the real unpublished-horizon placeholder) still stops cleanly with a nil error. --- internal/cli/cli_test.go | 114 ++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 104 insertions(+), 10 deletions(-) (limited to 'internal/cli/cli_test.go') diff --git a/internal/cli/cli_test.go b/internal/cli/cli_test.go index 85097ed..de9220a 100644 --- a/internal/cli/cli_test.go +++ b/internal/cli/cli_test.go @@ -14,10 +14,18 @@ func TestHelp(t *testing.T) { } } -func TestUnknownCommand(t *testing.T) { +func TestHelpFlag(t *testing.T) { + var out, errb bytes.Buffer + code := Run([]string{"-h"}, nil, &out, &errb) + if code != 0 || !strings.Contains(out.String(), "lectio") { + t.Errorf("-h code=%d out=%q", code, out.String()) + } +} + +func TestUnexpectedPositional(t *testing.T) { var out, errb bytes.Buffer if code := Run([]string{"bogus"}, nil, &out, &errb); code != 2 { - t.Errorf("unknown cmd code=%d want 2", code) + t.Errorf("unexpected positional code=%d want 2 (stderr=%q)", code, errb.String()) } } @@ -28,24 +36,110 @@ func TestVersion(t *testing.T) { } } -// TestBadDate exercises the "date" subcommand's usage-error path (bad -// YYYY-MM-DD format) without touching the network: format validation -// happens before any fetch is attempted. +// TestBadDate exercises the usage-error path for a malformed date: a token +// that doesn't match dateRe's YYYY-MM-DD shape is left as an unrecognised +// positional argument once flag parsing is done, which is a usage error. func TestBadDate(t *testing.T) { t.Setenv("XDG_CONFIG_HOME", t.TempDir()) var out, errb bytes.Buffer - if code := Run([]string{"date", "13-13-13"}, nil, &out, &errb); code != 2 { + if code := Run([]string{"13-13-13"}, nil, &out, &errb); code != 2 { t.Errorf("bad date code=%d want 2 (stderr=%q)", code, errb.String()) } } -// TestUnknownVersion exercises "show"'s version-validation usage-error path -// without touching the network: the version code is checked against the -// five known codes before any fetch is attempted. +// TestTwoDatesIsAmbiguous exercises extractDate's ambiguity check: two +// dateRe-shaped tokens can't both be the positional date. +func TestTwoDatesIsAmbiguous(t *testing.T) { + t.Setenv("XDG_CONFIG_HOME", t.TempDir()) + var out, errb bytes.Buffer + if code := Run([]string{"2026-07-22", "2026-07-23"}, nil, &out, &errb); code != 2 { + t.Errorf("two dates code=%d want 2 (stderr=%q)", code, errb.String()) + } +} + +// TestUnknownVersion exercises -b/--bible's version-validation usage-error +// path without touching the network: the version code is checked against +// the five known codes before any fetch is attempted. func TestUnknownVersion(t *testing.T) { t.Setenv("XDG_CONFIG_HOME", t.TempDir()) var out, errb bytes.Buffer - if code := Run([]string{"show", "zzz"}, nil, &out, &errb); code != 2 { + if code := Run([]string{"-b", "zzz"}, nil, &out, &errb); code != 2 { t.Errorf("unknown version code=%d want 2 (stderr=%q)", code, errb.String()) } } + +// TestLectionaryTradMapsToTraditional exercises -l/--lectionary's trad -> +// traditional normalisation. It goes through extractDate/flag parsing and +// validation only (no network): a bogus date lets it exit before any fetch +// is attempted, but only after the -l value has already been validated and +// mapped, which is what this test checks indirectly via the exit code (a +// bad --lectionary would exit 2 for a different reason, so this alone +// wouldn't distinguish the two -- see TestLectionaryBogus for that side). +// Exercised directly against parseFlags below. +func TestLectionaryTradMapsToTraditional(t *testing.T) { + lectionary, err := normalizeLectionary("trad") + if err != nil { + t.Fatalf("normalizeLectionary(trad): %v", err) + } + if lectionary != "traditional" { + t.Errorf("normalizeLectionary(trad) = %q, want traditional", lectionary) + } +} + +func TestLectionaryBogus(t *testing.T) { + t.Setenv("XDG_CONFIG_HOME", t.TempDir()) + var out, errb bytes.Buffer + if code := Run([]string{"-l", "bogus"}, nil, &out, &errb); code != 2 { + t.Errorf("bogus lectionary code=%d want 2 (stderr=%q)", code, errb.String()) + } + if _, err := normalizeLectionary("bogus"); err == nil { + t.Error("normalizeLectionary(bogus): want error, got nil") + } +} + +func TestLangBogus(t *testing.T) { + t.Setenv("XDG_CONFIG_HOME", t.TempDir()) + var out, errb bytes.Buffer + if code := Run([]string{"-g", "de"}, nil, &out, &errb); code != 2 { + t.Errorf("bogus lang code=%d want 2 (stderr=%q)", code, errb.String()) + } +} + +// TestDateTokenAnyPosition exercises extractDate directly: the date token +// is found regardless of where it appears among other flags. +func TestDateTokenAnyPosition(t *testing.T) { + cases := [][]string{ + {"-a", "2026-07-22"}, + {"2026-07-22", "-a"}, + {"-a", "2026-07-22", "-r"}, + } + for _, args := range cases { + date, rest, err := extractDate(args) + if err != nil { + t.Fatalf("extractDate(%v): %v", args, err) + } + if date != "2026-07-22" { + t.Errorf("extractDate(%v) date = %q, want 2026-07-22", args, date) + } + for _, r := range rest { + if r == "2026-07-22" { + t.Errorf("extractDate(%v) rest = %v still contains the date", args, rest) + } + } + } +} + +// TestExtractDateDefaultsToday checks that omitting a date token leaves +// today's date and passes every token through untouched. +func TestExtractDateDefaultsToday(t *testing.T) { + date, rest, err := extractDate([]string{"-a", "-r"}) + if err != nil { + t.Fatalf("extractDate: %v", err) + } + if date != today() { + t.Errorf("extractDate date = %q, want today() = %q", date, today()) + } + if len(rest) != 2 || rest[0] != "-a" || rest[1] != "-r" { + t.Errorf("extractDate rest = %v, want [-a -r]", rest) + } +} -- cgit v1.3