Skip to content

Commit 6b990fc

Browse files
authored
Merge pull request #25 from laravel/fix/failing_test_for_search_docs
Refactor SearchDocs tests to use Http facade
2 parents a672f48 + b2acd6e commit 6b990fc

File tree

7 files changed

+82
-125
lines changed

7 files changed

+82
-125
lines changed

.github/workflows/tests.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,13 @@ jobs:
7474
tools: composer:v2
7575
coverage: none
7676

77+
- name: Setup SSH Keys
78+
run: |
79+
mkdir -p ~/.ssh
80+
echo "${{ secrets.MCP_DEPLOY_KEY }}" > ~/.ssh/id_rsa
81+
chmod 600 ~/.ssh/id_rsa
82+
ssh-keyscan github.com >> ~/.ssh/known_hosts
83+
7784
- name: Install dependencies
7885
run: |
7986
composer update --prefer-dist --no-interaction --no-progress --with="illuminate/contracts:^${{ matrix.laravel }}"

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
],
2121
"require": {
2222
"php": "^8.1|^8.2",
23+
"guzzlehttp/guzzle": "^7.9",
2324
"illuminate/console": "^10.0|^11.0|^12.0",
2425
"illuminate/contracts": "^10.0|^11.0|^12.0",
2526
"illuminate/routing": "^10.0|^11.0|^12.0",

src/BoostServiceProvider.php

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -126,9 +126,6 @@ private function registerRoutes(): void
126126
/**
127127
* Build a string message for the log based on various input types. Single-dimensional, and multi:
128128
* "data": {"message":"Unhandled Promise Rejection","reason":{"name":"TypeError","message":"NetworkError when attempting to fetch resource.","stack":""}}]
129-
*
130-
* @param array $data
131-
* @return string
132129
*/
133130
private function buildLogMessageFromData(array $data): string
134131
{
@@ -141,6 +138,7 @@ private function buildLogMessageFromData(array $data): string
141138
is_bool($value) => $value ? 'true' : 'false',
142139
is_null($value) => 'null',
143140
is_object($value) => json_encode($value),
141+
default => $value,
144142
};
145143
}
146144

src/Concerns/MakesHttpRequests.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ public function client(): PendingRequest
1616
'User-Agent' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:140.0) Gecko/20100101 Firefox/140.0 Laravel Boost',
1717
]);
1818

19-
// Disable SSL verification for local development URLs
20-
if (app()->environment('local') || str_contains(config('boost.hosted.api_url', ''), '.test')) {
19+
// Disable SSL verification for local development URLs and testing
20+
if (app()->environment(['local', 'testing']) || str_contains(config('boost.hosted.api_url', ''), '.test')) {
2121
$client = $client->withoutVerifying();
2222
}
2323

src/Install/Contracts/DetectionStrategy.php

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,7 @@ interface DetectionStrategy
1111
/**
1212
* Detect if the application is installed on the machine.
1313
*
14-
* @param array{command:string, ?files:array<string>, ?paths:array<string>} $config
15-
* @param ?Platform $platform
16-
* @return bool
14+
* @param array{command?:string, basePath?:string, files?:array<string>, paths?:array<string>} $config
1715
*/
1816
public function detect(array $config, ?Platform $platform = null): bool;
1917
}

src/Middleware/InjectBoost.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ public function handle(Request $request, Closure $next): Response
2222
$injectedContent = $this->injectScript($response->getContent());
2323
$response->setContent($injectedContent);
2424

25-
if ($originalView instanceof View) {
25+
if ($originalView instanceof View && property_exists($response, 'original')) {
2626
$response->original = $originalView;
2727
}
2828
}

tests/Feature/Mcp/Tools/SearchDocsTest.php

Lines changed: 69 additions & 116 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@
22

33
declare(strict_types=1);
44

5-
use Illuminate\Http\Client\PendingRequest;
6-
use Illuminate\Http\Client\Response;
5+
use Illuminate\Support\Facades\Http;
76
use Laravel\Boost\Mcp\Tools\SearchDocs;
87
use Laravel\Mcp\Server\Tools\ToolResult;
98
use Laravel\Roster\Enums\Packages;
@@ -20,34 +19,29 @@
2019
$roster = Mockery::mock(Roster::class);
2120
$roster->shouldReceive('packages')->andReturn($packages);
2221

