From c9d5c048b71b6a9d3f386a706b26d1d7caceb540 Mon Sep 17 00:00:00 2001 From: Trey t Date: Sat, 25 Apr 2026 10:33:48 -0500 Subject: [PATCH] =?UTF-8?q?test:=20failing=20=E2=80=94=20DataManager.updat?= =?UTF-8?q?eTask=20must=20seed=20=5FallTasks=20when=20cache=20is=20empty?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Captures gitea#2 at the cache layer. Three tests: - updateTask_seedsAllTasks_whenCacheIsEmpty (the core bug) - updateTask_distributesAcrossColumns_whenSeedingThenAdding - updateTask_replacesExistingTaskById_acrossColumns All three FAIL on this commit because updateTask is a conditional ?.let{} that no-ops when _allTasks is null. Phase 1 fix in the next commit makes them green. --- .../honeyDue/data/DataManagerTaskCacheTest.kt | 105 ++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 composeApp/src/commonTest/kotlin/com/tt/honeyDue/data/DataManagerTaskCacheTest.kt diff --git a/composeApp/src/commonTest/kotlin/com/tt/honeyDue/data/DataManagerTaskCacheTest.kt b/composeApp/src/commonTest/kotlin/com/tt/honeyDue/data/DataManagerTaskCacheTest.kt new file mode 100644 index 0000000..98722eb --- /dev/null +++ b/composeApp/src/commonTest/kotlin/com/tt/honeyDue/data/DataManagerTaskCacheTest.kt @@ -0,0 +1,105 @@ +package com.tt.honeyDue.data + +import com.tt.honeyDue.models.TaskResponse +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertTrue +import kotlin.test.BeforeTest + +/** + * Regression tests for the gitea#2 task-cache bug: + * `DataManager.updateTask` was a no-op when both `_allTasks` was null AND + * `_tasksByResidence[residenceId]` was empty — exactly the cache state + * after a fresh register-then-bulkCreateTasks flow. The just-created + * tasks would only appear after an app restart. + * + * After the fix, `updateTask` must seed `_allTasks` from empty rather + * than skipping the update. + */ +class DataManagerTaskCacheTest { + + @BeforeTest + fun resetState() { + DataManager.clear() + } + + /// Onboarding-flow scenario: brand-new user, fresh launch, no kanban + /// has ever been fetched, then a task arrives via bulkCreateTasks → + /// DataManager.updateTask. The new task MUST land in `_allTasks` and + /// be visible to any observer. + @Test + fun updateTask_seedsAllTasks_whenCacheIsEmpty() { + // Given: fresh DataManager, kanban never loaded + assertEquals(null, DataManager.allTasks.value, "_allTasks must start null after clear()") + + // When: a new task arrives via the same path bulkCreateTasks uses + DataManager.updateTask(sampleTask(id = 1, residenceId = 100, column = "upcoming_tasks")) + + // Then: _allTasks must contain that task in the right column + val allTasks = DataManager.allTasks.value + assertNotNull(allTasks, "updateTask must seed _allTasks even when it was null") + + val upcoming = allTasks.columns.firstOrNull { it.name == "upcoming_tasks" } + assertNotNull(upcoming, "the seeded kanban must include an upcoming_tasks column") + assertTrue( + upcoming.tasks.any { it.id == 1 }, + "the new task must land in upcoming_tasks; got columns=${allTasks.columns.map { it.name to it.tasks.map { t -> t.id } }}" + ) + assertEquals(upcoming.tasks.size, upcoming.count, "column count must match tasks.size") + } + + /// Reasonable-defaults sanity check for the bulk-create scenario: + /// multiple tasks land across different kanban columns and end up + /// distributed correctly. This exercises the upsert when _allTasks + /// was seeded by a previous call. + @Test + fun updateTask_distributesAcrossColumns_whenSeedingThenAdding() { + DataManager.updateTask(sampleTask(id = 1, residenceId = 100, column = "overdue_tasks")) + DataManager.updateTask(sampleTask(id = 2, residenceId = 100, column = "upcoming_tasks")) + DataManager.updateTask(sampleTask(id = 3, residenceId = 100, column = "upcoming_tasks")) + + val allTasks = DataManager.allTasks.value + assertNotNull(allTasks) + + val overdue = allTasks.columns.first { it.name == "overdue_tasks" } + val upcoming = allTasks.columns.first { it.name == "upcoming_tasks" } + + assertEquals(setOf(1), overdue.tasks.map { it.id }.toSet()) + assertEquals(setOf(2, 3), upcoming.tasks.map { it.id }.toSet()) + } + + /// Replacement contract: calling updateTask with the same id twice + /// must not duplicate; the second call replaces the first wherever it + /// lives. Catches the "always-append" implementation mistake. + @Test + fun updateTask_replacesExistingTaskById_acrossColumns() { + DataManager.updateTask(sampleTask(id = 5, residenceId = 100, column = "upcoming_tasks", title = "v1")) + DataManager.updateTask(sampleTask(id = 5, residenceId = 100, column = "in_progress_tasks", title = "v2")) + + val allTasks = DataManager.allTasks.value + assertNotNull(allTasks) + + val upcoming = allTasks.columns.first { it.name == "upcoming_tasks" } + val inProgress = allTasks.columns.first { it.name == "in_progress_tasks" } + + assertTrue(upcoming.tasks.none { it.id == 5 }, "task 5 must move out of upcoming_tasks") + assertEquals(1, inProgress.tasks.count { it.id == 5 }, "task 5 must appear once in in_progress_tasks") + assertEquals("v2", inProgress.tasks.first { it.id == 5 }.title) + } + + private fun sampleTask( + id: Int, + residenceId: Int, + column: String, + title: String = "Task $id" + ) = TaskResponse( + id = id, + residenceId = residenceId, + createdById = 1, + title = title, + kanbanColumn = column, + createdAt = "2026-04-25T00:00:00Z", + updatedAt = "2026-04-25T00:00:00Z" + ) +}