diff --git a/src/Install/GuidelineComposer.php b/src/Install/GuidelineComposer.php index 2a957af1..5e5edee2 100644 --- a/src/Install/GuidelineComposer.php +++ b/src/Install/GuidelineComposer.php @@ -273,7 +273,7 @@ protected function renderContent(string $content, string $path): string */ protected function guideline(string $path, bool $thirdParty = false): array { - $path = $this->guidelinePath($path); + $path = $this->guidelinePath($path, $thirdParty); if (is_null($path)) { return [ 'content' => '', @@ -349,7 +349,7 @@ private function prependGuidelinePath(string $path, string $basePath): string return str_replace('/', DIRECTORY_SEPARATOR, $basePath.$path); } - protected function guidelinePath(string $path): ?string + protected function guidelinePath(string $path, bool $thirdParty = false): ?string { // Relative path, prepend our package path to it if (! file_exists($path)) { @@ -366,6 +366,27 @@ protected function guidelinePath(string $path): ?string return $path; } + // Check if this is a third-party package guideline from vendor directory + if ($thirdParty) { + $vendorPos = strpos($path, DIRECTORY_SEPARATOR.'vendor'.DIRECTORY_SEPARATOR); + $afterVendor = substr($path, $vendorPos + strlen(DIRECTORY_SEPARATOR.'vendor'.DIRECTORY_SEPARATOR)); + + // Remove common guideline directory patterns + $relativePath = str_replace( + DIRECTORY_SEPARATOR.implode(DIRECTORY_SEPARATOR, [ + 'resources', + 'boost', + 'guidelines', + ]).DIRECTORY_SEPARATOR, + DIRECTORY_SEPARATOR, + $afterVendor + ); + + $customPath = $this->prependUserGuidelinePath($relativePath); + + return file_exists($customPath) ? $customPath : $path; + } + // The path is not a custom guideline, check if the user has an override for this $basePath = realpath(__DIR__.'/../../'); $relativePath = ltrim(str_replace([$basePath, '.ai'.DIRECTORY_SEPARATOR, '.ai/'], '', $path), '/\\'); diff --git a/tests/Feature/Install/GuidelineComposerTest.php b/tests/Feature/Install/GuidelineComposerTest.php index dfb2399b..b690a311 100644 --- a/tests/Feature/Install/GuidelineComposerTest.php +++ b/tests/Feature/Install/GuidelineComposerTest.php @@ -588,3 +588,109 @@ ->not->toContain('Wayfinder + Inertia') ->not->toContain('Wayfinder Form Component'); }); + +test('loads third-party guidelines from vendor directory when no override exists', function (): void { + $packages = new PackageCollection([ + new Package(Packages::LARAVEL, 'laravel/framework', '11.0.0'), + ]); + + $this->roster->shouldReceive('packages')->andReturn($packages); + + $composer = Mockery::mock(GuidelineComposer::class, [$this->roster, $this->herd])->makePartial(); + $composer + ->shouldReceive('customGuidelinePath') + ->andReturnUsing(fn ($path = ''): string => realpath(testDirectory('fixtures/.ai/guidelines')).DIRECTORY_SEPARATOR.ltrim((string) $path, '/\\')); + + // Simulate loading a vendor guideline without an override + $vendorPath = realpath(testDirectory('fixtures/vendor/spatie/laravel-permission/resources/boost/guidelines/core.blade.php')); + $reflection = new ReflectionClass($composer); + $method = $reflection->getMethod('guideline'); + $method->setAccessible(true); + $result = $method->invoke($composer, $vendorPath, true); + + expect($result) + ->toBeArray() + ->toHaveKey('content') + ->toHaveKey('path') + ->and($result['content']) + ->toContain('This is a third-party guideline without an override') + ->toContain('Always use Spatie\'s permission system for authorization') + ->and($result['third_party']) + ->toBeTrue() + ->and($result['path']) + ->toBe($vendorPath); // Should use vendor path since no override exists +}); + +test('overrides third-party guidelines from vendor directory with custom guidelines', function (): void { + $packages = new PackageCollection([ + new Package(Packages::LARAVEL, 'laravel/framework', '11.0.0'), + ]); + + $this->roster->shouldReceive('packages')->andReturn($packages); + + $composer = Mockery::mock(GuidelineComposer::class, [$this->roster, $this->herd])->makePartial(); + $composer + ->shouldReceive('customGuidelinePath') + ->andReturnUsing(fn ($path = ''): string => realpath(testDirectory('fixtures/.ai/guidelines')).DIRECTORY_SEPARATOR.ltrim((string) $path, '/\\')); + + // Simulate loading a vendor guideline that has an override + $vendorPath = realpath(testDirectory('fixtures/vendor/laravel/fortify/resources/boost/guidelines/core.blade.php')); + $reflection = new ReflectionClass($composer); + $method = $reflection->getMethod('guideline'); + $method->setAccessible(true); + $result = $method->invoke($composer, $vendorPath, true); + + // Normalize paths for cross-platform comparison + $expectedOverridePath = str_replace('/', DIRECTORY_SEPARATOR, '.ai/guidelines/laravel/fortify/core.blade.php'); + + expect($result) + ->toBeArray() + ->toHaveKey('content') + ->toHaveKey('path') + ->and($result['content']) + ->toContain('This is a custom override for Laravel Fortify guidelines') + ->toContain('Never use two-factor authentication in development') + ->not->toContain('This is the original third-party guideline from the vendor directory') + ->and($result['third_party']) + ->toBeTrue() + ->and($result['path']) + ->toContain($expectedOverridePath) // Should use override path + ->not->toContain('vendor'); +}); + +test('guidelinePath correctly resolves vendor package paths to override locations', function (): void { + $packages = new PackageCollection([ + new Package(Packages::LARAVEL, 'laravel/framework', '11.0.0'), + ]); + + $this->roster->shouldReceive('packages')->andReturn($packages); + + $composer = Mockery::mock(GuidelineComposer::class, [$this->roster, $this->herd])->makePartial(); + $composer + ->shouldReceive('customGuidelinePath') + ->andReturnUsing(fn ($path = ''): string => realpath(testDirectory('fixtures/.ai/guidelines')).DIRECTORY_SEPARATOR.ltrim((string) $path, '/\\')); + + $reflection = new ReflectionClass($composer); + $method = $reflection->getMethod('guidelinePath'); + $method->setAccessible(true); + + // Test with override present + $vendorPathWithOverride = realpath(testDirectory('fixtures/vendor/laravel/fortify/resources/boost/guidelines/core.blade.php')); + $resolvedPath = $method->invoke($composer, $vendorPathWithOverride, true); + + // Normalize paths for cross-platform comparison + $expectedOverridePath = str_replace('/', DIRECTORY_SEPARATOR, '.ai/guidelines/laravel/fortify/core.blade.php'); + $expectedVendorPath = str_replace('/', DIRECTORY_SEPARATOR, 'vendor/spatie/laravel-permission'); + + expect($resolvedPath) + ->toContain($expectedOverridePath) + ->not->toContain('vendor'); + + // Test without override + $vendorPathWithoutOverride = realpath(testDirectory('fixtures/vendor/spatie/laravel-permission/resources/boost/guidelines/core.blade.php')); + $resolvedPath = $method->invoke($composer, $vendorPathWithoutOverride, true); + + expect($resolvedPath) + ->toContain($expectedVendorPath) + ->toBe($vendorPathWithoutOverride); +}); diff --git a/tests/fixtures/.ai/guidelines/laravel/fortify/core.blade.php b/tests/fixtures/.ai/guidelines/laravel/fortify/core.blade.php new file mode 100644 index 00000000..a1f96f83 --- /dev/null +++ b/tests/fixtures/.ai/guidelines/laravel/fortify/core.blade.php @@ -0,0 +1,5 @@ +# Laravel Fortify (Custom Override) + +This is a custom override for Laravel Fortify guidelines. + +Never use two-factor authentication in development. diff --git a/tests/fixtures/vendor/laravel/fortify/resources/boost/guidelines/core.blade.php b/tests/fixtures/vendor/laravel/fortify/resources/boost/guidelines/core.blade.php new file mode 100644 index 00000000..a6c0b81a --- /dev/null +++ b/tests/fixtures/vendor/laravel/fortify/resources/boost/guidelines/core.blade.php @@ -0,0 +1,5 @@ +# Laravel Fortify (Vendor Version) + +This is the original third-party guideline from the vendor directory. + +Use Fortify's two-factor authentication features. diff --git a/tests/fixtures/vendor/spatie/laravel-permission/resources/boost/guidelines/core.blade.php b/tests/fixtures/vendor/spatie/laravel-permission/resources/boost/guidelines/core.blade.php new file mode 100644 index 00000000..d2f7460e --- /dev/null +++ b/tests/fixtures/vendor/spatie/laravel-permission/resources/boost/guidelines/core.blade.php @@ -0,0 +1,5 @@ +# Spatie Laravel Permission + +This is a third-party guideline without an override. + +Always use Spatie's permission system for authorization.