* [Tests] Introduce client-server integration tests * Add an in-process loopback that connects a real client transport to a real server transport, draining both Fiber loops in one synchronous pass instead of polling. A stalled exchange fails where it happened rather than hanging. * Cover protocol version negotiation across both implementations, where the unit tests only ever drive one side against a canned counter-offer. * Cover the server-to-client round-trips end to end: elicitation, sampling, roots, plus progress and logging notifications. * Add the `integration` test suite and a `make integration-tests` target. * [Tests] Run the integration suite in CI The unit job pins `--testsuite=unit`, so the new suite would never have run on GitHub. It goes in that job rather than its own: the loopback leans on Fibers, and the matrix is what covers them down to the PHP 8.1 floor. * [Tests] Run integration tests against a real server process Replaces the in-process loopback with the setup from examples/client: the client spawns a fixture server over the real StdioTransport, so the tests exercise the shipped transports and no longer nest both sides' Fibers. Moves the suite into its own CI job, matrixed over PHP 8.1-8.5.
83 lines
2.7 KiB
PHP
83 lines
2.7 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\Integration;
|
|
|
|
use Mcp\Client;
|
|
use Mcp\Client\Builder as ClientBuilder;
|
|
use Mcp\Client\Transport\StdioTransport;
|
|
use Mcp\Exception\ConnectionException;
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
/**
|
|
* Base for tests that run a real client against a real server process.
|
|
*
|
|
* Every other test in the suite mocks one side of the conversation. These run
|
|
* both, wired the way `examples/client` wires them, so what they cover is the
|
|
* agreement between the two halves. The servers live in {@see Fixture}, one
|
|
* script per scenario.
|
|
*
|
|
* @author Christopher Hertel <mail@christopher-hertel.de>
|
|
*/
|
|
abstract class IntegrationTestCase extends TestCase
|
|
{
|
|
/**
|
|
* Both sides answer immediately, so anything reaching this is a deadlock.
|
|
* Far below the SDK's two-minute default, to fail rather than hang.
|
|
*/
|
|
private const TIMEOUT = 5;
|
|
|
|
private ?Client $client = null;
|
|
|
|
protected function clientBuilder(): ClientBuilder
|
|
{
|
|
return Client::builder()
|
|
->setClientInfo('integration-client', '1.0.0')
|
|
->setInitTimeout(self::TIMEOUT)
|
|
->setRequestTimeout(self::TIMEOUT);
|
|
}
|
|
|
|
/**
|
|
* Spawn a fixture server and connect a client to it.
|
|
*
|
|
* The returned client has completed the handshake.
|
|
*
|
|
* @param string $fixture basename of a script in {@see Fixture}
|
|
* @param array<string, string> $env added to the server process environment
|
|
*/
|
|
protected function connect(string $fixture, ?ClientBuilder $client = null, array $env = []): Client
|
|
{
|
|
$script = __DIR__.'/Fixture/'.$fixture.'.php';
|
|
|
|
$this->client = ($client ?? $this->clientBuilder())->build();
|
|
|
|
try {
|
|
$this->client->connect(new StdioTransport(
|
|
command: \PHP_BINARY,
|
|
args: [$script],
|
|
// proc_open() replaces the environment rather than adding to it.
|
|
env: [] === $env ? null : array_merge(getenv(), $env),
|
|
));
|
|
} catch (ConnectionException $e) {
|
|
// The transport discards the child's stderr, so a fixture dying on
|
|
// startup arrives here as a bare timeout.
|
|
$this->fail(\sprintf('Could not connect to fixture server "%s": %s. Run `%s %s` to see why.', $fixture, $e->getMessage(), \PHP_BINARY, $script));
|
|
}
|
|
|
|
return $this->client;
|
|
}
|
|
|
|
protected function tearDown(): void
|
|
{
|
|
$this->client?->disconnect();
|
|
$this->client = null;
|
|
}
|
|
}
|