Skip to content

feat(015): 回访花园闭环 — 留存触达隐喻与 Maestro UAT - #10

Merged
moyunzero merged 1 commit into
masterfrom
260703-feat-retention-garden-loop
Jul 5, 2026
Merged

feat(015): 回访花园闭环 — 留存触达隐喻与 Maestro UAT#10
moyunzero merged 1 commit into
masterfrom
260703-feat-retention-garden-loop

Conversation

@moyunzero

@moyunzero moyunzero commented Jul 4, 2026

Copy link
Copy Markdown
Owner

Summary

  • RET-01:Dashboard 回访横幅保留 daysSince 主标题,新增花园成长阶段副句(resolveRevisitSubtitleKey)及 Maestro testID
  • RET-02/03:周回顾 Banner 静态桥接 014 竖版分享卡;retention.json zh/en 同步温暖隐喻通知与 Profile 副文案
  • RET-04:新增 retentionCopy.test.ts,扩展 bilingualSmokeCopy(含 onboarding 回归键)
  • RET-05:Maestro 015-retention-garden-loop(8 个 UAT 场景 + 18 张本地验收截图)、__DEV__ dev-seed 深链、getRetentionNow 周末 mock

Test plan

  • yarn typecheck
  • yarn test(360 passed)
  • yarn test:maestro:preflight && yarn test:maestro:015(18/18 截图 + flow 全绿)
  • Bugbot + Security Review(0 medium+)
  • CI(typecheck / lint / test)

Maestro 本地复跑:

yarn start
yarn test:maestro:preflight && yarn test:maestro:015

验收截图在 .maestro/acceptance/015-retention-garden-loop/(gitignore,本地留存)。

Made with Cursor

Summary by CodeRabbit

  • New Features

    • Added updated retention experiences, including a new revisit banner with stage-based subtitle copy and refreshed weekly review/share card messaging.
    • Expanded retention reminder and notification text in both English and Chinese.
    • Added support for improved end-to-end validation across the app’s retention flows.
  • Bug Fixes

    • Improved banner timing and dismissal behavior for more consistent display.
    • Refined bilingual copy handling to keep retention messaging aligned across locales.

…aestro UAT

Phase 5 links revisit/weekly banners to garden metaphor copy, syncs warm
notification and Profile subtext in zh/en, and adds Maestro 015 with dev seed
for full RET-01–RET-05 acceptance.

Co-authored-by: Cursor <cursoragent@cursor.com>
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds a "retention garden loop" feature: a dev-mockable retention clock, subtitle-stage resolution for the revisit banner, updated retention copy in both locales, banner UI/testID wiring, a dev-only Maestro seed service and deep-link route, new Maestro E2E flows/subflows, and openspec documentation with tests.

Changes

Retention Garden Loop feature

Layer / File(s) Summary
Retention time source
shared/retention/getRetentionNow.ts
Adds a dev-mockable getRetentionNow() with AsyncStorage-backed mock timestamp hydration/setting.
Revisit subtitle stage resolution
shared/retention/resolveRevisitSubtitleKey.ts, __tests__/unit/shared/retention/resolveRevisitSubtitleKey.test.ts
Computes a growth-stage id (seed/sprout/seedling/bud/bloom) from resolved-entry ratio, with unit tests.
Retention copy updates
locales/en-US/retention.json, locales/zh-Hans/retention.json
Updates reminder subtext, notification bodies, weekly banner body, and adds nested revisitBanner.subtitle stage strings.
Banner UI integration and copy tests
components/retention/RevisitBanner.tsx, components/retention/WeeklyReviewBanner.tsx, components/retention/retention.styles.ts, __tests__/unit/i18n/bilingualSmokeCopy.test.ts, __tests__/unit/i18n/retentionCopy.test.ts
Banners use getRetentionNow() for visibility/dismissal, render new subtitle text and testIDs, and i18n tests assert the new retention keys.
Profile/settings testID wiring
components/settings/GroupedSettingsCard.tsx, features/profile/ProfileScreen.tsx, features/profile/components/ProfileSettingsSection.tsx
Adds testID props to settings card, profile root, locale buttons, and reminder rows/subtext.
Maestro retention seed service
services/maestroRetentionSeed.ts
Adds scenario/locale/reminder validation, entry construction, and runMaestroRetentionSeed/parseMaestroRetentionSeedParams for dev seeding.
Dev seed deep-link route
app/dev-seed-retention.tsx
New __DEV__-only screen that parses deep-link params, runs the seed, reloads store entries, and redirects.
Maestro E2E flow, subflows, config and scripts
.maestro/flows/015-retention-garden-loop.yaml, .maestro/subflows/apply-retention-seed.yaml, .maestro/subflows/seed-retention-touchpoints.yaml, .maestro/config.yaml, package.json
New Maestro flow covering revisit, weekly review, profile, and core-loop UAT scenarios in zh-Hans/en-US, plus seeding subflows, screenshot config, and a test:maestro:015 script.
OpenSpec change documentation
openspec/changes/015-retention-garden-loop/SPEC.md, openspec/changes/015-retention-garden-loop/VERIFICATION.md
Documents requirements, design decisions, key files, scope boundaries, and verification/UAT steps.

Estimated code review effort: 3 (Moderate) | ~30 minutes

Possibly related PRs

  • moyunzero/Emotion-Diary#7: Both PRs modify RevisitBanner.tsx and WeeklyReviewBanner.tsx, with this PR building time-source and subtitle logic on top of the i18n-driven rendering introduced there.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the PR’s main focus on the retention “garden loop” copy changes and Maestro UAT coverage.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 260703-feat-retention-garden-loop

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@sonarqubecloud

sonarqubecloud Bot commented Jul 4, 2026

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (3)
shared/retention/resolveRevisitSubtitleKey.ts (2)

17-17: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Unnecessary array copy before filtering.

excludeSoftDeletedEntries already accepts readonly MoodEntry[], so spreading entries into a new array here is redundant.

🧹 Optional cleanup
-  const visible = excludeSoftDeletedEntries([...entries]);
+  const visible = excludeSoftDeletedEntries(entries);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@shared/retention/resolveRevisitSubtitleKey.ts` at line 17, The call in
resolveRevisitSubtitleKey unnecessarily copies entries before filtering even
though excludeSoftDeletedEntries accepts readonly MoodEntry[]; remove the spread
and pass entries directly to the filtering step so the existing array is reused.

15-32: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Duplicated business logic risks silent drift across three files.

computeResolveRate and growthStageFromRate explicitly duplicate logic from services/gardenMilestone.ts and components/Insights/utils.tsx (per the comments themselves). If either source changes its rate formula or thresholds, this file will silently diverge and the revisit-banner subtitle stage will no longer match the garden growth stage shown elsewhere in the app.

Consider extracting a single shared computeResolveRate/growthStageFromRate (or threshold table) into a shared module that all three call sites import, rather than maintaining three independent copies.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@shared/retention/resolveRevisitSubtitleKey.ts` around lines 15 - 32, The
resolve-rate and stage-threshold logic is duplicated in computeResolveRate and
growthStageFromRate, which can drift from the implementations in
services/gardenMilestone and components/Insights/utils. Extract the shared
resolve-rate calculation and growth-stage mapping into a single reusable module
or shared helper, then update resolveRevisitSubtitleKey and the other call sites
to import and use those symbols instead of keeping separate copies.
services/maestroRetentionSeed.ts (1)