23-
$mockResponse = Mockery::mock(Response::class);
24-
$mockResponse->shouldReceive('successful')->andReturn(true);
25-
$mockResponse->shouldReceive('json')->andReturn([
26-
'results' => [
27-
['content' => 'Laravel documentation content'],
28-
['content' => 'Pest documentation content'],
29-
],
22+
Http::fake([
23+
'https://boost.laravel.com/api/docs' => Http::response('Documentation search results', 200),
3024
]);
3125

32-
$mockClient = Mockery::mock(PendingRequest::class);
33-
$mockClient->shouldReceive('asJson')->andReturnSelf();
34-
$mockClient->shouldReceive('post')->andReturn($mockResponse);
35-
36-
$tool = Mockery::mock(SearchDocs::class, [$roster])->makePartial();
37-
$tool->shouldReceive('client')->andReturn($mockClient);
38-
39-
$result = $tool->handle(['queries' => 'authentication, testing']);
26+
$tool = new SearchDocs($roster);
27+
$result = $tool->handle(['queries' => 'authentication###testing']);
4028

4129
expect($result)->toBeInstanceOf(ToolResult::class);
4230

4331
$data = $result->toArray();
44-
expect($data['isError'])->toBeFalse();
45-
46-
$content = json_decode($data['content'][0]['text'], true);
47-
expect($content['knowledge_count'])->toBe(2);
48-
expect($content['knowledge'])->toContain('Laravel documentation content');
49-
expect($content['knowledge'])->toContain('Pest documentation content');
50-
expect($content['knowledge'])->toContain('---');
32+
expect($data['isError'])->toBeFalse()
33+
->and($data['content'][0]['text'])->toBe('Documentation search results');
34+
35+
Http::assertSent(function ($request) {
36+
return $request->url() === 'https://boost.laravel.com/api/docs' &&
37+
$request->data()['queries'] === ['authentication', 'testing'] &&
38+
$request->data()['packages'] === [
39+
['name' => 'laravel/framework', 'version' => '11.x'],
40+
['name' => 'pestphp/pest', 'version' => '2.x'],
41+
] &&
42+
$request->data()['token_limit'] === 10000 &&
43+
$request->data()['format'] === 'markdown';
44+
});
5145
});
5246

5347
test('it handles API error response', function () {
@@ -58,25 +52,18 @@
5852
$roster = Mockery::mock(Roster::class);
5953
$roster->shouldReceive('packages')->andReturn($packages);
6054

61-
$mockResponse = Mockery::mock(Response::class);
62-
$mockResponse->shouldReceive('successful')->andReturn(false);
63-
$mockResponse->shouldReceive('status')->andReturn(500);
64-
$mockResponse->shouldReceive('body')->andReturn('API Error');
65-
66-
$mockClient = Mockery::mock(PendingRequest::class);
67-
$mockClient->shouldReceive('asJson')->andReturnSelf();
68-
$mockClient->shouldReceive('post')->andReturn($mockResponse);
69-
70-
$tool = Mockery::mock(SearchDocs::class, [$roster])->makePartial();
71-
$tool->shouldReceive('client')->andReturn($mockClient);
55+
Http::fake([
56+
'https://boost.laravel.com/api/docs' => Http::response('API Error', 500),
57+
]);
7258

59+
$tool = new SearchDocs($roster);
7360
$result = $tool->handle(['queries' => 'authentication']);
7461

7562
expect($result)->toBeInstanceOf(ToolResult::class);
7663

7764
$data = $result->toArray();
78-
expect($data['isError'])->toBeTrue();
79-
expect($data['content'][0]['text'])->toBe('Failed to search documentation: API Error');
65+
expect($data['isError'])->toBeTrue()
66+
->and($data['content'][0]['text'])->toBe('Failed to search documentation: API Error');
8067
});
8168

