638 lines
22 KiB
PHP
638 lines
22 KiB
PHP
<?php
|
|
|
|
/*
|
|
* This file is part of the official PHP MCP SDK.
|
|
*
|
|
* A collaboration between Symfony and the PHP Foundation.
|
|
*
|
|
* For the full copyright and license information, please view the LICENSE
|
|
* file that was distributed with this source code.
|
|
*/
|
|
|
|
namespace Mcp\Tests\Unit\Capability\Prompt;
|
|
|
|
use Mcp\Capability\Completion\EnumCompletionProvider;
|
|
use Mcp\Capability\Prompt\PromptGetter;
|
|
use Mcp\Capability\Registry\PromptReference;
|
|
use Mcp\Capability\Registry\ReferenceHandlerInterface;
|
|
use Mcp\Capability\Registry\ReferenceProviderInterface;
|
|
use Mcp\Exception\PromptGetException;
|
|
use Mcp\Exception\PromptNotFoundException;
|
|
use Mcp\Exception\RegistryException;
|
|
use Mcp\Schema\Content\PromptMessage;
|
|
use Mcp\Schema\Content\TextContent;
|
|
use Mcp\Schema\Enum\Role;
|
|
use Mcp\Schema\Prompt;
|
|
use Mcp\Schema\Request\GetPromptRequest;
|
|
use Mcp\Schema\Result\GetPromptResult;
|
|
use PHPUnit\Framework\MockObject\MockObject;
|
|
use PHPUnit\Framework\TestCase;
|
|
use Psr\Log\LoggerInterface;
|
|
|
|
class PromptGetterTest extends TestCase
|
|
{
|
|
private PromptGetter $promptGetter;
|
|
private ReferenceProviderInterface|MockObject $referenceProvider;
|
|
private ReferenceHandlerInterface|MockObject $referenceHandler;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
$this->referenceProvider = $this->createMock(ReferenceProviderInterface::class);
|
|
$this->referenceHandler = $this->createMock(ReferenceHandlerInterface::class);
|
|
|
|
$this->promptGetter = new PromptGetter(
|
|
$this->referenceProvider,
|
|
$this->referenceHandler,
|
|
);
|
|
}
|
|
|
|
public function testGetExecutesPromptSuccessfully(): void
|
|
{
|
|
$request = new GetPromptRequest('test_prompt', ['param' => 'value']);
|
|
$prompt = $this->createValidPrompt('test_prompt');
|
|
$promptReference = new PromptReference($prompt, fn () => 'test result');
|
|
$handlerResult = [
|
|
'role' => 'user',
|
|
'content' => 'Generated prompt content',
|
|
];
|
|
|
|
$this->referenceProvider
|
|
->expects($this->once())
|
|
->method('getPrompt')
|
|
->with('test_prompt')
|
|
->willReturn($promptReference);
|
|
|
|
$this->referenceHandler
|
|
->expects($this->once())
|
|
->method('handle')
|
|
->with($promptReference, ['param' => 'value'])
|
|
->willReturn($handlerResult);
|
|
|
|
$result = $this->promptGetter->get($request);
|
|
|
|
$this->assertInstanceOf(GetPromptResult::class, $result);
|
|
$this->assertCount(1, $result->messages);
|
|
$this->assertInstanceOf(PromptMessage::class, $result->messages[0]);
|
|
$this->assertEquals(Role::User, $result->messages[0]->role);
|
|
$this->assertInstanceOf(TextContent::class, $result->messages[0]->content);
|
|
$this->assertEquals('Generated prompt content', $result->messages[0]->content->text);
|
|
}
|
|
|
|
public function testGetWithEmptyArguments(): void
|
|
{
|
|
$request = new GetPromptRequest('empty_args_prompt', []);
|
|
$prompt = $this->createValidPrompt('empty_args_prompt');
|
|
$promptReference = new PromptReference($prompt, fn () => 'Empty args content');
|
|
|
|
$this->referenceProvider
|
|
->expects($this->once())
|
|
->method('getPrompt')
|
|
->with('empty_args_prompt')
|
|
->willReturn($promptReference);
|
|
|
|
$this->referenceHandler
|
|
->expects($this->once())
|
|
->method('handle')
|
|
->with($promptReference, [])
|
|
->willReturn([
|
|
'role' => 'user',
|
|
'content' => 'Empty args content',
|
|
]);
|
|
|
|
$result = $this->promptGetter->get($request);
|
|
|
|
$this->assertInstanceOf(GetPromptResult::class, $result);
|
|
$this->assertCount(1, $result->messages);
|
|
}
|
|
|
|
public function testGetWithComplexArguments(): void
|
|
{
|
|
$arguments = [
|
|
'string_param' => 'value',
|
|
'int_param' => 42,
|
|
'bool_param' => true,
|
|
'array_param' => ['nested' => 'data'],
|
|
'null_param' => null,
|
|
];
|
|
$request = new GetPromptRequest('complex_prompt', $arguments);
|
|
$prompt = $this->createValidPrompt('complex_prompt');
|
|
$promptReference = new PromptReference($prompt, fn () => 'Complex content');
|
|
|
|
$this->referenceProvider
|
|
->expects($this->once())
|
|
->method('getPrompt')
|
|
->with('complex_prompt')
|
|
->willReturn($promptReference);
|
|
|
|
$this->referenceHandler
|
|
->expects($this->once())
|
|
->method('handle')
|
|
->with($promptReference, $arguments)
|
|
->willReturn([
|
|
'role' => 'assistant',
|
|
'content' => 'Complex response',
|
|
]);
|
|
|
|
$result = $this->promptGetter->get($request);
|
|
|
|
$this->assertInstanceOf(GetPromptResult::class, $result);
|
|
$this->assertCount(1, $result->messages);
|
|
}
|
|
|
|
public function testGetThrowsInvalidArgumentExceptionWhenPromptNotFound(): void
|
|
{
|
|
$request = new GetPromptRequest('nonexistent_prompt', ['param' => 'value']);
|
|
|
|
$this->referenceProvider
|
|
->expects($this->once())
|
|
->method('getPrompt')
|
|
->with('nonexistent_prompt')
|
|
->willReturn(null);
|
|
|
|
$this->referenceHandler
|
|
->expects($this->never())
|
|
->method('handle');
|
|
|
|
$this->expectException(PromptNotFoundException::class);
|
|
$this->expectExceptionMessage('Prompt not found for name: "nonexistent_prompt".');
|
|
|
|
$this->promptGetter->get($request);
|
|
}
|
|
|
|
public function testGetThrowsRegistryExceptionWhenHandlerFails(): void
|
|
{
|
|
$request = new GetPromptRequest('failing_prompt', ['param' => 'value']);
|
|
$prompt = $this->createValidPrompt('failing_prompt');
|
|
$promptReference = new PromptReference($prompt, fn () => throw new \RuntimeException('Handler failed'));
|
|
$handlerException = RegistryException::internalError('Handler failed');
|
|
|
|
$this->referenceProvider
|
|
->expects($this->once())
|
|
->method('getPrompt')
|
|
->with('failing_prompt')
|
|
->willReturn($promptReference);
|
|
|
|
$this->referenceHandler
|
|
->expects($this->once())
|
|
->method('handle')
|
|
->with($promptReference, ['param' => 'value'])
|
|
->willThrowException($handlerException);
|
|
|
|
$this->expectException(PromptGetException::class);
|
|
|
|
$this->promptGetter->get($request);
|
|
}
|
|
|
|
public function testGetHandlesJsonExceptionDuringFormatting(): void
|
|
{
|
|
$request = new GetPromptRequest('json_error_prompt', []);
|
|
$prompt = $this->createValidPrompt('json_error_prompt');
|
|
|
|
// Create a mock PromptReference that will throw JsonException during formatResult
|
|
$promptReference = $this->createMock(PromptReference::class);
|
|
$promptReference->expects($this->once())
|
|
->method('formatResult')
|
|
->willThrowException(new \JsonException('JSON encoding failed'));
|
|
|
|
$this->referenceProvider
|
|
->expects($this->once())
|
|
->method('getPrompt')
|
|
->with('json_error_prompt')
|
|
->willReturn($promptReference);
|
|
|
|
$this->referenceHandler
|
|
->expects($this->once())
|
|
->method('handle')
|
|
->with($promptReference, [])
|
|
->willReturn('some result');
|
|
|
|
$this->expectException(PromptGetException::class);
|
|
$this->expectExceptionMessage('JSON encoding failed');
|
|
|
|
$this->promptGetter->get($request);
|
|
}
|
|
|
|
public function testGetHandlesArrayOfMessages(): void
|
|
{
|
|
$request = new GetPromptRequest('multi_message_prompt', ['context' => 'test']);
|
|
$prompt = $this->createValidPrompt('multi_message_prompt');
|
|
$promptReference = new PromptReference($prompt, fn () => 'Multiple messages');
|
|
$handlerResult = [
|
|
[
|
|
'role' => 'user',
|
|
'content' => 'First message',
|
|
],
|
|
[
|
|
'role' => 'assistant',
|
|
'content' => 'Second message',
|
|
],
|
|
];
|
|
|
|
$this->referenceProvider
|
|
->expects($this->once())
|
|
->method('getPrompt')
|
|
->with('multi_message_prompt')
|
|
->willReturn($promptReference);
|
|
|
|
$this->referenceHandler
|
|
->expects($this->once())
|
|
->method('handle')
|
|
->with($promptReference, ['context' => 'test'])
|
|
->willReturn($handlerResult);
|
|
|
|
$result = $this->promptGetter->get($request);
|
|
|
|
$this->assertInstanceOf(GetPromptResult::class, $result);
|
|
$this->assertCount(2, $result->messages);
|
|
$this->assertEquals(Role::User, $result->messages[0]->role);
|
|
$this->assertEquals('First message', $result->messages[0]->content->text);
|
|
$this->assertEquals(Role::Assistant, $result->messages[1]->role);
|
|
$this->assertEquals('Second message', $result->messages[1]->content->text);
|
|
}
|
|
|
|
public function testGetHandlesPromptMessageObjects(): void
|
|
{
|
|
$request = new GetPromptRequest('prompt_message_prompt', []);
|
|
$prompt = $this->createValidPrompt('prompt_message_prompt');
|
|
$promptMessage = new PromptMessage(
|
|
Role::User,
|
|
new TextContent('Direct prompt message')
|
|
);
|
|
$promptReference = new PromptReference($prompt, fn () => $promptMessage);
|
|
|
|
$this->referenceProvider
|
|
->expects($this->once())
|
|
->method('getPrompt')
|
|
->with('prompt_message_prompt')
|
|
->willReturn($promptReference);
|
|
|
|
$this->referenceHandler
|
|
->expects($this->once())
|
|
->method('handle')
|
|
->with($promptReference, [])
|
|
->willReturn($promptMessage);
|
|
|
|
$result = $this->promptGetter->get($request);
|
|
|
|
$this->assertInstanceOf(GetPromptResult::class, $result);
|
|
$this->assertCount(1, $result->messages);
|
|
$this->assertSame($promptMessage, $result->messages[0]);
|
|
}
|
|
|
|
public function testGetHandlesUserAssistantStructure(): void
|
|
{
|
|
$request = new GetPromptRequest('user_assistant_prompt', []);
|
|
$prompt = $this->createValidPrompt('user_assistant_prompt');
|
|
$promptReference = new PromptReference($prompt, fn () => 'Conversation content');
|
|
$handlerResult = [
|
|
'user' => 'What is the weather?',
|
|
'assistant' => 'I can help you check the weather.',
|
|
];
|
|
|
|
$this->referenceProvider
|
|
->expects($this->once())
|
|
->method('getPrompt')
|
|
->with('user_assistant_prompt')
|
|
->willReturn($promptReference);
|
|
|
|
$this->referenceHandler
|
|
->expects($this->once())
|
|
->method('handle')
|
|
->with($promptReference, [])
|
|
->willReturn($handlerResult);
|
|
|
|
$result = $this->promptGetter->get($request);
|
|
|
|
$this->assertInstanceOf(GetPromptResult::class, $result);
|
|
$this->assertCount(2, $result->messages);
|
|
$this->assertEquals(Role::User, $result->messages[0]->role);
|
|
$this->assertEquals('What is the weather?', $result->messages[0]->content->text);
|
|
$this->assertEquals(Role::Assistant, $result->messages[1]->role);
|
|
$this->assertEquals('I can help you check the weather.', $result->messages[1]->content->text);
|
|
}
|
|
|
|
public function testGetHandlesEmptyArrayResult(): void
|
|
{
|
|
$request = new GetPromptRequest('empty_array_prompt', []);
|
|
$prompt = $this->createValidPrompt('empty_array_prompt');
|
|
$promptReference = new PromptReference($prompt, fn () => []);
|
|
|
|
$this->referenceProvider
|
|
->expects($this->once())
|
|
->method('getPrompt')
|
|
->with('empty_array_prompt')
|
|
->willReturn($promptReference);
|
|
|
|
$this->referenceHandler
|
|
->expects($this->once())
|
|
->method('handle')
|
|
->with($promptReference, [])
|
|
->willReturn([]);
|
|
|
|
$result = $this->promptGetter->get($request);
|
|
|
|
$this->assertInstanceOf(GetPromptResult::class, $result);
|
|
$this->assertCount(0, $result->messages);
|
|
}
|
|
|
|
public function testGetWithTypedContentStructure(): void
|
|
{
|
|
$request = new GetPromptRequest('typed_content_prompt', []);
|
|
$prompt = $this->createValidPrompt('typed_content_prompt');
|
|
$promptReference = new PromptReference($prompt, fn () => 'Typed content');
|
|
$handlerResult = [
|
|
'role' => 'user',
|
|
'content' => [
|
|
'type' => 'text',
|
|
'text' => 'Typed text content',
|
|
],
|
|
];
|
|
|
|
$this->referenceProvider
|
|
->expects($this->once())
|
|
->method('getPrompt')
|
|
->with('typed_content_prompt')
|
|
->willReturn($promptReference);
|
|
|
|
$this->referenceHandler
|
|
->expects($this->once())
|
|
->method('handle')
|
|
->with($promptReference, [])
|
|
->willReturn($handlerResult);
|
|
|
|
$result = $this->promptGetter->get($request);
|
|
|
|
$this->assertInstanceOf(GetPromptResult::class, $result);
|
|
$this->assertCount(1, $result->messages);
|
|
$this->assertEquals(Role::User, $result->messages[0]->role);
|
|
$this->assertEquals('Typed text content', $result->messages[0]->content->text);
|
|
}
|
|
|
|
public function testGetWithPromptReferenceHavingCompletionProviders(): void
|
|
{
|
|
$request = new GetPromptRequest('completion_prompt', ['param' => 'value']);
|
|
$prompt = $this->createValidPrompt('completion_prompt');
|
|
$completionProviders = ['param' => EnumCompletionProvider::class];
|
|
$promptReference = new PromptReference(
|
|
$prompt,
|
|
fn () => 'Completion content',
|
|
false,
|
|
$completionProviders
|
|
);
|
|
|
|
$this->referenceProvider
|
|
->expects($this->once())
|
|
->method('getPrompt')
|
|
->with('completion_prompt')
|
|
->willReturn($promptReference);
|
|
|
|
$this->referenceHandler
|
|
->expects($this->once())
|
|
->method('handle')
|
|
->with($promptReference, ['param' => 'value'])
|
|
->willReturn([
|
|
'role' => 'user',
|
|
'content' => 'Completion content',
|
|
]);
|
|
|
|
$result = $this->promptGetter->get($request);
|
|
|
|
$this->assertInstanceOf(GetPromptResult::class, $result);
|
|
$this->assertCount(1, $result->messages);
|
|
}
|
|
|
|
public function testGetHandlesMixedMessageArray(): void
|
|
{
|
|
$request = new GetPromptRequest('mixed_prompt', []);
|
|
$prompt = $this->createValidPrompt('mixed_prompt');
|
|
$promptMessage = new PromptMessage(Role::Assistant, new TextContent('Direct message'));
|
|
$promptReference = new PromptReference($prompt, fn () => 'Mixed content');
|
|
$handlerResult = [
|
|
$promptMessage,
|
|
[
|
|
'role' => 'user',
|
|
'content' => 'Regular message',
|
|
],
|
|
];
|
|
|
|
$this->referenceProvider
|
|
->expects($this->once())
|
|
->method('getPrompt')
|
|
->with('mixed_prompt')
|
|
->willReturn($promptReference);
|
|
|
|
$this->referenceHandler
|
|
->expects($this->once())
|
|
->method('handle')
|
|
->with($promptReference, [])
|
|
->willReturn($handlerResult);
|
|
|
|
$result = $this->promptGetter->get($request);
|
|
|
|
$this->assertInstanceOf(GetPromptResult::class, $result);
|
|
$this->assertCount(2, $result->messages);
|
|
$this->assertSame($promptMessage, $result->messages[0]);
|
|
$this->assertEquals(Role::User, $result->messages[1]->role);
|
|
$this->assertEquals('Regular message', $result->messages[1]->content->text);
|
|
}
|
|
|
|
public function testGetReflectsFormattedMessagesCorrectly(): void
|
|
{
|
|
$request = new GetPromptRequest('format_test_prompt', []);
|
|
$prompt = $this->createValidPrompt('format_test_prompt');
|
|
$promptReference = new PromptReference($prompt, fn () => 'Format test');
|
|
|
|
// Test that the formatted result from PromptReference.formatResult is properly returned
|
|
$handlerResult = [
|
|
'role' => 'user',
|
|
'content' => 'Test formatting',
|
|
];
|
|
|
|
$this->referenceProvider
|
|
->expects($this->once())
|
|
->method('getPrompt')
|
|
->with('format_test_prompt')
|
|
->willReturn($promptReference);
|
|
|
|
$this->referenceHandler
|
|
->expects($this->once())
|
|
->method('handle')
|
|
->with($promptReference, [])
|
|
->willReturn($handlerResult);
|
|
|
|
$result = $this->promptGetter->get($request);
|
|
|
|
$this->assertInstanceOf(GetPromptResult::class, $result);
|
|
$this->assertCount(1, $result->messages);
|
|
$this->assertEquals('Test formatting', $result->messages[0]->content->text);
|
|
$this->assertEquals(Role::User, $result->messages[0]->role);
|
|
}
|
|
|
|
/**
|
|
* Test that invalid handler results throw RuntimeException from PromptReference.formatResult().
|
|
*/
|
|
public function testGetThrowsRuntimeExceptionForInvalidHandlerResult(): void
|
|
{
|
|
$request = new GetPromptRequest('invalid_prompt', []);
|
|
$prompt = $this->createValidPrompt('invalid_prompt');
|
|
$promptReference = new PromptReference($prompt, fn () => 'Invalid content');
|
|
|
|
$this->referenceProvider
|
|
->expects($this->once())
|
|
->method('getPrompt')
|
|
->with('invalid_prompt')
|
|
->willReturn($promptReference);
|
|
|
|
$this->referenceHandler
|
|
->expects($this->once())
|
|
->method('handle')
|
|
->with($promptReference, [])
|
|
->willReturn('This is not a valid prompt format');
|
|
|
|
$this->expectException(PromptGetException::class);
|
|
$this->expectExceptionMessage('Prompt generator method must return an array of messages.');
|
|
|
|
$this->promptGetter->get($request);
|
|
}
|
|
|
|
/**
|
|
* Test that null result from handler throws RuntimeException.
|
|
*/
|
|
public function testGetThrowsRuntimeExceptionForNullHandlerResult(): void
|
|
{
|
|
$request = new GetPromptRequest('null_prompt', []);
|
|
$prompt = $this->createValidPrompt('null_prompt');
|
|
$promptReference = new PromptReference($prompt, fn () => null);
|
|
|
|
$this->referenceProvider
|
|
->expects($this->once())
|
|
->method('getPrompt')
|
|
->with('null_prompt')
|
|
->willReturn($promptReference);
|
|
|
|
$this->referenceHandler
|
|
->expects($this->once())
|
|
->method('handle')
|
|
->with($promptReference, [])
|
|
->willReturn(null);
|
|
|
|
$this->expectException(PromptGetException::class);
|
|
$this->expectExceptionMessage('Prompt generator method must return an array of messages.');
|
|
|
|
$this->promptGetter->get($request);
|
|
}
|
|
|
|
/**
|
|
* Test that scalar result from handler throws RuntimeException.
|
|
*/
|
|
public function testGetThrowsRuntimeExceptionForScalarHandlerResult(): void
|
|
{
|
|
$request = new GetPromptRequest('scalar_prompt', []);
|
|
$prompt = $this->createValidPrompt('scalar_prompt');
|
|
$promptReference = new PromptReference($prompt, fn () => 42);
|
|
|
|
$this->referenceProvider
|
|
->expects($this->once())
|
|
->method('getPrompt')
|
|
->with('scalar_prompt')
|
|
->willReturn($promptReference);
|
|
|
|
$this->referenceHandler
|
|
->expects($this->once())
|
|
->method('handle')
|
|
->with($promptReference, [])
|
|
->willReturn(42);
|
|
|
|
$this->expectException(PromptGetException::class);
|
|
$this->expectExceptionMessage('Prompt generator method must return an array of messages.');
|
|
|
|
$this->promptGetter->get($request);
|
|
}
|
|
|
|
/**
|
|
* Test that boolean result from handler throws RuntimeException.
|
|
*/
|
|
public function testGetThrowsRuntimeExceptionForBooleanHandlerResult(): void
|
|
{
|
|
$request = new GetPromptRequest('boolean_prompt', []);
|
|
$prompt = $this->createValidPrompt('boolean_prompt');
|
|
$promptReference = new PromptReference($prompt, fn () => true);
|
|
|
|
$this->referenceProvider
|
|
->expects($this->once())
|
|
->method('getPrompt')
|
|
->with('boolean_prompt')
|
|
->willReturn($promptReference);
|
|
|
|
$this->referenceHandler
|
|
->expects($this->once())
|
|
->method('handle')
|
|
->with($promptReference, [])
|
|
->willReturn(true);
|
|
|
|
$this->expectException(PromptGetException::class);
|
|
$this->expectExceptionMessage('Prompt generator method must return an array of messages.');
|
|
|
|
$this->promptGetter->get($request);
|
|
}
|
|
|
|
/**
|
|
* Test that object result from handler throws RuntimeException.
|
|
*/
|
|
public function testGetThrowsRuntimeExceptionForObjectHandlerResult(): void
|
|
{
|
|
$request = new GetPromptRequest('object_prompt', []);
|
|
$prompt = $this->createValidPrompt('object_prompt');
|
|
$objectResult = new \stdClass();
|
|
$objectResult->property = 'value';
|
|
$promptReference = new PromptReference($prompt, fn () => $objectResult);
|
|
|
|
$this->referenceProvider
|
|
->expects($this->once())
|
|
->method('getPrompt')
|
|
->with('object_prompt')
|
|
->willReturn($promptReference);
|
|
|
|
$this->referenceHandler
|
|
->expects($this->once())
|
|
->method('handle')
|
|
->with($promptReference, [])
|
|
->willReturn($objectResult);
|
|
|
|
$this->expectException(PromptGetException::class);
|
|
$this->expectExceptionMessage('Prompt generator method must return an array of messages.');
|
|
|
|
$this->promptGetter->get($request);
|
|
}
|
|
|
|
public function testConstructorWithDefaultLogger(): void
|
|
{
|
|
$promptGetter = new PromptGetter(
|
|
$this->referenceProvider,
|
|
$this->referenceHandler,
|
|
);
|
|
|
|
$this->assertInstanceOf(PromptGetter::class, $promptGetter);
|
|
}
|
|
|
|
public function testConstructorWithCustomLogger(): void
|
|
{
|
|
$logger = $this->createMock(LoggerInterface::class);
|
|
$promptGetter = new PromptGetter(
|
|
$this->referenceProvider,
|
|
$this->referenceHandler,
|
|
$logger,
|
|
);
|
|
|
|
$this->assertInstanceOf(PromptGetter::class, $promptGetter);
|
|
}
|
|
|
|
private function createValidPrompt(string $name): Prompt
|
|
{
|
|
return new Prompt(
|
|
name: $name,
|
|
description: "Test prompt: {$name}",
|
|
arguments: null,
|
|
);
|
|
}
|
|
}
|