diff --git a/src/Core/Console/Boot.php b/src/Core/Console/Boot.php index 7e7090c..05de287 100644 --- a/src/Core/Console/Boot.php +++ b/src/Core/Console/Boot.php @@ -30,5 +30,6 @@ class Boot $event->command(Commands\MakePlugCommand::class); $event->command(Commands\MakeWebsiteCommand::class); $event->command(Commands\PruneEmailShieldStatsCommand::class); + $event->command(Commands\ScheduleSyncCommand::class); } } diff --git a/src/Core/Console/Commands/ScheduleSyncCommand.php b/src/Core/Console/Commands/ScheduleSyncCommand.php new file mode 100644 index 0000000..1cbce72 --- /dev/null +++ b/src/Core/Console/Commands/ScheduleSyncCommand.php @@ -0,0 +1,90 @@ +scan($paths); + + $added = 0; + $disabled = 0; + $unchanged = 0; + + // Upsert discovered actions + foreach ($discovered as $class => $attribute) { + $existing = ScheduledAction::where('action_class', $class)->first(); + + if ($existing) { + $unchanged++; + + continue; + } + + ScheduledAction::create([ + 'action_class' => $class, + 'frequency' => $attribute->frequency, + 'timezone' => $attribute->timezone, + 'without_overlapping' => $attribute->withoutOverlapping, + 'run_in_background' => $attribute->runInBackground, + 'is_enabled' => true, + ]); + + $added++; + } + + // Disable actions no longer in codebase + $discoveredClasses = array_keys($discovered); + $stale = ScheduledAction::where('is_enabled', true) + ->whereNotIn('action_class', $discoveredClasses) + ->get(); + + foreach ($stale as $action) { + $action->update(['is_enabled' => false]); + $disabled++; + } + + $this->info("Schedule sync complete: {$added} added, {$disabled} disabled, {$unchanged} unchanged."); + + return Command::SUCCESS; + } +} diff --git a/tests/Feature/ScheduleSyncCommandTest.php b/tests/Feature/ScheduleSyncCommandTest.php new file mode 100644 index 0000000..8200310 --- /dev/null +++ b/tests/Feature/ScheduleSyncCommandTest.php @@ -0,0 +1,111 @@ +loadMigrationsFrom(__DIR__.'/../../database/migrations'); + } + + protected function defineEnvironment($app): void + { + $app['config']->set('database.default', 'testing'); + $app['config']->set('database.connections.testing', [ + 'driver' => 'sqlite', + 'database' => ':memory:', + ]); + + // Point scanner at test fixtures only + $app['config']->set('core.scheduled_action_paths', [ + __DIR__.'/../Fixtures/Mod/Scheduled', + ]); + } + + protected function setUp(): void + { + parent::setUp(); + + $this->app->make(\Illuminate\Contracts\Console\Kernel::class)->registerCommand( + $this->app->make(ScheduleSyncCommand::class) + ); + } + + public function test_sync_inserts_new_scheduled_actions(): void + { + $this->artisan('schedule:sync') + ->assertSuccessful(); + + $this->assertDatabaseHas('scheduled_actions', [ + 'action_class' => 'Core\\Tests\\Fixtures\\Mod\\Scheduled\\Actions\\EveryMinuteAction', + 'frequency' => 'everyMinute', + 'is_enabled' => true, + ]); + + $this->assertDatabaseHas('scheduled_actions', [ + 'action_class' => 'Core\\Tests\\Fixtures\\Mod\\Scheduled\\Actions\\DailyAction', + 'frequency' => 'dailyAt:09:00', + 'timezone' => 'Europe/London', + ]); + } + + public function test_sync_disables_removed_actions(): void + { + // Pre-populate with an action that no longer exists + ScheduledAction::create([ + 'action_class' => 'App\\Actions\\RemovedAction', + 'frequency' => 'hourly', + 'is_enabled' => true, + ]); + + $this->artisan('schedule:sync') + ->assertSuccessful(); + + $this->assertDatabaseHas('scheduled_actions', [ + 'action_class' => 'App\\Actions\\RemovedAction', + 'is_enabled' => false, + ]); + } + + public function test_sync_preserves_manually_edited_frequency(): void + { + // Pre-populate with a manually edited action + ScheduledAction::create([ + 'action_class' => 'Core\\Tests\\Fixtures\\Mod\\Scheduled\\Actions\\EveryMinuteAction', + 'frequency' => 'hourly', // Manually changed from everyMinute + 'is_enabled' => true, + ]); + + $this->artisan('schedule:sync') + ->assertSuccessful(); + + // Should preserve the manual edit + $this->assertDatabaseHas('scheduled_actions', [ + 'action_class' => 'Core\\Tests\\Fixtures\\Mod\\Scheduled\\Actions\\EveryMinuteAction', + 'frequency' => 'hourly', + ]); + } + + public function test_sync_is_idempotent(): void + { + $this->artisan('schedule:sync')->assertSuccessful(); + $this->artisan('schedule:sync')->assertSuccessful(); + + $count = ScheduledAction::where( + 'action_class', + 'Core\\Tests\\Fixtures\\Mod\\Scheduled\\Actions\\EveryMinuteAction' + )->count(); + + $this->assertSame(1, $count); + } +}