8269
test('it filters empty queries', function () {
@@ -85,28 +72,24 @@
8572
$roster = Mockery::mock(Roster::class);
8673
$roster->shouldReceive('packages')->andReturn($packages);
8774

88-
$mockResponse = Mockery::mock(Response::class);
89-
$mockResponse->shouldReceive('successful')->andReturn(true);
90-
$mockResponse->shouldReceive('json')->andReturn(['results' => []]);
91-
92-
$mockClient = Mockery::mock(PendingRequest::class);
93-
$mockClient->shouldReceive('asJson')->andReturnSelf();
94-
$mockClient->shouldReceive('post')->withArgs(function ($url, $payload) {
95-
return $url === 'https://boost.laravel.com/api/docs' &&
96-
$payload['queries'] === ['test'] &&
97-
empty($payload['packages']) &&
98-
$payload['token_limit'] === 10000;
99-
})->andReturn($mockResponse);
100-
101-
$tool = Mockery::mock(SearchDocs::class, [$roster])->makePartial();
102-
$tool->shouldReceive('client')->andReturn($mockClient);
75+
Http::fake([
76+
'https://boost.laravel.com/api/docs' => Http::response('Empty results', 200),
77+
]);
10378

79+
$tool = new SearchDocs($roster);
10480
$result = $tool->handle(['queries' => 'test### ###*### ']);
10581

10682
expect($result)->toBeInstanceOf(ToolResult::class);
10783

10884
$data = $result->toArray();
10985
expect($data['isError'])->toBeFalse();
86+
87+
Http::assertSent(function ($request) {
88+
return $request->url() === 'https://boost.laravel.com/api/docs' &&
89+
$request->data()['queries'] === ['test'] &&
90+
empty($request->data()['packages']) &&
91+
$request->data()['token_limit'] === 10000;
92+
});
11093
});
11194

11295
test('it formats package data correctly', function () {
@@ -118,28 +101,21 @@
118101
$roster = Mockery::mock(Roster::class);
119102
$roster->shouldReceive('packages')->andReturn($packages);
120103

121-
$mockResponse = Mockery::mock(Response::class);
122-
$mockResponse->shouldReceive('successful')->andReturn(true);
123-
$mockResponse->shouldReceive('json')->andReturn(['results' => []]);
124-
125-
$mockClient = Mockery::mock(PendingRequest::class);
126-
$mockClient->shouldReceive('asJson')->andReturnSelf();
127-
$mockClient->shouldReceive('post')->with(
128-
'https://boost.laravel.com/api/docs',
129-
Mockery::on(function ($payload) {
130-
return $payload['packages'] === [
131-
['name' => 'laravel/framework', 'version' => '11.x'],
132-
['name' => 'livewire/livewire', 'version' => '3.x'],
133-
] && $payload['token_limit'] === 10000;
134-
})
135-
)->andReturn($mockResponse);
136-
137-
$tool = Mockery::mock(SearchDocs::class, [$roster])->makePartial();
138-
$tool->shouldReceive('client')->andReturn($mockClient);
104+
Http::fake([
105+
'https://boost.laravel.com/api/docs' => Http::response('Package data results', 200),
106+
]);
139107

108+
$tool = new SearchDocs($roster);
140109
$result = $tool->handle(['queries' => 'test']);
141110

142111
expect($result)->toBeInstanceOf(ToolResult::class);
112+
113+
Http::assertSent(function ($request) {
114+
return $request->data()['packages'] === [
115+
['name' => 'laravel/framework', 'version' => '11.x'],
116+
['name' => 'livewire/livewire', 'version' => '3.x'],
117+
] && $request->data()['token_limit'] === 10000;
118+
});
143119
});
144120

