From 97779f928d73d4fe50a53934b5f667333f8739e2 Mon Sep 17 00:00:00 2001 From: Robertus Chris Date: Sat, 21 Feb 2026 11:27:59 +0700 Subject: [PATCH] fix: pause state did not resume after end time The problem is that when we already hit or exceed the end time of pomodoro timer and we are resuming from the paused state, the timer will immediately finish, even though there's still time left when we pause the timer. The cause of the problem is because when we already hit or exceed the end time, we only updating the `PausePoint` to `nil` and not updating the `EndTime`. When we resume the timer, we should update the `EndTime` with the current time extended by the remaining duration from `PausePoint`. Let's say we paused at 5:30 A.M and the `EndTime` is 5:40 A.M, which means we have 10 minutes left. When we hit 5:45 A.M and resume the timer, we should update the `EndTime` to 5:55 A.M (extending 5:45 by 10 minutes). With that in mind, move the conditional check for expired `EndTime` after the check for `PausePoint` so that we don't messed up with the resume condition and still able to _not_ enter pause state when the `EndTime` already expired. --- internal/pause.go | 30 +++++------ internal/pause_test.go | 112 +++++++++++++++++++++++++++++++++++++++-- main.go | 2 +- 3 files changed, 125 insertions(+), 19 deletions(-) diff --git a/internal/pause.go b/internal/pause.go index 63b0c18..45814f0 100644 --- a/internal/pause.go +++ b/internal/pause.go @@ -8,7 +8,17 @@ import ( "github.com/bruhtus/simo/utils" ) -func Pause(statusPath string) { +func Pause( + now func() time.Time, + statusPath string, +) { + // Substitute this in test. + // Reference: + // https://stackoverflow.com/a/25791617 + if now == nil { + now = time.Now + } + var ( status = utils.ReadStatusFile(statusPath) isExpired = utils.DetermineIsExpired(status.EndTime) @@ -19,21 +29,9 @@ func Pause(statusPath string) { newPausePoint = fmt.Sprintf("%dm%ds", minutes, seconds) ) - if isExpired { - if status.PausePoint != nil { - status.PausePoint = nil - - statusJSON, err := json.Marshal(status) - utils.CheckError(err) - - utils.WriteStatusFile(statusPath, statusJSON) - } - return - } - if status.PausePoint != nil { remainingDuration := utils.ParseDuration(*status.PausePoint) - newEndTime := time.Now().Add(remainingDuration) + newEndTime := now().Add(remainingDuration) status.PausePoint = nil status.EndTime = newEndTime @@ -45,6 +43,10 @@ func Pause(statusPath string) { return } + if isExpired { + return + } + status.PausePoint = &newPausePoint statusJSON, err := json.Marshal(status) diff --git a/internal/pause_test.go b/internal/pause_test.go index ae70aea..fe96af2 100644 --- a/internal/pause_test.go +++ b/internal/pause_test.go @@ -28,6 +28,14 @@ func TestPause(t *testing.T) { isNotifyInput bool isNotifyOutput bool }{ + { + time.Duration(-1 * time.Second), "", + false, false, + }, + { + time.Duration(0 * time.Second), "", + false, false, + }, { time.Duration(1 * time.Second), "0m1s", true, true, @@ -37,7 +45,11 @@ func TestPause(t *testing.T) { false, false, }, { - time.Duration(1 * time.Hour), "60m0s", + time.Duration(60 * time.Minute), "60m0s", + true, true, + }, + { + time.Duration(90 * time.Minute), "90m0s", true, true, }, } @@ -54,12 +66,20 @@ func TestPause(t *testing.T) { } utils.TestSetupStatusFile(t, status, file) - scmd.Pause(file.Name()) + scmd.Pause(nil, file.Name()) resultJSON := utils.ReadStatusFile(file.Name()) - if resultJSON.PausePoint == nil { + + if resultJSON.PausePoint == nil && + tt.remainingDurationOutput != "" { + t.Errorf("Got unexpected nil") + } + + if resultJSON.PausePoint != nil && + *resultJSON.PausePoint != tt.remainingDurationOutput { t.Errorf( - "Got nil, want %s", + "Got %s, want %s", + *resultJSON.PausePoint, tt.remainingDurationOutput, ) } @@ -75,3 +95,87 @@ func TestPause(t *testing.T) { ) } } + +func TestResume(t *testing.T) { + dirPath := t.TempDir() + file := utils.TestSetupTempFile(t, dirPath) + + t.Cleanup(func() { + err := file.Close() + if err != nil { + t.Fatalf( + "Failed to close file: %v", + err, + ) + } + }) + + remainingDurationCases := []struct { + pausePoint string + endTime time.Duration + expectedEndTime time.Duration + }{ + { + "0m1s", + time.Duration(-1 * time.Second), + time.Duration(1 * time.Second), + }, + { + "0m10s", + time.Duration(-10 * time.Second), + time.Duration(10 * time.Second), + }, + { + "1m0s", + time.Duration(-1 * time.Minute), + time.Duration(1 * time.Minute), + }, + { + "60m0s", + time.Duration(-60 * time.Minute), + time.Duration(60 * time.Minute), + }, + { + "90m0s", + time.Duration(-90 * time.Minute), + time.Duration(90 * time.Minute), + }, + } + + for _, tt := range remainingDurationCases { + t.Run( + tt.endTime.String(), + func(t *testing.T) { + endTime := time.Now().Add(tt.endTime) + expectedEndTime := endTime.Add(tt.expectedEndTime) + + status := utils.Status{ + State: utils.StateFocus, + IsNotify: false, + PausePoint: &tt.pausePoint, + EndTime: endTime, + } + + utils.TestSetupStatusFile(t, status, file) + + // Substitute the time.Now() result. + now := func() time.Time { + return endTime + } + + scmd.Pause(now, file.Name()) + resultJSON := utils.ReadStatusFile(file.Name()) + + comparation := expectedEndTime.Compare(resultJSON.EndTime) + + if comparation != 0 { + t.Errorf( + "Got %v, want %v", + endTime, + expectedEndTime, + ) + } + }, + ) + } +} diff --git a/main.go b/main.go index 5385957..09c7953 100644 --- a/main.go +++ b/main.go @@ -93,7 +93,7 @@ func main() { utils.CheckError(err) case "pause": - scmd.Pause(statusPath) + scmd.Pause(nil, statusPath) default: utils.HelpUsage()