diff --git a/phpunit.xml.dist b/phpunit.xml.dist index d5adf9e..e18fb6d 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -12,8 +12,8 @@ tests/Unit - - tests/Inspector + + tests/Application diff --git a/tests/Application/ApplicationTestCase.php b/tests/Application/ApplicationTestCase.php new file mode 100644 index 0000000..5f2da9f --- /dev/null +++ b/tests/Application/ApplicationTestCase.php @@ -0,0 +1,172 @@ + $messages + * @param array $env + * + * @return array>> + */ + protected function runServer(array $messages, float $timeout = 5.0, array $env = []): array + { + if (0 === \count($messages)) { + return []; + } + + $process = new Process([ + 'php', + $this->getServerScript(), + ], \dirname(__DIR__, 2), [] === $env ? null : $env, null, $timeout); + + $process->setInput($this->formatInput($messages)); + $process->mustRun(); + + return $this->decodeJsonLines($process->getOutput()); + } + + abstract protected function getServerScript(): string; + + /** + * @param mixed[] $params + * + * @throws \JsonException + */ + protected function jsonRequest(string $method, ?array $params = null, ?string $id = null): string + { + $payload = [ + 'jsonrpc' => MessageInterface::JSONRPC_VERSION, + 'id' => $id, + 'method' => $method, + ]; + + if (null !== $params) { + $payload['params'] = $params; + } + + return (string) json_encode($payload, \JSON_THROW_ON_ERROR); + } + + /** + * @param list $messages + */ + private function formatInput(array $messages): string + { + return implode("\n", $messages)."\n"; + } + + /** + * @return array>> + */ + private function decodeJsonLines(string $output): array + { + $output = trim($output); + $responses = []; + + if ('' === $output) { + return $responses; + } + + foreach (preg_split('/\R+/', $output) as $line) { + if ('' === $line) { + continue; + } + + try { + $decoded = json_decode($line, true, 512, \JSON_THROW_ON_ERROR); + } catch (\JsonException) { + continue; + } + + if (!\is_array($decoded)) { + continue; + } + + $id = $decoded['id'] ?? null; + + if (\is_string($id) || \is_int($id)) { + $responses[(string) $id] = $decoded; + } + } + + return $responses; + } + + protected function getSnapshotFilePath(string $method): string + { + $className = substr(static::class, strrpos(static::class, '\\') + 1); + + return __DIR__.'/snapshots/'.$className.'-'.str_replace('/', '_', $method).'.json'; + } + + /** + * @return array + * + * @throws \JsonException + */ + protected function loadSnapshot(string $method): array + { + $path = $this->getSnapshotFilePath($method); + + $contents = file_get_contents($path); + $this->assertNotFalse($contents, 'Failed to read snapshot: '.$path); + + return json_decode($contents, true, 512, \JSON_THROW_ON_ERROR); + } + + /** + * @param array> $response + * + * @throws \JsonException + */ + protected function assertResponseMatchesSnapshot(array $response, string $method): void + { + $this->assertArrayHasKey('result', $response); + $actual = $response['result']; + + $expected = $this->loadSnapshot($method); + + $this->assertEquals($expected, $actual, 'Response payload does not match snapshot '.$this->getSnapshotFilePath($method)); + } + + /** + * @param array> $capabilities + * @param string[] $clientInfo + * + * @throws \JsonException + */ + protected function initializeMessage( + ?string $id = null, + string $protocolVersion = MessageInterface::PROTOCOL_VERSION, + array $capabilities = [], + array $clientInfo = [ + 'name' => 'test-suite', + 'version' => '1.0.0', + ], + ): string { + $id ??= uniqid(); + + return $this->jsonRequest('initialize', [ + 'protocolVersion' => $protocolVersion, + 'capabilities' => $capabilities, + 'clientInfo' => $clientInfo, + ], $id); + } +} diff --git a/tests/Application/ManualStdioExampleTest.php b/tests/Application/ManualStdioExampleTest.php new file mode 100644 index 0000000..72c6408 --- /dev/null +++ b/tests/Application/ManualStdioExampleTest.php @@ -0,0 +1,107 @@ +runServer([ + $this->initializeMessage($initializeId), + ]); + + $this->assertArrayHasKey($initializeId, $responses); + + $initialize = $responses[$initializeId]; + $this->assertResponseMatchesSnapshot($initialize, 'initialize'); + } + + /** + * @throws \JsonException + */ + public function testToolsListMatchesSnapshot(): void + { + $toolsListId = uniqid('t_'); + + $responses = $this->runServer([ + $this->initializeMessage(), + $this->jsonRequest('tools/list', id: $toolsListId), + ]); + + $this->assertArrayHasKey($toolsListId, $responses); + $toolsList = $responses[$toolsListId]; + $this->assertResponseMatchesSnapshot($toolsList, 'tools/list'); + } + + /** + * @throws \JsonException + */ + public function testPromptsListMatchesSnapshot(): void + { + $promptsListId = uniqid('t_'); + + $responses = $this->runServer([ + $this->initializeMessage(), + $this->jsonRequest('prompts/list', id: $promptsListId), + ]); + + $this->assertArrayHasKey($promptsListId, $responses); + $promptsList = $responses[$promptsListId]; + $this->assertResponseMatchesSnapshot($promptsList, 'prompts/list'); + } + + /** + * @throws \JsonException + */ + public function testResourcesListMatchesSnapshot(): void + { + $resourcesListId = uniqid('t_'); + + $responses = $this->runServer([ + $this->initializeMessage(), + $this->jsonRequest('resources/list', id: $resourcesListId), + ]); + + $this->assertArrayHasKey($resourcesListId, $responses); + $resourcesList = $responses[$resourcesListId]; + $this->assertResponseMatchesSnapshot($resourcesList, 'resources/list'); + } + + /** + * @throws \JsonException + */ + public function testResourceTemplatesListMatchesSnapshot(): void + { + $templatesListId = uniqid('t_'); + + $responses = $this->runServer([ + $this->initializeMessage(), + $this->jsonRequest('resources/templates/list', id: $templatesListId), + ]); + + $this->assertArrayHasKey($templatesListId, $responses); + $templatesList = $responses[$templatesListId]; + $this->assertResponseMatchesSnapshot($templatesList, 'resources/templates/list'); + } + + protected function getServerScript(): string + { + return \dirname(__DIR__, 2).'/examples/stdio-explicit-registration/server.php'; + } +} diff --git a/tests/Application/StdioCalculatorExampleTest.php b/tests/Application/StdioCalculatorExampleTest.php new file mode 100644 index 0000000..36973b0 --- /dev/null +++ b/tests/Application/StdioCalculatorExampleTest.php @@ -0,0 +1,92 @@ +runServer([ + $this->initializeMessage(), + $this->jsonRequest('tools/list', id: $toolsListId), + ]); + + $this->assertArrayHasKey($toolsListId, $responses); + $toolsList = $responses[$toolsListId]; + $this->assertResponseMatchesSnapshot($toolsList, 'tools/list'); + } + + /** + * @throws \JsonException + */ + public function testPromptsListMatchesSnapshot(): void + { + $promptsListId = uniqid('t_'); + + $responses = $this->runServer([ + $this->initializeMessage(), + $this->jsonRequest('prompts/list', id: $promptsListId), + ]); + + $this->assertArrayHasKey($promptsListId, $responses); + $promptsList = $responses[$promptsListId]; + $this->assertResponseMatchesSnapshot($promptsList, 'prompts/list'); + } + + /** + * @throws \JsonException + */ + public function testResourcesListMatchesSnapshot(): void + { + $resourcesListId = uniqid('t_'); + + $responses = $this->runServer([ + $this->initializeMessage(), + $this->jsonRequest('resources/list', id: $resourcesListId), + ]); + + $this->assertArrayHasKey($resourcesListId, $responses); + $resourcesList = $responses[$resourcesListId]; + $this->assertResponseMatchesSnapshot($resourcesList, 'resources/list'); + } + + /** + * @throws \JsonException + */ + public function testResourceTemplatesListMatchesSnapshot(): void + { + $templatesListId = uniqid('t_'); + + $responses = $this->runServer([ + $this->initializeMessage(), + $this->jsonRequest('resources/templates/list', id: $templatesListId), + ]); + + $this->assertArrayHasKey($templatesListId, $responses); + $templatesList = $responses[$templatesListId]; + $this->assertResponseMatchesSnapshot($templatesList, 'resources/templates/list'); + } + + protected function getServerScript(): string + { + return \dirname(__DIR__, 2).'/examples/stdio-discovery-calculator/server.php'; + } +} diff --git a/tests/Application/snapshots/ManualStdioExampleTest-initialize.json b/tests/Application/snapshots/ManualStdioExampleTest-initialize.json new file mode 100644 index 0000000..30da842 --- /dev/null +++ b/tests/Application/snapshots/ManualStdioExampleTest-initialize.json @@ -0,0 +1,13 @@ +{ + "capabilities": { + "prompts": {}, + "resources": {}, + "tools": {}, + "completions": {} + }, + "serverInfo": { + "name": "Manual Reg Server", + "version": "1.0.0" + }, + "protocolVersion": "2025-06-18" +} diff --git a/tests/Inspector/snapshots/ManualStdioExampleTest-prompts_list.json b/tests/Application/snapshots/ManualStdioExampleTest-prompts_list.json similarity index 100% rename from tests/Inspector/snapshots/ManualStdioExampleTest-prompts_list.json rename to tests/Application/snapshots/ManualStdioExampleTest-prompts_list.json diff --git a/tests/Inspector/snapshots/ManualStdioExampleTest-resources_list.json b/tests/Application/snapshots/ManualStdioExampleTest-resources_list.json similarity index 100% rename from tests/Inspector/snapshots/ManualStdioExampleTest-resources_list.json rename to tests/Application/snapshots/ManualStdioExampleTest-resources_list.json diff --git a/tests/Inspector/snapshots/ManualStdioExampleTest-resources_templates_list.json b/tests/Application/snapshots/ManualStdioExampleTest-resources_templates_list.json similarity index 100% rename from tests/Inspector/snapshots/ManualStdioExampleTest-resources_templates_list.json rename to tests/Application/snapshots/ManualStdioExampleTest-resources_templates_list.json diff --git a/tests/Inspector/snapshots/ManualStdioExampleTest-tools_list.json b/tests/Application/snapshots/ManualStdioExampleTest-tools_list.json similarity index 100% rename from tests/Inspector/snapshots/ManualStdioExampleTest-tools_list.json rename to tests/Application/snapshots/ManualStdioExampleTest-tools_list.json diff --git a/tests/Inspector/snapshots/StdioCalculatorExampleTest-prompts_list.json b/tests/Application/snapshots/StdioCalculatorExampleTest-prompts_list.json similarity index 100% rename from tests/Inspector/snapshots/StdioCalculatorExampleTest-prompts_list.json rename to tests/Application/snapshots/StdioCalculatorExampleTest-prompts_list.json diff --git a/tests/Inspector/snapshots/StdioCalculatorExampleTest-resources_list.json b/tests/Application/snapshots/StdioCalculatorExampleTest-resources_list.json similarity index 100% rename from tests/Inspector/snapshots/StdioCalculatorExampleTest-resources_list.json rename to tests/Application/snapshots/StdioCalculatorExampleTest-resources_list.json diff --git a/tests/Inspector/snapshots/StdioCalculatorExampleTest-resources_templates_list.json b/tests/Application/snapshots/StdioCalculatorExampleTest-resources_templates_list.json similarity index 100% rename from tests/Inspector/snapshots/StdioCalculatorExampleTest-resources_templates_list.json rename to tests/Application/snapshots/StdioCalculatorExampleTest-resources_templates_list.json diff --git a/tests/Inspector/snapshots/StdioCalculatorExampleTest-tools_list.json b/tests/Application/snapshots/StdioCalculatorExampleTest-tools_list.json similarity index 100% rename from tests/Inspector/snapshots/StdioCalculatorExampleTest-tools_list.json rename to tests/Application/snapshots/StdioCalculatorExampleTest-tools_list.json diff --git a/tests/Inspector/InspectorSnapshotTestCase.php b/tests/Inspector/InspectorSnapshotTestCase.php deleted file mode 100644 index e576795..0000000 --- a/tests/Inspector/InspectorSnapshotTestCase.php +++ /dev/null @@ -1,68 +0,0 @@ -getServerScript(), $method) - )->mustRun(); - - $output = $process->getOutput(); - $snapshotFile = $this->getSnapshotFilePath($method); - - if (!file_exists($snapshotFile)) { - file_put_contents($snapshotFile, $output.\PHP_EOL); - $this->markTestIncomplete("Snapshot created at $snapshotFile, please re-run tests."); - } - - $expected = file_get_contents($snapshotFile); - - $this->assertJsonStringEqualsJsonString($expected, $output); - } - - /** - * List of methods to test. - * - * @return array - */ - abstract public static function provideMethods(): array; - - abstract protected function getServerScript(): string; - - /** - * @return array - */ - protected static function provideListMethods(): array - { - return [ - 'Prompt Listing' => ['method' => 'prompts/list'], - 'Resource Listing' => ['method' => 'resources/list'], - 'Resource Template Listing' => ['method' => 'resources/templates/list'], - 'Tool Listing' => ['method' => 'tools/list'], - ]; - } - - private function getSnapshotFilePath(string $method): string - { - $className = substr(static::class, strrpos(static::class, '\\') + 1); - - return __DIR__.'/snapshots/'.$className.'-'.str_replace('/', '_', $method).'.json'; - } -} diff --git a/tests/Inspector/ManualStdioExampleTest.php b/tests/Inspector/ManualStdioExampleTest.php deleted file mode 100644 index 582de0d..0000000 --- a/tests/Inspector/ManualStdioExampleTest.php +++ /dev/null @@ -1,29 +0,0 @@ -