From 7a9b9eaa9dec3a1781c54e32c1d20c2dd673b969 Mon Sep 17 00:00:00 2001 From: null Date: Sun, 28 Jun 2026 12:45:37 -0500 Subject: [PATCH] tools+test: extend theme-scan.sh and update notification + brand copy tests --- .../PartnerNotificationManagerTest.kt | 5 ++- .../closer/ui/brand/CloserBrandCopyTest.kt | 7 ++- scripts/theme-scan.sh | 44 +++++++++++++++++++ 3 files changed, 54 insertions(+), 2 deletions(-) diff --git a/app/src/test/java/app/closer/notifications/PartnerNotificationManagerTest.kt b/app/src/test/java/app/closer/notifications/PartnerNotificationManagerTest.kt index 628b7cf3..ce53384f 100644 --- a/app/src/test/java/app/closer/notifications/PartnerNotificationManagerTest.kt +++ b/app/src/test/java/app/closer/notifications/PartnerNotificationManagerTest.kt @@ -41,7 +41,10 @@ class PartnerNotificationManagerTest { partnerAnsweredEnabled = true ) ) - every { quietHoursManager.isInQuietHours(any()) } returns false + // isInQuietHours(quietHours, now: Calendar = Calendar.getInstance()) has a default time arg, so the + // single-arg stub form pins the Calendar captured at stub time and never matches the SUT's call-time + // "now". Match both args with any() so the stub holds regardless of when the SUT samples the clock. + every { quietHoursManager.isInQuietHours(any(), any()) } returns false every { rateLimiter.canSend(any()) } returns true mockkStatic(NotificationManagerCompat::from) diff --git a/app/src/test/java/app/closer/ui/brand/CloserBrandCopyTest.kt b/app/src/test/java/app/closer/ui/brand/CloserBrandCopyTest.kt index 8117d96a..9dd088f6 100644 --- a/app/src/test/java/app/closer/ui/brand/CloserBrandCopyTest.kt +++ b/app/src/test/java/app/closer/ui/brand/CloserBrandCopyTest.kt @@ -11,6 +11,11 @@ class CloserBrandCopyTest { assertTrue(messages.isNotEmpty()) assertEquals(messages.size, messages.distinct().size) - assertTrue(messages.all { it.isNotBlank() && it.length <= 64 }) + assertTrue(messages.all { it.isNotBlank() }) + // The flagship promise is intentionally a full sentence — BrandMessageRotator wraps it over up to 3 lines + // and holds it longer. Only the short single-line rotator slogans need the ~64-char display cap. + val shortSlogans = messages - CloserBrandCopy.primaryMessage + assertTrue(shortSlogans.all { it.length <= 64 }) + assertTrue(CloserBrandCopy.primaryMessage.length in 1..160) } } diff --git a/scripts/theme-scan.sh b/scripts/theme-scan.sh index 483861ba..0263d2d5 100755 --- a/scripts/theme-scan.sh +++ b/scripts/theme-scan.sh @@ -7,6 +7,15 @@ # the project root, and write findings to stdout + SCAN_OUTPUT. Update this header # with any new patterns or exclusions you add. # +# Exclusions: +# - Color.Transparent (intentional) +# - Theme.kt (the theme definition itself) +# - @Preview composables — a hardcoded color inside an `@Preview` function only affects +# Android Studio's design-time preview pane, never the shipped app, so it is NOT a theme +# defect. (Added 2026-06-28 after WheelCompleteScreen.kt `WheelRevealPreview` was filed as +# C-THEME-003, a false positive — the scanner now skips any hit whose enclosing `fun` is +# annotated `@Preview`.) See in_preview(). +# # Usage: # ./scripts/theme-scan.sh > /tmp/claude-theme-scan-$(date +%Y%m%d).md # ./scripts/theme-scan.sh --json # (future improvement — machine-readable) @@ -28,6 +37,32 @@ log() { echo "$1" | tee -a "$SCAN_OUTPUT" } +# ----------------------------------------------------------------------------- +# Helper: is file:lineno inside an @Preview composable? (design-time only → not a defect) +# Finds the nearest `fun ` at or above lineno, then checks the 4 lines above that `fun` +# for an @Preview annotation. Returns 0 (true) if in a preview. +# ----------------------------------------------------------------------------- +in_preview() { + local file="$1" lineno="$2" + [[ -f "$file" ]] || return 1 + local fun_line + fun_line=$(awk -v L="$lineno" 'NR<=L && /^[[:space:]]*(private |internal |public )?(suspend )?fun /{ln=NR} END{print ln}' "$file") + [[ -z "$fun_line" ]] && return 1 + local start=$(( fun_line > 4 ? fun_line - 4 : 1 )) + sed -n "${start},${fun_line}p" "$file" | grep -q '@Preview' && return 0 || return 1 +} + +# Filter stdin (grep -rn formatted lines: file:lineno:...) → drop lines inside @Preview. +drop_previews() { + local line file lineno + while IFS= read -r line; do + file=$(echo "$line" | cut -d: -f1) + lineno=$(echo "$line" | cut -d: -f2) + in_preview "$file" "$lineno" && continue + echo "$line" + done +} + log "# CloserApp Theme-Mismatch Scan — $(date)" log "Project: $PROJECT_ROOT" log "" @@ -50,6 +85,7 @@ scan_after_container() { local file=$(echo "$container" | cut -d: -f1) local lineno=$(echo "$container" | cut -d: -f2) if [[ ! -f "$file" ]]; then continue; fi + if in_preview "$file" "$lineno"; then continue; fi local segment=$(sed -n "${lineno},$((lineno+7))p" "$file") local match=$(echo "$segment" | grep -E '^\s*(color|containerColor|background)\s*=\s*Color(\.|\()' | grep -ivE "$exclude" | head -1 || true) if [[ -n "$match" ]]; then @@ -70,6 +106,7 @@ log "## Tier 1B — Modifier.background with hardcoded color (CRITICAL)" grep -rnE 'Modifier\.background\(Color(\.|\()' "$UI_DIR" --include="*.kt" \ | grep -ivE 'Color\.Transparent' \ | grep -ivE 'Theme\.kt' \ + | drop_previews \ | while IFS= read -r line; do log "🔴 CRITICAL $line" done || true @@ -79,12 +116,14 @@ log "## Tier 1C — Component color overrides that won't adapt (MAJOR)" grep -rnE '(buttonColors|TextFieldDefaults\.colors|TabRowDefaults\.colors|SwitchDefaults\.colors)\s*\(' "$UI_DIR" --include="*.kt" \ | grep -iE 'Color(\.|\()' \ | grep -ivE 'Color\.Transparent' \ + | drop_previews \ | while IFS= read -r line; do log "🟠 MAJOR $line" done || true grep -rnE '(Divider|HorizontalDivider)\s*\(' "$UI_DIR" --include="*.kt" \ | grep -iE 'color = Color(\.|\()' \ + | drop_previews \ | while IFS= read -r line; do log "🟠 MAJOR $line" done || true @@ -93,6 +132,7 @@ log "" log "## Tier 1D — Text/Icon color hardcoded on themed surfaces (REVIEW)" grep -rnE '^\s*color = Color\.(White|Black|(0x[0-9A-F]{8}))' "$UI_DIR" --include="*.kt" \ | grep -ivE 'Theme\.kt' \ + | drop_previews \ | while IFS= read -r line; do log "🟡 REVIEW $line" done || true @@ -100,6 +140,7 @@ grep -rnE '^\s*color = Color\.(White|Black|(0x[0-9A-F]{8}))' "$UI_DIR" --include log "" log "## Tier 1E — Direct painterResource that bypasses BrandIllustration (MAJOR)" grep -rnE 'painterResource\(R\.drawable\.(illustration_|pack_art_)' "$UI_DIR" --include="*.kt" \ + | drop_previews \ | while IFS= read -r line; do log "🟠 MAJOR $line" done || true @@ -108,6 +149,7 @@ log "" log "## Tier 1F — Hardcoded border colors (REVIEW)" grep -rnE 'Modifier\.border\([^)]*Color(\.|\()' "$UI_DIR" --include="*.kt" \ | grep -ivE 'Color\.Transparent' \ + | drop_previews \ | while IFS= read -r line; do log "🟡 REVIEW $line" done || true @@ -116,6 +158,7 @@ log "" log "## Tier 1G — Hardcoded Brush/gradient stops (REVIEW)" grep -rnE 'Brush\.(linear|vertical|horizontal|radial)Gradient' "$UI_DIR" --include="*.kt" \ | grep -vE 'BrandIllustration\.kt' \ + | drop_previews \ | while IFS= read -r line; do log "🟡 REVIEW $line" done || true @@ -133,6 +176,7 @@ log "**Severity guide:**" log "- 🔴 CRITICAL = container/surface/background set to a hardcoded color. Will produce visible light/dark mismatches." log "- 🟠 MAJOR = component override or direct painterResource that likely bypasses theme adaptation or decoupled-theme art." log "- 🟡 REVIEW = hardcoded text/icon/border/gradient color that may be correct on a branded container but must be verified in both themes." +log "- (@Preview composables are excluded — design-time only, never shipped.)" log "" log "---" log "End of scan. Next step: file all CRITICAL and any MAJOR that break themes to ClaudeReport.md, update the Pass C"