summaryrefslogtreecommitdiff
path: root/internal/store/chains_test.go
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-08-12 23:35:47 +0000
committerPeter Stone <thepeterstone@gmail.com>2026-08-12 23:35:47 +0000
commit2509dde6aa372a505b186657706f4d21bd391807 (patch)
treee4147991747b8dd14d0b8654b2b8e50717b666bd /internal/store/chains_test.go
parent3e8ad60431d6cc783f9f7c555bfde5db54ebec75 (diff)
Add task title editing/deletion, timeline click-to-open, widget app launch
Task-detail modal was description-only with no delete affordance; HandleUpdateTask now saves the title too and a Delete button hits a new DELETE /tasks/{id} route backed by store.DeleteNativeTask, which repairs chain_position/unlocks the successor when the deleted task belongs to a chain. Timeline tab task/card/gtask rows now open the same detail modal as the Tasks tab. Android widget's "TODAY" header is now a tap target that launches DashboardActivity, since nothing previously opened the full app from the widget.
Diffstat (limited to 'internal/store/chains_test.go')
-rw-r--r--internal/store/chains_test.go114
1 files changed, 114 insertions, 0 deletions
diff --git a/internal/store/chains_test.go b/internal/store/chains_test.go
index 6359d11..bcfc09e 100644
--- a/internal/store/chains_test.go
+++ b/internal/store/chains_test.go
@@ -263,3 +263,117 @@ func TestGetChain_UnknownID_ReturnsErrNotFound(t *testing.T) {
t.Errorf("err = %v, want ErrNativeTaskNotFound", err)
}
}
+
+// TestDeleteNativeTask_LockedChainTask_ClosesPositionGap proves deleting a
+// not-yet-reached chain step doesn't strand the chain: positions after the
+// deleted one shift down by one and stay a contiguous 0..N-1 sequence, since
+// advanceChain's chain_position+1 lookup depends on that contiguity.
+func TestDeleteNativeTask_LockedChainTask_ClosesPositionGap(t *testing.T) {
+ s := newNativeTasksTestStore(t)
+
+ chain, err := s.CreateChain("Track", chainTasks("Step 1", "Step 2", "Step 3"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ tasks, err := s.GetChainTasks(chain.ID)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if err := s.DeleteNativeTask(tasks[1].ID); err != nil {
+ t.Fatalf("DeleteNativeTask: %v", err)
+ }
+
+ after, err := s.GetChainTasks(chain.ID)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(after) != 2 {
+ t.Fatalf("len(after) = %d, want 2", len(after))
+ }
+ if after[0].Content != "Step 1" || after[1].Content != "Step 3" {
+ t.Errorf("unexpected content order: %q, %q", after[0].Content, after[1].Content)
+ }
+ if after[1].ChainPosition != 1 {
+ t.Errorf("Step 3 chain_position = %d, want 1 (gap closed)", after[1].ChainPosition)
+ }
+ if !after[0].ChainUnlocked {
+ t.Errorf("position 0 should still be unlocked")
+ }
+ if after[1].ChainUnlocked {
+ t.Errorf("position 1 (formerly locked position 2) should still be locked")
+ }
+
+ // Completing position 0 should now correctly advance to the
+ // renumbered position 1 (Step 3), proving advanceChain's
+ // chain_position+1 lookup still works post-deletion.
+ if err := s.CompleteNativeTask(after[0].ID); err != nil {
+ t.Fatalf("CompleteNativeTask: %v", err)
+ }
+ final, err := s.GetChainTasks(chain.ID)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !final[1].ChainUnlocked {
+ t.Errorf("Step 3 should be unlocked after completing Step 1")
+ }
+}
+
+// TestDeleteNativeTask_UnlockedChainTask_PromotesSuccessor proves deleting
+// the currently-actionable (unlocked) step unlocks whatever now sits at its
+// position, rather than leaving the chain with nothing unlocked.
+func TestDeleteNativeTask_UnlockedChainTask_PromotesSuccessor(t *testing.T) {
+ s := newNativeTasksTestStore(t)
+
+ chain, err := s.CreateChain("Track", chainTasks("Step 1", "Step 2"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ tasks, err := s.GetChainTasks(chain.ID)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if err := s.DeleteNativeTask(tasks[0].ID); err != nil {
+ t.Fatalf("DeleteNativeTask: %v", err)
+ }
+
+ after, err := s.GetChainTasks(chain.ID)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(after) != 1 {
+ t.Fatalf("len(after) = %d, want 1", len(after))
+ }
+ if !after[0].ChainUnlocked || after[0].DueDate == nil {
+ t.Errorf("Step 2 should be promoted to unlocked with a due date, got ChainUnlocked=%v DueDate=%v", after[0].ChainUnlocked, after[0].DueDate)
+ }
+}
+
+// TestDeleteNativeTask_LastRemainingChainTask_MarksChainCompleted proves
+// deleting the sole unlocked task with nothing left to promote finishes the
+// chain instead of leaving it active with zero unlocked tasks forever.
+func TestDeleteNativeTask_LastRemainingChainTask_MarksChainCompleted(t *testing.T) {
+ s := newNativeTasksTestStore(t)
+
+ chain, err := s.CreateChain("Track", chainTasks("Only step"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ tasks, err := s.GetChainTasks(chain.ID)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if err := s.DeleteNativeTask(tasks[0].ID); err != nil {
+ t.Fatalf("DeleteNativeTask: %v", err)
+ }
+
+ updatedChain, err := s.GetChain(chain.ID)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if updatedChain.Status != "completed" {
+ t.Errorf("chain.Status = %q, want completed", updatedChain.Status)
+ }
+}