chore: add tests for the batch complete/delete paths
first tests in the repo. covers CompleteTasks/DeleteTasks on a multi-task selection, the recurrence path through a batch, and the not-found error on the single-task wrapper. the bulk ones are regression tests for 407feb5. closes 12b61de
diff --git a/actions_test.go b/actions_test.go
@@ -0,0 +1,128 @@
+package main
+
+import (
+ "os"
+ "path/filepath"
+ "slices"
+ "strings"
+ "testing"
+)
+
+// newTestConfig writes lines to a throwaway todo file and points a config at
+// it, with a done file alongside.
+func newTestConfig(t *testing.T, lines ...string) (*Config, string) {
+ t.Helper()
+
+ dir := t.TempDir()
+ todo := filepath.Join(dir, "todo.txt")
+ if err := os.WriteFile(todo, []byte(strings.Join(lines, "\n")+"\n"), 0644); err != nil {
+ t.Fatalf("writing todo file: %v", err)
+ }
+
+ return &Config{TodoFile: todo, DoneFile: filepath.Join(dir, "done.txt")}, todo
+}
+
+func readLines(t *testing.T, path string) []string {
+ t.Helper()
+
+ b, err := os.ReadFile(path)
+ if err != nil {
+ t.Fatalf("reading %s: %v", path, err)
+ }
+
+ trimmed := strings.TrimRight(string(b), "\n")
+ if trimmed == "" {
+ return nil
+ }
+ return strings.Split(trimmed, "\n")
+}
+
+// regression test for bulk completion. IDs are line numbers, so the old
+// approach of calling CompleteTask once per selected ID archived the wrong
+// tasks as soon as the first removal shifted everything below it.
+func TestCompleteTasksBulk(t *testing.T) {
+ cfg, todo := newTestConfig(t,
+ "first task +home",
+ "second task +home",
+ "third task +home",
+ "fourth task +home",
+ )
+
+ completed, err := CompleteTasks(cfg, []int{1, 2, 4})
+ if err != nil {
+ t.Fatalf("CompleteTasks: %v", err)
+ }
+ if len(completed) != 3 {
+ t.Fatalf("completed %d tasks, want 3", len(completed))
+ }
+
+ if remaining := readLines(t, todo); !slices.Equal(remaining, []string{"third task +home"}) {
+ t.Errorf("remaining = %v, want [third task +home]", remaining)
+ }
+
+ archived := strings.Join(readLines(t, cfg.DoneFile), "\n")
+ for _, want := range []string{"first task +home", "second task +home", "fourth task +home"} {
+ if !strings.Contains(archived, want) {
+ t.Errorf("done.txt missing %q; got:\n%s", want, archived)
+ }
+ }
+ if strings.Contains(archived, "third task") {
+ t.Errorf("done.txt archived a task that wasn't selected:\n%s", archived)
+ }
+}
+
+// same hazard as TestCompleteTasksBulk, on the delete path.
+func TestDeleteTasksBulk(t *testing.T) {
+ cfg, todo := newTestConfig(t,
+ "keep me +home",
+ "delete me +home",
+ "keep me too +home",
+ "delete me too +home",
+ )
+
+ deleted, err := DeleteTasks(cfg, []int{2, 4})
+ if err != nil {
+ t.Fatalf("DeleteTasks: %v", err)
+ }
+ if len(deleted) != 2 {
+ t.Fatalf("deleted %d tasks, want 2", len(deleted))
+ }
+
+ want := []string{"keep me +home", "keep me too +home"}
+ if remaining := readLines(t, todo); !slices.Equal(remaining, want) {
+ t.Errorf("remaining = %v, want %v", remaining, want)
+ }
+}
+
+// completing a recurring task in a batch should still queue up its next
+// occurrence, and that new line has to survive the same rewrite.
+func TestCompleteTasksGeneratesRecurrence(t *testing.T) {
+ cfg, todo := newTestConfig(t,
+ "water plants +home due:2026-01-01 rec:+1w",
+ "unrelated task +home",
+ )
+
+ if _, err := CompleteTasks(cfg, []int{1, 2}); err != nil {
+ t.Fatalf("CompleteTasks: %v", err)
+ }
+
+ remaining := readLines(t, todo)
+ if len(remaining) != 1 {
+ t.Fatalf("remaining = %v, want a single recurring line", remaining)
+ }
+ if !strings.Contains(remaining[0], "due:2026-01-08") {
+ t.Errorf("recurring line = %q, want due:2026-01-08", remaining[0])
+ }
+ if !strings.Contains(remaining[0], "rec:+1w") {
+ t.Errorf("recurring line lost its rec: tag: %q", remaining[0])
+ }
+}
+
+// the single-task wrappers still have to report a missing ID as an error.
+func TestCompleteTaskNotFound(t *testing.T) {
+ cfg, _ := newTestConfig(t, "only task +home")
+
+ if _, err := CompleteTask(cfg, []string{"99"}); err == nil {
+ t.Error("CompleteTask on a missing ID returned no error")
+ }
+}