diff options
| author | Paul Buetow <paul@buetow.org> | 2026-03-03 23:57:40 +0200 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-03-03 23:57:40 +0200 |
| commit | 461ba1af2801dbae3661ce4edc2f470ceb70e792 (patch) | |
| tree | bc6ed1df19b2fbb861922de444dd11a5ba074c60 | |
| parent | a6df85f96e618de0e38edc90e10d2b3925b5c91f (diff) | |
timer: inject tracker dependency for TrackTime
| -rw-r--r-- | internal/timer/operations.go | 39 | ||||
| -rw-r--r-- | internal/timer/operations_test.go | 134 |
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") |
