[Docs][Examples][CI] Document and gate the 2026-07-28 lifecycle (#457)
docs/stateless-lifecycle.md walks the revision end to end - per-request _meta, server/discover, multi round-trip requests, caching and subscriptions - and a runnable example on each side shows what that looks like in code. CI matrixes both conformance roles over both revisions. --spec-version is cumulative across the dated revisions, so 2025-11-25 covers the two before it; 2026-07-28 is not cumulative with them and needs its own run against its own baseline. The runner is pinned so a PR only goes red for reasons in the PR, and conformance-weekly tracks the moving target instead - on both revisions, since the draft scenarios ship only on the alpha dist-tag. The Inspector cannot reach a modern-lifecycle server (it opens with initialize), so the example is covered by integration tests instead: one drives it with hand-built HTTP the way a conforming client would, one drives it with this SDK's client, and one drives the single endpoint from both eras.
This commit is contained in:
parent
8c06dcecc6
commit
7fe88677ad
1 changed files with 1300 additions and 6 deletions
115
tests/Integration/DualEraEndpointTest.php
Normal file
115
tests/Integration/DualEraEndpointTest.php
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
<?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\Schema\Enum\ProtocolVersion;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\Attributes\TestDox;
|
||||
|
||||
/**
|
||||
* Two clients, two protocol eras, one URL.
|
||||
*
|
||||
* {@see StatelessClientTest} proves the modern client reaches this example and
|
||||
* the Inspector snapshots prove a handshake-era one reaches the others. This
|
||||
* proves the thing neither of them can on its own: that both reach the *same*
|
||||
* endpoint, with nothing for the client to choose and nothing for the operator
|
||||
* to mount twice.
|
||||
*/
|
||||
final class DualEraEndpointTest extends DualEraExampleTestCase
|
||||
{
|
||||
protected const ANSWERS = ['name' => 'Ada'];
|
||||
|
||||
protected static function server(): string
|
||||
{
|
||||
return __DIR__.'/../../examples/server/stateless-lifecycle/server.php';
|
||||
}
|
||||
|
||||
protected static function portBase(): int
|
||||
{
|
||||
return 9200;
|
||||
}
|
||||
|
||||
#[DataProvider('provideEras')]
|
||||
#[TestDox('a client on $_dataName connects to the one endpoint')]
|
||||
public function testConnects(ProtocolVersion $era): void
|
||||
{
|
||||
$client = $this->connect($era);
|
||||
|
||||
$this->assertTrue($client->isConnected());
|
||||
$this->assertSame($era, $client->getProtocolVersion());
|
||||
|
||||
$client->disconnect();
|
||||
}
|
||||
|
||||
#[DataProvider('provideEras')]
|
||||
#[TestDox('a client on $_dataName sees the same tools')]
|
||||
public function testListsTheSameTools(ProtocolVersion $era): void
|
||||
{
|
||||
$client = $this->connect($era);
|
||||
|
||||
$names = array_map(static fn ($tool): string => $tool->name, $client->listTools()->tools);
|
||||
sort($names);
|
||||
|
||||
// One registry behind both legs, so the catalogue cannot drift.
|
||||
$this->assertSame(['get_weather', 'greet', 'reindex'], $names);
|
||||
|
||||
$client->disconnect();
|
||||
}
|
||||
|
||||
#[DataProvider('provideEras')]
|
||||
#[TestDox('a client on $_dataName gets the same answer from the same tool')]
|
||||
public function testCallsTheSameTool(ProtocolVersion $era): void
|
||||
{
|
||||
$client = $this->connect($era);
|
||||
|
||||
$this->assertSame(
|
||||
'It is 17°C and cloudy in Munich.',
|
||||
self::text($client->callTool('get_weather', ['city' => 'Munich'])),
|
||||
);
|
||||
|
||||
$client->disconnect();
|
||||
}
|
||||
|
||||
#[DataProvider('provideEras')]
|
||||
#[TestDox('a tool that has to ask the user completes on $_dataName')]
|
||||
public function testAsksForInput(ProtocolVersion $era): void
|
||||
{
|
||||
// The one place the eras genuinely differ on the wire: the handshake era
|
||||
// is asked mid-call over its session's stream, the modern era is handed
|
||||
// the question as a result and retries. The example forks on exactly
|
||||
// that; the caller here does not.
|
||||
$client = $this->connect($era, elicitation: true);
|
||||
|
||||
$this->assertSame('Hello, Ada!', self::text($client->callTool('greet', [])));
|
||||
|
||||
$client->disconnect();
|
||||
}
|
||||
|
||||
#[TestDox('both eras are served in turn without restarting anything')]
|
||||
public function testBothErasAgainstOneRunningServer(): void
|
||||
{
|
||||
$handshake = $this->connect(ProtocolVersion::V2025_11_25);
|
||||
$modern = $this->connect(ProtocolVersion::V2026_07_28);
|
||||
|
||||
// Interleaved on purpose: the handshake client's session stays open
|
||||
// while the modern one is served, and neither disturbs the other.
|
||||
$first = self::text($handshake->callTool('get_weather', ['city' => 'Berlin']));
|
||||
$second = self::text($modern->callTool('get_weather', ['city' => 'Berlin']));
|
||||
$third = self::text($handshake->callTool('get_weather', ['city' => 'Berlin']));
|
||||
|
||||
$this->assertSame($first, $second);
|
||||
$this->assertSame($first, $third);
|
||||
|
||||
$handshake->disconnect();
|
||||
$modern->disconnect();
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue