feat(Deployment): Integrate Ansible deployment via PHP deployment pipeline
- 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
This commit is contained in:
@@ -0,0 +1,282 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Framework\Cache\Cache;
|
||||
use App\Framework\Core\ValueObjects\Duration;
|
||||
use App\Framework\Core\ValueObjects\Timestamp;
|
||||
use App\Framework\DateTime\Clock;
|
||||
use App\Framework\DateTime\SystemClock;
|
||||
use App\Framework\Discovery\DiscoveryRegistry;
|
||||
use App\Framework\Discovery\Results\AttributeRegistry;
|
||||
use App\Framework\Logging\Logger;
|
||||
use App\Framework\Scheduler\Services\SchedulerService;
|
||||
use App\Framework\Worker\Every;
|
||||
use App\Framework\Worker\Schedule;
|
||||
use App\Framework\Worker\ScheduleDiscoveryService;
|
||||
|
||||
// Test job classes with Schedule attribute
|
||||
#[Schedule(at: new Every(minutes: 5))]
|
||||
final class TestFiveMinuteJob
|
||||
{
|
||||
public static int $executionCount = 0;
|
||||
|
||||
public function handle(): array
|
||||
{
|
||||
self::$executionCount++;
|
||||
return ['status' => 'success', 'count' => self::$executionCount];
|
||||
}
|
||||
}
|
||||
|
||||
#[Schedule(at: new Every(hours: 1))]
|
||||
final class TestHourlyJob
|
||||
{
|
||||
public static int $executionCount = 0;
|
||||
|
||||
public function __invoke(): string
|
||||
{
|
||||
self::$executionCount++;
|
||||
return 'hourly job executed';
|
||||
}
|
||||
}
|
||||
|
||||
describe('ScheduleDiscoveryService Integration', function () {
|
||||
beforeEach(function () {
|
||||
// Reset execution counters
|
||||
TestFiveMinuteJob::$executionCount = 0;
|
||||
TestHourlyJob::$executionCount = 0;
|
||||
|
||||
// Create minimal logger mock
|
||||
$this->logger = Mockery::mock(Logger::class);
|
||||
$this->logger->shouldReceive('debug')->andReturn(null);
|
||||
$this->logger->shouldReceive('info')->andReturn(null);
|
||||
$this->logger->shouldReceive('warning')->andReturn(null);
|
||||
$this->logger->shouldReceive('error')->andReturn(null);
|
||||
|
||||
$this->schedulerService = new SchedulerService(
|
||||
$this->logger
|
||||
);
|
||||
|
||||
// Create minimal DiscoveryRegistry mock
|
||||
$this->discoveryRegistry = Mockery::mock(DiscoveryRegistry::class);
|
||||
|
||||
$this->scheduleDiscovery = new ScheduleDiscoveryService(
|
||||
$this->discoveryRegistry,
|
||||
$this->schedulerService
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
Mockery::close();
|
||||
});
|
||||
|
||||
it('discovers and registers scheduled jobs from attribute registry', function () {
|
||||
// Mock discovery to return our test jobs
|
||||
$this->discoveryRegistry
|
||||
->shouldReceive('getClassesWithAttribute')
|
||||
->with(Schedule::class)
|
||||
->once()
|
||||
->andReturn([
|
||||
TestFiveMinuteJob::class,
|
||||
TestHourlyJob::class
|
||||
]);
|
||||
|
||||
$registered = $this->scheduleDiscovery->discoverAndRegister();
|
||||
|
||||
expect($registered)->toBe(2);
|
||||
|
||||
// Verify tasks were registered with scheduler
|
||||
$scheduledTasks = $this->schedulerService->getScheduledTasks();
|
||||
expect($scheduledTasks)->toHaveCount(2);
|
||||
});
|
||||
|
||||
it('generates correct task IDs from class names', function () {
|
||||
$this->discoveryRegistry
|
||||
->shouldReceive('getClassesWithAttribute')
|
||||
->with(Schedule::class)
|
||||
->once()
|
||||
->andReturn([
|
||||
TestFiveMinuteJob::class,
|
||||
TestHourlyJob::class
|
||||
]);
|
||||
|
||||
$this->scheduleDiscovery->discoverAndRegister();
|
||||
|
||||
$scheduledTasks = $this->schedulerService->getScheduledTasks();
|
||||
|
||||
$taskIds = array_map(fn($task) => $task->taskId, $scheduledTasks);
|
||||
|
||||
expect($taskIds)->toContain('test-five-minute-job');
|
||||
expect($taskIds)->toContain('test-hourly-job');
|
||||
});
|
||||
|
||||
it('executes scheduled jobs correctly', function () {
|
||||
$this->discoveryRegistry
|
||||
->shouldReceive('getClassesWithAttribute')
|
||||
->with(Schedule::class)
|
||||
->once()
|
||||
->andReturn([TestFiveMinuteJob::class]);
|
||||
|
||||
$this->scheduleDiscovery->discoverAndRegister();
|
||||
|
||||
// Get the scheduled task
|
||||
$scheduledTasks = $this->schedulerService->getScheduledTasks();
|
||||
expect($scheduledTasks)->toHaveCount(1);
|
||||
|
||||
$task = $scheduledTasks[0];
|
||||
|
||||
// Execute the task
|
||||
$result = $this->schedulerService->executeTask($task);
|
||||
|
||||
expect($result->success)->toBeTrue();
|
||||
expect($result->result)->toBeArray();
|
||||
expect($result->result['status'])->toBe('success');
|
||||
expect($result->result['count'])->toBe(1);
|
||||
expect(TestFiveMinuteJob::$executionCount)->toBe(1);
|
||||
});
|
||||
|
||||
it('executes callable jobs correctly', function () {
|
||||
$this->discoveryRegistry
|
||||
->shouldReceive('getClassesWithAttribute')
|
||||
->with(Schedule::class)
|
||||
->once()
|
||||
->andReturn([TestHourlyJob::class]);
|
||||
|
||||
$this->scheduleDiscovery->discoverAndRegister();
|
||||
|
||||
$scheduledTasks = $this->schedulerService->getScheduledTasks();
|
||||
expect($scheduledTasks)->toHaveCount(1);
|
||||
|
||||
$task = $scheduledTasks[0];
|
||||
|
||||
// Execute the task
|
||||
$result = $this->schedulerService->executeTask($task);
|
||||
|
||||
expect($result->success)->toBeTrue();
|
||||
expect($result->result)->toBe('hourly job executed');
|
||||
expect(TestHourlyJob::$executionCount)->toBe(1);
|
||||
});
|
||||
|
||||
it('uses correct intervals from Every value object', function () {
|
||||
$this->discoveryRegistry
|
||||
->shouldReceive('getClassesWithAttribute')
|
||||
->with(Schedule::class)
|
||||
->once()
|
||||
->andReturn([
|
||||
TestFiveMinuteJob::class, // 5 minutes = 300 seconds
|
||||
TestHourlyJob::class // 1 hour = 3600 seconds
|
||||
]);
|
||||
|
||||
$this->scheduleDiscovery->discoverAndRegister();
|
||||
|
||||
$scheduledTasks = $this->schedulerService->getScheduledTasks();
|
||||
|
||||
// Find the 5-minute job
|
||||
$fiveMinuteTask = array_values(array_filter(
|
||||
$scheduledTasks,
|
||||
fn($task) => $task->taskId === 'test-five-minute-job'
|
||||
))[0] ?? null;
|
||||
|
||||
expect($fiveMinuteTask)->not->toBeNull();
|
||||
|
||||
// Execute task
|
||||
$result = $this->schedulerService->executeTask($fiveMinuteTask);
|
||||
|
||||
expect($result->success)->toBeTrue();
|
||||
|
||||
// Get updated task
|
||||
$scheduledTasks = $this->schedulerService->getScheduledTasks();
|
||||
$updatedTask = array_values(array_filter(
|
||||
$scheduledTasks,
|
||||
fn($task) => $task->taskId === 'test-five-minute-job'
|
||||
))[0] ?? null;
|
||||
|
||||
// Next execution should be set (schedule updated)
|
||||
expect($updatedTask->nextExecution)->not->toBeNull();
|
||||
});
|
||||
|
||||
it('handles jobs without handle() or __invoke() gracefully', function () {
|
||||
// Create a job class without handle() or __invoke()
|
||||
$invalidJobClass = new class {
|
||||
// No handle() or __invoke()
|
||||
};
|
||||
|
||||
$this->discoveryRegistry
|
||||
->shouldReceive('getClassesWithAttribute')
|
||||
->with(Schedule::class)
|
||||
->once()
|
||||
->andReturn([$invalidJobClass::class]);
|
||||
|
||||
$this->scheduleDiscovery->discoverAndRegister();
|
||||
|
||||
$scheduledTasks = $this->schedulerService->getScheduledTasks();
|
||||
expect($scheduledTasks)->toHaveCount(1);
|
||||
|
||||
$task = $scheduledTasks[0];
|
||||
|
||||
// Executing should throw RuntimeException
|
||||
$result = $this->schedulerService->executeTask($task);
|
||||
|
||||
expect($result->success)->toBeFalse();
|
||||
expect($result->error)->toContain('must have handle() method or be callable');
|
||||
});
|
||||
|
||||
it('returns 0 when no scheduled jobs found', function () {
|
||||
$this->discoveryRegistry
|
||||
->shouldReceive('getClassesWithAttribute')
|
||||
->with(Schedule::class)
|
||||
->once()
|
||||
->andReturn([]);
|
||||
|
||||
$registered = $this->scheduleDiscovery->discoverAndRegister();
|
||||
|
||||
expect($registered)->toBe(0);
|
||||
|
||||
$scheduledTasks = $this->schedulerService->getScheduledTasks();
|
||||
expect($scheduledTasks)->toHaveCount(0);
|
||||
});
|
||||
|
||||
it('can retrieve scheduled tasks via getScheduledTasks()', function () {
|
||||
$this->discoveryRegistry
|
||||
->shouldReceive('getClassesWithAttribute')
|
||||
->with(Schedule::class)
|
||||
->once()
|
||||
->andReturn([
|
||||
TestFiveMinuteJob::class,
|
||||
TestHourlyJob::class
|
||||
]);
|
||||
|
||||
$this->scheduleDiscovery->discoverAndRegister();
|
||||
|
||||
$tasks = $this->scheduleDiscovery->getScheduledTasks();
|
||||
|
||||
expect($tasks)->toHaveCount(2);
|
||||
expect($tasks[0])->toHaveProperty('taskId');
|
||||
expect($tasks[0])->toHaveProperty('nextExecution');
|
||||
});
|
||||
|
||||
it('executes multiple jobs independently', function () {
|
||||
$this->discoveryRegistry
|
||||
->shouldReceive('getClassesWithAttribute')
|
||||
->with(Schedule::class)
|
||||
->once()
|
||||
->andReturn([
|
||||
TestFiveMinuteJob::class,
|
||||
TestHourlyJob::class
|
||||
]);
|
||||
|
||||
$this->scheduleDiscovery->discoverAndRegister();
|
||||
|
||||
$scheduledTasks = $this->schedulerService->getScheduledTasks();
|
||||
|
||||
// Execute both jobs
|
||||
foreach ($scheduledTasks as $task) {
|
||||
$result = $this->schedulerService->executeTask($task);
|
||||
expect($result->success)->toBeTrue();
|
||||
}
|
||||
|
||||
// Both counters should have incremented
|
||||
expect(TestFiveMinuteJob::$executionCount)->toBe(1);
|
||||
expect(TestHourlyJob::$executionCount)->toBe(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,516 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Framework\Core\ValueObjects\Version;
|
||||
use App\Framework\Core\ValueObjects\Timestamp;
|
||||
use App\Framework\MachineLearning\ModelManagement\DatabaseModelRegistry;
|
||||
use App\Framework\MachineLearning\ModelManagement\DatabasePerformanceStorage;
|
||||
use App\Framework\MachineLearning\ModelManagement\ModelPerformanceMonitor;
|
||||
use App\Framework\MachineLearning\ModelManagement\NotificationAlertingService;
|
||||
use App\Framework\MachineLearning\ModelManagement\MLConfig;
|
||||
use App\Framework\MachineLearning\ModelManagement\ValueObjects\ModelMetadata;
|
||||
use App\Framework\MachineLearning\ModelManagement\ValueObjects\ModelType;
|
||||
use App\Framework\Database\ConnectionInterface;
|
||||
use App\Framework\Database\ValueObjects\SqlQuery;
|
||||
use App\Framework\Notification\NotificationDispatcher;
|
||||
|
||||
/**
|
||||
* Integration Tests for ML Management System
|
||||
*
|
||||
* Tests the complete ML Management system including:
|
||||
* - DatabaseModelRegistry
|
||||
* - DatabasePerformanceStorage
|
||||
* - ModelPerformanceMonitor
|
||||
* - NotificationAlertingService
|
||||
* - MLConfig
|
||||
*/
|
||||
|
||||
describe('ML Management System Integration', function () {
|
||||
|
||||
beforeEach(function () {
|
||||
// Get services from container
|
||||
$this->connection = container()->get(ConnectionInterface::class);
|
||||
$this->registry = container()->get(DatabaseModelRegistry::class);
|
||||
$this->storage = container()->get(DatabasePerformanceStorage::class);
|
||||
$this->config = container()->get(MLConfig::class);
|
||||
$this->dispatcher = container()->get(NotificationDispatcher::class);
|
||||
|
||||
// Clean up test data
|
||||
$this->connection->execute(
|
||||
SqlQuery::create(
|
||||
'DELETE FROM ml_models WHERE model_name LIKE ?',
|
||||
['test-%']
|
||||
)
|
||||
);
|
||||
$this->connection->execute(
|
||||
SqlQuery::create(
|
||||
'DELETE FROM ml_predictions WHERE model_name LIKE ?',
|
||||
['test-%']
|
||||
)
|
||||
);
|
||||
$this->connection->execute(
|
||||
SqlQuery::create(
|
||||
'DELETE FROM ml_confidence_baselines WHERE model_name LIKE ?',
|
||||
['test-%']
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
// Clean up test data
|
||||
$this->connection->execute(
|
||||
SqlQuery::create(
|
||||
'DELETE FROM ml_models WHERE model_name LIKE ?',
|
||||
['test-%']
|
||||
)
|
||||
);
|
||||
$this->connection->execute(
|
||||
SqlQuery::create(
|
||||
'DELETE FROM ml_predictions WHERE model_name LIKE ?',
|
||||
['test-%']
|
||||
)
|
||||
);
|
||||
$this->connection->execute(
|
||||
SqlQuery::create(
|
||||
'DELETE FROM ml_confidence_baselines WHERE model_name LIKE ?',
|
||||
['test-%']
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
test('can register a new model in database', function () {
|
||||
$metadata = new ModelMetadata(
|
||||
modelName: 'test-sentiment-analyzer',
|
||||
modelType: ModelType::SUPERVISED,
|
||||
version: new Version(1, 0, 0),
|
||||
configuration: ['hidden_layers' => 3, 'learning_rate' => 0.001],
|
||||
performanceMetrics: ['accuracy' => 0.95, 'precision' => 0.93],
|
||||
createdAt: Timestamp::now(),
|
||||
deployedAt: Timestamp::now(),
|
||||
environment: 'production',
|
||||
metadata: ['description' => 'Test sentiment analysis model']
|
||||
);
|
||||
|
||||
$this->registry->register($metadata);
|
||||
|
||||
// Verify model was registered
|
||||
$retrievedMetadata = $this->registry->get('test-sentiment-analyzer', new Version(1, 0, 0));
|
||||
|
||||
expect($retrievedMetadata)->not->toBeNull();
|
||||
expect($retrievedMetadata->modelName)->toBe('test-sentiment-analyzer');
|
||||
expect($retrievedMetadata->version->toString())->toBe('1.0.0');
|
||||
expect($retrievedMetadata->modelType)->toBe(ModelType::SUPERVISED);
|
||||
expect($retrievedMetadata->isDeployed())->toBeTrue();
|
||||
expect($retrievedMetadata->environment)->toBe('production');
|
||||
});
|
||||
|
||||
test('can update model deployment status', function () {
|
||||
$metadata = new ModelMetadata(
|
||||
modelName: 'test-recommender',
|
||||
modelType: ModelType::SUPERVISED,
|
||||
version: new Version(2, 1, 0),
|
||||
configuration: ['features' => 100],
|
||||
performanceMetrics: ['rmse' => 0.15],
|
||||
createdAt: Timestamp::now(),
|
||||
deployedAt: null,
|
||||
environment: 'staging',
|
||||
metadata: ['description' => 'Test recommendation model']
|
||||
);
|
||||
|
||||
$this->registry->register($metadata);
|
||||
|
||||
// Update deployment status
|
||||
$this->registry->updateDeploymentStatus('test-recommender', new Version(2, 1, 0), true);
|
||||
|
||||
// Verify update
|
||||
$updated = $this->registry->get('test-recommender', new Version(2, 1, 0));
|
||||
expect($updated->isDeployed())->toBeTrue();
|
||||
});
|
||||
|
||||
test('can get all model names', function () {
|
||||
// Register multiple models
|
||||
$models = [
|
||||
'test-classifier-1',
|
||||
'test-classifier-2',
|
||||
'test-regressor-1',
|
||||
];
|
||||
|
||||
foreach ($models as $modelName) {
|
||||
$metadata = new ModelMetadata(
|
||||
modelName: $modelName,
|
||||
modelType: ModelType::SUPERVISED,
|
||||
version: new Version(1, 0, 0),
|
||||
configuration: [],
|
||||
performanceMetrics: [],
|
||||
createdAt: Timestamp::now(),
|
||||
deployedAt: null,
|
||||
environment: 'development'
|
||||
);
|
||||
$this->registry->register($metadata);
|
||||
}
|
||||
|
||||
$allNames = $this->registry->getAllModelNames();
|
||||
|
||||
foreach ($models as $expectedName) {
|
||||
expect($allNames)->toContain($expectedName);
|
||||
}
|
||||
});
|
||||
|
||||
test('can store prediction records', function () {
|
||||
$predictionRecord = [
|
||||
'model_name' => 'test-predictor',
|
||||
'version' => '1.0.0',
|
||||
'prediction' => ['class' => 'positive', 'probability' => 0.85],
|
||||
'actual' => ['class' => 'positive'],
|
||||
'confidence' => 0.85,
|
||||
'features' => ['text_length' => 150, 'sentiment_score' => 0.7],
|
||||
'timestamp' => Timestamp::now(),
|
||||
'is_correct' => true,
|
||||
];
|
||||
|
||||
$this->storage->storePrediction($predictionRecord);
|
||||
|
||||
// Verify prediction was stored by getting recent predictions
|
||||
$recentPredictions = $this->storage->getRecentPredictions(
|
||||
'test-predictor',
|
||||
new Version(1, 0, 0),
|
||||
100
|
||||
);
|
||||
|
||||
expect($recentPredictions)->toHaveCount(1);
|
||||
expect($recentPredictions[0]['model_name'])->toBe('test-predictor');
|
||||
expect($recentPredictions[0]['confidence'])->toBe(0.85);
|
||||
});
|
||||
|
||||
test('can calculate accuracy from predictions', function () {
|
||||
$modelName = 'test-accuracy-model';
|
||||
$version = new Version(1, 0, 0);
|
||||
|
||||
// Store multiple predictions
|
||||
$predictions = [
|
||||
['prediction' => ['class' => 'A'], 'actual' => ['class' => 'A'], 'confidence' => 0.9, 'is_correct' => true],
|
||||
['prediction' => ['class' => 'B'], 'actual' => ['class' => 'B'], 'confidence' => 0.85, 'is_correct' => true],
|
||||
['prediction' => ['class' => 'A'], 'actual' => ['class' => 'B'], 'confidence' => 0.6, 'is_correct' => false],
|
||||
['prediction' => ['class' => 'C'], 'actual' => ['class' => 'C'], 'confidence' => 0.95, 'is_correct' => true],
|
||||
];
|
||||
|
||||
foreach ($predictions as $pred) {
|
||||
$record = [
|
||||
'model_name' => $modelName,
|
||||
'version' => $version->toString(),
|
||||
'prediction' => $pred['prediction'],
|
||||
'actual' => $pred['actual'],
|
||||
'confidence' => $pred['confidence'],
|
||||
'features' => [],
|
||||
'timestamp' => Timestamp::now(),
|
||||
'is_correct' => $pred['is_correct'],
|
||||
];
|
||||
$this->storage->storePrediction($record);
|
||||
}
|
||||
|
||||
// Calculate accuracy (should be 3/4 = 0.75)
|
||||
$accuracy = $this->storage->calculateAccuracy($modelName, $version, 100);
|
||||
|
||||
expect($accuracy)->toBe(0.75);
|
||||
});
|
||||
|
||||
test('can store and retrieve confidence baseline', function () {
|
||||
$modelName = 'test-baseline-model';
|
||||
$version = new Version(1, 2, 3);
|
||||
|
||||
$this->storage->storeConfidenceBaseline(
|
||||
$modelName,
|
||||
$version,
|
||||
avgConfidence: 0.82,
|
||||
stdDevConfidence: 0.12
|
||||
);
|
||||
|
||||
$baseline = $this->storage->getConfidenceBaseline($modelName, $version);
|
||||
|
||||
expect($baseline)->not->toBeNull();
|
||||
expect($baseline['avg_confidence'])->toBe(0.82);
|
||||
expect($baseline['std_dev_confidence'])->toBe(0.12);
|
||||
});
|
||||
|
||||
test('can update confidence baseline (upsert)', function () {
|
||||
$modelName = 'test-upsert-model';
|
||||
$version = new Version(1, 0, 0);
|
||||
|
||||
// Initial insert
|
||||
$this->storage->storeConfidenceBaseline($modelName, $version, 0.80, 0.10);
|
||||
|
||||
// Update (upsert)
|
||||
$this->storage->storeConfidenceBaseline($modelName, $version, 0.85, 0.08);
|
||||
|
||||
$baseline = $this->storage->getConfidenceBaseline($modelName, $version);
|
||||
|
||||
expect($baseline['avg_confidence'])->toBe(0.85);
|
||||
expect($baseline['std_dev_confidence'])->toBe(0.08);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Model Performance Monitor Integration', function () {
|
||||
|
||||
beforeEach(function () {
|
||||
$this->connection = container()->get(ConnectionInterface::class);
|
||||
$this->registry = container()->get(DatabaseModelRegistry::class);
|
||||
$this->storage = container()->get(DatabasePerformanceStorage::class);
|
||||
$this->config = MLConfig::testing(); // Use testing config
|
||||
$this->alerting = new NotificationAlertingService(
|
||||
container()->get(NotificationDispatcher::class),
|
||||
$this->config
|
||||
);
|
||||
|
||||
$this->monitor = new ModelPerformanceMonitor(
|
||||
$this->registry,
|
||||
$this->storage,
|
||||
$this->alerting,
|
||||
$this->config
|
||||
);
|
||||
|
||||
// Clean up
|
||||
$this->connection->execute(
|
||||
SqlQuery::create(
|
||||
'DELETE FROM ml_models WHERE model_name LIKE ?',
|
||||
['test-%']
|
||||
)
|
||||
);
|
||||
$this->connection->execute(
|
||||
SqlQuery::create(
|
||||
'DELETE FROM ml_predictions WHERE model_name LIKE ?',
|
||||
['test-%']
|
||||
)
|
||||
);
|
||||
$this->connection->execute(
|
||||
SqlQuery::create(
|
||||
'DELETE FROM ml_confidence_baselines WHERE model_name LIKE ?',
|
||||
['test-%']
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
// Clean up
|
||||
$this->connection->execute(
|
||||
SqlQuery::create(
|
||||
'DELETE FROM ml_models WHERE model_name LIKE ?',
|
||||
['test-%']
|
||||
)
|
||||
);
|
||||
$this->connection->execute(
|
||||
SqlQuery::create(
|
||||
'DELETE FROM ml_predictions WHERE model_name LIKE ?',
|
||||
['test-%']
|
||||
)
|
||||
);
|
||||
$this->connection->execute(
|
||||
SqlQuery::create(
|
||||
'DELETE FROM ml_confidence_baselines WHERE model_name LIKE ?',
|
||||
['test-%']
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
test('can track prediction with performance monitoring', function () {
|
||||
$modelName = 'test-tracking-model';
|
||||
$version = new Version(1, 0, 0);
|
||||
|
||||
// Register model
|
||||
$metadata = new ModelMetadata(
|
||||
modelName: $modelName,
|
||||
modelType: ModelType::SUPERVISED,
|
||||
version: $version,
|
||||
configuration: [],
|
||||
performanceMetrics: ['baseline_accuracy' => 0.90],
|
||||
createdAt: Timestamp::now(),
|
||||
deployedAt: Timestamp::now(),
|
||||
environment: 'production'
|
||||
);
|
||||
$this->registry->register($metadata);
|
||||
|
||||
// Track prediction
|
||||
$this->monitor->trackPrediction(
|
||||
$modelName,
|
||||
$version,
|
||||
prediction: ['class' => 'spam'],
|
||||
confidence: 0.92,
|
||||
features: ['word_count' => 50],
|
||||
actual: ['class' => 'spam']
|
||||
);
|
||||
|
||||
// Verify prediction was stored
|
||||
$predictions = $this->storage->getRecentPredictions($modelName, $version, 10);
|
||||
|
||||
expect($predictions)->toHaveCount(1);
|
||||
expect($predictions[0]['confidence'])->toBe(0.92);
|
||||
});
|
||||
|
||||
test('can detect low confidence', function () {
|
||||
$modelName = 'test-low-confidence-model';
|
||||
$version = new Version(1, 0, 0);
|
||||
|
||||
// Store baseline with high confidence
|
||||
$this->storage->storeConfidenceBaseline($modelName, $version, 0.85, 0.05);
|
||||
|
||||
// Store predictions with low confidence
|
||||
for ($i = 0; $i < 50; $i++) {
|
||||
$this->storage->storePrediction([
|
||||
'model_name' => $modelName,
|
||||
'version' => $version->toString(),
|
||||
'prediction' => ['value' => $i],
|
||||
'actual' => ['value' => $i],
|
||||
'confidence' => 0.55, // Low confidence
|
||||
'features' => [],
|
||||
'timestamp' => Timestamp::now(),
|
||||
'is_correct' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
// Check for low confidence
|
||||
$hasLowConfidence = $this->monitor->hasLowConfidence($modelName, $version);
|
||||
|
||||
expect($hasLowConfidence)->toBeTrue();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Notification Integration', function () {
|
||||
|
||||
beforeEach(function () {
|
||||
$this->dispatcher = container()->get(NotificationDispatcher::class);
|
||||
$this->config = MLConfig::development();
|
||||
$this->alerting = new NotificationAlertingService(
|
||||
$this->dispatcher,
|
||||
$this->config,
|
||||
'test-admin'
|
||||
);
|
||||
});
|
||||
|
||||
test('can send generic alert', function () {
|
||||
// This should not throw
|
||||
$this->alerting->sendAlert(
|
||||
'warning',
|
||||
'Test Alert',
|
||||
'This is a test alert message',
|
||||
['test_data' => 'value']
|
||||
);
|
||||
|
||||
expect(true)->toBeTrue();
|
||||
});
|
||||
|
||||
test('can send drift detected alert', function () {
|
||||
$this->alerting->alertDriftDetected(
|
||||
'test-model',
|
||||
new Version(1, 0, 0),
|
||||
0.25
|
||||
);
|
||||
|
||||
expect(true)->toBeTrue();
|
||||
});
|
||||
|
||||
test('can send performance degradation alert', function () {
|
||||
$this->alerting->alertPerformanceDegradation(
|
||||
'test-model',
|
||||
new Version(1, 0, 0),
|
||||
currentAccuracy: 0.70,
|
||||
baselineAccuracy: 0.90
|
||||
);
|
||||
|
||||
expect(true)->toBeTrue();
|
||||
});
|
||||
|
||||
test('can send low confidence alert', function () {
|
||||
$this->alerting->alertLowConfidence(
|
||||
'test-model',
|
||||
new Version(1, 0, 0),
|
||||
0.55
|
||||
);
|
||||
|
||||
expect(true)->toBeTrue();
|
||||
});
|
||||
|
||||
test('can send model deployed alert', function () {
|
||||
$this->alerting->alertModelDeployed(
|
||||
'test-model',
|
||||
new Version(2, 0, 0),
|
||||
'production'
|
||||
);
|
||||
|
||||
expect(true)->toBeTrue();
|
||||
});
|
||||
|
||||
test('respects monitoring disabled config', function () {
|
||||
$config = new MLConfig(monitoringEnabled: false);
|
||||
$alerting = new NotificationAlertingService(
|
||||
$this->dispatcher,
|
||||
$config,
|
||||
'test-admin'
|
||||
);
|
||||
|
||||
// Should not throw even with monitoring disabled
|
||||
$alerting->alertDriftDetected(
|
||||
'test-model',
|
||||
new Version(1, 0, 0),
|
||||
0.25
|
||||
);
|
||||
|
||||
expect(true)->toBeTrue();
|
||||
});
|
||||
});
|
||||
|
||||
describe('MLConfig Integration', function () {
|
||||
|
||||
test('can create config from environment', function () {
|
||||
$config = MLConfig::fromEnvironment();
|
||||
|
||||
expect($config)->toBeInstanceOf(MLConfig::class);
|
||||
expect($config->monitoringEnabled)->toBeTrue();
|
||||
expect($config->driftThreshold)->toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('production config has strict thresholds', function () {
|
||||
$config = MLConfig::production();
|
||||
|
||||
expect($config->monitoringEnabled)->toBeTrue();
|
||||
expect($config->autoTuningEnabled)->toBeFalse();
|
||||
expect($config->driftThreshold)->toBe(0.15);
|
||||
expect($config->confidenceAlertThreshold)->toBe(0.65);
|
||||
});
|
||||
|
||||
test('development config has relaxed thresholds', function () {
|
||||
$config = MLConfig::development();
|
||||
|
||||
expect($config->monitoringEnabled)->toBeTrue();
|
||||
expect($config->autoTuningEnabled)->toBeTrue();
|
||||
expect($config->driftThreshold)->toBe(0.25);
|
||||
});
|
||||
|
||||
test('testing config has very relaxed thresholds', function () {
|
||||
$config = MLConfig::testing();
|
||||
|
||||
expect($config->monitoringEnabled)->toBeFalse();
|
||||
expect($config->autoTuningEnabled)->toBeTrue();
|
||||
expect($config->driftThreshold)->toBe(0.50);
|
||||
});
|
||||
|
||||
test('can detect drift using config threshold', function () {
|
||||
$config = MLConfig::production();
|
||||
|
||||
expect($config->isDriftDetected(0.10))->toBeFalse(); // Below threshold
|
||||
expect($config->isDriftDetected(0.20))->toBeTrue(); // Above threshold
|
||||
});
|
||||
|
||||
test('can detect low confidence using config threshold', function () {
|
||||
$config = MLConfig::production();
|
||||
|
||||
expect($config->isLowConfidence(0.70))->toBeFalse(); // Above threshold
|
||||
expect($config->isLowConfidence(0.60))->toBeTrue(); // Below threshold
|
||||
});
|
||||
|
||||
test('can detect low accuracy using config threshold', function () {
|
||||
$config = MLConfig::production();
|
||||
|
||||
expect($config->isLowAccuracy(0.80))->toBeFalse(); // Above threshold
|
||||
expect($config->isLowAccuracy(0.70))->toBeTrue(); // Below threshold
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user