Skip to content
Merged
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
38 changes: 28 additions & 10 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,27 +14,36 @@ permissions:

jobs:
tests:
runs-on: ubuntu-22.04
runs-on: ${{ matrix.os }}
defaults:
run:
shell: bash

strategy:
fail-fast: true
matrix:
os: [ ubuntu-22.04, windows-latest ]
php: [ 8.2, 8.3, 8.4 ]
laravel: [ 11, 12 ]

name: PHP ${{ matrix.php }} - Laravel ${{ matrix.laravel }}
name: PHP ${{ matrix.php }} - Laravel ${{ matrix.laravel }} - ${{ matrix.os }}

steps:
- name: Set git to use LF
run: |
git config --global core.autocrlf false
git config --global core.eol lf

- name: Checkout code
uses: actions/checkout@v4

- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: dom, curl, libxml, mbstring, zip
extensions: dom, curl, libxml, mbstring, zip, fileinfo, pdo, sqlite, pdo_sqlite
ini-values: error_reporting=E_ALL
tools: composer:v2
tools: composer:v2.8
coverage: none

- name: Setup SSH Keys
Expand All @@ -46,32 +55,41 @@ jobs:

- name: Install dependencies
run: |
composer update --prefer-dist --no-interaction --no-progress --with="illuminate/contracts:^${{ matrix.laravel }}"
composer update --prefer-dist --no-interaction --no-progress --with='illuminate/contracts:^${{ matrix.laravel }}'

- name: Execute tests
run: vendor/bin/pest
test-l10:
runs-on: ubuntu-22.04
runs-on: ${{ matrix.os }}
defaults:
run:
shell: bash

strategy:
fail-fast: true
matrix:
os: [ ubuntu-22.04, windows-latest ]
php: [ 8.1, 8.2 ]
laravel: [ 10 ]

name: PHP ${{ matrix.php }} - Laravel ${{ matrix.laravel }}
name: PHP ${{ matrix.php }} - Laravel ${{ matrix.laravel }} - ${{ matrix.os }}

steps:
- name: Set git to use LF
run: |
git config --global core.autocrlf false
git config --global core.eol lf

- name: Checkout code
uses: actions/checkout@v4

- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: dom, curl, libxml, mbstring, zip
extensions: dom, curl, libxml, mbstring, zip, fileinfo, pdo, sqlite, pdo_sqlite
ini-values: error_reporting=E_ALL
tools: composer:v2
tools: composer:v2.8
coverage: none

- name: Setup SSH Keys
Expand All @@ -83,7 +101,7 @@ jobs:

- name: Install dependencies
run: |
composer update --prefer-dist --no-interaction --no-progress --with="illuminate/contracts:^${{ matrix.laravel }}"
composer update --prefer-dist --no-interaction --no-progress --with="illuminate/contracts:^${{ matrix.laravel }}.0"

- name: Execute tests
run: vendor/bin/pest
3 changes: 2 additions & 1 deletion src/Install/GuidelineComposer.php
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,8 @@ protected function guidelinePath(string $path): ?string
}

// The path is not a custom guideline, check if the user has an override for this
$relativePath = ltrim(str_replace([realpath(__DIR__.'/../../'), '.ai/'], '', $path), '/');
$basePath = realpath(__DIR__.'/../../');
$relativePath = ltrim(str_replace([$basePath, '.ai'.DIRECTORY_SEPARATOR, '.ai/'], '', $path), '/\\');
$customPath = $this->prependUserGuidelinePath($relativePath);

