From 839754f4ee6e3e19a6027c1916c1e345ef10ce54 Mon Sep 17 00:00:00 2001 From: Jake Date: Fri, 24 Apr 2026 20:18:33 +0100 Subject: [PATCH] feat(gamification): clear auto_completed on uncomplete uncomplete_task now clears auto_completed alongside done / done_at on both the target row and the cascaded-parent reopen. Keeps the flag accurate so a later re-completion via a different path is counted correctly by the Phase 8 daily-count query. Co-Authored-By: Claude Opus 4.7 (1M context) --- crates/storage/src/database.rs | 59 ++++++++++++++++++++++++++++++---- 1 file changed, 53 insertions(+), 6 deletions(-) diff --git a/crates/storage/src/database.rs b/crates/storage/src/database.rs index ce1f9e6..fe4e422 100644 --- a/crates/storage/src/database.rs +++ b/crates/storage/src/database.rs @@ -487,7 +487,7 @@ pub async fn uncomplete_task(pool: &SqlitePool, id: &str) -> Result<()> { .await .map_err(|e| KonError::StorageError(format!("Begin transaction failed: {e}")))?; - sqlx::query("UPDATE tasks SET done = 0, done_at = NULL WHERE id = ?") + sqlx::query("UPDATE tasks SET done = 0, done_at = NULL, auto_completed = 0 WHERE id = ?") .bind(id) .execute(&mut *tx) .await @@ -507,11 +507,14 @@ pub async fn uncomplete_task(pool: &SqlitePool, id: &str) -> Result<()> { .flatten(); if let Some(pid) = parent_id { - sqlx::query("UPDATE tasks SET done = 0, done_at = NULL WHERE id = ? AND done = 1") - .bind(&pid) - .execute(&mut *tx) - .await - .map_err(|e| KonError::StorageError(format!("Reopen parent failed: {e}")))?; + sqlx::query( + "UPDATE tasks SET done = 0, done_at = NULL, auto_completed = 0 \ + WHERE id = ? AND done = 1", + ) + .bind(&pid) + .execute(&mut *tx) + .await + .map_err(|e| KonError::StorageError(format!("Reopen parent failed: {e}")))?; } tx.commit() @@ -2077,4 +2080,48 @@ mod tests { assert_eq!(s1_auto, 0, "subtask completion is manual"); assert_eq!(s2_auto, 0, "subtask completion is manual"); } + + #[tokio::test] + async fn uncomplete_clears_auto_completed_on_parent() { + let pool = test_pool().await; + + insert_task( + &pool, + "parent", + "Parent task", + "inbox", + None, + None, + None, + None, + ) + .await + .unwrap(); + insert_subtask(&pool, "s1", "Only step", "parent") + .await + .unwrap(); + + // Cascade closes the parent with auto_completed = 1. + complete_subtask_and_check_parent(&pool, "s1") + .await + .unwrap(); + + // Uncompleting the subtask reopens the parent (existing invariant) + // and must also clear auto_completed on the parent so a later + // manual re-completion is counted cleanly. + uncomplete_task(&pool, "s1").await.unwrap(); + + let parent_done: i64 = sqlx::query_scalar("SELECT done FROM tasks WHERE id = 'parent'") + .fetch_one(&pool) + .await + .unwrap(); + let parent_auto: i64 = + sqlx::query_scalar("SELECT auto_completed FROM tasks WHERE id = 'parent'") + .fetch_one(&pool) + .await + .unwrap(); + + assert_eq!(parent_done, 0, "parent reopens when child is uncompleted"); + assert_eq!(parent_auto, 0, "auto_completed must clear on reopen"); + } }