Move the command line from cmd/bsdaily into internal/cli (closes #8)
check / check (push) Successful in 4m15s

cmd/bsdaily/main.go now only passes Version to cli.Main and exits with
the status it returns, as REPO_POLICIES.md requires of cmd/. The cobra
command, the flag rules and the date parsing moved unchanged into
internal/cli, so flags, help text, error messages, exit status and the
first log line stay as they were. The fallback to dev for an empty
version moved with them into cli.Main.

parseTargetDates is exported as ParseTargetDates so that a table test
outside the package can reach it; the test never touches the filesystem
or runs the extraction. README describes the new layout and TODO.md
records the step.

Model: opus-5-5
Co-authored-by: clawbot <sneak+clawbot@sneak.cloud>
This commit was merged in pull request #13.
This commit is contained in:
2026-10-06 23:07:50 +02:00
committed by clawbot
parent f5c7614768
commit bcf2c8550d
6 changed files with 281 additions and 122 deletions
+127
View File
@@ -0,0 +1,127 @@
package cli_test
import (
"slices"
"strings"
"testing"
"git.eeqj.de/sneak/bsdaily/internal/cli"
)
func TestParseTargetDates(t *testing.T) {
t.Parallel()
const oneDay = "2026-05-14"
// want lists the expected days as YYYY-MM-DD.
tests := []struct {
name string
date string
from string
to string
want []string
}{
{name: "no flags"},
{name: "single date", date: "2026-06-27", want: []string{"2026-06-27"}},
{
name: "range across a month end", from: "2026-06-29", to: "2026-07-02",
want: []string{"2026-06-29", "2026-06-30", "2026-07-01", "2026-07-02"},
},
{name: "range of one day", from: oneDay, to: oneDay, want: []string{oneDay}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got, err := cli.ParseTargetDates(tt.date, tt.from, tt.to)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
days := make([]string, 0, len(got))
for _, day := range got {
days = append(days, day.Format("2006-01-02"))
}
if !slices.Equal(days, tt.want) {
t.Errorf("days = %v, want %v", days, tt.want)
}
})
}
}
func TestParseTargetDatesErrors(t *testing.T) {
t.Parallel()
const dateExclusive = "--date and --from/--to are mutually exclusive"
// wantErr is the whole error message. With startsWith set it is only how
// the message starts: for a malformed date, the date parser's own
// explanation follows it.
tests := []struct {
name string
date string
from string
to string
wantErr string
startsWith bool
}{
{
name: "from after to", from: "2026-03-02", to: "2026-03-01",
wantErr: "--from 2026-03-02 is after --to 2026-03-01",
},
{name: "from without to", from: "2026-04-01", wantErr: "--from requires --to"},
{name: "to without from", to: "2026-04-02", wantErr: "--to requires --from"},
{
name: "date with from", date: "2026-01-05", from: "2026-01-06",
wantErr: dateExclusive,
},
{
name: "date with to", date: "2026-01-13", to: "2026-01-14",
wantErr: dateExclusive,
},
{
name: "date with from and to", date: "2026-01-07",
from: "2026-01-08", to: "2026-01-09",
wantErr: dateExclusive,
},
{
name: "malformed date", date: "27.06.2026",
wantErr: `invalid --date "27.06.2026" (expected YYYY-MM-DD): `,
startsWith: true,
},
{
name: "date that does not exist", date: "2026-02-30",
wantErr: `invalid --date "2026-02-30" (expected YYYY-MM-DD): `,
startsWith: true,
},
{
name: "malformed from", from: "2026-1-10", to: "2026-01-11",
wantErr: `invalid --from "2026-1-10" (expected YYYY-MM-DD): `,
startsWith: true,
},
{
name: "malformed to", from: "2026-01-12", to: "tomorrow",
wantErr: `invalid --to "tomorrow" (expected YYYY-MM-DD): `,
startsWith: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
_, err := cli.ParseTargetDates(tt.date, tt.from, tt.to)
switch {
case err == nil:
t.Errorf("no error, want %q", tt.wantErr)
case tt.startsWith && !strings.HasPrefix(err.Error(), tt.wantErr):
t.Errorf("error = %q, want one starting %q", err, tt.wantErr)
case !tt.startsWith && err.Error() != tt.wantErr:
t.Errorf("error = %q, want %q", err, tt.wantErr)
}
})
}
}