From 941f22cdbd9ec434cf76da0e8f94285330040ac0 Mon Sep 17 00:00:00 2001 From: null Date: Tue, 30 Jun 2026 01:26:09 -0500 Subject: [PATCH] feat(home): HomePriorityEngine priority logic, HomeViewModel wiring, unit test coverage --- .../app/closer/ui/home/HomePriorityEngine.kt | 11 +++ .../java/app/closer/ui/home/HomeViewModel.kt | 19 +++++ .../closer/ui/home/HomePriorityEngineTest.kt | 75 +++++++++++++++++++ 3 files changed, 105 insertions(+) diff --git a/app/src/main/java/app/closer/ui/home/HomePriorityEngine.kt b/app/src/main/java/app/closer/ui/home/HomePriorityEngine.kt index 3dc315b5..78ca9b11 100644 --- a/app/src/main/java/app/closer/ui/home/HomePriorityEngine.kt +++ b/app/src/main/java/app/closer/ui/home/HomePriorityEngine.kt @@ -44,6 +44,8 @@ object HomePriorityEngine { val gameWaiting: Boolean = false, val challengeWaiting: Boolean = false, val dailyQuestionUnanswered: Boolean = false, + val dailyQuestionAwaitingPartner: Boolean = false, + val dailyQuestionRevealed: Boolean = false, val weeklyRecapReady: Boolean = false, val capsuleUnlocked: Boolean = false, val dateReminder: Boolean = false, @@ -60,9 +62,14 @@ object HomePriorityEngine { GAME_WAITING, DAILY_QUESTION_UNANSWERED, CHALLENGE_WAITING, + // You answered; waiting on your partner. Ritual band: below actionable games/challenges, + // above reflective/generic content. Becomes the hero only when nothing more urgent is active. + DAILY_QUESTION_AWAITING_PARTNER, WEEKLY_RECAP_READY, CAPSULE_UNLOCKED, DATE_REMINDER, + // You already revealed today's question — a low-priority "keep the conversation going" closure card. + DAILY_QUESTION_REVEALED, SUGGESTED_PACK, EXPLORE_GAMES } @@ -129,6 +136,8 @@ object HomePriorityEngine { Priority.GAME_WAITING -> input.gameWaiting Priority.CHALLENGE_WAITING -> input.challengeWaiting Priority.DAILY_QUESTION_UNANSWERED -> input.dailyQuestionUnanswered + Priority.DAILY_QUESTION_AWAITING_PARTNER -> input.dailyQuestionAwaitingPartner + Priority.DAILY_QUESTION_REVEALED -> input.dailyQuestionRevealed Priority.WEEKLY_RECAP_READY -> input.weeklyRecapReady Priority.CAPSULE_UNLOCKED -> input.capsuleUnlocked Priority.DATE_REMINDER -> input.dateReminder @@ -154,6 +163,8 @@ object HomePriorityEngine { */ private fun Priority.isValueAction(): Boolean = when (this) { Priority.DAILY_QUESTION_UNANSWERED, + Priority.DAILY_QUESTION_AWAITING_PARTNER, + Priority.DAILY_QUESTION_REVEALED, Priority.WEEKLY_RECAP_READY, Priority.DATE_REMINDER -> true else -> false diff --git a/app/src/main/java/app/closer/ui/home/HomeViewModel.kt b/app/src/main/java/app/closer/ui/home/HomeViewModel.kt index 3516e2fe..4899b1af 100644 --- a/app/src/main/java/app/closer/ui/home/HomeViewModel.kt +++ b/app/src/main/java/app/closer/ui/home/HomeViewModel.kt @@ -567,6 +567,8 @@ class HomeViewModel @Inject constructor( gameWaiting = hasWaitingGame(), challengeWaiting = hasIncompleteChallenge(), dailyQuestionUnanswered = dailyQuestionState == DailyQuestionState.UNANSWERED && dailyQuestion != null, + dailyQuestionAwaitingPartner = dailyQuestionState == DailyQuestionState.USER_ANSWERED_PARTNER_PENDING, + dailyQuestionRevealed = dailyQuestionState == DailyQuestionState.REVEALED && dailyQuestion != null, weeklyRecapReady = weeklyRecapReady, capsuleUnlocked = hasUnlockedCapsule(), dateReminder = hasUpcomingDate(), @@ -666,6 +668,23 @@ class HomeViewModel @Inject constructor( cta = "Answer privately" ) + // You answered; the reveal waits on your partner. The hero card (PrimaryHomeActionCard) overrides + // this title/body and routes the CTA to the gentle-reminder send; this copy/CTA label is what shows + // when it renders as a smaller secondary card (a game/challenge is the hero). + Priority.DAILY_QUESTION_AWAITING_PARTNER -> buildDailyQuestionAction( + title = "You showed up tonight.", + body = partnerName?.let { "Your answer stays private until $it answers too — no pressure." } + ?: "Your answer stays private until your partner answers too — no pressure.", + cta = "Send a gentle nudge" + ) + + // You already revealed today — a low-priority closure card that links to the discussion thread. + Priority.DAILY_QUESTION_REVEALED -> buildDailyQuestionAction( + title = "You opened a conversation tonight.", + body = "Keep it going whenever you're both ready.", + cta = "Keep the conversation going" + ) + Priority.WEEKLY_RECAP_READY -> HomeAction( eyebrow = "Your week together", title = "Look back at what you built this week.", diff --git a/app/src/test/java/app/closer/ui/home/HomePriorityEngineTest.kt b/app/src/test/java/app/closer/ui/home/HomePriorityEngineTest.kt index 9c875e00..9e40b7f7 100644 --- a/app/src/test/java/app/closer/ui/home/HomePriorityEngineTest.kt +++ b/app/src/test/java/app/closer/ui/home/HomePriorityEngineTest.kt @@ -4,6 +4,7 @@ import app.closer.ui.home.HomePriorityEngine.Input import app.closer.ui.home.HomePriorityEngine.Priority import org.junit.Assert.assertEquals import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue import org.junit.Test class HomePriorityEngineTest { @@ -122,6 +123,80 @@ class HomePriorityEngineTest { assertEquals(Priority.DAILY_QUESTION_UNANSWERED, output.primary?.priority) } + @Test + fun `awaiting partner is primary when no blockers`() { + val input = Input( + isPaired = true, + dailyQuestionAwaitingPartner = true + ) + + val output = HomePriorityEngine.compute(input) + + assertEquals(Priority.DAILY_QUESTION_AWAITING_PARTNER, output.primary?.priority) + } + + @Test + fun `game and challenge waiting outrank awaiting partner, which drops to a secondary card`() { + val input = Input( + isPaired = true, + gameWaiting = true, + challengeWaiting = true, + dailyQuestionAwaitingPartner = true + ) + + val output = HomePriorityEngine.compute(input) + + // Ritual band: an actionable waiting game is the hero; "you answered, waiting" still shows below. + assertEquals(Priority.GAME_WAITING, output.primary?.priority) + assertTrue(output.secondary.any { it.priority == Priority.DAILY_QUESTION_AWAITING_PARTNER }) + } + + @Test + fun `awaiting partner outranks weekly recap`() { + val input = Input( + isPaired = true, + dailyQuestionAwaitingPartner = true, + weeklyRecapReady = true + ) + + val output = HomePriorityEngine.compute(input) + + assertEquals(Priority.DAILY_QUESTION_AWAITING_PARTNER, output.primary?.priority) + } + + @Test + fun `revealed closure ranks below date reminder but surfaces as a secondary value card`() { + val input = Input( + isPaired = true, + dateReminder = true, + dailyQuestionRevealed = true, + suggestedPackAvailable = true + ) + + val output = HomePriorityEngine.compute(input) + + assertEquals(Priority.DATE_REMINDER, output.primary?.priority) + // Revealed is a value action (kept); the generic suggested pack is filtered out of secondary. + assertEquals( + listOf(Priority.DAILY_QUESTION_REVEALED), + output.secondary.map { it.priority } + ) + } + + @Test + fun `revealed closure outranks generic browse content`() { + val input = Input( + isPaired = true, + dailyQuestionRevealed = true, + suggestedPackAvailable = true, + exploreGamesAvailable = true + ) + + val output = HomePriorityEngine.compute(input) + + assertEquals(Priority.DAILY_QUESTION_REVEALED, output.primary?.priority) + } + @Test fun `weekly recap outranks capsule and date reminder`() { val input = Input(