From 915a5d47429d3e465a9e8f302026a27b67948d8c Mon Sep 17 00:00:00 2001 From: Trey t Date: Sat, 25 Apr 2026 10:40:20 -0500 Subject: [PATCH] test: characterize getTasksForResidence filter contract Locks down the contract that becomes the primary path for residence detail in Phase 3: - filters _allTasks by residenceId - returns empty shell for residence with no tasks (vs null for cache miss) - returns null when _allTasks itself is null (caller must hit API) --- .../honeyDue/data/DataManagerTaskCacheTest.kt | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/composeApp/src/commonTest/kotlin/com/tt/honeyDue/data/DataManagerTaskCacheTest.kt b/composeApp/src/commonTest/kotlin/com/tt/honeyDue/data/DataManagerTaskCacheTest.kt index b050dd7..48d770f 100644 --- a/composeApp/src/commonTest/kotlin/com/tt/honeyDue/data/DataManagerTaskCacheTest.kt +++ b/composeApp/src/commonTest/kotlin/com/tt/honeyDue/data/DataManagerTaskCacheTest.kt @@ -88,6 +88,53 @@ class DataManagerTaskCacheTest { assertEquals("v2", inProgress.tasks.first { it.id == 5 }.title) } + /// Characterization: getTasksForResidence filters _allTasks by + /// residence id. This is the helper that becomes the primary path + /// for residence-detail in Phase 3 (collapse the dual cache). + @Test + fun getTasksForResidence_filtersAllTasksByResidenceId() { + // Seed _allTasks with tasks across two residences via the upsert path. + DataManager.updateTask(sampleTask(id = 1, residenceId = 100, column = "upcoming_tasks")) + DataManager.updateTask(sampleTask(id = 2, residenceId = 100, column = "overdue_tasks")) + DataManager.updateTask(sampleTask(id = 3, residenceId = 200, column = "upcoming_tasks")) + + val r100 = DataManager.getTasksForResidence(100) + assertNotNull(r100) + val r100Ids = r100.columns.flatMap { it.tasks }.map { it.id }.toSet() + assertEquals(setOf(1, 2), r100Ids) + + val r200 = DataManager.getTasksForResidence(200) + assertNotNull(r200) + val r200Ids = r200.columns.flatMap { it.tasks }.map { it.id }.toSet() + assertEquals(setOf(3), r200Ids) + + // Counts on each column must match the filtered task lists. + for (column in r100.columns) { + assertEquals(column.tasks.size, column.count, "column ${column.name} count mismatch") + } + } + + /// Characterization: residenceId with no tasks returns a non-null + /// shell so the residence-detail screen can distinguish "loading" + /// (null) from "loaded, no tasks" (non-null with empty columns). + @Test + fun getTasksForResidence_returnsEmptyShellForResidenceWithNoTasks() { + DataManager.updateTask(sampleTask(id = 1, residenceId = 100, column = "upcoming_tasks")) + + val r999 = DataManager.getTasksForResidence(999) + assertNotNull(r999, "residence with no tasks must return an empty shell, not null") + assertEquals(0, r999.columns.sumOf { it.tasks.size }) + } + + /// Characterization: when _allTasks is null entirely (cache never + /// populated), getTasksForResidence returns null — caller must call + /// the API path. Phase 3's getTasksByResidence relies on this. + @Test + fun getTasksForResidence_returnsNullWhenAllTasksIsNull() { + DataManager.clear() + assertEquals(null, DataManager.getTasksForResidence(100)) + } + /// Lockdown: updateTask must NOT touch `_tasksByResidence`. That cache /// is being deleted in Phase 3; until then, updateTask must leave it /// alone. If a future commit re-introduces the conditional write