summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-03-03 23:57:40 +0200
committerPaul Buetow <paul@buetow.org>2026-03-03 23:57:40 +0200
commit461ba1af2801dbae3661ce4edc2f470ceb70e792 (patch)
treebc6ed1df19b2fbb861922de444dd11a5ba074c60 /internal
parenta6df85f96e618de0e38edc90e10d2b3925b5c91f (diff)
timer: inject tracker dependency for TrackTime
Diffstat (limited to 'internal')
-rw-r--r--internal/timer/operations.go39
-rw-r--r--internal/timer/operations_test.go134
2 files changed, 79 insertions, 94 deletions
diff --git a/internal/timer/operations.go b/internal/timer/operations.go
index 3de7f97..ae45304 100644
--- a/internal/timer/operations.go
+++ b/internal/timer/operations.go
@@ -1,12 +1,32 @@
package timer
import (
+ "errors"
"fmt"
"os"
"os/exec"
"time"
)
+// Tracker abstracts timer-to-task tracking so TrackTime can be unit-tested.
+type Tracker interface {
+ Track(description string, minutes int) error
+}
+
+type taskwarriorTracker struct{}
+
+func (taskwarriorTracker) Track(description string, minutes int) error {
+ taskDescription := fmt.Sprintf("%dmin %s", minutes, description)
+ cmd := exec.Command("task", "add", "+track", taskDescription)
+
+ output, err := cmd.CombinedOutput()
+ if err != nil {
+ return fmt.Errorf("task command failed: %s\nOutput: %s", err, string(output))
+ }
+
+ return nil
+}
+
func StartTimer(continued bool) (string, error) {
state, err := LoadState()
if err != nil {
@@ -132,6 +152,14 @@ func GetPromptStatus() (string, error) {
}
func TrackTime(description string) (string, error) {
+ return TrackTimeWithTracker(description, taskwarriorTracker{})
+}
+
+func TrackTimeWithTracker(description string, tracker Tracker) (string, error) {
+ if tracker == nil {
+ return "", errors.New("tracker is nil")
+ }
+
// Load current state
state, err := LoadState()
if err != nil {
@@ -156,15 +184,8 @@ func TrackTime(description string) (string, error) {
}
}
- // Build and execute the task command
- taskDescription := fmt.Sprintf("%dmin %s", minutes, description)
- cmd := exec.Command("task", "add", "+track", taskDescription)
-
- // Execute the command and capture output
- output, err := cmd.CombinedOutput()
- if err != nil {
- // Command failed, return error with output
- return "", fmt.Errorf("task command failed: %s\nOutput: %s", err, string(output))
+ if err := tracker.Track(description, minutes); err != nil {
+ return "", err
}
// Command succeeded, reset the timer
diff --git a/internal/timer/operations_test.go b/internal/timer/operations_test.go
index efdc492..0ad88d3 100644
--- a/internal/timer/operations_test.go
+++ b/internal/timer/operations_test.go
@@ -1,9 +1,7 @@
package timer
import (
- "fmt"
- "os"
- "path/filepath"
+ "errors"
"strconv"
"testing"
"time"
@@ -17,6 +15,20 @@ func setup(t *testing.T) {
t.Setenv("HOME", tempDir)
}
+type mockTracker struct {
+ description string
+ minutes int
+ err error
+ calls int
+}
+
+func (m *mockTracker) Track(description string, minutes int) error {
+ m.description = description
+ m.minutes = minutes
+ m.calls++
+ return m.err
+}
+
func TestStartTimer(t *testing.T) {
setup(t)
@@ -214,46 +226,9 @@ func TestResetTimer(t *testing.T) {
func TestTrackTime(t *testing.T) {
setup(t)
- // Helper to create a mock task command
- createMockTaskCommand := func(t *testing.T, shouldSucceed bool) {
- t.Helper()
-
- // Create a mock script that simulates the task command
- mockScript := `#!/bin/sh
-if [ "$1" = "add" ] && [ "$2" = "+timrtest" ]; then
- if [ "%s" = "true" ]; then
- echo "Created task 1."
- exit 0
- else
- echo "Error: Failed to add task"
- exit 1
- fi
-fi
-echo "Invalid command"
-exit 1
-`
- scriptContent := fmt.Sprintf(mockScript, shouldSucceed)
-
- // Create temp directory for our mock
- tempDir := t.TempDir()
- mockPath := filepath.Join(tempDir, "task")
-
- // Write the mock script
- if err := os.WriteFile(mockPath, []byte(scriptContent), 0o755); err != nil {
- t.Fatalf("Failed to create mock script: %v", err)
- }
-
- // Update PATH to use our mock
- oldPath := os.Getenv("PATH")
- os.Setenv("PATH", tempDir+":"+oldPath)
- t.Cleanup(func() {
- os.Setenv("PATH", oldPath)
- })
- }
-
t.Run("TrackWithRunningTimer", func(t *testing.T) {
setup(t)
- createMockTaskCommand(t, true)
+ tracker := &mockTracker{}
// Start timer and let it run for a bit
state, _ := LoadState()
@@ -262,33 +237,29 @@ exit 1
state.ElapsedTime = 0
state.Save()
- // We'll modify TrackTime to use +timrtest for testing
- // For now, test with the actual implementation
- // In a real scenario, we'd want to make the tag configurable
-
- // Since we can't easily test the actual command execution,
- // we'll test the error case when task command is not found
- msg, err := TrackTime("test description")
-
- // We expect an error because 'task' command likely doesn't exist
- // or our mock won't match the exact command
- if err == nil {
- // If it somehow succeeded, check the message
- if msg == "" {
- t.Error("TrackTime() returned empty message on success")
- }
+ msg, err := TrackTimeWithTracker("test description", tracker)
+ if err != nil {
+ t.Fatalf("TrackTimeWithTracker() error = %v", err)
+ }
+ if msg == "" {
+ t.Fatal("TrackTimeWithTracker() msg is empty")
+ }
+ if tracker.calls != 1 {
+ t.Fatalf("tracker calls = %d, want 1", tracker.calls)
+ }
+ if tracker.minutes < 4 || tracker.minutes > 6 {
+ t.Fatalf("tracker minutes = %d, want around 5", tracker.minutes)
}
- // Verify timer was stopped
state, _ = LoadState()
- if state.Running {
- t.Error("Timer should be stopped after track attempt")
+ if state.Running || state.ElapsedTime != 0 {
+ t.Fatalf("state after tracking = %+v, want reset stopped state", state)
}
})
- t.Run("TrackWithStoppedTimer", func(t *testing.T) {
+ t.Run("TrackFailureKeepsElapsedState", func(t *testing.T) {
setup(t)
- createMockTaskCommand(t, true)
+ tracker := &mockTracker{err: errTestTracker}
// Set up a stopped timer with some elapsed time
state, _ := LoadState()
@@ -296,34 +267,27 @@ exit 1
state.ElapsedTime = 10 * time.Minute
state.Save()
- // Try to track time
- _, err := TrackTime("another test")
-
- // We expect an error because task command likely doesn't exist
- // but we can verify the state handling
+ _, err := TrackTimeWithTracker("another test", tracker)
if err == nil {
- // Verify timer was reset on success
- state, _ = LoadState()
- if state.ElapsedTime != 0 {
- t.Error("Timer should be reset after successful track")
- }
+ t.Fatal("TrackTimeWithTracker() error = nil, want tracker error")
+ }
+
+ state, _ = LoadState()
+ if state.Running {
+ t.Fatal("state.Running = true, want false after failed tracking")
+ }
+ if state.ElapsedTime != 10*time.Minute {
+ t.Fatalf("state.ElapsedTime = %v, want %v", state.ElapsedTime, 10*time.Minute)
}
})
- t.Run("TrackWithZeroTime", func(t *testing.T) {
+ t.Run("NilTracker", func(t *testing.T) {
setup(t)
-
- // Fresh timer with no elapsed time
- state, _ := LoadState()
- state.Running = false
- state.ElapsedTime = 0
- state.Save()
-
- // Try to track with zero time
- _, err := TrackTime("zero time test")
-
- // Even with zero time, the command should be attempted
- // We just verify no panic occurs
- _ = err
+ _, err := TrackTimeWithTracker("zero time test", nil)
+ if err == nil {
+ t.Fatal("TrackTimeWithTracker() error = nil, want error")
+ }
})
}
+
+var errTestTracker = errors.New("tracker failure")