145121
test('it handles empty results', function () {
@@ -148,27 +124,18 @@
148124
$roster = Mockery::mock(Roster::class);
149125
$roster->shouldReceive('packages')->andReturn($packages);
150126

151-
$mockResponse = Mockery::mock(Response::class);
152-
$mockResponse->shouldReceive('successful')->andReturn(true);
153-
$mockResponse->shouldReceive('json')->andReturn(['results' => []]);
154-
155-
$mockClient = Mockery::mock(PendingRequest::class);
156-
$mockClient->shouldReceive('asJson')->andReturnSelf();
157-
$mockClient->shouldReceive('post')->andReturn($mockResponse);
158-
159-
$tool = Mockery::mock(SearchDocs::class, [$roster])->makePartial();
160-
$tool->shouldReceive('client')->andReturn($mockClient);
127+
Http::fake([
128+
'https://boost.laravel.com/api/docs' => Http::response('Empty response', 200),
129+
]);
161130

131+
$tool = new SearchDocs($roster);
162132
$result = $tool->handle(['queries' => 'nonexistent']);
163133

164134
expect($result)->toBeInstanceOf(ToolResult::class);
165135

166136
$data = $result->toArray();
167-
expect($data['isError'])->toBeFalse();
168-
169-
$content = json_decode($data['content'][0]['text'], true);
170-
expect($content['knowledge_count'])->toBe(0);
171-
expect($content['knowledge'])->toBe('');
137+
expect($data['isError'])->toBeFalse()
138+
->and($data['content'][0]['text'])->toBe('Empty response');
172139
});
173140

174141
test('it uses custom token_limit when provided', function () {
@@ -177,25 +144,18 @@
177144
$roster = Mockery::mock(Roster::class);
178145
$roster->shouldReceive('packages')->andReturn($packages);
179146

180-
$mockResponse = Mockery::mock(Response::class);
181-
$mockResponse->shouldReceive('successful')->andReturn(true);
182-
$mockResponse->shouldReceive('json')->andReturn(['results' => []]);
183-
184-
$mockClient = Mockery::mock(PendingRequest::class);
185-
$mockClient->shouldReceive('asJson')->andReturnSelf();
186-
$mockClient->shouldReceive('post')->with(
187-
'https://boost.laravel.com/api/docs',
188-
Mockery::on(function ($payload) {
189-
return $payload['token_limit'] === 5000;
190-
})
191-
)->andReturn($mockResponse);
192-
193-
$tool = Mockery::mock(SearchDocs::class, [$roster])->makePartial();
194-
$tool->shouldReceive('client')->andReturn($mockClient);
147+
Http::fake([
148+
'https://boost.laravel.com/api/docs' => Http::response('Custom token limit results', 200),
149+
]);
195150

151+
$tool = new SearchDocs($roster);
196152
$result = $tool->handle(['queries' => 'test', 'token_limit' => 5000]);
197153

198154
expect($result)->toBeInstanceOf(ToolResult::class);
155+
156+
Http::assertSent(function ($request) {
157+
return $request->data()['token_limit'] === 5000;
158+
});
199159
});
200160

201161
test('it caps token_limit at maximum of 1000000', function () {
@@ -204,23 +164,16 @@
204164
$roster = Mockery::mock(Roster::class);
205165
$roster->shouldReceive('packages')->andReturn($packages);
206166

207-
$mockResponse = Mockery::mock(Response::class);
208-
$mockResponse->shouldReceive('successful')->andReturn(true);
209-
$mockResponse->shouldReceive('json')->andReturn(['results' => []]);
210-
211-
$mockClient = Mockery::mock(PendingRequest::class);
212-
$mockClient->shouldReceive('asJson')->andReturnSelf();
213-
$mockClient->shouldReceive('post')->with(
214-
'https://boost.laravel.com/api/docs',
215-
Mockery::on(function ($payload) {
216-
return $payload['token_limit'] === 1000000; // Should be capped at 1M
217-
})
218-
)->andReturn($mockResponse);
219-
220-
$tool = Mockery::mock(SearchDocs::class, [$roster])->makePartial();
221-
$tool->shouldReceive('client')->andReturn($mockClient);
167+
Http::fake([
168+
'https://boost.laravel.com/api/docs' => Http::response('Capped token limit results', 200),
169+
]);
222170

223-
$result = $tool->handle(['queries' => 'test', 'token_limit' => 2000000]); // Request 2M but get capped at 1M
171+
$tool = new SearchDocs($roster);
172+
$result = $tool->handle(['queries' => 'test', 'token_limit' => 2000000]);
224173

225174
expect($result)->toBeInstanceOf(ToolResult::class);
175+
176+
Http::assertSent(function ($request) {
177+
return $request->data()['token_limit'] === 1000000;
178+
});
226179
});

0 commit comments

Comments
 (0)