return file_exists($customPath) ? $customPath : $path;
Expand Down
8 changes: 8 additions & 0 deletions src/Install/Mcp/FileWriter.php
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,9 @@ protected function generateServerJson(string $key, array $serverConfig, int $bas
{
$json = json_encode($serverConfig, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);

// Normalize line endings to Unix style
$json = str_replace("\r\n", "\n", $json);

// If no indentation needed, return as-is
if (empty($baseIndent)) {
return '"'.$key.'": '.$json;
Expand Down Expand Up @@ -380,6 +383,11 @@ protected function writeJsonConfig(array $config): bool
{
$json = json_encode($config, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);

// Normalize line endings to Unix style
if ($json) {
$json = str_replace("\r\n", "\n", $json);
}

return $json && $this->writeFile($json);
}

Expand Down
167 changes: 74 additions & 93 deletions tests/Feature/Console/InstallCommandMultiselectTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,110 +2,91 @@

declare(strict_types=1);

namespace Tests\Feature\Console;

use Laravel\Prompts\Key;
use Laravel\Prompts\Prompt;
use Tests\TestCase;

class InstallCommandMultiselectTest extends TestCase
{
/**
* Test that multiselect returns keys when given an associative array.
*/
public function test_multiselect_returns_keys_for_associative_array(): void
{
// Mock the prompt to simulate user selecting options
// Note: mcp_server is already selected by default, so we don't toggle it
Prompt::fake([
Key::DOWN, // Move to second option (ai_guidelines)
Key::SPACE, // Select second option
Key::ENTER, // Submit
]);
test('multiselect returns keys for associative array', function () {
// Mock the prompt to simulate user selecting options
// Note: mcp_server is already selected by default, so we don't toggle it
Prompt::fake([
Key::DOWN, // Move to second option (ai_guidelines)
Key::SPACE, // Select second option
Key::ENTER, // Submit
]);

$result = \Laravel\Prompts\multiselect(
label: 'What shall we install?',
options: [
'mcp_server' => 'Boost MCP Server',
'ai_guidelines' => 'Package AI Guidelines',
'style_guidelines' => 'Laravel Style AI Guidelines',
],
default: ['mcp_server']
);
$result = \Laravel\Prompts\multiselect(
label: 'What shall we install?',
options: [
'mcp_server' => 'Boost MCP Server',
'ai_guidelines' => 'Package AI Guidelines',
'style_guidelines' => 'Laravel Style AI Guidelines',
],
default: ['mcp_server']
);

// Assert that we get the keys, not the values
$this->assertIsArray($result);
$this->assertCount(2, $result, 'Should have 2 items selected');
$this->assertContains('mcp_server', $result);
$this->assertContains('ai_guidelines', $result);
$this->assertNotContains('Boost MCP Server', $result);
$this->assertNotContains('Package AI Guidelines', $result);
}
// Assert that we get the keys, not the values
expect($result)->toBeArray();
expect($result)->toHaveCount(2, 'Should have 2 items selected');
expect($result)->toContain('mcp_server');
expect($result)->toContain('ai_guidelines');
expect($result)->not->toContain('Boost MCP Server');
expect($result)->not->toContain('Package AI Guidelines');
})->skipOnWindows();

/**
* Test multiselect with numeric indexed array returns values.
*/
public function test_multiselect_returns_values_for_indexed_array(): void
{
Prompt::fake([
Key::SPACE, // Select first option
Key::DOWN, // Move to second option
Key::SPACE, // Select second option
Key::ENTER, // Submit
]);
test('multiselect returns values for indexed array', function () {
Prompt::fake([
Key::SPACE, // Select first option
Key::DOWN, // Move to second option
Key::SPACE, // Select second option
Key::ENTER, // Submit
]);

$result = \Laravel\Prompts\multiselect(
label: 'Select options',
options: ['Option 1', 'Option 2', 'Option 3'],
default: []
);
$result = \Laravel\Prompts\multiselect(
label: 'Select options',
options: ['Option 1', 'Option 2', 'Option 3'],
default: []
);

// For indexed arrays, it returns the actual values
$this->assertIsArray($result);
$this->assertContains('Option 1', $result);
$this->assertContains('Option 2', $result);
}
// For indexed arrays, it returns the actual values
expect($result)->toBeArray();
expect($result)->toContain('Option 1');
expect($result)->toContain('Option 2');
})->skipOnWindows();

/**
* Test that demonstrates multiselect behavior is consistent with InstallCommand expectations.
* This ensures Laravel 10/11 compatibility.
*/
public function test_multiselect_behavior_matches_install_command_expectations(): void
{
// Test the exact same structure used in InstallCommand::selectBoostFeatures()
// Note: mcp_server and ai_guidelines are already selected by default
Prompt::fake([
Key::DOWN, // Move to ai_guidelines (already selected)
Key::DOWN, // Move to style_guidelines
Key::SPACE, // Select style_guidelines
Key::ENTER, // Submit
]);
test('multiselect behavior matches install command expectations', function () {
// Test the exact same structure used in InstallCommand::selectBoostFeatures()
// Note: mcp_server and ai_guidelines are already selected by default
Prompt::fake([
Key::DOWN, // Move to ai_guidelines (already selected)
Key::DOWN, // Move to style_guidelines
Key::SPACE, // Select style_guidelines
Key::ENTER, // Submit
]);

$toInstallOptions = [
'mcp_server' => 'Boost MCP Server',
'ai_guidelines' => 'Package AI Guidelines (i.e. Framework, Inertia, Pest)',
'style_guidelines' => 'Laravel Style AI Guidelines',
];
$toInstallOptions = [
'mcp_server' => 'Boost MCP Server',
'ai_guidelines' => 'Package AI Guidelines (i.e. Framework, Inertia, Pest)',
'style_guidelines' => 'Laravel Style AI Guidelines',
];

$result = \Laravel\Prompts\multiselect(
label: 'What shall we install?',
options: $toInstallOptions,
default: ['mcp_server', 'ai_guidelines'],
required: true,
hint: 'Style guidelines are best for new projects',
);
$result = \Laravel\Prompts\multiselect(
label: 'What shall we install?',
options: $toInstallOptions,
default: ['mcp_server', 'ai_guidelines'],
required: true,
hint: 'Style guidelines are best for new projects',
);

// Verify we get keys that can be used with in_array checks
$this->assertIsArray($result);
$this->assertCount(3, $result); // All 3 selected (2 default + 1 added)
// Verify we get keys that can be used with in_array checks
expect($result)->toBeArray();
expect($result)->toHaveCount(3); // All 3 selected (2 default + 1 added)

// These are the exact checks used in InstallCommand
$this->assertTrue(in_array('mcp_server', $result, true));
$this->assertTrue(in_array('ai_guidelines', $result, true));
$this->assertTrue(in_array('style_guidelines', $result, true));
// These are the exact checks used in InstallCommand
expect(in_array('mcp_server', $result, true))->toBeTrue();
expect(in_array('ai_guidelines', $result, true))->toBeTrue();
expect(in_array('style_guidelines', $result, true))->toBeTrue();

// Verify it doesn't contain the display values
$this->assertFalse(in_array('Boost MCP Server', $result, true));
$this->assertFalse(in_array('Package AI Guidelines (i.e. Framework, Inertia, Pest)', $result, true));
}
}
// Verify it doesn't contain the display values
expect(in_array('Boost MCP Server', $result, true))->toBeFalse();
expect(in_array('Package AI Guidelines (i.e. Framework, Inertia, Pest)', $result, true))->toBeFalse();
})->skipOnWindows();
1 change: 1 addition & 0 deletions tests/Feature/Mcp/Tools/DatabaseSchemaTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
}

// Build a throw-away table that we expect in the dump.
Schema::dropIfExists('examples');
Schema::create('examples', function (Blueprint $table) {
$table->id();
$table->string('name');
Expand Down
4 changes: 2 additions & 2 deletions tests/Unit/Install/GuidelineWriterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@

expect(fn () => $writer->write('test guidelines'))
->toThrow(RuntimeException::class, 'Failed to create directory: /root/boost_test');
});
})->skipOnWindows();

test('it writes guidelines to new file', function () {
$tempFile = tempnam(sys_get_temp_dir(), 'boost_test_');
Expand Down Expand Up @@ -150,7 +150,7 @@

expect(fn () => $writer->write('test guidelines'))
->toThrow(RuntimeException::class, "Failed to open file: {$dirPath}");
});
})->skipOnWindows();

test('it preserves file content structure with proper spacing', function () {
$tempFile = tempnam(sys_get_temp_dir(), 'boost_test_');
Expand Down
Loading