summaryrefslogtreecommitdiff
path: root/internal/task
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-06-22 09:29:56 +0300
committerPaul Buetow <paul@buetow.org>2026-06-22 09:29:56 +0300
commit735276fe213f9d640c226dbd8cf9730bc7e620bb (patch)
tree330c26fb0527b6348f1a71e148e8e8b5863e297c /internal/task
parent1888b70cf4981cabaef0b6e33e0ae9982d3a788e (diff)
Fix task operation cancellation for lq0
Diffstat (limited to 'internal/task')
-rw-r--r--internal/task/task.go46
-rw-r--r--internal/task/task_test.go128
2 files changed, 165 insertions, 9 deletions
diff --git a/internal/task/task.go b/internal/task/task.go
index ee9f0c9..e419f0a 100644
--- a/internal/task/task.go
+++ b/internal/task/task.go
@@ -63,7 +63,11 @@ type CompletionSources struct {
}
func run(args ...string) error {
- _, err := RunArgs(context.Background(), args)
+ return runContext(context.Background(), args...)
+}
+
+func runContext(ctx context.Context, args ...string) error {
+ _, err := RunArgs(ctx, args)
return err
}
@@ -342,6 +346,12 @@ func SetPriority(id int, priority string) error {
// AddTags adds tags to the task with the given id.
func AddTags(id int, tags []string) error {
+ return AddTagsContext(context.Background(), id, tags)
+}
+
+// AddTagsContext adds tags to the task with the given id using ctx for the
+// underlying Taskwarrior command.
+func AddTagsContext(ctx context.Context, id int, tags []string) error {
if id <= 0 {
return fmt.Errorf("invalid task ID: %d", id)
}
@@ -352,11 +362,17 @@ func AddTags(id int, tags []string) error {
}
args = append(args, t)
}
- return run(args...)
+ return runContext(ctx, args...)
}
// RemoveTags removes tags from the task with the given id.
func RemoveTags(id int, tags []string) error {
+ return RemoveTagsContext(context.Background(), id, tags)
+}
+
+// RemoveTagsContext removes tags from the task with the given id using ctx for
+// the underlying Taskwarrior command.
+func RemoveTagsContext(ctx context.Context, id int, tags []string) error {
if id <= 0 {
return fmt.Errorf("invalid task ID: %d", id)
}
@@ -367,7 +383,7 @@ func RemoveTags(id int, tags []string) error {
}
args = append(args, t)
}
- return run(args...)
+ return runContext(ctx, args...)
}
// SetTags sets the tags of the task with the given id to exactly the provided set.
@@ -405,12 +421,12 @@ func SetTags(ctx context.Context, id int, tags []string) error {
}
if len(adds) > 0 {
- if err := AddTags(id, adds); err != nil {
+ if err := AddTagsContext(ctx, id, adds); err != nil {
return err
}
}
if len(removes) > 0 {
- if err := RemoveTags(id, removes); err != nil {
+ if err := RemoveTagsContext(ctx, id, removes); err != nil {
return err
}
}
@@ -439,10 +455,16 @@ func SetProject(id int, project string) error {
// Annotate adds an annotation to the task with the given id.
func Annotate(id int, text string) error {
+ return AnnotateContext(context.Background(), id, text)
+}
+
+// AnnotateContext adds an annotation to the task with the given id using ctx
+// for the underlying Taskwarrior command.
+func AnnotateContext(ctx context.Context, id int, text string) error {
if id <= 0 {
return fmt.Errorf("invalid task ID: %d", id)
}
- return run(strconv.Itoa(id), "annotate", text)
+ return runContext(ctx, strconv.Itoa(id), "annotate", text)
}
// Denotate removes an annotation from the task with the given id.
@@ -450,6 +472,12 @@ func Annotate(id int, text string) error {
// annotation text is matched exactly when provided. If text is empty, the
// oldest annotation is removed.
func Denotate(id int, text string) error {
+ return DenotateContext(context.Background(), id, text)
+}
+
+// DenotateContext removes an annotation from the task with the given id using
+// ctx for the underlying Taskwarrior command.
+func DenotateContext(ctx context.Context, id int, text string) error {
if id <= 0 {
return fmt.Errorf("invalid task ID: %d", id)
}
@@ -457,7 +485,7 @@ func Denotate(id int, text string) error {
if text != "" {
args = append(args, text)
}
- return run(args...)
+ return runContext(ctx, args...)
}
// ReplaceAnnotations removes all existing annotations from the task with the
@@ -476,14 +504,14 @@ func ReplaceAnnotations(ctx context.Context, id int, text string) error {
}
anns := tasks[0].Annotations
for i := len(anns) - 1; i >= 0; i-- {
- if err := Denotate(id, anns[i].Description); err != nil {
+ if err := DenotateContext(ctx, id, anns[i].Description); err != nil {
return err
}
}
if text == "" {
return nil
}
- return Annotate(id, text)
+ return AnnotateContext(ctx, id, text)
}
// Edit opens the task in an editor for manual modification.
diff --git a/internal/task/task_test.go b/internal/task/task_test.go
index 59f94ba..2bca940 100644
--- a/internal/task/task_test.go
+++ b/internal/task/task_test.go
@@ -262,6 +262,134 @@ func TestExportReturnsCapturedErrorOutput(t *testing.T) {
}
}
+func TestSetTagsHonorsContextDuringMutations(t *testing.T) {
+ tmp := t.TempDir()
+ taskPath := filepath.Join(tmp, "task")
+ script := "#!/bin/sh\n" +
+ "for arg in \"$@\"; do\n" +
+ " if [ \"$arg\" = modify ]; then\n" +
+ " sleep 5\n" +
+ " exit 0\n" +
+ " fi\n" +
+ "done\n" +
+ "printf '%s\\n' '{\"id\":1,\"tags\":[\"old\"]}'\n"
+ if err := os.WriteFile(taskPath, []byte(script), 0o755); err != nil {
+ t.Fatal(err)
+ }
+
+ origPath := os.Getenv("PATH")
+ t.Setenv("PATH", tmp+":"+origPath)
+
+ ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
+ defer cancel()
+
+ start := time.Now()
+ err := SetTags(ctx, 1, []string{"new"})
+ elapsed := time.Since(start)
+ if !errors.Is(err, context.DeadlineExceeded) {
+ t.Fatalf("SetTags error = %v, want context deadline exceeded", err)
+ }
+ if elapsed > time.Second {
+ t.Fatalf("SetTags took %s, expected prompt context cancellation", elapsed)
+ }
+}
+
+func TestSetTagsHonorsContextDuringRemovals(t *testing.T) {
+ tmp := t.TempDir()
+ taskPath := filepath.Join(tmp, "task")
+ script := "#!/bin/sh\n" +
+ "for arg in \"$@\"; do\n" +
+ " if [ \"$arg\" = modify ]; then\n" +
+ " sleep 5\n" +
+ " exit 0\n" +
+ " fi\n" +
+ "done\n" +
+ "printf '%s\\n' '{\"id\":1,\"tags\":[\"old\"]}'\n"
+ if err := os.WriteFile(taskPath, []byte(script), 0o755); err != nil {
+ t.Fatal(err)
+ }
+
+ origPath := os.Getenv("PATH")
+ t.Setenv("PATH", tmp+":"+origPath)
+
+ ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
+ defer cancel()
+
+ start := time.Now()
+ err := SetTags(ctx, 1, nil)
+ elapsed := time.Since(start)
+ if !errors.Is(err, context.DeadlineExceeded) {
+ t.Fatalf("SetTags error = %v, want context deadline exceeded", err)
+ }
+ if elapsed > time.Second {
+ t.Fatalf("SetTags took %s, expected prompt context cancellation", elapsed)
+ }
+}
+
+func TestReplaceAnnotationsHonorsContextDuringMutations(t *testing.T) {
+ tmp := t.TempDir()
+ taskPath := filepath.Join(tmp, "task")
+ script := "#!/bin/sh\n" +
+ "for arg in \"$@\"; do\n" +
+ " if [ \"$arg\" = denotate ] || [ \"$arg\" = annotate ]; then\n" +
+ " sleep 5\n" +
+ " exit 0\n" +
+ " fi\n" +
+ "done\n" +
+ "printf '%s\\n' '{\"id\":1,\"annotations\":[{\"entry\":\"20260622T000000Z\",\"description\":\"old note\"}]}'\n"
+ if err := os.WriteFile(taskPath, []byte(script), 0o755); err != nil {
+ t.Fatal(err)
+ }
+
+ origPath := os.Getenv("PATH")
+ t.Setenv("PATH", tmp+":"+origPath)
+
+ ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
+ defer cancel()
+
+ start := time.Now()
+ err := ReplaceAnnotations(ctx, 1, "new note")
+ elapsed := time.Since(start)
+ if !errors.Is(err, context.DeadlineExceeded) {
+ t.Fatalf("ReplaceAnnotations error = %v, want context deadline exceeded", err)
+ }
+ if elapsed > time.Second {
+ t.Fatalf("ReplaceAnnotations took %s, expected prompt context cancellation", elapsed)
+ }
+}
+
+func TestReplaceAnnotationsHonorsContextDuringAnnotate(t *testing.T) {
+ tmp := t.TempDir()
+ taskPath := filepath.Join(tmp, "task")
+ script := "#!/bin/sh\n" +
+ "for arg in \"$@\"; do\n" +
+ " if [ \"$arg\" = annotate ]; then\n" +
+ " sleep 5\n" +
+ " exit 0\n" +
+ " fi\n" +
+ "done\n" +
+ "printf '%s\\n' '{\"id\":1,\"annotations\":[]}'\n"
+ if err := os.WriteFile(taskPath, []byte(script), 0o755); err != nil {
+ t.Fatal(err)
+ }
+
+ origPath := os.Getenv("PATH")
+ t.Setenv("PATH", tmp+":"+origPath)
+
+ ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
+ defer cancel()
+
+ start := time.Now()
+ err := ReplaceAnnotations(ctx, 1, "new note")
+ elapsed := time.Since(start)
+ if !errors.Is(err, context.DeadlineExceeded) {
+ t.Fatalf("ReplaceAnnotations error = %v, want context deadline exceeded", err)
+ }
+ if elapsed > time.Second {
+ t.Fatalf("ReplaceAnnotations took %s, expected prompt context cancellation", elapsed)
+ }
+}
+
func TestLoadCompletionSources(t *testing.T) {
tmp := t.TempDir()
taskPath := filepath.Join(tmp, "task")