Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 23 additions & 2 deletions src/Install/GuidelineComposer.php
Original file line number Diff line number Diff line change
Expand Up @@ -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' => '',
Expand Down Expand Up @@ -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)) {
Expand All @@ -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), '/\\');
Expand Down
106 changes: 106 additions & 0 deletions tests/Feature/Install/GuidelineComposerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
5 changes: 5 additions & 0 deletions tests/fixtures/.ai/guidelines/laravel/fortify/core.blade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Laravel Fortify (Custom Override)

This is a custom override for Laravel Fortify guidelines.

Never use two-factor authentication in development.
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Spatie Laravel Permission

This is a third-party guideline without an override.

Always use Spatie's permission system for authorization.