docs(ios): add comprehensive iOS implementation documentation
Adds complete iOS documentation suite to support iOS implementation parity with Android features. Includes implementation directives, recovery scenario mappings, database migration guide, troubleshooting guide, and test scripts. New Documentation: - iOS Implementation Directive: Phase-based implementation guide mirroring Android structure with iOS-specific considerations - iOS Recovery Scenario Mapping: Maps Android recovery scenarios to iOS equivalents with detection logic comparisons - iOS Core Data Migration Guide: Complete Room → Core Data entity mappings with implementation checklist for missing entities - iOS Troubleshooting Guide: Common issues, debugging techniques, and error code reference Enhanced Documentation: - API.md: Added iOS-only methods (permissions, background tasks, pending notifications), platform differences table, and iOS-specific error types. Updated version to 2.3.0. Test Infrastructure: - iOS test scripts for Phase 1 (cold start), Phase 2 (termination), and Phase 3 (boot recovery) testing scenarios All documentation addresses gaps identified in iOS Implementation Documentation Review and provides foundation for iOS recovery feature implementation (currently pending). Note: iOS recovery features (ReactivationManager, scenario detection) are NOT yet implemented. Documentation is ready to guide implementation.
This commit is contained in:
436
test-apps/ios-test-app/test-phase1.sh
Executable file
436
test-apps/ios-test-app/test-phase1.sh
Executable file
@@ -0,0 +1,436 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Phase 1 Testing Script - iOS Interactive Test Runner
|
||||
# Guides through all Phase 1 tests with clear prompts for UI interaction
|
||||
# Adapted from Android test-phase1.sh for iOS testing
|
||||
|
||||
set -e # Exit on error
|
||||
|
||||
# Source shared library (if exists)
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
if [ -f "${SCRIPT_DIR}/ios-test-lib.sh" ]; then
|
||||
source "${SCRIPT_DIR}/ios-test-lib.sh"
|
||||
fi
|
||||
|
||||
# Phase 1 specific configuration
|
||||
APP_BUNDLE_ID="com.timesafari.ios-test-app"
|
||||
SIMULATOR_DEVICE="iPhone 15"
|
||||
LOG_PREFIX="DNP"
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Helper functions
|
||||
print_header() {
|
||||
echo ""
|
||||
echo -e "${BLUE}========================================${NC}"
|
||||
echo -e "${BLUE}$1${NC}"
|
||||
echo -e "${BLUE}========================================${NC}"
|
||||
echo ""
|
||||
}
|
||||
|
||||
print_step() {
|
||||
echo -e "${GREEN}[STEP $1]${NC} $2"
|
||||
echo ""
|
||||
}
|
||||
|
||||
print_info() {
|
||||
echo -e "${BLUE}ℹ️ $1${NC}"
|
||||
}
|
||||
|
||||
print_success() {
|
||||
echo -e "${GREEN}✅ $1${NC}"
|
||||
}
|
||||
|
||||
print_warn() {
|
||||
echo -e "${YELLOW}⚠️ $1${NC}"
|
||||
}
|
||||
|
||||
print_error() {
|
||||
echo -e "${RED}❌ $1${NC}"
|
||||
}
|
||||
|
||||
wait_for_user() {
|
||||
echo ""
|
||||
read -p "Press Enter to continue..."
|
||||
echo ""
|
||||
}
|
||||
|
||||
wait_for_ui_action() {
|
||||
echo ""
|
||||
echo -e "${YELLOW}$1${NC}"
|
||||
echo ""
|
||||
read -p "Press Enter when done..."
|
||||
echo ""
|
||||
}
|
||||
|
||||
# iOS-specific helper functions
|
||||
get_simulator_id() {
|
||||
xcrun simctl list devices available | grep "${SIMULATOR_DEVICE}" | head -1 | sed -E 's/.*\(([^)]+)\).*/\1/'
|
||||
}
|
||||
|
||||
get_app_logs() {
|
||||
local device_id=$1
|
||||
local lines=${2:-50}
|
||||
xcrun simctl spawn "${device_id}" log stream --level=debug --predicate 'processImagePath contains "ios-test-app"' --style=compact 2>/dev/null | head -n "${lines}" || echo ""
|
||||
}
|
||||
|
||||
check_plugin_configured() {
|
||||
print_info "Checking if plugin is configured..."
|
||||
|
||||
local device_id=$(get_simulator_id)
|
||||
if [ -z "${device_id}" ]; then
|
||||
print_error "Simulator not found: ${SIMULATOR_DEVICE}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Check if app has been launched (indicates configuration may exist)
|
||||
local app_data=$(xcrun simctl get_app_container "${device_id}" "${APP_BUNDLE_ID}" 2>/dev/null || echo "")
|
||||
|
||||
if [ -n "${app_data}" ]; then
|
||||
print_success "App data exists (plugin may be configured)"
|
||||
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."
|
||||
return 0
|
||||
else
|
||||
print_info "Plugin not configured (no app data found)"
|
||||
print_info "You will need to click 'Configure Plugin' in the app UI"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
check_permissions() {
|
||||
print_info "Checking notification permissions..."
|
||||
|
||||
# Note: iOS permissions are checked at runtime, not via command line
|
||||
# We can only check if the app has been granted permission by checking logs
|
||||
print_info "iOS notification permissions are checked at runtime."
|
||||
print_info "Please verify in the app UI that notifications are authorized."
|
||||
print_info "If not authorized, you'll need to grant permission in the app."
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
ensure_permissions() {
|
||||
if check_permissions; then
|
||||
print_success "Permissions check passed"
|
||||
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."
|
||||
|
||||
return 0
|
||||
fi
|
||||
}
|
||||
|
||||
launch_app() {
|
||||
print_info "Launching app on simulator..."
|
||||
|
||||
local device_id=$(get_simulator_id)
|
||||
if [ -z "${device_id}" ]; then
|
||||
print_error "Simulator not found: ${SIMULATOR_DEVICE}"
|
||||
print_info "Available simulators:"
|
||||
xcrun simctl list devices available | grep "iPhone" | head -5
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Boot simulator if not running
|
||||
local booted=$(xcrun simctl list devices | grep "${device_id}" | grep -c "Booted" || echo "0")
|
||||
if [ "${booted}" -eq "0" ]; then
|
||||
print_info "Booting simulator..."
|
||||
xcrun simctl boot "${device_id}" 2>/dev/null || true
|
||||
sleep 3
|
||||
fi
|
||||
|
||||
# Launch app
|
||||
xcrun simctl launch "${device_id}" "${APP_BUNDLE_ID}" 2>/dev/null || {
|
||||
print_warn "App may already be running or needs to be built first"
|
||||
print_info "Please build and run the app in Xcode first:"
|
||||
echo " 1. Open: test-apps/ios-test-app/ios/App/App.xcworkspace"
|
||||
echo " 2. Select simulator: ${SIMULATOR_DEVICE}"
|
||||
echo " 3. Press Cmd+R to build and run"
|
||||
echo ""
|
||||
wait_for_user
|
||||
}
|
||||
|
||||
sleep 2 # Give app time to launch
|
||||
print_success "App launched"
|
||||
}
|
||||
|
||||
get_pending_notifications() {
|
||||
local device_id=$(get_simulator_id)
|
||||
if [ -z "${device_id}" ]; then
|
||||
echo "0"
|
||||
return
|
||||
fi
|
||||
|
||||
# Note: iOS doesn't provide direct command-line access to pending notifications
|
||||
# This would need to be implemented via a plugin method
|
||||
# For now, we'll check logs for notification scheduling
|
||||
local count=$(get_app_logs "${device_id}" 100 | grep -c "scheduled notification" || echo "0")
|
||||
echo "${count}"
|
||||
}
|
||||
|
||||
should_run_test() {
|
||||
local test_id=$1
|
||||
shift
|
||||
local selected_tests=("$@")
|
||||
|
||||
if [ ${#selected_tests[@]} -eq 0 ]; then
|
||||
return 0 # Run all tests if none specified
|
||||
fi
|
||||
|
||||
for selected in "${selected_tests[@]}"; do
|
||||
if [ "${selected}" = "${test_id}" ]; then
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
# ============================================
|
||||
# TEST 0: Daily Rollover (Core Contract Verification)
|
||||
# ============================================
|
||||
if should_run_test "0" "$@"; then
|
||||
print_header "TEST 0: Daily Rollover Verification"
|
||||
echo "Purpose: Verify that after a notification fires, the next day's"
|
||||
echo " schedule is correctly computed and only ONE notification exists."
|
||||
echo ""
|
||||
echo "Note: This test verifies the core 'one notification per day' contract."
|
||||
echo " It requires either:"
|
||||
echo " 1. Scheduling a notification for 'now + N seconds' and waiting, OR"
|
||||
echo " 2. Manipulating the simulator clock to cross the fire boundary."
|
||||
echo ""
|
||||
wait_for_user
|
||||
|
||||
print_step "1" "Schedule a test notification for near-future..."
|
||||
launch_app
|
||||
check_plugin_configured || {
|
||||
wait_for_ui_action "In the app UI, click 'Configure Plugin' to set up the plugin.
|
||||
|
||||
This will initialize the database and storage.
|
||||
|
||||
Once configured, press Enter to continue."
|
||||
}
|
||||
|
||||
INITIAL_COUNT=$(get_pending_notifications)
|
||||
print_info "Current pending notifications: ${INITIAL_COUNT}"
|
||||
|
||||
wait_for_ui_action "In the app UI, schedule a daily notification.
|
||||
|
||||
For this test, you may want to schedule it for a time very soon
|
||||
(e.g., 1-2 minutes from now) to observe the rollover behavior.
|
||||
|
||||
This will schedule:
|
||||
- 1 notification (UNUserNotificationCenter) for the specified time
|
||||
- 1 prefetch task (BGTaskScheduler) for 2 minutes before that time"
|
||||
|
||||
sleep 3 # Give notification time to be registered
|
||||
|
||||
POST_SCHEDULE_COUNT=$(get_pending_notifications)
|
||||
print_info "Pending notifications after scheduling: ${POST_SCHEDULE_COUNT}"
|
||||
|
||||
print_step "2" "Manual verification steps..."
|
||||
echo ""
|
||||
echo "To complete this test, you need to:"
|
||||
echo " 1. Wait for the notification to fire (or advance simulator clock)"
|
||||
echo " 2. Check that the plugin:"
|
||||
echo " - Computed the next day's time (24 hours later)"
|
||||
echo " - Scheduled exactly ONE notification for tomorrow"
|
||||
echo " - Did NOT create duplicate notifications"
|
||||
echo " 3. Verify in logs (Xcode Console or Console.app):"
|
||||
echo " - Next run time calculation shows tomorrow's time"
|
||||
echo " - Only one notification scheduled"
|
||||
echo ""
|
||||
echo "Expected log patterns:"
|
||||
echo " DNP-SCHEDULE: Scheduling next daily notification: ... source=ROLLOVER_ON_FIRE"
|
||||
echo " DNP-NOTIFY: Scheduling notification: triggerTime=<tomorrow's time>"
|
||||
echo ""
|
||||
|
||||
wait_for_ui_action "After the notification fires (or you advance the clock),
|
||||
check the logs and verify:
|
||||
|
||||
1. Only ONE notification exists (one per day)
|
||||
2. The notification time is for tomorrow (24 hours later)
|
||||
3. No duplicate notifications were created
|
||||
|
||||
Press Enter when verification is complete."
|
||||
|
||||
print_success "TEST 0: Daily Rollover Verification - Manual verification required"
|
||||
fi
|
||||
|
||||
# ============================================
|
||||
# TEST 1: Cold Start Recovery
|
||||
# ============================================
|
||||
if should_run_test "1" "$@"; then
|
||||
print_header "TEST 1: Cold Start Recovery"
|
||||
echo "Purpose: Verify that when the app launches after termination,"
|
||||
echo " missed notifications are detected and future notifications"
|
||||
echo " are verified/rescheduled."
|
||||
echo ""
|
||||
wait_for_user
|
||||
|
||||
print_step "1" "Schedule a notification for future time..."
|
||||
launch_app
|
||||
ensure_permissions
|
||||
|
||||
wait_for_ui_action "In the app UI, schedule a daily notification for a future time
|
||||
(e.g., 1 hour from now).
|
||||
|
||||
This creates a notification that should persist across app termination."
|
||||
|
||||
sleep 2
|
||||
|
||||
print_step "2" "Terminate app (simulate cold start)..."
|
||||
print_info "Terminating app to simulate cold start scenario..."
|
||||
|
||||
local device_id=$(get_simulator_id)
|
||||
xcrun simctl terminate "${device_id}" "${APP_BUNDLE_ID}" 2>/dev/null || true
|
||||
|
||||
print_info "App terminated. Waiting 5 seconds..."
|
||||
sleep 5
|
||||
|
||||
print_step "3" "Launch app and verify recovery..."
|
||||
launch_app
|
||||
|
||||
print_info "Checking logs for recovery activity..."
|
||||
sleep 3
|
||||
|
||||
local device_id=$(get_simulator_id)
|
||||
local logs=$(get_app_logs "${device_id}" 100)
|
||||
|
||||
if echo "${logs}" | grep -q "DNP-REACTIVATION\|recovery\|missed"; then
|
||||
print_success "Recovery activity detected in logs"
|
||||
else
|
||||
print_warn "No recovery activity detected in logs"
|
||||
print_info "This may indicate recovery is not yet implemented (expected for Phase 1)"
|
||||
fi
|
||||
|
||||
wait_for_ui_action "Verify in the app UI that:
|
||||
1. The notification is still scheduled (check 'Scheduled Notifications' screen)
|
||||
2. Any missed notifications are marked as missed
|
||||
3. Future notifications are verified/rescheduled
|
||||
|
||||
Press Enter when verification is complete."
|
||||
|
||||
print_success "TEST 1: Cold Start Recovery - Manual verification required"
|
||||
fi
|
||||
|
||||
# ============================================
|
||||
# TEST 2: Notification Persistence (Swipe from App Switcher)
|
||||
# ============================================
|
||||
if should_run_test "2" "$@"; then
|
||||
print_header "TEST 2: Notification Persistence (App Termination)"
|
||||
echo "Purpose: Verify that notifications persist when app is terminated"
|
||||
echo " (iOS OS-guaranteed behavior)."
|
||||
echo ""
|
||||
wait_for_user
|
||||
|
||||
print_step "1" "Schedule a notification..."
|
||||
launch_app
|
||||
ensure_permissions
|
||||
|
||||
wait_for_ui_action "In the app UI, schedule a daily notification for a future time."
|
||||
|
||||
sleep 2
|
||||
|
||||
print_step "2" "Terminate app..."
|
||||
print_info "Terminating app (simulating swipe from app switcher)..."
|
||||
|
||||
local device_id=$(get_simulator_id)
|
||||
xcrun simctl terminate "${device_id}" "${APP_BUNDLE_ID}" 2>/dev/null || true
|
||||
|
||||
print_info "App terminated. Waiting 3 seconds..."
|
||||
sleep 3
|
||||
|
||||
print_step "3" "Verify notification still exists..."
|
||||
print_info "On iOS, notifications persist automatically (OS-guaranteed)."
|
||||
print_info "The notification should still fire even though the app is terminated."
|
||||
|
||||
wait_for_ui_action "Verify that:
|
||||
1. The notification fires at the scheduled time (even though app is terminated)
|
||||
2. When you tap the notification, the app launches
|
||||
3. The notification is marked as delivered
|
||||
|
||||
Press Enter when verification is complete."
|
||||
|
||||
print_success "TEST 2: Notification Persistence - iOS OS-guaranteed behavior verified"
|
||||
fi
|
||||
|
||||
# ============================================
|
||||
# TEST 3: Invalid Data Handling
|
||||
# ============================================
|
||||
if should_run_test "3" "$@"; then
|
||||
print_header "TEST 3: Invalid Data Handling"
|
||||
echo "Purpose: Verify that the plugin handles invalid data gracefully"
|
||||
echo " without crashing."
|
||||
echo ""
|
||||
wait_for_user
|
||||
|
||||
print_step "1" "Test invalid notification time..."
|
||||
launch_app
|
||||
ensure_permissions
|
||||
|
||||
wait_for_ui_action "In the app UI, try to schedule a notification with invalid data:
|
||||
1. Empty time string
|
||||
2. Invalid time format (e.g., '25:00' or '12:99')
|
||||
3. Negative time values
|
||||
|
||||
The app should show an error message and NOT crash."
|
||||
|
||||
print_info "Checking logs for error handling..."
|
||||
sleep 2
|
||||
|
||||
local device_id=$(get_simulator_id)
|
||||
local logs=$(get_app_logs "${device_id}" 50)
|
||||
|
||||
if echo "${logs}" | grep -q "error\|invalid\|Error"; then
|
||||
print_success "Error handling detected in logs"
|
||||
else
|
||||
print_info "No errors in recent logs (may indicate graceful handling)"
|
||||
fi
|
||||
|
||||
wait_for_ui_action "Verify that:
|
||||
1. Invalid data is rejected with clear error messages
|
||||
2. The app does NOT crash
|
||||
3. Valid notifications can still be scheduled after errors
|
||||
|
||||
Press Enter when verification is complete."
|
||||
|
||||
print_success "TEST 3: Invalid Data Handling - Manual verification required"
|
||||
fi
|
||||
|
||||
# ============================================
|
||||
# Summary
|
||||
# ============================================
|
||||
print_header "Phase 1 Testing Complete"
|
||||
echo "All Phase 1 tests have been executed."
|
||||
echo ""
|
||||
echo "Note: iOS recovery features (ReactivationManager) are NOT yet implemented."
|
||||
echo " Tests 1 and 2 will show expected behavior once recovery is implemented."
|
||||
echo ""
|
||||
echo "Next Steps:"
|
||||
echo " 1. Review test results"
|
||||
echo " 2. Check logs for any errors"
|
||||
echo " 3. Implement recovery features (Phase 1 directive)"
|
||||
echo " 4. Re-run tests after implementation"
|
||||
echo ""
|
||||
|
||||
58
test-apps/ios-test-app/test-phase2.sh
Executable file
58
test-apps/ios-test-app/test-phase2.sh
Executable file
@@ -0,0 +1,58 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Phase 2 Testing Script - iOS App Termination Recovery
|
||||
# Tests app termination detection and recovery (iOS equivalent of Android force stop)
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
if [ -f "${SCRIPT_DIR}/test-phase1.sh" ]; then
|
||||
source "${SCRIPT_DIR}/test-phase1.sh" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
print_header "Phase 2: App Termination Recovery Testing"
|
||||
echo "Note: iOS doesn't have user-facing 'force stop' like Android."
|
||||
echo " This tests system termination scenarios and recovery."
|
||||
echo ""
|
||||
|
||||
# Note: Phase 2 features are NOT yet implemented
|
||||
print_warn "⚠️ Phase 2 recovery features (termination detection) are NOT yet implemented."
|
||||
print_info "These tests will verify expected behavior once implementation is complete."
|
||||
echo ""
|
||||
|
||||
wait_for_user
|
||||
|
||||
print_header "TEST 1: App Termination Detection"
|
||||
echo "Purpose: Verify that when app is terminated by system,"
|
||||
echo " recovery detects termination and reschedules notifications."
|
||||
echo ""
|
||||
|
||||
launch_app
|
||||
check_plugin_configured
|
||||
|
||||
wait_for_ui_action "Schedule a notification for future time."
|
||||
|
||||
print_info "Terminating app to simulate system termination..."
|
||||
local device_id=$(get_simulator_id)
|
||||
xcrun simctl terminate "${device_id}" "${APP_BUNDLE_ID}" 2>/dev/null || true
|
||||
|
||||
sleep 3
|
||||
|
||||
print_info "Launching app to trigger recovery..."
|
||||
launch_app
|
||||
sleep 5
|
||||
|
||||
print_info "Checking logs for termination detection..."
|
||||
local device_id=$(get_simulator_id)
|
||||
local logs=$(get_app_logs "${device_id}" 100)
|
||||
|
||||
if echo "${logs}" | grep -q "termination\|DNP-REACTIVATION"; then
|
||||
print_success "Recovery activity detected"
|
||||
else
|
||||
print_warn "No recovery activity detected (expected - not yet implemented)"
|
||||
fi
|
||||
|
||||
wait_for_ui_action "Verify notifications are rescheduled after termination."
|
||||
|
||||
print_success "Phase 2 testing complete (implementation pending)"
|
||||
|
||||
52
test-apps/ios-test-app/test-phase3.sh
Executable file
52
test-apps/ios-test-app/test-phase3.sh
Executable file
@@ -0,0 +1,52 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Phase 3 Testing Script - iOS Boot Recovery
|
||||
# Tests boot recovery and background task registration
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
if [ -f "${SCRIPT_DIR}/test-phase1.sh" ]; then
|
||||
source "${SCRIPT_DIR}/test-phase1.sh" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
print_header "Phase 3: Boot Recovery Testing"
|
||||
echo "Note: iOS automatically persists notifications across reboot (OS-guaranteed)."
|
||||
echo " This tests BGTaskScheduler registration and boot recovery logic."
|
||||
echo ""
|
||||
|
||||
# Note: Phase 3 features are NOT yet implemented
|
||||
print_warn "⚠️ Phase 3 recovery features (BGTaskScheduler boot recovery) are NOT yet implemented."
|
||||
print_info "These tests will verify expected behavior once implementation is complete."
|
||||
echo ""
|
||||
|
||||
wait_for_user
|
||||
|
||||
print_header "TEST 1: Boot Recovery with Future Notifications"
|
||||
echo "Purpose: Verify notifications persist across reboot and recovery logic runs."
|
||||
echo ""
|
||||
|
||||
launch_app
|
||||
check_plugin_configured
|
||||
|
||||
wait_for_ui_action "Schedule a notification for future time."
|
||||
|
||||
print_info "On iOS, notifications persist automatically across reboot."
|
||||
print_info "We'll verify BGTaskScheduler registration and recovery logic."
|
||||
|
||||
print_warn "⚠️ Simulator reboot testing requires manual steps:"
|
||||
echo " 1. Schedule notification"
|
||||
echo " 2. Reboot simulator (Device → Restart in Simulator menu)"
|
||||
echo " 3. Launch app after reboot"
|
||||
echo " 4. Verify notifications still exist"
|
||||
echo " 5. Check logs for recovery activity"
|
||||
echo ""
|
||||
|
||||
wait_for_ui_action "After rebooting simulator and launching app,
|
||||
verify that:
|
||||
1. Notifications still exist (iOS OS-guaranteed)
|
||||
2. Recovery logic runs (once implemented)
|
||||
3. Any missed notifications are detected"
|
||||
|
||||
print_success "Phase 3 testing complete (implementation pending)"
|
||||
|
||||
Reference in New Issue
Block a user