Files
daily-notification-plugin/test-apps/android-test-app/test-phase1.sh
Matthew Raymer dced4b49e1 feat: add comprehensive logging for UI refresh and capture JS console logs
- Add detailed logging to loadPluginStatus() with [UI Refresh] prefix
- Add detailed logging to checkNotificationDelivery() with [Poll] prefix
- Log status check results, change detection, and refresh triggers
- Log nextNotificationTime comparisons to debug rollover detection
- Include Capacitor/Console in logcat capture pattern to capture JS logs
- Log notification delivery detection and time calculations
- Log when rollover is detected and UI refresh is triggered

This enables debugging of UI auto-refresh mechanism and visibility
into JavaScript console logs in captured logcat output.
2025-12-29 10:22:36 +00:00

1699 lines
67 KiB
Bash
Executable File

#!/usr/bin/env bash
# Phase 1 Testing Script - Interactive Test Runner
# Guides through all Phase 1 tests with clear prompts for UI interaction
# Strict mode (P0.2)
set -euo pipefail
IFS=$'\n\t'
# Source shared library
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/alarm-test-lib.sh"
# Initialize run directory (P1)
ensure_run_dir || {
error "Failed to initialize run directory"
exit 1
}
# Phase 1 specific configuration
VERIFY_FIRE=${VERIFY_FIRE:-false} # Set VERIFY_FIRE=true to enable alarm fire verification
APP_DIR="${SCRIPT_DIR}"
PROJECT_ROOT="$(cd "${APP_DIR}/../.." && pwd)"
# Allow selecting specific tests on the command line (e.g. ./test-phase1.sh 1 2)
SELECTED_TESTS=()
check_plugin_configured() {
print_info "Checking if plugin is already configured..."
# Wait a moment for app to fully load
sleep 3
# Check if database exists (indicates plugin has been used)
DB_EXISTS=$($ADB_BIN shell run-as "${APP_ID}" ls databases/ 2>/dev/null | grep -c "daily_notification" || echo "0")
# Check if SharedPreferences has configuration (more reliable)
# The plugin stores config in SharedPreferences
PREFS_EXISTS=$($ADB_BIN shell run-as "${APP_ID}" ls shared_prefs/ 2>/dev/null | grep -c "DailyNotification" || echo "0")
# Check recent logs for configuration activity
RECENT_CONFIG=$($ADB_BIN logcat -d -t 50 | grep -E "Plugin configured|configurePlugin|Configuration" | tail -3)
if [ "${DB_EXISTS}" -gt "0" ] || [ "${PREFS_EXISTS}" -gt "0" ]; then
print_success "Plugin appears to be configured (database or preferences exist)"
# Show user what to check in UI
print_info "Please verify in the app UI that you see:"
echo " ⚙️ Plugin Settings: ✅ Configured"
echo " 🔌 Native Fetcher: ✅ Configured"
echo ""
echo "If both show ✅, the plugin is configured and you can skip configuration."
echo "If either shows ❌ or 'Not configured', you'll need to click 'Configure Plugin'."
echo ""
return 0
else
print_info "Plugin not configured (no database or preferences found)"
print_info "You will need to click 'Configure Plugin' in the app UI"
return 1
fi
}
check_permissions() {
print_info "Checking notification permissions..."
# Check if POST_NOTIFICATIONS permission is granted (Android 13+)
# Strip whitespace/newlines to ensure we get a single integer
PERM_CHECK=$($ADB_BIN shell dumpsys package "${APP_ID}" | grep -A 5 "granted=true" | grep -c "android.permission.POST_NOTIFICATIONS" 2>/dev/null | tr -d '\n\r ' || echo "0")
# Also check via app's permission status if available
PERM_GRANTED=false
# Ensure PERM_CHECK is a valid integer (strip any remaining non-digits)
PERM_CHECK=$(echo "${PERM_CHECK}" | tr -d '\n\r ' | grep -oE '^[0-9]+$' || echo "0")
if [ "${PERM_CHECK}" -gt "0" ] 2>/dev/null; then
PERM_GRANTED=true
else
# Check if we're on Android 12 or below (permission not required)
SDK_VERSION=$($ADB_BIN shell getprop ro.build.version.sdk 2>/dev/null | tr -d '\n\r ' || echo "0")
SDK_VERSION=$(echo "${SDK_VERSION}" | grep -oE '^[0-9]+$' || echo "0")
if [ "${SDK_VERSION}" -lt "33" ] 2>/dev/null; then
PERM_GRANTED=true # Pre-Android 13 doesn't need runtime permission
fi
fi
if [ "${PERM_GRANTED}" = true ]; then
print_success "Notification permissions granted"
return 0
else
print_warn "Notification permissions may not be granted"
return 1
fi
}
# Comprehensive prerequisite verification (aligned with PHASE1_TEST0_GOLDEN.md)
# Verifies all 5 items specified in the golden run document:
# 1. Plugin Settings: Configured
# 2. Native Fetcher: Configured
# 3. Notifications: Granted
# 4. Exact Alarms: Granted
# 5. Channel: Enabled (High)
#
# Usage: verify_all_prerequisites "phase1" "phase1_test0" "p1_t0_s2"
# This will verify all prerequisites and emit step events for each check
verify_all_prerequisites() {
local phase_id="${1:-}"
local test_id="${2:-}"
local base_step_id="${3:-}"
info "Verifying all prerequisites (aligned with golden run specification)..."
local all_ok=true
local missing_items=()
# 1. Check Plugin Settings: Configured
local step_id="${base_step_id}_plugin"
set_test_context "${phase_id}" "${test_id}" "${step_id}"
step_start "${step_id}" "Verifying Plugin Settings: Configured"
if check_plugin_configured >/dev/null 2>&1; then
ok "Plugin Settings: ✅ Configured"
step_pass "${step_id}" "Plugin Settings configured"
else
warn "Plugin Settings: ❌ Not configured"
all_ok=false
missing_items+=("Plugin Settings")
step_fail "${step_id}" "Plugin Settings not configured"
fi
# 2. Check Native Fetcher: Configured
# (Plugin config check also verifies native fetcher via SharedPreferences)
# For explicit verification, we check logs for fetcher configuration
step_id="${base_step_id}_fetcher"
set_test_context "${phase_id}" "${test_id}" "${step_id}"
step_start "${step_id}" "Verifying Native Fetcher: Configured"
local fetcher_configured=false
local fetcher_logs=$($ADB_BIN logcat -d -t 100 | grep -E "Native fetcher.*registered|FETCHER.*CONFIGURE|configureNativeFetcher" | tail -5 || true)
if [ -n "${fetcher_logs}" ] || check_plugin_configured >/dev/null 2>&1; then
ok "Native Fetcher: ✅ Configured"
step_pass "${step_id}" "Native Fetcher configured"
fetcher_configured=true
else
warn "Native Fetcher: ⚠️ Status unclear (check UI)"
step_warn "${step_id}" "Native Fetcher status unclear - verify in UI"
# Don't fail on this - let UI verification catch it
fi
# 3. Check Notifications: Granted
step_id="${base_step_id}_notifications"
set_test_context "${phase_id}" "${test_id}" "${step_id}"
step_start "${step_id}" "Verifying Notifications: Granted"
if check_permissions >/dev/null 2>&1; then
ok "Notifications: ✅ Granted"
step_pass "${step_id}" "Notifications permission granted"
else
error "Notifications: ❌ Not granted"
all_ok=false
missing_items+=("Notifications permission")
step_fail "${step_id}" "Notifications permission not granted"
fi
# 4. Check Exact Alarms: Granted
# Best way: Check if app has alarms with exactAllowReason=policy_permission
# This indicates the permission is granted and working
step_id="${base_step_id}_exact_alarms"
set_test_context "${phase_id}" "${test_id}" "${step_id}"
step_start "${step_id}" "Verifying Exact Alarms: Granted"
local exact_alarm_granted=false
local sdk_version=$($ADB_BIN shell getprop ro.build.version.sdk 2>/dev/null | tr -d '\n\r ' | grep -oE '^[0-9]+$' || echo '0')
if [ "${sdk_version}" -ge "31" ] 2>/dev/null; then
# Android 12+ requires SCHEDULE_EXACT_ALARM permission
# Check if app has any alarms with exactAllowReason=policy_permission
# This is the most reliable indicator that exact alarms are working
local alarm_dump=$($ADB_BIN shell dumpsys alarm 2>/dev/null | grep -A 10 "${APP_ID}" | grep -c "exactAllowReason=policy_permission" || echo "0")
alarm_dump=$(echo "${alarm_dump}" | tr -d '\n\r ' | grep -oE '^[0-9]+$' || echo "0")
if [ "${alarm_dump}" -gt "0" ] 2>/dev/null; then
# Found alarms with policy_permission - exact alarms are working
exact_alarm_granted=true
else
# No alarms yet, but check if permission is declared in manifest
# (We can't easily check runtime permission status via ADB, so we'll rely on UI)
local has_perm=$($ADB_BIN shell dumpsys package "${APP_ID}" | grep -c "android.permission.SCHEDULE_EXACT_ALARM" 2>/dev/null | tr -d '\n\r ' || echo "0")
has_perm=$(echo "${has_perm}" | grep -oE '^[0-9]+$' || echo "0")
if [ "${has_perm}" -gt "0" ] 2>/dev/null; then
# Permission is declared, assume it's granted (will be verified in UI)
exact_alarm_granted=true
fi
fi
else
# Pre-Android 12 doesn't require this permission
exact_alarm_granted=true
fi
if [ "${exact_alarm_granted}" = true ]; then
ok "Exact Alarms: ✅ Granted"
step_pass "${step_id}" "Exact Alarms permission granted"
else
warn "Exact Alarms: ⚠️ May not be granted (check UI)"
step_warn "${step_id}" "Exact Alarms permission unclear - verify in UI"
# Don't fail - let UI verification catch it
fi
# 5. Check Channel: Enabled (High)
# Channel status is best verified via UI, but we can check logs
step_id="${base_step_id}_channel"
set_test_context "${phase_id}" "${test_id}" "${step_id}"
step_start "${step_id}" "Verifying Channel: Enabled (High)"
local channel_logs=$($ADB_BIN logcat -d -t 100 | grep -E "Channel.*enabled|channel.*importance|NotificationChannel" | tail -5 || true)
if [ -n "${channel_logs}" ]; then
ok "Channel: ✅ Status found in logs (verify 'Enabled (High)' in UI)"
step_pass "${step_id}" "Channel status verified"
else
info "Channel: ⚠️ Verify 'Enabled (High)' status in UI"
step_warn "${step_id}" "Channel status unclear - verify 'Enabled (High)' in UI"
# Don't fail - channel is created automatically
fi
# Final UI verification prompt (aligned with golden run step 4)
if [ "${MODE}" != "ci" ]; then
echo ""
info "=== Final UI Verification (Golden Run Step 4) ==="
info "Please confirm in the app UI that ALL of the following show ✅:"
echo " ⚙️ Plugin Settings: ✅ Configured"
echo " 🔌 Native Fetcher: ✅ Configured"
echo " 🔔 Notifications: ✅ Granted"
echo " ⏰ Exact Alarms: ✅ Granted"
echo " 📢 Channel: ✅ Enabled (High)"
echo ""
if [ "${all_ok}" = false ]; then
error "Some prerequisites are missing: ${missing_items[*]}"
ui_prompt "Please fix the missing prerequisites in the app UI, then press Enter to continue."
else
ui_prompt "If all 5 items show ✅ in the UI, press Enter to continue. Otherwise, fix any issues first."
fi
fi
if [ "${all_ok}" = false ] && [ "${MODE}" = "ci" ]; then
error "Prerequisites not met: ${missing_items[*]}"
return 1
fi
return 0
}
ensure_permissions() {
if check_permissions; then
print_success "Permissions already granted"
return 0
else
print_info "Notification permissions needed"
wait_for_ui_action "In the app UI, click the 'Request Permissions' button.
This will show a system permission dialog.
Steps:
1. Click 'Request Permissions' button
2. In the system dialog, tap 'Allow' to grant notification permission
3. Return to the app and verify the status shows:
- 🔔 Notifications: ✅ Granted (or similar)
Once permission is granted, press Enter to continue."
# Re-check permissions
sleep 2
if check_permissions; then
print_success "Permissions granted"
return 0
else
print_warn "Permissions may still not be granted - continuing anyway"
print_info "If notifications fail, you may need to grant permissions manually"
return 1
fi
fi
}
ensure_plugin_configured() {
if check_plugin_configured; then
# Plugin might be configured, but let user verify
wait_for_ui_action "Please check the Plugin Status section at the top of the app.
If you see:
- ⚙️ Plugin Settings: ✅ Configured
- 🔌 Native Fetcher: ✅ Configured
- 🔔 Notifications: ✅ Granted (or similar)
Then the plugin is already configured - just press Enter to continue.
If any show ❌ or 'Not configured':
- Click 'Request Permissions' if notifications are not granted
- Click 'Configure Plugin' if settings/fetcher are not configured
- Wait for all to show ✅, then press Enter."
# Give a moment for any configuration that just happened
sleep 2
print_success "Continuing with tests (plugin configuration verified or skipped)"
return 0
else
# Plugin definitely needs configuration
print_info "Plugin needs configuration"
# First ensure permissions
ensure_permissions
wait_for_ui_action "Click the 'Configure Plugin' button in the app UI.
Wait for the status to update:
- ⚙️ Plugin Settings: Should change to ✅ Configured
- 🔌 Native Fetcher: Should change to ✅ Configured
Once both show ✅, press Enter to continue."
# Verify configuration completed
sleep 2
print_success "Plugin configuration completed (or verified)"
fi
}
# kill_app is now provided by the library, but Phase 1 uses it with PACKAGE variable
# The library version uses APP_ID, which is set to PACKAGE, so it should work
# But we keep this as a compatibility wrapper if needed
# Phase 1 specific helper: get_current_time (used for fire verification)
get_current_time() {
$ADB_BIN shell date +%s
}
# ========================================
# CLI Mode Parsing (P2.1)
# ========================================
parse_cli_args() {
local mode="all"
SELECTED_TESTS=()
while [[ $# -gt 0 ]]; do
case "$1" in
--setup)
mode="setup"
shift
;;
--run)
mode="run"
shift
;;
--smoke)
mode="smoke"
shift
;;
--all)
mode="all"
shift
;;
--ci)
mode="ci"
shift
;;
-h|--help)
show_help
exit 0
;;
[0-9]*)
SELECTED_TESTS+=("$1")
shift
;;
*)
error "Unknown option: $1"
show_help
exit 1
;;
esac
done
echo "$mode"
}
show_help() {
cat <<EOF
Usage: $0 [MODE] [TEST_IDS...]
Modes:
--setup Run setup only (permissions, app install, plugin config checks)
--run Run actual Phase 1 tests (requires setup first)
--smoke Minimal test: schedule one notification + verify pending
--all Run setup + all tests (default)
--ci Non-interactive: automated checks only, fail fast
Test Selection:
[TEST_IDS] Optional test numbers to run (e.g., 0 1 2)
If omitted, all tests run
Examples:
$0 # Run all (setup + tests)
$0 --setup # Setup only
$0 --run # Run tests only
$0 --smoke # Quick smoke test
$0 --ci # CI mode (non-interactive)
$0 --run 1 2 # Run only tests 1 and 2
$0 --smoke 0 # Run smoke test for test 0
EOF
}
# ========================================
# Setup Mode (P2.1)
# ========================================
run_setup() {
section "Phase 1 Setup"
info "This mode performs pre-flight checks and setup:"
echo " • ADB device connection"
echo " • Emulator readiness"
echo " • App build and installation"
echo " • Permission checks"
echo " • Plugin configuration checks"
echo ""
if [ "${MODE}" != "ci" ]; then
pause
fi
# Pre-flight checks
section "Pre-Flight Checks"
require_adb_device
# Build and install
build_app
install_app
# Clear logs
clear_logs
# Launch app for setup checks
launch_app
sleep 2
# Check permissions and plugin configuration together
section "Permissions & Plugin Configuration"
info "Checking both permissions and plugin/fetcher configuration..."
echo ""
# Check both silently first
local perms_ok=false
local plugin_ok=false
if check_permissions >/dev/null 2>&1; then
perms_ok=true
fi
if check_plugin_configured >/dev/null 2>&1; then
plugin_ok=true
fi
# Show status
if [ "${perms_ok}" = "true" ]; then
ok "Notification permissions: ✅ Granted"
else
warn "Notification permissions: ❌ Not granted"
fi
if [ "${plugin_ok}" = "true" ]; then
ok "Plugin configuration: ✅ Configured"
else
warn "Plugin configuration: ❌ Not configured"
fi
# If either needs attention, show single combined prompt
if [ "${MODE}" = "ci" ]; then
if [ "${perms_ok}" != "true" ]; then
error "Permissions not granted - cannot proceed in CI mode"
exit 1
fi
if [ "${plugin_ok}" != "true" ]; then
error "Plugin not configured - cannot proceed in CI mode"
exit 1
fi
else
# Show single combined prompt for both
if [ "${perms_ok}" != "true" ] || [ "${plugin_ok}" != "true" ]; then
ui_prompt "Please configure in the app UI:
REQUIRED STATUS (all must show ✅):
1) 🔔 Notifications: ✅ Granted
2) ⚙️ Plugin Settings: ✅ Configured
3) 🔌 Native Fetcher: ✅ Configured
ACTIONS:
- If Notifications shows ❌: Click 'Request Permissions' button
- If Plugin Settings or Native Fetcher show ❌: Click 'Configure Plugin' button
- Wait for all three to show ✅
Once all three show ✅, press Enter to continue."
# Re-check both after user action
sleep 2
echo ""
info "Re-checking after configuration..."
perms_ok=false
plugin_ok=false
if check_permissions >/dev/null 2>&1; then
perms_ok=true
ok "Notification permissions: ✅ Verified"
else
warn "Notification permissions: ⚠️ Still not granted"
fi
if check_plugin_configured >/dev/null 2>&1; then
plugin_ok=true
ok "Plugin configuration: ✅ Verified"
else
warn "Plugin configuration: ⚠️ Still not configured"
fi
echo ""
if [ "${perms_ok}" = "true" ] && [ "${plugin_ok}" = "true" ]; then
ok "All checks passed - ready to proceed"
else
warn "Some items may still need configuration - continuing anyway"
info "If tests fail, verify all three items show ✅ in the app UI"
fi
else
# Both are OK, but still prompt for final verification
ui_prompt "Please verify in the app UI that you see:
1) 🔔 Notifications: ✅ Granted
2) ⚙️ Plugin Settings: ✅ Configured
3) 🔌 Native Fetcher: ✅ Configured
If all show ✅, press Enter to continue.
If any show ❌, configure them first, then press Enter."
fi
fi
ok "Setup complete!"
# Capture initial state
capture_alarms "setup_initial"
capture_logcat "setup_initial" "DNP" 100
evidence_block "phase1_setup"
}
# ========================================
# Smoke Test Mode (P2.1)
# ========================================
run_smoke_test() {
section "Phase 1 Smoke Test"
info "Minimal test: schedule one notification + verify pending"
echo "Expected time: 2-4 minutes"
echo ""
if [ "${MODE}" != "ci" ]; then
pause
fi
# Ensure setup is done
if ! check_permissions >/dev/null 2>&1; then
warn "Setup not complete - running setup first..."
run_setup
fi
# Capture before state
capture_alarms "smoke_before"
# Schedule notification
section "Smoke Test: Schedule Notification"
launch_app
if [ "${MODE}" != "ci" ]; then
ui_prompt "1) In the app UI, click 'Test Notification' to schedule a notification for 4 minutes in the future."
else
info "CI mode: Assuming notification is scheduled manually or via automation"
sleep 3
fi
# Wait for alarm to be registered
sleep 3
# Capture after state
capture_alarms "smoke_after"
capture_logcat "smoke_after" "DNP-SCHEDULE|DNP-NOTIFY" 50
# Verify alarm exists
local alarm_count
alarm_count="$(get_plugin_alarm_count)"
if [ "${alarm_count}" -eq "1" ]; then
verdict_pass "phase1_smoke" "Smoke test passed: 1 alarm scheduled"
elif [ "${alarm_count}" -gt "1" ]; then
verdict_fail "phase1_smoke" "Smoke test failed: ${alarm_count} alarms found (expected: 1)"
else
verdict_warn "phase1_smoke" "Smoke test inconclusive: ${alarm_count} alarms found (expected: 1, may be race condition)"
fi
evidence_block "phase1_smoke"
}
# ========================================
# Main Test Execution
# ========================================
main() {
# Parse CLI arguments
MODE="$(parse_cli_args "$@")"
section "Phase 1 Testing Script"
echo "Mode: ${MODE}"
echo "Run ID: ${RUN_ID}"
echo "Evidence directory: $(get_run_dir)"
echo ""
if [ "${MODE}" = "setup" ]; then
run_setup
return 0
elif [ "${MODE}" = "smoke" ]; then
run_smoke_test
return 0
elif [ "${MODE}" = "ci" ]; then
info "CI mode: Non-interactive, automated checks only"
run_setup
# In CI mode, we might want to run a subset of tests
# For now, just run smoke test
run_smoke_test
return 0
fi
# For --run and --all modes, continue with full test suite
if [ "${MODE}" = "all" ]; then
info "Running full test suite (setup + tests)"
if [ "${MODE}" != "ci" ]; then
pause
fi
run_setup
elif [ "${MODE}" = "run" ]; then
info "Running tests only (assuming setup already done)"
if [ "${MODE}" != "ci" ]; then
pause
fi
fi
# ============================================
# TEST 0: Daily Rollover (Core Contract Verification)
# ============================================
if should_run_test "0" SELECTED_TESTS; then
section "TEST 0: Daily Rollover Verification"
# Set test context for event emission
set_test_context "phase1" "phase1_test0" ""
info "Purpose: Verify that after a notification fires, the next day's"
info " schedule is correctly computed and only ONE alarm exists."
info ""
info "Expected time: 5-10 minutes"
info "Automatable: Partial (requires manual clock manipulation or waiting)"
echo ""
if [ "${MODE}" != "ci" ]; then
pause
fi
# Capture initial state
set_test_context "phase1" "phase1_test0" "p1_t0_s1"
step_start "p1_t0_s1" "Capturing initial state"
capture_alarms "test0_initial"
capture_logcat "test0_initial" "DNP" 50
step_pass "p1_t0_s1" "Initial state captured"
substep "Step 1: Verify setup and schedule a test notification"
set_test_context "phase1" "phase1_test0" "p1_t0_s2"
step_start "p1_t0_s2" "Verifying prerequisites and scheduling test notification"
launch_app
# Comprehensive prerequisite verification (aligned with PHASE1_TEST0_GOLDEN.md step 4)
# Verifies all 5 items: Plugin Settings, Native Fetcher, Notifications, Exact Alarms, Channel
verify_all_prerequisites "phase1" "phase1_test0" "p1_t0_s2"
# If prerequisites failed and we're in CI mode, exit
if [ $? -ne 0 ] && [ "${MODE}" = "ci" ]; then
error "Prerequisites not met - cannot proceed with Test 0"
exit 1
fi
# Capture pre-schedule state
capture_alarms "test0_before_schedule"
capture_screenshot "test0_before_schedule"
local initial_count system_count
initial_count="$(get_plugin_alarm_count)"
system_count="$(get_system_alarm_count)"
info "Current notification alarms: ${initial_count} (expected before scheduling: 0)"
info "System/other alarms: ${system_count} (for context)"
if [ "${initial_count}" -eq "0" ] 2>/dev/null; then
ok "No existing notification alarms (clean state)"
if [ "${MODE}" != "ci" ]; then
ui_prompt "1) In the app UI, schedule a daily notification.
For this test, schedule it for a time very soon (e.g., 1-2 minutes from now)
to observe the rollover behavior.
This will schedule:
• 1 notification alarm (AlarmManager) for the specified time
• 1 prefetch job (WorkManager) for 2 minutes before that time"
else
info "CI mode: Assuming notification is scheduled manually"
sleep 3
fi
sleep 3 # Give alarm time to be registered
# Capture post-schedule state
capture_alarms "test0_after_schedule"
capture_screenshot "test0_after_schedule"
local post_schedule_count
post_schedule_count="$(get_plugin_alarm_count)"
# Preliminary check (final verdict comes after rollover)
if [ "${post_schedule_count}" -eq "0" ] 2>/dev/null; then
warn "Found 0 plugin alarms right after scheduling (may be race condition)"
info "Alarm may not be visible in dumpsys yet - will verify after rollover"
elif [ "${post_schedule_count}" -eq "1" ] 2>/dev/null; then
ok "Found 1 notification alarm (expected: 1) - preliminary check passed"
else
warn "Found ${post_schedule_count} notification alarms (expected: 1) - DUPLICATES DETECTED!"
error "This indicates duplicate NOTIFICATION alarms were created (BUG)"
show_plugin_alarms_compact
fi
initial_count="${post_schedule_count}"
fi
# Show alarm details if we found exactly 1 alarm
local initial_alarm_time=""
if [ "${initial_count}" -eq "1" ] 2>/dev/null; then
ok "Single notification alarm scheduled (one per day)"
local alarm_details
alarm_details="$($ADB_BIN shell dumpsys alarm | grep -A 3 "com.timesafari.dailynotification" | grep -A 3 "com.timesafari.daily.NOTIFICATION" | head -10)"
if [ -n "${alarm_details}" ]; then
info "Notification alarm details:"
echo "${alarm_details}" | head -5
echo ""
# Extract initial alarm time for rollover verification (normalize seconds to 00)
initial_alarm_time=$(echo "${alarm_details}" | grep -oE "origWhen=[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}" | head -1 | sed 's/origWhen=//' | sed 's/:[0-9][0-9]$/:00/')
if [ -n "${initial_alarm_time}" ]; then
info "Initial alarm time: ${initial_alarm_time} (normalized)"
fi
fi
elif [ "${initial_count}" -gt "1" ] 2>/dev/null; then
warn "Found ${initial_count} notification alarms (expected: 1) - DUPLICATES DETECTED!"
fi
step_pass "p1_t0_s2" "Notification scheduled"
substep "Step 2: Wait for notification to fire"
set_test_context "phase1" "phase1_test0" "p1_t0_s3"
step_start "p1_t0_s3" "Waiting for notification to fire"
info "The test will automatically verify rollover after the notification fires."
echo ""
echo "What will be verified automatically:"
echo " • Alarm count (should be exactly 1)"
echo " • Alarm time changed (should be 24 hours later)"
echo " • Rollover logs present"
echo ""
if [ "${MODE}" != "ci" ]; then
ui_prompt "Please wait for the notification to fire (or advance the emulator clock past the scheduled time).
The test will automatically verify:
• Only ONE alarm exists (one per day)
• Alarm time advanced to tomorrow (24 hours later)
• Rollover occurred (logs will be checked)
Press Enter after the notification has fired (or after advancing the clock)."
else
info "CI mode: Waiting for notification to fire..."
sleep 5 # Give time for rollover if automated
fi
step_pass "p1_t0_s3" "Notification fired (or time advanced)"
substep "Step 3: Verify rollover state"
set_test_context "phase1" "phase1_test0" "p1_t0_s4"
step_start "p1_t0_s4" "Waiting for rollover to complete"
info "Waiting for rollover to complete (notification should auto-schedule next day's alarm)..."
info "Allowing up to ~15 seconds for Android to process rollover and update alarms..."
# Wait longer for rollover to complete (notification fires → rollover schedules next day)
sleep 5 # Give Android time to process the notification fire and rollover
local post_rollover_count system_final
post_rollover_count="$(wait_for_stable_plugin_alarm_count 5 2)"
system_final="$(get_system_alarm_count)"
# Capture post-rollover state
capture_alarms "test0_after_rollover"
# Look for rollover logs: DN|RESCHEDULE, DN|DISPLAY, DN|RECEIVE, ROLLOVER_ON_FIRE, etc.
# Also capture JavaScript console logs (Capacitor/Console) for UI debugging
capture_logcat "test0_after_rollover" "DN|RESCHEDULE|DN|DISPLAY|DN|RECEIVE|ROLLOVER|DNP-SCHEDULE|Capacitor/Console" 200
capture_screenshot "test0_after_rollover"
step_pass "p1_t0_s4" "Post-rollover evidence captured"
set_test_context "phase1" "phase1_test0" "p1_t0_s6"
step_start "p1_t0_s6" "Verifying schedule state"
info "Notification alarms after rollover: ${post_rollover_count} (expected: 1)"
info "System/other alarms: ${system_final} (for context)"
# Extract post-rollover alarm time and verify it changed
local post_rollover_alarm_time=""
local rollover_verified=false
if [ "${post_rollover_count}" -eq "1" ] 2>/dev/null; then
local post_alarm_details
post_alarm_details="$($ADB_BIN shell dumpsys alarm | grep -A 3 "com.timesafari.dailynotification" | grep -A 3 "com.timesafari.daily.NOTIFICATION" | head -10)"
if [ -n "${post_alarm_details}" ]; then
# Extract post-rollover alarm time (normalize seconds to 00 for comparison)
post_rollover_alarm_time=$(echo "${post_alarm_details}" | grep -oE "origWhen=[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}" | head -1 | sed 's/origWhen=//' | sed 's/:[0-9][0-9]$/:00/')
if [ -n "${post_rollover_alarm_time}" ]; then
info "Post-rollover alarm time: ${post_rollover_alarm_time} (normalized)"
# Verify alarm time changed (rollover occurred)
# Compare dates only (YYYY-MM-DD) to detect day change
if [ -n "${initial_alarm_time}" ] && [ -n "${post_rollover_alarm_time}" ]; then
local initial_date=$(echo "${initial_alarm_time}" | cut -d' ' -f1)
local post_date=$(echo "${post_rollover_alarm_time}" | cut -d' ' -f1)
if [ "${initial_date}" != "${post_date}" ]; then
ok "Alarm date changed: ${initial_alarm_time}${post_rollover_alarm_time}"
rollover_verified=true
else
warn "Alarm date did NOT change: ${post_rollover_alarm_time} (same date as initial: ${initial_date})"
warn "This indicates the notification did not fire and rollover did not occur"
rollover_verified=false
fi
fi
fi
fi
# Check for rollover logs (wider search including all possible rollover patterns)
# Look for: ROLLOVER_ON_FIRE source, RESCHEDULE logs, DNP-SCHEDULE with ROLLOVER, etc.
# Note: DN|RESCHEDULE uses pipe character, so we need to escape it properly
local rollover_logs=$($ADB_BIN logcat -d -t 1000 | grep -E "ROLLOVER_ON_FIRE|source=ROLLOVER|DNP-SCHEDULE.*ROLLOVER|DN\|RESCHEDULE|DN\|RESCHEDULE_OK|DN\|RESCHEDULE_START|Scheduling next.*notification|scheduleNextNotification|Next notification scheduled" | tail -15 || true)
if [ -n "${rollover_logs}" ]; then
ok "Rollover logs found - notification fired and rollover occurred"
info "Rollover log excerpt:"
echo "${rollover_logs}" | head -3 | sed 's/^/ /'
rollover_verified=true
else
warn "No rollover logs found - checking if notification fired at all..."
# Check for any notification display logs
local notification_logs=$($ADB_BIN logcat -d -t 500 | grep -E "DN\|DISPLAY_NOTIF|Notification.*displayed|com.timesafari.daily.NOTIFICATION" | tail -5 || true)
if [ -n "${notification_logs}" ]; then
warn "Notification display logs found, but no rollover logs - rollover may have failed"
info "Notification log excerpt:"
echo "${notification_logs}" | head -2 | sed 's/^/ /'
else
warn "No notification logs found - notification may not have fired"
fi
if [ "${rollover_verified}" = false ]; then
warn "⚠️ Rollover verification failed: alarm date unchanged AND no rollover logs"
fi
fi
fi
# Final verdict
if [ "${post_rollover_count}" -eq "1" ] 2>/dev/null && [ "${rollover_verified}" = true ]; then
step_pass "p1_t0_s6" "Schedule state verified - exactly 1 alarm, rollover confirmed"
set_test_context "phase1" "phase1_test0" "p1_t0_s7"
verdict_pass "test0_daily_rollover" "Daily rollover created exactly one NOTIFICATION alarm for tomorrow"
elif [ "${post_rollover_count}" -eq "1" ] 2>/dev/null && [ "${rollover_verified}" = false ]; then
step_warn "p1_t0_s6" "Alarm count correct but rollover not verified"
warn "Found 1 alarm but time did not change - notification may not have fired"
set_test_context "phase1" "phase1_test0" "p1_t0_s7"
verdict_warn "test0_daily_rollover" "Alarm count correct (1) but rollover not verified - alarm time unchanged, no rollover logs"
elif [ "${post_rollover_count}" -gt "1" ] 2>/dev/null; then
step_fail "p1_t0_s6" "Duplicate alarms detected"
error "Daily rollover created ${post_rollover_count} NOTIFICATION alarms (duplicates)"
show_plugin_alarms_compact
set_test_context "phase1" "phase1_test0" "p1_t0_s7"
verdict_fail "test0_daily_rollover" "Duplicate alarms detected after rollover (expected: 1, got: ${post_rollover_count})"
else
step_warn "p1_t0_s6" "No alarm found after rollover"
warn "No NOTIFICATION alarm found after rollover (expected: 1, got: 0)"
show_plugin_alarms_compact
set_test_context "phase1" "phase1_test0" "p1_t0_s7"
verdict_warn "test0_daily_rollover" "Rollover may have failed - no alarm found (expected: 1, got: 0)"
fi
evidence_block "test0_daily_rollover"
if [ "${MODE}" != "ci" ]; then
pause
fi
fi # End of "if should_run_test 0" block
# ============================================
# TEST 1: Force-Stop Recovery - Database Restoration
# ============================================
if should_run_test "1" SELECTED_TESTS; then
section "TEST 1: Force-Stop Recovery - Database Restoration"
# Set test context
set_test_context "phase1" "phase1_test1" ""
info "Purpose: Verify that after force-stop (which clears alarms), recovery"
info " uses the database to rebuild alarms on app relaunch."
info ""
info "Expected time: 5-8 minutes"
info "Automatable: Partial (requires manual force-stop verification)"
echo ""
if [ "${MODE}" != "ci" ]; then
pause
fi
# Capture initial state
set_test_context "phase1" "phase1_test1" "p1_t1_s1"
step_start "p1_t1_s1" "Clean start - verify no lingering alarms"
capture_alarms "test1_initial"
capture_logcat "test1_initial" "DNP" 50
local lingering_count system_count
lingering_count="$(get_plugin_alarm_count)"
system_count="$(get_system_alarm_count)"
info "Current plugin notification alarms: ${lingering_count}"
info "System/other alarms: ${system_count} (for context)"
local goto_test1_end=false
if [ "${lingering_count}" -gt "0" ] 2>/dev/null; then
warn "Found ${lingering_count} lingering plugin alarm(s) - these will interfere with TEST 1"
info "TEST 1 needs a clean state (no existing plugin alarms)"
info "Resetting app state via uninstall + reinstall..."
# Uninstall existing app
info "Uninstalling existing app..."
set +e
local uninstall_output uninstall_status
uninstall_output="$($ADB_BIN uninstall "$APP_ID" 2>&1)"
uninstall_status=$?
set -e
if [ $uninstall_status -eq 0 ]; then
ok "App uninstall succeeded (clean slate)"
else
if grep -q "DELETE_FAILED_INTERNAL_ERROR" <<<"$uninstall_output"; then
info "No existing app to uninstall (continuing)"
else
warn "App uninstall reported an error: $uninstall_output (continuing anyway)"
fi
fi
# Reinstall APK
info "Reinstalling APK..."
if $ADB_BIN install -r "$APK_PATH" >/dev/null 2>&1; then
ok "App reinstall succeeded"
else
error "App reinstall FAILED - cannot proceed with TEST 1"
warn "Marking TEST 1 as INCONCLUSIVE due to dirty starting state"
capture_screenshot "test1_dirty_state_reinstall_failed"
if [ "${MODE}" != "ci" ]; then
pause
fi
goto_test1_end=true
fi
if [ "${goto_test1_end}" != "true" ]; then
# Verify install
if ! $ADB_BIN shell pm list packages | grep -q "$APP_ID"; then
error "App not found in package list after reinstall - aborting TEST 1"
capture_screenshot "test1_dirty_state_verify_failed"
if [ "${MODE}" != "ci" ]; then
pause
fi
goto_test1_end=true
fi
fi
if [ "${goto_test1_end}" != "true" ]; then
# Clear logs
info "Clearing logcat buffer after reinstall..."
$ADB_BIN logcat -c || true
# Re-check plugin alarms
info "Rechecking plugin alarms after reset..."
sleep 2
lingering_count="$(get_plugin_alarm_count)"
info "Plugin alarms after reset: ${lingering_count} (expected: 0)"
if [ "${lingering_count}" -ne "0" ] 2>/dev/null; then
warn "TEST 1 starting with non-zero alarm count even after reset; treating as INCONCLUSIVE"
info "This may indicate device-specific behavior where alarms persist across uninstall"
capture_screenshot "test1_unexpected_alarms_after_reset"
if [ "${MODE}" != "ci" ]; then
pause
fi
goto_test1_end=true
fi
fi
if [ "${goto_test1_end}" = "true" ]; then
warn "TEST 1 skipped due to inability to achieve clean starting state"
if [ "${MODE}" != "ci" ]; then
pause
fi
else
ok "App state reset complete. TEST 1 starting from clean state"
fi
else
ok "No lingering plugin alarms found (clean state)"
fi
step_pass "p1_t1_s1" "Clean state verified"
# Skip remaining TEST 1 steps if we couldn't achieve clean state
if [ "${goto_test1_end}" != "true" ]; then
substep "Step 2: Schedule a known future alarm"
set_test_context "phase1" "phase1_test1" "p1_t1_s2"
step_start "p1_t1_s2" "Scheduling notification"
launch_app
ensure_plugin_configured
if [ "${MODE}" != "ci" ]; then
ui_prompt "1) In the app UI, click the 'Test Notification' button.
This will schedule ONE notification for 4 minutes in the future.
(App supports one alarm per day)
The alarm will be stored in the database AND scheduled in AlarmManager."
else
info "CI mode: Assuming notification is scheduled manually"
sleep 3
fi
sleep 2
# Capture before force-stop state
capture_alarms "test1_before_force_stop"
capture_logcat "test1_before_force_stop" "DNP-SCHEDULE|DNP-NOTIFY" 50
step_pass "p1_t1_s2" "Notification scheduled"
substep "Step 3: Verify alarm exists in AlarmManager (BEFORE force-stop)"
set_test_context "phase1" "phase1_test1" "p1_t1_s3"
step_start "p1_t1_s3" "Verifying alarm before force-stop"
local alarm_count_before system_count_before
alarm_count_before="$(get_plugin_alarm_count)"
system_count_before="$(get_system_alarm_count)"
info "Plugin alarms: ${alarm_count_before} (expected: 1)"
info "System/other alarms: ${system_count_before} (for context)"
if [ "${alarm_count_before}" -eq "0" ] 2>/dev/null; then
error "No plugin alarms found in AlarmManager - cannot test force-stop recovery"
info "Make sure you clicked 'Test Notification' and wait a moment"
if [ "${MODE}" != "ci" ]; then
pause
fi
# Re-check
alarm_count_before="$(get_plugin_alarm_count)"
if [ "${alarm_count_before}" -eq "0" ] 2>/dev/null; then
error "Still no alarms found - aborting test"
verdict_fail "test1_force_stop_recovery" "No alarms found before force-stop - cannot test recovery"
evidence_block "test1_force_stop_recovery"
goto_test1_end=true
fi
fi
if [ "${alarm_count_before}" -eq "1" ] 2>/dev/null; then
ok "Single plugin alarm confirmed in AlarmManager (one per day)"
else
warn "Found ${alarm_count_before} plugin alarms (expected: 1) - continuing anyway"
fi
# Extract trigger time for later verification
local alarm_details_before alarm_trigger_ms alarm_trigger_sec alarm_readable
alarm_details_before="$($ADB_BIN shell dumpsys alarm | grep -A 3 "$APP_ID" | head -10)"
if [ -n "${alarm_details_before}" ]; then
info "Alarm details:"
echo "${alarm_details_before}" | head -5
echo ""
fi
alarm_trigger_ms=$(echo "${alarm_details_before}" | grep -oE "origWhen [0-9]+" | head -1 | awk '{print $2}')
if [ -n "${alarm_trigger_ms}" ]; then
alarm_trigger_sec=$((alarm_trigger_ms / 1000))
alarm_readable=$(date -d "@${alarm_trigger_sec}" 2>/dev/null || echo "${alarm_trigger_sec}")
info "Alarm scheduled for: ${alarm_readable} (${alarm_trigger_ms} ms)"
fi
if [ "${MODE}" != "ci" ]; then
pause
fi
substep "Step 4: Force-stop the app (clears alarms)"
set_test_context "phase1" "phase1_test1" "p1_t1_s3"
step_start "p1_t1_s3" "Force-stopping app"
warn "Force-stop will clear ALL alarms from AlarmManager"
info "Executing: $ADB_BIN shell am force-stop ${APP_ID}"
force_stop_app
sleep 2
# Capture after force-stop state
capture_alarms "test1_after_force_stop"
substep "Step 5: Verify alarms are MISSING (cleared by OS)"
set_test_context "phase1" "phase1_test1" "p1_t1_s4"
step_start "p1_t1_s4" "Verifying alarms cleared"
sleep 1
local alarm_count_after system_count_after
alarm_count_after="$(get_plugin_alarm_count)"
system_count_after="$(get_system_alarm_count)"
info "Plugin alarms after force-stop: ${alarm_count_after} (expected: 0)"
info "System/other alarms: ${system_count_after} (for context)"
if [ "${alarm_count_after}" -eq "0" ] 2>/dev/null; then
ok "Plugin alarms cleared by force-stop (count: ${alarm_count_after})"
info "This confirms: Force-stop cleared alarms from AlarmManager"
step_pass "p1_t1_s4" "Alarms cleared by force-stop"
else
warn "Plugin alarms still present after force-stop (count: ${alarm_count_after})"
info "Some devices/OS versions may not clear alarms on force-stop"
info "Continuing test anyway - recovery should still work"
step_warn "p1_t1_s4" "Alarms not cleared (device-specific behavior)"
fi
if [ "${MODE}" != "ci" ]; then
pause
fi
substep "Step 6: Relaunch app (triggers recovery from database)"
set_test_context "phase1" "phase1_test1" "p1_t1_s4"
step_start "p1_t1_s4" "Relaunching app to trigger recovery"
clear_logs
launch_app
sleep 4 # Give recovery time to run
substep "Step 7: Verify recovery rebuilt alarms from database"
set_test_context "phase1" "phase1_test1" "p1_t1_s4"
step_start "p1_t1_s4" "Verifying recovery rebuilt alarms"
sleep 2
# Capture after recovery state
capture_alarms "test1_after_recovery"
capture_logcat "test1_after_recovery" "DNP-REACTIVATION" 100
capture_screenshot "test1_after_recovery"
local alarm_count_recovered system_count_recovered
alarm_count_recovered="$(get_plugin_alarm_count)"
system_count_recovered="$(get_system_alarm_count)"
info "Plugin alarms after recovery: ${alarm_count_recovered} (expected: 1)"
info "System/other alarms: ${system_count_recovered} (for context)"
info "Checking recovery logs..."
check_recovery_logs
info "Expected log output:"
echo " DNP-REACTIVATION: Starting app launch recovery"
echo " DNP-REACTIVATION: Rescheduled alarm: <id> for <time>"
echo " DNP-REACTIVATION: Cold start recovery complete: ... rescheduled>=1, ..."
echo ""
# Check recovery logs for rescheduling and recovery source
local recovery_result rescheduled_count verified_count recovery_source
recovery_result="$($ADB_BIN logcat -d | grep "Cold start recovery complete\|Boot recovery complete" | tail -1)"
rescheduled_count=$(echo "${recovery_result}" | grep -oE "rescheduled=[0-9]+" | grep -oE "[0-9]+" || echo "0")
verified_count=$(echo "${recovery_result}" | grep -oE "verified=[0-9]+" | grep -oE "[0-9]+" || echo "0")
recovery_source="$($ADB_BIN logcat -d | grep -E "recovery source|from database|DATABASE" | tail -1 || echo "")"
# Pass/fail criteria
local test1_passed=false
if [ "${alarm_count_recovered}" -gt "0" ] 2>/dev/null; then
ok "Alarms restored in AlarmManager (count: ${alarm_count_recovered})"
if [ "${rescheduled_count}" -gt "0" ] 2>/dev/null; then
ok "Recovery logs confirm rescheduling (rescheduled=${rescheduled_count})"
test1_passed=true
else
warn "Alarms restored but logs show rescheduled=0"
info "This might be okay if alarms were verified instead of rescheduled"
# Still pass if alarms are restored, even if rescheduled=0
test1_passed=true
fi
else
error "No alarms restored (count: ${alarm_count_recovered})"
info "Recovery may have failed or alarms were not in database"
fi
# Final verdict
if [ "${test1_passed}" = "true" ]; then
step_pass "p1_t1_s4" "Recovery successful"
set_test_context "phase1" "phase1_test1" "p1_t1_s5"
verdict_pass "test1_force_stop_recovery" "Recovery successfully rebuilt alarms from database (before: ${alarm_count_before}, after stop: ${alarm_count_after}, after recovery: ${alarm_count_recovered}, rescheduled: ${rescheduled_count})"
else
step_fail "p1_t1_s4" "Recovery failed"
set_test_context "phase1" "phase1_test1" "p1_t1_s5"
verdict_fail "test1_force_stop_recovery" "Recovery did not rebuild alarms correctly (before: ${alarm_count_before}, after stop: ${alarm_count_after}, after recovery: ${alarm_count_recovered})"
fi
evidence_block "test1_force_stop_recovery"
# Optional: Verify alarm fires (controlled by VERIFY_FIRE flag)
if [ "${VERIFY_FIRE}" = "true" ] && [ -n "${alarm_trigger_ms}" ] && [ "${goto_test1_end}" != "true" ]; then
substep "Step 8: Verify alarm fires at scheduled time (optional)"
local current_time_sec current_time_ms wait_ms wait_sec
current_time_sec=$(get_current_time)
current_time_ms=$((current_time_sec * 1000))
wait_ms=$((alarm_trigger_ms - current_time_ms))
if [ "${wait_ms}" -lt 0 ]; then
warn "Alarm time already passed (${wait_ms} ms ago); skipping fire verification"
else
wait_sec=$((wait_ms / 1000))
# Clamp upper bound to prevent accidentally waiting 30+ minutes
if [ "${wait_sec}" -gt 600 ]; then
warn "Alarm is >10 minutes away (${wait_sec}s); skipping fire verification"
info "To test fire verification, schedule alarm closer to current time"
else
info "Alarm scheduled for: ${alarm_readable}"
info "Current time: $(date -d "@${current_time_sec}" 2>/dev/null || echo "${current_time_sec}")"
info "Waiting ~${wait_sec} seconds for alarm to fire..."
clear_logs
sleep ${wait_sec}
sleep 2
info "Checking logs for fired alarm..."
local alarm_fired
alarm_fired="$($ADB_BIN logcat -d | grep -E "DNP-RECEIVE|DNP-NOTIFY|DNP-WORK|Alarm fired|Notification displayed" | tail -10)"
if [ -n "${alarm_fired}" ]; then
ok "Alarm fired! Logs:"
echo "${alarm_fired}"
else
warn "No alarm fire logs found"
info "Check notification tray manually or review recent logs"
fi
fi
fi
fi
fi # End of "if goto_test1_end != true" block
if [ "${MODE}" != "ci" ]; then
pause
fi
fi # End of "if should_run_test 1" block
# ============================================
# TEST 2: Schedule Update (One-Per-Day Semantics)
# ============================================
if should_run_test "2" SELECTED_TESTS; then
section "TEST 2: Schedule Update Verification"
# Set test context
set_test_context "phase1" "phase1_test2" ""
info "Purpose: Verify that updating the schedule time maintains 'one per day' semantics"
info ""
info "Expected time: 4-6 minutes"
info "Automatable: Partial (requires manual schedule update)"
echo ""
if [ "${MODE}" != "ci" ]; then
pause
fi
# Capture initial state
set_test_context "phase1" "phase1_test2" "p1_t2_s1"
step_start "p1_t2_s1" "Schedule initial notification"
capture_alarms "test2_initial"
capture_logcat "test2_initial" "DNP" 50
substep "Step 1: Launch app and verify initial schedule"
launch_app
ensure_plugin_configured
# Get initial alarm count
local initial_alarm_count system_alarm_count
initial_alarm_count="$(get_plugin_alarm_count)"
system_alarm_count="$(get_system_alarm_count)"
info "Plugin alarms: ${initial_alarm_count} (expected: 1)"
info "System/other alarms: ${system_alarm_count} (for context)"
if [ "${initial_alarm_count}" -eq "1" ] 2>/dev/null; then
ok "Initial alarm confirmed (one per day)"
step_pass "p1_t2_s1" "Initial notification scheduled"
elif [ "${initial_alarm_count}" -eq "0" ] 2>/dev/null; then
warn "No initial alarm found - scheduling one first..."
if [ "${MODE}" != "ci" ]; then
ui_prompt "1) In the app UI, schedule a daily notification (e.g., click 'Test Notification')."
else
info "CI mode: Assuming notification is scheduled manually"
sleep 2
fi
sleep 2
initial_alarm_count="$(get_plugin_alarm_count)"
if [ "${initial_alarm_count}" -eq "1" ] 2>/dev/null; then
ok "Alarm scheduled"
step_pass "p1_t2_s1" "Initial notification scheduled"
else
error "Failed to schedule initial alarm"
step_fail "p1_t2_s1" "Failed to schedule"
if [ "${MODE}" != "ci" ]; then
pause
fi
fi
else
warn "Found ${initial_alarm_count} plugin alarms (expected: 1) - continuing anyway"
step_warn "p1_t2_s1" "Unexpected alarm count"
fi
# Capture before update state
capture_alarms "test2_before_update"
substep "Step 2: Update schedule time"
set_test_context "phase1" "phase1_test2" "p1_t2_s2"
step_start "p1_t2_s2" "Update schedule"
if [ "${MODE}" != "ci" ]; then
ui_prompt "2) In the app UI, change the schedule time (e.g., from 06:50 to 07:10)
and apply the schedule.
This should cancel the old alarm and schedule a new one at the new time.
You should still have exactly 1 alarm (one per day)."
else
info "CI mode: Assuming schedule is updated manually"
sleep 3
fi
sleep 3
# Capture after update state
capture_alarms "test2_after_update"
capture_logcat "test2_after_update" "DNP-SCHEDULE|DNP-NOTIFY" 50
local updated_alarm_count
updated_alarm_count="$(get_plugin_alarm_count)"
info "Plugin alarms after update: ${updated_alarm_count} (expected: 1)"
local test2_failed=false
if [ "${updated_alarm_count}" -eq "1" ] 2>/dev/null; then
ok "Single alarm confirmed after schedule update (one per day maintained)"
step_pass "p1_t2_s2" "Schedule updated"
elif [ "${updated_alarm_count}" -gt "1" ] 2>/dev/null; then
error "TEST 2 FAILED: Found ${updated_alarm_count} plugin alarms after update (expected: 1)"
error "Old alarm was NOT canceled - violates 'one per day' semantics"
info "This indicates the plugin did not clean up existing schedules before creating new one"
test2_failed=true
step_fail "p1_t2_s2" "Multiple alarms detected"
else
warn "Found ${updated_alarm_count} plugin alarms (expected: 1) - no alarm scheduled after update"
step_warn "p1_t2_s2" "No alarm after update"
fi
substep "Step 3: Kill app and relaunch (triggers recovery)"
set_test_context "phase1" "phase1_test2" "p1_t2_s3"
step_start "p1_t2_s3" "Verify one-per-day semantics"
kill_app
clear_logs
launch_app
substep "Step 4: Check recovery logs and verify alarm count"
sleep 3
# Capture after recovery state
capture_alarms "test2_after_recovery"
capture_logcat "test2_after_recovery" "DNP-REACTIVATION" 100
check_recovery_logs
info "Expected log output (either):"
echo " DNP-REACTIVATION: Verified scheduled alarm: <id> at <time>"
echo " OR"
echo " DNP-REACTIVATION: Rescheduled missing alarm: <id> at <time>"
echo " DNP-REACTIVATION: Cold start recovery complete: ..., verified=1 or rescheduled=1, ..."
echo ""
local recovery_result rescheduled_count verified_count alarm_count_after_recovery
recovery_result="$($ADB_BIN logcat -d | grep "Cold start recovery complete" | tail -1)"
rescheduled_count=$(echo "${recovery_result}" | grep -oE "rescheduled=[0-9]+" | grep -oE "[0-9]+" || echo "0")
verified_count=$(echo "${recovery_result}" | grep -oE "verified=[0-9]+" | grep -oE "[0-9]+" || echo "0")
alarm_count_after_recovery="$(get_plugin_alarm_count)"
info "Plugin alarms after recovery: ${alarm_count_after_recovery} (expected: 1)"
# CRITICAL: Test fails if alarm count > 1 after recovery (violates "one per day")
if [ "${alarm_count_after_recovery}" -gt "1" ] 2>/dev/null; then
error "TEST 2 FAILED: Found ${alarm_count_after_recovery} plugin alarms after recovery (expected: 1)"
error "Multiple schedules in database caused recovery to reschedule duplicates"
error "This violates 'one per day' semantics - only one notification per day should exist"
test2_failed=true
step_fail "p1_t2_s3" "Multiple alarms after recovery"
elif [ "${alarm_count_after_recovery}" -eq "1" ] 2>/dev/null; then
step_pass "p1_t2_s3" "One-per-day semantics verified"
else
step_warn "p1_t2_s3" "Inconclusive alarm count"
fi
# Final verdict
set_test_context "phase1" "phase1_test2" "p1_t2_s4"
if [ "${test2_failed}" = "true" ]; then
verdict_fail "test2_schedule_update" "Multiple alarms detected - violates 'one per day' semantics (after update: ${updated_alarm_count}, after recovery: ${alarm_count_after_recovery})"
elif [ "${alarm_count_after_recovery}" -eq "1" ] 2>/dev/null; then
if [ "${rescheduled_count}" -gt "0" ] 2>/dev/null; then
verdict_pass "test2_schedule_update" "Schedule update maintained one per day semantics (rescheduled: ${rescheduled_count})"
elif [ "${verified_count}" -gt "0" ] 2>/dev/null; then
verdict_pass "test2_schedule_update" "Schedule update maintained one per day semantics (verified: ${verified_count})"
else
verdict_pass "test2_schedule_update" "Schedule update maintained one per day semantics (no recovery needed)"
fi
else
verdict_warn "test2_schedule_update" "Schedule update result inconclusive (after update: ${updated_alarm_count}, after recovery: ${alarm_count_after_recovery})"
fi
evidence_block "test2_schedule_update"
if [ "${MODE}" != "ci" ]; then
pause
fi
fi # End of "if should_run_test 2" block
# ============================================
# TEST 3: Recovery Timeout
# ============================================
if should_run_test "3" SELECTED_TESTS; then
section "TEST 3: Recovery Timeout"
# Set test context
set_test_context "phase1" "phase1_test3" ""
info "Purpose: Verify recovery times out gracefully"
info ""
info "Expected time: 2-3 minutes"
info "Automatable: Yes (code verification only)"
info "Note: Full test (100+ schedules) requires manual execution"
echo ""
if [ "${MODE}" != "ci" ]; then
pause
fi
# Capture initial state
capture_alarms "test3_initial"
substep "Step 1: Check recovery timeout implementation"
set_test_context "phase1" "phase1_test3" "p1_t3_s1"
step_start "p1_t3_s1" "Check recovery timeout implementation"
local timeout_found timeout_protection_found
timeout_found=false
timeout_protection_found=false
if grep -q "RECOVERY_TIMEOUT_SECONDS.*2L" "${PROJECT_ROOT}/android/src/main/java/com/timesafari/dailynotification/ReactivationManager.kt" 2>/dev/null; then
ok "Timeout is set to 2 seconds"
timeout_found=true
else
error "Timeout not found in code"
fi
if grep -q "withTimeout" "${PROJECT_ROOT}/android/src/main/java/com/timesafari/dailynotification/ReactivationManager.kt" 2>/dev/null; then
ok "Timeout protection is implemented"
timeout_protection_found=true
else
error "Timeout protection not found"
fi
# Capture evidence
capture_logcat "test3_timeout_check" "DNP-REACTIVATION" 50
if [ "${timeout_found}" = "true" ] && [ "${timeout_protection_found}" = "true" ]; then
step_pass "p1_t3_s1" "Timeout mechanism verified"
else
step_fail "p1_t3_s1" "Timeout mechanism not found"
fi
# Final verdict
set_test_context "phase1" "phase1_test3" "p1_t3_s2"
if [ "${timeout_found}" = "true" ] && [ "${timeout_protection_found}" = "true" ]; then
verdict_pass "test3_recovery_timeout" "Timeout mechanism verified in code (2 seconds, withTimeout protection)"
else
verdict_fail "test3_recovery_timeout" "Timeout mechanism not properly implemented (timeout found: ${timeout_found}, protection found: ${timeout_protection_found})"
fi
evidence_block "test3_recovery_timeout"
if [ "${MODE}" != "ci" ]; then
pause
fi
fi # End of "if should_run_test 3" block
# ============================================
# TEST 4: Invalid Data Handling
# ============================================
if should_run_test "4" SELECTED_TESTS; then
section "TEST 4: Invalid Data Handling"
# Set test context
set_test_context "phase1" "phase1_test4" ""
info "Purpose: Verify invalid data doesn't crash recovery"
info ""
info "Expected time: 5-8 minutes"
info "Automatable: Partial (requires debuggable app or manual injection)"
info "Note: This test injects invalid data (empty IDs, null nextRunAt) and"
info " verifies that recovery handles it gracefully without crashing"
echo ""
if [ "${MODE}" != "ci" ]; then
pause
fi
# Capture initial state
set_test_context "phase1" "phase1_test4" "p1_t4_s1"
step_start "p1_t4_s1" "Inject invalid test data"
capture_alarms "test4_initial"
capture_logcat "test4_initial" "DNP" 50
substep "Step 1: Inject invalid test data"
# Clear logs before test
clear_logs
# Check if app is debuggable (look for DEBUGGABLE flag)
local is_debuggable=false
if $ADB_BIN shell dumpsys package "${APP_ID}" | grep -qi "DEBUGGABLE"; then
is_debuggable=true
fi
if [ "${is_debuggable}" = "true" ]; then
ok "App is debuggable - can inject data via database"
substep "Step 2: Inject invalid data via direct database access"
# Launch app first to ensure database is initialized
info "Launching app to initialize database..."
$ADB_BIN shell am start -n "${APP_ID}/.MainActivity" > /dev/null 2>&1
sleep 2
# Stop app before database injection (prevents locking issues)
info "Stopping app before database injection..."
$ADB_BIN shell am force-stop "${APP_ID}"
sleep 1
# Inject invalid data via direct database access
info "Injecting invalid test data into database..."
# Calculate next run time (24 hours from now in milliseconds)
local next_run past_time now_time
next_run=$(($(date +%s) * 1000 + 86400000))
past_time=$(($(date +%s) * 1000 - 3600000))
now_time=$(($(date +%s) * 1000))
# Inject schedule with empty ID
info " - Injecting schedule with empty ID..."
$ADB_BIN shell "run-as ${APP_ID} sqlite3 databases/daily_notification_plugin.db \"INSERT OR REPLACE INTO schedules (id, kind, cron, clockTime, enabled, nextRunAt, jitterMs, backoffPolicy) VALUES ('', 'notify', '0 9 * * *', '09:00', 1, ${next_run}, 0, 'exp');\"" 2>&1 || true
# Inject schedule with null nextRunAt
info " - Injecting schedule with null nextRunAt..."
$ADB_BIN shell "run-as ${APP_ID} sqlite3 databases/daily_notification_plugin.db \"INSERT OR REPLACE INTO schedules (id, kind, cron, clockTime, enabled, nextRunAt, jitterMs, backoffPolicy) VALUES ('test_null_nextrunat', 'notify', '0 9 * * *', '09:00', 1, NULL, 0, 'exp');\"" 2>&1 || true
# Checkpoint WAL file to ensure changes are visible
info " - Checkpointing WAL file..."
$ADB_BIN shell "run-as ${APP_ID} sqlite3 databases/daily_notification_plugin.db \"PRAGMA wal_checkpoint(FULL);\"" 2>&1 || true
# Verify data was inserted
info "Verifying data injection..."
local schedule_count
schedule_count="$($ADB_BIN shell "run-as ${APP_ID} sqlite3 databases/daily_notification_plugin.db \"SELECT COUNT(*) FROM schedules;\"" 2>&1 | tr -d '\r\n')"
info " - Schedules in database: ${schedule_count}"
if [ "${schedule_count}" -gt "0" ] 2>/dev/null; then
ok "Invalid test data injected successfully"
step_pass "p1_t4_s1" "Invalid data injected"
else
warn "No schedules found after injection - data may not have been inserted"
step_warn "p1_t4_s1" "Data injection may have failed"
fi
substep "Step 3: Trigger recovery with invalid data"
set_test_context "phase1" "phase1_test4" "p1_t4_s2"
step_start "p1_t4_s2" "Trigger recovery with invalid data"
$ADB_BIN shell am start -n "${APP_ID}/.MainActivity" > /dev/null 2>&1
sleep 3 # Give recovery time to run
info "Waiting for recovery to complete..."
sleep 2
# Capture after recovery state
capture_alarms "test4_after_recovery"
capture_logcat "test4_after_recovery" "DNP-REACTIVATION|Skipping invalid" 100
capture_screenshot "test4_after_recovery"
step_pass "p1_t4_s2" "Recovery triggered"
substep "Step 4: Check recovery logs for invalid data handling"
set_test_context "phase1" "phase1_test4" "p1_t4_s3"
step_start "p1_t4_s3" "Check recovery logs"
# Check logs
local recovery_logs
recovery_logs="$($ADB_BIN logcat -d | grep -E "DNP-REACTIVATION|Skipping invalid|TEST:" | tail -30)"
echo ""
info "Recovery logs:"
echo "${recovery_logs}"
echo ""
# Check for invalid data handling
local test4_passed=false
local test4_failed=false
if echo "${recovery_logs}" | grep -q "Skipping invalid"; then
ok "Invalid data was detected and skipped"
echo "${recovery_logs}" | grep "Skipping invalid"
test4_passed=true
else
warn "No 'Skipping invalid' logs found"
info "This could mean:"
echo " - Invalid data wasn't injected (database constraints prevented it)"
echo " - Recovery didn't encounter invalid data"
echo " - Logs were cleared"
fi
if echo "${recovery_logs}" | grep -q "recovery complete\|Recovery completed"; then
ok "Recovery completed successfully"
if [ "${test4_passed}" = "false" ]; then
test4_passed=true # Recovery completed = passed (even if no invalid data found)
fi
else
warn "Recovery completion message not found in logs"
fi
if echo "${recovery_logs}" | grep -qiE "crash|fatal|exception.*recovery|Failed.*recovery"; then
error "TEST 4 FAILED: Recovery crashed or threw fatal exception"
test4_failed=true
step_fail "p1_t4_s3" "Recovery crashed"
else
step_pass "p1_t4_s3" "Recovery handled invalid data gracefully"
fi
# Final verdict
set_test_context "phase1" "phase1_test4" "p1_t4_s4"
if [ "${test4_failed}" = "true" ]; then
verdict_fail "test4_invalid_data_handling" "Recovery crashed or threw fatal exception when handling invalid data"
elif [ "${test4_passed}" = "true" ]; then
verdict_pass "test4_invalid_data_handling" "Recovery handled invalid data gracefully (or no invalid data found)"
else
verdict_warn "test4_invalid_data_handling" "Could not verify invalid data handling"
fi
else
info "App is not debuggable - cannot inject data via database"
info "TEST 4: Code review confirms invalid data handling exists"
info " - ReactivationManager.kt checks for empty IDs"
info " - Errors are logged but don't crash recovery"
verdict_warn "test4_invalid_data_handling" "App not debuggable - cannot inject invalid data for testing"
fi
evidence_block "test4_invalid_data_handling"
if [ "${MODE}" != "ci" ]; then
pause
fi
fi # End of "if should_run_test 4" block
# ============================================
# Summary
# ============================================
section "Testing Complete"
info "Test Results Summary:"
echo ""
echo "All test verdicts are shown above with evidence locations."
echo "Review evidence in: $(get_run_dir)"
echo ""
info "All recovery logs:"
echo ""
$ADB_BIN logcat -d | grep "$REACTIVATION_TAG" | tail -20
echo ""
ok "Phase 1 testing script complete!"
echo ""
echo "Next steps:"
echo " - Review evidence in: $(get_run_dir)"
echo " - Verify all test verdicts above"
echo " - Check database if needed (debuggable app)"
echo " - Update documentation with test results"
}
# Run main function
main "$@"