23-26: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Centralize these retention/storage keys. GUEST_STORAGE_KEY, DISMISS_REVISIT_KEY, and DISMISS_WEEKLY_KEY are duplicated here and in store/modules/storage.ts, components/retention/RevisitBanner.tsx, and components/retention/WeeklyReviewBanner.tsx. Import the canonical constants instead of repeating raw strings to avoid drift.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@services/maestroRetentionSeed.ts` around lines 23 - 26, The retention/storage
key strings are duplicated across the seed module and the storage/banner
components, so update maestroRetentionSeed to use the canonical constants
instead of local raw string copies. Import and reuse the existing symbols from
store/modules/storage.ts in maestroRetentionSeed, and ensure RevisitBanner and
WeeklyReviewBanner continue referencing the same shared constants so all
retention keys stay aligned.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.maestro/flows/015-retention-garden-loop.yaml:
- Around line 265-272: The reseed flow in the core-loop branches can start
before back navigation finishes, so add the same extendedWaitUntil used
elsewhere after review-export-back-button and before the
seed-retention-touchpoints runFlow in 015-retention-garden-loop. Update both
branches around the tapOn/review-export-back-button step to wait for
insights-screen before starting the reseed, matching the existing pattern used
for UAT 7/8.

In `@app/dev-seed-retention.tsx`:
- Around line 21-47: The useEffect in dev-seed-retention.tsx is intentionally
using granular params fields instead of the whole params object, but the
exhaustive-deps warning is still left unresolved. Keep the existing dependency
list on router, params.scenario, params.locale, and params.reminders, and add an
explicit lint suppression for the exhaustive-deps rule on this effect so the
pr-gate lint check passes cleanly without introducing a loop risk.

In `@shared/retention/getRetentionNow.ts`:
- Around line 10-29: Wire hydrateDevRetentionMockNow() into the app startup path
so the persisted mock time is restored before retention logic runs. The current
runMaestroRetentionSeed() only saves MOCK_NOW_KEY to AsyncStorage, so add a
bootstrap call to hydrateDevRetentionMockNow() in the main app initialization
flow (where retention/mock time is first configured) and make sure any consumers
of getRetentionNow() can see the rehydrated value on a fresh JS load.

---

Nitpick comments:
In `@services/maestroRetentionSeed.ts`:
- Around line 23-26: The retention/storage key strings are duplicated across the
seed module and the storage/banner components, so update maestroRetentionSeed to
use the canonical constants instead of local raw string copies. Import and reuse
the existing symbols from store/modules/storage.ts in maestroRetentionSeed, and
ensure RevisitBanner and WeeklyReviewBanner continue referencing the same shared
constants so all retention keys stay aligned.

In `@shared/retention/resolveRevisitSubtitleKey.ts`:
- Line 17: The call in resolveRevisitSubtitleKey unnecessarily copies entries
before filtering even though excludeSoftDeletedEntries accepts readonly
MoodEntry[]; remove the spread and pass entries directly to the filtering step
so the existing array is reused.
- Around line 15-32: The resolve-rate and stage-threshold logic is duplicated in
computeResolveRate and growthStageFromRate, which can drift from the
implementations in services/gardenMilestone and components/Insights/utils.
Extract the shared resolve-rate calculation and growth-stage mapping into a
single reusable module or shared helper, then update resolveRevisitSubtitleKey
and the other call sites to import and use those symbols instead of keeping
separate copies.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b4a915bd-c699-491d-a385-360b614fd8eb

📥 Commits

Reviewing files that changed from the base of the PR and between e1aabd3 and c82fe99.

📒 Files selected for processing (22)
  • .maestro/config.yaml
  • .maestro/flows/015-retention-garden-loop.yaml
  • .maestro/subflows/apply-retention-seed.yaml
  • .maestro/subflows/seed-retention-touchpoints.yaml
  • __tests__/unit/i18n/bilingualSmokeCopy.test.ts
  • __tests__/unit/i18n/retentionCopy.test.ts
  • __tests__/unit/shared/retention/resolveRevisitSubtitleKey.test.ts
  • app/dev-seed-retention.tsx
  • components/retention/RevisitBanner.tsx
  • components/retention/WeeklyReviewBanner.tsx
  • components/retention/retention.styles.ts
  • components/settings/GroupedSettingsCard.tsx
  • features/profile/ProfileScreen.tsx
  • features/profile/components/ProfileSettingsSection.tsx
  • locales/en-US/retention.json
  • locales/zh-Hans/retention.json
  • openspec/changes/015-retention-garden-loop/SPEC.md
  • openspec/changes/015-retention-garden-loop/VERIFICATION.md
  • package.json
  • services/maestroRetentionSeed.ts
  • shared/retention/getRetentionNow.ts
  • shared/retention/resolveRevisitSubtitleKey.ts

Comment on lines +265 to +272
- tapOn:
id: review-export-back-button
- runFlow:
file: ../subflows/seed-retention-touchpoints.yaml
env:
SCENARIO: revisit
LOCALE: zh-Hans
REMINDERS: default

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE=".maestro/flows/015-retention-garden-loop.yaml"

echo "== File size =="
wc -l "$FILE"

echo
echo "== UAT 3/4 surrounding lines =="
sed -n '70,125p' "$FILE" | cat -n

echo
echo "== UAT 7 surrounding lines =="
sed -n '250,285p' "$FILE" | cat -n

echo
echo "== UAT 8 surrounding lines =="
sed -n '328,365p' "$FILE" | cat -n

Repository: moyunzero/Emotion-Diary

Length of output: 4518


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== seed-retention-touchpoints subflow =="
FILE=".maestro/subflows/seed-retention-touchpoints.yaml"
wc -l "$FILE"
sed -n '1,260p' "$FILE" | cat -n

echo
echo "== Search for similar back-button -> runFlow patterns =="
rg -n -A3 -B3 "review-export-back-button|seed-retention-touchpoints|extendedWaitUntil" .maestro/flows .maestro/subflows

Repository: moyunzero/Emotion-Diary

Length of output: 44331


Wait for insights-screen before reseeding in UAT 7/8. Add the same extendedWaitUntil after review-export-back-button and before the runFlow reseed in both core-loop branches; otherwise the back navigation can still be in flight when the new flow starts.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.maestro/flows/015-retention-garden-loop.yaml around lines 265 - 272, The
reseed flow in the core-loop branches can start before back navigation finishes,
so add the same extendedWaitUntil used elsewhere after review-export-back-button
and before the seed-retention-touchpoints runFlow in 015-retention-garden-loop.
Update both branches around the tapOn/review-export-back-button step to wait for
insights-screen before starting the reseed, matching the existing pattern used
for UAT 7/8.

Comment on lines +21 to +47
useEffect(() => {
if (!__DEV__) {
router.replace("/");
return;
}

void (async () => {
const seedOptions = parseMaestroRetentionSeedParams(params);
if (!seedOptions) {
router.replace("/");
return;
}

await runMaestroRetentionSeed(seedOptions);
await useAppStore.getState()._loadEntries();

if (seedOptions.locale) {
await useAppStore.getState().setLocale(seedOptions.locale);
}

if (seedOptions.scenario === "weekly") {
router.replace("/insights");
} else {
router.replace("/");
}
})();
}, [router, params.scenario, params.locale, params.reminders]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Silence the exhaustive-deps warning explicitly rather than leaving it unresolved.

The pr-gate check flags params as a missing dependency at Line 47. Adding the whole params object would likely reintroduce an effect-loop risk if useLocalSearchParams returns a new object reference each render; the current granular deps (params.scenario, params.locale, params.reminders) are intentionally more correct. Suppress the rule explicitly so CI lint passes cleanly instead of leaving an open warning.

🔇 Proposed fix
-  }, [router, params.scenario, params.locale, params.reminders]);
+    // eslint-disable-next-line react-hooks/exhaustive-deps -- params object identity is unstable; track primitive fields instead
+  }, [router, params.scenario, params.locale, params.reminders]);
As per coding guidelines, `**/*.{ts,tsx,js,jsx}` should "establish verifiable success criteria (run `typecheck` / `lint` etc. after changes)".
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useEffect(() => {
if (!__DEV__) {
router.replace("/");
return;
}
void (async () => {
const seedOptions = parseMaestroRetentionSeedParams(params);
if (!seedOptions) {
router.replace("/");
return;
}
await runMaestroRetentionSeed(seedOptions);
await useAppStore.getState()._loadEntries();
if (seedOptions.locale) {
await useAppStore.getState().setLocale(seedOptions.locale);
}
if (seedOptions.scenario === "weekly") {
router.replace("/insights");
} else {
router.replace("/");
}
})();
}, [router, params.scenario, params.locale, params.reminders]);
useEffect(() => {
if (!__DEV__) {
router.replace("/");
return;
}
void (async () => {
const seedOptions = parseMaestroRetentionSeedParams(params);
if (!seedOptions) {
router.replace("/");
return;
}
await runMaestroRetentionSeed(seedOptions);
await useAppStore.getState()._loadEntries();
if (seedOptions.locale) {
await useAppStore.getState().setLocale(seedOptions.locale);
}
if (seedOptions.scenario === "weekly") {
router.replace("/insights");
} else {
router.replace("/");
}
})();
// eslint-disable-next-line react-hooks/exhaustive-deps -- params object identity is unstable; track primitive fields instead
}, [router, params.scenario, params.locale, params.reminders]);
🧰 Tools
🪛 GitHub Check: pr-gate

[warning] 47-47:
React Hook useEffect has a missing dependency: 'params'. Either include it or remove the dependency array

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/dev-seed-retention.tsx` around lines 21 - 47, The useEffect in
dev-seed-retention.tsx is intentionally using granular params fields instead of
the whole params object, but the exhaustive-deps warning is still left
unresolved. Keep the existing dependency list on router, params.scenario,
params.locale, and params.reminders, and add an explicit lint suppression for
the exhaustive-deps rule on this effect so the pr-gate lint check passes cleanly
without introducing a loop risk.

Sources: Coding guidelines, Linters/SAST tools

Comment on lines +10 to +29
export async function hydrateDevRetentionMockNow(): Promise<void> {
if (!__DEV__) {
devMockNowMs = null;
return;
}
try {
const { default: AsyncStorage } = await import(
"@react-native-async-storage/async-storage"
);
const raw = await AsyncStorage.getItem(MOCK_NOW_KEY);
if (!raw) {
devMockNowMs = null;
return;
}
const parsed = Date.parse(raw);
devMockNowMs = Number.isNaN(parsed) ? null : parsed;
} catch {
devMockNowMs = null;
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n "hydrateDevRetentionMockNow" --type=ts --type=tsx -C3 2>/dev/null || rg -n "hydrateDevRetentionMockNow" -C3

Repository: moyunzero/Emotion-Diary

Length of output: 161


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== Files mentioning retention mock helpers ==\n'
rg -n "setDevRetentionMockNow|MOCK_NOW_KEY|runMaestroRetentionSeed|hydrateDevRetentionMockNow|devMockNowMs" . --type=ts --type=tsx -C 2 || true

printf '\n== Likely bootstrap entry points ==\n'
rg -n "App\.|registerRootComponent|bootstrap|persist|rehydrate|AsyncStorage" . --type=ts --type=tsx -C 2 || true

Repository: moyunzero/Emotion-Diary

Length of output: 301


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== Retention helper references ==\n'
rg -n "hydrateDevRetentionMockNow|setDevRetentionMockNow|MOCK_NOW_KEY|runMaestroRetentionSeed|devMockNowMs" . -t ts -t tsx -C 2 || true

printf '\n== Startup/bootstrap references ==\n'
rg -n "registerRootComponent|AppRegistry|bootstrap|root component|AsyncStorage|rehydrate" . -t ts -t tsx -C 2 || true

Repository: moyunzero/Emotion-Diary

Length of output: 288


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== Tracked TS/TSX files containing retention helper symbols ==\n'
git ls-files | rg '\.(ts|tsx)$' | xargs rg -n "hydrateDevRetentionMockNow|setDevRetentionMockNow|MOCK_NOW_KEY|runMaestroRetentionSeed|devMockNowMs" -C 2 || true

printf '\n== Tracked TS/TSX files containing startup/bootstrap wiring ==\n'
git ls-files | rg '\.(ts|tsx)$' | xargs rg -n "registerRootComponent|AppRegistry|bootstrap|rehydrate|AsyncStorage" -C 2 || true

Repository: moyunzero/Emotion-Diary

Length of output: 48076


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== Imports/uses of shared/retention/getRetentionNow ==\n'
git ls-files | rg '\.(ts|tsx)$' | xargs rg -n 'from "`@/shared/retention/getRetentionNow`"|from "\./shared/retention/getRetentionNow"|from "\.\./shared/retention/getRetentionNow"|from "../shared/retention/getRetentionNow"' -C 2 || true

printf '\n== App entry/bootstrap files ==\n'
git ls-files | rg '(^app/.*\.(ts|tsx)$|(^|/)index\.(ts|tsx)$|(^|/)main\.(ts|tsx)$|(^|/)App\.(ts|tsx)$|(^|/)entry\.(ts|tsx)$)' | sed -n '1,120p'

Repository: moyunzero/Emotion-Diary

Length of output: 1980


Wire hydrateDevRetentionMockNow() into app bootstrap. runMaestroRetentionSeed persists the mock ISO to AsyncStorage, but nothing rehydrates it on a fresh JS load, so any reload/cold start falls back to real device time and breaks Maestro retention scenarios.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@shared/retention/getRetentionNow.ts` around lines 10 - 29, Wire
hydrateDevRetentionMockNow() into the app startup path so the persisted mock
time is restored before retention logic runs. The current
runMaestroRetentionSeed() only saves MOCK_NOW_KEY to AsyncStorage, so add a
bootstrap call to hydrateDevRetentionMockNow() in the main app initialization
flow (where retention/mock time is first configured) and make sure any consumers
of getRetentionNow() can see the rehydrated value on a fresh JS load.

@moyunzero
moyunzero merged commit 7b508c5 into master Jul 5, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant