From d02f4361e3f2d48beb607a8794ccfec5d068d4ec Mon Sep 17 00:00:00 2001 From: Snider Date: Thu, 12 Mar 2026 15:45:45 +0000 Subject: [PATCH] fix(scheduler): skip test directories in ScheduledActionScanner Test files inside module Tests/ directories (e.g. app/Mod/Lem/Tests/) extend Tests\TestCase which isn't available in production without dev dependencies. The scanner now skips /Tests/ directories and *Test.php files, and wraps class_exists() in try/catch for defence in depth. Co-Authored-By: Virgil --- src/Core/Actions/ScheduledActionScanner.php | 19 ++++++++++++++- tests/Feature/ScheduledActionScannerTest.php | 17 +++++++++++++ .../Mod/Scheduled/Tests/FakeScheduledTest.php | 24 +++++++++++++++++++ 3 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 tests/Fixtures/Mod/Scheduled/Tests/FakeScheduledTest.php diff --git a/src/Core/Actions/ScheduledActionScanner.php b/src/Core/Actions/ScheduledActionScanner.php index 8c22869..55a521d 100644 --- a/src/Core/Actions/ScheduledActionScanner.php +++ b/src/Core/Actions/ScheduledActionScanner.php @@ -52,9 +52,26 @@ class ScheduledActionScanner continue; } + // Skip test directories — test files extend base classes + // that aren't available without dev dependencies. + // Convention: module test dirs use capital "Tests/" (e.g. app/Mod/Lem/Tests/). + if (preg_match('#[/\\\\]Tests[/\\\\]#', $file->getPathname()) + || str_ends_with($file->getBasename(), 'Test.php')) { + continue; + } + $class = $this->classFromFile($file->getPathname()); - if ($class === null || ! class_exists($class)) { + if ($class === null) { + continue; + } + + try { + if (! class_exists($class)) { + continue; + } + } catch (\Throwable) { + // Class may reference unavailable dependencies (e.g. dev-only) continue; } diff --git a/tests/Feature/ScheduledActionScannerTest.php b/tests/Feature/ScheduledActionScannerTest.php index f37a725..b228689 100644 --- a/tests/Feature/ScheduledActionScannerTest.php +++ b/tests/Feature/ScheduledActionScannerTest.php @@ -70,4 +70,21 @@ class ScheduledActionScannerTest extends TestCase $results = $this->scanner->scan(['/nonexistent/path']); $this->assertEmpty($results); } + + public function test_scan_skips_test_directories(): void + { + $results = $this->scanner->scan([ + dirname(__DIR__).'/Fixtures/Mod/Scheduled', + ]); + + // FakeScheduledTest is inside a Tests/ directory and should be skipped + $classes = array_keys($results); + foreach ($classes as $class) { + $this->assertStringNotContainsString('FakeScheduledTest', $class); + } + + // But real actions are still discovered + $this->assertArrayHasKey(EveryMinuteAction::class, $results); + $this->assertArrayHasKey(DailyAction::class, $results); + } } diff --git a/tests/Fixtures/Mod/Scheduled/Tests/FakeScheduledTest.php b/tests/Fixtures/Mod/Scheduled/Tests/FakeScheduledTest.php new file mode 100644 index 0000000..a76e361 --- /dev/null +++ b/tests/Fixtures/Mod/Scheduled/Tests/FakeScheduledTest.php @@ -0,0 +1,24 @@ +