summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-03-03 23:43:48 +0200
committerPaul Buetow <paul@buetow.org>2026-03-03 23:43:48 +0200
commit7e6b2e4c2ba957899b12dac7e0ea9f7c29a9a7b3 (patch)
tree40ddb216b080bd0da08502bd855c25d45f9dc4cd
parent293406c3bd2acc85490da11afabaf6733babd5e4 (diff)
worktime: use sentinel login state errors
Replace string matching in timer sync with errors.Is and add regression tests.
-rw-r--r--internal/cli/timer.go2
-rw-r--r--internal/cli/timer_test.go27
-rw-r--r--internal/worktime/entries.go11
-rw-r--r--internal/worktime/entries_test.go5
4 files changed, 42 insertions, 3 deletions
diff --git a/internal/cli/timer.go b/internal/cli/timer.go
index 78d6d8d..46292ad 100644
--- a/internal/cli/timer.go
+++ b/internal/cli/timer.go
@@ -242,7 +242,7 @@ func syncWorktimeWithTimer(start bool) error {
}
// Avoid failing timer commands on no-op state sync mismatches.
- if strings.Contains(err.Error(), "already logged in") || strings.Contains(err.Error(), "not logged in") {
+ if errors.Is(err, worktime.ErrAlreadyLoggedIn) || errors.Is(err, worktime.ErrNotLoggedIn) {
return nil
}
diff --git a/internal/cli/timer_test.go b/internal/cli/timer_test.go
index 25f9618..32b9b8b 100644
--- a/internal/cli/timer_test.go
+++ b/internal/cli/timer_test.go
@@ -5,6 +5,7 @@ import (
"path/filepath"
"strings"
"testing"
+ "time"
timrTimer "codeberg.org/snonux/timr/internal/timer"
"codeberg.org/snonux/timr/internal/worktime"
@@ -113,6 +114,32 @@ func TestTimerAutoWorktimeSync(t *testing.T) {
}
}
+func TestTimerAutoWorktimeSyncIgnoresAlreadyLoggedIn(t *testing.T) {
+ setupTimerState(t)
+
+ dbDir := t.TempDir()
+ if _, err := worktime.Login(dbDir, "host-auto", "work", time.Unix(100, 0), "seed"); err != nil {
+ t.Fatalf("seed Login() error = %v", err)
+ }
+
+ cfgPath := writeWorkConfigWithAuto(t, dbDir, "host-auto", true)
+ out, err := runRootCommand("--config", cfgPath, "timer", "start")
+ if err != nil {
+ t.Fatalf("timer start error = %v (output: %q)", err, out)
+ }
+}
+
+func TestTimerAutoWorktimeSyncIgnoresNotLoggedInOnStop(t *testing.T) {
+ setupTimerState(t)
+
+ dbDir := t.TempDir()
+ cfgPath := writeWorkConfigWithAuto(t, dbDir, "host-auto", true)
+ out, err := runRootCommand("--config", cfgPath, "timer", "stop")
+ if err != nil {
+ t.Fatalf("timer stop error = %v (output: %q)", err, out)
+ }
+}
+
func setupTimerState(t *testing.T) {
t.Helper()
diff --git a/internal/worktime/entries.go b/internal/worktime/entries.go
index f9226ce..b9a7e40 100644
--- a/internal/worktime/entries.go
+++ b/internal/worktime/entries.go
@@ -13,6 +13,13 @@ const (
actionAdd = "add"
)
+var (
+ // ErrAlreadyLoggedIn indicates that a category already has an open login entry.
+ ErrAlreadyLoggedIn = errors.New("already logged in")
+ // ErrNotLoggedIn indicates that a category has no active login entry.
+ ErrNotLoggedIn = errors.New("not logged in")
+)
+
// Login creates a login entry after validating the category is not already logged in.
func Login(dbDir, hostname, category string, at time.Time, descr string) (Entry, error) {
host, err := normalizeHostname(hostname)
@@ -26,7 +33,7 @@ func Login(dbDir, hostname, category string, at time.Time, descr string) (Entry,
return Entry{}, err
}
if loggedIn {
- return Entry{}, fmt.Errorf("already logged in for %q", cat)
+ return Entry{}, fmt.Errorf("%w for %q", ErrAlreadyLoggedIn, cat)
}
entry := newEntry(actionLogin, host, cat, at, 0, descr)
@@ -46,7 +53,7 @@ func Logout(dbDir, hostname, category string, at time.Time, descr string) (Entry
return Entry{}, err
}
if !loggedIn {
- return Entry{}, fmt.Errorf("not logged in for %q", cat)
+ return Entry{}, fmt.Errorf("%w for %q", ErrNotLoggedIn, cat)
}
entry := newEntry(actionLogout, host, cat, at, 0, descr)
diff --git a/internal/worktime/entries_test.go b/internal/worktime/entries_test.go
index 1327a1a..f49dbd8 100644
--- a/internal/worktime/entries_test.go
+++ b/internal/worktime/entries_test.go
@@ -1,6 +1,7 @@
package worktime
import (
+ "errors"
"testing"
"time"
)
@@ -19,6 +20,8 @@ func TestLoginLogoutValidation(t *testing.T) {
if _, err := Login(dbDir, host, "work", time.Unix(110, 0), "start again"); err == nil {
t.Fatal("Login() error = nil, want already logged in error")
+ } else if !errors.Is(err, ErrAlreadyLoggedIn) {
+ t.Fatalf("Login() error = %v, want ErrAlreadyLoggedIn", err)
}
logoutEntry, err := Logout(dbDir, host, "work", time.Unix(120, 0), "stop")
@@ -31,6 +34,8 @@ func TestLoginLogoutValidation(t *testing.T) {
if _, err := Logout(dbDir, host, "work", time.Unix(130, 0), "stop again"); err == nil {
t.Fatal("Logout() error = nil, want not logged in error")
+ } else if !errors.Is(err, ErrNotLoggedIn) {
+ t.Fatalf("Logout() error = %v, want ErrNotLoggedIn", err)
}
}