diff options
| author | Paul Buetow <paul@buetow.org> | 2026-06-22 13:51:57 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-06-22 13:51:57 +0300 |
| commit | c3f3c356beda685e0327fd7346b43310fba990ab (patch) | |
| tree | 49c3a6b172ffa7215839ecd778358be2567552c0 | |
| parent | ea9fe5e52b3a95651792b5781327f6100afd8fcb (diff) | |
Fix lq0 task mutation cancellation
| -rw-r--r-- | internal/task/task.go | 125 | ||||
| -rw-r--r-- | internal/task/task_test.go | 49 | ||||
| -rw-r--r-- | internal/ui/editor_handlers.go | 4 | ||||
| -rw-r--r-- | internal/ui/handlers.go | 33 | ||||
| -rw-r--r-- | internal/ui/keyactions.go | 49 | ||||
| -rw-r--r-- | internal/ui/table.go | 18 | ||||
| -rw-r--r-- | internal/ui/table_test.go | 41 |
7 files changed, 269 insertions, 50 deletions
diff --git a/internal/task/task.go b/internal/task/task.go index e419f0a..5777af8 100644 --- a/internal/task/task.go +++ b/internal/task/task.go @@ -73,18 +73,26 @@ func runContext(ctx context.Context, args ...string) error { // modifyTask runs a modify command with validation func modifyTask(id int, args ...string) error { + return modifyTaskContext(context.Background(), id, args...) +} + +func modifyTaskContext(ctx context.Context, id int, args ...string) error { if id <= 0 { return fmt.Errorf("invalid task ID: %d", id) } - return run(append([]string{strconv.Itoa(id), "modify"}, args...)...) + return runContext(ctx, append([]string{strconv.Itoa(id), "modify"}, args...)...) } // simpleTaskCommand runs a simple command on a task with validation func simpleTaskCommand(id int, command string) error { + return simpleTaskCommandContext(context.Background(), id, command) +} + +func simpleTaskCommandContext(ctx context.Context, id int, command string) error { if id <= 0 { return fmt.Errorf("invalid task ID: %d", id) } - return run(strconv.Itoa(id), command) + return runContext(ctx, strconv.Itoa(id), command) } // debugConfig groups the optional debug-logging state for the task package. @@ -125,6 +133,12 @@ func SetDebugLog(path string) error { // Add creates a new task with the given description and tags. func Add(description string, tags []string) error { + return AddContext(context.Background(), description, tags) +} + +// AddContext creates a new task with the given description and tags using ctx +// for the underlying Taskwarrior command. +func AddContext(ctx context.Context, description string, tags []string) error { var args []string for _, t := range tags { if len(t) > 0 && t[0] != '+' { @@ -133,25 +147,38 @@ func Add(description string, tags []string) error { args = append(args, t) } args = append(args, description) - return AddArgs(args) + return AddArgsContext(ctx, args) } // AddArgs runs "task add" with the provided arguments. Each element in args // is passed as a separate command-line argument, allowing the caller to // specify additional modifiers like due dates or tags. func AddArgs(args []string) error { - return run(append([]string{"add"}, args...)...) + return AddArgsContext(context.Background(), args) +} + +// AddArgsContext runs "task add" with the provided arguments using ctx for the +// underlying Taskwarrior command. +func AddArgsContext(ctx context.Context, args []string) error { + return runContext(ctx, append([]string{"add"}, args...)...) } // AddLine splits the given line into shell words and runs "task add" with the // resulting arguments. This allows users to pass raw Taskwarrior parameters // such as "due:today" directly. func AddLine(line string) error { + return AddLineContext(context.Background(), line) +} + +// AddLineContext splits the given line into shell words and runs "task add" +// with the resulting arguments using ctx for the underlying Taskwarrior +// command. +func AddLineContext(ctx context.Context, line string) error { fields, err := shlex.Split(line) if err != nil { return err } - return AddArgs(fields) + return AddArgsContext(ctx, fields) } // RunLine splits line using shell-word rules and runs the resulting task @@ -302,12 +329,24 @@ func Export(ctx context.Context, filters ...string) ([]Task, error) { // SetStatus changes the status of the task with the given id. func SetStatus(id int, status string) error { - return modifyTask(id, "status:"+status) + return SetStatusContext(context.Background(), id, status) +} + +// SetStatusContext changes the status of the task with the given id using ctx +// for the underlying Taskwarrior command. +func SetStatusContext(ctx context.Context, id int, status string) error { + return modifyTaskContext(ctx, id, "status:"+status) } // SetStatusUUID changes the status of the task with the given UUID. func SetStatusUUID(uuid, status string) error { - return run(uuid, "modify", "status:"+status) + return SetStatusUUIDContext(context.Background(), uuid, status) +} + +// SetStatusUUIDContext changes the status of the task with the given UUID +// using ctx for the underlying Taskwarrior command. +func SetStatusUUIDContext(ctx context.Context, uuid, status string) error { + return runContext(ctx, uuid, "modify", "status:"+status) } // RecurringSeries returns the recurring template and generated instances for @@ -321,27 +360,57 @@ func RecurringSeries(ctx context.Context, rootUUID string) ([]Task, error) { // Start begins the task with the given id. func Start(id int) error { - return simpleTaskCommand(id, "start") + return StartContext(context.Background(), id) +} + +// StartContext begins the task with the given id using ctx for the underlying +// Taskwarrior command. +func StartContext(ctx context.Context, id int) error { + return simpleTaskCommandContext(ctx, id, "start") } // Stop stops the task with the given id. func Stop(id int) error { - return simpleTaskCommand(id, "stop") + return StopContext(context.Background(), id) +} + +// StopContext stops the task with the given id using ctx for the underlying +// Taskwarrior command. +func StopContext(ctx context.Context, id int) error { + return simpleTaskCommandContext(ctx, id, "stop") } // Done marks the task with the given id as completed. func Done(id int) error { - return simpleTaskCommand(id, "done") + return DoneContext(context.Background(), id) +} + +// DoneContext marks the task with the given id as completed using ctx for the +// underlying Taskwarrior command. +func DoneContext(ctx context.Context, id int) error { + return simpleTaskCommandContext(ctx, id, "done") } // Delete removes the task with the given id. func Delete(id int) error { - return simpleTaskCommand(id, "delete") + return DeleteContext(context.Background(), id) +} + +// DeleteContext removes the task with the given id using ctx for the +// underlying Taskwarrior command. +func DeleteContext(ctx context.Context, id int) error { + return simpleTaskCommandContext(ctx, id, "delete") } // SetPriority changes the priority of the task with the given id. func SetPriority(id int, priority string) error { - return modifyTask(id, "priority:"+priority) + return SetPriorityContext(context.Background(), id, priority) +} + +// SetPriorityContext changes the priority of the task with the given id using +// ctx for the underlying Taskwarrior command. +func SetPriorityContext(ctx context.Context, id int, priority string) error { + return modifyTaskContext(ctx, id, "priority:"+priority) } // AddTags adds tags to the task with the given id. @@ -435,22 +504,46 @@ func SetTags(ctx context.Context, id int, tags []string) error { // SetRecurrence sets the recurrence for the task with the given id. func SetRecurrence(id int, rec string) error { - return modifyTask(id, "recur:"+rec) + return SetRecurrenceContext(context.Background(), id, rec) +} + +// SetRecurrenceContext sets the recurrence for the task with the given id +// using ctx for the underlying Taskwarrior command. +func SetRecurrenceContext(ctx context.Context, id int, rec string) error { + return modifyTaskContext(ctx, id, "recur:"+rec) } // SetDueDate sets the due date for the task with the given id. func SetDueDate(id int, due string) error { - return modifyTask(id, "due:"+due) + return SetDueDateContext(context.Background(), id, due) +} + +// SetDueDateContext sets the due date for the task with the given id using ctx +// for the underlying Taskwarrior command. +func SetDueDateContext(ctx context.Context, id int, due string) error { + return modifyTaskContext(ctx, id, "due:"+due) } // SetDescription changes the description of the task with the given id. func SetDescription(id int, desc string) error { - return modifyTask(id, "description:"+desc) + return SetDescriptionContext(context.Background(), id, desc) +} + +// SetDescriptionContext changes the description of the task with the given id +// using ctx for the underlying Taskwarrior command. +func SetDescriptionContext(ctx context.Context, id int, desc string) error { + return modifyTaskContext(ctx, id, "description:"+desc) } // SetProject changes the project of the task with the given id. func SetProject(id int, project string) error { - return modifyTask(id, "project:"+project) + return SetProjectContext(context.Background(), id, project) +} + +// SetProjectContext changes the project of the task with the given id using +// ctx for the underlying Taskwarrior command. +func SetProjectContext(ctx context.Context, id int, project string) error { + return modifyTaskContext(ctx, id, "project:"+project) } // Annotate adds an annotation to the task with the given id. diff --git a/internal/task/task_test.go b/internal/task/task_test.go index 2bca940..f35f167 100644 --- a/internal/task/task_test.go +++ b/internal/task/task_test.go @@ -262,6 +262,55 @@ func TestExportReturnsCapturedErrorOutput(t *testing.T) { } } +func TestMutationHelpersHonorContextCancellation(t *testing.T) { + tmp := t.TempDir() + taskPath := filepath.Join(tmp, "task") + script := "#!/bin/sh\nsleep 5\n" + if err := os.WriteFile(taskPath, []byte(script), 0o755); err != nil { + t.Fatal(err) + } + + origPath := os.Getenv("PATH") + t.Setenv("PATH", tmp+":"+origPath) + + tests := []struct { + name string + run func(context.Context) error + }{ + {"AddContext", func(ctx context.Context) error { return AddContext(ctx, "new task", []string{"tag"}) }}, + {"AddArgsContext", func(ctx context.Context) error { return AddArgsContext(ctx, []string{"new task", "+tag"}) }}, + {"AddLineContext", func(ctx context.Context) error { return AddLineContext(ctx, `"new task" +tag`) }}, + {"SetStatusContext", func(ctx context.Context) error { return SetStatusContext(ctx, 1, "pending") }}, + {"SetStatusUUIDContext", func(ctx context.Context) error { return SetStatusUUIDContext(ctx, "task-uuid", "pending") }}, + {"StartContext", func(ctx context.Context) error { return StartContext(ctx, 1) }}, + {"StopContext", func(ctx context.Context) error { return StopContext(ctx, 1) }}, + {"DoneContext", func(ctx context.Context) error { return DoneContext(ctx, 1) }}, + {"DeleteContext", func(ctx context.Context) error { return DeleteContext(ctx, 1) }}, + {"SetPriorityContext", func(ctx context.Context) error { return SetPriorityContext(ctx, 1, "H") }}, + {"SetRecurrenceContext", func(ctx context.Context) error { return SetRecurrenceContext(ctx, 1, "daily") }}, + {"SetDueDateContext", func(ctx context.Context) error { return SetDueDateContext(ctx, 1, "tomorrow") }}, + {"SetDescriptionContext", func(ctx context.Context) error { return SetDescriptionContext(ctx, 1, "new description") }}, + {"SetProjectContext", func(ctx context.Context) error { return SetProjectContext(ctx, 1, "home") }}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond) + defer cancel() + + start := time.Now() + err := tt.run(ctx) + elapsed := time.Since(start) + if !errors.Is(err, context.DeadlineExceeded) { + t.Fatalf("%s error = %v, want context deadline exceeded", tt.name, err) + } + if elapsed > time.Second { + t.Fatalf("%s took %s, expected prompt context cancellation", tt.name, elapsed) + } + }) + } +} + func TestSetTagsHonorsContextDuringMutations(t *testing.T) { tmp := t.TempDir() taskPath := filepath.Join(tmp, "task") diff --git a/internal/ui/editor_handlers.go b/internal/ui/editor_handlers.go index 8f81420..2db20d0 100644 --- a/internal/ui/editor_handlers.go +++ b/internal/ui/editor_handlers.go @@ -56,7 +56,9 @@ func (m *Model) handleDescEditDone(msg descEditDoneMsg) (tea.Model, tea.Cmd) { // Update the description newDesc := strings.TrimSpace(string(content)) if m.currentTaskDetail != nil { - err = task.SetDescription(m.currentTaskDetail.ID, newDesc) + ctx, cancel := m.taskOperationContext() + err = task.SetDescriptionContext(ctx, m.currentTaskDetail.ID, newDesc) + cancel() if err != nil { m.statusMsg = fmt.Sprintf("Error updating description: %v", err) cmd := tea.Tick(2*time.Second, func(time.Time) tea.Msg { diff --git a/internal/ui/handlers.go b/internal/ui/handlers.go index 45c0b10..05309b2 100644 --- a/internal/ui/handlers.go +++ b/internal/ui/handlers.go @@ -47,14 +47,14 @@ func (m *Model) handleAnnotationMode(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) { } if m.replaceAnnotations { - ctx, cancel := m.taskExportContext() + ctx, cancel := m.taskOperationContext() defer cancel() if err := task.ReplaceAnnotations(ctx, m.annotateID, value); err != nil { return err } m.replaceAnnotations = false } else { - ctx, cancel := m.taskExportContext() + ctx, cancel := m.taskOperationContext() defer cancel() if err := task.AnnotateContext(ctx, m.annotateID, value); err != nil { return err @@ -85,7 +85,9 @@ func (m *Model) handleDescriptionMode(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) if err := validateDescription(value); err != nil { return err } - if err := task.SetDescription(m.descID, value); err != nil { + ctx, cancel := m.taskOperationContext() + defer cancel() + if err := task.SetDescriptionContext(ctx, m.descID, value); err != nil { return err } if err := m.reload(); err != nil { @@ -130,7 +132,7 @@ func (m *Model) handleTagsMode(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) { } } if len(adds) > 0 || len(removes) > 0 { - ctx, cancel := m.taskExportContext() + ctx, cancel := m.taskOperationContext() defer cancel() if len(adds) > 0 { if err := task.AddTagsContext(ctx, m.tagsID, adds); err != nil { @@ -168,7 +170,10 @@ func (m *Model) handleTagsMode(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) { func (m *Model) handleDueEditMode(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) { switch msg.String() { case "enter": - if err := task.SetDueDate(m.dueID, m.dueDate.Format("2006-01-02")); err != nil { + ctx, cancel := m.taskOperationContext() + err := task.SetDueDateContext(ctx, m.dueID, m.dueDate.Format("2006-01-02")) + cancel() + if err != nil { m.statusMsg = fmt.Sprintf("Error: %v", err) cmd := tea.Tick(2*time.Second, func(time.Time) tea.Msg { return struct{ clearStatus bool }{true} @@ -213,7 +218,9 @@ func (m *Model) handleRecurrenceMode(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) { if err := validateRecurrence(value); err != nil { return err } - if err := task.SetRecurrence(m.recurID, value); err != nil { + ctx, cancel := m.taskOperationContext() + defer cancel() + if err := task.SetRecurrenceContext(ctx, m.recurID, value); err != nil { return err } if err := m.reload(); err != nil { @@ -244,7 +251,9 @@ func (m *Model) handleRecurrenceMode(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) { // handleProjectMode handles project editing func (m *Model) handleProjectMode(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) { onEnter := func(value string) error { - return task.SetProject(m.projID, value) + ctx, cancel := m.taskOperationContext() + defer cancel() + return task.SetProjectContext(ctx, m.projID, value) } onExit := func() { @@ -275,7 +284,10 @@ func (m *Model) handlePriorityMode(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) { }) return m, cmd } - if err := task.SetPriority(m.priorityID, priority); err != nil { + ctx, cancel := m.taskOperationContext() + err := task.SetPriorityContext(ctx, m.priorityID, priority) + cancel() + if err != nil { m.statusMsg = fmt.Sprintf("Error: %v", err) cmd := tea.Tick(2*time.Second, func(time.Time) tea.Msg { return struct{ clearStatus bool }{true} @@ -351,7 +363,10 @@ func (m *Model) handleAddTaskMode(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) { oldIDs[tsk.ID] = struct{}{} } - if err := task.AddLine(m.addInput.Value()); err != nil { + ctx, cancel := m.taskOperationContext() + err := task.AddLineContext(ctx, m.addInput.Value()) + cancel() + if err != nil { m.statusMsg = fmt.Sprintf("Error: %v", err) cmd := tea.Tick(2*time.Second, func(time.Time) tea.Msg { return struct{ clearStatus bool }{true} diff --git a/internal/ui/keyactions.go b/internal/ui/keyactions.go index c84c172..4d8c682 100644 --- a/internal/ui/keyactions.go +++ b/internal/ui/keyactions.go @@ -1,6 +1,7 @@ package ui import ( + "context" "fmt" "math/rand" "os/exec" @@ -38,12 +39,18 @@ func (m *Model) handleToggleStart() (tea.Model, tea.Cmd) { } if started { - if err := task.Stop(id); err != nil { + ctx, cancel := m.taskOperationContext() + err := task.StopContext(ctx, id) + cancel() + if err != nil { m.showError(err) return m, nil } } else { - if err := task.Start(id); err != nil { + ctx, cancel := m.taskOperationContext() + err := task.StartContext(ctx, id) + cancel() + if err != nil { m.showError(err) return m, nil } @@ -119,12 +126,15 @@ func (m *Model) handleUndo() (tea.Model, tea.Cmd) { } action := m.undoStack[len(m.undoStack)-1] + ctx, cancel := m.taskOperationContext() for _, restore := range action.restores { - if err := task.SetStatusUUID(restore.uuid, restore.status); err != nil { + if err := task.SetStatusUUIDContext(ctx, restore.uuid, restore.status); err != nil { + cancel() m.showError(err) return m, nil } } + cancel() m.undoStack = m.undoStack[:len(m.undoStack)-1] // Reload the task list to get the updated task with its new ID @@ -159,7 +169,7 @@ func (m *Model) handleUndo() (tea.Model, tea.Cmd) { } filters = append(filters, "status:"+restore.status) - ctx, cancel := m.taskExportContext() + ctx, cancel := m.taskOperationContext() tasks, err := task.Export(ctx, filters...) cancel() if err == nil && len(tasks) > 0 { @@ -199,10 +209,10 @@ func (m *Model) deleteTaskWithUndo(tsk task.Task) (int, bool, error) { recurring := isRecurringTask(tsk) tasks := []task.Task{tsk} + ctx, cancel := m.taskOperationContext() + defer cancel() if recurring { - ctx, cancel := m.taskExportContext() series, err := task.RecurringSeries(ctx, recurringRootUUID(tsk)) - cancel() if err != nil { return 0, true, fmt.Errorf("loading recurring series: %w", err) } @@ -223,8 +233,8 @@ func (m *Model) deleteTaskWithUndo(tsk task.Task) (int, bool, error) { completed := make([]undoRestore, 0, len(restores)) for _, restore := range restores { - if err := task.SetStatusUUID(restore.uuid, "deleted"); err != nil { - rollbackUndoRestores(completed) + if err := task.SetStatusUUIDContext(ctx, restore.uuid, "deleted"); err != nil { + rollbackUndoRestores(ctx, completed) return 0, recurring, fmt.Errorf("deleting task %s: %w", restore.uuid, err) } completed = append(completed, restore) @@ -294,9 +304,9 @@ func undoStatusForTask(tsk task.Task) string { return tsk.Status } -func rollbackUndoRestores(restores []undoRestore) { +func rollbackUndoRestores(ctx context.Context, restores []undoRestore) { for i := len(restores) - 1; i >= 0; i-- { - _ = task.SetStatusUUID(restores[i].uuid, restores[i].status) + _ = task.SetStatusUUIDContext(ctx, restores[i].uuid, restores[i].status) } } @@ -328,7 +338,10 @@ func (m *Model) handleRemoveDueDate() (tea.Model, tea.Cmd) { } // In Taskwarrior, passing an empty value to due: removes the due date - if err := task.SetDueDate(id, ""); err != nil { + ctx, cancel := m.taskOperationContext() + err = task.SetDueDateContext(ctx, id, "") + cancel() + if err != nil { m.showError(err) return m, nil } @@ -348,7 +361,10 @@ func (m *Model) handleRandomDueDate() (tea.Model, tea.Cmd) { days := rand.Intn(31) + 7 due := time.Now().AddDate(0, 0, days).Format("2006-01-02") - if err := task.SetDueDate(id, due); err != nil { + ctx, cancel := m.taskOperationContext() + err = task.SetDueDateContext(ctx, id, due) + cancel() + if err != nil { m.showError(err) return m, nil } @@ -489,18 +505,21 @@ func (m *Model) handleTagToProject() (tea.Model, tea.Cmd) { firstTag := currentTask.Tags[0] // Set the tag as project - if err := task.SetProject(id, firstTag); err != nil { + ctx, cancel := m.taskOperationContext() + err = task.SetProjectContext(ctx, id, firstTag) + if err != nil { + cancel() m.showError(err) return m, nil } // Remove the tag from the task - ctx, cancel := m.taskExportContext() - defer cancel() if err := task.RemoveTagsContext(ctx, id, []string{firstTag}); err != nil { + cancel() m.showError(err) return m, nil } + cancel() if !m.reloadAndReport() { return m, nil diff --git a/internal/ui/table.go b/internal/ui/table.go index c948d89..f37d2ab 100644 --- a/internal/ui/table.go +++ b/internal/ui/table.go @@ -25,7 +25,7 @@ import ( var priorityOptions = []string{"H", "M", "L", ""} -const taskExportTimeout = 30 * time.Second +const taskOperationTimeout = 30 * time.Second var ( urlRegex = regexp.MustCompile(`https?://\S+`) @@ -278,9 +278,9 @@ func (m *Model) cancelTaskOperations() { } } -func (m *Model) taskExportContext() (context.Context, context.CancelFunc) { +func (m *Model) taskOperationContext() (context.Context, context.CancelFunc) { m.initTaskContext() - return context.WithTimeout(m.taskContext, taskExportTimeout) + return context.WithTimeout(m.taskContext, taskOperationTimeout) } // blinkInterval controls how quickly the row flashes when a task changes. @@ -397,7 +397,10 @@ func (m *Model) startBlink(id int, markDone bool) tea.Cmd { break } } - if err := task.Done(id); err != nil { + ctx, cancel := m.taskOperationContext() + err := task.DoneContext(ctx, id) + cancel() + if err != nil { m.showError(err) } } @@ -510,7 +513,7 @@ func (m *Model) fetchTasks() (reloadData, error) { // Always show only pending tasks by default. filters := append([]string(nil), m.filters...) filters = append(filters, "status:pending") - ctx, cancel := m.taskExportContext() + ctx, cancel := m.taskOperationContext() defer cancel() tasks, err := task.Export(ctx, filters...) @@ -752,7 +755,10 @@ func (m *Model) handleBlinkMsg() (tea.Model, tea.Cmd) { break } } - if err := task.Done(id); err != nil { + ctx, cancel := m.taskOperationContext() + err := task.DoneContext(ctx, id) + cancel() + if err != nil { m.showError(err) } } diff --git a/internal/ui/table_test.go b/internal/ui/table_test.go index 5549e97..8880a03 100644 --- a/internal/ui/table_test.go +++ b/internal/ui/table_test.go @@ -1503,9 +1503,9 @@ func TestEscDoesNotQuitFromTable(t *testing.T) { } } -func TestQuitCancelsTaskExportContext(t *testing.T) { +func TestQuitCancelsTaskOperationContext(t *testing.T) { m := Model{} - ctx, cancel := m.taskExportContext() + ctx, cancel := m.taskOperationContext() defer cancel() _, cmd := m.handleQuitKey() @@ -1513,7 +1513,42 @@ func TestQuitCancelsTaskExportContext(t *testing.T) { t.Fatal("quit returned nil command; want tea.Quit") } if !errors.Is(ctx.Err(), context.Canceled) { - t.Fatalf("task export context error = %v, want context canceled", ctx.Err()) + t.Fatalf("task operation context error = %v, want context canceled", ctx.Err()) + } +} + +func TestCanceledTaskOperationContextReachesToggleStart(t *testing.T) { + tmp := t.TempDir() + taskPath := filepath.Join(tmp, "task") + startedFile := filepath.Join(tmp, "started") + + script := fmt.Sprintf("#!/bin/sh\n"+ + "if echo \"$@\" | grep -q export; then\n"+ + " echo '{\"id\":1,\"uuid\":\"x\",\"description\":\"alpha\",\"status\":\"pending\",\"entry\":\"\",\"priority\":\"\",\"urgency\":0}'\n"+ + " exit 0\n"+ + "fi\n"+ + "if [ \"$1\" = \"1\" ] && [ \"$2\" = \"start\" ]; then\n"+ + " printf started > %q\n"+ + " exit 0\n"+ + "fi\n", startedFile) + if err := os.WriteFile(taskPath, []byte(script), 0o755); err != nil { + t.Fatal(err) + } + setupEnv(t, taskPath) + + m, err := New(nil, "firefox") + if err != nil { + t.Fatalf("New: %v", err) + } + m.cancelTaskOperations() + + mv, _ := (&m).Update(tea.KeyPressMsg{Code: 's', Text: "s"}) + m = *mv.(*Model) + if !strings.Contains(m.statusMsg, context.Canceled.Error()) { + t.Fatalf("status = %q, want context canceled error", m.statusMsg) + } + if _, err := os.Stat(startedFile); !errors.Is(err, os.ErrNotExist) { + t.Fatalf("start command ran despite canceled model context; stat error = %v", err) } } |
