diff --git a/backend/app/Services/ModuleManager.php b/backend/app/Services/ModuleManager.php index e2e566f2..228aa69e 100644 --- a/backend/app/Services/ModuleManager.php +++ b/backend/app/Services/ModuleManager.php @@ -5,6 +5,7 @@ use App\Support\CoreVersionConstraint; use Illuminate\Support\Collection; use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Log; class ModuleManager { @@ -44,6 +45,24 @@ public function discover(): Collection continue; } + // A module lives in a directory named after itself — that is how + // installFromZip() puts it there and how loadEnabled() finds it. + // Anything else carrying a module.json is a leftover: a backup an + // updater parked next to the module, a half-finished copy, an + // unpacked archive. Those used to show up here as a second, also + // "enabled" install of the same module, because enablement matches + // on the manifest name and the copy carries the same one — two + // cards, two directories, the same provider class, and no way for + // the operator to tell which is which. + if ($manifest['name'] !== $entry) { + Log::warning('module.directory_name_mismatch', [ + 'directory' => $entry, + 'declares' => $manifest['name'], + ]); + + continue; + } + $manifest['enabled'] = in_array($manifest['name'], $enabled); $manifest['directory'] = $entry; $manifest['has_error'] = false; diff --git a/backend/tests/Feature/ModuleDiscoveryTest.php b/backend/tests/Feature/ModuleDiscoveryTest.php new file mode 100644 index 00000000..ad1a0b4f --- /dev/null +++ b/backend/tests/Feature/ModuleDiscoveryTest.php @@ -0,0 +1,89 @@ + */ + private array $doPosprzatania = []; + + protected function setUp(): void + { + parent::setUp(); + + $this->modulesPath = base_path('modules'); + } + + protected function tearDown(): void + { + foreach ($this->doPosprzatania as $katalog) { + if (is_dir($katalog)) { + @unlink("{$katalog}/module.json"); + @rmdir($katalog); + } + } + + parent::tearDown(); + } + + private function zrobKatalogModulu(string $katalog, string $nazwaWManifescie): void + { + $sciezka = "{$this->modulesPath}/{$katalog}"; + @mkdir($sciezka, 0755, true); + + file_put_contents("{$sciezka}/module.json", json_encode([ + 'name' => $nazwaWManifescie, + 'display_name' => $nazwaWManifescie, + 'version' => '1.0.0', + 'provider' => "Modules\\{$nazwaWManifescie}\\Providers\\Provider", + ])); + + $this->doPosprzatania[] = $sciezka; + } + + public function test_katalog_nazwany_jak_modul_jest_wykrywany(): void + { + $this->zrobKatalogModulu('DiscoveryProbe', 'DiscoveryProbe'); + + $nazwy = app(ModuleManager::class)->discover()->pluck('name')->all(); + + $this->assertContains('DiscoveryProbe', $nazwy); + } + + public function test_kopia_obok_modulu_nie_jest_drugim_modulem(): void + { + $this->zrobKatalogModulu('DiscoveryProbe', 'DiscoveryProbe'); + $this->zrobKatalogModulu('DiscoveryProbe.poprzednia-20260930195044', 'DiscoveryProbe'); + + $moduly = app(ModuleManager::class)->discover() + ->where('name', 'DiscoveryProbe'); + + $this->assertCount(1, $moduly, 'kopia poprzedniej wersji pokazała się jako osobny moduł'); + $this->assertSame('DiscoveryProbe', $moduly->first()['directory']); + } + + public function test_katalog_deklarujacy_obca_nazwe_jest_pomijany(): void + { + $this->zrobKatalogModulu('DiscoveryProbe', 'ZupelnieCosInnego'); + + $nazwy = app(ModuleManager::class)->discover()->pluck('name')->all(); + + $this->assertNotContains('ZupelnieCosInnego', $nazwy); + } +}