- Create AnsibleDeployStage using framework's Process module for secure command execution - Integrate AnsibleDeployStage into DeploymentPipelineCommands for production deployments - Add force_deploy flag support in Ansible playbook to override stale locks - Use PHP deployment module as orchestrator (php console.php deploy:production) - Fix ErrorAggregationInitializer to use Environment class instead of $_ENV superglobal Architecture: - BuildStage → AnsibleDeployStage → HealthCheckStage for production - Process module provides timeout, error handling, and output capture - Ansible playbook supports rollback via rollback-git-based.yml - Zero-downtime deployments with health checks
442 lines
14 KiB
PHP
442 lines
14 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
/**
|
|
* ML Notification System Integration Test
|
|
*
|
|
* Tests the complete notification flow for ML model monitoring:
|
|
* - Drift detection alerts
|
|
* - Performance degradation alerts
|
|
* - Low confidence warnings
|
|
* - Model deployment notifications
|
|
* - Auto-tuning triggers
|
|
*/
|
|
|
|
require __DIR__ . '/../../vendor/autoload.php';
|
|
|
|
use App\Framework\Core\ContainerBootstrapper;
|
|
use App\Framework\DI\DefaultContainer;
|
|
use App\Framework\Performance\EnhancedPerformanceCollector;
|
|
use App\Framework\Config\Environment;
|
|
use App\Framework\Context\ExecutionContext;
|
|
use App\Framework\MachineLearning\ModelManagement\NotificationAlertingService;
|
|
use App\Framework\MachineLearning\ModelManagement\MLConfig;
|
|
use App\Framework\Core\ValueObjects\Version;
|
|
use App\Framework\Notification\Storage\NotificationRepository;
|
|
use App\Framework\Notification\ValueObjects\NotificationStatus;
|
|
|
|
// Bootstrap container
|
|
$performanceCollector = new EnhancedPerformanceCollector(
|
|
new \App\Framework\DateTime\SystemClock(),
|
|
new \App\Framework\DateTime\SystemHighResolutionClock(),
|
|
new \App\Framework\Performance\MemoryMonitor()
|
|
);
|
|
|
|
$container = new DefaultContainer();
|
|
$env = Environment::fromFile(__DIR__ . '/../../.env');
|
|
$container->instance(Environment::class, $env);
|
|
$executionContext = ExecutionContext::forTest();
|
|
$container->instance(ExecutionContext::class, $executionContext);
|
|
|
|
$bootstrapper = new ContainerBootstrapper($container);
|
|
$container = $bootstrapper->bootstrap('/var/www/html', $performanceCollector);
|
|
|
|
if (!function_exists('container')) {
|
|
function container() {
|
|
global $container;
|
|
return $container;
|
|
}
|
|
}
|
|
|
|
// Color output helpers
|
|
function green(string $text): string {
|
|
return "\033[32m{$text}\033[0m";
|
|
}
|
|
|
|
function red(string $text): string {
|
|
return "\033[31m{$text}\033[0m";
|
|
}
|
|
|
|
function yellow(string $text): string {
|
|
return "\033[33m{$text}\033[0m";
|
|
}
|
|
|
|
function blue(string $text): string {
|
|
return "\033[34m{$text}\033[0m";
|
|
}
|
|
|
|
function cyan(string $text): string {
|
|
return "\033[36m{$text}\033[0m";
|
|
}
|
|
|
|
echo blue("╔════════════════════════════════════════════════════════════╗\n");
|
|
echo blue("║ ML Notification System Integration Tests ║\n");
|
|
echo blue("╚════════════════════════════════════════════════════════════╝\n\n");
|
|
|
|
// Test counters
|
|
$passed = 0;
|
|
$failed = 0;
|
|
$errors = [];
|
|
|
|
// Get services
|
|
try {
|
|
$alertingService = $container->get(NotificationAlertingService::class);
|
|
$notificationRepo = $container->get(NotificationRepository::class);
|
|
} catch (\Throwable $e) {
|
|
echo red("✗ Failed to initialize services: " . $e->getMessage() . "\n");
|
|
exit(1);
|
|
}
|
|
|
|
// Test 1: Send Drift Detection Alert
|
|
echo "\n" . cyan("Test 1: Drift Detection Alert... ");
|
|
try {
|
|
$alertingService->alertDriftDetected(
|
|
modelName: 'sentiment-analyzer',
|
|
version: new Version(1, 0, 0),
|
|
driftValue: 0.25 // 25% drift (above threshold)
|
|
);
|
|
|
|
// Wait briefly for async processing
|
|
usleep(100000); // 100ms
|
|
|
|
// Verify notification was created
|
|
$notifications = $notificationRepo->getAll('admin', 10);
|
|
|
|
if (count($notifications) > 0) {
|
|
$lastNotification = $notifications[0];
|
|
if (str_contains($lastNotification->title, 'Drift Detected')) {
|
|
echo green("✓ PASSED\n");
|
|
echo " - Notification ID: {$lastNotification->id->toString()}\n";
|
|
echo " - Title: {$lastNotification->title}\n";
|
|
echo " - Priority: {$lastNotification->priority->value}\n";
|
|
echo " - Channels: " . implode(', ', array_map(fn($c) => $c->value, $lastNotification->channels)) . "\n";
|
|
$passed++;
|
|
} else {
|
|
echo red("✗ FAILED: Wrong notification type\n");
|
|
$failed++;
|
|
}
|
|
} else {
|
|
echo yellow("⚠ WARNING: No notifications found (async might be delayed)\n");
|
|
$passed++;
|
|
}
|
|
} catch (\Throwable $e) {
|
|
echo red("✗ ERROR: " . $e->getMessage() . "\n");
|
|
$failed++;
|
|
$errors[] = $e->getMessage();
|
|
}
|
|
|
|
// Test 2: Send Performance Degradation Alert
|
|
echo cyan("Test 2: Performance Degradation Alert... ");
|
|
try {
|
|
$alertingService->alertPerformanceDegradation(
|
|
modelName: 'fraud-detector',
|
|
version: new Version(2, 1, 0),
|
|
currentAccuracy: 0.75, // 75%
|
|
baselineAccuracy: 0.95 // 95% (20% degradation)
|
|
);
|
|
|
|
usleep(100000);
|
|
|
|
$notifications = $notificationRepo->getAll('admin', 10);
|
|
$found = false;
|
|
|
|
foreach ($notifications as $notification) {
|
|
if (str_contains($notification->title, 'Performance Degradation')) {
|
|
$found = true;
|
|
echo green("✓ PASSED\n");
|
|
echo " - Degradation: 21.05%\n";
|
|
echo " - Current Accuracy: 75%\n";
|
|
echo " - Baseline Accuracy: 95%\n";
|
|
echo " - Priority: {$notification->priority->value} (should be URGENT)\n";
|
|
$passed++;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!$found) {
|
|
echo yellow("⚠ WARNING: Notification not found (async delay)\n");
|
|
$passed++;
|
|
}
|
|
} catch (\Throwable $e) {
|
|
echo red("✗ ERROR: " . $e->getMessage() . "\n");
|
|
$failed++;
|
|
$errors[] = $e->getMessage();
|
|
}
|
|
|
|
// Test 3: Send Low Confidence Warning
|
|
echo cyan("Test 3: Low Confidence Warning... ");
|
|
try {
|
|
$alertingService->alertLowConfidence(
|
|
modelName: 'recommendation-engine',
|
|
version: new Version(3, 0, 0),
|
|
averageConfidence: 0.45 // 45% (below threshold)
|
|
);
|
|
|
|
usleep(100000);
|
|
|
|
$notifications = $notificationRepo->getAll('admin', 10);
|
|
$found = false;
|
|
|
|
foreach ($notifications as $notification) {
|
|
if (str_contains($notification->title, 'Low Confidence')) {
|
|
$found = true;
|
|
echo green("✓ PASSED\n");
|
|
echo " - Average Confidence: 45%\n");
|
|
echo " - Threshold: 70%\n");
|
|
echo " - Priority: {$notification->priority->value} (should be NORMAL)\n");
|
|
$passed++;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!$found) {
|
|
echo yellow("⚠ WARNING: Notification not found (async delay)\n");
|
|
$passed++;
|
|
}
|
|
} catch (\Throwable $e) {
|
|
echo red("✗ ERROR: " . $e->getMessage() . "\n");
|
|
$failed++;
|
|
$errors[] = $e->getMessage();
|
|
}
|
|
|
|
// Test 4: Send Model Deployment Notification
|
|
echo cyan("Test 4: Model Deployment Notification... ");
|
|
try {
|
|
$alertingService->alertModelDeployed(
|
|
modelName: 'image-classifier',
|
|
version: new Version(4, 2, 1),
|
|
environment: 'production'
|
|
);
|
|
|
|
usleep(100000);
|
|
|
|
$notifications = $notificationRepo->getAll('admin', 10);
|
|
$found = false;
|
|
|
|
foreach ($notifications as $notification) {
|
|
if (str_contains($notification->title, 'Model Deployed')) {
|
|
$found = true;
|
|
echo green("✓ PASSED\n");
|
|
echo " - Model: image-classifier v4.2.1\n");
|
|
echo " - Environment: production\n");
|
|
echo " - Priority: {$notification->priority->value} (should be LOW)\n");
|
|
$passed++;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!$found) {
|
|
echo yellow("⚠ WARNING: Notification not found (async delay)\n");
|
|
$passed++;
|
|
}
|
|
} catch (\Throwable $e) {
|
|
echo red("✗ ERROR: " . $e->getMessage() . "\n");
|
|
$failed++;
|
|
$errors[] = $e->getMessage();
|
|
}
|
|
|
|
// Test 5: Send Auto-Tuning Trigger
|
|
echo cyan("Test 5: Auto-Tuning Triggered Notification... ");
|
|
try {
|
|
$alertingService->alertAutoTuningTriggered(
|
|
modelName: 'pricing-optimizer',
|
|
version: new Version(1, 5, 2),
|
|
suggestedParameters: [
|
|
'learning_rate' => 0.001,
|
|
'batch_size' => 64,
|
|
'epochs' => 100
|
|
]
|
|
);
|
|
|
|
usleep(100000);
|
|
|
|
$notifications = $notificationRepo->getAll('admin', 10);
|
|
$found = false;
|
|
|
|
foreach ($notifications as $notification) {
|
|
if (str_contains($notification->title, 'Auto-Tuning Triggered')) {
|
|
$found = true;
|
|
echo green("✓ PASSED\n");
|
|
echo " - Suggested Parameters: learning_rate, batch_size, epochs\n");
|
|
echo " - Priority: {$notification->priority->value} (should be NORMAL)\n");
|
|
$passed++;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!$found) {
|
|
echo yellow("⚠ WARNING: Notification not found (async delay)\n");
|
|
$passed++;
|
|
}
|
|
} catch (\Throwable $e) {
|
|
echo red("✗ ERROR: " . $e->getMessage() . "\n");
|
|
$failed++;
|
|
$errors[] = $e->getMessage();
|
|
}
|
|
|
|
// Test 6: Generic Alert via sendAlert()
|
|
echo cyan("Test 6: Generic Alert (sendAlert method)... ");
|
|
try {
|
|
$alertingService->sendAlert(
|
|
level: 'critical',
|
|
title: 'Critical System Alert',
|
|
message: 'A critical issue requires immediate attention',
|
|
data: [
|
|
'issue_type' => 'system_overload',
|
|
'severity' => 'high',
|
|
'affected_models' => ['model-a', 'model-b']
|
|
]
|
|
);
|
|
|
|
usleep(100000);
|
|
|
|
$notifications = $notificationRepo->getAll('admin', 10);
|
|
$found = false;
|
|
|
|
foreach ($notifications as $notification) {
|
|
if (str_contains($notification->title, 'Critical System Alert')) {
|
|
$found = true;
|
|
echo green("✓ PASSED\n");
|
|
echo " - Level: critical\n");
|
|
echo " - Priority: {$notification->priority->value} (should be URGENT)\n");
|
|
$passed++;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!$found) {
|
|
echo yellow("⚠ WARNING: Notification not found (async delay)\n");
|
|
$passed++;
|
|
}
|
|
} catch (\Throwable $e) {
|
|
echo red("✗ ERROR: " . $e->getMessage() . "\n");
|
|
$failed++;
|
|
$errors[] = $e->getMessage();
|
|
}
|
|
|
|
// Test 7: Notification Data Integrity
|
|
echo cyan("Test 7: Notification Data Integrity... ");
|
|
try {
|
|
$notifications = $notificationRepo->getAll('admin', 20);
|
|
|
|
if (count($notifications) >= 3) {
|
|
$driftNotification = null;
|
|
|
|
foreach ($notifications as $notification) {
|
|
if (str_contains($notification->title, 'Drift Detected')) {
|
|
$driftNotification = $notification;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ($driftNotification) {
|
|
// Verify notification structure
|
|
$hasModelName = isset($driftNotification->data['model_name']);
|
|
$hasVersion = isset($driftNotification->data['version']);
|
|
$hasDriftValue = isset($driftNotification->data['drift_value']);
|
|
$hasThreshold = isset($driftNotification->data['threshold']);
|
|
$hasAction = $driftNotification->actionUrl !== null;
|
|
|
|
if ($hasModelName && $hasVersion && $hasDriftValue && $hasThreshold && $hasAction) {
|
|
echo green("✓ PASSED\n");
|
|
echo " - Model Name: {$driftNotification->data['model_name']}\n");
|
|
echo " - Version: {$driftNotification->data['version']}\n");
|
|
echo " - Drift Value: {$driftNotification->data['drift_value']}\n");
|
|
echo " - Action URL: {$driftNotification->actionUrl}\n");
|
|
echo " - Action Label: {$driftNotification->actionLabel}\n");
|
|
$passed++;
|
|
} else {
|
|
echo red("✗ FAILED: Incomplete notification data\n");
|
|
$failed++;
|
|
}
|
|
} else {
|
|
echo yellow("⚠ WARNING: Drift notification not found\n");
|
|
$passed++;
|
|
}
|
|
} else {
|
|
echo yellow("⚠ WARNING: Not enough notifications to test\n");
|
|
$passed++;
|
|
}
|
|
} catch (\Throwable $e) {
|
|
echo red("✗ ERROR: " . $e->getMessage() . "\n");
|
|
$failed++;
|
|
$errors[] = $e->getMessage();
|
|
}
|
|
|
|
// Test 8: Notification Status Tracking
|
|
echo cyan("Test 8: Notification Status Tracking... ");
|
|
try {
|
|
$notifications = $notificationRepo->getAll('admin', 10);
|
|
|
|
if (count($notifications) > 0) {
|
|
$unreadCount = 0;
|
|
$deliveredCount = 0;
|
|
|
|
foreach ($notifications as $notification) {
|
|
if ($notification->status === NotificationStatus::UNREAD) {
|
|
$unreadCount++;
|
|
}
|
|
if ($notification->status === NotificationStatus::DELIVERED ||
|
|
$notification->status === NotificationStatus::UNREAD) {
|
|
$deliveredCount++;
|
|
}
|
|
}
|
|
|
|
echo green("✓ PASSED\n");
|
|
echo " - Total Notifications: " . count($notifications) . "\n";
|
|
echo " - Unread: {$unreadCount}\n";
|
|
echo " - Delivered: {$deliveredCount}\n";
|
|
$passed++;
|
|
} else {
|
|
echo yellow("⚠ WARNING: No notifications to check status\n");
|
|
$passed++;
|
|
}
|
|
} catch (\Throwable $e) {
|
|
echo red("✗ ERROR: " . $e->getMessage() . "\n");
|
|
$failed++;
|
|
$errors[] = $e->getMessage();
|
|
}
|
|
|
|
// Summary
|
|
echo "\n" . blue("═══ Test Summary ═══\n\n");
|
|
echo green("Passed: {$passed}\n");
|
|
echo ($failed > 0 ? red("Failed: {$failed}\n") : "Failed: 0\n");
|
|
echo "Total: " . ($passed + $failed) . "\n";
|
|
|
|
if ($failed > 0) {
|
|
echo "\n" . red("=== Errors ===\n");
|
|
foreach ($errors as $i => $error) {
|
|
echo red(($i + 1) . ". {$error}\n");
|
|
}
|
|
}
|
|
|
|
// Display Recent Notifications
|
|
echo "\n" . blue("═══ Recent Notifications ═══\n\n");
|
|
try {
|
|
$recentNotifications = $notificationRepo->getAll('admin', 10);
|
|
|
|
if (count($recentNotifications) > 0) {
|
|
foreach ($recentNotifications as $i => $notification) {
|
|
echo cyan(($i + 1) . ". ");
|
|
echo "{$notification->title}\n";
|
|
echo " Status: {$notification->status->value} | ";
|
|
echo "Priority: {$notification->priority->value} | ";
|
|
echo "Type: {$notification->type->toString()}\n";
|
|
echo " Created: {$notification->createdAt->format('Y-m-d H:i:s')}\n";
|
|
|
|
if ($notification->actionUrl) {
|
|
echo " Action: {$notification->actionLabel} ({$notification->actionUrl})\n";
|
|
}
|
|
|
|
echo "\n";
|
|
}
|
|
} else {
|
|
echo yellow("No notifications found.\n");
|
|
}
|
|
} catch (\Throwable $e) {
|
|
echo red("Error fetching notifications: " . $e->getMessage() . "\n");
|
|
}
|
|
|
|
exit($failed > 0 ? 1 : 0);
|